summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2022-03-29 17:11:41 +0200
committerJoas Schilling <coding@schilljs.com>2022-05-17 10:13:40 +0200
commitc15f9d831872e6be4a8214d7bac1c0725eacd119 (patch)
treefc32c70fa3c15d618d076f45a3b892b789ed93af
parent43a4e291e278eb3580a54f0d611ee827e0163c23 (diff)
Allow to "Send call notifications"
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r--appinfo/routes/routesCallController.php2
-rw-r--r--docs/call.md22
-rw-r--r--docs/capabilities.md1
-rw-r--r--lib/AppInfo/Application.php2
-rw-r--r--lib/Capabilities.php1
-rw-r--r--lib/Controller/CallController.php24
-rw-r--r--lib/Events/SendCallNotificationEvent.php46
-rw-r--r--lib/Middleware/InjectionMiddleware.php3
-rw-r--r--lib/Notification/Listener.php31
-rw-r--r--lib/Service/ParticipantService.php23
-rw-r--r--src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue39
-rw-r--r--src/services/participantsService.js11
-rw-r--r--src/store/participantsStore.js13
-rw-r--r--tests/php/CapabilitiesTest.php1
14 files changed, 218 insertions, 1 deletions
diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php
index 9ea9f6018..f66a754f8 100644
--- a/appinfo/routes/routesCallController.php
+++ b/appinfo/routes/routesCallController.php
@@ -34,6 +34,8 @@ return [
['name' => 'Call#getPeersForCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\CallController::joinCall() */
['name' => 'Call#joinCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => $requirements],
+ /** @see \OCA\Talk\Controller\CallController::ringAttendee() */
+ ['name' => 'Call#ringAttendee', 'url' => '/api/{apiVersion}/call/{token}/ring/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\CallController::updateCallFlags() */
['name' => 'Call#updateCallFlags', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'PUT', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\CallController::leaveCall() */
diff --git a/docs/call.md b/docs/call.md
index fba81dfe8..c92a64182 100644
--- a/docs/call.md
+++ b/docs/call.md
@@ -47,6 +47,28 @@
+ `404 Not Found` When the user did not join the conversation before
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator
+## Send call notification
+
+* Required capability: `send-call-notification`
+* Method: `POST`
+* Endpoint: `/call/{token}/ring/{attendeeId}`
+* Data:
+
+ field | type | Description
+ ---|---|---
+ `attendeeId` | int | The participant to notify
+
+* Response:
+ - Status code:
+ + `200 OK`
+ + `400 Bad Request` When the target participant is not a user (Guest, group, etc.)
+ + `400 Bad Request` When the target participant is already in the call
+ + `400 Bad Request` When the room has no call in process
+ + `400 Bad Request` When the actor is not in the call
+ + `403 Forbidden` When the current user is not a moderator
+ + `404 Not Found` When the conversation could not be found for the participant
+ + `412 Precondition Failed` When the lobby is active and the user is not a moderator
+
## Update call flags
* Method: `PUT`
diff --git a/docs/capabilities.md b/docs/capabilities.md
index 6208c6412..25084a2e2 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -97,4 +97,5 @@ title: Capabilities
* `chat-permission` - When permission 128 is required to post chat messages, reaction or share items to the conversation
* `silent-send` - Whether the chat API allows to send chat messages without triggering notifications
* `sip-support-nopin` - Whether SIP can be configured to not require a custom attendee PIN
+* `send-call-notification` - When the API allows to resend call notifications for individual users that did not join yet
* `config => call => enabled` - Whether calling is enabled on the instance or not
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 67446c369..3d13c7c66 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -44,6 +44,7 @@ use OCA\Talk\Deck\DeckPluginLoader;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Events\AttendeesRemovedEvent;
use OCA\Talk\Events\RoomEvent;
+use OCA\Talk\Events\SendCallNotificationEvent;
use OCA\Talk\Federation\CloudFederationProviderTalk;
use OCA\Talk\Files\Listener as FilesListener;
use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader;
@@ -129,6 +130,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(RegisterOperationsEvent::class, RegisterOperationsListener::class);
$context->registerEventListener(AttendeesAddedEvent::class, SystemMessageListener::class);
$context->registerEventListener(AttendeesRemovedEvent::class, SystemMessageListener::class);
+ $context->registerEventListener(SendCallNotificationEvent::class, NotificationListener::class);
$context->registerEventListener(CircleDestroyedEvent::class, CircleDeletedListener::class);
$context->registerEventListener(AddingCircleMemberEvent::class, CircleMembershipListener::class);
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 18b4b2362..37f85fee2 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -104,6 +104,7 @@ class Capabilities implements IPublicCapability {
'rich-object-delete',
'chat-permission',
'silent-send',
+ 'send-call-notification',
],
'config' => [
'attachments' => [
diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php
index 6b967a0c8..26797eae5 100644
--- a/lib/Controller/CallController.php
+++ b/lib/Controller/CallController.php
@@ -137,6 +137,30 @@ class CallController extends AEnvironmentAwareController {
* @PublicPage
* @RequireCallEnabled
* @RequireParticipant
+ * @RequirePermissions(permissions=call-start)
+ *
+ * @param int $attendeeId
+ * @return DataResponse
+ */
+ public function ringAttendee(int $attendeeId): DataResponse {
+ if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ if ($this->participant->getSession() && $this->participant->getSession()->getInCall() === Participant::FLAG_DISCONNECTED) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ if (!$this->participantService->sendCallNotificationForAttendee($this->room, $this->participant, $attendeeId)) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ return new DataResponse();
+ }
+
+ /**
+ * @PublicPage
+ * @RequireParticipant
*
* @param int flags
* @return DataResponse
diff --git a/lib/Events/SendCallNotificationEvent.php b/lib/Events/SendCallNotificationEvent.php
new file mode 100644
index 000000000..8f449cb7f
--- /dev/null
+++ b/lib/Events/SendCallNotificationEvent.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Talk\Events;
+
+use OCA\Talk\Participant;
+use OCA\Talk\Room;
+
+class SendCallNotificationEvent extends RoomEvent {
+ protected Participant $actor;
+ protected Participant $target;
+
+ public function __construct(Room $room, Participant $actor, Participant $target) {
+ parent::__construct($room);
+ $this->actor = $actor;
+ $this->target = $target;
+ }
+
+ public function getActor(): Participant {
+ return $this->actor;
+ }
+
+ public function getTarget(): Participant {
+ return $this->target;
+ }
+}
diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php
index 0671aac8c..9d63c59d5 100644
--- a/lib/Middleware/InjectionMiddleware.php
+++ b/lib/Middleware/InjectionMiddleware.php
@@ -209,6 +209,9 @@ class InjectionMiddleware extends Middleware {
if ($textPermission === 'chat' && !($participant->getPermissions() & Attendee::PERMISSIONS_CHAT)) {
throw new PermissionsException();
}
+ if ($textPermission === 'call-start' && !($participant->getPermissions() & Attendee::PERMISSIONS_CALL_START)) {
+ throw new PermissionsException();
+ }
}
}
diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php
index c4da4e2fe..5ca34f052 100644
--- a/lib/Notification/Listener.php
+++ b/lib/Notification/Listener.php
@@ -26,11 +26,14 @@ namespace OCA\Talk\Notification;
use OCA\Talk\Events\AddParticipantsEvent;
use OCA\Talk\Events\JoinRoomUserEvent;
use OCA\Talk\Events\RoomEvent;
+use OCA\Talk\Events\SendCallNotificationEvent;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
use OCP\IDBConnection;
use OCP\Notification\IManager;
use OCP\IUser;
@@ -38,7 +41,7 @@ use OCP\IUserSession;
use OCP\Server;
use Psr\Log\LoggerInterface;
-class Listener {
+class Listener implements IEventListener {
protected IDBConnection $connection;
protected IManager $notificationManager;
protected ParticipantService $participantsService;
@@ -288,4 +291,30 @@ class Listener {
return;
}
}
+
+ public function handle(Event $event): void {
+ if ($event instanceof SendCallNotificationEvent) {
+ $this->sendCallNotification($event->getRoom(), $event->getActor()->getAttendee(), $event->getTarget()->getAttendee());
+ }
+ }
+
+ public function sendCallNotification(Room $room, Attendee $actor, Attendee $target): void {
+ try {
+ // Remove previous call notifications
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp('spreed')
+ ->setObject('call', $room->getToken())
+ ->setUser($target->getActorId());
+ $this->notificationManager->markProcessed($notification);
+
+ $dateTime = $this->timeFactory->getDateTime();
+ $notification->setSubject('call', [
+ 'callee' => $actor->getActorId(),
+ ])
+ ->setDateTime($dateTime);
+ $this->notificationManager->notify($notification);
+ } catch (\InvalidArgumentException $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
}
diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php
index 6e181d71f..274ed2986 100644
--- a/lib/Service/ParticipantService.php
+++ b/lib/Service/ParticipantService.php
@@ -42,6 +42,7 @@ use OCA\Talk\Events\ParticipantEvent;
use OCA\Talk\Events\RemoveParticipantEvent;
use OCA\Talk\Events\RemoveUserEvent;
use OCA\Talk\Events\RoomEvent;
+use OCA\Talk\Events\SendCallNotificationEvent;
use OCA\Talk\Exceptions\ForbiddenException;
use OCA\Talk\Exceptions\InvalidPasswordException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
@@ -990,6 +991,28 @@ class ParticipantService {
}
}
+ public function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): bool {
+ $attendee = $this->attendeeMapper->getById($targetAttendeeId);
+ if ($attendee->getActorType() !== Attendee::ACTOR_USERS) {
+ return false;
+ }
+
+ $sessions = $this->sessionMapper->findByAttendeeId($targetAttendeeId);
+ foreach ($sessions as $session) {
+ if ($session->getInCall() !== Participant::FLAG_DISCONNECTED) {
+ return false;
+ }
+ }
+
+ $this->dispatcher->dispatchTyped(new SendCallNotificationEvent(
+ $room,
+ $currentParticipant,
+ new Participant($room, $attendee, null)
+ ));
+
+ return true;
+ }
+
public function updateCallFlags(Room $room, Participant $participant, int $flags): void {
$session = $participant->getSession();
if (!$session instanceof Session) {
diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
index 8c5fc49c7..8987ec0b3 100644
--- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
@@ -189,6 +189,16 @@
@click="resendInvitation">
{{ t('spreed', 'Resend invitation') }}
</ActionButton>
+ <ActionButton v-if="canSendCallNotification"
+ :close-after-click="true"
+ @click="sendCallNotification">
+ <template #icon>
+ <Bell :size="20"
+ title=""
+ decorative />
+ </template>
+ {{ t('spreed', 'Send call notification') }}
+ </ActionButton>
<ActionSeparator v-if="attendeePin || canBePromoted || canBeDemoted || isEmailActor" />
<ActionButton icon="icon-delete"
:close-after-click="true"
@@ -228,6 +238,7 @@ import AvatarWrapper from '../../../../AvatarWrapper/AvatarWrapper'
import ParticipantPermissionsEditor from './ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue'
// Material design icons
+import Bell from 'vue-material-design-icons/Bell'
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal'
import Microphone from 'vue-material-design-icons/Microphone'
import Phone from 'vue-material-design-icons/Phone'
@@ -253,6 +264,7 @@ export default {
ParticipantPermissionsEditor,
// Material design icons
+ Bell,
DotsHorizontal,
Microphone,
Phone,
@@ -388,6 +400,19 @@ export default {
return this.participant.actorType === ATTENDEE.ACTOR_TYPE.EMAILS
},
+ isUserActor() {
+ return this.participant.actorType === ATTENDEE.ACTOR_TYPE.USERS
+ },
+
+ canSendCallNotification() {
+ return this.isUserActor
+ && !this.isSelf
+ && (this.currentParticipant.permissions & PARTICIPANT.PERMISSIONS.CALL_START) !== 0
+ // Can also be undefined, so have to check > than disconnect
+ && this.currentParticipant.participantFlags > PARTICIPANT.CALL_FLAG.DISCONNECTED
+ && this.participant.inCall === PARTICIPANT.CALL_FLAG.DISCONNECTED
+ },
+
computedName() {
if (!this.isSearched) {
const displayName = this.participant.displayName.trim()
@@ -483,6 +508,7 @@ export default {
currentParticipant() {
return this.$store.getters.conversation(this.token) || {
sessionId: '0',
+ participantFlags: 0,
participantType: this.$store.getters.getUserId() !== null ? PARTICIPANT.TYPE.USER : PARTICIPANT.TYPE.GUEST,
}
},
@@ -649,6 +675,19 @@ export default {
}
},
+ async sendCallNotification() {
+ try {
+ await this.$store.dispatch('sendCallNotification', {
+ token: this.token,
+ attendeeId: this.attendeeId,
+ })
+ showSuccess(t('spreed', 'Notification was sent to {displayName}.', { displayName: this.participant.displayName }))
+ } catch (error) {
+ console.error(error)
+ showError(t('spreed', 'Could not send notification to {displayName}', { displayName: this.participant.displayName }))
+ }
+ },
+
async removeParticipant() {
await this.$store.dispatch('removeParticipant', {
token: this.token,
diff --git a/src/services/participantsService.js b/src/services/participantsService.js
index 4c5e26dfd..098b68e77 100644
--- a/src/services/participantsService.js
+++ b/src/services/participantsService.js
@@ -164,6 +164,16 @@ const resendInvitations = async (token, { attendeeId = null }) => {
}
/**
+ * Sends call notification for the given attendee in the conversation.
+ *
+ * @param {string} token conversation token
+ * @param {number} attendeeId attendee id to target
+ */
+const sendCallNotification = async (token, { attendeeId }) => {
+ await axios.post(generateOcsUrl('apps/spreed/api/v4/call/{token}/ring/{attendeeId}', { token, attendeeId }))
+}
+
+/**
* Grants all permissions to an attendee in a given conversation
*
* @param {string} token conversation token
@@ -222,6 +232,7 @@ export {
fetchParticipants,
setGuestUserName,
resendInvitations,
+ sendCallNotification,
grantAllPermissionsToParticipant,
removeAllPermissi