Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mentions): prevent mention @all for non moderators #12618

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
* @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
Loading