Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f5198c0
Optionally return 409 when trying to join a second time
nickvergessen May 15, 2020
5e6a21b
Also support guests to prevent breaking their sessions
nickvergessen May 15, 2020
d5c42d8
Return session id, in call flag and last ping
nickvergessen May 15, 2020
9db6853
Always ask if we can join if we are not reloading the page
nickvergessen May 29, 2020
6b36032
Ask for confirmation to join in case of session conflict
nickvergessen May 29, 2020
cd81c6e
Automatically force join if the session is "old"
nickvergessen May 29, 2020
da78e4c
Add documentation for the conflict handling
nickvergessen May 29, 2020
5bce99b
Leave the room with the old session
nickvergessen May 29, 2020
ee2632b
Further pass on the sessionId to leaveRoomAsParticipant
nickvergessen Jun 15, 2020
61134a0
Handle duplicated sessions more gracefully with the internal signaling
nickvergessen Apr 16, 2020
dd48b52
Better handle 404 error as well as all other undefined scenarios
nickvergessen Apr 16, 2020
9fc01a6
Show session conflict warning when a session joins again for your own…
nickvergessen Jun 15, 2020
9775215
Redirect to a plain page to avoid reconnections
nickvergessen Jun 16, 2020
237d2f5
Send a signal to disconnect the old session before killing it
nickvergessen Jun 16, 2020
5f83e23
Don't show call state when asking to kill the other session
nickvergessen Jun 16, 2020
389d7b6
Trigger a vue event when SessionStorage "joined_conversation" changes
nickvergessen Jun 24, 2020
d54249f
Don't kill the previous session when we navigate away
nickvergessen Jun 24, 2020
eba73a3
Handle the disinvite event properly when the session was kicked
nickvergessen Jun 24, 2020
0f2176b
Add a hack to check if the dialog was closed via the X
nickvergessen Jun 29, 2020
1efead6
If the user has no participant anymore, it was not a conflict
nickvergessen Jun 29, 2020
0c729f0
Fix mixin state also when the component is loaded after the state alr…
nickvergessen Jun 30, 2020
e5c75cb
Update the session and the call flag when force joining
nickvergessen Jul 1, 2020
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
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
'url' => '/not-found',
'verb' => 'GET',
],
[
'name' => 'Page#duplicateSession',
'url' => '/duplicate-session',
'verb' => 'GET',
],

[
'name' => 'Page#showCall',
Expand Down
12 changes: 11 additions & 1 deletion docs/participant.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 11 additions & 1 deletion lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
40 changes: 38 additions & 2 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 19 additions & 1 deletion lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 16 additions & 4 deletions lib/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();

Expand Down
14 changes: 10 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</AppContent>
<RightSidebar
:show-chat-in-sidebar="isInCall" />
<PreventUnload :when="isInCall" />
<PreventUnload :when="warnLeaving" />
</Content>
</template>

Expand All @@ -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'

Expand All @@ -73,6 +75,8 @@ export default {
mixins: [
browserCheck,
talkHashCheck,
duplicateSessionHandler,
isInCall,
],

data: function() {
Expand Down Expand Up @@ -113,8 +117,8 @@ export default {
}
},

isInCall() {
return this.participant.inCall !== PARTICIPANT.CALL_FLAG.DISCONNECTED
warnLeaving() {
return !this.isLeavingAfterSessionConflict && this.isInCall
},

/**
Expand Down Expand Up @@ -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)
}
}
})

Expand Down
34 changes: 21 additions & 13 deletions src/FilesSidebarCallViewApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
<template>
<div v-if="isInFile">
<CallView
v-show="isInCall"
v-show="showCallView"
:token="token"
:is-sidebar="true" />
<PreventUnload :when="isInCall" />
<PreventUnload :when="warnLeaving" />
</div>
</template>

Expand All @@ -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 {
Expand All @@ -48,6 +50,8 @@ export default {

mixins: [
browserCheck,
duplicateSessionHandler,
isInCall,
talkHashCheck,
],

Expand Down Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion src/FilesSidebarTabApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -72,6 +73,10 @@ export default {
ChatView,
},

mixins: [
duplicateSessionHandler,
],

data() {
return {
// needed for reactivity
Expand Down Expand Up @@ -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)
}
}
})
},
Expand Down
6 changes: 5 additions & 1 deletion src/PublicShareAuthSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -62,6 +63,7 @@ export default {

mixins: [
browserCheck,
duplicateSessionHandler,
talkHashCheck,
],

Expand Down Expand Up @@ -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)
}
}
})
},
Expand Down
Loading