Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a1578c9
Optionally return 409 when trying to join a second time
nickvergessen May 15, 2020
fef57e9
Handle duplicated sessions more gracefully with the internal signaling
nickvergessen Apr 16, 2020
6467e6b
Better handle 404 error as well as all other undefined scenarios
nickvergessen Apr 16, 2020
cb9eb66
Also support guests to prevent breaking their sessions
nickvergessen May 15, 2020
d424d08
Return session id, in call flag and last ping
nickvergessen May 15, 2020
ebc4b0a
Always ask if we can join if we are not reloading the page
nickvergessen May 29, 2020
e6e627f
Ask for confirmation to join in case of session conflict
nickvergessen May 29, 2020
73603d4
Automatically force join if the session is "old"
nickvergessen May 29, 2020
2939bd9
Add documentation for the conflict handling
nickvergessen May 29, 2020
18c10ec
Leave the room with the old session
nickvergessen May 29, 2020
77e6ecf
Further pass on the sessionId to leaveRoomAsParticipant
nickvergessen Jun 15, 2020
a4ef453
Show session conflict warning when a session joins again for your own…
nickvergessen Jun 15, 2020
8b350af
Redirect to a plain page to avoid reconnections
nickvergessen Jun 16, 2020
0385e9a
Send a signal to disconnect the old session before killing it
nickvergessen Jun 16, 2020
5733c49
Don't show call state when asking to kill the other session
nickvergessen Jun 16, 2020
8846755
Trigger a vue event when SessionStorage "joined_conversation" changes
nickvergessen Jun 24, 2020
cd0d497
Don't kill the previous session when we navigate away
nickvergessen Jun 24, 2020
159ea23
Handle the disinvite event properly when the session was kicked
nickvergessen Jun 24, 2020
2a5b1ff
Add a hack to check if the dialog was closed via the X
nickvergessen Jun 29, 2020
1faa457
If the user has no participant anymore, it was not a conflict
nickvergessen Jun 29, 2020
7012568
Fix mixin state also when the component is loaded after the state alr…
nickvergessen Jun 30, 2020
4a83d42
Update the session and the call flag when force joining
nickvergessen Jul 1, 2020
25c3b36
Add browser-storage
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
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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 @@ -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'

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

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

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

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

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
Loading