diff --git a/appinfo/routes.php b/appinfo/routes.php
index db160f4d3a5..722d0ed2af9 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -33,6 +33,11 @@
'url' => '/not-found',
'verb' => 'GET',
],
+ [
+ 'name' => 'Page#duplicateSession',
+ 'url' => '/duplicate-session',
+ 'verb' => 'GET',
+ ],
[
'name' => 'Page#showCall',
diff --git a/docs/participant.md b/docs/participant.md
index 3482581f498..bb72dd0e42c 100644
--- a/docs/participant.md
+++ b/docs/participant.md
@@ -109,18 +109,28 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
field | type | Description
------|------|------------
`password` | string | Optional: Password is only required for users which are of type `4` or `5` and only when the conversation has `hasPassword` set to true.
+ `force` | bool | If set to `false` and the user has an active session already a `409 Conflict` will be returned (Default: true - to keep the old behaviour)
* Response:
- Status code:
+ `200 OK`
+ `403 Forbidden` When the password is required and didn't match
+ `404 Not Found` When the conversation could not be found for the participant
+ + `409 Conflict` When the user already has an active session in the conversation. The suggested behaviour is to ask the user whether they want to kill the old session and force join unless the last ping is older than 60 seconds or older than 40 seconds when the conflicting session is not marked as in a call.
- - Data:
+ - Data in case of `200 OK`:
+
+ field | type | Description
+ ------|------|------------
+ `sessionId` | string | 512 character long string
+
+ - Data in case of `409 Conflict`:
field | type | Description
------|------|------------
`sessionId` | string | 512 character long string
+ `inCall` | int | Flags whether the conflicting session is in a potential call
+ `lastPing` | int | Timestamp of the last ping of the conflicting session
## Leave a conversation (not available for call and chat anymore)
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index b75843c5543..d3246e1d06e 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -152,7 +152,17 @@ public function authenticatePassword(string $token, string $password = ''): Resp
* @return Response
*/
public function notFound(): Response {
- return new RedirectResponse($this->url->linkToRouteAbsolute('spreed.Page.index'));
+ return $this->index();
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ *
+ * @return Response
+ */
+ public function duplicateSession(): Response {
+ return $this->index();
}
/**
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index ae071a0287a..8e9ee6fc5c1 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -1159,15 +1159,51 @@ public function setPassword(string $password): DataResponse {
*
* @param string $token
* @param string $password
+ * @param bool $force
* @return DataResponse
*/
- public function joinRoom(string $token, string $password = ''): DataResponse {
+ public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse {
try {
$room = $this->manager->getRoomForParticipantByToken($token, $this->userId);
} catch (RoomNotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
+ $previousSession = null;
+ if ($this->userId !== null) {
+ try {
+ $previousSession = $room->getParticipant($this->userId);
+ } catch (ParticipantNotFoundException $e) {
+ }
+ } else {
+ $session = $this->session->getSessionForRoom($token);
+ try {
+ $previousSession = $room->getParticipantBySession($session);
+ } catch (ParticipantNotFoundException $e) {
+ }
+ }
+
+ if ($previousSession instanceof Participant && $previousSession->getSessionId() !== '0') {
+ // Previous session was active
+ if ($force === false) {
+ return new DataResponse([
+ 'sessionId' => $previousSession->getSessionId(),
+ 'inCall' => $previousSession->getInCallFlags(),
+ 'lastPing' => $previousSession->getLastPing(),
+ ], Http::STATUS_CONFLICT);
+ }
+
+ if ($previousSession->getInCallFlags() !== Participant::FLAG_DISCONNECTED) {
+ $room->changeInCall($previousSession, Participant::FLAG_DISCONNECTED);
+ }
+
+ if ($this->userId === null) {
+ $room->removeParticipantBySession($previousSession, Room::PARTICIPANT_LEFT);
+ } else {
+ $room->leaveRoomAsParticipant($previousSession);
+ }
+ }
+
$user = $this->userManager->get($this->userId);
try {
$result = $room->verifyPassword((string) $this->session->getPasswordForRoom($token));
@@ -1209,7 +1245,7 @@ public function leaveRoom(string $token): DataResponse {
$room->removeParticipantBySession($participant, Room::PARTICIPANT_LEFT);
} else {
$participant = $room->getParticipant($this->userId);
- $room->leaveRoom($participant->getUser());
+ $room->leaveRoomAsParticipant($participant);
}
} catch (RoomNotFoundException $e) {
} catch (ParticipantNotFoundException $e) {
diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index 4070f72973b..6c3dc06cdad 100644
--- a/lib/Controller/SignalingController.php
+++ b/lib/Controller/SignalingController.php
@@ -313,7 +313,25 @@ public function pullMessages(string $token): DataResponse {
$data[] = ['type' => 'usersInRoom', 'data' => $this->getUsersInRoom($room, $pingTimestamp)];
} catch (RoomNotFoundException $e) {
$data[] = ['type' => 'usersInRoom', 'data' => []];
- return new DataResponse($data, Http::STATUS_NOT_FOUND);
+
+ // Was the session killed or the complete conversation?
+ try {
+ $room = $this->manager->getRoomForParticipantByToken($token, $this->userId);
+ if ($this->userId) {
+ // For logged in users we check if they are still part of the public conversation,
+ // if not they were removed instead of having a conflict.
+ $room->getParticipant($this->userId);
+ }
+
+ // Session was killed, make the UI redirect to an error
+ return new DataResponse($data, Http::STATUS_CONFLICT);
+ } catch (ParticipantNotFoundException $e) {
+ // User removed from conversation, bye!
+ return new DataResponse($data, Http::STATUS_NOT_FOUND);
+ } catch (RoomNotFoundException $e) {
+ // Complete conversation was killed, bye!
+ return new DataResponse($data, Http::STATUS_NOT_FOUND);
+ }
}
return new DataResponse($data);
diff --git a/lib/Room.php b/lib/Room.php
index 9924241af7d..ca02918e804 100644
--- a/lib/Room.php
+++ b/lib/Room.php
@@ -839,6 +839,14 @@ public function leaveRoom(string $userId, ?string $sessionId = null): void {
return;
}
+ $this->leaveRoomAsParticipant($participant, $sessionId);
+ }
+
+ /**
+ * @param Participant $participant
+ * @param string|null $sessionId
+ */
+ public function leaveRoomAsParticipant(Participant $participant, ?string $sessionId = null): void {
$event = new ParticipantEvent($this, $participant);
$this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_DISCONNECT, $event);
@@ -847,22 +855,26 @@ public function leaveRoom(string $userId, ?string $sessionId = null): void {
$query->update('talk_participants')
->set('session_id', $query->createNamedParameter('0'))
->set('in_call', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)))
+ ->where($query->expr()->eq('user_id', $query->createNamedParameter($participant->getUser())))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->neq('participant_type', $query->createNamedParameter(Participant::USER_SELF_JOINED, IQueryBuilder::PARAM_INT)));
- if (!empty($sessionId)) {
+ if ($sessionId !== null && $sessionId !== '0') {
$query->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($sessionId)));
+ } elseif ($participant->getSessionId() !== '0') {
+ $query->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($participant->getSessionId())));
}
$query->execute();
// And kill session when leaving a self joined room
$query = $this->db->getQueryBuilder();
$query->delete('talk_participants')
- ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)))
+ ->where($query->expr()->eq('user_id', $query->createNamedParameter($participant->getUser())))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('participant_type', $query->createNamedParameter(Participant::USER_SELF_JOINED, IQueryBuilder::PARAM_INT)));
- if (!empty($sessionId)) {
+ if ($sessionId !== null && $sessionId !== '0') {
$query->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($sessionId)));
+ } elseif ($participant->getSessionId() !== '0') {
+ $query->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($participant->getSessionId())));
}
$query->execute();
diff --git a/src/App.vue b/src/App.vue
index 969de7944d8..655be16cd52 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -31,7 +31,7 @@
-
+
@@ -57,6 +57,8 @@ import {
} from './utils/webrtc/index'
import { emit } from '@nextcloud/event-bus'
import browserCheck from './mixins/browserCheck'
+import duplicateSessionHandler from './mixins/duplicateSessionHandler'
+import isInCall from './mixins/isInCall'
import talkHashCheck from './mixins/talkHashCheck'
import { generateUrl } from '@nextcloud/router'
@@ -73,6 +75,8 @@ export default {
mixins: [
browserCheck,
talkHashCheck,
+ duplicateSessionHandler,
+ isInCall,
],
data: function() {
@@ -113,8 +117,8 @@ export default {
}
},
- isInCall() {
- return this.participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED
+ warnLeaving() {
+ return !this.isLeavingAfterSessionConflict && this.isInCall
},
/**
@@ -221,7 +225,9 @@ export default {
// We have to do this synchronously, because in unload and beforeunload
// Promises, async and await are prohibited.
signalingKill()
- leaveConversationSync(this.token)
+ if (!this.isLeavingAfterSessionConflict) {
+ leaveConversationSync(this.token)
+ }
}
})
diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue
index fc1af2a2551..7ad6202810b 100644
--- a/src/FilesSidebarCallViewApp.vue
+++ b/src/FilesSidebarCallViewApp.vue
@@ -23,10 +23,10 @@
@@ -35,6 +35,8 @@ import { PARTICIPANT } from './constants'
import CallView from './components/CallView/CallView'
import PreventUnload from 'vue-prevent-unload'
import browserCheck from './mixins/browserCheck'
+import duplicateSessionHandler from './mixins/duplicateSessionHandler'
+import isInCall from './mixins/isInCall'
import talkHashCheck from './mixins/talkHashCheck'
export default {
@@ -48,6 +50,8 @@ export default {
mixins: [
browserCheck,
+ duplicateSessionHandler,
+ isInCall,
talkHashCheck,
],
@@ -90,34 +94,38 @@ export default {
* otherwise.
*/
isInFile() {
- if (this.fileId !== this.fileIdForToken) {
- return false
- }
-
- return true
+ return this.fileId === this.fileIdForToken
},
- isInCall() {
+ showCallView() {
// FIXME Remove participants as soon as the file changes so this
// condition is not needed.
if (!this.isInFile) {
return false
}
+ return this.isInCall
+ },
+
+ participant() {
const participantIndex = this.$store.getters.getParticipantIndex(this.token, this.$store.getters.getParticipantIdentifier())
if (participantIndex === -1) {
- return false
+ return {
+ inCall: PARTICIPANT.CALL_FLAG.DISCONNECTED,
+ }
}
- const participant = this.$store.getters.getParticipant(this.token, participantIndex)
+ return this.$store.getters.getParticipant(this.token, participantIndex)
+ },
- return participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED
+ warnLeaving() {
+ return !this.isLeavingAfterSessionConflict && this.showCallView
},
},
watch: {
- isInCall: function(isInCall) {
- if (isInCall) {
+ showCallView: function(showCallView) {
+ if (showCallView) {
this.replaceSidebarHeaderContentsWithCallView()
} else {
this.restoreSidebarHeaderContents()
diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue
index e2ee8b8e8bd..8e9ffd73885 100644
--- a/src/FilesSidebarTabApp.vue
+++ b/src/FilesSidebarTabApp.vue
@@ -62,6 +62,7 @@ import { loadState } from '@nextcloud/initial-state'
import Axios from '@nextcloud/axios'
import CallButton from './components/TopBar/CallButton'
import ChatView from './components/ChatView'
+import duplicateSessionHandler from './mixins/duplicateSessionHandler'
export default {
@@ -72,6 +73,10 @@ export default {
ChatView,
},
+ mixins: [
+ duplicateSessionHandler,
+ ],
+
data() {
return {
// needed for reactivity
@@ -151,7 +156,9 @@ export default {
// We have to do this synchronously, because in unload and beforeunload
// Promises, async and await are prohibited.
signalingKill()
- leaveConversationSync(this.token)
+ if (!this.isLeavingAfterSessionConflict) {
+ leaveConversationSync(this.token)
+ }
}
})
},
diff --git a/src/PublicShareAuthSidebar.vue b/src/PublicShareAuthSidebar.vue
index fd46d399d3c..c4ad49552ad 100644
--- a/src/PublicShareAuthSidebar.vue
+++ b/src/PublicShareAuthSidebar.vue
@@ -49,6 +49,7 @@ import {
} from './services/participantsService'
import { signalingKill } from './utils/webrtc/index'
import browserCheck from './mixins/browserCheck'
+import duplicateSessionHandler from './mixins/duplicateSessionHandler'
import talkHashCheck from './mixins/talkHashCheck'
export default {
@@ -62,6 +63,7 @@ export default {
mixins: [
browserCheck,
+ duplicateSessionHandler,
talkHashCheck,
],
@@ -107,7 +109,9 @@ export default {
// We have to do this synchronously, because in unload and beforeunload
// Promises, async and await are prohibited.
signalingKill()
- leaveConversationSync(this.token)
+ if (!this.isLeavingAfterSessionConflict) {
+ leaveConversationSync(this.token)
+ }
}
})
},
diff --git a/src/PublicShareSidebar.vue b/src/PublicShareSidebar.vue
index 9edb2377afb..13d2bde1300 100644
--- a/src/PublicShareSidebar.vue
+++ b/src/PublicShareSidebar.vue
@@ -35,7 +35,7 @@
-
+
@@ -59,6 +59,8 @@ import {
} from './services/participantsService'
import { signalingKill } from './utils/webrtc/index'
import browserCheck from './mixins/browserCheck'
+import duplicateSessionHandler from './mixins/duplicateSessionHandler'
+import isInCall from './mixins/isInCall'
import talkHashCheck from './mixins/talkHashCheck'
export default {
@@ -74,6 +76,8 @@ export default {
mixins: [
browserCheck,
+ duplicateSessionHandler,
+ isInCall,
talkHashCheck,
],
@@ -109,15 +113,19 @@ export default {
return this.state.isOpen
},
- isInCall() {
+ participant() {
const participantIndex = this.$store.getters.getParticipantIndex(this.token, this.$store.getters.getParticipantIdentifier())
if (participantIndex === -1) {
- return false
+ return {
+ inCall: PARTICIPANT.CALL_FLAG.DISCONNECTED,
+ }
}
- const participant = this.$store.getters.getParticipant(this.token, participantIndex)
+ return this.$store.getters.getParticipant(this.token, participantIndex)
+ },
- return participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED
+ warnLeaving() {
+ return !this.isLeavingAfterSessionConflict && this.isInCall
},
},
@@ -127,7 +135,9 @@ export default {
// We have to do this synchronously, because in unload and beforeunload
// Promises, async and await are prohibited.
signalingKill()
- leaveConversationSync(this.token)
+ if (!this.isLeavingAfterSessionConflict) {
+ leaveConversationSync(this.token)
+ }
}
})
},
diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue
index 1a28e0e9e75..e25c295a7ff 100644
--- a/src/components/TopBar/CallButton.vue
+++ b/src/components/TopBar/CallButton.vue
@@ -51,6 +51,7 @@