feat(calls): Support "Send call notification" for and with federated users

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2024-08-26 14:53:24 +02:00
parent 1a9988be19
commit ece1decade
No known key found for this signature in database
GPG key ID: 74434EFE0D2E2205
13 changed files with 134 additions and 9 deletions

View file

@ -60,6 +60,7 @@
## Send call notification
* Required capability: `send-call-notification`
* Federation capability: `federation-v2`
* Method: `POST`
* Endpoint: `/call/{token}/ring/{attendeeId}`
* Data:

View file

@ -248,11 +248,18 @@ class CallController extends AEnvironmentAwareController {
* 400: Ringing attendee is not possible
* 404: Attendee could not be found
*/
#[FederationSupported]
#[PublicPage]
#[RequireCallEnabled]
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::START_CALL)]
public function ringAttendee(int $attendeeId): DataResponse {
if ($this->room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class);
return $proxy->ringAttendee($this->room, $this->participant, $attendeeId);
}
if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

View file

@ -15,6 +15,7 @@ abstract class AParticipantModifiedEvent extends ARoomEvent {
public const PROPERTY_IN_CALL = 'inCall';
public const PROPERTY_NAME = 'name';
public const PROPERTY_PERMISSIONS = 'permissions';
public const PROPERTY_RESEND_CALL = 'resend_call_notification';
public const PROPERTY_TYPE = 'type';
public const DETAIL_IN_CALL_SILENT = 'silent';

View file

@ -18,13 +18,13 @@ class CallNotificationSendEvent extends ARoomEvent {
public function __construct(
Room $room,
protected Participant $actor,
protected ?Participant $actor,
protected Participant $target,
) {
parent::__construct($room);
}
public function getActor(): Participant {
public function getActor(): ?Participant {
return $this->actor;
}

View file

@ -17,6 +17,7 @@ use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Events\CallNotificationSendEvent;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
@ -324,6 +325,9 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
if ($notification['changedProperty'] === AParticipantModifiedEvent::PROPERTY_PERMISSIONS) {
$this->participantService->updatePermissions($room, $participant, Attendee::PERMISSIONS_MODIFY_SET, $notification['newValue']);
} elseif ($notification['changedProperty'] === AParticipantModifiedEvent::PROPERTY_RESEND_CALL) {
$event = new CallNotificationSendEvent($room, null, $participant);
$this->dispatcher->dispatchTyped($event);
} else {
$this->logger->debug('Update of participant property "' . $notification['changedProperty'] . '" is not handled and should not be send via federation');
}

View file

@ -104,6 +104,36 @@ class CallController {
return new DataResponse([], $statusCode);
}
/**
* @see \OCA\Talk\Controller\RoomController::ringAttendee()
*
* @param int $attendeeId ID of the attendee to ring
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Attendee rang successfully
* 400: Ringing attendee is not possible
* 404: Attendee could not be found
*/
public function ringAttendee(Room $room, Participant $participant, int $attendeeId): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/ring/' . $attendeeId,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse($data, $statusCode);
}
/**
* @see \OCA\Talk\Controller\RoomController::updateFederatedCallFlags()
*

View file

@ -43,6 +43,7 @@ class ParticipantModifiedListener implements IEventListener {
if (!in_array($event->getProperty(), [
AParticipantModifiedEvent::PROPERTY_PERMISSIONS,
AParticipantModifiedEvent::PROPERTY_RESEND_CALL,
], true)) {
return;
}

View file

@ -57,7 +57,7 @@ class Listener implements IEventListener {
public function handle(Event $event): void {
match (get_class($event)) {
CallNotificationSendEvent::class => $this->sendCallNotification($event->getRoom(), $event->getActor()->getAttendee(), $event->getTarget()->getAttendee()),
CallNotificationSendEvent::class => $this->sendCallNotification($event->getRoom(), $event->getActor()?->getAttendee(), $event->getTarget()->getAttendee()),
AttendeesAddedEvent::class => $this->generateInvitation($event->getRoom(), $event->getAttendees()),
UserJoinedRoomEvent::class => $this->handleUserJoinedRoomEvent($event),
BeforeCallStartedEvent::class => $this->checkCallNotifications($event),
@ -335,7 +335,7 @@ class Listener implements IEventListener {
/**
* Forced call notification when ringing a single participant again
*/
protected function sendCallNotification(Room $room, Attendee $actor, Attendee $target): void {
protected function sendCallNotification(Room $room, ?Attendee $actor, Attendee $target): void {
try {
// Remove previous call notifications
$notification = $this->notificationManager->createNotification();
@ -346,7 +346,7 @@ class Listener implements IEventListener {
$dateTime = $this->timeFactory->getDateTime();
$notification->setSubject('call', [
'callee' => $actor->getActorId(),
'callee' => $actor?->getActorId(),
])
->setDateTime($dateTime);
$this->notificationManager->notify($notification);

View file

@ -999,7 +999,7 @@ class Notifier implements INotifier {
$roomName = $room->getDisplayName($notification->getUser());
if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
$parameters = $notification->getSubjectParameters();
$calleeId = $parameters['callee'];
$calleeId = $parameters['callee']; // TODO can be null on federated conversations, so needs to be changed once we have federated 1-1
$userDisplayName = $this->userManager->getDisplayName($calleeId);
if ($userDisplayName !== null) {
if ($this->notificationManager->isPreparingPushNotification() || $this->participantService->hasActiveSessionsInCall($room)) {

View file

@ -1223,6 +1223,12 @@ class ParticipantService {
*/
public function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): void {
$attendee = $this->attendeeMapper->getById($targetAttendeeId);
if ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
$target = new Participant($room, $attendee, null);
$event = new ParticipantModifiedEvent($room, $target, AParticipantModifiedEvent::PROPERTY_RESEND_CALL, 1);
$this->dispatcher->dispatchTyped($event);
return;
}
if ($attendee->getActorType() !== Attendee::ACTOR_USERS) {
throw new \InvalidArgumentException('actor-type');
}

View file

@ -610,7 +610,7 @@ export default {
},
canSendCallNotification() {
return this.isUserActor
return (this.isUserActor || this.isFederatedActor)
&& !this.isSelf
&& (this.currentParticipant.permissions & PARTICIPANT.PERMISSIONS.CALL_START) !== 0
// Can also be undefined, so have to check > than disconnect

View file

@ -144,6 +144,19 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
public function getAttendeeId(string $type, string $id, string $room, ?string $user = null) {
if ($type === 'federated_users') {
if (!str_contains($id, '@')) {
$id .= '@' . $this->localRemoteServerUrl;
} else {
$id = str_replace(
['LOCAL', 'REMOTE'],
[$this->localServerUrl, $this->remoteServerUrl],
$id
);
}
$id = rtrim($id, '/');
}
if (!isset(self::$userToAttendeeId[$room][$type][$id])) {
if ($user !== null) {
$this->userLoadsAttendeeIdsInRoom($user, $room, 'v4');
@ -2114,7 +2127,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
/**
* @Then /^user "([^"]*)" pings (user|guest) "([^"]*)"( attendeeIdPlusOne)? to join call "([^"]*)" with (\d+) \((v4)\)$/
* @Then /^user "([^"]*)" pings (federated_user|user|guest) "([^"]*)"( attendeeIdPlusOne)? to join call "([^"]*)" with (\d+) \((v4)\)$/
*
* @param string $user
* @param string $actorType
@ -2126,7 +2139,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
public function userPingsAttendeeInRoomTo(string $user, string $actorType, string $actorId, ?string $offset, string $identifier, int $statusCode, string $apiVersion): void {
$this->setCurrentUser($user);
$attendeeId = self::$userToAttendeeId[$identifier][$actorType . 's'][$actorId];
$attendeeId = $this->getAttendeeId($actorType . 's', $actorId, $identifier, $user);
if ($offset) {
$attendeeId++;
}

View file

@ -430,3 +430,65 @@ Feature: federation/call
| type | name | recordingConsent |
| 2 | room | 0 |
When user "participant1" leaves call "room" with 200 (v4)
Scenario: Resend call notification for federated user
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" has the following invitations (v1)
| remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
| LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| LOCAL::room | room | 2 | LOCAL | room |
And user "participant2" joins room "LOCAL::room" with 200 (v4)
And using server "LOCAL"
And user "participant1" joins room "room" with 200 (v4)
When user "participant1" joins call "room" with 200 (v4)
Then using server "REMOTE"
And user "participant2" has the following notifications
| app | object_type | object_id | subject |
| spreed | call | LOCAL::room | A group call has started in room |
And user "participant2" joins room "LOCAL::room" with 200 (v4)
When user "participant2" joins call "LOCAL::room" with 200 (v4)
When user "participant2" leaves call "LOCAL::room" with 200 (v4)
And user "participant2" has the following notifications
And using server "LOCAL"
Then user "participant1" loads attendees attendee ids in room "room" (v4)
Then user "participant1" pings federated_user "participant2@REMOTE" to join call "room" with 200 (v4)
Then using server "REMOTE"
And user "participant2" has the following notifications
| app | object_type | object_id | subject |
| spreed | call | LOCAL::room | A group call has started in room |
Scenario: Resend call notification as a federated user
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" has the following invitations (v1)
| remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
| LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| LOCAL::room | room | 2 | LOCAL | room |
And user "participant2" joins room "LOCAL::room" with 200 (v4)
When user "participant2" joins call "LOCAL::room" with 200 (v4)
And using server "LOCAL"
And user "participant1" has the following notifications
| app | object_type | object_id | subject |
| spreed | call | room | A group call has started in room |
And user "participant1" joins room "room" with 200 (v4)
When user "participant1" joins call "room" with 200 (v4)
When user "participant1" leaves call "room" with 200 (v4)
And user "participant1" has the following notifications
Then using server "REMOTE"
Then user "participant2" loads attendees attendee ids in room "LOCAL::room" (v4)
Then user "participant2" pings federated_user "participant1@LOCAL" to join call "LOCAL::room" with 200 (v4)
And using server "LOCAL"
And user "participant1" has the following notifications
| app | object_type | object_id | subject |
| spreed | call | room | A group call has started in room |