From fd2dc3fb8b700c60cca3a9c48f624ac6c64df8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 26 Apr 2026 23:06:52 +0200 Subject: [PATCH 1/3] feat: Mute conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- appinfo/info.xml | 2 +- docs/capabilities.md | 1 + lib/Capabilities.php | 1 + lib/Chat/Notifier.php | 8 + lib/Controller/RoomController.php | 51 ++++ .../Version24000Date20260419190830.php | 41 +++ lib/Model/Attendee.php | 9 +- lib/Model/AttendeeMapper.php | 1 + lib/Model/SelectHelper.php | 1 + lib/ResponseDefinitions.php | 2 + lib/Service/ParticipantService.php | 13 + lib/Service/RoomFormatter.php | 2 + openapi-backend-sipbridge.json | 8 +- openapi-federation.json | 8 +- openapi-full.json | 248 +++++++++++++++++- openapi.json | 248 +++++++++++++++++- .../openapi/openapi-backend-sipbridge.ts | 5 + src/types/openapi/openapi-federation.ts | 5 + src/types/openapi/openapi-full.ts | 129 +++++++++ src/types/openapi/openapi.ts | 129 +++++++++ .../features/bootstrap/FeatureContext.php | 26 ++ .../features/conversation-5/mute.feature | 50 ++++ tests/php/Chat/ChatManagerTest.php | 3 + tests/php/Chat/NotifierTest.php | 40 ++- tests/php/Model/AttendeeMapperTest.php | 1 + tests/php/Service/ParticipantServiceTest.php | 1 + 26 files changed, 1014 insertions(+), 19 deletions(-) create mode 100644 lib/Migration/Version24000Date20260419190830.php create mode 100644 tests/integration/features/conversation-5/mute.feature diff --git a/appinfo/info.xml b/appinfo/info.xml index c5034914a5c..ce22dac5020 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ * 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa. ]]> - 24.0.0-dev.2 + 24.0.0-dev.3 agpl Anna Larch diff --git a/docs/capabilities.md b/docs/capabilities.md index 58f2ea9d908..ab17cd608e6 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -226,3 +226,4 @@ * `config => conversations => group-mode` (local) - User selected grouping mode for conversations (`none`, `group-first` or `private-first`) * `private-reply` - Whether clients can link the original message to a private reply in one-to-one conversations * `config => attachments => conversation-subfolders` (local) - Whether per-conversation subfolders are used for Talk attachments; when `true` files must be uploaded to `Talk/-/-/` before calling the attachment endpoint +* `mute-conversations` - Wether conversations can be muted for given time. diff --git a/lib/Capabilities.php b/lib/Capabilities.php index db47841106c..8856d7f2274 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -132,6 +132,7 @@ class Capabilities implements IPublicCapability { 'scheduled-messages', 'conversation-presets', 'private-reply', + 'mute-conversations', ]; public const CONDITIONAL_FEATURES = [ diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index 30f35f3b93c..bfde87d47db 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -692,6 +692,10 @@ protected function shouldMentionedUserBeNotified(string $userId, IComment $comme return self::PRIORITY_NONE; } + if ($attendee->getMuteUntil() >= $this->timeFactory->getDateTime()) { + return self::PRIORITY_NONE; + } + $notificationLevel = $attendee->getNotificationLevel(); $threadId = (int)$comment->getTopmostParentId(); if ($threadId !== 0) { @@ -768,6 +772,10 @@ protected function shouldParticipantBeNotified(Participant $participant, ICommen return self::PRIORITY_NONE; } + if ($participant->getAttendee()->getMuteUntil() >= $this->timeFactory->getDateTime()) { + return self::PRIORITY_NONE; + } + if ($participant->getAttendee()->isImportant()) { return self::PRIORITY_IMPORTANT; } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 1e1e9ca838d..716ffc4268c 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -3238,4 +3238,55 @@ public function scheduleMeeting(string $calendarUri, int $start, ?array $attende return new DataResponse(null, Http::STATUS_OK); } + + /** + * Mute all notifications in a conversation until a specific time. + * Does not alter notification settings for the attendee. + * + * Required capability: `mute-conversations` + * + * @param int $muteUntil Unix timestamp until notifications are muted + * @return DataResponse + * + * 200: Conversation muted + */ + #[NoAdminRequired] + #[FederationSupported] + #[RequireLoggedInParticipant] + #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/mute', requirements: [ + 'apiVersion' => '(v4)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function muteConversation(int $muteUntil): DataResponse { + $muteUntilDateTime = $this->timeFactory->getDateTime('@' . $muteUntil); + $muteUntilDateTime->setTimezone(new \DateTimeZone('UTC')); + + $this->participantService->setMuteUntil($this->participant, $muteUntilDateTime); + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } + + /** + * Unmute all notifications in a conversation, when they were muted before. + * Does not alter notification settings for the attendee. + * + * Required capability: `mute-conversations` + * + * @return DataResponse + * + * 200: Conversation unmuted + */ + #[NoAdminRequired] + #[FederationSupported] + #[RequireLoggedInParticipant] + #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/room/{token}/mute', requirements: [ + 'apiVersion' => '(v4)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function unmuteConversation(): DataResponse { + $muteUntilDateTime = new \DateTime(); + $muteUntilDateTime->setTimestamp(0); + + $this->participantService->setMuteUntil($this->participant, $muteUntilDateTime); + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } } diff --git a/lib/Migration/Version24000Date20260419190830.php b/lib/Migration/Version24000Date20260419190830.php new file mode 100644 index 00000000000..e8283694a68 --- /dev/null +++ b/lib/Migration/Version24000Date20260419190830.php @@ -0,0 +1,41 @@ +getTable('talk_attendees'); + if (!$table->hasColumn('mute_until')) { + $table->addColumn('mute_until', Types::DATETIME, [ + 'notnull' => true, + ]); + } + + return $schema; + } +} diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 75cb0013f72..5822a9a884c 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -74,6 +74,8 @@ * @method int getHiddenPinnedId() * @method void setHasScheduledMessages(int $scheduledMessages) * @method int getHasScheduledMessages() + * @method void setMuteUntil(\DateTime $muteUntil) + * @method \DateTime getMuteUntil() */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; @@ -150,8 +152,13 @@ class Attendee extends Entity { protected bool $hasUnreadThreadDirects = false; protected int $hiddenPinnedId = 0; protected int $hasScheduledMessages = 0; + protected \DateTime $muteUntil; public function __construct() { + $muteUntilDateTime = new \DateTime(); + $muteUntilDateTime->setTimestamp(0); + $this->muteUntil = $muteUntilDateTime; + $this->addType('roomId', Types::BIGINT); $this->addType('actorType', Types::STRING); $this->addType('actorId', Types::STRING); @@ -183,7 +190,7 @@ public function __construct() { $this->addType('hasUnreadThreadDirects', Types::BOOLEAN); $this->addType('hiddenPinnedId', Types::BIGINT); $this->addType('hasScheduledMessages', Types::INTEGER); - + $this->addType('muteUntil', Types::DATETIME); } public function getDisplayName(): string { diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index f1715b06d1e..29621cdaa99 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -316,6 +316,7 @@ public function createAttendeeFromRow(array $row): Attendee { 'has_unread_thread_directs' => (bool)$row['has_unread_thread_directs'], 'hidden_pinned_id' => (int)$row['hidden_pinned_id'], 'has_scheduled_messages' => (int)$row['has_scheduled_messages'], + 'mute_until' => new \DateTime($row['mute_until']), ]); } } diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 6ccc084f84f..9be81cc9a4e 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -114,6 +114,7 @@ public function selectAttendeesTable(IQueryBuilder $query, string $alias = 'a'): $alias . 'has_unread_thread_directs', $alias . 'hidden_pinned_id', $alias . 'has_scheduled_messages', + $alias . 'mute_until', ])->selectAlias($alias . 'id', 'a_id'); } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 6f1a8e00aec..e9c806d7953 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -570,6 +570,8 @@ * hasScheduledMessages: int, * // Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details * attributes: int, + * // Required capability: `mute-conversations` + * muteUntil: int, * } * * @psalm-type TalkDashboardEventAttachment = array{ diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index b0dc6c87f10..5c59096915f 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -382,6 +382,13 @@ public function hidePinnedMessage(Participant $participant, int $messagesId): vo $this->attendeeMapper->update($attendee); } + public function setMuteUntil(Participant $participant, \DateTime $muteUntil): void { + $attendee = $participant->getAttendee(); + $attendee->setMuteUntil($muteUntil); + $attendee->setLastAttendeeActivity($this->timeFactory->getTime()); + $this->attendeeMapper->update($attendee); + } + /** * @param RoomService $roomService * @param Room $room @@ -512,6 +519,7 @@ public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $attendee->setParticipantType(Participant::GUEST); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setLastReadMessage($lastMessage); + $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); if ($displayName !== null && $displayName !== '') { $attendee->setDisplayName($displayName); @@ -667,6 +675,7 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setLastReadMessage($participant['lastReadMessage'] ?? $lastMessage); $attendee->setReadPrivacy($readPrivacy); + $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $attendees[] = $attendee; } @@ -825,6 +834,7 @@ public function addGroup(Room $room, IGroup $group, array &$existingParticipants $attendee->setParticipantType(Participant::USER); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE); + $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $attendeeEvent = new AttendeesAddedEvent($room, [$attendee]); @@ -963,6 +973,7 @@ public function addCircle(Room $room, Circle $circle, array &$existingParticipan $attendee->setParticipantType(Participant::USER); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE); + $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $attendeeEvent = new AttendeesAddedEvent($room, [$attendee]); @@ -1002,6 +1013,7 @@ public function inviteEmailAddress(Room $room, string $actorId, string $email, ? $attendee->setParticipantType(Participant::GUEST); $attendee->setLastReadMessage($lastMessage); + $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); // FIXME handle duplicate invites gracefully @@ -2058,6 +2070,7 @@ public function getParticipantUsersForCallNotifications(Room $room): array { ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->eq('a.notification_calls', $query->createNamedParameter(Participant::NOTIFY_CALLS_ON))) + ->andWhere($query->expr()->lte('a.mute_until', $query->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATETIME_MUTABLE))) ->andWhere($query->expr()->isNull('s.in_call')); if ($room->getLobbyState() !== Webinary::LOBBY_NONE) { diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 2af9f20aa0e..b741dcca9ae 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -160,6 +160,7 @@ public function formatRoomV4( 'isSensitive' => false, 'hasScheduledMessages' => 0, 'attributes' => 0, + 'muteUntil' => 0, ]; if ($room->isFederatedConversation()) { @@ -251,6 +252,7 @@ public function formatRoomV4( 'lastPinnedId' => $room->getLastPinnedId(), 'hiddenPinnedId' => $attendee->getHiddenPinnedId(), 'attributes' => $room->getAttributes(), + 'muteUntil' => max($attendee->getMuteUntil()->getTimestamp(), 0), ]); if ($room->isFederatedConversation()) { diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index c23432e87eb..55b5e5ba8fd 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -941,7 +941,8 @@ "lastPinnedId", "hiddenPinnedId", "hasScheduledMessages", - "attributes" + "attributes", + "muteUntil" ], "properties": { "actorId": { @@ -1255,6 +1256,11 @@ "type": "integer", "format": "int64", "description": "Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details" + }, + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Required capability: `mute-conversations`" } } }, diff --git a/openapi-federation.json b/openapi-federation.json index 1f99fd6b5da..30475f57277 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -1006,7 +1006,8 @@ "lastPinnedId", "hiddenPinnedId", "hasScheduledMessages", - "attributes" + "attributes", + "muteUntil" ], "properties": { "actorId": { @@ -1320,6 +1321,11 @@ "type": "integer", "format": "int64", "description": "Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details" + }, + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Required capability: `mute-conversations`" } } }, diff --git a/openapi-full.json b/openapi-full.json index cd620ec092c..09cfdaf69e7 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1990,7 +1990,8 @@ "lastPinnedId", "hiddenPinnedId", "hasScheduledMessages", - "attributes" + "attributes", + "muteUntil" ], "properties": { "actorId": { @@ -2304,6 +2305,11 @@ "type": "integer", "format": "int64", "description": "Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details" + }, + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Required capability: `mute-conversations`" } } }, @@ -25587,6 +25593,246 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mute": { + "post": { + "operationId": "room-mute-conversation", + "summary": "Mute all notifications in a conversation until a specific time. Does not alter notification settings for the attendee.", + "description": "Required capability: `mute-conversations`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "muteUntil" + ], + "properties": { + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp until notifications are muted" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Conversation muted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "room-unmute-conversation", + "summary": "Unmute all notifications in a conversation, when they were muted before. Does not alter notification settings for the attendee.", + "description": "Required capability: `mute-conversations`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Conversation unmuted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { "post": { "operationId": "settings-set-user-setting", diff --git a/openapi.json b/openapi.json index d80abd78355..6857171fddc 100644 --- a/openapi.json +++ b/openapi.json @@ -1878,7 +1878,8 @@ "lastPinnedId", "hiddenPinnedId", "hasScheduledMessages", - "attributes" + "attributes", + "muteUntil" ], "properties": { "actorId": { @@ -2192,6 +2193,11 @@ "type": "integer", "format": "int64", "description": "Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details" + }, + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Required capability: `mute-conversations`" } } }, @@ -25475,6 +25481,246 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mute": { + "post": { + "operationId": "room-mute-conversation", + "summary": "Mute all notifications in a conversation until a specific time. Does not alter notification settings for the attendee.", + "description": "Required capability: `mute-conversations`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "muteUntil" + ], + "properties": { + "muteUntil": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp until notifications are muted" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Conversation muted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "room-unmute-conversation", + "summary": "Unmute all notifications in a conversation, when they were muted before. Does not alter notification settings for the attendee.", + "description": "Required capability: `mute-conversations`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Conversation unmuted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { "post": { "operationId": "settings-set-user-setting", diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 4fa17d36ce1..50b81cd1119 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -782,6 +782,11 @@ export type components = { * @description Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details */ attributes: number; + /** + * Format: int64 + * @description Required capability: `mute-conversations` + */ + muteUntil: number; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; }; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 7160990adda..6efcd21328d 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -826,6 +826,11 @@ export type components = { * @description Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details */ attributes: number; + /** + * Format: int64 + * @description Required capability: `mute-conversations` + */ + muteUntil: number; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; }; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index d2ba011e53d..46c46f0703a 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1782,6 +1782,30 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mute": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mute all notifications in a conversation until a specific time. Does not alter notification settings for the attendee. + * @description Required capability: `mute-conversations` + */ + post: operations["room-mute-conversation"]; + /** + * Unmute all notifications in a conversation, when they were muted before. Does not alter notification settings for the attendee. + * @description Required capability: `mute-conversations` + */ + delete: operations["room-unmute-conversation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { parameters: { query?: never; @@ -3581,6 +3605,11 @@ export type components = { * @description Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details */ attributes: number; + /** + * Format: int64 + * @description Required capability: `mute-conversations` + */ + muteUntil: number; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; RoomWithInvalidInvitations: components["schemas"]["Room"] & { @@ -12933,6 +12962,106 @@ export interface operations { }; }; }; + "room-mute-conversation": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: int64 + * @description Unix timestamp until notifications are muted + */ + muteUntil: number; + }; + }; + }; + responses: { + /** @description Conversation muted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "room-unmute-conversation": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Conversation unmuted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "settings-set-user-setting": { parameters: { query?: never; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 38444c0a210..b13221012f7 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1782,6 +1782,30 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mute": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mute all notifications in a conversation until a specific time. Does not alter notification settings for the attendee. + * @description Required capability: `mute-conversations` + */ + post: operations["room-mute-conversation"]; + /** + * Unmute all notifications in a conversation, when they were muted before. Does not alter notification settings for the attendee. + * @description Required capability: `mute-conversations` + */ + delete: operations["room-unmute-conversation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { parameters: { query?: never; @@ -3014,6 +3038,11 @@ export type components = { * @description Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details */ attributes: number; + /** + * Format: int64 + * @description Required capability: `mute-conversations` + */ + muteUntil: number; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; RoomWithInvalidInvitations: components["schemas"]["Room"] & { @@ -12366,6 +12395,106 @@ export interface operations { }; }; }; + "room-mute-conversation": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: int64 + * @description Unix timestamp until notifications are muted + */ + muteUntil: number; + }; + }; + }; + responses: { + /** @description Conversation muted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; + "room-unmute-conversation": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Conversation unmuted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "settings-set-user-setting": { parameters: { query?: never; diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index f8e1f212d97..4f2eb79c29d 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -548,6 +548,12 @@ private function assertRooms(array $rooms, TableNode $formData, bool $shouldOrde $data['lobbyTimer'] = 'GREATER_THAN_ZERO'; } } + if (isset($expectedRoom['muteUntil'])) { + $data['muteUntil'] = (int)$room['muteUntil']; + if ($expectedRoom['muteUntil'] === 'GREATER_THAN_ZERO' && $room['muteUntil'] > 0) { + $data['muteUntil'] = 'GREATER_THAN_ZERO'; + } + } if (isset($expectedRoom['breakoutRoomMode'])) { $data['breakoutRoomMode'] = (int)$room['breakoutRoomMode']; } @@ -4975,6 +4981,26 @@ public function userMarksConversationSensitive(string $user, string $identifier, $this->assertStatusCode($this->response, $statusCode); } + #[When('/^user "([^"]*)" mutes room "([^"]*)" until OFFSET\((\d+)\) with (\d+) \((v4)\)$/')] + public function userMutesConversationUntil(string $user, string $identifier, int $muteUntil, int $statusCode, string $apiVersion): void { + $this->setCurrentUser($user); + $this->sendRequest( + 'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/mute', [ + 'muteUntil' => time() + $muteUntil, + ], + ); + $this->assertStatusCode($this->response, $statusCode); + } + + #[When('/^user "([^"]*)" unmutes room "([^"]*)" with (\d+) \((v4)\)$/')] + public function userUnmutesConversationUntil(string $user, string $identifier, int $statusCode, string $apiVersion): void { + $this->setCurrentUser($user); + $this->sendRequest( + 'DELETE', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/mute', + ); + $this->assertStatusCode($this->response, $statusCode); + } + public function sendRequestFullUrl(string $verb, string $fullUrl, TableNode|array|string|null $body = null, array $headers = [], array $options = []): void { $client = new Client(); $options = array_merge($options, ['cookies' => $this->getUserCookieJar($this->currentUser)]); diff --git a/tests/integration/features/conversation-5/mute.feature b/tests/integration/features/conversation-5/mute.feature new file mode 100644 index 00000000000..1bffbf0da2c --- /dev/null +++ b/tests/integration/features/conversation-5/mute.feature @@ -0,0 +1,50 @@ +Feature: conversation-5/mute + Background: + Given user "participant1" exists + Given user "participant2" exists + + Scenario: Mark as (un-)mute + Given user "participant1" creates room "group room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" is participant of the following unordered rooms (v4) + | id | name | muteUntil | + | group room | room | 0 | + And user "participant1" mutes room "group room" until OFFSET(3600) with 200 (v4) + And user "participant1" is participant of the following unordered rooms (v4) + | id | name | muteUntil | + | group room | room | GREATER_THAN_ZERO | + And user "participant1" unmutes room "group room" with 200 (v4) + And user "participant1" is participant of the following unordered rooms (v4) + | id | name | muteUntil | + | group room | room | 0 | + + Scenario: No notifications for muted rooms + When user "participant1" creates room "one-to-one room" (v4) + | roomType | 1 | + | invite | participant2 | + And user "participant2" creates room "one-to-one room" with 200 (v4) + | roomType | 1 | + | invite | participant1 | + And user "participant1" sends message "Message1" to room "one-to-one room" with 201 + And user "participant2" mutes room "one-to-one room" until OFFSET(3600) with 200 (v4) + And user "participant1" sends message "Message with mention for @participant2" to room "one-to-one room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | chat | one-to-one room/Message1 | participant1-displayname sent you a private message | Message1 | + And user "participant2" unmutes room "one-to-one room" with 200 (v4) + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | chat | one-to-one room/Message1 | participant1-displayname sent you a private message | Message1 | + + Scenario: Participant1 calls while participant2 has muted the conversation + When user "participant1" creates room "room" (v4) + | roomType | 3 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + Then user "participant1" is participant of room "room" (v4) + And user "participant2" is participant of room "room" (v4) + And user "participant2" mutes room "room" until OFFSET(3600) with 200 (v4) + Then user "participant1" joins room "room" with 200 (v4) + Then user "participant1" joins call "room" with 200 (v4) + Then user "participant2" has the following notifications diff --git a/tests/php/Chat/ChatManagerTest.php b/tests/php/Chat/ChatManagerTest.php index f931b8c27b2..af541dcbf71 100644 --- a/tests/php/Chat/ChatManagerTest.php +++ b/tests/php/Chat/ChatManagerTest.php @@ -444,6 +444,7 @@ public function testDeleteMessage(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, + 'mute_until' => '@0', ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -514,6 +515,7 @@ public function testDeleteMessageFileShare(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, + 'mute_until' => '@0', ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -606,6 +608,7 @@ public function testDeleteMessageFileShareNotFound(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, + 'mute_until' => '@0', ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php index eb0987342e8..2334953d947 100644 --- a/tests/php/Chat/NotifierTest.php +++ b/tests/php/Chat/NotifierTest.php @@ -177,6 +177,10 @@ public function testNotifyMentionedUsers(string $message, array $alreadyNotified ->method('notify'); } + $current = 1234567; + $this->timeFactory->method('getDateTime') + ->willReturn(new \DateTime('@' . $current)); + $room = $this->getRoom(); $comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message); $notifier = $this->getNotifier([]); @@ -188,24 +192,30 @@ public function testNotifyMentionedUsers(string $message, array $alreadyNotified public static function dataShouldParticipantBeNotified(): array { return [ - [Attendee::ACTOR_GROUPS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NORMAL], - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_USERS]], false, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_FEDERATED_USERS]], false, Notifier::PRIORITY_NORMAL], - [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], false, Notifier::PRIORITY_NORMAL], + [Attendee::ACTOR_GROUPS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], false, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], false, 0, Notifier::PRIORITY_NORMAL], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_USERS]], false, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [['id' => 'test1', 'type' => Attendee::ACTOR_FEDERATED_USERS]], false, 0, Notifier::PRIORITY_NORMAL], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], false, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], false, 0, Notifier::PRIORITY_NORMAL], // Marked as important, still blocked by session and being the author, but otherwise with PRIORITY_IMPORTANT - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], true, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_IMPORTANT], - [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_NONE], - [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], true, Notifier::PRIORITY_IMPORTANT], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], true, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], true, 0, Notifier::PRIORITY_IMPORTANT], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], true, 0, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], true, 0, Notifier::PRIORITY_IMPORTANT], + + // Marked as muted, no notification even with PRIORITY_IMPORTANT + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test1', [], true, 9999999, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', null, Attendee::ACTOR_USERS, 'test2', [], true, 9999999, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT - 5, Attendee::ACTOR_USERS, 'test2', [], true, 9999999, Notifier::PRIORITY_NONE], + [Attendee::ACTOR_USERS, 'test1', Session::SESSION_TIMEOUT + 5, Attendee::ACTOR_USERS, 'test2', [], true, 9999999, Notifier::PRIORITY_NONE], ]; } #[DataProvider('dataShouldParticipantBeNotified')] - public function testShouldParticipantBeNotified(string $actorType, string $actorId, ?int $sessionAge, string $commentActorType, string $commentActorId, array $alreadyNotifiedUsers, bool $isImportant, int $expected): void { + public function testShouldParticipantBeNotified(string $actorType, string $actorId, ?int $sessionAge, string $commentActorType, string $commentActorId, array $alreadyNotifiedUsers, bool $isImportant, int $muteUntil, int $expected): void { $comment = $this->createMock(IComment::class); $comment->method('getActorType') ->willReturn($commentActorType); @@ -217,10 +227,14 @@ public function testShouldParticipantBeNotified(string $actorType, string $actor 'actor_type' => $actorType, 'actor_id' => $actorId, 'important' => $isImportant, + 'mute_until' => '@' . $muteUntil, ]); + $current = 1234567; + $this->timeFactory->method('getDateTime') + ->willReturn(new \DateTime('@' . $current)); + $session = null; if ($sessionAge !== null) { - $current = 1234567; $this->timeFactory->method('getTime') ->willReturn($current); diff --git a/tests/php/Model/AttendeeMapperTest.php b/tests/php/Model/AttendeeMapperTest.php index 54884ace207..fec765002c2 100644 --- a/tests/php/Model/AttendeeMapperTest.php +++ b/tests/php/Model/AttendeeMapperTest.php @@ -356,6 +356,7 @@ public function testModifyPermissions(array $attendees, string $mode, int $permi $attendee->setActorId($attendeeData['actor_id']); $attendee->setParticipantType($attendeeData['participant_type']); $attendee->setPermissions($attendeeData['permissions']); + $attendee->setMuteUntil((new \DateTime)->setTimestamp(0)); $this->attendeeMapper->insert($attendee); } diff --git a/tests/php/Service/ParticipantServiceTest.php b/tests/php/Service/ParticipantServiceTest.php index 10810697341..737234da674 100644 --- a/tests/php/Service/ParticipantServiceTest.php +++ b/tests/php/Service/ParticipantServiceTest.php @@ -113,6 +113,7 @@ public function testGetParticipantsByNotificationLevel(): void { $attendee->setActorId('test'); $attendee->setRoomId(123456789); $attendee->setNotificationLevel(Participant::NOTIFY_MENTION); + $attendee->setMuteUntil((new \DateTime)->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $session1 = new Session(); From 164d7db1d6bbe366e577d842d43e7ce3778f7e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 27 Apr 2026 14:59:56 +0200 Subject: [PATCH 2/3] fixup! feat: Mute conversations --- docs/capabilities.md | 2 +- lib/Chat/Notifier.php | 4 +- lib/Controller/RoomController.php | 15 +++---- .../Version24000Date20260419190830.php | 4 +- lib/Model/Attendee.php | 12 ++---- lib/Model/AttendeeMapper.php | 2 +- lib/ResponseDefinitions.php | 2 +- lib/Service/ParticipantService.php | 9 +--- lib/Service/RoomFormatter.php | 2 +- openapi-backend-sipbridge.json | 2 +- openapi-federation.json | 2 +- openapi-full.json | 43 ++++++++++++++++++- openapi.json | 43 ++++++++++++++++++- .../openapi/openapi-backend-sipbridge.ts | 2 +- src/types/openapi/openapi-federation.ts | 2 +- src/types/openapi/openapi-full.ts | 19 +++++++- src/types/openapi/openapi.ts | 19 +++++++- tests/php/Chat/ChatManagerTest.php | 6 +-- tests/php/Chat/NotifierTest.php | 2 +- tests/php/Model/AttendeeMapperTest.php | 1 - tests/php/Service/ParticipantServiceTest.php | 1 - 21 files changed, 150 insertions(+), 44 deletions(-) diff --git a/docs/capabilities.md b/docs/capabilities.md index ab17cd608e6..6d1a1f0d3e8 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -226,4 +226,4 @@ * `config => conversations => group-mode` (local) - User selected grouping mode for conversations (`none`, `group-first` or `private-first`) * `private-reply` - Whether clients can link the original message to a private reply in one-to-one conversations * `config => attachments => conversation-subfolders` (local) - Whether per-conversation subfolders are used for Talk attachments; when `true` files must be uploaded to `Talk/-/-/` before calling the attachment endpoint -* `mute-conversations` - Wether conversations can be muted for given time. +* `mute-conversations` - Whether conversations can be muted for a given time diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index bfde87d47db..c38104700d3 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -692,7 +692,7 @@ protected function shouldMentionedUserBeNotified(string $userId, IComment $comme return self::PRIORITY_NONE; } - if ($attendee->getMuteUntil() >= $this->timeFactory->getDateTime()) { + if ($attendee->getMuteUntil() >= $this->timeFactory->getDateTime()->getTimestamp()) { return self::PRIORITY_NONE; } @@ -772,7 +772,7 @@ protected function shouldParticipantBeNotified(Participant $participant, ICommen return self::PRIORITY_NONE; } - if ($participant->getAttendee()->getMuteUntil() >= $this->timeFactory->getDateTime()) { + if ($participant->getAttendee()->getMuteUntil() >= $this->timeFactory->getDateTime()->getTimestamp()) { return self::PRIORITY_NONE; } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 716ffc4268c..1e48b4c306b 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -3246,9 +3246,10 @@ public function scheduleMeeting(string $calendarUri, int $start, ?array $attende * Required capability: `mute-conversations` * * @param int $muteUntil Unix timestamp until notifications are muted - * @return DataResponse + * @return DataResponse|DataResponse * * 200: Conversation muted + * 400: Timestamp is in the past */ #[NoAdminRequired] #[FederationSupported] @@ -3258,10 +3259,11 @@ public function scheduleMeeting(string $calendarUri, int $start, ?array $attende 'token' => '[a-z0-9]{4,30}', ])] public function muteConversation(int $muteUntil): DataResponse { - $muteUntilDateTime = $this->timeFactory->getDateTime('@' . $muteUntil); - $muteUntilDateTime->setTimezone(new \DateTimeZone('UTC')); + if ($muteUntil <= $this->timeFactory->getDateTime()->getTimestamp()) { + return new DataResponse(['error' => 'mute-until'], Http::STATUS_BAD_REQUEST); + } - $this->participantService->setMuteUntil($this->participant, $muteUntilDateTime); + $this->participantService->setMuteUntil($this->participant, $muteUntil); return new DataResponse($this->formatRoom($this->room, $this->participant)); } @@ -3283,10 +3285,7 @@ public function muteConversation(int $muteUntil): DataResponse { 'token' => '[a-z0-9]{4,30}', ])] public function unmuteConversation(): DataResponse { - $muteUntilDateTime = new \DateTime(); - $muteUntilDateTime->setTimestamp(0); - - $this->participantService->setMuteUntil($this->participant, $muteUntilDateTime); + $this->participantService->setMuteUntil($this->participant, 0); return new DataResponse($this->formatRoom($this->room, $this->participant)); } } diff --git a/lib/Migration/Version24000Date20260419190830.php b/lib/Migration/Version24000Date20260419190830.php index e8283694a68..446bf3885e9 100644 --- a/lib/Migration/Version24000Date20260419190830.php +++ b/lib/Migration/Version24000Date20260419190830.php @@ -31,8 +31,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table = $schema->getTable('talk_attendees'); if (!$table->hasColumn('mute_until')) { - $table->addColumn('mute_until', Types::DATETIME, [ + $table->addColumn('mute_until', Types::INTEGER, [ 'notnull' => true, + 'length' => 11, + 'default' => 0, ]); } diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 5822a9a884c..a3637b66c53 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -74,8 +74,8 @@ * @method int getHiddenPinnedId() * @method void setHasScheduledMessages(int $scheduledMessages) * @method int getHasScheduledMessages() - * @method void setMuteUntil(\DateTime $muteUntil) - * @method \DateTime getMuteUntil() + * @method void setMuteUntil(int $muteUntil) + * @method int getMuteUntil() */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; @@ -152,13 +152,9 @@ class Attendee extends Entity { protected bool $hasUnreadThreadDirects = false; protected int $hiddenPinnedId = 0; protected int $hasScheduledMessages = 0; - protected \DateTime $muteUntil; + protected int $muteUntil = 0; public function __construct() { - $muteUntilDateTime = new \DateTime(); - $muteUntilDateTime->setTimestamp(0); - $this->muteUntil = $muteUntilDateTime; - $this->addType('roomId', Types::BIGINT); $this->addType('actorType', Types::STRING); $this->addType('actorId', Types::STRING); @@ -190,7 +186,7 @@ public function __construct() { $this->addType('hasUnreadThreadDirects', Types::BOOLEAN); $this->addType('hiddenPinnedId', Types::BIGINT); $this->addType('hasScheduledMessages', Types::INTEGER); - $this->addType('muteUntil', Types::DATETIME); + $this->addType('muteUntil', Types::INTEGER); } public function getDisplayName(): string { diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 29621cdaa99..fd6aebaa1c0 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -316,7 +316,7 @@ public function createAttendeeFromRow(array $row): Attendee { 'has_unread_thread_directs' => (bool)$row['has_unread_thread_directs'], 'hidden_pinned_id' => (int)$row['hidden_pinned_id'], 'has_scheduled_messages' => (int)$row['has_scheduled_messages'], - 'mute_until' => new \DateTime($row['mute_until']), + 'mute_until' => (int)$row['mute_until'], ]); } } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index e9c806d7953..041f380071c 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -570,7 +570,7 @@ * hasScheduledMessages: int, * // Bit-flag of enabled attributes of this conversation (only available with capability: `conversation-attributes`). See [attributes list](https://nextcloud-talk.readthedocs.io/en/latest/constants/#conversation-attributes) for details * attributes: int, - * // Required capability: `mute-conversations` + * // Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications * muteUntil: int, * } * diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 5c59096915f..006d230e767 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -382,7 +382,7 @@ public function hidePinnedMessage(Participant $participant, int $messagesId): vo $this->attendeeMapper->update($attendee); } - public function setMuteUntil(Participant $participant, \DateTime $muteUntil): void { + public function setMuteUntil(Participant $participant, int $muteUntil): void { $attendee = $participant->getAttendee(); $attendee->setMuteUntil($muteUntil); $attendee->setLastAttendeeActivity($this->timeFactory->getTime()); @@ -519,7 +519,6 @@ public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $attendee->setParticipantType(Participant::GUEST); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setLastReadMessage($lastMessage); - $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); if ($displayName !== null && $displayName !== '') { $attendee->setDisplayName($displayName); @@ -675,7 +674,6 @@ public function addUsers(Room $room, array $participants, ?IUser $addedBy = null $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setLastReadMessage($participant['lastReadMessage'] ?? $lastMessage); $attendee->setReadPrivacy($readPrivacy); - $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $attendees[] = $attendee; } @@ -834,7 +832,6 @@ public function addGroup(Room $room, IGroup $group, array &$existingParticipants $attendee->setParticipantType(Participant::USER); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE); - $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $attendeeEvent = new AttendeesAddedEvent($room, [$attendee]); @@ -973,7 +970,6 @@ public function addCircle(Room $room, Circle $circle, array &$existingParticipan $attendee->setParticipantType(Participant::USER); $attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT); $attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE); - $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $attendeeEvent = new AttendeesAddedEvent($room, [$attendee]); @@ -1013,7 +1009,6 @@ public function inviteEmailAddress(Room $room, string $actorId, string $email, ? $attendee->setParticipantType(Participant::GUEST); $attendee->setLastReadMessage($lastMessage); - $attendee->setMuteUntil((new \DateTime())->setTimestamp(0)); $this->attendeeMapper->insert($attendee); // FIXME handle duplicate invites gracefully @@ -2070,7 +2065,7 @@ public function getParticipantUsersForCallNotifications(Room $room): array { ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->eq('a.notification_calls', $query->createNamedParameter(Participant::NOTIFY_CALLS_ON))) - ->andWhere($query->expr()->lte('a.mute_until', $query->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATETIME_MUTABLE))) + ->andWhere($query->expr()->lte('a.mute_until', $query->createNamedParameter($this->timeFactory->getDateTime()->getTimestamp(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->isNull('s.in_call')); if ($room->getLobbyState() !== Webinary::LOBBY_NONE) { diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index b741dcca9ae..92f875d7260 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -252,7 +252,7 @@ public function formatRoomV4( 'lastPinnedId' => $room->getLastPinnedId(), 'hiddenPinnedId' => $attendee->getHiddenPinnedId(), 'attributes' => $room->getAttributes(), - 'muteUntil' => max($attendee->getMuteUntil()->getTimestamp(), 0), + 'muteUntil' => $attendee->getMuteUntil(), ]); if ($room->isFederatedConversation()) { diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index 55b5e5ba8fd..50628184c0c 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -1260,7 +1260,7 @@ "muteUntil": { "type": "integer", "format": "int64", - "description": "Required capability: `mute-conversations`" + "description": "Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications" } } }, diff --git a/openapi-federation.json b/openapi-federation.json index 30475f57277..377569a4136 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -1325,7 +1325,7 @@ "muteUntil": { "type": "integer", "format": "int64", - "description": "Required capability: `mute-conversations`" + "description": "Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications" } } }, diff --git a/openapi-full.json b/openapi-full.json index 09cfdaf69e7..4f6e233b8f6 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -2309,7 +2309,7 @@ "muteUntil": { "type": "integer", "format": "int64", - "description": "Required capability: `mute-conversations`" + "description": "Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications" } } }, @@ -25693,6 +25693,47 @@ } } }, + "400": { + "description": "Timestamp is in the past", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "mute-until" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { diff --git a/openapi.json b/openapi.json index 6857171fddc..e8898b71b0f 100644 --- a/openapi.json +++ b/openapi.json @@ -2197,7 +2197,7 @@ "muteUntil": { "type": "integer", "format": "int64", - "description": "Required capability: `mute-conversations`" + "description": "Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications" } } }, @@ -25581,6 +25581,47 @@ } } }, + "400": { + "description": "Timestamp is in the past", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "mute-until" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 50b81cd1119..6f203a17002 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -784,7 +784,7 @@ export type components = { attributes: number; /** * Format: int64 - * @description Required capability: `mute-conversations` + * @description Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications */ muteUntil: number; }; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 6efcd21328d..ed6246bae83 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -828,7 +828,7 @@ export type components = { attributes: number; /** * Format: int64 - * @description Required capability: `mute-conversations` + * @description Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications */ muteUntil: number; }; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 46c46f0703a..359f4fd8106 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -3607,7 +3607,7 @@ export type components = { attributes: number; /** * Format: int64 - * @description Required capability: `mute-conversations` + * @description Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications */ muteUntil: number; }; @@ -13001,6 +13001,23 @@ export interface operations { }; }; }; + /** @description Timestamp is in the past */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "mute-until"; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ 401: { headers: { diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index b13221012f7..16324f78962 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -3040,7 +3040,7 @@ export type components = { attributes: number; /** * Format: int64 - * @description Required capability: `mute-conversations` + * @description Required capability: `mute-conversations`. Timestamp until the conversation is muted, i.e. not receiving notifications */ muteUntil: number; }; @@ -12434,6 +12434,23 @@ export interface operations { }; }; }; + /** @description Timestamp is in the past */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "mute-until"; + }; + }; + }; + }; + }; /** @description Current user is not logged in */ 401: { headers: { diff --git a/tests/php/Chat/ChatManagerTest.php b/tests/php/Chat/ChatManagerTest.php index af541dcbf71..2a9ef1680c3 100644 --- a/tests/php/Chat/ChatManagerTest.php +++ b/tests/php/Chat/ChatManagerTest.php @@ -444,7 +444,7 @@ public function testDeleteMessage(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, - 'mute_until' => '@0', + 'mute_until' => 0, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -515,7 +515,7 @@ public function testDeleteMessageFileShare(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, - 'mute_until' => '@0', + 'mute_until' => 0, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -608,7 +608,7 @@ public function testDeleteMessageFileShareNotFound(): void { 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, 'has_scheduled_messages' => 0, - 'mute_until' => '@0', + 'mute_until' => 0, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php index 2334953d947..0252e8deb36 100644 --- a/tests/php/Chat/NotifierTest.php +++ b/tests/php/Chat/NotifierTest.php @@ -227,7 +227,7 @@ public function testShouldParticipantBeNotified(string $actorType, string $actor 'actor_type' => $actorType, 'actor_id' => $actorId, 'important' => $isImportant, - 'mute_until' => '@' . $muteUntil, + 'mute_until' => $muteUntil, ]); $current = 1234567; $this->timeFactory->method('getDateTime') diff --git a/tests/php/Model/AttendeeMapperTest.php b/tests/php/Model/AttendeeMapperTest.php index fec765002c2..54884ace207 100644 --- a/tests/php/Model/AttendeeMapperTest.php +++ b/tests/php/Model/AttendeeMapperTest.php @@ -356,7 +356,6 @@ public function testModifyPermissions(array $attendees, string $mode, int $permi $attendee->setActorId($attendeeData['actor_id']); $attendee->setParticipantType($attendeeData['participant_type']); $attendee->setPermissions($attendeeData['permissions']); - $attendee->setMuteUntil((new \DateTime)->setTimestamp(0)); $this->attendeeMapper->insert($attendee); } diff --git a/tests/php/Service/ParticipantServiceTest.php b/tests/php/Service/ParticipantServiceTest.php index 737234da674..10810697341 100644 --- a/tests/php/Service/ParticipantServiceTest.php +++ b/tests/php/Service/ParticipantServiceTest.php @@ -113,7 +113,6 @@ public function testGetParticipantsByNotificationLevel(): void { $attendee->setActorId('test'); $attendee->setRoomId(123456789); $attendee->setNotificationLevel(Participant::NOTIFY_MENTION); - $attendee->setMuteUntil((new \DateTime)->setTimestamp(0)); $this->attendeeMapper->insert($attendee); $session1 = new Session(); From 95d512c0605d752168fc63eef24957173bf5f48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 27 Apr 2026 15:38:51 +0200 Subject: [PATCH 3/3] fixup! fixup! feat: Mute conversations --- lib/Chat/Notifier.php | 4 ++-- lib/Controller/RoomController.php | 2 +- lib/Migration/Version24000Date20260419190830.php | 3 +-- lib/Model/Attendee.php | 2 +- lib/Service/ParticipantService.php | 2 +- tests/php/Chat/NotifierTest.php | 11 ++++------- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index c38104700d3..a1fb24952a8 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -692,7 +692,7 @@ protected function shouldMentionedUserBeNotified(string $userId, IComment $comme return self::PRIORITY_NONE; } - if ($attendee->getMuteUntil() >= $this->timeFactory->getDateTime()->getTimestamp()) { + if ($attendee->getMuteUntil() >= $this->timeFactory->getTime()) { return self::PRIORITY_NONE; } @@ -772,7 +772,7 @@ protected function shouldParticipantBeNotified(Participant $participant, ICommen return self::PRIORITY_NONE; } - if ($participant->getAttendee()->getMuteUntil() >= $this->timeFactory->getDateTime()->getTimestamp()) { + if ($participant->getAttendee()->getMuteUntil() >= $this->timeFactory->getTime()) { return self::PRIORITY_NONE; } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 1e48b4c306b..3c32552239f 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -3259,7 +3259,7 @@ public function scheduleMeeting(string $calendarUri, int $start, ?array $attende 'token' => '[a-z0-9]{4,30}', ])] public function muteConversation(int $muteUntil): DataResponse { - if ($muteUntil <= $this->timeFactory->getDateTime()->getTimestamp()) { + if ($muteUntil <= $this->timeFactory->getTime()) { return new DataResponse(['error' => 'mute-until'], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Migration/Version24000Date20260419190830.php b/lib/Migration/Version24000Date20260419190830.php index 446bf3885e9..2feae38211a 100644 --- a/lib/Migration/Version24000Date20260419190830.php +++ b/lib/Migration/Version24000Date20260419190830.php @@ -31,9 +31,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table = $schema->getTable('talk_attendees'); if (!$table->hasColumn('mute_until')) { - $table->addColumn('mute_until', Types::INTEGER, [ + $table->addColumn('mute_until', Types::BIGINT, [ 'notnull' => true, - 'length' => 11, 'default' => 0, ]); } diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index a3637b66c53..c56c8185efb 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -186,7 +186,7 @@ public function __construct() { $this->addType('hasUnreadThreadDirects', Types::BOOLEAN); $this->addType('hiddenPinnedId', Types::BIGINT); $this->addType('hasScheduledMessages', Types::INTEGER); - $this->addType('muteUntil', Types::INTEGER); + $this->addType('muteUntil', Types::BIGINT); } public function getDisplayName(): string { diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 006d230e767..dfd03443b39 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -2065,7 +2065,7 @@ public function getParticipantUsersForCallNotifications(Room $room): array { ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->eq('a.notification_calls', $query->createNamedParameter(Participant::NOTIFY_CALLS_ON))) - ->andWhere($query->expr()->lte('a.mute_until', $query->createNamedParameter($this->timeFactory->getDateTime()->getTimestamp(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->lte('a.mute_until', $query->createNamedParameter($this->timeFactory->getTime(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->isNull('s.in_call')); if ($room->getLobbyState() !== Webinary::LOBBY_NONE) { diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php index 0252e8deb36..6d7e079ddaf 100644 --- a/tests/php/Chat/NotifierTest.php +++ b/tests/php/Chat/NotifierTest.php @@ -178,8 +178,8 @@ public function testNotifyMentionedUsers(string $message, array $alreadyNotified } $current = 1234567; - $this->timeFactory->method('getDateTime') - ->willReturn(new \DateTime('@' . $current)); + $this->timeFactory->method('getTime') + ->willReturn($current); $room = $this->getRoom(); $comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message); @@ -230,14 +230,11 @@ public function testShouldParticipantBeNotified(string $actorType, string $actor 'mute_until' => $muteUntil, ]); $current = 1234567; - $this->timeFactory->method('getDateTime') - ->willReturn(new \DateTime('@' . $current)); + $this->timeFactory->method('getTime') + ->willReturn($current); $session = null; if ($sessionAge !== null) { - $this->timeFactory->method('getTime') - ->willReturn($current); - $session = Session::fromRow([ 'last_ping' => $current - $sessionAge, ]);