Skip to content

Commit

Permalink
Merge pull request #12618 from sanskar-soni-9/feat/prevent-mention-al…
Browse files Browse the repository at this point in the history
…l-for-non-moderators

feat(mentions): prevent mention `@all` for non moderators
  • Loading branch information
nickvergessen committed Jul 23, 2024
2 parents 22e1ff0 + 00cf77d commit 74ff5c2
Show file tree
Hide file tree
Showing 39 changed files with 880 additions and 20 deletions.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
]]></description>

<version>20.0.0-dev.6</version>
<version>20.0.0-dev.7</version>
<licence>agpl</licence>

<author>Daniel Calviño Sánchez</author>
Expand Down
2 changes: 2 additions & 0 deletions appinfo/routes/routesRoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
],
];
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
4 changes: 4 additions & 0 deletions docs/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions docs/conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Capabilities implements IPublicCapability {
'federation-v1',
'ban-v1',
'chat-reference-id',
'mention-permissions',
];

public const LOCAL_FEATURES = [
Expand Down
15 changes: 11 additions & 4 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down
19 changes: 14 additions & 5 deletions lib/Chat/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
* @param bool $silent
* @param Participant|null $participant
* @return string[] Users that were mentioned
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
*/
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;
Expand Down Expand Up @@ -102,13 +106,14 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea
* @param IComment $comment
* @param array $alreadyNotifiedUsers
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}> $alreadyNotifiedUsers
* @param Participant|null $participant
* @return array
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
*/
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;
Expand Down Expand Up @@ -137,17 +142,21 @@ private function removeAlreadyNotifiedUsers(array $usersToNotify, array $already
* @param Room $chat
* @param array $list
* @psalm-param array<int, array{id: string, type: string, reason: string, sourceId?: string}> $list
* @param Participant|null $participant
* @return array
* @psalm-return array<int, array{id: string, type: string, reason: string, sourceId?: string, attendee?: Attendee}>
*/
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';
});

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) {
Expand Down
5 changes: 5 additions & 0 deletions lib/Chat/Parser/UserMention.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

Expand Down
22 changes: 22 additions & 0 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, 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
*
Expand Down
1 change: 1 addition & 0 deletions lib/Events/ARoomModifiedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function createRoomObjectFromData(array $data): Room {
'call_recording' => 0,
'recording_consent' => 0,
'has_federation' => 0,
'mention_permissions' => 0,
], $data));
}

Expand Down Expand Up @@ -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'],
);
}

Expand Down
40 changes: 40 additions & 0 deletions lib/Migration/Version20000Date20240623123938.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version20000Date20240623123938 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$table = $schema->getTable('talk_rooms');
if (!$table->hasColumn('mention_permissions')) {
$table->addColumn('mention_permissions', Types::INTEGER, [
'default' => 0,
'notnull' => true,
]);
}

return $schema;
}
}
1 change: 1 addition & 0 deletions lib/Model/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/Model/SelectHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
* listable: int,
* lobbyState: int,
* lobbyTimer: int,
* mentionPermissions: int,
* messageExpiration: int,
* name: string,
* notificationCalls: int,
Expand Down
12 changes: 12 additions & 0 deletions lib/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -124,6 +127,7 @@ public function __construct(
private int $callRecording,
private int $recordingConsent,
private int $hasFederation,
private int $mentionPermissions,
) {
}

Expand Down Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions lib/Service/RoomFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
38 changes: 38 additions & 0 deletions lib/Service/RoomService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading

0 comments on commit 74ff5c2

Please sign in to comment.