diff --git a/appinfo/routes.php b/appinfo/routes.php
index 22074e93878..f8e7c614559 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/package-lock.json b/package-lock.json
index a5121ec4c10..6ba71eda3ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2560,6 +2560,21 @@
}
}
},
+ "@nextcloud/browser-storage": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.1.1.tgz",
+ "integrity": "sha512-bWzs/A44rEK8b3CMOFw0ZhsenagrWdsB902LOEwmlMCcFysiFgWiOPbF4/0/ODlOYjvPrO02wf6RigWtb8P+gA==",
+ "requires": {
+ "core-js": "3.6.1"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.1.tgz",
+ "integrity": "sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ=="
+ }
+ }
+ },
"@nextcloud/browserslist-config": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-1.0.0.tgz",
diff --git a/package.json b/package.json
index c8f617d6e3f..0c3a6de282f 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@juliushaertl/vue-richtext": "^0.3.1",
"@nextcloud/auth": "^1.2.3",
"@nextcloud/axios": "^1.3.2",
+ "@nextcloud/browser-storage": "^0.1.1",
"@nextcloud/dialogs": "^1.3.0",
"@nextcloud/event-bus": "^1.1.4",
"@nextcloud/initial-state": "^1.1.2",
diff --git a/src/App.vue b/src/App.vue
index 943839506b5..68ca324ccba 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -31,7 +31,7 @@
-
+
@@ -56,6 +56,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'
@@ -72,6 +74,8 @@ export default {
mixins: [
browserCheck,
talkHashCheck,
+ duplicateSessionHandler,
+ isInCall,
],
data: function() {
@@ -112,8 +116,8 @@ export default {
}
},
- isInCall() {
- return this.participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED
+ warnLeaving() {
+ return !this.isLeavingAfterSessionConflict && this.isInCall
},
/**
@@ -220,7 +224,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 31608031e83..fb25c896c7a 100644
--- a/src/components/TopBar/CallButton.vue
+++ b/src/components/TopBar/CallButton.vue
@@ -51,6 +51,7 @@