diff --git a/.drone.yml b/.drone.yml index f12a6e62b2b..12bc343f6fa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -453,6 +453,41 @@ trigger: - pull_request - push +--- +kind: pipeline +name: int-sqlite-command + +steps: + - name: integration-command + image: nextcloudci/php7.3:php7.3-5 + environment: + APP_NAME: spreed + CORE_BRANCH: stable20 + DATABASEHOST: sqlite + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://github.com/raw/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/command + +services: + - name: cache + image: redis + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push + --- kind: pipeline name: int-sqlite-conversation @@ -613,6 +648,51 @@ trigger: # - pull_request - push +--- +kind: pipeline +name: int-mysql-command + +steps: + - name: integration-command + image: nextcloudci/php7.3:php7.3-5 + environment: + APP_NAME: spreed + CORE_BRANCH: stable20 + DATABASEHOST: mysql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://github.com/raw/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/command + +services: + - name: cache + image: redis + - name: mysql + image: mysql:5.7.22 + environment: + MYSQL_ROOT_PASSWORD: owncloud + MYSQL_USER: oc_autotest + MYSQL_PASSWORD: owncloud + MYSQL_DATABASE: oc_autotest + command: [ "--innodb_large_prefix=true", "--innodb_file_format=barracuda", "--innodb_file_per_table=true" ] + tmpfs: + - /var/lib/mysql + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + --- kind: pipeline name: int-mysql-conversation @@ -791,6 +871,50 @@ trigger: # - pull_request - push +--- +kind: pipeline +name: int-pgsql-command + +steps: + - name: integration-command + image: nextcloudci/php7.3:php7.3-5 + environment: + APP_NAME: spreed + CORE_BRANCH: stable20 + DATABASEHOST: pgsql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://github.com/raw/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/command + +services: + - name: cache + image: redis + - name: pgsql + image: postgres:10 + environment: + POSTGRES_USER: oc_autotest + POSTGRES_DB: oc_autotest_dummy + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PASSWORD: + tmpfs: + - /var/lib/postgresql/data + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + --- kind: pipeline name: int-pgsql-conversation diff --git a/appinfo/info.xml b/appinfo/info.xml index 235fa98e04d..3f6d5959404 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m ]]> - 10.0.3 + 10.1.0-dev.1 agpl Daniel Calviño Sánchez diff --git a/appinfo/routes.php b/appinfo/routes.php index 49651fe46a7..1e1f2259173 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -62,7 +62,7 @@ 'url' => '/api/{apiVersion}/signaling/settings', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2)', ], ], [ @@ -70,7 +70,7 @@ 'url' => '/api/{apiVersion}/signaling/welcome/{serverId}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2)', 'serverId' => '^\d+$', ], ], @@ -79,7 +79,7 @@ 'url' => '/api/{apiVersion}/signaling/backend', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2)', ], ], [ @@ -87,7 +87,7 @@ 'url' => '/api/{apiVersion}/signaling/{token}', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -96,7 +96,7 @@ 'url' => '/api/{apiVersion}/signaling/{token}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -109,7 +109,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -118,7 +118,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -127,7 +127,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -180,7 +180,7 @@ 'url' => '/api/{apiVersion}/room', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', ], ], [ @@ -188,7 +188,7 @@ 'url' => '/api/{apiVersion}/room', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', ], ], [ @@ -196,7 +196,7 @@ 'url' => '/api/{apiVersion}/room/{token}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -205,7 +205,7 @@ 'url' => '/api/{apiVersion}/room/{token}', 'verb' => 'PUT', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -214,7 +214,7 @@ 'url' => '/api/{apiVersion}/room/{token}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -223,7 +223,7 @@ 'url' => '/api/{apiVersion}/room/{token}/public', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -232,7 +232,7 @@ 'url' => '/api/{apiVersion}/room/{token}/public', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -241,7 +241,7 @@ 'url' => '/api/{apiVersion}/room/{token}/read-only', 'verb' => 'PUT', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -250,7 +250,7 @@ 'url' => '/api/{apiVersion}/room/{token}/password', 'verb' => 'PUT', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -259,7 +259,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -268,7 +268,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -277,7 +277,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -286,7 +286,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants/self', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -295,7 +295,16 @@ 'url' => '/api/{apiVersion}/room/{token}/participants/guests', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], + [ + 'name' => 'Room#removeAttendeeFromRoom', + 'url' => '/api/{apiVersion}/room/{token}/attendees', + 'verb' => 'DELETE', + 'requirements' => [ + 'apiVersion' => 'v3', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -304,7 +313,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -313,7 +322,7 @@ 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -322,7 +331,7 @@ 'url' => '/api/{apiVersion}/room/{token}/moderators', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -331,7 +340,7 @@ 'url' => '/api/{apiVersion}/room/{token}/moderators', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -340,7 +349,7 @@ 'url' => '/api/{apiVersion}/room/{token}/favorite', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -349,8 +358,18 @@ 'url' => '/api/{apiVersion}/room/{token}/favorite', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], + [ + 'name' => 'Room#getParticipantByDialInPin', + 'url' => '/api/{apiVersion}/room/{token}/pin/{pin}', + 'verb' => 'GET', + 'requirements' => [ + 'apiVersion' => 'v3', 'token' => '^[a-z0-9]{4,30}$', + 'pin' => '^\d{7,32}$', ], ], [ @@ -358,7 +377,27 @@ 'url' => '/api/{apiVersion}/room/{token}/notify', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', + 'apiVersion' => 'v(1|2|3)', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], + [ + 'name' => 'Room#setLobby', + 'url' => '/api/{apiVersion}/room/{token}/{webinar}/lobby', + 'verb' => 'PUT', + 'requirements' => [ + 'apiVersion' => 'v(1|2|3)', + 'webinar' => 'webinary?', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], + [ + 'name' => 'Room#setSIPEnabled', + 'url' => '/api/{apiVersion}/room/{token}/{webinar}/sip', + 'verb' => 'PUT', + 'requirements' => [ + 'apiVersion' => 'v3', + 'webinar' => 'webinary?', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -481,22 +520,16 @@ ], /** - * Webinar + * Settings */ [ - 'name' => 'Webinar#setLobby', - 'url' => '/api/{apiVersion}/room/{token}/{webinar}/lobby', - 'verb' => 'PUT', + 'name' => 'Settings#setSIPSettings', + 'url' => '/api/{apiVersion}/settings/sip', + 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v(1|2)', - 'webinar' => 'webinary?', - 'token' => '^[a-z0-9]{4,30}$', + 'apiVersion' => 'v1', ], ], - - /** - * UserSettings - */ [ 'name' => 'Settings#setUserSetting', 'url' => '/api/{apiVersion}/settings/user', diff --git a/docs/capabilities.md b/docs/capabilities.md index 03c878d7d75..9cc183581ed 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -51,3 +51,6 @@ title: Capabilities * `force-mute` - "forceMute" signaling messages can be sent to mute other participants. * `conversation-v2` - The conversations API v2 is less load heavy and should be used by clients when available. Check the difference in the [Conversation API documentation](conversation.md). * `chat-reference-id` - an optional referenceId can be sent with a chat message to be able to identify it in parallel get requests to earlier fade out a temporary message + +## 10.0 +* `sip-support` - Whether conversations API v3 exists and SIP can be configured and enabled by moderators. The conversations API will come with some new values `sipEnabled` which signals whether this conversation has SIP configured as well as `canEnableSIP` to see if a user can enable it. When it is enabled `attendeePin` will contain the unique dial-in code for this user. diff --git a/docs/conversation.md b/docs/conversation.md index f6722359717..a87107e76df 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -2,6 +2,7 @@ * Base endpoint for API v1 is: `/ocs/v2.php/apps/spreed/api/v1` * Base endpoint for API v2 is: `/ocs/v2.php/apps/spreed/api/v2` +* Base endpoint for API v3 is: `/ocs/v2.php/apps/spreed/api/v3` ## Get user´s conversations @@ -29,6 +30,10 @@ `name` | string | * | Name of the conversation (can also be empty) `displayName` | string | * | `name` if non empty, otherwise it falls back to a list of participants `participantType` | int | * | Permissions level of the current user + `attendeeId` | int | v3 | Unique attendee id + `attendeePin` | string | v3 | Unique dial-in authentication code for this user, when the conversation has SIP enabled (see `sipEnabled` attribute) + `actorType` | string | v3 | Currently known `users|guests|emails|groups` + `actorId` | string | v3 | The unique identifier for the given actor type `participantInCall` | bool | 🏴 v1 | Flag if the current user is in the call (deprecated, use `participantFlags` instead) `participantFlags` | int | * | Flags of the current user (only available with `in-call-flags` capability) `readOnly` | int | * | Read-only state for the current user (only available with `read-only-rooms` capability) @@ -46,6 +51,8 @@ `notificationLevel` | int | * | The notification level for the user (one of `Participant::NOTIFY_*` (1-3)) `lobbyState` | int | * | Webinary lobby restriction (0-1), if the participant is a moderator they can always join the conversation (only available with `webinary-lobby` capability) `lobbyTimer` | int | * | Timestamp when the lobby will be automatically disabled (only available with `webinary-lobby` capability) + `sipEnabled` | int | v3 | SIP enable status (0-1) + `canEnableSIP` | int | v3 | Whether the given user can enable SIP for this conversation. Note that when the token is not-numeric only, SIP can not be enabled even if the user is permitted and a moderator of the conversation `unreadMessages` | int | * | Number of unread chat messages in the conversation (only available with `chat-v2` capability) `unreadMention` | bool | * | Flag if the user was mentioned since their last visit `lastReadMessage` | int | * | ID of the last read message in a room (only available with `chat-read-marker` capability) diff --git a/docs/internal-signaling.md b/docs/internal-signaling.md index 3524791b791..1a4db437ae7 100644 --- a/docs/internal-signaling.md +++ b/docs/internal-signaling.md @@ -14,15 +14,16 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` * Response: - field | type | Description - ------|------|------------ - `signalingMode` | string | See [Signaling modes](constants.md#Signaling_modes) - `userId` | string | Current user id - `hideWarning` | string | Don't show a performance warning although internal signaling is used - `server` | string | URL of the external signaling server - `ticket` | string | Ticket for the external signaling server - `stunservers` | array | STUN servers - `turnservers` | array | TURN servers + field | type | API | Description + ------|------|-----|------------ + `signalingMode` | string | * | See [Signaling modes](constants.md#Signaling_modes) + `userId` | string | * | Current user id + `hideWarning` | string | * | Don't show a performance warning although internal signaling is used + `server` | string | * | URL of the external signaling server + `ticket` | string | * | Ticket for the external signaling server + `stunservers` | array | * | STUN servers + `turnservers` | array | * | TURN servers + `sipDialinInfo` | string | v2 | Generic SIP dial-in information for this conversation (admin free text containing the phone number etc) - STUN server diff --git a/docs/participant.md b/docs/participant.md index 245ea387d2c..8b149ff1936 100644 --- a/docs/participant.md +++ b/docs/participant.md @@ -1,6 +1,8 @@ # Participant API -Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` +* Base endpoint for API v1 is: `/ocs/v2.php/apps/spreed/api/v1` +* Base endpoint for API v2 is: `/ocs/v2.php/apps/spreed/api/v2` +* Base endpoint for API v3 is: `/ocs/v2.php/apps/spreed/api/v3` ## Get list of participants in a conversation @@ -22,16 +24,19 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` - Data: Array of participants, each participant has at least: - field | type | Description - ------|------|------------ - `userId` | string | Is empty for guests - `displayName` | string | Can be empty for guests - `participantType` | int | Permissions level of the participant - `lastPing` | int | Timestamp of the last ping of the user (should be used for sorting) - `sessionId` | string | `'0'` if not connected, otherwise a 512 character long string - `status` | string | Optional: Only available with `includeStatus=true` and for users with a set status - `statusIcon` | string | Optional: Only available with `includeStatus=true` and for users with a set status - `statusMessage` | string | Optional: Only available with `includeStatus=true` and for users with a set status + field | type | API | Description + ------|------|-----|------------ + `userId` | string | v1 + v2| Is empty for guests + `attendeeId` | int | v3 | Unique attendee id + `actorType` | string | v3 | Currently known `users|guests|emails|groups` + `actorId` | string | v3 | The unique identifier for the given actor type + `displayName` | string | | Can be empty for guests + `participantType` | int | | Permissions level of the participant + `lastPing` | int | | Timestamp of the last ping of the user (should be used for sorting) + `sessionId` | string | | `'0'` if not connected, otherwise a 512 character long string + `status` | string | | Optional: Only available with `includeStatus=true` and for users with a set status + `statusIcon` | string | | Optional: Only available with `includeStatus=true` and for users with a set status + `statusMessage` | string | | Optional: Only available with `includeStatus=true` and for users with a set status ## Add a participant to a conversation @@ -59,8 +64,30 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ------|------|------------ `type` | int | In case the conversation type changed, the new value is returned +## Delete an attendee by id from a conversation + +* API: Only `v3` or later +* Method: `DELETE` +* Endpoint: `/room/{token}/attendees` +* Data: + + field | type | Description + ------|------|------------ + `attendeeId` | int | The participant to delete + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the participant is a moderator or owner + + `400 Bad Request` When there are no other moderators or owners left + + `403 Forbidden` When the current user is not a moderator or owner + + `403 Forbidden` When the participant to remove is an owner + + `404 Not Found` When the conversation could not be found for the participant + + `404 Not Found` When the participant to remove could not be found + ## Delete a participant from a conversation +* API: Only `v1` and `v2` * Method: `DELETE` * Endpoint: `/room/{token}/participants` * Data: @@ -92,6 +119,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ## Remove a guest from a conversation +* API: Only `v1` and `v2` * Method: `DELETE` * Endpoint: `/room/{token}/participants/guests` * Data: @@ -156,10 +184,11 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` * Endpoint: `/room/{token}/moderators` * Data: - field | type | Description - ------|------|------------ - `participant` | string or null | User to promote - `sessionId` | string or null | Guest session to promote + field | type | API | Description + ------|------|-----|------------ + `participant` | string or null | v1 + v2 | User to demote + `sessionId` | string or null | v1 + v2 | Guest session to demote + `attendeeId` | int or null | v3 | Attendee id can be used for guests and users * Response: - Status code: @@ -176,10 +205,11 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` * Endpoint: `/room/{token}/moderators` * Data: - field | type | Description - ------|------|------------ - `participant` | string or null | User to demote - `sessionId` | string or null | Guest session to demote + field | type | API | Description + ------|------|-----|------------ + `participant` | string or null | v1 + v2 | User to demote + `sessionId` | string or null | v1 + v2 | Guest session to demote + `attendeeId` | int or null | v3 | Attendee id can be used for guests and users * Response: - Status code: @@ -190,6 +220,22 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` + `404 Not Found` When the conversation could not be found for the participant + `404 Not Found` When the participant to demote could not be found +## Get a participant by their pin + +Note: This is only allowed with validate SIP bridge requests + +* API: Only `v3` or later +* Method: `GET` +* Endpoint: `/room/{token}/pin/{pin}` + +* Response: + - Status code: + + `200 OK` + + `401 Unauthorized` When the validation as SIP bridge failed + + `404 Not Found` When the conversation or participant could not be found + + - Data: See array definition in `Get user´s conversations` + ## Set display name as a guest * Method: `POST` diff --git a/docs/webinar.md b/docs/webinar.md index a3331359b4c..ea3f37bf3de 100644 --- a/docs/webinar.md +++ b/docs/webinar.md @@ -1,6 +1,6 @@ # Webinar management -Group and public conversations can be used to host webinaries. Those online meetings can have a lobby, which come with the following restrictions: +Group and public conversations can be used to host webinars. Those online meetings can have a lobby, which come with the following restrictions: * Only moderators can start/join a call * Only moderators can read and write chat messages @@ -28,3 +28,23 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` + `400 Bad Request` When the given timestamp is invalid + `403 Forbidden` When the current user is not a moderator/owner + `404 Not Found` When the conversation could not be found for the participant + +## Enabled or disable SIP dial-in + +* Required capability: `sip-support` +* Method: `PUT` +* Endpoint: `/room/{token}/webinar/sip` +* Data: + + field | type | Description + ------|------|------------ + `state` | int | New SIP state for the conversation (0 = disabled, 1 = enabled) + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the state was invalid or the same + + `401 Unauthorized` When the user can not enabled SIP + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant + + `412 Precondition Failed` When SIP is not configured on the server diff --git a/img/phone.png b/img/phone.png new file mode 100644 index 00000000000..c385083f17f Binary files /dev/null and b/img/phone.png differ diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index 9928c53a95a..47449606775 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -27,7 +27,9 @@ use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\ModifyParticipantEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\Activity\IManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -46,6 +48,9 @@ class Listener { /** @var ChatManager */ protected $chatManager; + /** @var ParticipantService */ + protected $participantService; + /** @var LoggerInterface */ protected $logger; @@ -55,11 +60,13 @@ class Listener { public function __construct(IManager $activityManager, IUserSession $userSession, ChatManager $chatManager, + ParticipantService $participantService, LoggerInterface $logger, ITimeFactory $timeFactory) { $this->activityManager = $activityManager; $this->userSession = $userSession; $this->chatManager = $chatManager; + $this->participantService = $participantService; $this->logger = $logger; $this->timeFactory = $timeFactory; } @@ -102,12 +109,12 @@ public function setActive(Room $room): void { */ public function generateCallActivity(Room $room): bool { $activeSince = $room->getActiveSince(); - if (!$activeSince instanceof \DateTime || $room->hasSessionsInCall()) { + if (!$activeSince instanceof \DateTime || $this->participantService->hasActiveSessionsInCall($room)) { return false; } $duration = $this->timeFactory->getTime() - $activeSince->getTimestamp(); - $userIds = $room->getParticipantUserIds($activeSince); + $userIds = $this->participantService->getParticipantUserIds($room, $activeSince); if ((\count($userIds) + $room->getActiveGuests()) === 1) { // Single user pinged or guests only => no summary/activity @@ -123,7 +130,7 @@ public function generateCallActivity(Room $room): bool { } $actorId = $userIds[0] ?? 'guests-only'; - $actorType = $actorId !== 'guests-only' ? 'users' : 'guests'; + $actorType = $actorId !== 'guests-only' ? Attendee::ACTOR_USERS : Attendee::ACTOR_GUESTS; $this->chatManager->addSystemMessage($room, $actorType, $actorId, json_encode([ 'message' => 'call_ended', 'parameters' => [ @@ -199,13 +206,18 @@ public function generateInvitationActivity(Room $room, array $participants): voi } foreach ($participants as $participant) { - if ($actorId === $participant['userId']) { + if ($participant['actorType'] !== Attendee::ACTOR_USERS) { + // No user => no activity + continue; + } + + if ($actorId === $participant['actorId']) { // No activity for self-joining and the creator continue; } try { - $roomName = $room->getDisplayName($participant['userId']); + $roomName = $room->getDisplayName($participant['actorId']); $event ->setObject('room', $room->getId(), $roomName) ->setSubject('invitation', [ @@ -213,7 +225,7 @@ public function generateInvitationActivity(Room $room, array $participants): voi 'room' => $room->getId(), 'name' => $roomName, ]) - ->setAffectedUser($participant['userId']); + ->setAffectedUser($participant['actorId']); $this->activityManager->publish($event); } catch (\InvalidArgumentException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); diff --git a/lib/BackgroundJob/RemoveEmptyRooms.php b/lib/BackgroundJob/RemoveEmptyRooms.php index 6446b1aef17..af973880966 100644 --- a/lib/BackgroundJob/RemoveEmptyRooms.php +++ b/lib/BackgroundJob/RemoveEmptyRooms.php @@ -23,6 +23,7 @@ namespace OCA\Talk\BackgroundJob; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; use OCA\Talk\Manager; @@ -39,6 +40,9 @@ class RemoveEmptyRooms extends TimedJob { /** @var Manager */ protected $manager; + /** @var ParticipantService */ + protected $participantService; + /** @var LoggerInterface */ protected $logger; @@ -46,6 +50,7 @@ class RemoveEmptyRooms extends TimedJob { public function __construct(ITimeFactory $timeFactory, Manager $manager, + ParticipantService $participantService, LoggerInterface $logger) { parent::__construct($timeFactory); @@ -53,6 +58,7 @@ public function __construct(ITimeFactory $timeFactory, $this->setInterval(60 * 5); $this->manager = $manager; + $this->participantService = $participantService; $this->logger = $logger; } @@ -71,7 +77,7 @@ public function callback(Room $room): void { return; } - if ($room->getNumberOfParticipants(false) === 0 && $room->getObjectType() !== 'file') { + if ($this->participantService->getNumberOfActors($room) === 0 && $room->getObjectType() !== 'file') { $room->deleteRoom(); $this->numDeletedRooms++; } diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 3a31ad55636..4af5b4ff23a 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -80,6 +80,7 @@ public function getCapabilities(): array { 'chat-replies', 'circles-support', 'force-mute', + 'sip-support', ], 'config' => [ 'attachments' => [ diff --git a/lib/Chat/AutoComplete/SearchPlugin.php b/lib/Chat/AutoComplete/SearchPlugin.php index cfa015adb5f..770c963634b 100644 --- a/lib/Chat/AutoComplete/SearchPlugin.php +++ b/lib/Chat/AutoComplete/SearchPlugin.php @@ -25,7 +25,9 @@ use OCA\Talk\Files\Util; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; use OCP\Collaboration\Collaborators\ISearchPlugin; use OCP\Collaboration\Collaborators\ISearchResult; @@ -42,6 +44,8 @@ class SearchPlugin implements ISearchPlugin { protected $guestManager; /** @var TalkSession */ protected $talkSession; + /** @var ParticipantService */ + protected $participantService; /** @var Util */ protected $util; /** @var string|null */ @@ -55,12 +59,14 @@ class SearchPlugin implements ISearchPlugin { public function __construct(IUserManager $userManager, GuestManager $guestManager, TalkSession $talkSession, + ParticipantService $participantService, Util $util, ?string $userId, IL10N $l) { $this->userManager = $userManager; $this->guestManager = $guestManager; $this->talkSession = $talkSession; + $this->participantService = $participantService; $this->util = $util; $this->userId = $userId; $this->l = $l; @@ -94,12 +100,13 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { $userIds[] = $userId; } } else { - $participants = $this->room->getParticipants(); + $participants = $this->participantService->getParticipantsForRoom($this->room); foreach ($participants as $participant) { - if ($participant->isGuest()) { - $guestSessionHashes[] = sha1($participant->getSessionId()); - } else { - $userIds[] = $participant->getUser(); + $attendee = $participant->getAttendee(); + if ($attendee->getActorType() === Attendee::ACTOR_GUESTS) { + $guestSessionHashes[] = $attendee->getActorId(); + } elseif ($attendee->getActorType() === Attendee::ACTOR_USERS) { + $userIds[] = $attendee->getActorId(); } } } diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 4c6ba48a658..13ead6d8ca4 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -28,8 +28,10 @@ use OC\Memcache\NullCache; use OCA\Talk\Events\ChatEvent; use OCA\Talk\Events\ChatParticipantEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; @@ -67,6 +69,8 @@ class ChatManager { private $connection; /** @var INotificationManager */ private $notificationManager; + /** @var ParticipantService */ + private $participantService; /** @var Notifier */ private $notifier; /** @var ITimeFactory */ @@ -78,6 +82,7 @@ public function __construct(CommentsManager $commentsManager, IEventDispatcher $dispatcher, IDBConnection $connection, INotificationManager $notificationManager, + ParticipantService $participantService, Notifier $notifier, ICacheFactory $cacheFactory, ITimeFactory $timeFactory) { @@ -85,6 +90,7 @@ public function __construct(CommentsManager $commentsManager, $this->dispatcher = $dispatcher; $this->connection = $connection; $this->notificationManager = $notificationManager; + $this->participantService = $participantService; $this->notifier = $notifier; $this->cache = $cacheFactory->createDistributed('talk/lastmsgid'); $this->timeFactory = $timeFactory; @@ -139,7 +145,7 @@ public function addSystemMessage(Room $chat, string $actorType, string $actorId, * @return IComment */ public function addChangelogMessage(Room $chat, string $message): IComment { - $comment = $this->commentsManager->create('guests', 'changelog', 'chat', (string) $chat->getId()); + $comment = $this->commentsManager->create(Attendee::ACTOR_GUESTS, 'changelog', 'chat', (string) $chat->getId()); $comment->setMessage($message, self::MAX_CHAT_LENGTH); $comment->setCreationDateTime($this->timeFactory->getDateTime()); @@ -208,7 +214,7 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT $alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $alreadyNotifiedUsers); if (!empty($alreadyNotifiedUsers)) { - $chat->markUsersAsMentioned($alreadyNotifiedUsers, (int) $comment->getId()); + $this->participantService->markUsersAsMentioned($chat, $alreadyNotifiedUsers, (int) $comment->getId()); } // User was not mentioned, send a normal notification diff --git a/lib/Chat/Command/Executor.php b/lib/Chat/Command/Executor.php index 21a01fce954..0862936c4bb 100644 --- a/lib/Chat/Command/Executor.php +++ b/lib/Chat/Command/Executor.php @@ -25,6 +25,7 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Events\CommandEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Command; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -90,7 +91,7 @@ public function exec(Room $room, IComment $message, Command $command, string $ar try { $command = $this->commandService->resolveAlias($command); } catch (DoesNotExistException $e) { - $user = $message->getActorType() === 'users' ? $message->getActorId() : ''; + $user = $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : ''; $message->setMessage(json_encode([ 'user' => $user, 'visibility' => $command->getResponse(), @@ -109,7 +110,7 @@ public function exec(Room $room, IComment $message, Command $command, string $ar $output = $this->execShell($room, $message, $command, $arguments); } - $user = $message->getActorType() === 'users' ? $message->getActorId() : ''; + $user = $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : ''; $message->setMessage(json_encode([ 'user' => $user, 'visibility' => $command->getResponse(), @@ -205,7 +206,7 @@ public function execShell(Room $room, IComment $message, Command $command, strin $command->getScript(), $arguments, $room->getToken(), - $message->getActorType() === 'users' ? $message->getActorId() : '' + $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : '' ); } catch (\InvalidArgumentException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 176be751e68..ebea4d20bea 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -27,6 +27,7 @@ use OCA\Talk\Events\ChatMessageEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -79,10 +80,10 @@ protected function setActor(Message $message): void { $comment = $message->getComment(); $displayName = ''; - if ($comment->getActorType() === 'users') { + if ($comment->getActorType() === Attendee::ACTOR_USERS) { $user = $this->userManager->get($comment->getActorId()); $displayName = $user instanceof IUser ? $user->getDisplayName() : $comment->getActorId(); - } elseif ($comment->getActorType() === 'guests') { + } elseif ($comment->getActorType() === Attendee::ACTOR_GUESTS) { if (isset($guestNames[$comment->getActorId()])) { $displayName = $this->guestNames[$comment->getActorId()]; } else { diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index d4130ef1782..0b5db45eb3e 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -28,8 +28,11 @@ use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Files\Util; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\Comments\IComment; use OCP\IConfig; use OCP\Notification\IManager as INotificationManager; @@ -49,6 +52,8 @@ class Notifier { private $notificationManager; /** @var IUserManager */ private $userManager; + /** @var ParticipantService */ + private $participantService; /** @var Manager */ private $manager; /** @var IConfig */ @@ -58,11 +63,13 @@ class Notifier { public function __construct(INotificationManager $notificationManager, IUserManager $userManager, + ParticipantService $participantService, Manager $manager, IConfig $config, Util $util) { $this->notificationManager = $notificationManager; $this->userManager = $userManager; + $this->participantService = $participantService; $this->manager = $manager; $this->config = $config; $this->util = $util; @@ -91,7 +98,7 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea $mentionedAll = array_search('all', $mentionedUserIds, true); if ($mentionedAll !== false) { - $mentionedUserIds = array_unique(array_merge($mentionedUserIds, $chat->getParticipantUserIds())); + $mentionedUserIds = array_unique(array_merge($mentionedUserIds, $this->participantService->getParticipantUserIds($chat))); } $notification = $this->createNotification($chat, $comment, 'mention'); @@ -129,7 +136,7 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea * @return string[] Users that were mentioned */ public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo): array { - if ($replyTo->getActorType() !== 'users') { + if ($replyTo->getActorType() !== Attendee::ACTOR_USERS) { // No reply notification when the replyTo-author was not a user return []; } @@ -159,7 +166,7 @@ public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $rep * @param string[] $alreadyNotifiedUsers */ public function notifyOtherParticipant(Room $chat, IComment $comment, array $alreadyNotifiedUsers): void { - $participants = $chat->getParticipantsByNotificationLevel(Participant::NOTIFY_ALWAYS); + $participants = $this->participantService->getParticipantsByNotificationLevel($chat, Participant::NOTIFY_ALWAYS); $notification = $this->createNotification($chat, $comment, 'chat'); foreach ($participants as $participant) { @@ -167,19 +174,19 @@ public function notifyOtherParticipant(Room $chat, IComment $comment, array $alr continue; } - $notification->setUser($participant->getUser()); + $notification->setUser($participant->getAttendee()->getActorId()); $this->notificationManager->notify($notification); } // Also notify default participants in one2one chats or when the admin default is "always" if ($this->getDefaultGroupNotification() === Participant::NOTIFY_ALWAYS || $chat->getType() === Room::ONE_TO_ONE_CALL) { - $participants = $chat->getParticipantsByNotificationLevel(Participant::NOTIFY_DEFAULT); + $participants = $this->participantService->getParticipantsByNotificationLevel($chat, Participant::NOTIFY_DEFAULT); foreach ($participants as $participant) { if (!$this->shouldParticipantBeNotified($participant, $comment, $alreadyNotifiedUsers)) { continue; } - $notification->setUser($participant->getUser()); + $notification->setUser($participant->getAttendee()->getActorId()); $this->notificationManager->notify($notification); } } @@ -301,8 +308,8 @@ protected function getDefaultGroupNotification(): int { * @param IComment $comment * @return bool */ - protected function shouldMentionedUserBeNotified($userId, IComment $comment): bool { - if ($comment->getActorType() === 'users' && $userId === $comment->getActorId()) { + protected function shouldMentionedUserBeNotified(string $userId, IComment $comment): bool { + if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) { // Do not notify the user if they mentioned themselves return false; } @@ -319,8 +326,8 @@ protected function shouldMentionedUserBeNotified($userId, IComment $comment): bo try { $participant = $room->getParticipant($userId); - $notificationLevel = $participant->getNotificationLevel(); - if ($participant->getNotificationLevel() === Participant::NOTIFY_DEFAULT) { + $notificationLevel = $participant->getAttendee()->getNotificationLevel(); + if ($notificationLevel === Participant::NOTIFY_DEFAULT) { if ($room->getType() === Room::ONE_TO_ONE_CALL) { $notificationLevel = Participant::NOTIFY_ALWAYS; } else { @@ -334,7 +341,10 @@ protected function shouldMentionedUserBeNotified($userId, IComment $comment): bo // so they can see the room in their room list and // the notification can be parsed and links to an existing room, // where they are a participant of. - $room->addUsers(['userId' => $userId]); + $this->participantService->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $userId, + ]]); return true; } return false; @@ -355,20 +365,21 @@ protected function shouldMentionedUserBeNotified($userId, IComment $comment): bo * @return bool */ protected function shouldParticipantBeNotified(Participant $participant, IComment $comment, array $alreadyNotifiedUsers): bool { - if ($participant->isGuest()) { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS) { return false; } - if ($comment->getActorType() === 'users' && $participant->getUser() === $comment->getActorId()) { + $userId = $participant->getAttendee()->getActorId(); + if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) { // Do not notify the author return false; } - if (\in_array($participant->getUser(), $alreadyNotifiedUsers, true)) { + if (\in_array($userId, $alreadyNotifiedUsers, true)) { return false; } - if ($participant->getSessionId() !== '0') { + if ($participant->getSession() instanceof Session) { // User is online return false; } diff --git a/lib/Chat/Parser/Changelog.php b/lib/Chat/Parser/Changelog.php index 2f6621848d2..d1c8ce12927 100644 --- a/lib/Chat/Parser/Changelog.php +++ b/lib/Chat/Parser/Changelog.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; class Changelog { @@ -32,7 +33,7 @@ class Changelog { * @throws \OutOfBoundsException */ public function parseMessage(Message $chatMessage): void { - if ($chatMessage->getActorType() !== 'guests' || + if ($chatMessage->getActorType() !== Attendee::ACTOR_GUESTS || $chatMessage->getActorId() !== 'changelog') { throw new \OutOfBoundsException('Not a changelog'); } diff --git a/lib/Chat/Parser/Command.php b/lib/Chat/Parser/Command.php index 4e093c2699c..6a69f2c8223 100644 --- a/lib/Chat/Parser/Command.php +++ b/lib/Chat/Parser/Command.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; class Command { @@ -44,7 +45,8 @@ public function parseMessage(Message $message): void { $participant = $message->getParticipant(); if ($data['visibility'] !== \OCA\Talk\Model\Command::RESPONSE_ALL && - $data['user'] !== $participant->getUser()) { + ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS + || $data['user'] !== $participant->getAttendee()->getActorId())) { $message->setVisibility(false); return; } diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index 1b892c410f1..3a89a96cf32 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -25,6 +25,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Participant; use OCA\Talk\Share\RoomShareProvider; @@ -92,11 +93,15 @@ public function parseMessage(Message $chatMessage): void { $participant = $chatMessage->getParticipant(); if (!$participant->isGuest()) { + $currentActorId = $participant->getAttendee()->getActorId(); $currentUserIsActor = $parsedParameters['actor']['type'] === 'user' && - $participant->getUser() === $parsedParameters['actor']['id']; + $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS && + $currentActorId === $parsedParameters['actor']['id']; } else { + $currentActorId = $participant->getAttendee()->getActorId(); $currentUserIsActor = $parsedParameters['actor']['type'] === 'guest' && - sha1($participant->getSessionId()) === $parsedParameters['actor']['id']; + $participant->getAttendee()->getActorType() === 'guest' && + $participant->getAttendee()->getActorId() === $parsedParameters['actor']['id']; } $cliIsActor = $parsedParameters['actor']['type'] === 'guest' && 'guest/cli' === $parsedParameters['actor']['id']; @@ -197,7 +202,7 @@ public function parseMessage(Message $chatMessage): void { } } elseif ($currentUserIsActor) { $parsedMessage = $this->l->t('You added {user}'); - } elseif (!$participant->isGuest() && $participant->getUser() === $parsedParameters['user']['id']) { + } elseif (!$participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} added you'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator added you'); @@ -217,7 +222,7 @@ public function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('{actor} removed {user}'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You removed {user}'); - } elseif (!$participant->isGuest() && $participant->getUser() === $parsedParameters['user']['id']) { + } elseif (!$participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} removed you'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator removed you'); @@ -231,7 +236,7 @@ public function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('{actor} promoted {user} to moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You promoted {user} to moderator'); - } elseif (!$participant->isGuest() && $participant->getUser() === $parsedParameters['user']['id']) { + } elseif (!$participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} promoted you to moderator'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator promoted you to moderator'); @@ -244,7 +249,7 @@ public function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('{actor} demoted {user} from moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You demoted {user} from moderator'); - } elseif (!$participant->isGuest() && $participant->getUser() === $parsedParameters['user']['id']) { + } elseif (!$participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} demoted you from moderator'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator demoted you from moderator'); @@ -257,7 +262,7 @@ public function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('{actor} promoted {user} to moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You promoted {user} to moderator'); - } elseif ($participant->isGuest() && $participant->getSessionId() === $parsedParameters['user']['id']) { + } elseif ($participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} promoted you to moderator'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator promoted you to moderator'); @@ -270,7 +275,7 @@ public function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('{actor} demoted {user} from moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You demoted {user} from moderator'); - } elseif ($participant->isGuest() && $participant->getSessionId() === $parsedParameters['user']['id']) { + } elseif ($participant->isGuest() && $currentActorId === $parsedParameters['user']['id']) { $parsedMessage = $this->l->t('{actor} demoted you from moderator'); if ($cliIsActor) { $parsedMessage = $this->l->t('An administrator demoted you from moderator'); @@ -326,8 +331,8 @@ protected function getFileFromShare(Participant $participant, string $shareId): $path = $name; if (!$participant->isGuest()) { - if ($share->getShareOwner() !== $participant->getUser()) { - $userFolder = $this->rootFolder->getUserFolder($participant->getUser()); + if ($share->getShareOwner() !== $participant->getAttendee()->getActorId()) { + $userFolder = $this->rootFolder->getUserFolder($participant->getAttendee()->getActorId()); if ($userFolder instanceof Node) { $userNodes = $userFolder->getById($node->getId()); @@ -336,7 +341,7 @@ protected function getFileFromShare(Participant $participant, string $shareId): // 1. Only be executed on "Waiting for new messages" // 2. Once per request \OC_Util::tearDownFS(); - \OC_Util::setupFS($participant->getUser()); + \OC_Util::setupFS($participant->getAttendee()->getActorId()); $userNodes = $userFolder->getById($node->getId()); } @@ -378,7 +383,7 @@ protected function getFileFromShare(Participant $participant, string $shareId): } protected function getActor(IComment $comment): array { - if ($comment->getActorType() === 'guests') { + if ($comment->getActorType() === Attendee::ACTOR_GUESTS) { return $this->getGuest($comment->getActorId()); } diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index 38a20a2de01..688a54e57ad 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -26,6 +26,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Room; use OCP\Comments\ICommentsManager; @@ -109,10 +110,15 @@ public function parseMessage(Message $chatMessage): void { $message = str_replace($placeholder, '{' . $mentionParameterId . '}', $message); if ($mention['type'] === 'call') { + $userId = ''; + if ($chatMessage->getParticipant()->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $userId = $chatMessage->getParticipant()->getAttendee()->getActorId(); + } + $messageParameters[$mentionParameterId] = [ 'type' => $mention['type'], 'id' => $chatMessage->getRoom()->getToken(), - 'name' => $chatMessage->getRoom()->getDisplayName($chatMessage->getParticipant()->getUser()), + 'name' => $chatMessage->getRoom()->getDisplayName($userId), 'call-type' => $this->getRoomType($chatMessage->getRoom()), ]; } elseif ($mention['type'] === 'guest') { diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index cc5db827621..83de21cd201 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -31,8 +31,11 @@ use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\Share\RoomShareProvider; use OCA\Talk\TalkSession; use OCA\Talk\Webinary; @@ -74,17 +77,20 @@ public static function register(IEventDispatcher $dispatcher): void { $room = $event->getRoom(); /** @var self $listener */ $listener = \OC::$server->query(self::class); + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->query(ParticipantService::class); - if ($room->hasSessionsInCall()) { - $listener->sendSystemMessage($room, 'call_joined'); + if ($participantService->hasActiveSessionsInCall($room)) { + $listener->sendSystemMessage($room, 'call_joined', [], $event->getParticipant()); } else { - $listener->sendSystemMessage($room, 'call_started'); + $listener->sendSystemMessage($room, 'call_started', [], $event->getParticipant()); } }); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, static function (ModifyParticipantEvent $event) { $room = $event->getRoom(); - if ($event->getParticipant()->getInCallFlags() === Participant::FLAG_DISCONNECTED) { + $session = $event->getParticipant()->getSession(); + if (!$session instanceof Session) { // This happens in case the user was kicked/lobbied return; } @@ -190,10 +196,14 @@ public static function register(IEventDispatcher $dispatcher): void { /** @var self $listener */ $listener = \OC::$server->query(self::class); foreach ($participants as $participant) { + if ($participant['actorType'] !== 'users') { + continue; + } + $userJoinedFileRoom = $room->getObjectType() === 'file' && (!isset($participant['participantType']) || $participant['participantType'] !== Participant::USER_SELF_JOINED); - if ($userJoinedFileRoom || $userId !== $participant['userId']) { - $listener->sendSystemMessage($room, 'user_added', ['user' => $participant['userId']]); + if ($userJoinedFileRoom || $userId !== $participant['actorId']) { + $listener->sendSystemMessage($room, 'user_added', ['user' => $participant['actorId']]); } } }); @@ -210,23 +220,28 @@ public static function register(IEventDispatcher $dispatcher): void { }); $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, static function (ModifyParticipantEvent $event) { $room = $event->getRoom(); + $attendee = $event->getParticipant()->getAttendee(); + + if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + return; + } if ($event->getNewValue() === Participant::MODERATOR) { /** @var self $listener */ $listener = \OC::$server->query(self::class); - $listener->sendSystemMessage($room, 'moderator_promoted', ['user' => $event->getParticipant()->getUser()]); + $listener->sendSystemMessage($room, 'moderator_promoted', ['user' => $attendee->getActorId()]); } elseif ($event->getNewValue() === Participant::USER) { /** @var self $listener */ $listener = \OC::$server->query(self::class); - $listener->sendSystemMessage($room, 'moderator_demoted', ['user' => $event->getParticipant()->getUser()]); + $listener->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]); } elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) { /** @var self $listener */ $listener = \OC::$server->query(self::class); - $listener->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => sha1($event->getParticipant()->getSessionId())]); + $listener->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]); } elseif ($event->getNewValue() === Participant::GUEST) { /** @var self $listener */ $listener = \OC::$server->query(self::class); - $listener->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => sha1($event->getParticipant()->getSessionId())]); + $listener->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]); } }); $listener = function (GenericEvent $event) { @@ -252,20 +267,18 @@ public static function register(IEventDispatcher $dispatcher): void { protected function sendSystemMessage(Room $room, string $message, array $parameters = [], Participant $participant = null): void { if ($participant instanceof Participant) { - $actorType = $participant->isGuest() ? 'guests' : 'users'; - $sessionId = $participant->getSessionId(); - $sessionHash = $sessionId ? sha1($sessionId) : 'failed-to-get-session'; - $actorId = $participant->isGuest() ? $sessionHash : $participant->getUser(); + $actorType = $participant->getAttendee()->getActorType(); + $actorId = $participant->getAttendee()->getActorId(); } else { $user = $this->userSession->getUser(); if ($user instanceof IUser) { $actorType = 'users'; $actorId = $user->getUID(); } elseif (\OC::$CLI) { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $actorId = 'cli'; } else { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $sessionId = $this->talkSession->getSessionForRoom($room->getToken()); $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session'; } diff --git a/lib/Collaboration/Collaborators/RoomPlugin.php b/lib/Collaboration/Collaborators/RoomPlugin.php index 6d33922f081..e2455caec22 100644 --- a/lib/Collaboration/Collaborators/RoomPlugin.php +++ b/lib/Collaboration/Collaborators/RoomPlugin.php @@ -58,7 +58,7 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $result = ['wide' => [], 'exact' => []]; - $rooms = $this->manager->getRoomsForParticipant($userId); + $rooms = $this->manager->getRoomsForUser($userId); foreach ($rooms as $room) { if ($room->getReadOnly() === Room::READ_ONLY) { // Can not add new shares to read-only rooms diff --git a/lib/Collaboration/Resources/ConversationProvider.php b/lib/Collaboration/Resources/ConversationProvider.php index 4ba797130ed..528cbf3fd2f 100644 --- a/lib/Collaboration/Resources/ConversationProvider.php +++ b/lib/Collaboration/Resources/ConversationProvider.php @@ -86,7 +86,7 @@ public function canAccessResource(IResource $resource, IUser $user = null): bool } try { - $room = $this->manager->getRoomForParticipantByToken( + $room = $this->manager->getRoomForUserByToken( $resource->getId(), $userId ); @@ -94,7 +94,7 @@ public function canAccessResource(IResource $resource, IUser $user = null): bool // Logged in users need to have a regular participant, // before they can do anything with the room. $participant = $room->getParticipant($userId); - return $participant->getParticipantType() !== Participant::USER_SELF_JOINED; + return $participant->getAttendee()->getParticipantType() !== Participant::USER_SELF_JOINED; } catch (RoomNotFoundException $e) { throw new ResourceException('Conversation not found'); } catch (ParticipantNotFoundException $e) { diff --git a/lib/Command/ActiveCalls.php b/lib/Command/ActiveCalls.php index f79e3c8769e..49282d16731 100644 --- a/lib/Command/ActiveCalls.php +++ b/lib/Command/ActiveCalls.php @@ -68,9 +68,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $query = $this->connection->getQueryBuilder(); $query->select($query->func()->count('*', 'num_participants')) - ->from('talk_participants') + ->from('talk_sessions') ->where($query->expr()->gt('in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED))) - ->andWhere($query->expr()->neq('session_id', $query->createNamedParameter('0'))) ->andWhere($query->expr()->gt('last_ping', $query->createNamedParameter(time() - 60))); $result = $query->execute(); diff --git a/lib/Command/Room/TRoomCommand.php b/lib/Command/Room/TRoomCommand.php index a0d85af96bb..b69c4e5e321 100644 --- a/lib/Command/Room/TRoomCommand.php +++ b/lib/Command/Room/TRoomCommand.php @@ -29,8 +29,10 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\RoomService; use OCP\IGroup; use OCP\IGroupManager; @@ -47,6 +49,9 @@ trait TRoomCommand { /** @var RoomService */ protected $roomService; + /** @var ParticipantService */ + protected $participantService; + /** @var IUserManager */ protected $userManager; @@ -55,12 +60,14 @@ trait TRoomCommand { public function __construct(Manager $manager, RoomService $roomService, + ParticipantService $participantService, IUserManager $userManager, IGroupManager $groupManager) { parent::__construct(); $this->manager = $manager; $this->roomService = $roomService; + $this->participantService = $participantService; $this->userManager = $userManager; $this->groupManager = $groupManager; } @@ -109,10 +116,6 @@ protected function setRoomPublic(Room $room, bool $public): void { return; } - if (!$public && $room->hasPassword()) { - throw new InvalidArgumentException('Unable to change password protected public room to private room.'); - } - if (!$room->setType($public ? Room::PUBLIC_CALL : Room::GROUP_CALL)) { throw new InvalidArgumentException('Unable to change room type.'); } @@ -169,7 +172,7 @@ protected function setRoomOwner(Room $room, string $userId): void { $this->unsetRoomOwner($room); - $room->setParticipantType($participant, Participant::OWNER); + $this->participantService->updateParticipantType($room, $participant, Participant::OWNER); } /** @@ -178,9 +181,10 @@ protected function setRoomOwner(Room $room, string $userId): void { * @throws InvalidArgumentException */ protected function unsetRoomOwner(Room $room): void { - foreach ($room->getParticipants() as $participant) { - if ($participant->getParticipantType() === Participant::OWNER) { - $room->setParticipantType($participant, Participant::USER); + $participants = $this->participantService->getParticipantsForRoom($room); + foreach ($participants as $participant) { + if ($participant->getAttendee()->getParticipantType() === Participant::OWNER) { + $this->participantService->updateParticipantType($room, $participant, Participant::USER); } } } @@ -210,7 +214,7 @@ protected function addRoomParticipantsByGroup(Room $room, array $groupIds): void $users = array_merge($users, array_values($groupUsers)); } - $this->addRoomParticipants($room, $users); + $this->addRoomParticipants($room, array_unique($users)); } /** @@ -245,12 +249,13 @@ protected function addRoomParticipants(Room $room, array $userIds): void { // we expect the user not to be a participant yet } - $participants[$user->getUID()] = [ - 'userId' => $user->getUID(), + $participants[] = [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), ]; } - \call_user_func_array([$room, 'addUsers'], $participants); + $this->participantService->addUsers($room, $participants); } /** @@ -272,7 +277,7 @@ protected function removeRoomParticipants(Room $room, array $userIds): void { } foreach ($users as $user) { - $room->removeUser($user, Room::PARTICIPANT_REMOVED); + $this->participantService->removeUser($room, $user, Room::PARTICIPANT_REMOVED); } } @@ -291,13 +296,13 @@ protected function addRoomModerators(Room $room, array $userIds): void { throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId)); } - if ($participant->getParticipantType() !== Participant::OWNER) { + if ($participant->getAttendee()->getParticipantType() !== Participant::OWNER) { $participants[] = $participant; } } foreach ($participants as $participant) { - $room->setParticipantType($participant, Participant::MODERATOR); + $this->participantService->updateParticipantType($room, $participant, Participant::MODERATOR); } } @@ -316,13 +321,13 @@ protected function removeRoomModerators(Room $room, array $userIds): void { throw new InvalidArgumentException(sprintf("User '%s' is no participant.", $userId)); } - if ($participant->getParticipantType() === Participant::MODERATOR) { + if ($participant->getAttendee()->getParticipantType() === Participant::MODERATOR) { $participants[] = $participant; } } foreach ($participants as $participant) { - $room->setParticipantType($participant, Participant::USER); + $this->participantService->updateParticipantType($room, $participant, Participant::USER); } } @@ -371,9 +376,11 @@ protected function completeParticipantValues(CompletionContext $context): array } $users = []; - foreach ($room->searchParticipants($context->getCurrentWord()) as $participant) { - if (!$participant->isGuest()) { - $users[] = $participant->getUser(); + $participants = $this->participantService->getParticipantsForRoom($room); + foreach ($participants as $participant) { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS + && stripos($participant->getAttendee()->getActorId(), $context->getCurrentWord()) !== false) { + $users[] = $participant->getAttendee()->getActorId(); } } diff --git a/lib/Config.php b/lib/Config.php index 6f8abca7ccd..16255dbfb17 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -43,6 +43,9 @@ class Config { /** @var ISecureRandom */ private $secureRandom; + /** @var array */ + protected $canEnableSIP = []; + public function __construct(IConfig $config, ISecureRandom $secureRandom, IGroupManager $groupManager, @@ -62,6 +65,46 @@ public function getAllowedTalkGroupIds(): array { return \is_array($groups) ? $groups : []; } + /** + * @return string[] + */ + public function getSIPGroups(): array { + $groups = $this->config->getAppValue('spreed', 'sip_bridge_groups', '[]'); + $groups = json_decode($groups, true); + return \is_array($groups) ? $groups : []; + } + + public function isSIPConfigured(): bool { + return $this->getSIPSharedSecret() !== '' + && $this->getDialInInfo() !== ''; + } + + public function getDialInInfo(): string { + return $this->config->getAppValue('spreed', 'sip_bridge_dialin_info'); + } + + public function getSIPSharedSecret(): string { + return $this->config->getAppValue('spreed', 'sip_bridge_shared_secret'); + } + + public function canUserEnableSIP(IUser $user): bool { + if (isset($this->canEnableSIP[$user->getUID()])) { + return $this->canEnableSIP[$user->getUID()]; + } + + $this->canEnableSIP[$user->getUID()] = false; + + $allowedGroups = $this->getSIPGroups(); + if (empty($allowedGroups)) { + $this->canEnableSIP[$user->getUID()] = true; + } else { + $userGroups = $this->groupManager->getUserGroupIds($user); + $this->canEnableSIP[$user->getUID()] = !empty(array_intersect($allowedGroups, $userGroups)); + } + + return $this->canEnableSIP[$user->getUID()]; + } + public function isDisabledForUser(IUser $user): bool { $allowedGroups = $this->getAllowedTalkGroupIds(); if (empty($allowedGroups)) { diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 2603922e5c4..85c85e834f6 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -27,7 +27,10 @@ namespace OCA\Talk\Controller; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; @@ -35,13 +38,17 @@ class CallController extends AEnvironmentAwareController { + /** @var ParticipantService */ + private $participantService; /** @var ITimeFactory */ private $timeFactory; public function __construct(string $appName, IRequest $request, + ParticipantService $participantService, ITimeFactory $timeFactory) { parent::__construct($appName, $request); + $this->participantService = $participantService; $this->timeFactory = $timeFactory; } @@ -56,19 +63,38 @@ public function __construct(string $appName, public function getPeersForCall(): DataResponse { $timeout = $this->timeFactory->getTime() - 30; $result = []; - $participants = $this->room->getParticipantsInCall(); + $participants = $this->participantService->getParticipantsInCall($this->room); foreach ($participants as $participant) { - if ($participant->getLastPing() < $timeout) { + /** @var Session $session */ + $session = $participant->getSession(); + if ($session->getLastPing() < $timeout) { // User is not active in call continue; } - $result[] = [ - 'userId' => $participant->getUser(), - 'token' => $this->room->getToken(), - 'lastPing' => $participant->getLastPing(), - 'sessionId' => $participant->getSessionId(), - ]; + if ($this->getAPIVersion() >= 3) { + $result[] = [ + 'actorType' => $participant->getAttendee()->getActorType(), + 'actorId' => $participant->getAttendee()->getActorId(), + // FIXME 'displayName' => $participant->getAttendee()->getDisplayName(), + 'displayName' => $participant->getAttendee()->getActorId(), + 'token' => $this->room->getToken(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]; + } else { + $userId = ''; + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $userId = $participant->getAttendee()->getActorId(); + } + + $result[] = [ + 'userId' => $userId, + 'token' => $this->room->getToken(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]; + } } return new DataResponse($result); @@ -84,10 +110,10 @@ public function getPeersForCall(): DataResponse { * @return DataResponse */ public function joinCall(?int $flags): DataResponse { - $this->room->ensureOneToOneRoomIsFilled(); + $this->participantService->ensureOneToOneRoomIsFilled($this->room); - $sessionId = $this->participant->getSessionId(); - if ($sessionId === '0') { + $session = $this->participant->getSession(); + if (!$session instanceof Session) { return new DataResponse([], Http::STATUS_NOT_FOUND); } @@ -96,7 +122,7 @@ public function joinCall(?int $flags): DataResponse { $flags = Participant::FLAG_IN_CALL | Participant::FLAG_WITH_AUDIO | Participant::FLAG_WITH_VIDEO; } - $this->room->changeInCall($this->participant, $flags); + $this->participantService->changeInCall($this->room, $this->participant, $flags); return new DataResponse(); } @@ -108,12 +134,12 @@ public function joinCall(?int $flags): DataResponse { * @return DataResponse */ public function leaveCall(): DataResponse { - $sessionId = $this->participant->getSessionId(); - if ($sessionId === '0') { + $session = $this->participant->getSession(); + if (!$session instanceof Session) { return new DataResponse([], Http::STATUS_NOT_FOUND); } - $this->room->changeInCall($this->participant, Participant::FLAG_DISCONNECTED); + $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); return new DataResponse(); } diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 6aaf30d8b5f..6e97a747bf0 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -29,9 +29,13 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Chat\MessageParser; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCA\Talk\Service\SessionService; use OCA\Talk\TalkSession; use OCP\App\IAppManager; use OCP\AppFramework\Http; @@ -67,6 +71,12 @@ class ChatController extends AEnvironmentAwareController { /** @var ChatManager */ private $chatManager; + /** @var ParticipantService */ + private $participantService; + + /** @var SessionService */ + private $sessionService; + /** @var GuestManager */ private $guestManager; @@ -88,13 +98,14 @@ class ChatController extends AEnvironmentAwareController { /** @var ISearchResult */ private $searchResult; + /** @var ITimeFactory */ + protected $timeFactory; + /** @var IEventDispatcher */ - private $eventDispatcher; + protected $eventDispatcher; /** @var IL10N */ private $l; - /** @var ITimeFactory */ - protected $timeFactory; public function __construct(string $appName, ?string $UserId, @@ -103,14 +114,16 @@ public function __construct(string $appName, TalkSession $session, IAppManager $appManager, ChatManager $chatManager, + ParticipantService $participantService, + SessionService $sessionService, GuestManager $guestManager, MessageParser $messageParser, IManager $autoCompleteManager, IUserStatusManager $statusManager, SearchPlugin $searchPlugin, ISearchResult $searchResult, - IEventDispatcher $eventDispatcher, ITimeFactory $timeFactory, + IEventDispatcher $eventDispatcher, IL10N $l) { parent::__construct($appName, $request); @@ -119,14 +132,16 @@ public function __construct(string $appName, $this->session = $session; $this->appManager = $appManager; $this->chatManager = $chatManager; + $this->participantService = $participantService; + $this->sessionService = $sessionService; $this->guestManager = $guestManager; $this->messageParser = $messageParser; $this->autoCompleteManager = $autoCompleteManager; $this->statusManager = $statusManager; $this->searchPlugin = $searchPlugin; $this->searchResult = $searchResult; - $this->eventDispatcher = $eventDispatcher; $this->timeFactory = $timeFactory; + $this->eventDispatcher = $eventDispatcher; $this->l = $l; } @@ -151,7 +166,7 @@ public function __construct(string $appName, */ public function sendMessage(string $message, string $actorDisplayName = '', string $referenceId = '', int $replyTo = 0): DataResponse { if ($this->userId === null) { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $sessionId = $this->session->getSessionForRoom($this->room->getToken()); // The character limit for actorId is 64, but the spreed-session is // 256 characters long, so it has to be hashed to get an ID that @@ -164,7 +179,7 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName); } } else { - $actorType = 'users'; + $actorType = Attendee::ACTOR_USERS; $actorId = $this->userId; } @@ -188,7 +203,7 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri } } - $this->room->ensureOneToOneRoomIsFilled(); + $this->participantService->ensureOneToOneRoomIsFilled($this->room); $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC')); try { @@ -206,7 +221,7 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri return new DataResponse([], Http::STATUS_CREATED); } - $this->participant->setLastReadMessage((int) $comment->getId()); + $this->participantService->updateLastReadMessage($this->participant, (int) $comment->getId()); $data = $chatMessage->toArray(); if ($parentMessage instanceof Message) { @@ -267,23 +282,27 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last $limit = min(200, $limit); $timeout = min(30, $timeout); - if ($noStatusUpdate === 0 && $this->participant->getSessionId() !== '0') { + $session = $this->participant->getSession(); + if ($noStatusUpdate === 0 && $session instanceof Session) { // The mobile apps dont do internal signaling unless in a call $isMobileApp = $this->request->isUserAgent([ IRequest::USER_AGENT_TALK_ANDROID, IRequest::USER_AGENT_TALK_IOS, ]); - if ($isMobileApp && $this->participant->getInCallFlags() === Participant::FLAG_DISCONNECTED) { - $this->room->ping($this->participant->getUser(), $this->participant->getSessionId(), $this->timeFactory->getTime()); + if ($isMobileApp && $session->getInCall() === Participant::FLAG_DISCONNECTED) { + $this->sessionService->updateLastPing($session, $this->timeFactory->getTime()); if ($lookIntoFuture) { - // Bump the user status again - $event = new UserLiveStatusEvent( - $this->userManager->get($this->participant->getUser()), - IUserStatus::ONLINE, - $this->timeFactory->getTime() - ); - $this->eventDispatcher->dispatchTyped($event); + $attendee = $this->participant->getAttendee(); + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + // Bump the user status again + $event = new UserLiveStatusEvent( + $this->userManager->get($attendee->getActorId()), + IUserStatus::ONLINE, + $this->timeFactory->getTime() + ); + $this->eventDispatcher->dispatchTyped($event); + } } } } @@ -299,9 +318,11 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last * we only update the read marker to the last known id, when it is higher * then the current read marker. */ + + $attendee = $this->participant->getAttendee(); if ($lookIntoFuture && $setReadMarker === 1 && - $lastKnownMessageId > $this->participant->getLastReadMessage()) { - $this->participant->setLastReadMessage($lastKnownMessageId); + $lastKnownMessageId > $attendee->getLastReadMessage()) { + $this->participantService->updateLastReadMessage($this->participant, $lastKnownMessageId); } $currentUser = $this->userManager->get($this->userId); @@ -393,14 +414,14 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last if ($newLastKnown instanceof IComment) { $response->addHeader('X-Chat-Last-Given', $newLastKnown->getId()); /** - * This false set the read marker on new messages although you + * This falsely set the read marker on new messages although you * navigated away to a different chat already. So we removed this * and instead update the read marker before your next waiting. * So when you are still there, it will just have a wrong read * marker for the time until your next request starts, while it will * not update the value, when you actually left the chat already. * if ($setReadMarker === 1 && $lookIntoFuture) { - * $this->participant->setLastReadMessage((int) $newLastKnown->getId()); + * $this->participantService->updateLastReadMessage($this->participant, (int) $newLastKnown->getId()); * } */ } @@ -416,7 +437,7 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last * @return DataResponse */ public function setReadMarker(int $lastReadMessage): DataResponse { - $this->participant->setLastReadMessage($lastReadMessage); + $this->participantService->updateLastReadMessage($this->participant, $lastReadMessage); return new DataResponse(); } @@ -464,7 +485,9 @@ public function mentions(string $search, int $limit = 20, bool $includeStatus = $results = $this->prepareResultArray($results, $statuses); - $roomDisplayName = $this->room->getDisplayName($this->participant->getUser()); + $attendee = $this->participant->getAttendee(); + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; + $roomDisplayName = $this->room->getDisplayName($userId); if (($search === '' || strpos('all', $search) !== false || stripos($roomDisplayName, $search) !== false) && $this->room->getType() !== Room::ONE_TO_ONE_CALL) { if ($search === '' || stripos($roomDisplayName, $search) === 0 || @@ -502,7 +525,7 @@ protected function prepareResultArray(array $results, array $statuses): array { 'source' => $type, ]; - if ($type === 'users' && isset($statuses[$data['id']])) { + if ($type === Attendee::ACTOR_USERS && isset($statuses[$data['id']])) { $data['status'] = $statuses[$data['id']]->getStatus(); $data['statusIcon'] = $statuses[$data['id']]->getIcon(); $data['statusMessage'] = $statuses[$data['id']]->getMessage(); diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 7174d527800..79b8fe53c93 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -205,7 +205,7 @@ public function index(string $token = '', string $callUser = '', string $passwor // If the room is not a public room, check if the user is in the participants if ($room->getType() !== Room::PUBLIC_CALL) { - $this->manager->getRoomForParticipant($room->getId(), $this->userId); + $this->manager->getRoomForUser($room->getId(), $this->userId); } } catch (RoomNotFoundException $e) { // Room not found, redirect to main page @@ -216,7 +216,7 @@ public function index(string $token = '', string $callUser = '', string $passwor // If the user joined themselves or is not found, they need the password. try { $participant = $room->getParticipant($this->userId); - $requirePassword = $participant->getParticipantType() === Participant::USER_SELF_JOINED; + $requirePassword = $participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED; } catch (ParticipantNotFoundException $e) { $requirePassword = true; } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 2547e4903de..6819d8b6b5d 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -40,9 +40,13 @@ use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\GuestManager; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\RoomService; +use OCA\Talk\Service\SessionService; use OCA\Talk\TalkSession; use OCA\Talk\Webinary; use OCP\App\IAppManager; @@ -79,6 +83,10 @@ class RoomController extends AEnvironmentAwareController { protected $manager; /** @var RoomService */ protected $roomService; + /** @var ParticipantService */ + protected $participantService; + /** @var SessionService */ + protected $sessionService; /** @var GuestManager */ protected $guestManager; /** @var IUserStatusManager */ @@ -107,6 +115,8 @@ public function __construct(string $appName, IGroupManager $groupManager, Manager $manager, RoomService $roomService, + ParticipantService $participantService, + SessionService $sessionService, GuestManager $guestManager, IUserStatusManager $statusManager, ChatManager $chatManager, @@ -124,6 +134,8 @@ public function __construct(string $appName, $this->groupManager = $groupManager; $this->manager = $manager; $this->roomService = $roomService; + $this->participantService = $participantService; + $this->sessionService = $sessionService; $this->guestManager = $guestManager; $this->statusManager = $statusManager; $this->chatManager = $chatManager; @@ -147,6 +159,9 @@ protected function getTalkHashHeader(): array { $this->config->getAppValue('spreed', 'allowed_groups', '') . '#' . $this->config->getAppValue('spreed', 'start_conversations', '') . '#' . $this->config->getAppValue('spreed', 'has_reference_id', '') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_groups', '[]') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_dialin_info') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_shared_secret') . '#' . $this->config->getAppValue('theming', 'cachebuster', '1') )]; } @@ -180,7 +195,8 @@ public function getRooms(int $noStatusUpdate = 0): DataResponse { } } - $rooms = $this->manager->getRoomsForParticipant($this->userId, true); + + $rooms = $this->manager->getRoomsForUser($this->userId, true); $return = []; foreach ($rooms as $room) { @@ -202,7 +218,20 @@ public function getRooms(int $noStatusUpdate = 0): DataResponse { */ public function getSingleRoom(string $token): DataResponse { try { - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId, true); + $isSIPBridgeRequest = $this->validateSIPBridgeRequest($token); + } catch (UnauthorizedException $e) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + + if ($isSIPBridgeRequest && $this->getAPIVersion() < 3) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + // The SIP bridge only needs room details (public, sip enabled, lobby state, etc) + $includeLastMessage = !$isSIPBridgeRequest; + + try { + $room = $this->manager->getRoomForUserByToken($token, $this->userId, $includeLastMessage); $participant = null; try { @@ -214,22 +243,64 @@ public function getSingleRoom(string $token): DataResponse { } } - return new DataResponse($this->formatRoom($room, $participant), Http::STATUS_OK, $this->getTalkHashHeader()); + return new DataResponse($this->formatRoom($room, $participant, $isSIPBridgeRequest), Http::STATUS_OK, $this->getTalkHashHeader()); } catch (RoomNotFoundException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } } /** - * @param string $apiVersion + * Check if the current request is coming from an allowed backend. + * + * The SIP bridge is sending the custom header "Talk-SIPBridge-Random" + * containing at least 32 bytes random data, and the header + * "Talk-SIPBridge-Checksum", which is the SHA256-HMAC of the random data + * and the body of the request, calculated with the shared secret from the + * configuration. + * + * @param string $data + * @return bool True if the request is from the SIP bridge and valid, false if not from SIP bridge + * @throws UnauthorizedException when the request tried to sign as SIP bridge but is not valid + */ + private function validateSIPBridgeRequest(string $data): bool { + $random = $this->request->getHeader('TALK_SIPBRIDGE_RANDOM'); + $checksum = $this->request->getHeader('TALK_SIPBRIDGE_CHECKSUM'); + + if ($random === '' && $checksum === '') { + return false; + } + + if (strlen($random) < 32) { + throw new UnauthorizedException('Invalid random provided'); + } + + if (empty($checksum)) { + throw new UnauthorizedException('Invalid checksum provided'); + } + + $secret = $this->talkConfig->getSIPSharedSecret(); + if (empty($secret)) { + throw new UnauthorizedException('No shared SIP secret provided'); + } + $hash = hash_hmac('sha256', $random . $data, $secret); + + if (hash_equals($hash, strtolower($checksum))) { + return true; + } + + throw new UnauthorizedException('Invalid HMAC provided'); + } + + /** * @param Room $room - * @param Participant $currentParticipant + * @param Participant|null $currentParticipant + * @param bool $isSIPBridgeRequest * @return array * @throws RoomNotFoundException */ - protected function formatRoom(Room $room, ?Participant $currentParticipant): array { - if ($this->getAPIVersion() === 2) { - return $this->formatRoomV2($room, $currentParticipant); + protected function formatRoom(Room $room, ?Participant $currentParticipant, bool $isSIPBridgeRequest = false): array { + if ($this->getAPIVersion() >= 2) { + return $this->formatRoomV2andV3($room, $currentParticipant, $isSIPBridgeRequest); } return $this->formatRoomV1($room, $currentParticipant); @@ -237,7 +308,7 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant): arr /** * @param Room $room - * @param Participant $currentParticipant + * @param Participant|null $currentParticipant * @return array * @throws RoomNotFoundException */ @@ -279,6 +350,9 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a return $roomData; } + $attendee = $currentParticipant->getAttendee(); + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; + $lastActivity = $room->getLastActivity(); if ($lastActivity instanceof \DateTimeInterface) { $lastActivity = $lastActivity->getTimestamp(); @@ -295,25 +369,31 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a $roomData = array_merge($roomData, [ 'name' => $room->getName(), - 'displayName' => $room->getDisplayName($currentParticipant->getUser()), + 'displayName' => $room->getDisplayName($userId), 'objectType' => $room->getObjectType(), 'objectId' => $room->getObjectId(), - 'participantType' => $currentParticipant->getParticipantType(), - // Deprecated, use participantFlags instead. - 'participantInCall' => ($currentParticipant->getInCallFlags() & Participant::FLAG_IN_CALL) !== 0, - 'participantFlags' => $currentParticipant->getInCallFlags(), + 'participantType' => $attendee->getParticipantType(), 'readOnly' => $room->getReadOnly(), 'count' => 0, // Deprecated, remove in future API version 'hasCall' => $room->getActiveSince() instanceof \DateTimeInterface, 'lastActivity' => $lastActivity, - 'isFavorite' => $currentParticipant->isFavorite(), - 'notificationLevel' => $currentParticipant->getNotificationLevel(), + 'isFavorite' => $attendee->isFavorite(), + 'notificationLevel' => $attendee->getNotificationLevel(), 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, - 'lastPing' => $currentParticipant->getLastPing(), - 'sessionId' => $currentParticipant->getSessionId(), ]); + $session = $currentParticipant->getSession(); + if ($session instanceof Session) { + $roomData = array_merge($roomData, [ + // Deprecated, use participantFlags instead. + 'participantInCall' => ($session->getInCall() & Participant::FLAG_IN_CALL) !== 0, + 'participantFlags' => $session->getInCall(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]); + } + if ($roomData['notificationLevel'] === Participant::NOTIFY_DEFAULT) { if ($currentParticipant->isGuest()) { $roomData['notificationLevel'] = Participant::NOTIFY_NEVER; @@ -335,67 +415,76 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a return $roomData; } - $roomData['canStartCall'] = $currentParticipant->canStartCall(); + $roomData['canStartCall'] = $currentParticipant->canStartCall($this->config); + + if ($userId !== '') { + $currentUser = $this->userManager->get($userId); + if ($currentUser instanceof IUser) { + $lastReadMessage = $attendee->getLastReadMessage(); + if ($lastReadMessage === -1) { + /* + * Because the migration from the old comment_read_markers was + * not possible in a programmatic way with a reasonable O(1) or O(n) + * but only with O(user×chat), we do the conversion here. + */ + $lastReadMessage = $this->chatManager->getLastReadMessageFromLegacy($room, $currentUser); + $this->participantService->updateLastReadMessage($currentParticipant, $lastReadMessage); + } + $roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $lastReadMessage); - $currentUser = $this->userManager->get($currentParticipant->getUser()); - if ($currentUser instanceof IUser) { - $lastReadMessage = $currentParticipant->getLastReadMessage(); - if ($lastReadMessage === -1) { - /* - * Because the migration from the old comment_read_markers was - * not possible in a programmatic way with a reasonable O(1) or O(n) - * but only with O(user×chat), we do the conversion here. - */ - $lastReadMessage = $this->chatManager->getLastReadMessageFromLegacy($room, $currentUser); - $currentParticipant->setLastReadMessage($lastReadMessage); + $lastMention = $attendee->getLastMentionMessage(); + $roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention; + $roomData['lastReadMessage'] = $lastReadMessage; } - $roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $lastReadMessage); - - $lastMention = $currentParticipant->getLastMentionMessage(); - $roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention; - $roomData['lastReadMessage'] = $lastReadMessage; } $numActiveGuests = 0; $cleanGuests = false; $participantList = []; - $participants = $room->getParticipants(); + $participants = $this->participantService->getParticipantsForRoom($room); uasort($participants, function (Participant $participant1, Participant $participant2) { - return $participant2->getLastPing() - $participant1->getLastPing(); + $s1 = $participant1->getSession() ? $participant1->getSession()->getLastPing() : 0; + $s2 = $participant2->getSession() ? $participant2->getSession()->getLastPing() : 0; + return $s2 - $s1; }); foreach ($participants as $participant) { + /** @var Participant $participant */ if ($participant->isGuest()) { - if ($participant->getLastPing() <= $this->timeFactory->getTime() - 100) { - $cleanGuests = true; - } else { - $numActiveGuests++; + if ($participant->getSession()) { + if ($participant->getSession()->getLastPing() <= $this->timeFactory->getTime() - 100) { + $cleanGuests = true; + } else { + $numActiveGuests++; + } } - } else { - $user = $this->userManager->get($participant->getUser()); + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $attendee = $participant->getAttendee(); + $session = $participant->getSession(); + $user = $this->userManager->get($attendee->getActorId()); if ($user instanceof IUser) { $participantList[(string)$user->getUID()] = [ 'name' => $user->getDisplayName(), - 'type' => $participant->getParticipantType(), - 'call' => $participant->getInCallFlags(), - 'sessionId' => $participant->getSessionId(), + 'type' => $attendee->getParticipantType(), + 'call' => $session ? $session->getInCall() : Participant::FLAG_DISCONNECTED, + 'sessionId' => $session ? $session->getSessionId() : '0', ]; if ($room->getType() === Room::ONE_TO_ONE_CALL && - $user->getUID() !== $currentParticipant->getUser()) { + $user->getUID() !== $currentParticipant->getAttendee()->getActorId()) { // FIXME This should not be done, but currently all the clients use it to get the avatar of the user … $roomData['name'] = $user->getUID(); } } - if ($participant->getSessionId() !== '0' && $participant->getLastPing() <= $this->timeFactory->getTime() - 100) { - $room->leaveRoom($participant->getUser()); + if ($session && $session->getLastPing() <= $this->timeFactory->getTime() - 100) { + $this->participantService->leaveRoomAsSession($room, $participant); } } } if ($cleanGuests) { - $room->cleanGuestParticipants(); + $this->participantService->cleanGuestParticipants($room); } $lastMessage = $room->getLastMessage(); @@ -416,11 +505,12 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a /** * @param Room $room - * @param Participant $currentParticipant + * @param Participant|null $currentParticipant + * @param bool $isSIPBridgeRequest * @return array * @throws RoomNotFoundException */ - protected function formatRoomV2(Room $room, ?Participant $currentParticipant): array { + protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipant, bool $isSIPBridgeRequest = false): array { $roomData = [ 'id' => $room->getId(), 'token' => $room->getToken(), @@ -450,9 +540,15 @@ protected function formatRoomV2(Room $room, ?Participant $currentParticipant): a 'guestList' => '', 'lastMessage' => [], ]; - - if (!$currentParticipant instanceof Participant) { - return $roomData; + if ($this->getAPIVersion() >= 3) { + $roomData = array_merge($roomData, [ + 'sipEnabled' => Webinary::SIP_DISABLED, + 'actorType' => '', + 'actorId' => '', + 'attendeeId' => 0, + 'canEnableSIP' => false, + 'attendeePin' => '', + ]); } $lastActivity = $room->getLastActivity(); @@ -469,23 +565,68 @@ protected function formatRoomV2(Room $room, ?Participant $currentParticipant): a $lobbyTimer = 0; } + if ($isSIPBridgeRequest) { + return array_merge($roomData, [ + 'name' => $room->getName(), + 'displayName' => $room->getDisplayName(''), + 'objectType' => $room->getObjectType(), + 'objectId' => $room->getObjectId(), + 'readOnly' => $room->getReadOnly(), + 'hasCall' => $room->getActiveSince() instanceof \DateTimeInterface, + 'lastActivity' => $lastActivity, + 'lobbyState' => $room->getLobbyState(), + 'lobbyTimer' => $lobbyTimer, + 'sipEnabled' => $room->getSIPEnabled(), + ]); + } + + if (!$currentParticipant instanceof Participant) { + return $roomData; + } + + $attendee = $currentParticipant->getAttendee(); + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; + $roomData = array_merge($roomData, [ 'name' => $room->getName(), - 'displayName' => $room->getDisplayName($currentParticipant->getUser()), + 'displayName' => $room->getDisplayName($userId), 'objectType' => $room->getObjectType(), 'objectId' => $room->getObjectId(), - 'participantType' => $currentParticipant->getParticipantType(), - 'participantFlags' => $currentParticipant->getInCallFlags(), + 'participantType' => $attendee->getParticipantType(), 'readOnly' => $room->getReadOnly(), 'hasCall' => $room->getActiveSince() instanceof \DateTimeInterface, 'lastActivity' => $lastActivity, - 'isFavorite' => $currentParticipant->isFavorite(), - 'notificationLevel' => $currentParticipant->getNotificationLevel(), + 'isFavorite' => $attendee->isFavorite(), + 'notificationLevel' => $attendee->getNotificationLevel(), 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, - 'lastPing' => $currentParticipant->getLastPing(), - 'sessionId' => $currentParticipant->getSessionId(), ]); + if ($this->getAPIVersion() >= 3) { + if ($this->talkConfig->isSIPConfigured()) { + $roomData['sipEnabled'] = $room->getSIPEnabled(); + if ($room->getSIPEnabled() === Webinary::SIP_ENABLED) { + // Generate a PIN if the attendee is a user and doesn't have one. + $this->participantService->generatePinForParticipant($room, $currentParticipant); + + $roomData['attendeePin'] = $attendee->getPin(); + } + } + + $roomData = array_merge($roomData, [ + 'actorType' => $attendee->getActorType(), + 'actorId' => $attendee->getActorId(), + 'attendeeId' => $attendee->getId(), + ]); + } + + $session = $currentParticipant->getSession(); + if ($session instanceof Session) { + $roomData = array_merge($roomData, [ + 'participantFlags' => $session->getInCall(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]); + } if ($roomData['notificationLevel'] === Participant::NOTIFY_DEFAULT) { if ($currentParticipant->isGuest()) { @@ -508,42 +649,52 @@ protected function formatRoomV2(Room $room, ?Participant $currentParticipant): a return $roomData; } - $roomData['canStartCall'] = $currentParticipant->canStartCall(); + $roomData['canStartCall'] = $currentParticipant->canStartCall($this->config); + + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + $currentUser = $this->userManager->get($attendee->getActorId()); + if ($currentUser instanceof IUser) { + $lastReadMessage = $attendee->getLastReadMessage(); + if ($lastReadMessage === -1) { + /* + * Because the migration from the old comment_read_markers was + * not possible in a programmatic way with a reasonable O(1) or O(n) + * but only with O(user×chat), we do the conversion here. + */ + $lastReadMessage = $this->chatManager->getLastReadMessageFromLegacy($room, $currentUser); + $this->participantService->updateLastReadMessage($currentParticipant, $lastReadMessage); + } + if ($room->getLastMessage() && $lastReadMessage === (int) $room->getLastMessage()->getId()) { + // When the last message is the last read message, there are no unread messages, + // so we can save the query. + $roomData['unreadMessages'] = 0; + } else { + $roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $lastReadMessage); + } - $currentUser = $this->userManager->get($currentParticipant->getUser()); - if ($currentUser instanceof IUser) { - $lastReadMessage = $currentParticipant->getLastReadMessage(); - if ($lastReadMessage === -1) { - /* - * Because the migration from the old comment_read_markers was - * not possible in a programmatic way with a reasonable O(1) or O(n) - * but only with O(user×chat), we do the conversion here. - */ - $lastReadMessage = $this->chatManager->getLastReadMessageFromLegacy($room, $currentUser); - $currentParticipant->setLastReadMessage($lastReadMessage); - } - if ($room->getLastMessage() && $lastReadMessage === (int) $room->getLastMessage()->getId()) { - // When the last message is the last read message, there are no unread messages, - // so we can save the query. - $roomData['unreadMessages'] = 0; - } else { - $roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $lastReadMessage); + $lastMention = $attendee->getLastMentionMessage(); + $roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention; + $roomData['lastReadMessage'] = $lastReadMessage; + + $roomData['canDeleteConversation'] = $room->getType() !== Room::ONE_TO_ONE_CALL + && $currentParticipant->hasModeratorPermissions(false); + $roomData['canLeaveConversation'] = true; + if ($this->getAPIVersion() >= 3) { + $roomData['canEnableSIP'] = + $this->talkConfig->isSIPConfigured() + && !preg_match(Room::SIP_INCOMPATIBLE_REGEX, $room->getToken()) + && ($room->getType() === Room::GROUP_CALL || $room->getType() === Room::PUBLIC_CALL) + && $currentParticipant->hasModeratorPermissions(false) + && $this->talkConfig->canUserEnableSIP($currentUser); + } } - - $lastMention = $currentParticipant->getLastMentionMessage(); - $roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention; - $roomData['lastReadMessage'] = $lastReadMessage; - - $roomData['canDeleteConversation'] = $room->getType() !== Room::ONE_TO_ONE_CALL - && $currentParticipant->hasModeratorPermissions(false); - $roomData['canLeaveConversation'] = true; } // FIXME This should not be done, but currently all the clients use it to get the avatar of the user … if ($room->getType() === Room::ONE_TO_ONE_CALL) { $participants = json_decode($room->getName(), true); foreach ($participants as $participant) { - if ($participant !== $currentParticipant->getUser()) { + if ($participant !== $attendee->getActorId()) { $roomData['name'] = $participant; } } @@ -638,7 +789,7 @@ protected function createOneToOneRoom(string $targetUserId): DataResponse { // We are only doing this manually here to be able to return different status codes // Actually createOneToOneConversation also checks it. $room = $this->manager->getOne2OneRoom($currentUser->getUID(), $targetUser->getUID()); - $room->ensureOneToOneRoomIsFilled(); + $this->participantService->ensureOneToOneRoomIsFilled($room); return new DataResponse( $this->formatRoom($room, $room->getParticipant($currentUser->getUID())), Http::STATUS_OK @@ -692,11 +843,12 @@ protected function createGroupRoom(string $targetGroupName): DataResponse { } $participants[] = [ - 'userId' => $user->getUID(), + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), ]; } - \call_user_func_array([$room, 'addUsers'], $participants); + $this->participantService->addUsers($room, $participants); return new DataResponse($this->formatRoom($room, $room->getParticipant($currentUser->getUID())), Http::STATUS_CREATED); } @@ -749,11 +901,12 @@ protected function createCircleRoom(string $targetCircleId): DataResponse { } $participants[] = [ - 'userId' => $member->getUserId(), + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $member->getUserId(), ]; } - \call_user_func_array([$room, 'addUsers'], $participants); + $this->participantService->addUsers($room, $participants); return new DataResponse($this->formatRoom($room, $room->getParticipant($currentUser->getUID())), Http::STATUS_CREATED); } @@ -790,7 +943,7 @@ protected function createEmptyRoom(string $roomName, bool $public = true): DataR * @return DataResponse */ public function addToFavorites(): DataResponse { - $this->participant->setFavorite(true); + $this->participantService->updateFavoriteStatus($this->participant, true); return new DataResponse([]); } @@ -801,7 +954,7 @@ public function addToFavorites(): DataResponse { * @return DataResponse */ public function removeFromFavorites(): DataResponse { - $this->participant->setFavorite(false); + $this->participantService->updateFavoriteStatus($this->participant, false); return new DataResponse([]); } @@ -813,7 +966,9 @@ public function removeFromFavorites(): DataResponse { * @return DataResponse */ public function setNotificationLevel(int $level): DataResponse { - if (!$this->participant->setNotificationLevel($level)) { + try { + $this->participantService->updateNotificationLevel($this->participant, $level); + } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -867,69 +1022,116 @@ public function deleteRoom(): DataResponse { * @return DataResponse */ public function getParticipants(bool $includeStatus = false): DataResponse { - if ($this->participant->getParticipantType() === Participant::GUEST) { + if ($this->participant->getAttendee()->getParticipantType() === Participant::GUEST) { return new DataResponse([], Http::STATUS_FORBIDDEN); } $maxPingAge = $this->timeFactory->getTime() - 100; - $participants = $this->room->getParticipantsLegacy(); + $participants = $this->participantService->getParticipantsForRoom($this->room); $results = $headers = $statuses = []; if ($this->userId !== null && $includeStatus - && count($participants['users']) < 100 + && count($participants) < 100 && $this->appManager->isEnabledForUser('user_status')) { - $userIds = array_map('strval', array_keys($participants['users'])); + $userIds = array_filter(array_map(static function (Participant $participant) { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + return $participant->getAttendee()->getActorId(); + } + return null; + }, $participants)); + $statuses = $this->statusManager->getUserStatuses($userIds); $headers['X-Nextcloud-Has-User-Statuses'] = true; } - foreach ($participants['users'] as $userId => $participant) { - $userId = (string) $userId; - if ($participant['sessionId'] !== '0' && $participant['lastPing'] <= $maxPingAge) { - $this->room->leaveRoom($userId); + $guestSessions = array_filter(array_map(static function (Participant $participant) { + $session = $participant->getSession(); + if (!$session || $participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { + return null; } - $user = $this->userManager->get($userId); - if (!$user instanceof IUser) { - continue; - } + return sha1($session->getSessionId()); + }, $participants)); - $participant['userId'] = $userId; - $participant['displayName'] = (string) $user->getDisplayName(); + $cleanGuests = false; + $guestNames = $this->guestManager->getNamesBySessionHashes($guestSessions); - if (isset($statuses[$userId])) { - $participant['status'] = $statuses[$userId]->getStatus(); - $participant['statusIcon'] = $statuses[$userId]->getIcon(); - $participant['statusMessage'] = $statuses[$userId]->getMessage(); - $participant['statusClearAt'] = $statuses[$userId]->getClearAt(); + /** @var Participant[] $participants */ + foreach ($participants as $participant) { + $result = [ + 'inCall' => Participant::FLAG_DISCONNECTED, + 'lastPing' => 0, + 'sessionId' => '0', // FIXME empty string or null? + 'participantType' => $participant->getAttendee()->getParticipantType(), + ]; + if ($this->getAPIVersion() >= 3) { + $result['attendeeId'] = $participant->getAttendee()->getId(); + $result['actorId'] = $participant->getAttendee()->getActorId(); + $result['actorType'] = $participant->getAttendee()->getActorType(); + $result['attendeePin'] = ''; + if ($this->talkConfig->isSIPConfigured() + && $this->room->getSIPEnabled() === Webinary::SIP_ENABLED + && ($this->participant->hasModeratorPermissions(false) + || $this->participant->getAttendee()->getId() === $participant->getAttendee()->getId())) { + // Generate a PIN if the attendee is a user and doesn't have one. + $this->participantService->generatePinForParticipant($this->room, $participant); + + $result['attendeePin'] = (string) $participant->getAttendee()->getPin(); + } + } + if ($participant->getSession() instanceof Session) { + $result['inCall'] = $participant->getSession()->getInCall(); + $result['lastPing'] = $participant->getSession()->getLastPing(); + $result['sessionId'] = $participant->getSession()->getSessionId(); } - $results[] = $participant; - } + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $userId = $participant->getAttendee()->getActorId(); + $user = $this->userManager->get($userId); + if (!$user instanceof IUser) { + continue; + } - $guestSessions = []; - foreach ($participants['guests'] as $participant) { - $guestSessions[] = sha1($participant['sessionId']); - } - $guestNames = $this->guestManager->getNamesBySessionHashes($guestSessions); + if ($result['lastPing'] > 0 && $result['lastPing'] <= $maxPingAge) { + $this->participantService->leaveRoomAsSession($this->room, $participant); + } - $cleanGuests = false; - foreach ($participants['guests'] as $participant) { - if ($participant['lastPing'] <= $maxPingAge) { - $cleanGuests = true; + if ($this->getAPIVersion() < 3) { + $result['userId'] = $participant->getAttendee()->getActorId(); + } + $result['displayName'] = (string) $user->getDisplayName(); + + if (isset($statuses[$userId])) { + $result['status'] = $statuses[$userId]->getStatus(); + $result['statusIcon'] = $statuses[$userId]->getIcon(); + $result['statusMessage'] = $statuses[$userId]->getMessage(); + $result['statusClearAt'] = $statuses[$userId]->getClearAt(); + } + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) { + if ($result['lastPing'] <= $maxPingAge) { + $cleanGuests = true; + continue; + } + + if ($this->getAPIVersion() < 3) { + $result['userId'] = ''; + } + $result['displayName'] = $guestNames[$participant->getAttendee()->getActorId()] ?? ''; + } elseif ($this->getAPIVersion() >= 3) { + // Other types are only reported on v3 or later + $result['displayName'] = $participant->getAttendee()->getActorId(); + } else { + // Skip unknown actor types + continue; } - $sessionHash = sha1($participant['sessionId']); - $results[] = array_merge($participant, [ - 'userId' => '', - 'displayName' => $guestNames[$sessionHash] ?? '', - ]); + $results[] = $result; } if ($cleanGuests) { - $this->room->cleanGuestParticipants(); + $this->participantService->cleanGuestParticipants($this->room); } return new DataResponse($results, Http::STATUS_OK, $headers); @@ -948,7 +1150,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u return new DataResponse([], Http::STATUS_BAD_REQUEST); } - $participants = $this->room->getParticipantUserIds(); + $participants = $this->participantService->getParticipantUserIds($this->room); $participantsToAdd = []; if ($source === 'users') { @@ -961,9 +1163,10 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u return new DataResponse([]); } - $this->room->addUsers([ - 'userId' => $newUser->getUID(), - ]); + $this->participantService->addUsers($this->room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $newUser->getUID(), + ]]); } elseif ($source === 'groups') { $group = $this->groupManager->get($newParticipant); if (!$group instanceof IGroup) { @@ -977,7 +1180,8 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $participantsToAdd[] = [ - 'userId' => $user->getUID(), + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), ]; } @@ -985,7 +1189,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u return new DataResponse([]); } - \call_user_func_array([$this->room, 'addUsers'], $participantsToAdd); + $this->participantService->addUsers($this->room, $participantsToAdd); } elseif ($source === 'circles') { if (!$this->appManager->isEnabledForUser('circles')) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -1015,7 +1219,8 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $participantsToAdd[] = [ - 'userId' => $member->getUserId(), + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $member->getUserId(), ]; } @@ -1023,14 +1228,16 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u return new DataResponse([]); } - \call_user_func_array([$this->room, 'addUsers'], $participantsToAdd); + $this->participantService->addUsers($this->room, $participantsToAdd); } elseif ($source === 'emails') { $data = []; if ($this->room->setType(Room::PUBLIC_CALL)) { $data = ['type' => $this->room->getType()]; } - $this->guestManager->inviteByEmail($this->room, $newParticipant); + $participant = $this->participantService->inviteEmailAddress($this->room, $newParticipant); + + $this->guestManager->sendEmailInvitation($this->room, $participant); return new DataResponse($data); } else { @@ -1048,7 +1255,8 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u * @return DataResponse */ public function removeParticipantFromRoom(string $participant): DataResponse { - if ($this->participant->getUser() === $participant) { + $attendee = $this->participant->getAttendee(); + if ($attendee->getActorType() === Attendee::ACTOR_USERS && $attendee->getActorId() === $participant) { // Removing self, abusing moderator power return $this->removeSelfFromRoomLogic($this->room, $this->participant); } @@ -1067,7 +1275,7 @@ public function removeParticipantFromRoom(string $participant): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } - if ($targetParticipant->getParticipantType() === Participant::OWNER) { + if ($targetParticipant->getAttendee()->getParticipantType() === Participant::OWNER) { return new DataResponse([], Http::STATUS_FORBIDDEN); } @@ -1076,7 +1284,7 @@ public function removeParticipantFromRoom(string $participant): DataResponse { return new DataResponse([], Http::STATUS_NOT_FOUND); } - $this->room->removeUser($targetUser, Room::PARTICIPANT_REMOVED); + $this->participantService->removeUser($this->room, $targetUser, Room::PARTICIPANT_REMOVED); return new DataResponse([]); } @@ -1093,25 +1301,25 @@ public function removeSelfFromRoom(): DataResponse { protected function removeSelfFromRoomLogic(Room $room, Participant $participant): DataResponse { if ($room->getType() !== Room::ONE_TO_ONE_CALL) { if ($participant->hasModeratorPermissions(false) - && $room->getNumberOfParticipants() > 1 - && $room->getNumberOfModerators() === 1) { + && $this->participantService->getNumberOfUsers($room) > 1 + && $this->participantService->getNumberOfModerators($room) === 1) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } } if ($room->getType() !== Room::CHANGELOG_CONVERSATION && $room->getObjectType() !== 'file' && - $room->getNumberOfParticipants() === 1) { + $this->participantService->getNumberOfUsers($room) === 1) { $room->deleteRoom(); return new DataResponse(); } - $currentUser = $this->userManager->get($participant->getUser()); + $currentUser = $this->userManager->get($this->userId); if (!$currentUser instanceof IUser) { return new DataResponse([], Http::STATUS_NOT_FOUND); } - $room->removeUser($currentUser, Room::PARTICIPANT_LEFT); + $this->participantService->removeUser($room, $currentUser, Room::PARTICIPANT_LEFT); return new DataResponse(); } @@ -1134,11 +1342,46 @@ public function removeGuestFromRoom(string $participant): DataResponse { return new DataResponse([], Http::STATUS_BAD_REQUEST); } - if ($targetParticipant->getSessionId() === $this->participant->getSessionId()) { + $targetSession = $targetParticipant->getSession(); + $currentSession = $this->participant->getSession(); + if ($targetSession instanceof Session + && $currentSession instanceof Session + && $targetSession->getSessionId() === $currentSession->getSessionId()) { return new DataResponse([], Http::STATUS_FORBIDDEN); } - $this->room->removeParticipantBySession($targetParticipant, Room::PARTICIPANT_REMOVED); + $this->participantService->removeAttendee($this->room, $targetParticipant, Room::PARTICIPANT_REMOVED); + return new DataResponse([]); + } + + /** + * @PublicPage + * @RequireModeratorParticipant + * + * @param int $attendeeId + * @return DataResponse + */ + public function removeAttendeeFromRoom(int $attendeeId): DataResponse { + try { + $targetParticipant = $this->room->getParticipantByAttendeeId($attendeeId); + } catch (ParticipantNotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if ($this->room->getType() === Room::ONE_TO_ONE_CALL) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + if ($this->participant->getAttendee()->getId() === $targetParticipant->getAttendee()->getId()) { + // FIXME switch to removeSelfFromRoomLogic() + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + if ($targetParticipant->getAttendee()->getParticipantType() === Participant::OWNER) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + $this->participantService->removeAttendee($this->room, $targetParticipant, Room::PARTICIPANT_REMOVED); return new DataResponse([]); } @@ -1212,53 +1455,55 @@ public function setPassword(string $password): DataResponse { */ public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse { try { - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); + $room = $this->manager->getRoomForUserByToken($token, $this->userId); } catch (RoomNotFoundException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } + /** @var Participant|null $previousSession */ + $previousParticipant = null; + /** @var Session|null $previousSession */ $previousSession = null; if ($this->userId !== null) { try { - $previousSession = $room->getParticipant($this->userId); + $previousParticipant = $room->getParticipant($this->userId); + $previousSession = $previousParticipant->getSession(); } catch (ParticipantNotFoundException $e) { } } else { - $session = $this->session->getSessionForRoom($token); + $sessionForToken = $this->session->getSessionForRoom($token); try { - $previousSession = $room->getParticipantBySession($session); + $previousParticipant = $room->getParticipantBySession($sessionForToken); + $previousSession = $previousParticipant->getSession(); } catch (ParticipantNotFoundException $e) { } } - if ($previousSession instanceof Participant && $previousSession->getSessionId() !== '0') { - if ($force === false && $previousSession->getInCallFlags() !== Participant::FLAG_DISCONNECTED) { + if ($previousSession instanceof Session && $previousSession->getSessionId() !== '0') { + if ($force === false && $previousSession->getInCall() !== Participant::FLAG_DISCONNECTED) { // Previous session was active in the call, show a warning return new DataResponse([ 'sessionId' => $previousSession->getSessionId(), - 'inCall' => $previousSession->getInCallFlags(), + 'inCall' => $previousSession->getInCall(), 'lastPing' => $previousSession->getLastPing(), ], Http::STATUS_CONFLICT); } - if ($previousSession->getInCallFlags() !== Participant::FLAG_DISCONNECTED) { - $room->changeInCall($previousSession, Participant::FLAG_DISCONNECTED); + if ($previousSession->getInCall() !== Participant::FLAG_DISCONNECTED) { + $this->participantService->changeInCall($room, $previousParticipant, Participant::FLAG_DISCONNECTED); } - if ($this->userId === null) { - $room->removeParticipantBySession($previousSession, Room::PARTICIPANT_LEFT); - } else { - $room->leaveRoomAsParticipant($previousSession); - } + $this->participantService->leaveRoomAsSession($room, $previousParticipant); } $user = $this->userManager->get($this->userId); try { $result = $room->verifyPassword((string) $this->session->getPasswordForRoom($token)); if ($user instanceof IUser) { - $newSessionId = $room->joinRoom($user, $password, $result['result']); + $participant = $this->participantService->joinRoom($room, $user, $password, $result['result']); + $this->participantService->generatePinForParticipant($room, $participant); } else { - $newSessionId = $room->joinRoomGuest($password, $result['result']); + $participant = $this->participantService->joinRoomAsNewGuest($room, $password, $result['result']); } } catch (InvalidPasswordException $e) { return new DataResponse([], Http::STATUS_FORBIDDEN); @@ -1267,11 +1512,38 @@ public function joinRoom(string $token, string $password = '', bool $force = tru } $this->session->removePasswordForRoom($token); - $this->session->setSessionForRoom($token, $newSessionId); - $room->ping($this->userId, $newSessionId, $this->timeFactory->getTime()); - $currentParticipant = $room->getParticipantBySession($newSessionId); + $session = $participant->getSession(); + if ($session instanceof Session) { + $this->session->setSessionForRoom($token, $session->getSessionId()); + $this->sessionService->updateLastPing($session, $this->timeFactory->getTime()); + } + + return new DataResponse($this->formatRoom($room, $participant)); + } + + /** + * @PublicPage + * @RequireRoom + * + * @param string $pin + * @return DataResponse + */ + public function getParticipantByDialInPin(string $pin): DataResponse { + try { + if (!$this->validateSIPBridgeRequest($this->room->getToken())) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + } catch (UnauthorizedException $e) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + + try { + $participant = $this->room->getParticipantByPin($pin); + } catch (ParticipantNotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } - return new DataResponse($this->formatRoom($room, $currentParticipant)); + return new DataResponse($this->formatRoom($this->room, $participant)); } /** @@ -1286,15 +1558,9 @@ public function leaveRoom(string $token): DataResponse { $this->session->removeSessionForRoom($token); try { - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); - - if ($this->userId === null) { - $participant = $room->getParticipantBySession($sessionId); - $room->removeParticipantBySession($participant, Room::PARTICIPANT_LEFT); - } else { - $participant = $room->getParticipant($this->userId); - $room->leaveRoomAsParticipant($participant); - } + $room = $this->manager->getRoomForUserByToken($token, $this->userId); + $participant = $room->getParticipantBySession($sessionId); + $this->participantService->leaveRoomAsSession($room, $participant); } catch (RoomNotFoundException $e) { } catch (ParticipantNotFoundException $e) { } @@ -1306,103 +1572,151 @@ public function leaveRoom(string $token): DataResponse { * @PublicPage * @RequireModeratorParticipant * + * @param int|null $attendeeId * @param string|null $participant * @param string|null $sessionId * @return DataResponse */ - public function promoteModerator(?string $participant, ?string $sessionId): DataResponse { - if ($participant !== null) { - return $this->promoteUserToModerator($this->room, $participant); - } + public function promoteModerator(?int $attendeeId, ?string $participant, ?string $sessionId): DataResponse { + return $this->changeParticipantType($attendeeId, $participant, $sessionId, true); + } - return $this->promoteGuestToModerator($this->room, $sessionId); + /** + * @PublicPage + * @RequireModeratorParticipant + * + * @param int|null $attendeeId + * @param string|null $participant + * @param string|null $sessionId + * @return DataResponse + */ + public function demoteModerator(?int $attendeeId, ?string $participant, ?string $sessionId): DataResponse { + return $this->changeParticipantType($attendeeId, $participant, $sessionId, false); } - protected function promoteUserToModerator(Room $room, string $participant): DataResponse { + /** + * Toggle a user/guest to moderator/guest-moderator or vice-versa based on + * attendeeId (v3) or userId/sessionId (v1+v2) + * + * @param int|null $attendeeId + * @param string|null $userId + * @param string|null $sessionId + * @param bool $promote Shall the attendee be promoted or demoted + * @return DataResponse + */ + protected function changeParticipantType(?int $attendeeId, ?string $userId, ?string $sessionId, bool $promote): DataResponse { try { - $targetParticipant = $room->getParticipant($participant); + if ($attendeeId !== null) { + $targetParticipant = $this->room->getParticipantByAttendeeId($attendeeId); + } elseif ($userId !== null) { + $targetParticipant = $this->room->getParticipant($userId); + } else { + $targetParticipant = $this->room->getParticipantBySession($sessionId); + } } catch (ParticipantNotFoundException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } - if ($targetParticipant->getParticipantType() !== Participant::USER) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); - } + $attendee = $targetParticipant->getAttendee(); - $room->setParticipantType($targetParticipant, Participant::MODERATOR); + // Prevent users/moderators modifying themselves + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + if ($attendee->getActorId() === $this->userId) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + } elseif ($attendee->getActorType() === Attendee::ACTOR_GUESTS) { + $session = $targetParticipant->getSession(); + $currentSessionId = $this->session->getSessionForRoom($this->room->getToken()); - return new DataResponse(); - } + if ($session instanceof Session && $currentSessionId === $session->getSessionId()) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + } - protected function promoteGuestToModerator(Room $room, string $sessionId): DataResponse { - try { - $targetParticipant = $room->getParticipantBySession($sessionId); - } catch (ParticipantNotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + if ($promote === $targetParticipant->hasModeratorPermissions()) { + // Prevent concurrent changes + return new DataResponse([], Http::STATUS_BAD_REQUEST); } - if ($targetParticipant->getParticipantType() !== Participant::GUEST) { + if ($attendee->getParticipantType() === Participant::USER) { + $newType = Participant::MODERATOR; + } elseif ($attendee->getParticipantType() === Participant::GUEST) { + $newType = Participant::GUEST_MODERATOR; + } elseif ($attendee->getParticipantType() === Participant::MODERATOR) { + $newType = Participant::USER; + } elseif ($attendee->getParticipantType() === Participant::GUEST_MODERATOR) { + $newType = Participant::GUEST; + } else { return new DataResponse([], Http::STATUS_BAD_REQUEST); } - $room->setParticipantType($targetParticipant, Participant::GUEST_MODERATOR); + $this->participantService->updateParticipantType($this->room, $targetParticipant, $newType); return new DataResponse(); } /** - * @PublicPage + * @NoAdminRequired * @RequireModeratorParticipant * - * @param string|null $participant - * @param string|null $sessionId + * @param int $state + * @param int|null $timer * @return DataResponse */ - public function demoteModerator(?string $participant, ?string $sessionId): DataResponse { - if ($participant !== null) { - return $this->demoteUserFromModerator($this->room, $participant); + public function setLobby(int $state, ?int $timer = null): DataResponse { + $timerDateTime = null; + if ($timer !== null && $timer > 0) { + try { + $timerDateTime = $this->timeFactory->getDateTime('@' . $timer); + $timerDateTime->setTimezone(new \DateTimeZone('UTC')); + } catch (\Exception $e) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } } - return $this->demoteGuestFromModerator($this->room, $sessionId); - } - - protected function demoteUserFromModerator(Room $room, string $participant): DataResponse { - if ($this->userId === $participant) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + if (!$this->room->setLobby($state, $timerDateTime)) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); } - try { - $targetParticipant = $room->getParticipant($participant); - } catch (ParticipantNotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); - } + if ($state === Webinary::LOBBY_NON_MODERATORS) { + $participants = $this->participantService->getParticipantsInCall($this->room); + foreach ($participants as $participant) { + if ($participant->hasModeratorPermissions()) { + continue; + } - if ($targetParticipant->getParticipantType() !== Participant::MODERATOR) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + $this->participantService->changeInCall($this->room, $participant, Participant::FLAG_DISCONNECTED); + } } - $room->setParticipantType($targetParticipant, Participant::USER); - - return new DataResponse(); + return new DataResponse($this->formatRoomV2andV3($this->room, $this->participant)); } - protected function demoteGuestFromModerator(Room $room, string $sessionId): DataResponse { - if ($this->session->getSessionForRoom($room->getToken()) === $sessionId) { + /** + * @NoAdminRequired + * @RequireModeratorParticipant + * + * @param int $state + * @return DataResponse + */ + public function setSIPEnabled(int $state): DataResponse { + $user = $this->userManager->get($this->userId); + if (!$user instanceof IUser) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + + if (!$this->talkConfig->canUserEnableSIP($user)) { return new DataResponse([], Http::STATUS_FORBIDDEN); } - try { - $targetParticipant = $room->getParticipantBySession($sessionId); - } catch (ParticipantNotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + if (!$this->talkConfig->isSIPConfigured()) { + return new DataResponse([], Http::STATUS_PRECONDITION_FAILED); } - if ($targetParticipant->getParticipantType() !== Participant::GUEST_MODERATOR) { + if (!$this->room->setSIPEnabled($state)) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } - $room->setParticipantType($targetParticipant, Participant::GUEST); - - return new DataResponse(); + return new DataResponse($this->formatRoomV2andV3($this->room, $this->participant)); } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 1124456e5ff..d8eb3b4e3ba 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -34,6 +34,8 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -43,6 +45,8 @@ class SettingsController extends OCSController { protected $rootFolder; /** @var IConfig */ protected $config; + /** @var IGroupManager */ + protected $groupManager; /** @var LoggerInterface */ protected $logger; /** @var string|null */ @@ -52,11 +56,13 @@ public function __construct(string $appName, IRequest $request, IRootFolder $rootFolder, IConfig $config, + IGroupManager $groupManager, LoggerInterface $logger, ?string $userId) { parent::__construct($appName, $request); $this->rootFolder = $rootFolder; $this->config = $config; + $this->groupManager = $groupManager; $this->logger = $logger; $this->userId = $userId; } @@ -99,4 +105,29 @@ protected function validateUserSetting(string $setting, ?string $value): bool { return false; } + + /** + * @param string[] $sipGroups + * @param string $dialInInfo + * @param string $sharedSecret + * @return DataResponse + */ + public function setSIPSettings( + array $sipGroups = [], + string $dialInInfo = '', + string $sharedSecret = ''): DataResponse { + $groups = []; + foreach ($sipGroups as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + $groups[] = $group->getGID(); + } + } + + $this->config->setAppValue('spreed', 'sip_bridge_groups', json_encode($groups)); + $this->config->setAppValue('spreed', 'sip_bridge_dialin_info', $dialInInfo); + $this->config->setAppValue('spreed', 'sip_bridge_shared_secret', $sharedSecret); + + return new DataResponse(); + } } diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 4f75a818f74..fc315158fba 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -25,13 +25,18 @@ namespace OCA\Talk\Controller; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OCA\Talk\Config; use OCA\Talk\Events\SignalingEvent; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCA\Talk\Service\SessionService; use OCA\Talk\Signaling\Messages; use OCA\Talk\TalkSession; use OCP\AppFramework\Http; @@ -60,6 +65,10 @@ class SignalingController extends OCSController { private $session; /** @var Manager */ private $manager; + /** @var ParticipantService */ + private $participantService; + /** @var SessionService */ + private $sessionService; /** @var IDBConnection */ private $dbConnection; /** @var Messages */ @@ -81,6 +90,8 @@ public function __construct(string $appName, \OCA\Talk\Signaling\Manager $signalingManager, TalkSession $session, Manager $manager, + ParticipantService $participantService, + SessionService $sessionService, IDBConnection $connection, Messages $messages, IUserManager $userManager, @@ -94,6 +105,8 @@ public function __construct(string $appName, $this->session = $session; $this->dbConnection = $connection; $this->manager = $manager; + $this->participantService = $participantService; + $this->sessionService = $sessionService; $this->messages = $messages; $this->userManager = $userManager; $this->dispatcher = $dispatcher; @@ -105,13 +118,16 @@ public function __construct(string $appName, /** * @PublicPage * + * @param string $apiVersion * @param string $token * @return DataResponse */ - public function getSettings(string $token = ''): DataResponse { + public function getSettings(string $apiVersion, string $token = ''): DataResponse { + $apiV = (int) substr($apiVersion, 1); + try { if ($token !== '') { - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); + $room = $this->manager->getRoomForUserByToken($token, $this->userId); } else { // FIXME Soft-fail for legacy support in mobile apps $room = null; @@ -145,7 +161,7 @@ public function getSettings(string $token = ''): DataResponse { $signalingMode = $this->talkConfig->getSignalingMode(); $signaling = $this->signalingManager->getSignalingServerLinkForConversation($room); - return new DataResponse([ + $data = [ 'signalingMode' => $signalingMode, 'userId' => $this->userId, 'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(), @@ -153,7 +169,13 @@ public function getSettings(string $token = ''): DataResponse { 'ticket' => $this->talkConfig->getSignalingTicket($this->userId), 'stunservers' => $stun, 'turnservers' => $turn, - ]); + ]; + + if ($apiV >= 2) { + $data['sipDialinInfo'] = $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : ''; + } + + return new DataResponse($data); } /** @@ -271,9 +293,12 @@ public function pullMessages(string $token): DataResponse { } $room = $this->manager->getRoomForSession($this->userId, $sessionId); + $participant = $room->getParticipantBySession($sessionId); // FIXME this causes another query $pingTimestamp = $this->timeFactory->getTime(); - $room->ping($this->userId, $sessionId, $pingTimestamp); + if ($participant->getSession() instanceof Session) { + $this->sessionService->updateLastPing($participant->getSession(), $pingTimestamp); + } } catch (RoomNotFoundException $e) { return new DataResponse([['type' => 'usersInRoom', 'data' => []]], Http::STATUS_NOT_FOUND); } @@ -319,7 +344,7 @@ public function pullMessages(string $token): DataResponse { // Was the session killed or the complete conversation? try { - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); + $room = $this->manager->getRoomForUserByToken($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. @@ -354,19 +379,25 @@ protected function getUsersInRoom(Room $room, int $pingTimestamp): array { $timestamp = min($this->timeFactory->getTime() - (self::PULL_MESSAGES_TIMEOUT + 10), $pingTimestamp); // "- 1" is needed because only the participants whose last ping is // greater than the given timestamp are returned. - $participants = $room->getParticipants($timestamp - 1); + $participants = $this->participantService->getParticipantsForRoom($room); foreach ($participants as $participant) { - if ($participant->getSessionId() === '0') { + $session = $participant->getSession(); + if (!$session instanceof Session) { // User is not active continue; } + $userId = ''; + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $userId = $participant->getAttendee()->getActorId(); + } + $usersInRoom[] = [ - 'userId' => $participant->getUser(), + 'userId' => $userId, 'roomId' => $room->getId(), - 'lastPing' => $participant->getLastPing(), - 'sessionId' => $participant->getSessionId(), - 'inCall' => $participant->getInCallFlags(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + 'inCall' => $session->getInCall(), ]; } @@ -499,39 +530,52 @@ private function backendAuth(array $auth): DataResponse { } private function backendRoom(array $roomRequest): DataResponse { - $roomId = $roomRequest['roomid']; + $token = $roomRequest['roomid']; // It's actually the room token $userId = $roomRequest['userid']; $sessionId = $roomRequest['sessionid']; $action = !empty($roomRequest['action']) ? $roomRequest['action'] : 'join'; - - try { - $room = $this->manager->getRoomByToken($roomId, $userId); - } catch (RoomNotFoundException $e) { - return new DataResponse([ - 'type' => 'error', - 'error' => [ - 'code' => 'no_such_room', - 'message' => 'The user is not invited to this room.', - ], - ]); - } + $actorId = $roomRequest['actorid'] ?? null; + $actorType = $roomRequest['actortype'] ?? null; + $inCall = $roomRequest['incall'] ?? null; $participant = null; - if (!empty($userId)) { - // User trying to join room. + if ($actorId !== null && $actorType !== null) { try { - $participant = $room->getParticipant($userId); - } catch (ParticipantNotFoundException $e) { - // Ignore, will check for public rooms below. + $room = $this->manager->getRoomByActor($token, $actorType, $actorId); + } catch (RoomNotFoundException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); } - } - if (!$participant instanceof Participant) { - // User was not invited to the room, check for access to public room. + if ($sessionId) { + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + if ($action === 'join') { + // If the user joins the session might not be known to the server yet. + // In this case we load by actor information and use the session id as new session. + try { + $participant = $room->getParticipantByActor($actorType, $actorId); + } catch (ParticipantNotFoundException $e) { + } + } + } + } else { + try { + $participant = $room->getParticipantByActor($actorType, $actorId); + } catch (ParticipantNotFoundException $e) { + } + } + } else { try { - $participant = $room->getParticipantBySession($sessionId); - } catch (ParticipantNotFoundException $e) { - // Return generic error to avoid leaking which rooms exist. + // FIXME Don't preload with the user as that misses the session, kinda meh. + $room = $this->manager->getRoomByToken($token); + } catch (RoomNotFoundException $e) { return new DataResponse([ 'type' => 'error', 'error' => [ @@ -540,15 +584,63 @@ private function backendRoom(array $roomRequest): DataResponse { ], ]); } + + if ($sessionId) { + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + } + } elseif (!empty($userId)) { + // User trying to join room. + try { + $participant = $room->getParticipant($userId); + } catch (ParticipantNotFoundException $e) { + } + } + } + + if (!$participant instanceof Participant) { + // Return generic error to avoid leaking which rooms exist. + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); } if ($action === 'join') { - $room->ping($userId, $sessionId, $this->timeFactory->getTime()); + if ($sessionId && !$participant->getSession() instanceof Session) { + try { + $session = $this->sessionService->createSessionForAttendee($participant->getAttendee(), $sessionId); + } catch (UniqueConstraintViolationException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'duplicate_session', + 'message' => 'The given session is already in use.', + ], + ]); + } + $participant->setSession($session); + } + + if ($participant->getSession() instanceof Session) { + if ($inCall !== null) { + $this->participantService->changeInCall($room, $participant, $inCall); + } + $this->sessionService->updateLastPing($participant->getSession(), $this->timeFactory->getTime()); + } } elseif ($action === 'leave') { - if (!empty($userId)) { - $room->leaveRoom($userId, $sessionId); - } elseif ($participant instanceof Participant) { - $room->removeParticipantBySession($participant, Room::PARTICIPANT_LEFT); + // Guests are removed completely as they don't reuse attendees, + // but this is only true for guests that joined directly. + // Emails are retained as their PIN needs to remain and stay + // valid. + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) { + $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_LEFT); + } else { + $this->participantService->leaveRoomAsSession($room, $participant); } } @@ -597,7 +689,7 @@ private function backendPing(array $request): DataResponse { } // Ping all active sessions with one query - $room->pingSessionIds($pingSessionIds, $now); + $this->sessionService->updateMultipleLastPings($pingSessionIds, $now); $response = [ 'type' => 'room', diff --git a/lib/Controller/WebinarController.php b/lib/Controller/WebinarController.php deleted file mode 100644 index c3ac6e5d540..00000000000 --- a/lib/Controller/WebinarController.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * @author Joas Schilling - * - * @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 . - * - */ - -namespace OCA\Talk\Controller; - -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\IRequest; - -class WebinarController extends AEnvironmentAwareController { - - /** @var ITimeFactory */ - protected $timeFactory; - - public function __construct(string $appName, - IRequest $request, - ITimeFactory $timeFactory) { - parent::__construct($appName, $request); - $this->timeFactory = $timeFactory; - } - - /** - * @NoAdminRequired - * @RequireModeratorParticipant - * - * @param int $state - * @param int|null $timer - * @return DataResponse - */ - public function setLobby(int $state, ?int $timer = null): DataResponse { - $timerDateTime = null; - if ($timer !== null && $timer > 0) { - try { - $timerDateTime = $this->timeFactory->getDateTime('@' . $timer); - $timerDateTime->setTimezone(new \DateTimeZone('UTC')); - } catch (\Exception $e) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); - } - } - - if (!$this->room->setLobby($state, $timerDateTime)) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); - } - - return new DataResponse(); - } -} diff --git a/lib/Files/Listener.php b/lib/Files/Listener.php index bcec71a98e0..f6f2f0e9454 100644 --- a/lib/Files/Listener.php +++ b/lib/Files/Listener.php @@ -28,7 +28,9 @@ use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; use OCP\EventDispatcher\IEventDispatcher; @@ -52,12 +54,16 @@ class Listener { /** @var Util */ protected $util; + /** @var ParticipantService */ + protected $participantService; /** @var TalkSession */ protected $talkSession; public function __construct(Util $util, + ParticipantService $participantService, TalkSession $talkSession) { $this->util = $util; + $this->participantService = $participantService; $this->talkSession = $talkSession; } @@ -148,7 +154,10 @@ public function addUserAsPersistentParticipant(Room $room, string $userId): void try { $room->getParticipant($userId); } catch (ParticipantNotFoundException $e) { - $room->addUsers(['userId' => $userId]); + $this->participantService->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $userId, + ]]); } } diff --git a/lib/Flow/Operation.php b/lib/Flow/Operation.php index dd152218adf..af00807a3f3 100644 --- a/lib/Flow/Operation.php +++ b/lib/Flow/Operation.php @@ -138,7 +138,7 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch $room, $participant, 'bots', - $participant->getUser(), + $participant->getAttendee()->getActorId(), $this->prepareMention($mode, $participant) . $message, new \DateTime(), null, @@ -173,9 +173,9 @@ protected function prepareMention(int $mode, Participant $participant): string { case self::MESSAGE_MODES['ROOM_MENTION']: return '@all '; case self::MESSAGE_MODES['SELF_MENTION']: - $hasWhitespace = strpos($participant->getUser(), ' ') !== false; + $hasWhitespace = strpos($participant->getAttendee()->getActorId(), ' ') !== false; $enclosure = $hasWhitespace ? '"' : ''; - return '@' . $enclosure . $participant->getUser() . $enclosure . ' '; + return '@' . $enclosure . $participant->getAttendee()->getActorId() . $enclosure . ' '; case self::MESSAGE_MODES['NO_MENTION']: default: return ''; @@ -244,7 +244,7 @@ protected function getUser(): IUser { * @throws RoomNotFoundException */ protected function getRoom(string $token, string $uid): Room { - return $this->talkManager->getRoomForParticipantByToken($token, $uid); + return $this->talkManager->getRoomForUserByToken($token, $uid); } /** diff --git a/lib/GuestManager.php b/lib/GuestManager.php index 2408ae27d33..1cc48ad7c20 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -45,6 +45,9 @@ class GuestManager { /** @var IDBConnection */ protected $connection; + /** @var Config */ + protected $talkConfig; + /** @var IMailer */ protected $mailer; @@ -64,6 +67,7 @@ class GuestManager { protected $dispatcher; public function __construct(IDBConnection $connection, + Config $talkConfig, IMailer $mailer, Defaults $defaults, IUserSession $userSession, @@ -71,6 +75,7 @@ public function __construct(IDBConnection $connection, IL10N $l, IEventDispatcher $dispatcher) { $this->connection = $connection; + $this->talkConfig = $talkConfig; $this->mailer = $mailer; $this->defaults = $defaults; $this->userSession = $userSession; @@ -86,7 +91,7 @@ public function __construct(IDBConnection $connection, * @throws \Doctrine\DBAL\DBALException */ public function updateName(Room $room, Participant $participant, string $displayName): void { - $sessionHash = sha1($participant->getSessionId()); + $sessionHash = $participant->getAttendee()->getActorId(); $dispatchEvent = true; try { @@ -163,7 +168,10 @@ public function getNamesBySessionHashes(array $sessionHashes): array { return $map; } - public function inviteByEmail(Room $room, string $email): void { + public function sendEmailInvitation(Room $room, Participant $participant): void { + $email = $participant->getAttendee()->getActorId(); + $pin = $participant->getAttendee()->getPin(); + $event = new AddEmailEvent($room, $email); $this->dispatcher->dispatch(self::EVENT_BEFORE_EMAIL_INVITE, $event); @@ -178,6 +186,8 @@ public function inviteByEmail(Room $room, string $email): void { 'invitee' => $invitee, 'roomName' => $room->getDisplayName(''), 'roomLink' => $link, + 'email' => $email, + 'pin' => $pin, ]); if ($user instanceof IUser) { @@ -201,6 +211,28 @@ public function inviteByEmail(Room $room, string $email): void { $link ); + if ($pin) { + $template->addBodyText($this->l->t('You can also dial-in via phone with the following details')); + + $template->addBodyListItem( + $this->talkConfig->getDialInInfo(), + $this->l->t('Dial-in information'), + $this->url->getAbsoluteURL($this->url->imagePath('spreed', 'phone.png')) + ); + + $template->addBodyListItem( + $room->getToken(), + $this->l->t('Meeting ID'), + $this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar-dark.png')) + ); + + $template->addBodyListItem( + $pin, + $this->l->t('Your PIN'), + $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/password.png')) + ); + } + $template->addFooter(); $message->setTo([$email]); diff --git a/lib/Listener/BeforeUserLoggedOutListener.php b/lib/Listener/BeforeUserLoggedOutListener.php index 160f7b14673..41d65c98740 100644 --- a/lib/Listener/BeforeUserLoggedOutListener.php +++ b/lib/Listener/BeforeUserLoggedOutListener.php @@ -27,6 +27,7 @@ use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; use OCA\Talk\Participant; +use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -37,12 +38,16 @@ class BeforeUserLoggedOutListener implements IEventListener { /** @var Manager */ private $manager; + /** @var ParticipantService */ + private $participantService; /** @var TalkSession */ private $talkSession; public function __construct(Manager $manager, + ParticipantService $participantService, TalkSession $talkSession) { $this->manager = $manager; + $this->participantService = $participantService; $this->talkSession = $talkSession; } @@ -63,10 +68,10 @@ public function handle(Event $event): void { try { $room = $this->manager->getRoomForSession($user->getUID(), $sessionId); $participant = $room->getParticipant($user->getUID()); - if ($participant->getInCallFlags() !== Participant::FLAG_DISCONNECTED) { - $room->changeInCall($participant, Participant::FLAG_DISCONNECTED); + if ($participant->getSession() && $participant->getSession()->getInCall() !== Participant::FLAG_DISCONNECTED) { + $this->participantService->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED); } - $room->leaveRoom($user->getUID(), $sessionId); + $this->participantService->leaveRoomAsSession($room, $participant); } catch (RoomNotFoundException $e) { } catch (ParticipantNotFoundException $e) { } diff --git a/lib/Listener/RestrictStartingCalls.php b/lib/Listener/RestrictStartingCalls.php index 623e96538e8..316f5564b05 100644 --- a/lib/Listener/RestrictStartingCalls.php +++ b/lib/Listener/RestrictStartingCalls.php @@ -26,6 +26,7 @@ use OCA\Talk\Events\ModifyParticipantEvent; use OCA\Talk\Exceptions\ForbiddenException; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; @@ -34,8 +35,13 @@ class RestrictStartingCalls { /** @var IConfig */ protected $config; - public function __construct(IConfig $config) { + /** @var ParticipantService */ + protected $participantService; + + public function __construct(IConfig $config, + ParticipantService $participantService) { $this->config = $config; + $this->participantService = $participantService; } public static function register(IEventDispatcher $dispatcher): void { @@ -54,7 +60,7 @@ public function checkStartCallPermissions(ModifyParticipantEvent $event): void { $room = $event->getRoom(); $participant = $event->getParticipant(); - if (!$participant->canStartCall() && !$room->hasSessionsInCall()) { + if (!$participant->canStartCall($this->config) && !$this->participantService->hasActiveSessionsInCall($room)) { throw new ForbiddenException('Can not start a call'); } } diff --git a/lib/Listener/UserDeletedListener.php b/lib/Listener/UserDeletedListener.php index 62fc9b373a0..c12a3fd9b10 100644 --- a/lib/Listener/UserDeletedListener.php +++ b/lib/Listener/UserDeletedListener.php @@ -25,6 +25,7 @@ use OCA\Talk\Manager; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\User\Events\UserDeletedEvent; @@ -33,9 +34,13 @@ class UserDeletedListener implements IEventListener { /** @var Manager */ private $manager; + /** @var ParticipantService */ + private $participantService; - public function __construct(Manager $manager) { + public function __construct(Manager $manager, + ParticipantService $participantService) { $this->manager = $manager; + $this->participantService = $participantService; } public function handle(Event $event): void { @@ -46,13 +51,13 @@ public function handle(Event $event): void { $user = $event->getUser(); - $rooms = $this->manager->getRoomsForParticipant($user->getUID()); + $rooms = $this->manager->getRoomsForUser($user->getUID()); foreach ($rooms as $room) { - if ($room->getNumberOfParticipants() === 1) { + if ($this->participantService->getNumberOfUsers($room) === 1) { $room->deleteRoom(); } else { - $room->removeUser($user, Room::PARTICIPANT_REMOVED); + $this->participantService->removeUser($room, $user, Room::PARTICIPANT_REMOVED); } } } diff --git a/lib/Manager.php b/lib/Manager.php index 5a4b81eb7cc..c4488261d7b 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -25,10 +25,13 @@ use OCA\Talk\Chat\Changelog; use OCA\Talk\Chat\CommentsManager; -use OCA\Talk\Events\CreateRoomTokenEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\AttendeeMapper; +use OCA\Talk\Model\SessionMapper; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\Comments\NotFoundException; @@ -50,6 +53,14 @@ class Manager { private $db; /** @var IConfig */ private $config; + /** @var Config */ + private $talkConfig; + /** @var AttendeeMapper */ + private $attendeeMapper; + /** @var SessionMapper */ + private $sessionMapper; + /** @var ParticipantService */ + private $participantService; /** @var ISecureRandom */ private $secureRandom; /** @var IUserManager */ @@ -69,6 +80,10 @@ class Manager { public function __construct(IDBConnection $db, IConfig $config, + Config $talkConfig, + AttendeeMapper $attendeeMapper, + SessionMapper $sessionMapper, + ParticipantService $participantService, ISecureRandom $secureRandom, IUserManager $userManager, CommentsManager $commentsManager, @@ -79,6 +94,10 @@ public function __construct(IDBConnection $db, IL10N $l) { $this->db = $db; $this->config = $config; + $this->talkConfig = $talkConfig; + $this->attendeeMapper = $attendeeMapper; + $this->sessionMapper = $sessionMapper; + $this->participantService = $participantService; $this->secureRandom = $secureRandom; $this->userManager = $userManager; $this->commentsManager = $commentsManager; @@ -92,6 +111,7 @@ public function __construct(IDBConnection $db, public function forAllRooms(callable $callback): void { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms'); $result = $query->execute(); @@ -144,10 +164,11 @@ public function createRoomObject(array $row): Room { $this->dispatcher, $this->timeFactory, $this->hasher, - (int) $row['id'], + (int) $row['r_id'], (int) $row['type'], (int) $row['read_only'], (int) $row['lobby_state'], + (int) $row['sip_enabled'], $assignedSignalingServer, (string) $row['token'], (string) $row['name'], @@ -169,26 +190,13 @@ public function createRoomObject(array $row): Room { * @return Participant */ public function createParticipantObject(Room $room, array $row): Participant { - $lastJoinedCall = null; - if (!empty($row['last_joined_call'])) { - $lastJoinedCall = $this->timeFactory->getDateTime($row['last_joined_call']); + $attendee = $this->attendeeMapper->createAttendeeFromRow($row); + $session = null; + if (!empty($row['s_id'])) { + $session = $this->sessionMapper->createSessionFromRow($row); } - return new Participant( - $this->db, - $this->config, - $room, - (string) $row['user_id'], - (int) $row['participant_type'], - (int) $row['last_ping'], - (string) $row['session_id'], - (int) $row['in_call'], - (int) $row['notification_level'], - (bool) $row['favorite'], - (int) $row['last_read_message'], - (int) $row['last_mention_message'], - $lastJoinedCall - ); + return new Participant($room, $attendee, $session); } public function createCommentObject(array $row): ?IComment { @@ -227,7 +235,7 @@ public function resetAssignedSignalingServers(ICache $cache): void { $result = $query->execute(); while ($row = $result->fetch()) { $room = $this->createRoomObject($row); - if (!$room->hasActiveSessions()) { + if (!$this->participantService->hasActiveSessions($room)) { $room->setAssignedSignalingServer(null); $cache->remove($room->getToken()); } @@ -237,13 +245,14 @@ public function resetAssignedSignalingServers(ICache $cache): void { /** * @param string $searchToken - * @param int $limit - * @param int $offset + * @param int|null $limit + * @param int|null $offset * @return Room[] */ public function searchRoomsByToken(string $searchToken = '', int $limit = null, int $offset = null): array { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms') ->setMaxResults(1); @@ -273,19 +282,28 @@ public function searchRoomsByToken(string $searchToken = '', int $limit = null, } /** - * @param string $participant + * @param string $userId * @param bool $includeLastMessage * @return Room[] */ - public function getRoomsForParticipant(string $participant, bool $includeLastMessage = false): array { + public function getRoomsForUser(string $userId, bool $includeLastMessage = false): array { $query = $this->db->getQueryBuilder(); - $query->select('r.*')->addSelect('p.*') + $query->select('r.*') + ->addSelect('a.*') + ->addSelect('s.*') + ->selectAlias('r.id', 'r_id') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') ->from('talk_rooms', 'r') - ->leftJoin('r', 'talk_participants', 'p', $query->expr()->andX( - $query->expr()->eq('p.user_id', $query->createNamedParameter($participant)), - $query->expr()->eq('p.room_id', 'r.id') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') )) - ->where($query->expr()->isNotNull('p.user_id')); + ->where($query->expr()->isNotNull('a.id')); if ($includeLastMessage) { $this->loadLastMessageInfo($query); @@ -300,8 +318,8 @@ public function getRoomsForParticipant(string $participant, bool $includeLastMes } $room = $this->createRoomObject($row); - if ($participant !== null && isset($row['user_id'])) { - $room->setParticipant($row['user_id'], $this->createParticipantObject($room, $row)); + if ($userId !== null && isset($row['actor_id'])) { + $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } $rooms[] = $room; } @@ -314,23 +332,32 @@ public function getRoomsForParticipant(string $participant, bool $includeLastMes * Does *not* return public rooms for participants that have not been invited * * @param int $roomId - * @param string $participant + * @param string|null $userId * @return Room * @throws RoomNotFoundException */ - public function getRoomForParticipant(int $roomId, ?string $participant): Room { + public function getRoomForUser(int $roomId, ?string $userId): Room { $query = $this->db->getQueryBuilder(); - $query->select('*') + $query->select('r.*') + ->selectAlias('r.id', 'r_id') ->from('talk_rooms', 'r') ->where($query->expr()->eq('r.id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); - if ($participant !== null) { + if ($userId !== null) { // Non guest user - $query->leftJoin('r', 'talk_participants', 'p', $query->expr()->andX( - $query->expr()->eq('p.user_id', $query->createNamedParameter($participant)), - $query->expr()->eq('p.room_id', 'r.id') + $query->addSelect('a.*') + ->addSelect('s.*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') )) - ->andWhere($query->expr()->isNotNull('p.user_id')); + ->andWhere($query->expr()->isNotNull('a.id')); } $result = $query->execute(); @@ -347,11 +374,11 @@ public function getRoomForParticipant(int $roomId, ?string $participant): Room { } $room = $this->createRoomObject($row); - if ($participant !== null && isset($row['user_id'])) { - $room->setParticipant($row['user_id'], $this->createParticipantObject($room, $row)); + if ($userId !== null && isset($row['actor_id'])) { + $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } - if ($participant === null && $room->getType() !== Room::PUBLIC_CALL) { + if ($userId === null && $room->getType() !== Room::PUBLIC_CALL) { throw new RoomNotFoundException(); } @@ -363,24 +390,32 @@ public function getRoomForParticipant(int $roomId, ?string $participant): Room { * so they can join. * * @param string $token - * @param string $participant + * @param string|null $userId * @param bool $includeLastMessage * @return Room * @throws RoomNotFoundException */ - public function getRoomForParticipantByToken(string $token, ?string $participant, bool $includeLastMessage = false): Room { + public function getRoomForUserByToken(string $token, ?string $userId, bool $includeLastMessage = false): Room { $query = $this->db->getQueryBuilder(); $query->select('r.*') + ->selectAlias('r.id', 'r_id') ->from('talk_rooms', 'r') ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))) ->setMaxResults(1); - if ($participant !== null) { + if ($userId !== null) { // Non guest user - $query->addSelect('p.*') - ->leftJoin('r', 'talk_participants', 'p', $query->expr()->andX( - $query->expr()->eq('p.user_id', $query->createNamedParameter($participant)), - $query->expr()->eq('p.room_id', 'r.id') + $query->addSelect('a.*') + ->addSelect('s.*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') )); } @@ -402,15 +437,15 @@ public function getRoomForParticipantByToken(string $token, ?string $participant } $room = $this->createRoomObject($row); - if ($participant !== null && isset($row['user_id'])) { - $room->setParticipant($row['user_id'], $this->createParticipantObject($room, $row)); + if ($userId !== null && isset($row['actor_id'])) { + $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } if ($room->getType() === Room::PUBLIC_CALL) { return $room; } - if ($participant !== null && $row['user_id'] === $participant) { + if ($userId !== null && $row['actor_id'] === $userId) { return $room; } @@ -425,6 +460,7 @@ public function getRoomForParticipantByToken(string $token, ?string $participant public function getRoomById(int $roomId): Room { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms') ->where($query->expr()->eq('id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); @@ -446,26 +482,30 @@ public function getRoomById(int $roomId): Room { /** * @param string $token - * @param string|null $preloadParticipant Load this participants information if possible + * @param string $actorType + * @param string $actorId * @return Room * @throws RoomNotFoundException */ - public function getRoomByToken(string $token, ?string $preloadParticipant = null): Room { - $preloadParticipant = $preloadParticipant === '' ? null : $preloadParticipant; - + public function getRoomByActor(string $token, string $actorType, string $actorId): Room { $query = $this->db->getQueryBuilder(); $query->select('r.*') + ->addSelect('a.*') + ->addSelect('s.*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->selectAlias('r.id', 'r_id') ->from('talk_rooms', 'r') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), + $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') + )) ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); - if ($preloadParticipant !== null) { - $query->addSelect('p.*') - ->leftJoin('r', 'talk_participants', 'p', $query->expr()->andX( - $query->expr()->eq('p.user_id', $query->createNamedParameter($preloadParticipant)), - $query->expr()->eq('p.room_id', 'r.id') - )); - } - $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); @@ -480,13 +520,47 @@ public function getRoomByToken(string $token, ?string $preloadParticipant = null } $room = $this->createRoomObject($row); - if ($preloadParticipant !== null && isset($row['user_id'])) { - $room->setParticipant($row['user_id'], $this->createParticipantObject($room, $row)); + if ($actorType === Attendee::ACTOR_USERS && isset($row['actor_id'])) { + $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } return $room; } + /** + * @param string $token + * @param string|null $preloadUserId Load this participants information if possible + * @return Room + * @throws RoomNotFoundException + */ + public function getRoomByToken(string $token, ?string $preloadUserId = null): Room { + $preloadUserId = $preloadUserId === '' ? null : $preloadUserId; + if ($preloadUserId !== null) { + return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId); + } + + $query = $this->db->getQueryBuilder(); + $query->select('r.*') + ->selectAlias('r.id', 'r_id') + ->from('talk_rooms', 'r') + ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); + + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new RoomNotFoundException(); + } + + if ($row['token'] === null) { + // FIXME Temporary solution for the Talk6 release + throw new RoomNotFoundException(); + } + + return $this->createRoomObject($row); + } + /** * @param string $objectType * @param string $objectId @@ -496,6 +570,7 @@ public function getRoomByToken(string $token, ?string $preloadParticipant = null public function getRoomByObject(string $objectType, string $objectId): Room { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms') ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType))) ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId))); @@ -529,21 +604,31 @@ public function getRoomForSession(?string $userId, ?string $sessionId): Room { $query = $this->db->getQueryBuilder(); $query->select('*') - ->from('talk_participants', 'p') - ->leftJoin('p', 'talk_rooms', 'r', $query->expr()->eq('p.room_id', 'r.id')) - ->where($query->expr()->eq('p.session_id', $query->createNamedParameter($sessionId))) + ->selectAlias('r.id', 'r_id') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_sessions', 's') + ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id')) + ->leftJoin('a', 'talk_rooms', 'r', $query->expr()->eq('a.room_id', 'r.id')) + ->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId))) ->setMaxResults(1); $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); - if ($row === false || !$row['id']) { + if ($row === false || !$row['r_id']) { throw new RoomNotFoundException(); } - if ((string) $userId !== $row['user_id']) { - throw new RoomNotFoundException(); + if ($userId !== null) { + if ($row['actor_type'] !== Attendee::ACTOR_USERS || $userId !== $row['actor_id']) { + throw new RoomNotFoundException(); + } + } else { + if ($row['actor_type'] !== Attendee::ACTOR_GUESTS) { + throw new RoomNotFoundException(); + } } if ($row['token'] === null) { @@ -553,9 +638,9 @@ public function getRoomForSession(?string $userId, ?string $sessionId): Room { $room = $this->createRoomObject($row); $participant = $this->createParticipantObject($room, $row); - $room->setParticipant($row['user_id'], $participant); + $room->setParticipant($row['actor_id'], $participant); - if ($room->getType() === Room::PUBLIC_CALL || !in_array($participant->getParticipantType(), [Participant::GUEST, Participant::USER_SELF_JOINED], true)) { + if ($room->getType() === Room::PUBLIC_CALL || !in_array($participant->getAttendee()->getParticipantType(), [Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], true)) { return $room; } @@ -575,6 +660,7 @@ public function getOne2OneRoom(string $participant1, string $participant2): Room $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms') ->where($query->expr()->eq('type', $query->createNamedParameter(Room::ONE_TO_ONE_CALL, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('name', $query->createNamedParameter($name))); @@ -604,6 +690,7 @@ public function getOne2OneRoom(string $participant1, string $participant2): Room public function getChangelogRoom(string $userId): Room { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms') ->where($query->expr()->eq('type', $query->createNamedParameter(Room::CHANGELOG_CONVERSATION, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('name', $query->createNamedParameter($userId))); @@ -614,8 +701,12 @@ public function getChangelogRoom(string $userId): Room { if ($row === false) { $room = $this->createRoom(Room::CHANGELOG_CONVERSATION, $userId); - $room->addUsers(['userId' => $userId]); $room->setReadOnly(Room::READ_ONLY); + + $this->participantService->addUsers($room,[[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $userId, + ]]); return $room; } @@ -624,7 +715,10 @@ public function getChangelogRoom(string $userId): Room { try { $room->getParticipant($userId); } catch (ParticipantNotFoundException $e) { - $room->addUsers(['userId' => $userId]); + $this->participantService->addUsers($room,[[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $userId, + ]]); } return $room; @@ -666,61 +760,6 @@ public function createRoom(int $type, string $name = '', string $objectType = '' return $room; } - /** - * @param string|null $userId - * @return string|null - */ - public function getCurrentSessionId(?string $userId): ?string { - if (empty($userId)) { - return null; - } - - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId))) - ->andWhere($query->expr()->neq('session_id', $query->createNamedParameter('0'))) - ->orderBy('last_ping', 'DESC') - ->setMaxResults(1); - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - if ($row === false) { - return null; - } - - return $row['session_id']; - } - - /** - * @param string $userId - * @return string[] - */ - public function getSessionIdsForUser(?string $userId): array { - if (!is_string($userId) || $userId === '') { - // No deleting messages for guests - return []; - } - - // Delete all messages from or to the current user - $query = $this->db->getQueryBuilder(); - $query->select('session_id') - ->from('talk_participants') - ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId))); - $result = $query->execute(); - - $sessionIds = []; - while ($row = $result->fetch()) { - if ($row['session_id'] !== '0') { - $sessionIds[] = $row['session_id']; - } - } - $result->closeCursor(); - - return $sessionIds; - } - public function resolveRoomDisplayName(Room $room, string $userId): string { if ($room->getObjectType() === 'share:password') { return $this->l->t('Password request: %s', [$room->getName()]); @@ -785,7 +824,7 @@ public function resolveRoomDisplayName(Room $room, string $userId): string { } protected function getRoomNameByParticipants(Room $room): string { - $users = $room->getParticipantUserIds(); + $users = $this->participantService->getParticipantUserIds($room); $displayNames = []; foreach ($users as $participantId) { @@ -804,9 +843,13 @@ protected function getRoomNameByParticipants(Room $room): string { * @return string */ protected function getNewToken(): string { - $chars = str_replace(['l', '0', '1'], '', ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $entropy = (int) $this->config->getAppValue('spreed', 'token_entropy', 8); $entropy = max(8, $entropy); // For update cases + $digitsOnly = $this->talkConfig->isSIPConfigured(); + if ($digitsOnly) { + // Increase default token length as we only use numbers + $entropy = max(10, $entropy); + } $query = $this->db->getQueryBuilder(); $query->select('id') @@ -816,7 +859,7 @@ protected function getNewToken(): string { $i = 0; while ($i < 1000) { try { - $token = $this->generateNewToken($query, $entropy, $chars); + $token = $this->generateNewToken($query, $entropy, $digitsOnly); if (\in_array($token, ['settings', 'backend'], true)) { throw new \OutOfBoundsException('Reserved word'); } @@ -832,27 +875,33 @@ protected function getNewToken(): string { $entropy++; $this->config->setAppValue('spreed', 'token_entropy', $entropy); - return $this->generateNewToken($query, $entropy, $chars); + return $this->generateNewToken($query, $entropy, $digitsOnly); } /** * @param IQueryBuilder $query * @param int $entropy - * @param string $chars + * @param bool $digitsOnly * @return string * @throws \OutOfBoundsException */ - protected function generateNewToken(IQueryBuilder $query, int $entropy, string $chars): string { - $event = new CreateRoomTokenEvent($entropy, $chars); - $this->dispatcher->dispatch(self::EVENT_TOKEN_GENERATE, $event); - try { - $token = $event->getToken(); - if ($token === '') { - // Will generate default token below. - throw new \InvalidArgumentException('token may not be empty'); - } - } catch (\InvalidArgumentException $e) { + protected function generateNewToken(IQueryBuilder $query, int $entropy, bool $digitsOnly): string { + if (!$digitsOnly) { + $chars = str_replace(['l', '0', '1'], '', ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $token = $this->secureRandom->generate($entropy, $chars); + } else { + $chars = ISecureRandom::CHAR_DIGITS; + $token = ''; + // Do not allow to start with a '0' as that is a special mode on the phone server + // Also there are issues with some providers when you enter the same number twice + // consecutive too fast, so we avoid this as well. + $lastDigit = '0'; + for ($i = 0; $i < $entropy; $i++) { + $lastDigit = $this->secureRandom->generate(1, + str_replace($lastDigit, '', $chars) + ); + $token .= $lastDigit; + } } $query->setParameter('token', $token); diff --git a/lib/MatterbridgeManager.php b/lib/MatterbridgeManager.php index a2a5ebb5fe6..19047268a90 100644 --- a/lib/MatterbridgeManager.php +++ b/lib/MatterbridgeManager.php @@ -24,6 +24,8 @@ namespace OCA\Talk; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Service\ParticipantService; use OCP\IConfig; use OCP\IDBConnection; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -53,6 +55,8 @@ class MatterbridgeManager { private $userManager; /** @var Manager */ private $manager; + /** @var ParticipantService */ + private $participantService; /** @var ChatManager */ private $chatManager; /** @var IAuthTokenProvider */ @@ -71,6 +75,7 @@ public function __construct(IDBConnection $db, IURLGenerator $urlGenerator, IUserManager $userManager, Manager $manager, + ParticipantService $participantService, ChatManager $chatManager, IAuthTokenProvider $tokenProvider, ISecureRandom $random, @@ -83,6 +88,7 @@ public function __construct(IDBConnection $db, $this->urlGenerator = $urlGenerator; $this->userManager = $userManager; $this->manager = $manager; + $this->participantService = $participantService; $this->chatManager = $chatManager; $this->tokenProvider = $tokenProvider; $this->random = $random; @@ -297,10 +303,11 @@ private function checkBotUser(Room $room, bool $create): array { try { $participant = $room->getParticipant($botUserId); } catch (ParticipantNotFoundException $e) { - $room->addUsers([ - 'userId' => $botUserId, + $this->participantService->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $botUserId, 'participantType' => Participant::USER, - ]); + ]]); } // delete old bot app tokens for this room @@ -645,7 +652,7 @@ private function compareBridgeParts(array $part1, array $part2): bool { private function sendSystemMessage(Room $room, string $userId, string $message): void { $this->chatManager->addSystemMessage( $room, - 'users', + Attendee::ACTOR_USERS, $userId, json_encode(['message' => $message, 'parameters' => []]), $this->timeFactory->getDateTime(), diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index 9b7e89427a6..9d099d69746 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -101,6 +101,10 @@ public function beforeController($controller, $methodName): void { $this->getLoggedInOrGuest($controller, true); } + if ($this->reflector->hasAnnotation('RequireRoom')) { + $this->getRoom($controller); + } + if ($this->reflector->hasAnnotation('RequireReadWriteConversation')) { $this->checkReadOnlyState($controller); } @@ -110,6 +114,15 @@ public function beforeController($controller, $methodName): void { } } + /** + * @param AEnvironmentAwareController $controller + */ + protected function getRoom(AEnvironmentAwareController $controller): void { + $token = $this->request->getParam('token'); + $room = $this->manager->getRoomByToken($token, $this->userId); + $controller->setRoom($room); + } + /** * @param AEnvironmentAwareController $controller * @param bool $moderatorRequired @@ -117,7 +130,7 @@ public function beforeController($controller, $methodName): void { */ protected function getLoggedIn(AEnvironmentAwareController $controller, bool $moderatorRequired): void { $token = $this->request->getParam('token'); - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); + $room = $this->manager->getRoomForUserByToken($token, $this->userId); $controller->setRoom($room); $participant = $room->getParticipant($this->userId); @@ -136,14 +149,14 @@ protected function getLoggedIn(AEnvironmentAwareController $controller, bool $mo */ protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, bool $moderatorRequired): void { $token = $this->request->getParam('token'); - $room = $this->manager->getRoomForParticipantByToken($token, $this->userId); + $room = $this->manager->getRoomForUserByToken($token, $this->userId); $controller->setRoom($room); - if ($this->userId !== null) { - $participant = $room->getParticipant($this->userId); - } else { - $sessionId = $this->talkSession->getSessionForRoom($token); + $sessionId = $this->talkSession->getSessionForRoom($token); + if ($sessionId !== null) { $participant = $room->getParticipantBySession($sessionId); + } else { + $participant = $room->getParticipant($this->userId); } $controller->setParticipant($participant); diff --git a/lib/Migration/Version10000Date20201012144235.php b/lib/Migration/Version10000Date20201012144235.php new file mode 100644 index 00000000000..9ee1d7f8954 --- /dev/null +++ b/lib/Migration/Version10000Date20201012144235.php @@ -0,0 +1,54 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use Doctrine\DBAL\Types\Type; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version10000Date20201012144235 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('talk_rooms'); + $table->addColumn('sip_enabled', Type::SMALLINT, [ + 'notnull' => true, + 'default' => 0, + 'unsigned' => true, + ]); + + return $schema; + } +} diff --git a/lib/Migration/Version10000Date20201015134000.php b/lib/Migration/Version10000Date20201015134000.php new file mode 100644 index 00000000000..aa7e5bb5e00 --- /dev/null +++ b/lib/Migration/Version10000Date20201015134000.php @@ -0,0 +1,227 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use Doctrine\DBAL\Types\Type; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Participant; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * In order to be able to keep "attendees" which are not users, but groups, + * email addresses, etc the sessions had to be decoupled from the participants + */ +class Version10000Date20201015134000 extends SimpleMigrationStep { + /** @var IDBConnection */ + protected $connection; + /** @var ITimeFactory */ + protected $timeFactory; + + public function __construct(IDBConnection $connection, + ITimeFactory $timeFactory) { + $this->connection = $connection; + $this->timeFactory = $timeFactory; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('talk_attendees')) { + $table = $schema->createTable('talk_attendees'); + + // Auto increment id + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + + // Unique key + $table->addColumn('room_id', Type::BIGINT, [ + 'notnull' => true, + 'unsigned' => true, + ]); + $table->addColumn('actor_type', Type::STRING, [ + 'notnull' => true, + 'length' => 32, + ]); + $table->addColumn('actor_id', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('display_name', Type::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 64, + ]); + + $table->addColumn('pin', Type::STRING, [ + 'notnull' => false, + 'length' => 32, + ]); + $table->addColumn('participant_type', Type::SMALLINT, [ + 'notnull' => true, + 'length' => 6, + 'default' => 0, + 'unsigned' => true, + ]); + $table->addColumn('favorite', Type::BOOLEAN, [ + 'default' => 0, + ]); + $table->addColumn('notification_level', Type::INTEGER, [ + 'default' => Participant::NOTIFY_DEFAULT, + 'notnull' => false, + ]); + $table->addColumn('last_joined_call', Type::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'default' => 0, + 'unsigned' => true, + ]); + $table->addColumn('last_read_message', Type::BIGINT, [ + 'default' => 0, + 'notnull' => false, + ]); + $table->addColumn('last_mention_message', Type::BIGINT, [ + 'default' => 0, + 'notnull' => false, + ]); + + $table->setPrimaryKey(['id']); + + $table->addUniqueIndex(['room_id', 'actor_type', 'actor_id'], 'ta_ident'); + $table->addIndex(['room_id', 'pin'], 'ta_roompin'); + $table->addIndex(['room_id'], 'ta_room'); + $table->addIndex(['actor_type', 'actor_id'], 'ta_actor'); + } + + + if (!$schema->hasTable('talk_sessions')) { + $table = $schema->createTable('talk_sessions'); + + // Auto increment id + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + + // Unique key (for now, might remove this in the future, + // so a user can join multiple times. + $table->addColumn('attendee_id', Type::BIGINT, [ + 'notnull' => true, + 'unsigned' => true, + ]); + + // Unique key to avoid duplication issues + $table->addColumn('session_id', Type::STRING, [ + 'notnull' => true, + 'length' => 512, + ]); + + $table->addColumn('in_call', Type::INTEGER, [ + 'default' => 0, + ]); + $table->addColumn('last_ping', Type::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'default' => 0, + 'unsigned' => true, + ]); + + $table->setPrimaryKey(['id']); + + $table->addUniqueIndex(['attendee_id'], 'ts_attendee'); + $table->addUniqueIndex(['session_id'], 'ts_session'); + $table->addIndex(['in_call'], 'ts_in_call'); + $table->addIndex(['last_ping'], 'ts_last_ping'); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $insert = $this->connection->getQueryBuilder(); + $insert->insert('talk_attendees') + ->values([ + 'room_id' => $insert->createParameter('room_id'), + 'actor_type' => $insert->createParameter('actor_type'), + 'actor_id' => $insert->createParameter('actor_id'), + 'participant_type' => $insert->createParameter('participant_type'), + 'favorite' => $insert->createParameter('favorite'), + 'notification_level' => $insert->createParameter('notification_level'), + 'last_joined_call' => $insert->createParameter('last_joined_call'), + 'last_read_message' => $insert->createParameter('last_read_message'), + 'last_mention_message' => $insert->createParameter('last_mention_message'), + ]); + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('talk_participants') + ->where($query->expr()->neq('user_id', $query->createNamedParameter(''))) + ->andWhere($query->expr()->isNotNull('user_id')); + + + $result = $query->execute(); + while ($row = $result->fetch()) { + $lastJoinedCall = 0; + if (!empty($row['last_joined_call'])) { + $lastJoinedCall = $this->timeFactory->getDateTime($row['last_joined_call'])->getTimestamp(); + } + + $insert + ->setParameter('room_id', (int) $row['room_id'], IQueryBuilder::PARAM_INT) + ->setParameter('actor_type', Attendee::ACTOR_USERS) + ->setParameter('actor_id', $row['user_id']) + ->setParameter('participant_type', (int) $row['participant_type'], IQueryBuilder::PARAM_INT) + ->setParameter('favorite', (bool) $row['favorite'], IQueryBuilder::PARAM_BOOL) + ->setParameter('notification_level', (int) $row['notification_level'], IQueryBuilder::PARAM_INT) + ->setParameter('last_joined_call', $lastJoinedCall, IQueryBuilder::PARAM_INT) + ->setParameter('last_read_message', (int) $row['last_read_message'], IQueryBuilder::PARAM_INT) + ->setParameter('last_mention_message', (int) $row['last_mention_message'], IQueryBuilder::PARAM_INT) + ; + + $insert->execute(); + } + $result->closeCursor(); + } +} diff --git a/lib/Migration/Version10000Date20201015143852.php b/lib/Migration/Version10000Date20201015143852.php new file mode 100644 index 00000000000..fcec06cf974 --- /dev/null +++ b/lib/Migration/Version10000Date20201015143852.php @@ -0,0 +1,52 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version10000Date20201015143852 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if ($schema->hasTable('talk_participants')) { + $schema->dropTable('talk_participants'); + return $schema; + } + + return null; + } +} diff --git a/lib/Migration/Version10000Date20201015150000.php b/lib/Migration/Version10000Date20201015150000.php new file mode 100644 index 00000000000..1180257d519 --- /dev/null +++ b/lib/Migration/Version10000Date20201015150000.php @@ -0,0 +1,62 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * The HPB is generating sessions longer than 255 chars. So we update the length + * But the install migration was fixed, so this only does something on update. + */ +class Version10000Date20201015150000 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if ($schema->hasTable('talk_sessions')) { + $table = $schema->getTable('talk_sessions'); + + $column = $table->getColumn('session_id'); + + if ($column->getLength() !== 512) { + $column->setLength(512); + return $schema; + } + } + + return null; + } +} diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php new file mode 100644 index 00000000000..3189ede37f5 --- /dev/null +++ b/lib/Model/Attendee.php @@ -0,0 +1,123 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Model; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setRoomId(int $roomId) + * @method string getRoomId() + * @method void setActorType(string $actorType) + * @method string getActorType() + * @method void setActorId(string $actorId) + * @method string getActorId() + * @method void setDisplayName(string $displayName) + * @method string getDisplayName() + * @method void setPin(string $pin) + * @method string getPin() + * @method void setParticipantType(int $participantType) + * @method int getParticipantType() + * @method void setFavorite(bool $favorite) + * @method bool isFavorite() + * @method void setNotificationLevel(int $notificationLevel) + * @method int getNotificationLevel() + * @method void setLastJoinedCall(int $lastJoinedCall) + * @method int getLastJoinedCall() + * @method void setLastReadMessage(int $lastReadMessage) + * @method int getLastReadMessage() + * @method void setLastMentionMessage(int $lastMentionMessage) + * @method int getLastMentionMessage() + */ +class Attendee extends Entity { + public const ACTOR_USERS = 'users'; + public const ACTOR_GUESTS = 'guests'; + public const ACTOR_EMAILS = 'emails'; + + /** @var int */ + protected $roomId; + + /** @var string */ + protected $actorType; + + /** @var string */ + protected $actorId; + + /** @var string */ + protected $displayName; + + /** @var string */ + protected $pin; + + /** @var int */ + protected $participantType; + + /** @var bool */ + protected $favorite; + + /** @var int */ + protected $notificationLevel; + + /** @var int */ + protected $lastJoinedCall; + + /** @var int */ + protected $lastReadMessage; + + /** @var int */ + protected $lastMentionMessage; + + public function __construct() { + $this->addType('roomId', 'int'); + $this->addType('actorType', 'string'); + $this->addType('actorId', 'string'); + $this->addType('displayName', 'string'); + $this->addType('pin', 'string'); + $this->addType('participantType', 'int'); + $this->addType('favorite', 'bool'); + $this->addType('notificationLevel', 'int'); + $this->addType('lastJoinedCall', 'int'); + $this->addType('lastReadMessage', 'int'); + $this->addType('lastMentionMessage', 'int'); + } + + /** + * @return array + */ + public function asArray(): array { + return [ + 'id' => $this->getId(), + 'room_id' => $this->getRoomId(), + 'actor_type' => $this->getActorType(), + 'actor_id' => $this->getActorId(), + // FIXME 'display_name' => $this->getDisplayName(), + 'pin' => $this->getPin(), + 'participant_type' => $this->getParticipantType(), + 'favorite' => $this->isFavorite(), + 'notification_level' => $this->getNotificationLevel(), + 'last_joined_call' => $this->getLastJoinedCall(), + 'last_read_message' => $this->getLastReadMessage(), + 'last_mention_message' => $this->getLastMentionMessage(), + ]; + } +} diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php new file mode 100644 index 00000000000..d071c753cc5 --- /dev/null +++ b/lib/Model/AttendeeMapper.php @@ -0,0 +1,131 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Model; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @method Attendee mapRowToEntity(array $row) + */ +class AttendeeMapper extends QBMapper { + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'talk_attendees', Attendee::class); + } + + /** + * @param int $roomId + * @param string $actorType + * @param string $actorId + * @return Attendee + * @throws \OCP\AppFramework\Db\DoesNotExistException + */ + public function findByActor(int $roomId, string $actorType, string $actorId): Attendee { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('actor_type', $query->createNamedParameter($actorType))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId))); + + return $this->findEntity($query); + } + + /** + * @param int $roomId + * @param string $actorType + * @param int|null $lastJoinedCall + * @return Attendee[] + */ + public function getActorsByType(int $roomId, string $actorType, ?int $lastJoinedCall = null): array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType))); + + if ($lastJoinedCall !== null) { + $query->andWhere($query->expr()->gte('last_joined_call', $query->createNamedParameter($lastJoinedCall, IQueryBuilder::PARAM_INT))); + } + + return $this->findEntities($query); + } + + /** + * @param int $roomId + * @param int[] $participantType + * @return int + */ + public function countActorsByParticipantType(int $roomId, array $participantType): int { + $query = $this->db->getQueryBuilder(); + $query->select($query->func()->count('*', 'num_actors')) + ->from($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); + + // TODO Should exclude groups and circles when we add them + + if (!empty($participantType)) { + $query->andWhere($query->expr()->in('participant_type', $query->createNamedParameter($participantType, IQueryBuilder::PARAM_INT_ARRAY))); + } + + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + return (int) ($row['num_actors'] ?? 0); + } + + /** + * @param int[] $ids + * @return int Number of deleted entities + */ + public function deleteByIds(array $ids): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->in('id', $query->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + + return (int) $query->execute(); + } + + public function createAttendeeFromRow(array $row): Attendee { + return $this->mapRowToEntity([ + 'id' => $row['a_id'], + 'room_id' => $row['room_id'], + 'actor_type' => $row['actor_type'], + 'actor_id' => $row['actor_id'], + 'pin' => $row['pin'], + 'participant_type' => (int) $row['participant_type'], + 'favorite' => (bool) $row['favorite'], + 'notification_level' => (int) $row['notification_level'], + 'last_joined_call' => (int) $row['last_joined_call'], + 'last_read_message' => (int) $row['last_read_message'], + 'last_mention_message' => (int) $row['last_mention_message'], + ]); + } +} diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 6d54f04c546..97d578c184e 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -158,7 +158,7 @@ public function getActorDisplayName(): string { public function isReplyable(): bool { return $this->getMessageType() !== 'system' && $this->getMessageType() !== 'command' && - \in_array($this->getActorType(), ['users', 'guests']); + \in_array($this->getActorType(), [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS]); } public function toArray(): array { diff --git a/lib/Model/Session.php b/lib/Model/Session.php new file mode 100644 index 00000000000..0acabf59383 --- /dev/null +++ b/lib/Model/Session.php @@ -0,0 +1,79 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Model; + +use OCP\AppFramework\Db\Entity; + +/** + * A session is the "I'm online in this conversation" state of Talk, you get one + * when opening the conversation while the inCall flag tells if you are just + * online (chatting), or in a call (with audio, camera or even sip). + * Currently it's limited to 1 per attendee, but the plan is to remove this + * restriction in the future, so e.g. in the future you can join with your phone + * on the SIP bridge, have your video/screenshare on the laptop and chat in the + * mobile app. + * + * @method void setAttendeeId(int $attendeeId) + * @method string getAttendeeId() + * @method void setSessionId(string $sessionId) + * @method string getSessionId() + * @method void setInCall(int $inCall) + * @method int getInCall() + * @method void setLastPing(int $lastPing) + * @method int getLastPing() + */ +class Session extends Entity { + + /** @var int */ + protected $attendeeId; + + /** @var string */ + protected $sessionId; + + /** @var int */ + protected $inCall; + + /** @var int */ + protected $lastPing; + + public function __construct() { + $this->addType('attendeeId', 'int'); + $this->addType('sessionId', 'string'); + $this->addType('inCall', 'int'); + $this->addType('lastPing', 'int'); + } + + /** + * @return array + */ + public function asArray(): array { + return [ + 'id' => $this->getId(), + 'attendee_id' => $this->getAttendeeId(), + 'session_id' => $this->getSessionId(), + 'in_call' => $this->getInCall(), + 'last_ping' => $this->getLastPing(), + ]; + } +} diff --git a/lib/Model/SessionMapper.php b/lib/Model/SessionMapper.php new file mode 100644 index 00000000000..d623ff335d8 --- /dev/null +++ b/lib/Model/SessionMapper.php @@ -0,0 +1,89 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Model; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @method Session mapRowToEntity(array $row) + */ +class SessionMapper extends QBMapper { + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'talk_sessions', Session::class); + } + + /** + * @param string $sessionId + * @return Session + * @throws \OCP\AppFramework\Db\DoesNotExistException + */ + public function findBySessionId(string $sessionId): Session { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('session_id', $query->createNamedParameter($sessionId))); + + return $this->findEntity($query); + } + + /** + * @param int $attendeeId + * @return int Number of deleted entities + */ + public function deleteByAttendeeId(int $attendeeId): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->eq('attendee_id', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT))); + + return (int) $query->execute(); + } + + /** + * @param int[] $ids + * @return int Number of deleted entities + */ + public function deleteByIds(array $ids): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->in('id', $query->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + + return (int) $query->execute(); + } + + public function createSessionFromRow(array $row): Session { + return $this->mapRowToEntity([ + 'id' => $row['s_id'], + 'session_id' => $row['session_id'], + 'attendee_id' => (int) $row['a_id'], + 'in_call' => (int) $row['in_call'], + 'last_ping' => (int) $row['last_ping'], + ]); + } +} diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php index 02cd0aaf728..1ba06641f49 100644 --- a/lib/Notification/Listener.php +++ b/lib/Notification/Listener.php @@ -26,7 +26,9 @@ use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Notification\IManager; @@ -38,6 +40,8 @@ class Listener { /** @var IManager */ protected $notificationManager; + /** @var ParticipantService */ + protected $participantsService; /** @var IEventDispatcher */ protected $dispatcher; /** @var IUserSession */ @@ -51,11 +55,13 @@ class Listener { protected $shouldSendCallNotification = false; public function __construct(IManager $notificationManager, + ParticipantService $participantsService, IEventDispatcher $dispatcher, IUserSession $userSession, ITimeFactory $timeFactory, LoggerInterface $logger) { $this->notificationManager = $notificationManager; + $this->participantsService = $participantsService; $this->dispatcher = $dispatcher; $this->userSession = $userSession; $this->timeFactory = $timeFactory; @@ -137,13 +143,18 @@ public function generateInvitation(Room $room, array $participants): void { } foreach ($participants as $participant) { - if ($actorId === $participant['userId']) { + if ($participant['actorType'] !== Attendee::ACTOR_USERS) { + // No user => no activity + continue; + } + + if ($actorId === $participant['actorId']) { // No activity for self-joining and the creator continue; } try { - $notification->setUser($participant['userId']); + $notification->setUser($participant['actorId']); $this->notificationManager->notify($notification); } catch (\InvalidArgumentException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -236,7 +247,7 @@ public function sendCallNotifications(Room $room): void { return; } - $userIds = $room->getNotInCallUserIds(); + $userIds = $this->participantsService->getParticipantUserIdsNotInCall($room); foreach ($userIds as $userId) { if ($actorId === $userId) { continue; diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 2c5bd965bd7..306e1df7c37 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -32,6 +32,7 @@ use OCA\Talk\Manager; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\Comments\NotFoundException; use OCP\IL10N; use OCP\IURLGenerator; @@ -64,6 +65,8 @@ class Notifier implements INotifier { private $shareManager; /** @var Manager */ protected $manager; + /** @var ParticipantService */ + protected $participantService; /** @var INotificationManager */ protected $notificationManager; /** @var CommentsManager */ @@ -85,6 +88,7 @@ public function __construct(IFactory $lFactory, GuestManager $guestManager, IShareManager $shareManager, Manager $manager, + ParticipantService $participantService, INotificationManager $notificationManager, CommentsManager $commentManager, MessageParser $messageParser, @@ -96,6 +100,7 @@ public function __construct(IFactory $lFactory, $this->guestManager = $guestManager; $this->shareManager = $shareManager; $this->manager = $manager; + $this->participantService = $participantService; $this->notificationManager = $notificationManager; $this->commentManager = $commentManager; $this->messageParser = $messageParser; @@ -487,7 +492,7 @@ protected function parseInvitation(INotification $notification, Room $room, IL10 $roomName = $room->getDisplayName($notification->getUser()); if ($room->getType() === Room::ONE_TO_ONE_CALL) { $subject = $l->t('{user} invited you to a private conversation'); - if ($room->hasSessionsInCall()) { + if ($this->participantService->hasActiveSessionsInCall($room)) { $notification = $this->addActionButton($notification, $l->t('Join call')); } else { $notification = $this->addActionButton($notification, $l->t('View chat'), false); @@ -512,7 +517,7 @@ protected function parseInvitation(INotification $notification, Room $room, IL10 ); } elseif (\in_array($room->getType(), [Room::GROUP_CALL, Room::PUBLIC_CALL], true)) { $subject = $l->t('{user} invited you to a group conversation: {call}'); - if ($room->hasSessionsInCall()) { + if ($this->participantService->hasActiveSessionsInCall($room)) { $notification = $this->addActionButton($notification, $l->t('Join call')); } else { $notification = $this->addActionButton($notification, $l->t('View chat'), false); @@ -561,7 +566,7 @@ protected function parseCall(INotification $notification, Room $room, IL10N $l): $calleeId = $parameters['callee']; $user = $this->userManager->get($calleeId); if ($user instanceof IUser) { - if ($this->notificationManager->isPreparingPushNotification() || $room->hasSessionsInCall()) { + if ($this->notificationManager->isPreparingPushNotification() || $this->participantService->hasActiveSessionsInCall($room)) { $notification = $this->addActionButton($notification, $l->t('Answer call')); $subject = $l->t('{user} wants to talk with you'); } else { @@ -590,7 +595,7 @@ protected function parseCall(INotification $notification, Room $room, IL10N $l): throw new AlreadyProcessedException(); } } elseif (\in_array($room->getType(), [Room::GROUP_CALL, Room::PUBLIC_CALL], true)) { - if ($this->notificationManager->isPreparingPushNotification() || $room->hasSessionsInCall()) { + if ($this->notificationManager->isPreparingPushNotification() || $this->participantService->hasActiveSessionsInCall($room)) { $notification = $this->addActionButton($notification, $l->t('Join call')); $subject = $l->t('A group call has started in {call}'); } else { @@ -646,7 +651,7 @@ protected function parsePasswordRequest(INotification $notification, Room $room, throw new AlreadyProcessedException(); } - $callIsActive = $this->notificationManager->isPreparingPushNotification() || $room->hasSessionsInCall(); + $callIsActive = $this->notificationManager->isPreparingPushNotification() || $this->participantService->hasActiveSessionsInCall($room); if ($callIsActive) { $notification = $this->addActionButton($notification, $l->t('Answer call')); } else { diff --git a/lib/Participant.php b/lib/Participant.php index 3b6d61c1235..b0a6bfaa6fc 100644 --- a/lib/Participant.php +++ b/lib/Participant.php @@ -25,9 +25,9 @@ namespace OCA\Talk; -use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCP\IConfig; -use OCP\IDBConnection; class Participant { public const OWNER = 1; @@ -48,190 +48,49 @@ class Participant { public const NOTIFY_MENTION = 2; public const NOTIFY_NEVER = 3; - /** @var IDBConnection */ - protected $db; - /** @var IConfig */ - protected $config; /** @var Room */ protected $room; - /** @var string */ - protected $user; - /** @var int */ - protected $participantType; - /** @var int */ - protected $lastPing; - /** @var string */ - protected $sessionId; - /** @var int */ - protected $inCall; - /** @var int */ - protected $notificationLevel; - /** @var bool */ - private $isFavorite; - /** @var int */ - private $lastReadMessage; - /** @var int */ - private $lastMentionMessage; - /** @var \DateTime|null */ - private $lastJoinedCall; - - public function __construct(IDBConnection $db, - IConfig $config, - Room $room, - string $user, - int $participantType, - int $lastPing, - string $sessionId, - int $inCall, - int $notificationLevel, - bool $isFavorite, - int $lastReadMessage, - int $lastMentionMessage, - \DateTime $lastJoinedCall = null) { - $this->db = $db; - $this->config = $config; + /** @var Attendee */ + protected $attendee; + /** @var Session|null */ + protected $session; + + public function __construct(Room $room, + Attendee $attendee, + ?Session $session) { $this->room = $room; - $this->user = $user; - $this->participantType = $participantType; - $this->lastPing = $lastPing; - $this->sessionId = $sessionId; - $this->inCall = $inCall; - $this->notificationLevel = $notificationLevel; - $this->isFavorite = $isFavorite; - $this->lastReadMessage = $lastReadMessage; - $this->lastMentionMessage = $lastMentionMessage; - $this->lastJoinedCall = $lastJoinedCall; + $this->attendee = $attendee; + $this->session = $session; } - public function getUser(): string { - return $this->user; + public function getAttendee(): Attendee { + return $this->attendee; } - public function getParticipantType(): int { - return $this->participantType; + public function getSession(): ?Session { + return $this->session; + } + + public function setSession(Session $session): void { + $this->session = $session; } public function isGuest(): bool { - return \in_array($this->participantType, [self::GUEST, self::GUEST_MODERATOR], true); + $participantType = $this->attendee->getParticipantType(); + return \in_array($participantType, [self::GUEST, self::GUEST_MODERATOR], true); } public function hasModeratorPermissions(bool $guestModeratorAllowed = true): bool { + $participantType = $this->attendee->getParticipantType(); if (!$guestModeratorAllowed) { - return \in_array($this->participantType, [self::OWNER, self::MODERATOR], true); - } - - return \in_array($this->participantType, [self::OWNER, self::MODERATOR, self::GUEST_MODERATOR], true); - } - - public function getLastPing(): int { - return $this->lastPing; - } - - public function getSessionId(): string { - return $this->sessionId; - } - - public function getInCallFlags(): int { - return $this->inCall; - } - - /** - * @return \DateTime|null - */ - public function getJoinedCall(): ?\DateTime { - return $this->lastJoinedCall; - } - - public function isFavorite(): bool { - return $this->isFavorite; - } - - public function setFavorite(bool $favor): bool { - if (!$this->user) { - return false; - } - - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('favorite', $query->createNamedParameter((int) $favor, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId()))); - $query->execute(); - - $this->isFavorite = $favor; - return true; - } - - public function getNotificationLevel(): int { - return $this->notificationLevel; - } - - public function setNotificationLevel(int $notificationLevel): bool { - if (!$this->user) { - return false; - } - - if (!\in_array($notificationLevel, [ - self::NOTIFY_ALWAYS, - self::NOTIFY_MENTION, - self::NOTIFY_NEVER - ], true)) { - return false; - } - - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('notification_level', $query->createNamedParameter($notificationLevel, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId()))); - $query->execute(); - - $this->notificationLevel = $notificationLevel; - return true; - } - - public function getLastReadMessage(): int { - return $this->lastReadMessage; - } - - public function setLastReadMessage(int $messageId): bool { - if (!$this->user) { - return false; - } - - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('last_read_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId()))); - $query->execute(); - - $this->lastReadMessage = $messageId; - return true; - } - - public function getLastMentionMessage(): int { - return $this->lastMentionMessage; - } - - public function setLastMentionMessage(int $messageId): bool { - if (!$this->user) { - return false; + return \in_array($participantType, [self::OWNER, self::MODERATOR], true); } - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId()))); - $query->execute(); - - $this->lastMentionMessage = $messageId; - return true; + return \in_array($participantType, [self::OWNER, self::MODERATOR, self::GUEST_MODERATOR], true); } - public function canStartCall(): bool { - $defaultStartCall = (int) $this->config->getAppValue('spreed', 'start_calls', Room::START_CALL_EVERYONE); + public function canStartCall(IConfig $config): bool { + $defaultStartCall = (int) $config->getAppValue('spreed', 'start_calls', Room::START_CALL_EVERYONE); if ($defaultStartCall === Room::START_CALL_EVERYONE) { return true; diff --git a/lib/PublicShareAuth/Listener.php b/lib/PublicShareAuth/Listener.php index a0518504f26..c029a5fc409 100644 --- a/lib/PublicShareAuth/Listener.php +++ b/lib/PublicShareAuth/Listener.php @@ -30,6 +30,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\EventDispatcher\IEventDispatcher; /** @@ -83,13 +84,15 @@ public static function preventExtraUsersFromJoining(Room $room, string $userId): try { $participant = $room->getParticipant($userId); - if ($participant->getParticipantType() === Participant::OWNER) { + if ($participant->getAttendee()->getParticipantType() === Participant::OWNER) { return; } } catch (ParticipantNotFoundException $e) { } - if ($room->getActiveGuests() > 0 || \count($room->getParticipantUserIds()) > 1) { + $participantService = \OC::$server->get(ParticipantService::class); + $users = $participantService->getParticipantUserIds($room); + if ($room->getActiveGuests() > 0 || \count($users) > 1) { throw new \OverflowException('Only the owner and another participant are allowed in rooms to request the password for a share'); } } @@ -108,7 +111,9 @@ public static function preventExtraGuestsFromJoining(Room $room): void { return; } - if ($room->getActiveGuests() > 0 || \count($room->getParticipantUserIds()) > 1) { + $participantService = \OC::$server->get(ParticipantService::class); + $users = $participantService->getParticipantUserIds($room); + if ($room->getActiveGuests() > 0 || \count($users) > 1) { throw new \OverflowException('Only the owner and another participant are allowed in rooms to request the password for a share'); } } diff --git a/lib/Room.php b/lib/Room.php index 09d52f69b7f..ee655156f0e 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -27,31 +27,32 @@ namespace OCA\Talk; -use OCA\Talk\Events\AddParticipantsEvent; -use OCA\Talk\Events\JoinRoomGuestEvent; -use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Events\ModifyLobbyEvent; -use OCA\Talk\Events\ModifyParticipantEvent; use OCA\Talk\Events\ModifyRoomEvent; -use OCA\Talk\Events\ParticipantEvent; -use OCA\Talk\Events\RemoveParticipantEvent; -use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Events\SignalingRoomPropertiesEvent; use OCA\Talk\Events\VerifyRoomPasswordEvent; -use OCA\Talk\Exceptions\InvalidPasswordException; use OCA\Talk\Exceptions\ParticipantNotFoundException; -use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; -use OCP\IUser; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; class Room { + + /** + * Regex that matches SIP incompatible rooms: + * 1. duplicate digit: …11… + * 2. leading zero: 0… + * 3. non-digit: …a… + */ + public const SIP_INCOMPATIBLE_REGEX = '/((\d)(?=\2+)|^0|\D)/'; + public const UNKNOWN_CALL = -1; public const ONE_TO_ONE_CALL = 1; public const GROUP_CALL = 2; @@ -81,6 +82,8 @@ class Room { public const EVENT_AFTER_READONLY_SET = self::class . '::postSetReadOnly'; public const EVENT_BEFORE_LOBBY_STATE_SET = self::class . '::preSetLobbyState'; public const EVENT_AFTER_LOBBY_STATE_SET = self::class . '::postSetLobbyState'; + public const EVENT_BEFORE_SIP_ENABLED_SET = self::class . '::preSetSIPEnabled'; + public const EVENT_AFTER_SIP_ENABLED_SET = self::class . '::postSetSIPEnabled'; public const EVENT_BEFORE_USERS_ADD = self::class . '::preAddUsers'; public const EVENT_AFTER_USERS_ADD = self::class . '::postAddUsers'; public const EVENT_BEFORE_PARTICIPANT_TYPE_SET = self::class . '::preSetParticipantType'; @@ -125,6 +128,8 @@ class Room { private $readOnly; /** @var int */ private $lobbyState; + /** @var int */ + private $sipEnabled; /** @var int|null */ private $assignedSignalingServer; /** @var \DateTime|null */ @@ -165,6 +170,7 @@ public function __construct(Manager $manager, int $type, int $readOnly, int $lobbyState, + int $sipEnabled, ?int $assignedSignalingServer, string $token, string $name, @@ -187,6 +193,7 @@ public function __construct(Manager $manager, $this->type = $type; $this->readOnly = $readOnly; $this->lobbyState = $lobbyState; + $this->sipEnabled = $sipEnabled; $this->assignedSignalingServer = $assignedSignalingServer; $this->token = $token; $this->name = $name; @@ -218,6 +225,10 @@ public function getLobbyState(): int { return $this->lobbyState; } + public function getSIPEnabled(): int { + return $this->sipEnabled; + } + public function getLobbyTimer(): ?\DateTime { $this->validateTimer(); return $this->lobbyTimer; @@ -240,13 +251,17 @@ public function getToken(): string { public function getName(): string { if ($this->type === self::ONE_TO_ONE_CALL) { if ($this->name === '') { + // TODO use DI + $participantService = \OC::$server->get(ParticipantService::class); // Fill the room name with the participants for 1-to-1 conversations - $users = $this->getParticipantUserIds(); + $users = $participantService->getParticipantUserIds($this); sort($users); $this->setName(json_encode($users), ''); } elseif (strpos($this->name, '["') !== 0) { + // TODO use DI + $participantService = \OC::$server->get(ParticipantService::class); // Not the json array, but the old fallback when someone left - $users = $this->getParticipantUserIds(); + $users = $participantService->getParticipantUserIds($this); if (count($users) !== 2) { $users[] = $this->name; } @@ -319,6 +334,7 @@ public function getPropertiesForSignaling(string $userId): array { 'lobby-timer' => $this->getLobbyTimer(), 'read-only' => $this->getReadOnly(), 'active-since' => $this->getActiveSince(), + 'sip-enabled' => $this->getSIPEnabled(), ]; $event = new SignalingRoomPropertiesEvent($this, $userId, $properties); @@ -342,9 +358,14 @@ public function getParticipant(?string $userId): Participant { $query = $this->db->getQueryBuilder(); $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId()))); + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) + ->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) + ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); @@ -373,9 +394,97 @@ public function getParticipantBySession(?string $sessionId): Participant { $query = $this->db->getQueryBuilder(); $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('session_id', $query->createNamedParameter($sessionId))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId()))); + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_sessions', 's') + ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id')) + ->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new ParticipantNotFoundException('User is not a participant'); + } + + return $this->manager->createParticipantObject($this, $row); + } + + /** + * @param string $pin + * @return Participant + * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) + */ + public function getParticipantByPin(string $pin): Participant { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) + ->andWhere($query->expr()->eq('a.pin', $query->createNamedParameter($pin))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new ParticipantNotFoundException('User is not a participant'); + } + + return $this->manager->createParticipantObject($this, $row); + } + + /** + * @param int $attendeeId + * @return Participant + * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) + */ + public function getParticipantByAttendeeId(int $attendeeId): Participant { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) + ->andWhere($query->expr()->eq('a.id', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new ParticipantNotFoundException('User is not a participant'); + } + + return $this->manager->createParticipantObject($this, $row); + } + + /** + * @param string $actorType + * @param string $actorId + * @return Participant + * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) + */ + public function getParticipantByActor(string $actorType, string $actorId): Participant { + if ($actorType === Attendee::ACTOR_USERS) { + return $this->getParticipant($actorId); + } + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType))) + ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); @@ -392,8 +501,8 @@ public function deleteRoom(): void { $this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_DELETE, $event); $query = $this->db->getQueryBuilder(); - // Delete all participants - $query->delete('talk_participants') + // Delete attendees + $query->delete('talk_attendees') ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); $query->execute(); @@ -572,7 +681,7 @@ public function setType(int $newType): bool { if ($oldType === self::PUBLIC_CALL) { // Kick all guests and users that were not invited $query = $this->db->getQueryBuilder(); - $query->delete('talk_participants') + $query->delete('talk_attendees') ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->in('participant_type', $query->createNamedParameter([Participant::GUEST, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY))); $query->execute(); @@ -658,310 +767,43 @@ public function setLobby(int $newState, ?\DateTime $dateTime, bool $timerReached $this->dispatcher->dispatch(self::EVENT_AFTER_LOBBY_STATE_SET, $event); - if ($newState === Webinary::LOBBY_NON_MODERATORS) { - $participants = $this->getParticipantsInCall(); - foreach ($participants as $participant) { - if ($participant->hasModeratorPermissions()) { - continue; - } - - $this->changeInCall($participant, Participant::FLAG_DISCONNECTED); - } - } - return true; } - public function ensureOneToOneRoomIsFilled(): void { - if ($this->getType() !== self::ONE_TO_ONE_CALL) { - return; - } - - $users = json_decode($this->getName(), true); - $participants = $this->getParticipantUserIds(); - $missingUsers = array_diff($users, $participants); - - foreach ($missingUsers as $userId) { - if ($this->manager->isValidParticipant($userId)) { - $this->addUsers([ - 'userId' => $userId, - 'participantType' => Participant::OWNER, - ]); - } - } - } - - /** - * @param array ...$participants - */ - public function addUsers(array ...$participants): void { - $event = new AddParticipantsEvent($this, $participants); - $this->dispatcher->dispatch(self::EVENT_BEFORE_USERS_ADD, $event); - - $lastMessage = 0; - if ($this->getLastMessage() instanceof IComment) { - $lastMessage = (int) $this->getLastMessage()->getId(); - } - - $query = $this->db->getQueryBuilder(); - $query->insert('talk_participants') - ->values( - [ - 'user_id' => $query->createParameter('user_id'), - 'session_id' => $query->createParameter('session_id'), - 'participant_type' => $query->createParameter('participant_type'), - 'room_id' => $query->createNamedParameter($this->getId()), - 'last_ping' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'last_read_message' => $query->createNamedParameter($lastMessage, IQueryBuilder::PARAM_INT), - ] - ); - - foreach ($participants as $participant) { - $query->setParameter('user_id', $participant['userId']) - ->setParameter('session_id', $participant['sessionId'] ?? '0') - ->setParameter('participant_type', $participant['participantType'] ?? Participant::USER, IQueryBuilder::PARAM_INT); - - $query->execute(); - } - - $this->dispatcher->dispatch(self::EVENT_AFTER_USERS_ADD, $event); - } - - /** - * @param Participant $participant - * @param int $participantType - */ - public function setParticipantType(Participant $participant, int $participantType): void { - $event = new ModifyParticipantEvent($this, $participant, 'type', $participantType, $participant->getParticipantType()); - $this->dispatcher->dispatch(self::EVENT_BEFORE_PARTICIPANT_TYPE_SET, $event); - - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('participant_type', $query->createNamedParameter($participantType, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($participant->getUser()))); - - if ($participant->getUser() === '') { - $query->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($participant->getSessionId()))); - } - - $query->execute(); - - $this->dispatcher->dispatch(self::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event); - } - - /** - * @param IUser $user - * @param string $reason - */ - public function removeUser(IUser $user, string $reason): void { - try { - $participant = $this->getParticipant($user->getUID()); - } catch (ParticipantNotFoundException $e) { - return; - } - - $event = new RemoveUserEvent($this, $participant, $user, $reason); - $this->dispatcher->dispatch(self::EVENT_BEFORE_USER_REMOVE, $event); + public function setSIPEnabled(int $newSipEnabled): bool { + $oldSipEnabled = $this->sipEnabled; - $query = $this->db->getQueryBuilder(); - $query->delete('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($user->getUID()))); - $query->execute(); - - $this->dispatcher->dispatch(self::EVENT_AFTER_USER_REMOVE, $event); - } - - /** - * @param Participant $participant - * @param string $reason - */ - public function removeParticipantBySession(Participant $participant, string $reason): void { - $event = new RemoveParticipantEvent($this, $participant, $reason); - $this->dispatcher->dispatch(self::EVENT_BEFORE_PARTICIPANT_REMOVE, $event); - - $query = $this->db->getQueryBuilder(); - $query->delete('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($participant->getSessionId()))); - $query->execute(); - - $this->dispatcher->dispatch(self::EVENT_AFTER_PARTICIPANT_REMOVE, $event); - } - - /** - * @param IUser $user - * @param string $password - * @param bool $passedPasswordProtection - * @return string - * @throws InvalidPasswordException - * @throws UnauthorizedException - */ - public function joinRoom(IUser $user, string $password, bool $passedPasswordProtection = false): string { - $event = new JoinRoomUserEvent($this, $user, $password, $passedPasswordProtection); - $this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_CONNECT, $event); - - if ($event->getCancelJoin() === true) { - $this->removeUser($user, self::PARTICIPANT_LEFT); - throw new UnauthorizedException('Participant is not allowed to join'); + if ($newSipEnabled === $oldSipEnabled) { + return false; } - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('session_id', $query->createParameter('session_id')) - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($user->getUID()))); - - $sessionId = $this->secureRandom->generate(255); - $query->setParameter('session_id', $sessionId); - $result = $query->execute(); - - if ($result === 0) { - if (!$event->getPassedPasswordProtection() && !$this->verifyPassword($password)['result']) { - throw new InvalidPasswordException(); - } - - // User joining a public room, without being invited - $this->addUsers([ - 'userId' => $user->getUID(), - 'participantType' => Participant::USER_SELF_JOINED, - 'sessionId' => $sessionId, - ]); + if (!in_array($this->getType(), [self::GROUP_CALL, self::PUBLIC_CALL], true)) { + return false; } - while (!$this->isSessionUnique($sessionId)) { - $sessionId = $this->secureRandom->generate(255); - $query->setParameter('session_id', $sessionId); - $query->execute(); + if (!in_array($newSipEnabled, [Webinary::SIP_ENABLED, Webinary::SIP_DISABLED], true)) { + return false; } - $this->dispatcher->dispatch(self::EVENT_AFTER_ROOM_CONNECT, $event); - - return $sessionId; - } - - /** - * @param string $userId - * @param string|null $sessionId - */ - public function leaveRoom(string $userId, ?string $sessionId = null): void { - try { - $participant = $this->getParticipant($userId); - } catch (ParticipantNotFoundException $e) { - return; + if (preg_match(self::SIP_INCOMPATIBLE_REGEX, $this->token)) { + return false; } - $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); - - // Reset session when leaving a normal room - $query = $this->db->getQueryBuilder(); - $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($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 ($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(); + $event = new ModifyRoomEvent($this, 'sipEnabled', $newSipEnabled, $oldSipEnabled); + $this->dispatcher->dispatch(self::EVENT_BEFORE_SIP_ENABLED_SET, $event); - // 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($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 ($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->update('talk_rooms') + ->set('sip_enabled', $query->createNamedParameter($newSipEnabled, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); $query->execute(); - $this->dispatcher->dispatch(self::EVENT_AFTER_ROOM_DISCONNECT, $event); - } + $this->sipEnabled = $newSipEnabled; - /** - * @param string $password - * @param bool $passedPasswordProtection - * @return string - * @throws InvalidPasswordException - * @throws UnauthorizedException - */ - public function joinRoomGuest(string $password, bool $passedPasswordProtection = false): string { - $event = new JoinRoomGuestEvent($this, $password, $passedPasswordProtection); - $this->dispatcher->dispatch(self::EVENT_BEFORE_GUEST_CONNECT, $event); + $this->dispatcher->dispatch(self::EVENT_AFTER_SIP_ENABLED_SET, $event); - if ($event->getCancelJoin()) { - throw new UnauthorizedException('Participant is not allowed to join'); - } - - if (!$event->getPassedPasswordProtection() && !$this->verifyPassword($password)['result']) { - throw new InvalidPasswordException(); - } - - $lastMessage = 0; - if ($this->getLastMessage() instanceof IComment) { - $lastMessage = (int) $this->getLastMessage()->getId(); - } - $sessionId = $this->secureRandom->generate(255); - while (!$this->db->insertIfNotExist('*PREFIX*talk_participants', [ - 'user_id' => '', - 'room_id' => $this->getId(), - 'last_ping' => 0, - 'session_id' => $sessionId, - 'participant_type' => Participant::GUEST, - 'last_read_message' => $lastMessage, - ], ['session_id'])) { - $sessionId = $this->secureRandom->generate(255); - } - - $this->dispatcher->dispatch(self::EVENT_AFTER_GUEST_CONNECT, $event); - - return $sessionId; - } - - public function changeInCall(Participant $participant, int $flags): void { - $event = new ModifyParticipantEvent($this, $participant, 'inCall', $flags, $participant->getInCallFlags()); - if ($flags !== Participant::FLAG_DISCONNECTED) { - $this->dispatcher->dispatch(self::EVENT_BEFORE_SESSION_JOIN_CALL, $event); - } else { - $this->dispatcher->dispatch(self::EVENT_BEFORE_SESSION_LEAVE_CALL, $event); - } - - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('in_call', $query->createNamedParameter($flags, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('session_id', $query->createNamedParameter($participant->getSessionId()))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); - - if ($flags !== Participant::FLAG_DISCONNECTED) { - $query->set('last_joined_call', $query->createNamedParameter( - $this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATE - )); - } - - $query->execute(); - - if ($flags !== Participant::FLAG_DISCONNECTED) { - $this->dispatcher->dispatch(self::EVENT_AFTER_SESSION_JOIN_CALL, $event); - } else { - $this->dispatcher->dispatch(self::EVENT_AFTER_SESSION_LEAVE_CALL, $event); - } + return true; } /** @@ -984,372 +826,4 @@ public function verifyPassword(string $password): array { 'url' => '', ]; } - - /** - * @param string $sessionId - * @return bool - */ - protected function isSessionUnique(string $sessionId): bool { - $query = $this->db->getQueryBuilder(); - $query->selectAlias($query->createFunction('COUNT(*)'), 'num_sessions') - ->from('talk_participants') - ->where($query->expr()->eq('session_id', $query->createNamedParameter($sessionId))); - $result = $query->execute(); - $numSessions = (int) $result->fetchColumn(); - $result->closeCursor(); - - return $numSessions === 1; - } - - public function cleanGuestParticipants(): void { - $event = new RoomEvent($this); - $this->dispatcher->dispatch(self::EVENT_BEFORE_GUESTS_CLEAN, $event); - - $query = $this->db->getQueryBuilder(); - $query->delete('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->emptyString('user_id')) - ->andWhere($query->expr()->lte('last_ping', $query->createNamedParameter($this->timeFactory->getTime() - 100, IQueryBuilder::PARAM_INT))); - $query->execute(); - - $this->dispatcher->dispatch(self::EVENT_AFTER_GUESTS_CLEAN, $event); - } - - /** - * @param int $lastPing When the last ping is older than the given timestamp, the user is ignored - * @return Participant[] - */ - public function getParticipants(int $lastPing = 0): array { - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); - - if ($lastPing > 0) { - $query->andWhere($query->expr()->gt('last_ping', $query->createNamedParameter($lastPing, IQueryBuilder::PARAM_INT))); - } - - $result = $query->execute(); - - $participants = []; - while ($row = $result->fetch()) { - $participants[] = $this->manager->createParticipantObject($this, $row); - } - $result->closeCursor(); - - return $participants; - } - - /** - * @param string $search - * @param int $limit - * @param int $offset - * @return Participant[] - */ - public function searchParticipants(string $search = '', int $limit = null, int $offset = null): array { - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId()))); - - if ($search !== '') { - $query->where($query->expr()->iLike('user_id', $query->createNamedParameter( - '%' . $this->db->escapeLikeParameter($search) . '%' - ))); - } - - if ($limit !== null) { - $query->setMaxResults($limit); - } - if ($offset !== null) { - $query->setFirstResult($offset); - } - $query->orderBy('user_id', 'ASC'); - $result = $query->execute(); - - $participants = []; - while ($row = $result->fetch()) { - $participants[] = $this->manager->createParticipantObject($this, $row); - } - $result->closeCursor(); - - return $participants; - } - - /** - * @return Participant[] - */ - public function getParticipantsInCall(): array { - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->neq('in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED))); - - $result = $query->execute(); - - $participants = []; - while ($row = $result->fetch()) { - $participants[] = $this->manager->createParticipantObject($this, $row); - } - $result->closeCursor(); - - return $participants; - } - - /** - * @param int $lastPing When the last ping is older than the given timestamp, the user is ignored - * @return array[] Array of users with [users => [userId => [lastPing, sessionId]], guests => [[lastPing, sessionId]]] - * @deprecated Use self::getParticipants() instead - */ - public function getParticipantsLegacy(int $lastPing = 0): array { - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); - - if ($lastPing > 0) { - $query->andWhere($query->expr()->gt('last_ping', $query->createNamedParameter($lastPing, IQueryBuilder::PARAM_INT))); - } - - $result = $query->execute(); - - $users = $guests = []; - while ($row = $result->fetch()) { - if ($row['user_id'] !== '' && $row['user_id'] !== null) { - $users[$row['user_id']] = [ - 'inCall' => (int) $row['in_call'], - 'lastPing' => (int) $row['last_ping'], - 'sessionId' => $row['session_id'], - 'participantType' => (int) $row['participant_type'], - ]; - } else { - $guests[] = [ - 'inCall' => (int) $row['in_call'], - 'lastPing' => (int) $row['last_ping'], - 'participantType' => (int) $row['participant_type'], - 'sessionId' => $row['session_id'], - ]; - } - } - $result->closeCursor(); - - return [ - 'users' => $users, - 'guests' => $guests, - ]; - } - - /** - * @param null|\DateTime $maxLastJoined When the "last joined call" is older than the given DateTime, the user is ignored - * @return string[] - */ - public function getParticipantUserIds(\DateTime $maxLastJoined = null): array { - $query = $this->db->getQueryBuilder(); - $query->select('user_id') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->nonEmptyString('user_id')); - - if ($maxLastJoined instanceof \DateTimeInterface) { - $query->andWhere($query->expr()->gte('last_joined_call', $query->createNamedParameter($maxLastJoined, IQueryBuilder::PARAM_DATE))); - } - - $result = $query->execute(); - - $users = []; - while ($row = $result->fetch()) { - $users[] = $row['user_id']; - } - $result->closeCursor(); - - return $users; - } - - /** - * @param int $notificationLevel - * @return Participant[] Array of participants - */ - public function getParticipantsByNotificationLevel(int $notificationLevel): array { - $query = $this->db->getQueryBuilder(); - $query->select('*') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('notification_level', $query->createNamedParameter($notificationLevel, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); - - $participants = []; - while ($row = $result->fetch()) { - $participants[] = $this->manager->createParticipantObject($this, $row); - } - $result->closeCursor(); - - return $participants; - } - - /** - * @return bool - */ - public function hasActiveSessions(): bool { - $query = $this->db->getQueryBuilder(); - $query->select('room_id') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->neq('session_id', $query->createNamedParameter('0'))) - ->setMaxResults(1); - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - return (bool) $row; - } - - /** - * @return string[] - */ - public function getActiveSessions(): array { - $query = $this->db->getQueryBuilder(); - $query->select('session_id') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->neq('session_id', $query->createNamedParameter('0'))); - $result = $query->execute(); - - $sessions = []; - while ($row = $result->fetch()) { - $sessions[] = $row['session_id']; - } - $result->closeCursor(); - - return $sessions; - } - - /** - * Get all user ids which are participants in a room but currently not in the call - * @return string[] - */ - public function getNotInCallUserIds(): array { - $query = $this->db->getQueryBuilder(); - $query->select('user_id') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->nonEmptyString('user_id')) - ->andWhere($query->expr()->eq('in_call', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))); - $result = $query->execute(); - - $userIds = []; - while ($row = $result->fetch()) { - $userIds[] = $row['user_id']; - } - $result->closeCursor(); - - return $userIds; - } - - /** - * @return bool - */ - public function hasSessionsInCall(): bool { - $query = $this->db->getQueryBuilder(); - $query->select('session_id') - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->neq('in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED, IQueryBuilder::PARAM_INT))) - ->setMaxResults(1); - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - return (bool) $row; - } - - public function getNumberOfModerators(bool $ignoreGuests = true): int { - $types = [ - Participant::OWNER, - Participant::MODERATOR, - ]; - if (!$ignoreGuests) { - $types[] = Participant::GUEST_MODERATOR; - } - - $query = $this->db->getQueryBuilder(); - $query->select($query->func()->count('*', 'num_moderators')) - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->in('participant_type', $query->createNamedParameter($types, IQueryBuilder::PARAM_INT_ARRAY))); - - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - return (int) ($row['num_moderators'] ?? 0); - } - - /** - * @param bool $ignoreGuests - * @param int $lastPing When the last ping is older than the given timestamp, the user is ignored - * @return int - */ - public function getNumberOfParticipants(bool $ignoreGuests = true, int $lastPing = 0): int { - $query = $this->db->getQueryBuilder(); - $query->select($query->func()->count('*', 'num_participants')) - ->from('talk_participants') - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); - - if ($lastPing > 0) { - $query->andWhere($query->expr()->gt('last_ping', $query->createNamedParameter($lastPing, IQueryBuilder::PARAM_INT))); - } - - if ($ignoreGuests) { - $query->andWhere($query->expr()->notIn('participant_type', $query->createNamedParameter([ - Participant::GUEST, - Participant::GUEST_MODERATOR, - Participant::USER_SELF_JOINED, - ], IQueryBuilder::PARAM_INT_ARRAY))); - } - - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - return (int) ($row['num_participants'] ?? 0); - } - - public function markUsersAsMentioned(array $userIds, int $messageId): void { - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->in('user_id', $query->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); - $query->execute(); - } - - /** - * @param string|null $userId - * @param string $sessionId - * @param int $timestamp - */ - public function ping(?string $userId, string $sessionId, int $timestamp): void { - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('last_ping', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('user_id', $query->createNamedParameter((string) $userId))) - ->andWhere($query->expr()->eq('session_id', $query->createNamedParameter($sessionId))) - ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); - - $query->execute(); - } - - /** - * @param string[] $sessionIds - * @param int $timestamp - */ - public function pingSessionIds(array $sessionIds, int $timestamp): void { - $query = $this->db->getQueryBuilder(); - $query->update('talk_participants') - ->set('last_ping', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->in('session_id', $query->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY))); - - $query->execute(); - } } diff --git a/lib/Search/ConversationSearch.php b/lib/Search/ConversationSearch.php index 6db88d459a7..89ace0a6242 100644 --- a/lib/Search/ConversationSearch.php +++ b/lib/Search/ConversationSearch.php @@ -83,7 +83,7 @@ public function getOrder(string $route, array $routeParameters): int { * @inheritDoc */ public function search(IUser $user, ISearchQuery $query): SearchResult { - $rooms = $this->manager->getRoomsForParticipant($user->getUID()); + $rooms = $this->manager->getRoomsForUser($user->getUID()); $result = []; foreach ($rooms as $room) { diff --git a/lib/Search/CurrentMessageSearch.php b/lib/Search/CurrentMessageSearch.php index 75b37ebbac1..0b855c166a3 100644 --- a/lib/Search/CurrentMessageSearch.php +++ b/lib/Search/CurrentMessageSearch.php @@ -76,7 +76,7 @@ public function search(IUser $user, ISearchQuery $query): SearchResult { } try { - $room = $this->roomManager->getRoomForParticipantByToken( + $room = $this->roomManager->getRoomForUserByToken( $currentToken, $user->getUID() ); diff --git a/lib/Search/MessageSearch.php b/lib/Search/MessageSearch.php index ed65ca91ec2..84b727a9762 100644 --- a/lib/Search/MessageSearch.php +++ b/lib/Search/MessageSearch.php @@ -29,6 +29,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\Manager as RoomManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCP\Comments\IComment; use OCP\IL10N; @@ -112,7 +113,7 @@ public function search(IUser $user, ISearchQuery $query): SearchResult { $title = $this->l->t('Messages in other conversations'); } - $rooms = $this->roomManager->getRoomsForParticipant($user->getUID()); + $rooms = $this->roomManager->getRoomsForUser($user->getUID()); $roomMap = []; foreach ($rooms as $room) { @@ -193,7 +194,7 @@ protected function commentToSearchResultEntry(Room $room, IUser $user, IComment } $iconUrl = ''; - if ($message->getActorType() === 'users') { + if ($message->getActorType() === Attendee::ACTOR_USERS) { $iconUrl = $this->url->linkToRouteAbsolute('core.avatar.getAvatar', [ 'userId' => $message->getActorId(), 'size' => 64, diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php new file mode 100644 index 00000000000..67ff97ced81 --- /dev/null +++ b/lib/Service/ParticipantService.php @@ -0,0 +1,725 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Service; + +use OCA\Talk\Config; +use OCA\Talk\Events\AddParticipantsEvent; +use OCA\Talk\Events\JoinRoomGuestEvent; +use OCA\Talk\Events\JoinRoomUserEvent; +use OCA\Talk\Events\ModifyParticipantEvent; +use OCA\Talk\Events\ParticipantEvent; +use OCA\Talk\Events\RemoveParticipantEvent; +use OCA\Talk\Events\RemoveUserEvent; +use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Exceptions\InvalidPasswordException; +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\AttendeeMapper; +use OCA\Talk\Model\Session; +use OCA\Talk\Model\SessionMapper; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCA\Talk\Webinary; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Comments\IComment; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; + +class ParticipantService { + /** @var Config */ + protected $talkConfig; + /** @var AttendeeMapper */ + protected $attendeeMapper; + /** @var SessionMapper */ + protected $sessionMapper; + /** @var SessionService */ + protected $sessionService; + /** @var ISecureRandom */ + private $secureRandom; + /** @var IDBConnection */ + protected $connection; + /** @var IEventDispatcher */ + private $dispatcher; + /** @var IUserManager */ + private $userManager; + /** @var ITimeFactory */ + private $timeFactory; + + public function __construct(Config $talkConfig, + AttendeeMapper $attendeeMapper, + SessionMapper $sessionMapper, + SessionService $sessionService, + ISecureRandom $secureRandom, + IDBConnection $connection, + IEventDispatcher $dispatcher, + IUserManager $userManager, + ITimeFactory $timeFactory) { + $this->talkConfig = $talkConfig; + $this->attendeeMapper = $attendeeMapper; + $this->sessionMapper = $sessionMapper; + $this->sessionService = $sessionService; + $this->secureRandom = $secureRandom; + $this->connection = $connection; + $this->dispatcher = $dispatcher; + $this->userManager = $userManager; + $this->timeFactory = $timeFactory; + } + + public function updateParticipantType(Room $room, Participant $participant, int $participantType): void { + $attendee = $participant->getAttendee(); + $oldType = $attendee->getParticipantType(); + + $event = new ModifyParticipantEvent($room, $participant, 'type', $participantType, $oldType); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_TYPE_SET, $event); + + $attendee->setParticipantType($participantType); + $this->attendeeMapper->update($attendee); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event); + } + + public function updateLastReadMessage(Participant $participant, int $lastReadMessage): void { + $attendee = $participant->getAttendee(); + $attendee->setLastReadMessage($lastReadMessage); + $this->attendeeMapper->update($attendee); + } + + public function updateFavoriteStatus(Participant $participant, bool $isFavorite): void { + $attendee = $participant->getAttendee(); + $attendee->setFavorite($isFavorite); + $this->attendeeMapper->update($attendee); + } + + /** + * @param Participant $participant + * @param int $level + * @throws \InvalidArgumentException When the notification level is invalid + */ + public function updateNotificationLevel(Participant $participant, int $level): void { + if (!\in_array($level, [ + Participant::NOTIFY_ALWAYS, + Participant::NOTIFY_MENTION, + Participant::NOTIFY_NEVER + ], true)) { + throw new \InvalidArgumentException('Invalid notification level'); + } + + $attendee = $participant->getAttendee(); + $attendee->setNotificationLevel($level); + $this->attendeeMapper->update($attendee); + } + + /** + * @param Room $room + * @param IUser $user + * @param string $password + * @param bool $passedPasswordProtection + * @return Participant + * @throws InvalidPasswordException + * @throws UnauthorizedException + */ + public function joinRoom(Room $room, IUser $user, string $password, bool $passedPasswordProtection = false): Participant { + $event = new JoinRoomUserEvent($room, $user, $password, $passedPasswordProtection); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_CONNECT, $event); + + if ($event->getCancelJoin() === true) { + $this->removeUser($room, $user, Room::PARTICIPANT_LEFT); + throw new UnauthorizedException('Participant is not allowed to join'); + } + + try { + $attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID()); + } catch (DoesNotExistException $e) { + if (!$event->getPassedPasswordProtection() && !$room->verifyPassword($password)['result']) { + throw new InvalidPasswordException('Provided password is invalid'); + } + + // User joining a public room, without being invited + $this->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), + 'participantType' => Participant::USER_SELF_JOINED, + ]]); + + $attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID()); + } + + $session = $this->sessionService->createSessionForAttendee($attendee); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_CONNECT, $event); + + return new Participant($room, $attendee, $session); + } + + /** + * @param Room $room + * @param string $password + * @param bool $passedPasswordProtection + * @return Participant + * @throws InvalidPasswordException + * @throws UnauthorizedException + */ + public function joinRoomAsNewGuest(Room $room, string $password, bool $passedPasswordProtection = false): Participant { + $event = new JoinRoomGuestEvent($room, $password, $passedPasswordProtection); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_GUEST_CONNECT, $event); + + if ($event->getCancelJoin()) { + throw new UnauthorizedException('Participant is not allowed to join'); + } + + if (!$event->getPassedPasswordProtection() && !$room->verifyPassword($password)['result']) { + throw new InvalidPasswordException(); + } + + $lastMessage = 0; + if ($room->getLastMessage() instanceof IComment) { + $lastMessage = (int) $room->getLastMessage()->getId(); + } + + $randomActorId = $this->secureRandom->generate(255); + + $attendee = new Attendee(); + $attendee->setRoomId($room->getId()); + $attendee->setActorType(Attendee::ACTOR_GUESTS); + $attendee->setActorId($randomActorId); + $attendee->setParticipantType(Participant::GUEST); + $attendee->setLastReadMessage($lastMessage); + $this->attendeeMapper->insert($attendee); + + $session = $this->sessionService->createSessionForAttendee($attendee); + + // Update the random guest id + $attendee->setActorId(sha1($session->getSessionId())); + $this->attendeeMapper->update($attendee); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_GUEST_CONNECT, $event); + + return new Participant($room, $attendee, $session); + } + + /** + * @param Room $room + * @param array $participants + */ + public function addUsers(Room $room, array $participants): void { + $event = new AddParticipantsEvent($room, $participants); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_USERS_ADD, $event); + + $lastMessage = 0; + if ($room->getLastMessage() instanceof IComment) { + $lastMessage = (int) $room->getLastMessage()->getId(); + } + + foreach ($participants as $participant) { + $attendee = new Attendee(); + $attendee->setRoomId($room->getId()); + $attendee->setActorType($participant['actorType']); + $attendee->setActorId($participant['actorId']); + $attendee->setParticipantType($participant['participantType'] ?? Participant::USER); + $attendee->setLastReadMessage($lastMessage); + $this->attendeeMapper->insert($attendee); + } + + $this->dispatcher->dispatch(Room::EVENT_AFTER_USERS_ADD, $event); + } + + /** + * @param Room $room + * @param string $email + * @return Participant + */ + public function inviteEmailAddress(Room $room, string $email): Participant { + $lastMessage = 0; + if ($room->getLastMessage() instanceof IComment) { + $lastMessage = (int) $room->getLastMessage()->getId(); + } + + $attendee = new Attendee(); + $attendee->setRoomId($room->getId()); + $attendee->setActorType(Attendee::ACTOR_EMAILS); + $attendee->setActorId($email); + + if ($room->getSIPEnabled() === Webinary::SIP_ENABLED + && $this->talkConfig->isSIPConfigured()) { + $attendee->setPin($this->generatePin()); + } + + $attendee->setParticipantType(Participant::GUEST); + $attendee->setLastReadMessage($lastMessage); + $this->attendeeMapper->insert($attendee); + // FIXME handle duplicate invites gracefully + + return new Participant($room, $attendee, null); + } + + public function generatePinForParticipant(Room $room, Participant $participant): void { + $attendee = $participant->getAttendee(); + if ($room->getSIPEnabled() === Webinary::SIP_ENABLED + && $this->talkConfig->isSIPConfigured() + && $attendee->getActorType() === Attendee::ACTOR_USERS + && !$attendee->getPin()) { + $attendee->setPin($this->generatePin()); + $this->attendeeMapper->update($attendee); + } + } + + public function ensureOneToOneRoomIsFilled(Room $room): void { + if ($room->getType() !== Room::ONE_TO_ONE_CALL) { + return; + } + + $users = json_decode($room->getName(), true); + $participants = $this->getParticipantUserIds($room); + $missingUsers = array_diff($users, $participants); + + foreach ($missingUsers as $userId) { + if ($this->userManager->userExists($userId)) { + $this->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $userId, + 'participantType' => Participant::OWNER, + ]]); + } + } + } + + public function leaveRoomAsSession(Room $room, Participant $participant): void { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { + $event = new ParticipantEvent($room, $participant); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_DISCONNECT, $event); + } else { + $event = new RemoveParticipantEvent($room, $participant, Room::PARTICIPANT_LEFT); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $event); + } + + $session = $participant->getSession(); + if ($session instanceof Session) { + $dispatchLeaveCallEvents = $session->getInCall() !== Participant::FLAG_DISCONNECTED; + if ($dispatchLeaveCallEvents) { + $event = new ModifyParticipantEvent($room, $participant, 'inCall', Participant::FLAG_DISCONNECTED, $session->getInCall()); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_LEAVE_CALL, $event); + } + + $this->sessionMapper->delete($session); + + if ($dispatchLeaveCallEvents) { + $this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $event); + } + } else { + $this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId()); + } + + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS + || $participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { + $this->attendeeMapper->delete($participant->getAttendee()); + } + + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { + $this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_DISCONNECT, $event); + } else { + $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event); + } + } + + public function removeAttendee(Room $room, Participant $participant, string $reason): void { + $isUser = $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS; + + if ($isUser) { + $user = $this->userManager->get($participant->getAttendee()->getActorId()); + $event = new RemoveUserEvent($room, $participant, $user, $reason); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event); + } else { + $event = new RemoveParticipantEvent($room, $participant, $reason); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $event); + } + + $this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId()); + $this->attendeeMapper->delete($participant->getAttendee()); + + if ($isUser) { + $this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event); + } else { + $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event); + } + } + + public function removeUser(Room $room, IUser $user, string $reason): void { + try { + $participant = $room->getParticipant($user->getUID()); + } catch (ParticipantNotFoundException $e) { + return; + } + + $event = new RemoveUserEvent($room, $participant, $user, $reason); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event); + + $session = $participant->getSession(); + if ($session instanceof Session) { + $this->sessionMapper->delete($session); + } + + $attendee = $participant->getAttendee(); + $this->attendeeMapper->delete($attendee); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event); + } + + public function cleanGuestParticipants(Room $room): void { + $event = new RoomEvent($room); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_GUESTS_CLEAN, $event); + + $query = $this->connection->getQueryBuilder(); + $query->selectAlias('s.id', 's_id') + ->from('talk_sessions', 's') + ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('s.attendee_id', 'a.id')) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS))) + ->andWhere($query->expr()->lte('s.last_ping', $query->createNamedParameter($this->timeFactory->getTime() - 100, IQueryBuilder::PARAM_INT))); + + $sessionTableIds = []; + $result = $query->execute(); + while ($row = $result->fetch()) { + $sessionTableIds[] = (int) $row['s_id']; + } + $result->closeCursor(); + + $this->sessionService->deleteSessionsById($sessionTableIds); + + $query = $this->connection->getQueryBuilder(); + $query->selectAlias('a.id', 'a_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('s.attendee_id', 'a.id')) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS))) + ->andWhere($query->expr()->isNull('s.id')); + + $attendeeIds = []; + $result = $query->execute(); + while ($row = $result->fetch()) { + $attendeeIds[] = (int) $row['a_id']; + } + $result->closeCursor(); + + $this->attendeeMapper->deleteByIds($attendeeIds); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_GUESTS_CLEAN, $event); + } + + public function changeInCall(Room $room, Participant $participant, int $flags): void { + $session = $participant->getSession(); + if (!$session instanceof Session) { + return; + } + + $event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall()); + if ($flags !== Participant::FLAG_DISCONNECTED) { + $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_JOIN_CALL, $event); + } else { + $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_LEAVE_CALL, $event); + } + + $session->setInCall($flags); + $this->sessionMapper->update($session); + + if ($flags !== Participant::FLAG_DISCONNECTED) { + $attendee = $participant->getAttendee(); + $attendee->setLastJoinedCall($this->timeFactory->getTime()); + $this->attendeeMapper->update($attendee); + } + + if ($flags !== Participant::FLAG_DISCONNECTED) { + $this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_JOIN_CALL, $event); + } else { + $this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $event); + } + } + + public function markUsersAsMentioned(Room $room, array $userIds, int $messageId): void { + $query = $this->connection->getQueryBuilder(); + $query->update('talk_attendees') + ->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) + ->andWhere($query->expr()->in('actor_id', $query->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); + $query->execute(); + } + + /** + * @param Room $room + * @return Participant[] + */ + public function getParticipantsForRoom(Room $room): array { + $query = $this->connection->getQueryBuilder(); + + $query->select('a.*') + ->selectAlias('a.id', 'a_id') + ->addSelect('s.*') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin( + 'a', 'talk_sessions', 's', + $query->expr()->eq('s.attendee_id', 'a.id') + ) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + + return $this->getParticipantsFromQuery($query, $room); + } + + /** + * @param Room $room + * @return Participant[] + */ + public function getParticipantsWithSession(Room $room): array { + $query = $this->connection->getQueryBuilder(); + + $query->select('a.*') + ->selectAlias('a.id', 'a_id') + ->addSelect('s.*') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin( + 'a', 'talk_sessions', 's', + $query->expr()->eq('s.attendee_id', 'a.id') + ) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->isNotNull('s.id')); + + return $this->getParticipantsFromQuery($query, $room); + } + + /** + * @param Room $room + * @return Participant[] + */ + public function getParticipantsInCall(Room $room): array { + $query = $this->connection->getQueryBuilder(); + + $query->select('a.*') + ->selectAlias('a.id', 'a_id') + ->addSelect('s.*') + ->selectAlias('s.id', 's_id') + ->from('talk_sessions', 's') + ->leftJoin( + 's', 'talk_attendees', 'a', + $query->expr()->eq('s.attendee_id', 'a.id') + ) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->neq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED))); + + return $this->getParticipantsFromQuery($query, $room); + } + + /** + * @param Room $room + * @param int $notificationLevel + * @return Participant[] + */ + public function getParticipantsByNotificationLevel(Room $room, int $notificationLevel): array { + $query = $this->connection->getQueryBuilder(); + + $query->select('a.*') + ->selectAlias('a.id', 'a_id') + ->addSelect('s.*') + ->selectAlias('s.id', 's_id') + ->from('talk_sessions', 's') + ->leftJoin( + 's', 'talk_attendees', 'a', + $query->expr()->eq('s.attendee_id', 'a.id') + ) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('notification_level', $query->createNamedParameter($notificationLevel, IQueryBuilder::PARAM_INT))); + + return $this->getParticipantsFromQuery($query, $room); + } + + /** + * @param IQueryBuilder $query + * @return Participant[] + */ + protected function getParticipantsFromQuery(IQueryBuilder $query, Room $room): array { + $participants = []; + $result = $query->execute(); + while ($row = $result->fetch()) { + $attendee = $this->attendeeMapper->createAttendeeFromRow($row); + if (isset($row['s_id'])) { + $session = $this->sessionMapper->createSessionFromRow($row); + } else { + $session = null; + } + + $participants[] = new Participant($room, $attendee, $session); + } + $result->closeCursor(); + + return $participants; + } + + /** + * @param Room $room + * @param \DateTime|null $maxLastJoined + * @return string[] + */ + public function getParticipantUserIds(Room $room, \DateTime $maxLastJoined = null): array { + $maxLastJoinedTimestamp = null; + if ($maxLastJoined !== null) { + $maxLastJoinedTimestamp = $maxLastJoined->getTimestamp(); + } + $attendees = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_USERS, $maxLastJoinedTimestamp); + + return array_map(static function (Attendee $attendee) { + return $attendee->getActorId(); + }, $attendees); + } + + /** + * @param Room $room + * @return string[] + */ + public function getParticipantUserIdsNotInCall(Room $room): array { + $query = $this->connection->getQueryBuilder(); + + $query->select('a.actor_id') + ->from('talk_sessions', 's') + ->leftJoin( + 's', 'talk_attendees', 'a', + $query->expr()->eq('s.attendee_id', 'a.id') + ) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) + ->andWhere($query->expr()->orX( + $query->expr()->eq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)), + $query->expr()->isNull('s.in_call') + )); + + $userIds = []; + $result = $query->execute(); + while ($row = $result->fetch()) { + $userIds[] = $row['actor_id']; + } + $result->closeCursor(); + + return $userIds; + } + + /** + * @param Room $room + * @return int + */ + public function getNumberOfUsers(Room $room): int { + return $this->attendeeMapper->countActorsByParticipantType($room->getId(), [ + Participant::USER, + Participant::MODERATOR, + Participant::OWNER, + ]); + } + + /** + * @param Room $room + * @param bool $ignoreGuestModerators + * @return int + */ + public function getNumberOfModerators(Room $room, bool $ignoreGuestModerators = true): int { + $participantTypes = [ + Participant::MODERATOR, + Participant::OWNER, + ]; + if (!$ignoreGuestModerators) { + $participantTypes[] = Participant::GUEST_MODERATOR; + } + return $this->attendeeMapper->countActorsByParticipantType($room->getId(), $participantTypes); + } + + /** + * @param Room $room + * @return int + */ + public function getNumberOfActors(Room $room): int { + return $this->attendeeMapper->countActorsByParticipantType($room->getId(), []); + } + + /** + * @param Room $room + * @return bool + */ + public function hasActiveSessions(Room $room): bool { + $query = $this->connection->getQueryBuilder(); + $query->select('a.room_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq( + 'a.id', 's.attendee_id' + )) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->isNotNull('s.id')) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + return (bool) $row; + } + + /** + * @param Room $room + * @return bool + */ + public function hasActiveSessionsInCall(Room $room): bool { + $query = $this->connection->getQueryBuilder(); + $query->select('a.room_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq( + 'a.id', 's.attendee_id' + )) + ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->isNotNull('s.in_call')) + ->andWhere($query->expr()->neq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED))) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + return (bool) $row; + } + + protected function generatePin(int $entropy = 7): string { + $pin = ''; + // Do not allow to start with a '0' as that is a special mode on the phone server + // Also there are issues with some providers when you enter the same number twice + // consecutive too fast, so we avoid this as well. + $lastDigit = '0'; + for ($i = 0; $i < $entropy; $i++) { + $lastDigit = $this->secureRandom->generate(1, + str_replace($lastDigit, '', ISecureRandom::CHAR_DIGITS) + ); + $pin .= $lastDigit; + } + + return $pin; + } +} diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 4a1f5debdb6..afc87851f8c 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -26,6 +26,7 @@ use InvalidArgumentException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; use OCP\IUser; @@ -34,9 +35,13 @@ class RoomService { /** @var Manager */ protected $manager; + /** @var ParticipantService */ + protected $participantService; - public function __construct(Manager $manager) { + public function __construct(Manager $manager, + ParticipantService $participantService) { $this->manager = $manager; + $this->participantService = $participantService; } /** @@ -53,17 +58,23 @@ public function createOneToOneConversation(IUser $actor, IUser $targetUser): Roo try { // If room exists: Reuse that one, otherwise create a new one. $room = $this->manager->getOne2OneRoom($actor->getUID(), $targetUser->getUID()); - $room->ensureOneToOneRoomIsFilled(); + $this->participantService->ensureOneToOneRoomIsFilled($room); } catch (RoomNotFoundException $e) { $users = [$actor->getUID(), $targetUser->getUID()]; sort($users); $room = $this->manager->createRoom(Room::ONE_TO_ONE_CALL, json_encode($users)); - $room->addUsers([ - 'userId' => $actor->getUID(), - 'participantType' => Participant::OWNER, - ], [ - 'userId' => $targetUser->getUID(), - 'participantType' => Participant::OWNER, + + $this->participantService->addUsers($room, [ + [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $actor->getUID(), + 'participantType' => Participant::OWNER, + ], + [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $targetUser->getUID(), + 'participantType' => Participant::OWNER, + ], ]); } @@ -113,10 +124,11 @@ public function createConversation(int $type, string $name, ?IUser $owner = null $room = $this->manager->createRoom($type, $name, $objectType, $objectId); if ($owner instanceof IUser) { - $room->addUsers([ - 'userId' => $owner->getUID(), + $this->participantService->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $owner->getUID(), 'participantType' => Participant::OWNER, - ]); + ]]); } return $room; diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php new file mode 100644 index 00000000000..7d66b8d9813 --- /dev/null +++ b/lib/Service/SessionService.php @@ -0,0 +1,117 @@ + + * + * @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 . + * + */ + +namespace OCA\Talk\Service; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; +use OCA\Talk\Model\SessionMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Security\ISecureRandom; + +class SessionService { + /** @var SessionMapper */ + protected $sessionMapper; + /** @var IDBConnection */ + protected $connection; + /** @var ISecureRandom */ + protected $secureRandom; + + public function __construct(SessionMapper $sessionMapper, + IDBConnection $connection, + ISecureRandom $secureRandom) { + $this->sessionMapper = $sessionMapper; + $this->connection = $connection; + $this->secureRandom = $secureRandom; + } + + /** + * Update last ping for multiple sessions + * + * Since this function is called by the HPB with potentially hundreds of + * sessions, we do not use the SessionMapper to get the entities first, as + * that would just not scale good enough. + * + * @param string[] $sessionIds + * @param int $lastPing + */ + public function updateMultipleLastPings(array $sessionIds, int $lastPing): void { + $query = $this->connection->getQueryBuilder(); + $query->update('talk_sessions') + ->set('last_ping', $query->createNamedParameter($lastPing, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->in('session_id', $query->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY))); + + $query->execute(); + } + + public function updateLastPing(Session $session, int $lastPing): void { + $session->setLastPing($lastPing); + $this->sessionMapper->update($session); + } + + /** + * @param int[] $ids + */ + public function deleteSessionsById(array $ids): void { + $this->sessionMapper->deleteByIds($ids); + } + + /** + * @param Attendee $attendee + * @param string $forceSessionId + * @return Session + * @throws UniqueConstraintViolationException + */ + public function createSessionForAttendee(Attendee $attendee, string $forceSessionId = ''): Session { + // Currently a participant can only join once + $this->sessionMapper->deleteByAttendeeId($attendee->getId()); + + $session = new Session(); + $session->setAttendeeId($attendee->getId()); + + if ($forceSessionId !== '') { + $session->setSessionId($forceSessionId); + try { + $this->sessionMapper->insert($session); + } catch (UniqueConstraintViolationException $e) { + // The HPB told us to use a session which exists already… + throw $e; + } + } else { + while (true) { + $sessionId = $this->secureRandom->generate(255); + $session->setSessionId($sessionId); + try { + $this->sessionMapper->insert($session); + break; + } catch (UniqueConstraintViolationException $e) { + // 255 chars are not unique? Try again... + } + } + } + + return $session; + } +} diff --git a/lib/Settings/Admin/AdminSettings.php b/lib/Settings/Admin/AdminSettings.php index 94b69e24649..66f6525ff3c 100644 --- a/lib/Settings/Admin/AdminSettings.php +++ b/lib/Settings/Admin/AdminSettings.php @@ -33,6 +33,8 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\ICacheFactory; use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; use OCP\IInitialStateService; use OCP\IL10N; use OCP\IUser; @@ -52,6 +54,8 @@ class AdminSettings implements ISettings { private $initialStateService; /** @var ICacheFactory */ private $memcacheFactory; + /** @var IGroupManager */ + private $groupManager; /** @var MatterbridgeManager */ private $bridgeManager; /** @var IUser */ @@ -66,6 +70,7 @@ public function __construct(Config $talkConfig, CommandService $commandService, IInitialStateService $initialStateService, ICacheFactory $memcacheFactory, + IGroupManager $groupManager, MatterbridgeManager $bridgeManager, IUserSession $userSession, IL10N $l10n, @@ -75,6 +80,7 @@ public function __construct(Config $talkConfig, $this->commandService = $commandService; $this->initialStateService = $initialStateService; $this->memcacheFactory = $memcacheFactory; + $this->groupManager = $groupManager; $this->bridgeManager = $bridgeManager; $this->currentUser = $userSession->getUser(); $this->l10n = $l10n; @@ -93,6 +99,7 @@ public function getForm(): TemplateResponse { $this->initTurnServers(); $this->initSignalingServers(); $this->initRequestSignalingServerTrial(); + $this->initSIPBridge(); return new TemplateResponse('spreed', 'settings/admin-settings', [], ''); } @@ -467,6 +474,24 @@ protected function initRequestSignalingServerTrial(): void { ]); } + protected function initSIPBridge(): void { + $gids = $this->talkConfig->getSIPGroups(); + $groups = []; + foreach ($gids as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + $groups[] = [ + 'id' => $group->getGID(), + 'displayname' => $group->getDisplayName(), + ]; + } + } + + $this->initialStateService->provideInitialState('talk', 'sip_bridge_groups', $groups); + $this->initialStateService->provideInitialState('talk', 'sip_bridge_shared_secret', $this->talkConfig->getSIPSharedSecret()); + $this->initialStateService->provideInitialState('talk', 'sip_bridge_dialin_info', $this->talkConfig->getDialInInfo()); + } + /** * @return string the section ID, e.g. 'sharing' */ diff --git a/lib/Share/RoomShareProvider.php b/lib/Share/RoomShareProvider.php index 4c3885f5f51..2276ebbc0ee 100644 --- a/lib/Share/RoomShareProvider.php +++ b/lib/Share/RoomShareProvider.php @@ -37,6 +37,7 @@ use OCA\Talk\Manager; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; @@ -82,6 +83,8 @@ class RoomShareProvider implements IShareProvider { private $dispatcher; /** @var Manager */ private $manager; + /** @var ParticipantService */ + private $participantService; /** @var ITimeFactory */ protected $timeFactory; /** @var IL10N */ @@ -95,6 +98,7 @@ public function __construct( IShareManager $shareManager, EventDispatcherInterface $dispatcher, Manager $manager, + ParticipantService $participantService, ITimeFactory $timeFactory, IL10N $l, IMimeTypeLoader $mimeTypeLoader @@ -104,6 +108,7 @@ public function __construct( $this->shareManager = $shareManager; $this->dispatcher = $dispatcher; $this->manager = $manager; + $this->participantService = $participantService; $this->timeFactory = $timeFactory; $this->l = $l; $this->mimeTypeLoader = $mimeTypeLoader; @@ -113,10 +118,10 @@ public static function register(IEventDispatcher $dispatcher): void { $listener = static function (ParticipantEvent $event) { $room = $event->getRoom(); - if ($event->getParticipant()->getParticipantType() === Participant::USER_SELF_JOINED) { + if ($event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { /** @var self $roomShareProvider */ $roomShareProvider = \OC::$server->query(self::class); - $roomShareProvider->deleteInRoom($room->getToken(), $event->getParticipant()->getUser()); + $roomShareProvider->deleteInRoom($room->getToken(), $event->getParticipant()->getAttendee()->getActorId()); } }; $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, $listener); @@ -776,7 +781,7 @@ public function getSharesByPath(Node $path): array { * @return IShare[] */ public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { - $allRooms = $this->manager->getRoomsForParticipant($userId); + $allRooms = $this->manager->getRoomsForUser($userId); /** @var IShare[] $shares */ $shares = []; @@ -976,7 +981,7 @@ public function getAccessList($nodes, $currentAccess): array { continue; } - $userList = $room->getParticipantUserIds(); + $userList = $this->participantService->getParticipantUserIds($room); foreach ($userList as $uid) { $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 1ac8743a744..e1429f9e90f 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -26,8 +26,11 @@ namespace OCA\Talk\Signaling; use OCA\Talk\Config; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\Http\Client\IClientService; use OCP\IURLGenerator; use OCP\Security\ISecureRandom; @@ -44,6 +47,8 @@ class BackendNotifier { private $secureRandom; /** @var Manager */ private $signalingManager; + /** @var ParticipantService */ + private $participantService; /** @var IUrlGenerator */ private $urlGenerator; @@ -52,12 +57,14 @@ public function __construct(Config $config, IClientService $clientService, ISecureRandom $secureRandom, Manager $signalingManager, + ParticipantService $participantService, IURLGenerator $urlGenerator) { $this->config = $config; $this->logger = $logger; $this->clientService = $clientService; $this->secureRandom = $secureRandom; $this->signalingManager = $signalingManager; + $this->participantService = $participantService; $this->urlGenerator = $urlGenerator; } @@ -141,7 +148,9 @@ public function roomInvited(Room $room, array $users): void { $this->logger->info('Now invited to ' . $room->getToken() . ': ' . print_r($users, true)); $userIds = []; foreach ($users as $user) { - $userIds[] = $user['userId']; + if ($user['actorType'] === Attendee::ACTOR_USERS) { + $userIds[] = $user['actorId']; + } } $this->backendRequest($room, [ 'type' => 'invite', @@ -149,7 +158,7 @@ public function roomInvited(Room $room, array $users): void { 'userids' => $userIds, // TODO(fancycode): We should try to get rid of 'alluserids' and // find a better way to notify existing users to update the room. - 'alluserids' => $room->getParticipantUserIds(), + 'alluserids' => $this->participantService->getParticipantUserIds($room), 'properties' => $room->getPropertiesForSignaling(''), ], ]); @@ -170,7 +179,7 @@ public function roomsDisinvited(Room $room, array $userIds): void { 'userids' => $userIds, // TODO(fancycode): We should try to get rid of 'alluserids' and // find a better way to notify existing users to update the room. - 'alluserids' => $room->getParticipantUserIds(), + 'alluserids' => $this->participantService->getParticipantUserIds($room), 'properties' => $room->getPropertiesForSignaling(''), ], ]); @@ -191,7 +200,7 @@ public function roomSessionsRemoved(Room $room, array $sessionIds): void { 'sessionids' => $sessionIds, // TODO(fancycode): We should try to get rid of 'alluserids' and // find a better way to notify existing users to update the room. - 'alluserids' => $room->getParticipantUserIds(), + 'alluserids' => $this->participantService->getParticipantUserIds($room), 'properties' => $room->getPropertiesForSignaling(''), ], ]); @@ -208,7 +217,7 @@ public function roomModified(Room $room): void { $this->backendRequest($room, [ 'type' => 'update', 'update' => [ - 'userids' => $room->getParticipantUserIds(), + 'userids' => $this->participantService->getParticipantUserIds($room), 'properties' => $room->getPropertiesForSignaling(''), ], ]); @@ -218,12 +227,11 @@ public function roomModified(Room $room): void { * The given room has been deleted. * * @param Room $room - * @param array $participants + * @param string[] $userIds * @throws \Exception */ - public function roomDeleted(Room $room, array $participants): void { + public function roomDeleted(Room $room, array $userIds): void { $this->logger->info('Room deleted: ' . $room->getToken()); - $userIds = array_keys($participants['users']); $this->backendRequest($room, [ 'type' => 'delete', 'delete' => [ @@ -243,29 +251,44 @@ public function participantsModified(Room $room, array $sessionIds): void { $this->logger->info('Room participants modified: ' . $room->getToken() . ' ' . print_r($sessionIds, true)); $changed = []; $users = []; - $participants = $room->getParticipantsLegacy(); - foreach ($participants['users'] as $userId => $participant) { - $participant['userId'] = $userId; - $users[] = $participant; - if (\in_array($participant['sessionId'], $sessionIds, true)) { - $participant['permissions'] = ['publish-media', 'publish-screen']; - if ($participant['participantType'] === Participant::OWNER || - $participant['participantType'] === Participant::MODERATOR) { - $participant['permissions'][] = 'control'; - } - $changed[] = $participant; + $participants = $this->participantService->getParticipantsForRoom($room); + foreach ($participants as $participant) { + $attendee = $participant->getAttendee(); + if ($attendee->getActorType() !== Attendee::ACTOR_USERS + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + continue; } - } - foreach ($participants['guests'] as $participant) { - if (!isset($participant['participantType'])) { - $participant['participantType'] = Participant::GUEST; + + $data = [ + 'inCall' => Participant::FLAG_DISCONNECTED, + 'lastPing' => 0, + 'sessionId' => '0', + 'participantType' => $attendee->getParticipantType(), + ]; + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + $data['userId'] = $attendee->getActorId(); } - $users[] = $participant; - if (\in_array($participant['sessionId'], $sessionIds, true)) { - $participant['permissions'] = ['publish-media', 'publish-screen']; - $changed[] = $participant; + + $session = $participant->getSession(); + if ($session instanceof Session) { + $data['inCall'] = $session->getInCall(); + $data['lastPing'] = $session->getLastPing(); + $data['sessionId'] = $session->getSessionId(); + $users[] = $data; + + if (\in_array($session->getSessionId(), $sessionIds, true)) { + $data['permissions'] = ['publish-media', 'publish-screen']; + if ($attendee->getParticipantType() === Participant::OWNER || + $attendee->getParticipantType() === Participant::MODERATOR) { + $data['permissions'][] = 'control'; + } + $changed[] = $data; + } + } else { + $users[] = $data; } } + $this->backendRequest($room, [ 'type' => 'participants', 'participants' => [ @@ -287,25 +310,38 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds): vo $this->logger->info('Room in-call status changed: ' . $room->getToken() . ' ' . $flags . ' ' . print_r($sessionIds, true)); $changed = []; $users = []; - $participants = $room->getParticipantsLegacy(); - foreach ($participants['users'] as $userId => $participant) { - $participant['userId'] = $userId; - if ($participant['inCall'] !== Participant::FLAG_DISCONNECTED) { - $users[] = $participant; - } - if (\in_array($participant['sessionId'], $sessionIds, true)) { - $changed[] = $participant; - } - } - foreach ($participants['guests'] as $participant) { - if (!isset($participant['participantType'])) { - $participant['participantType'] = Participant::GUEST; + + $participants = $this->participantService->getParticipantsForRoom($room); + foreach ($participants as $participant) { + $attendee = $participant->getAttendee(); + if ($attendee->getActorType() !== Attendee::ACTOR_USERS + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + continue; } - if ($participant['inCall'] !== Participant::FLAG_DISCONNECTED) { - $users[] = $participant; + + $data = [ + 'inCall' => Participant::FLAG_DISCONNECTED, + 'lastPing' => 0, + 'sessionId' => '0', + 'participantType' => $attendee->getParticipantType(), + ]; + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { + $data['userId'] = $attendee->getActorId(); } - if (\in_array($participant['sessionId'], $sessionIds, true)) { - $changed[] = $participant; + + $session = $participant->getSession(); + if ($session instanceof Session) { + $data['inCall'] = $session->getInCall(); + $data['lastPing'] = $session->getLastPing(); + $data['sessionId'] = $session->getSessionId(); + + if ($session->getInCall() !== Participant::FLAG_DISCONNECTED) { + $users[] = $data; + } + + if (\in_array($session->getSessionId(), $sessionIds, true)) { + $changed[] = $data; + } } } diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 3081a4c8dbd..8bdc251a985 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -34,8 +34,10 @@ use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\EventDispatcher\IEventDispatcher; class Listener { @@ -80,10 +82,10 @@ protected static function registerInternalSignaling(IEventDispatcher $dispatcher // When "addMessageForAllParticipants" is called the participant is // no longer in the room, so the message needs to be explicitly // added for the participant. - /** @var Participant $participant */ $participant = $event->getParticipant(); - if ($participant->getSessionId() !== '0') { - $messages->addMessage($participant->getSessionId(), $participant->getSessionId(), 'refresh-participant-list'); + $session = $participant->getSession(); + if ($session instanceof Session) { + $messages->addMessage($session->getSessionId(), $session->getSessionId(), 'refresh-participant-list'); } }; $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, $listener); @@ -98,12 +100,15 @@ protected static function registerInternalSignaling(IEventDispatcher $dispatcher /** @var Messages $messages */ $messages = \OC::$server->query(Messages::class); - $participants = $room->getParticipantsLegacy(); - foreach ($participants['users'] as $participant) { - $messages->addMessage($participant['sessionId'], $participant['sessionId'], 'refresh-participant-list'); - } - foreach ($participants['guests'] as $participant) { - $messages->addMessage($participant['sessionId'], $participant['sessionId'], 'refresh-participant-list'); + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->query(ParticipantService::class); + + $participants = $participantService->getParticipantsForRoom($room); + foreach ($participants as $participant) { + $session = $participant->getSession(); + if ($session instanceof Session) { + $messages->addMessage($session->getSessionId(), $session->getSessionId(), 'refresh-participant-list'); + } } }; $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, $listener); @@ -135,6 +140,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, $listener); $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, $listener); $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_SIP_ENABLED_SET, $listener); // TODO remove handler with "roomModified" in favour of handler with // "participantsModified" once the clients no longer expect a // "roomModified" message for participant type changes. @@ -151,8 +157,10 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher // If the participant is not active in the room the "participants" // request will be sent anyway, although with an empty "changed" // property. - if ($event->getParticipant()->getSessionId()) { - $sessionIds[] = $event->getParticipant()->getSessionId(); + $participant = $event->getParticipant(); + $session = $participant->getSession(); + if ($session instanceof Session) { + $sessionIds[] = $session->getSessionId(); } $notifier->participantsModified($event->getRoom(), $sessionIds); }); @@ -163,10 +171,11 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher /** @var BackendNotifier $notifier */ $notifier = \OC::$server->query(BackendNotifier::class); + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->query(ParticipantService::class); $room = $event->getRoom(); - $participants = $room->getParticipantsLegacy(); - $notifier->roomDeleted($room, $participants); + $notifier->roomDeleted($room, $participantService->getParticipantUserIds($room)); }); $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, static function (RemoveUserEvent $event) { if (self::isUsingInternalSignaling()) { @@ -186,7 +195,11 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher /** @var BackendNotifier $notifier */ $notifier = \OC::$server->query(BackendNotifier::class); - $notifier->roomSessionsRemoved($event->getRoom(), [$event->getParticipant()->getSessionId()]); + $participant = $event->getParticipant(); + $session = $participant->getSession(); + if ($session instanceof Session) { + $notifier->roomSessionsRemoved($event->getRoom(), [$session->getSessionId()]); + } }); $listener = static function (ModifyParticipantEvent $event) { @@ -197,11 +210,15 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher /** @var BackendNotifier $notifier */ $notifier = \OC::$server->query(BackendNotifier::class); - $notifier->roomInCallChanged( - $event->getRoom(), - $event->getNewValue(), - [$event->getParticipant()->getSessionId()] - ); + $participant = $event->getParticipant(); + $session = $participant->getSession(); + if ($session instanceof Session) { + $notifier->roomInCallChanged( + $event->getRoom(), + $event->getNewValue(), + [$session->getSessionId()] + ); + } }; $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener); @@ -227,7 +244,11 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher /** @var BackendNotifier $notifier */ $notifier = \OC::$server->query(BackendNotifier::class); - $notifier->participantsModified($event->getRoom(), [$event->getParticipant()->getSessionId()]); + $participant = $event->getParticipant(); + $session = $participant->getSession(); + if ($session instanceof Session) { + $notifier->participantsModified($event->getRoom(), [$session->getSessionId()]); + } }); $dispatcher->addListener(ChatManager::EVENT_AFTER_MESSAGE_SEND , static function (ChatParticipantEvent $event) { if (self::isUsingInternalSignaling()) { diff --git a/lib/Signaling/Messages.php b/lib/Signaling/Messages.php index 83407e14458..09d5f5fa6b2 100644 --- a/lib/Signaling/Messages.php +++ b/lib/Signaling/Messages.php @@ -23,7 +23,9 @@ namespace OCA\Talk\Signaling; +use OCA\Talk\Model\Session; use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -33,12 +35,17 @@ class Messages { /** @var IDBConnection */ protected $db; + /** @var ParticipantService */ + protected $participantService; + /** @var ITimeFactory */ protected $timeFactory; public function __construct(IDBConnection $db, + ParticipantService $participantService, ITimeFactory $timeFactory) { $this->db = $db; + $this->participantService = $participantService; $this->timeFactory = $timeFactory; } @@ -88,10 +95,14 @@ public function addMessageForAllParticipants(Room $room, string $message): void ] ); - foreach ($room->getActiveSessions() as $sessionId) { - $query->setParameter('sender', $sessionId) - ->setParameter('recipient', $sessionId) - ->execute(); + $participants = $this->participantService->getParticipantsWithSession($room); + foreach ($participants as $participant) { + $session = $participant->getSession(); + if ($session instanceof Session) { + $query->setParameter('sender', $session->getSessionId()) + ->setParameter('recipient', $session->getSessionId()) + ->execute(); + } } } diff --git a/lib/TInitialState.php b/lib/TInitialState.php index c918befe3fa..5b771d50f65 100644 --- a/lib/TInitialState.php +++ b/lib/TInitialState.php @@ -67,6 +67,11 @@ protected function publishInitialStateShared(): void { 'talk', 'signaling_mode', $this->talkConfig->getSignalingMode() ); + + $this->initialStateService->provideInitialState( + 'talk', 'sip_dialin_info', + $this->talkConfig->getDialInInfo() + ); } protected function publishInitialStateForUser(IUser $user, IRootFolder $rootFolder, IAppManager $appManager): void { diff --git a/lib/Webinary.php b/lib/Webinary.php index d29866b6ac0..fecbe44fa34 100644 --- a/lib/Webinary.php +++ b/lib/Webinary.php @@ -26,4 +26,7 @@ class Webinary { public const LOBBY_NONE = 0; public const LOBBY_NON_MODERATORS = 1; + + public const SIP_DISABLED = 0; + public const SIP_ENABLED = 1; } diff --git a/psalm.xml b/psalm.xml index b8d2d0fe147..8aab1628e97 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,16 +20,24 @@ + + + + + + + + diff --git a/src/components/AdminSettings/SIPBridge.vue b/src/components/AdminSettings/SIPBridge.vue new file mode 100644 index 00000000000..6559c4b0a36 --- /dev/null +++ b/src/components/AdminSettings/SIPBridge.vue @@ -0,0 +1,170 @@ + + +