diff --git a/appinfo/info.xml b/appinfo/info.xml index 260cb01f7d6..ce507e1f9ae 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. ]]> - 20.0.0-dev.6 + 20.0.0-dev.7 agpl Daniel Calviño Sánchez diff --git a/appinfo/routes/routesRoomController.php b/appinfo/routes/routesRoomController.php index 6921e128079..4687f1ddea3 100644 --- a/appinfo/routes/routesRoomController.php +++ b/appinfo/routes/routesRoomController.php @@ -107,5 +107,7 @@ ['name' => 'Room#setMessageExpiration', 'url' => '/api/{apiVersion}/room/{token}/message-expiration', 'verb' => 'POST', 'requirements' => $requirementsWithToken], /** @see \OCA\Talk\Controller\RoomController::getCapabilities() */ ['name' => 'Room#getCapabilities', 'url' => '/api/{apiVersion}/room/{token}/capabilities', 'verb' => 'GET', 'requirements' => $requirementsWithToken], + /** @see \OCA\Talk\Controller\RoomController::setMentionPermissions() */ + ['name' => 'Room#setMentionPermissions', 'url' => '/api/{apiVersion}/room/{token}/mention-permissions', 'verb' => 'PUT', 'requirements' => $requirementsWithToken], ], ]; diff --git a/docs/capabilities.md b/docs/capabilities.md index 9b973cfe88f..16807130597 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -152,3 +152,4 @@ ## 20 * `ban-v1` - Whether the API to ban attendees is available +* `mention-permissions` - Whether non-moderators are allowed to mention `@all` diff --git a/docs/constants.md b/docs/constants.md index 5a7520e3328..9b5aaba891c 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -47,6 +47,10 @@ * `0` Stopped (breakout rooms lobbies are enabled) * `1` Started (breakout rooms lobbies are disabled) +### Mention permissions +* `0` Everyone (default) - All participants can mention using `@all` +* `1` Moderators - Only moderators can mention using `@all` + ## Participants ### Participant types diff --git a/docs/conversation.md b/docs/conversation.md index 21eca51b43d..e9888b81725 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -109,6 +109,7 @@ | `callStartTime` | int | v4 | | Timestamp when the call was started (only available with `recording-v1` capability) | | `callRecording` | int | v4 | | Type of call recording (see [Constants - Call recording status](constants.md#call-recording-status)) (only available with `recording-v1` capability) | | `recordingConsent` | int | v4 | | Whether recording consent is required before joining a call (see [constants list](constants.md#recording-consent-required)) (only available with `recording-consent` capability) | +| `mentionPermissions` | int | v4 | | Whether all participants can mention using `@all` or only moderators (see [constants list](constants.md#mention-permissions)) (only available with `mention-permissions` capability) | ## Creating a new conversation @@ -483,6 +484,26 @@ Get all (for moderators and in case of "free selection") or the assigned breakou + `403 Forbidden` When the current user is not a moderator/owner or the conversation is not a public conversation + `404 Not Found` When the conversation could not be found for the participant +## Set mention permissions + +* Required capability: `mention-permissions` +* Method: `PUT` +* Endpoint: `/room/{token}/mention-permissions` +* Data: + +| field | type | Description | +|----------------------|------|-----------------------------------------------------------------------------------------------------------| +| `mentionPermissions` | int | New mention permissions for the conversation (See [mention permssions](constants.md#mention-permissions)) | + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the conversation type does not support setting mention permissions (only group and public conversation) + + `400 Bad Request` When the conversation is a breakout room + + `400 Bad Request` When permissions value is invalid + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant + ## Get conversation capabilities See [Capability handling in federated conversations](https://github.com/nextcloud/spreed/issues/10680) to learn which capabilities diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 904b747e323..e7aca3c0b55 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -100,6 +100,7 @@ class Capabilities implements IPublicCapability { 'federation-v1', 'ban-v1', 'chat-reference-id', + 'mention-permissions', ]; public const LOCAL_FEATURES = [ diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index bfae650db27..1c761d36f9c 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -321,11 +321,14 @@ public function sendMessage( } } + $metadata = []; if ($silent) { - $comment->setMetaData([ - Message::METADATA_SILENT => true, - ]); + $metadata[Message::METADATA_SILENT] = true; + } + if ($chat->getMentionPermissions() === Room::MENTION_PERMISSIONS_EVERYONE || $participant?->hasModeratorPermissions()) { + $metadata[Message::METADATA_CAN_MENTION_ALL] = true; } + $comment->setMetaData($metadata); $event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent, $replyTo); $this->dispatcher->dispatchTyped($event); @@ -360,7 +363,7 @@ public function sendMessage( } } - $alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $alreadyNotifiedUsers, $silent); + $alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $alreadyNotifiedUsers, $silent, $participant); if (!empty($alreadyNotifiedUsers)) { $userIds = array_column($alreadyNotifiedUsers, 'id'); $this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_USERS, $userIds, (int) $comment->getId(), $usersDirectlyMentioned); @@ -947,6 +950,10 @@ public function addConversationNotify(array $results, string $search, Room $room if ($room->getType() === Room::TYPE_ONE_TO_ONE) { return $results; } + if ($room->getMentionPermissions() === Room::MENTION_PERMISSIONS_MODERATORS && !$participant->hasModeratorPermissions()) { + return $results; + } + $attendee = $participant->getAttendee(); if ($attendee->getActorType() === Attendee::ACTOR_USERS) { $roomDisplayName = $room->getDisplayName($attendee->getActorId()); diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index e8c4bab0a3b..f32d2dc97c9 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -53,13 +53,17 @@ public function __construct( * Not every user mentioned in the message is notified, but only those that * are able to participate in the room. * + * @param Room $chat + * @param IComment $comment * @param array[] $alreadyNotifiedUsers * @psalm-param array $alreadyNotifiedUsers + * @param bool $silent + * @param Participant|null $participant * @return string[] Users that were mentioned * @psalm-return array */ - public function notifyMentionedUsers(Room $chat, IComment $comment, array $alreadyNotifiedUsers, bool $silent): array { - $usersToNotify = $this->getUsersToNotify($chat, $comment, $alreadyNotifiedUsers); + public function notifyMentionedUsers(Room $chat, IComment $comment, array $alreadyNotifiedUsers, bool $silent, ?Participant $participant = null): array { + $usersToNotify = $this->getUsersToNotify($chat, $comment, $alreadyNotifiedUsers, $participant); if (!$usersToNotify) { return $alreadyNotifiedUsers; @@ -102,13 +106,14 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea * @param IComment $comment * @param array $alreadyNotifiedUsers * @psalm-param array $alreadyNotifiedUsers + * @param Participant|null $participant * @return array * @psalm-return array */ - public function getUsersToNotify(Room $chat, IComment $comment, array $alreadyNotifiedUsers): array { + public function getUsersToNotify(Room $chat, IComment $comment, array $alreadyNotifiedUsers, ?Participant $participant = null): array { $usersToNotify = $this->getMentionedUsers($comment); $usersToNotify = $this->getMentionedGroupMembers($chat, $comment, $usersToNotify); - $usersToNotify = $this->addMentionAllToList($chat, $usersToNotify); + $usersToNotify = $this->addMentionAllToList($chat, $usersToNotify, $participant); $usersToNotify = $this->removeAlreadyNotifiedUsers($usersToNotify, $alreadyNotifiedUsers); return $usersToNotify; @@ -137,10 +142,11 @@ private function removeAlreadyNotifiedUsers(array $usersToNotify, array $already * @param Room $chat * @param array $list * @psalm-param array $list + * @param Participant|null $participant * @return array * @psalm-return array */ - private function addMentionAllToList(Room $chat, array $list): array { + private function addMentionAllToList(Room $chat, array $list, ?Participant $participant = null): array { $usersToNotify = array_filter($list, static function (array $entry): bool { return $entry['type'] !== Attendee::ACTOR_USERS || $entry['id'] !== 'all'; }); @@ -148,6 +154,9 @@ private function addMentionAllToList(Room $chat, array $list): array { if (count($list) === count($usersToNotify)) { return $usersToNotify; } + if ($chat->getMentionPermissions() === Room::MENTION_PERMISSIONS_MODERATORS && (!$participant instanceof Participant || !$participant->hasModeratorPermissions())) { + return $usersToNotify; + } $attendees = $this->participantService->getActorsByType($chat, Attendee::ACTOR_USERS); foreach ($attendees as $attendee) { diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index d1507456b0b..8b181d57315 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -90,8 +90,13 @@ protected function parseMessage(Message $chatMessage): void { return mb_strlen($m2['id']) <=> mb_strlen($m1['id']); }); + $metadata = $comment->getMetaData() ?? []; foreach ($mentions as $mention) { if ($mention['type'] === 'user' && $mention['id'] === 'all') { + if (!isset($metadata[Message::METADATA_CAN_MENTION_ALL])) { + continue; + } + $mention['type'] = 'call'; } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index fe6316abb3b..2b9d7cefbc7 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1440,6 +1440,28 @@ public function setListable(int $scope): DataResponse { return new DataResponse(); } + /** + * Update the mention permissions for a room + * + * @param 0|1 $mentionPermissions New mention permissions + * @psalm-param Room::MENTION_PERMISSIONS_* $mentionPermissions + * @return DataResponse|DataResponse, array{}> + * + * 200: Permissions updated successfully + * 400: Updating permissions is not possible + */ + #[NoAdminRequired] + #[RequireModeratorParticipant] + public function setMentionPermissions(int $mentionPermissions): DataResponse { + try { + $this->roomService->setMentionPermissions($this->room, $mentionPermissions); + } catch (\InvalidArgumentException) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } + /** * Set a password for a room * diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index 3fcc1bed8b8..d1e92dc723a 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -29,6 +29,7 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public const PROPERTY_RECORDING_CONSENT = 'recordingConsent'; public const PROPERTY_SIP_ENABLED = 'sipEnabled'; public const PROPERTY_TYPE = 'type'; + public const PROPERTY_MENTION_PERMISSIONS = 'mentionPermissions'; /** * @param self::PROPERTY_* $property diff --git a/lib/Manager.php b/lib/Manager.php index dc9029f38c0..514536abc03 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -120,6 +120,7 @@ public function createRoomObjectFromData(array $data): Room { 'call_recording' => 0, 'recording_consent' => 0, 'has_federation' => 0, + 'mention_permissions' => 0, ], $data)); } @@ -189,6 +190,7 @@ public function createRoomObject(array $row): Room { (int) $row['call_recording'], (int) $row['recording_consent'], (int) $row['has_federation'], + (int) $row['mention_permissions'], ); } diff --git a/lib/Migration/Version20000Date20240623123938.php b/lib/Migration/Version20000Date20240623123938.php new file mode 100644 index 00000000000..e9a7e7afc5a --- /dev/null +++ b/lib/Migration/Version20000Date20240623123938.php @@ -0,0 +1,40 @@ +getTable('talk_rooms'); + if (!$table->hasColumn('mention_permissions')) { + $table->addColumn('mention_permissions', Types::INTEGER, [ + 'default' => 0, + 'notnull' => true, + ]); + } + + return $schema; + } +} diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 0beab9513f1..c1407bd2e29 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -23,6 +23,7 @@ class Message { public const METADATA_LAST_EDITED_BY_ID = 'last_edited_by_id'; public const METADATA_LAST_EDITED_TIME = 'last_edited_time'; public const METADATA_SILENT = 'silent'; + public const METADATA_CAN_MENTION_ALL = 'can_mention_all'; /** @var bool */ protected $visible = true; diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 82ef76f0cd0..3dcf3a693ea 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -45,6 +45,7 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ->addSelect($alias . 'call_recording') ->addSelect($alias . 'recording_consent') ->addSelect($alias . 'has_federation') + ->addSelect($alias . 'mention_permissions') ->selectAlias($alias . 'id', 'r_id'); } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 781acf78dc8..c1665b45dd3 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -246,6 +246,7 @@ * listable: int, * lobbyState: int, * lobbyTimer: int, + * mentionPermissions: int, * messageExpiration: int, * name: string, * notificationCalls: int, diff --git a/lib/Room.php b/lib/Room.php index 53321ee403d..bba1c88c6a9 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -80,6 +80,9 @@ class Room { public const HAS_FEDERATION_NONE = 0; public const HAS_FEDERATION_TALKv1 = 1; + public const MENTION_PERMISSIONS_EVERYONE = 0; + public const MENTION_PERMISSIONS_MODERATORS = 1; + protected ?string $currentUser = null; protected ?Participant $participant = null; @@ -124,6 +127,7 @@ public function __construct( private int $callRecording, private int $recordingConsent, private int $hasFederation, + private int $mentionPermissions, ) { } @@ -559,4 +563,12 @@ public function hasFederatedParticipants(): int { public function setFederatedParticipants(int $hasFederation): void { $this->hasFederation = $hasFederation; } + + public function getMentionPermissions(): int { + return $this->mentionPermissions; + } + + public function setMentionPermissions(int $mentionPermissions): void { + $this->mentionPermissions = $mentionPermissions; + } } diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 54630f9e0a4..ccad235d61b 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -139,6 +139,7 @@ public function formatRoomV4( 'breakoutRoomMode' => BreakoutRoom::MODE_NOT_CONFIGURED, 'breakoutRoomStatus' => BreakoutRoom::STATUS_STOPPED, 'recordingConsent' => $this->talkConfig->recordingConsentRequired() === RecordingService::CONSENT_REQUIRED_OPTIONAL ? $room->getRecordingConsent() : $this->talkConfig->recordingConsentRequired(), + 'mentionPermissions' => Room::MENTION_PERMISSIONS_EVERYONE, ]; $lastActivity = $room->getLastActivity(); @@ -217,6 +218,7 @@ public function formatRoomV4( 'messageExpiration' => $room->getMessageExpiration(), 'breakoutRoomMode' => $room->getBreakoutRoomMode(), 'breakoutRoomStatus' => $room->getBreakoutRoomStatus(), + 'mentionPermissions' => $room->getMentionPermissions(), ]); if ($currentParticipant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 144f9bc0ef7..2ad9a904394 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -603,6 +603,44 @@ public function setListable(Room $room, int $newState): bool { return true; } + /** + * @param Room $room + * @param int $newState New mention permissions from self::MENTION_PERMISSIONS_* + * @throws \InvalidArgumentException When the room type, state or breakout rooms where invalid + */ + public function setMentionPermissions(Room $room, int $newState): void { + $oldState = $room->getMentionPermissions(); + if ($newState === $oldState) { + return; + } + + if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true)) { + throw new \InvalidArgumentException('type'); + } + + if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) { + throw new \InvalidArgumentException('breakout-room'); + } + + if (!in_array($newState, [Room::MENTION_PERMISSIONS_EVERYONE, Room::MENTION_PERMISSIONS_MODERATORS], true)) { + throw new \InvalidArgumentException('state'); + } + + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS, $newState, $oldState); + $this->dispatcher->dispatchTyped($event); + + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set('mention_permissions', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT)) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + $room->setMentionPermissions($newState); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS, $newState, $oldState); + $this->dispatcher->dispatchTyped($event); + } + public function setAssignedSignalingServer(Room $room, ?int $signalingServer): bool { $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index b878bb8f9fc..de1b1b96f96 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -533,6 +533,7 @@ "listable", "lobbyState", "lobbyTimer", + "mentionPermissions", "messageExpiration", "name", "notificationCalls", @@ -675,6 +676,10 @@ "type": "integer", "format": "int64" }, + "mentionPermissions": { + "type": "integer", + "format": "int64" + }, "messageExpiration": { "type": "integer", "format": "int64" diff --git a/openapi-federation.json b/openapi-federation.json index 9b7b96c4372..8b689ea9426 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -587,6 +587,7 @@ "listable", "lobbyState", "lobbyTimer", + "mentionPermissions", "messageExpiration", "name", "notificationCalls", @@ -729,6 +730,10 @@ "type": "integer", "format": "int64" }, + "mentionPermissions": { + "type": "integer", + "format": "int64" + }, "messageExpiration": { "type": "integer", "format": "int64" diff --git a/openapi-full.json b/openapi-full.json index 7fe5f42df82..f3c96085ebc 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1097,6 +1097,7 @@ "listable", "lobbyState", "lobbyTimer", + "mentionPermissions", "messageExpiration", "name", "notificationCalls", @@ -1239,6 +1240,10 @@ "type": "integer", "format": "int64" }, + "mentionPermissions": { + "type": "integer", + "format": "int64" + }, "messageExpiration": { "type": "integer", "format": "int64" @@ -14752,6 +14757,130 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mention-permissions": { + "put": { + "operationId": "room-set-mention-permissions", + "summary": "Update the mention permissions for a room", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "mentionPermissions", + "in": "query", + "description": "New mention permissions", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "enum": [ + 0, + 1 + ] + } + }, + { + "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": "Permissions updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "400": { + "description": "Updating permissions is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { "post": { "operationId": "settings-set-user-setting", diff --git a/openapi.json b/openapi.json index 241e53ff097..790fac8963e 100644 --- a/openapi.json +++ b/openapi.json @@ -984,6 +984,7 @@ "listable", "lobbyState", "lobbyTimer", + "mentionPermissions", "messageExpiration", "name", "notificationCalls", @@ -1126,6 +1127,10 @@ "type": "integer", "format": "int64" }, + "mentionPermissions": { + "type": "integer", + "format": "int64" + }, "messageExpiration": { "type": "integer", "format": "int64" @@ -14862,6 +14867,130 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mention-permissions": { + "put": { + "operationId": "room-set-mention-permissions", + "summary": "Update the mention permissions for a room", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "mentionPermissions", + "in": "query", + "description": "New mention permissions", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "enum": [ + 0, + 1 + ] + } + }, + { + "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": "Permissions updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Room" + } + } + } + } + } + } + } + }, + "400": { + "description": "Updating permissions is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { "post": { "operationId": "settings-set-user-setting", diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 782e9b86917..ef9bc0fa792 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -37,6 +37,7 @@ + @@ -113,6 +114,7 @@ import ListableSettings from './ListableSettings.vue' import LobbySettings from './LobbySettings.vue' import LockingSettings from './LockingSettings.vue' import MatterbridgeSettings from './Matterbridge/MatterbridgeSettings.vue' +import MentionsSettings from './MentionsSettings.vue' import NotificationsSettings from './NotificationsSettings.vue' import RecordingConsentSettings from './RecordingConsentSettings.vue' import SipSettings from './SipSettings.vue' @@ -137,6 +139,7 @@ export default { LobbySettings, LockingSettings, MatterbridgeSettings, + MentionsSettings, NcAppSettingsDialog, NcAppSettingsSection, NcCheckboxRadioSwitch, diff --git a/src/components/ConversationSettings/MentionsSettings.vue b/src/components/ConversationSettings/MentionsSettings.vue new file mode 100644 index 00000000000..486bc3386a2 --- /dev/null +++ b/src/components/ConversationSettings/MentionsSettings.vue @@ -0,0 +1,136 @@ + + + + + + + diff --git a/src/constants.js b/src/constants.js index 6d617d5736b..992c77bc3bb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -56,6 +56,11 @@ export const CONVERSATION = { ALL: 2, }, + MENTION_PERMISSIONS: { + EVERYONE: 0, + MODERATORS: 1, + }, + TYPE: { ONE_TO_ONE: 1, GROUP: 2, diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index c2fc37cf0a8..b565f942566 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -306,6 +306,12 @@ const changeListable = async function(token, listable) { }) } +const setMentionPermissions = async function(token, mentionPermissions) { + return axios.put(generateOcsUrl('apps/spreed/api/v4/room/{token}/mention-permissions', { token }), { + mentionPermissions, + }) +} + /** * Set the default permissions for participants in a conversation. * @@ -375,4 +381,5 @@ export { setConversationPermissions, setCallPermissions, setMessageExpiration, + setMentionPermissions, } diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index febdd3db5c9..295ec7d94cf 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -46,6 +46,7 @@ import { setConversationPassword, createPublicConversation, createPrivateConversation, + setMentionPermissions, } from '../services/conversationsService.js' import { clearConversationHistory, @@ -75,6 +76,7 @@ const DUMMY_CONVERSATION = { participantType: PARTICIPANT.TYPE.USER, readOnly: CONVERSATION.STATE.READ_ONLY, listable: CONVERSATION.LISTABLE.NONE, + mentions: CONVERSATION.MENTION_PERMISSIONS.EVERYONE, hasCall: false, canStartCall: false, lobbyState: WEBINAR.LOBBY.NONE, @@ -228,6 +230,10 @@ const mutations = { Vue.set(state.conversations[token], 'callPermissions', permissions) }, + setMentionPermissions(state, { token, mentionPermissions }) { + Vue.set(state.conversations[token], 'mentionPermissions', mentionPermissions) + }, + setCallRecording(state, { token, callRecording }) { Vue.set(state.conversations[token], 'callRecording', callRecording) }, @@ -975,6 +981,15 @@ const actions = { } }, + async setMentionPermissions(context, { token, mentionPermissions }) { + try { + await setMentionPermissions(token, mentionPermissions) + context.commit('setMentionPermissions', { token, mentionPermissions }) + } catch (error) { + console.error('Error while updating mention permissions: ', error) + } + }, + async startCallRecording(context, { token, callRecording }) { try { await startCallRecording(token, callRecording) diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 37500bdd847..60fea67f2de 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -294,6 +294,8 @@ export type components = { /** Format: int64 */ lobbyTimer: number; /** Format: int64 */ + mentionPermissions: number; + /** Format: int64 */ messageExpiration: number; name: string; /** Format: int64 */ diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 10f6282a268..b558303de44 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -337,6 +337,8 @@ export type components = { /** Format: int64 */ lobbyTimer: number; /** Format: int64 */ + mentionPermissions: number; + /** Format: int64 */ messageExpiration: number; name: string; /** Format: int64 */ diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 63fc2217721..b1412694208 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1183,6 +1183,23 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mention-permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the mention permissions for a room */ + put: operations["room-set-mention-permissions"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { parameters: { query?: never; @@ -2087,6 +2104,8 @@ export type components = { /** Format: int64 */ lobbyTimer: number; /** Format: int64 */ + mentionPermissions: number; + /** Format: int64 */ messageExpiration: number; name: string; /** Format: int64 */ @@ -7328,6 +7347,54 @@ export interface operations { }; }; }; + "room-set-mention-permissions": { + parameters: { + query: { + /** @description New mention permissions */ + mentionPermissions: 0 | 1; + }; + 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 Permissions updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Updating permissions is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "settings-set-user-setting": { parameters: { query: { diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 0f7fdedff7c..4f36d9b9ef2 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1185,6 +1185,23 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/mention-permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update the mention permissions for a room */ + put: operations["room-set-mention-permissions"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/settings/user": { parameters: { query?: never; @@ -1572,6 +1589,8 @@ export type components = { /** Format: int64 */ lobbyTimer: number; /** Format: int64 */ + mentionPermissions: number; + /** Format: int64 */ messageExpiration: number; name: string; /** Format: int64 */ @@ -6906,6 +6925,54 @@ export interface operations { }; }; }; + "room-set-mention-permissions": { + parameters: { + query: { + /** @description New mention permissions */ + mentionPermissions: 0 | 1; + }; + 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 Permissions updated successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Room"]; + }; + }; + }; + }; + /** @description Updating permissions is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; "settings-set-user-setting": { parameters: { query: { diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index ea13b2a903a..9cd7513e2fa 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -4627,6 +4627,31 @@ public function sendXMLRequest($verb, $url, $body = null, array $headers = [], a $this->sendRequestFullUrl($verb, $fullUrl, $body, $headers, $options); } + /** + * @When /^user "([^"]*)" sets mention permissions for room "([^"]*)" to (all|moderators) with (\d+) \((v4)\)$/ + * + * @param string $user + * @param string $identifier + * @param string $mentionPermissions + * @param int $statusCode + * @param string $apiVersion + */ + public function userSetsMentionPermissionsOfTheRoom(string $user, string $identifier, string $mentionPermissions, int $statusCode, string $apiVersion): void { + $intMentionPermissions = 0; // all - default + if($mentionPermissions === 'moderators') { + $intMentionPermissions = 1; + } + + $this->setCurrentUser($user); + $this->sendRequest( + 'PUT', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/mention-permissions', + new TableNode([ + ['mentionPermissions', $intMentionPermissions], + ]) + ); + $this->assertStatusCode($this->response, $statusCode); + } + /** * @param string $verb * @param string $url diff --git a/tests/integration/features/chat-1/mentions.feature b/tests/integration/features/chat-1/mentions.feature index efb32e32efd..c3140dddf35 100644 --- a/tests/integration/features/chat-1/mentions.feature +++ b/tests/integration/features/chat-1/mentions.feature @@ -633,3 +633,25 @@ Feature: chat/mentions Then user "participant1" sees the following messages in room "participant1-note-to-self" with 200 | room | actorType | actorId | actorDisplayName | message | messageParameters | | participant1-note-to-self | users | participant1 | participant1-displayname | Test {mention-call1} | "IGNORE" | + + Scenario: get mentions with different mention permissions available + Given user "participant1" creates room "group room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds user "participant2" to room "group room" with 200 (v4) + Then user "participant1" gets the following candidate mentions in room "group room" for "" with 200 + | id | label | source | mentionId | + | all | room | calls | all | + | participant2 | participant2-displayname | users | participant2 | + And user "participant2" gets the following candidate mentions in room "group room" for "" with 200 + | id | label | source | mentionId | + | all | room | calls | all | + | participant1 | participant1-displayname | users | participant1 | + When user "participant1" sets mention permissions for room "group room" to moderators with 200 (v4) + Then user "participant1" gets the following candidate mentions in room "group room" for "" with 200 + | id | label | source | mentionId | + | all | room | calls | all | + | participant2 | participant2-displayname | users | participant2 | + And user "participant2" gets the following candidate mentions in room "group room" for "" with 200 + | id | label | source | mentionId | + | participant1 | participant1-displayname | users | participant1 | diff --git a/tests/integration/features/chat-1/notifications.feature b/tests/integration/features/chat-1/notifications.feature index e74482e1d8c..e29accc663d 100644 --- a/tests/integration/features/chat-1/notifications.feature +++ b/tests/integration/features/chat-1/notifications.feature @@ -544,3 +544,18 @@ Feature: chat/notifications When user "participant1" sets lobby state for room "room" to "non moderators" with 200 (v4) Then user "participant2" has the following notifications | app | object_type | object_id | subject | + + Scenario: At-all with different mention permissions + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + When user "participant2" sends message "Hi @all" to room "room" with 201 + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | + | spreed | chat | room/Hi @all | participant2-displayname mentioned everyone in conversation room | + When user "participant1" reads message "Hi @all" in room "room" with 200 + And user "participant1" sets mention permissions for room "room" to moderators with 200 (v4) + And user "participant2" sends message "Hi @all" to room "room" with 201 + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | diff --git a/tests/php/Chat/ChatManagerTest.php b/tests/php/Chat/ChatManagerTest.php index 05444b0a43b..6160e01b8c9 100644 --- a/tests/php/Chat/ChatManagerTest.php +++ b/tests/php/Chat/ChatManagerTest.php @@ -694,38 +694,56 @@ public static function dataAddConversationNotify(): array { ], [ '', - ['getDisplayName' => 'test'], + ['getDisplayName' => 'test', 'getMentionPermissions' => 0], ['getAttendee' => Attendee::fromRow([ 'actor_type' => Attendee::ACTOR_USERS, 'actor_id' => 'user', ])], [['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all']] ], + [ + '', + ['getMentionPermissions' => 1], + ['hasModeratorPermissions' => false], + [] + ], [ 'all', - ['getDisplayName' => 'test'], + ['getDisplayName' => 'test', 'getMentionPermissions' => 0], ['getAttendee' => Attendee::fromRow([ 'actor_type' => Attendee::ACTOR_USERS, 'actor_id' => 'user', ])], [['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all']] ], + [ + 'all', + ['getDisplayName' => 'test', 'getMentionPermissions' => 1], + [ + 'getAttendee' => Attendee::fromRow([ + 'actor_type' => Attendee::ACTOR_USERS, + 'actor_id' => 'user', + ]), + 'hasModeratorPermissions' => true, + ], + [['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all']] + ], [ 'here', - ['getDisplayName' => 'test'], + ['getDisplayName' => 'test', 'getMentionPermissions' => 0], ['getAttendee' => Attendee::fromRow([ 'actor_type' => Attendee::ACTOR_GUESTS, 'actor_id' => 'guest', ])], [['id' => 'all', 'label' => 'test', 'source' => 'calls', 'mentionId' => 'all']] - ] + ], ]; } /** * @dataProvider dataAddConversationNotify */ - public function testAddConversationNotify(string $search, array$roomMocks, array $participantMocks, array $expected): void { + public function testAddConversationNotify(string $search, array $roomMocks, array $participantMocks, array $expected): void { $room = $this->createMock(Room::class); foreach ($roomMocks as $method => $return) { $room->expects($this->once()) diff --git a/tests/php/Chat/NotifierTest.php b/tests/php/Chat/NotifierTest.php index 72b84d75479..445b56b51c7 100644 --- a/tests/php/Chat/NotifierTest.php +++ b/tests/php/Chat/NotifierTest.php @@ -176,7 +176,8 @@ public function testNotifyMentionedUsers(string $message, array $alreadyNotified $room = $this->getRoom(); $comment = $this->newComment('108', 'users', 'testUser', new \DateTime('@' . 1000000016), $message); $notifier = $this->getNotifier([]); - $actual = $notifier->notifyMentionedUsers($room, $comment, $alreadyNotifiedUsers, false); + $participant = $this->createMock(Participant::class); + $actual = $notifier->notifyMentionedUsers($room, $comment, $alreadyNotifiedUsers, false, $participant); $this->assertEqualsCanonicalizing($expectedReturn, $actual); } @@ -286,6 +287,8 @@ public static function dataAddMentionAllToList(): array { 'not notify' => [ [], [], + 0, + true, [], ], 'preserve notify list and do not notify all' => [ @@ -293,6 +296,8 @@ public static function dataAddMentionAllToList(): array { ['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], ], [], + 0, + true, [ ['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], ], @@ -306,24 +311,48 @@ public static function dataAddMentionAllToList(): array { Attendee::fromRow(['actor_id' => 'user1', 'actor_type' => Attendee::ACTOR_USERS]), Attendee::fromRow(['actor_id' => 'user2', 'actor_type' => Attendee::ACTOR_USERS]), ], + 0, + false, [ ['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], ['id' => 'user2', 'type' => Attendee::ACTOR_USERS, 'reason' => 'all'], ], ], + 'prevent non-moderator to notify all' => [ + [ + ['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], + ['id' => 'all', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], + ], + [ + Attendee::fromRow(['actor_id' => 'user1', 'actor_type' => Attendee::ACTOR_USERS]), + Attendee::fromRow(['actor_id' => 'user2', 'actor_type' => Attendee::ACTOR_USERS]), + ], + 1, + false, + [ + ['id' => 'user1', 'type' => Attendee::ACTOR_USERS, 'reason' => 'direct'], + ], + ], ]; } /** * @dataProvider dataAddMentionAllToList */ - public function testAddMentionAllToList(array $usersToNotify, array $participants, array $return): void { + public function testAddMentionAllToList(array $usersToNotify, array $participants, int $mentionPermissions, bool $moderatorPermissions, array $return): void { $room = $this->createMock(Room::class); + $room->method('getMentionPermissions') + ->willReturn($mentionPermissions); + $this->participantService ->method('getActorsByType') ->willReturn($participants); - $actual = self::invokePrivate($this->getNotifier(), 'addMentionAllToList', [$room, $usersToNotify]); + $participant = $this->createMock(Participant::class); + $participant->method('hasModeratorPermissions') + ->willReturn($moderatorPermissions); + + $actual = self::invokePrivate($this->getNotifier(), 'addMentionAllToList', [$room, $usersToNotify, $participant]); $this->assertCount(count($return), $actual); foreach ($actual as $key => $value) { $this->assertIsArray($value); diff --git a/tests/php/Chat/Parser/UserMentionTest.php b/tests/php/Chat/Parser/UserMentionTest.php index 60e9aac9713..5f9386ce6e0 100644 --- a/tests/php/Chat/Parser/UserMentionTest.php +++ b/tests/php/Chat/Parser/UserMentionTest.php @@ -65,13 +65,18 @@ public function setUp(): void { /** * @param array $mentions + * @param array|null $metadata * @return MockObject|IComment */ - private function newComment(array $mentions): IComment { + private function newComment(array $mentions, ?array $metadata = null): IComment { $comment = $this->createMock(IComment::class); $comment->method('getMentions')->willReturn($mentions); + if ($metadata !== null) { + $comment->method('getMetaData')->willReturn($metadata); + } + return $comment; } @@ -385,7 +390,10 @@ public function testGetRichMessageWithAtAll(): void { $mentions = [ ['type' => 'user', 'id' => 'all'], ]; - $comment = $this->newComment($mentions); + $metadata = [ + Message::METADATA_CAN_MENTION_ALL => true, + ]; + $comment = $this->newComment($mentions, $metadata); /** @var Room&MockObject $room */ $room = $this->createMock(Room::class); diff --git a/tests/php/Service/RoomServiceTest.php b/tests/php/Service/RoomServiceTest.php index b43ecc21a19..7307630cef9 100644 --- a/tests/php/Service/RoomServiceTest.php +++ b/tests/php/Service/RoomServiceTest.php @@ -366,6 +366,7 @@ public function testVerifyPassword(): void { Room::RECORDING_NONE, RecordingService::CONSENT_REQUIRED_NO, Room::HAS_FEDERATION_NONE, + Room::MENTION_PERMISSIONS_EVERYONE, ); $verificationResult = $service->verifyPassword($room, '1234');