diff --git a/CHANGELOG.md b/CHANGELOG.md index d10b0cd4a49..6c4c6cbec5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,36 @@ # Changelog All notable changes to this project will be documented in this file. +## 17.1.0-rc.1 – 2023-08-11 +### Added +- Add support for bots via webhooks + [#10139](https://github.com/nextcloud/spreed/issues/10139) + [#10151](https://github.com/nextcloud/spreed/issues/10151) +- Add Markdown support for chat messages + [#10089](https://github.com/nextcloud/spreed/issues/10089) + [#10090](https://github.com/nextcloud/spreed/issues/10090) +- Allow to filter the conversation list for unread mentions and messages + [#10093](https://github.com/nextcloud/spreed/issues/10093) +- Provide an overview list of open conversations + [#10095](https://github.com/nextcloud/spreed/issues/10095) +- Set a reminder to get notified about a chat messages at a later time + [#10104](https://github.com/nextcloud/spreed/issues/10104) + [#10152](https://github.com/nextcloud/spreed/issues/10152) + [#10155](https://github.com/nextcloud/spreed/issues/10155) +- Show a hint when the call is running since one hour + [#10101](https://github.com/nextcloud/spreed/issues/10101) +- Show the talking time of participants in the right sidebar + [#10145](https://github.com/nextcloud/spreed/issues/10145) + +### Changed +- System messages of the same actions are now grouped + [#10143](https://github.com/nextcloud/spreed/issues/10143) +- Update dependencies + +### Fixed +- Fix resetting the bruteforce protection when joining a conversation + [#10165](https://github.com/nextcloud/spreed/issues/10165) + ## 17.0.3 – 2023-07-28 ### Changed - Update dependencies diff --git a/appinfo/info.xml b/appinfo/info.xml index 29bc9b786a3..4c95e242734 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 ]]> - 17.1.0-dev.2 + 17.1.0-rc.1 agpl Daniel Calviño Sánchez diff --git a/appinfo/routes/routesBotController.php b/appinfo/routes/routesBotController.php index 90910d2a3d4..3578cc7f514 100644 --- a/appinfo/routes/routesBotController.php +++ b/appinfo/routes/routesBotController.php @@ -48,6 +48,8 @@ ['name' => 'Bot#react', 'url' => '/api/{apiVersion}/bot/{token}/reaction/{messageId}', 'verb' => 'POST', 'requirements' => $requirementsWithMessageId], /** @see \OCA\Talk\Controller\BotController::deleteReaction() */ ['name' => 'Bot#deleteReaction', 'url' => '/api/{apiVersion}/bot/{token}/reaction/{messageId}', 'verb' => 'DELETE', 'requirements' => $requirementsWithMessageId], + /** @see \OCA\Talk\Controller\BotController::adminListBots() */ + ['name' => 'Bot#adminListBots', 'url' => '/api/{apiVersion}/bot/admin', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\BotController::listBots() */ ['name' => 'Bot#listBots', 'url' => '/api/{apiVersion}/bot/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\BotController::enableBot() */ diff --git a/docs/bot-list.md b/docs/bot-list.md new file mode 100644 index 00000000000..9961dde5a38 --- /dev/null +++ b/docs/bot-list.md @@ -0,0 +1,31 @@ +# List of available bots + +If you want to write your own bot or webhook, please see the +[Bots and Webhooks developer documentation](bots.md). Afterwards send a pull +request against the [docs/bot-list.md](https://github.com/nextcloud/spreed/blob/master/docs/bot-list.md) +to add your bot to the list, using the following template: + +```markdown +## Name of the bot + +Useful, but short description, best to keep it to 1-3 lines of text. + +![Screenshot can be put into docs/botscreenshots/bot-name.png, max. width 700px, max. height 480px](botscreenshots/bot-name.png) + +* License: Identifier of the license (See https://spdx.org/licenses/) +* [Link to source code](https://github.com/nextcloud/call_summary_bot) +* [Link to installation documentation](https://github.com/nextcloud/call_summary_bot#readme) +``` + +Here you can find a brief overview of known bots that allows administration to +easily discover your bot and install it on their Nextcloud server. + +## Call summary + +The call summary bot posts an overview message after the call listing all participants and outlining tasks. + +![Screenshot showing a call summary chat message](botscreenshots/call-summary.png) + +* License: AGPL-3.0-or-later +* [Link to source code](https://github.com/nextcloud/call_summary_bot) +* [Link to installation documentation](https://github.com/nextcloud/call_summary_bot#readme) diff --git a/docs/bot-management.md b/docs/bot-management.md new file mode 100644 index 00000000000..301f8ba617f --- /dev/null +++ b/docs/bot-management.md @@ -0,0 +1,84 @@ +# Setup and management bots + +Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`: (requires the `bots-v1` capability - available since Nextcloud 27.1) + +## Get list of bots installed on the server + +Lists the bots that are enabled and can be enabled for the conversation + +* Required capability: `bots-v1` +* Method: `GET` +* Endpoint: `bot/admin` + +* Response: + - Status code: + + `200 OK` + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant + - Data: + List of bots, each bot has at least: + +| field | type | Description | +|----------------------|--------|----------------------------------------------------------------------------------------------| +| `id` | int | Unique numeric identifier of the bot on this server | +| `name` | string | Display name of the bot shown as author when it posts a message or reaction | +| `description` | string | A longer description of the bot helping moderators to decide if they want to enable this bot | +| `url` | string | URL endpoint that is triggered by this bot | +| `url_hash` | string | Hash of the URL prefixed with `bot-` serves as `actor_id` | +| `state` | int | One of the [Bot states](constants.md#bot-states) | +| `error_count` | int | Number of consecutive errors | +| `last_error_date` | int | UNIX timestamp of the last error | +| `last_error_message` | string | The last exception message or error response information when trying to reach the bot. | + +## Get list of bots for a conversation + +Lists the bots that are enabled and can be enabled for the conversation + +* Required capability: `bots-v1` +* Method: `GET` +* Endpoint: `bot/{token}` + +* Response: + - Status code: + + `200 OK` + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant + - Data: + List of bots, each bot has at least: + +| field | type | Description | +|-----------------------|--------|----------------------------------------------------------------------------------------------| +| `id` | int | Unique numeric identifier of the bot on this server | +| `name` | string | Display name of the bot shown as author when it posts a message or reaction | +| `description` | string | A longer description of the bot helping moderators to decide if they want to enable this bot | +| `state` | int | One of the [Bot states](constants.md#bot-states) | + +## Enable a bot for a conversation as a moderator + +* Required capability: `bots-v1` +* Method: `POST` +* Endpoint: `bot/{token}/{botId}` + +* Response: + - Status code: + + `200 OK` When the bot is already enabled in the conversation + + `201 Created` When the bot is now enabled in the conversation + + `400 Bad Request` When the bot ID is unknown on the server + + `400 Bad Request` When the bot is disabled or set to "no-setup" on the server + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant + +## Disable a bot for a conversation as a moderator + +* Required capability: `bots-v1` +* Method: `DELETE` +* Endpoint: `bot/{token}/{botId}` + +* Response: + - Status code: + + `200 OK` When the bot is already enabled in the conversation + + `201 Created` When the bot is now enabled in the conversation + + `400 Bad Request` When the bot ID is unknown on the server + + `400 Bad Request` When the bot is disabled or set to "no-setup" on the server + + `403 Forbidden` When the current user is not a moderator/owner + + `404 Not Found` When the conversation could not be found for the participant diff --git a/docs/bots.md b/docs/bots.md new file mode 100644 index 00000000000..573d99968da --- /dev/null +++ b/docs/bots.md @@ -0,0 +1,171 @@ +# Bots and Webhooks + +Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`: (requires the `bots-v1` capability - available since Nextcloud 27.1) + +Webhook based bots are available with the Nextcloud 27.1 compatible Nextcloud Talk 17.1 release as a first version + +!!! note + + For security reasons bots can only be added via the + command line. `./occ talk:bot:install --help` gives you + a short overview of the required arguments, but they are + also explained in the [OCC documentation](occ.md#talkbotinstall). + +--- + +## Receiving chat messages + +Messages are signed using the shared secret that is specified when installing a bot on the server. +Create a HMAC with SHA256 over the request body and the `RANDOM` header using the shared secret. +Only when the `SIGNATURE` matches the request should be accepted and handled. + +**Sample PHP code:** + +```php +$digest = hash_hmac('sha256', $_SERVER['HTTP_X_NEXTCLOUD_TALK_RANDOM'] . file_get_contents('php://input'), $secret); + +if (!hash_equals($digest, strtolower($_SERVER['HTTP_X_NEXTCLOUD_TALK_SIGNATURE']))) { + exit; +} +``` + +### Headers + +| Header | Content type | Description | +|-----------------------------------|---------------------|------------------------------------------------------| +| `HTTP_X_NEXTCLOUD_TALK_SIGNATURE` | `[a-f0-9]{64}` | SHA265 signature of the body | +| `HTTP_X_NEXTCLOUD_TALK_RANDOM` | `[A-Za-z0-9+\]{64}` | Random string used when signing the body | +| `HTTP_X_NEXTCLOUD_TALK_BACKEND` | URI | Base URL of the Nextcloud server sending the message | + +### Content + +The content format follows the [ActivityPub](https://www.w3.org/TR/activitypub/) dictionary. + +#### Sample chat message + +```json +{ + "type": "Create", + "actor": { + "type": "Person", + "id": "users/ada-lovelace", + "name": "Ada Lovelace" + }, + "object": { + "type": "Note", + "id": "1567", + "name": "message", + "content": "{\"message\":\"hi {mention-call1} !\",\"parameters\":{\"mention-call1\":{\"type\":\"call\",\"id\":\"n3xtc10ud\",\"name\":\"world\",\"call-type\":\"group\",\"icon-url\":\"https:\\/\\/nextcloud.local\\/ocs\\/v2.php\\/apps\\/spreed\\/api\\/v1\\/room\\/n3xtc10ud\\/avatar\"}}}", + "mediaType": "text/markdown" + }, + "target": { + "type": "Collection", + "id": "n3xtc10ud", + "name": "world" + } +} +``` + +#### Explanation + +| Path | Description | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| actor.id | One of the known [attendee types](constants.md#attendee-types) followed by the `/` slash character and a unique identifier within the given type. For users it is the Nextcloud user ID, for guests a sha1 value. | +| actor.name | The display name of the attendee sending the message. | +| object.id | The message ID of the given message on the origin server. It can be used to react or reply to the given message. | +| object.name | For normal written messages `message`, otherwise one of the known [system message identifiers](chat.md#system-messages). | +| object.content | A JSON encoded dictionary with a `message` and `parameters` key. The message can include placeholders and the [Rich Object parameters](https://github.com/nextcloud/server/blob/master/lib/public/RichObjectStrings/Definitions.php) are rendered into it in the chat view. | +| object.mediaType | `text/markdown` when the message should be interpreted as Markdown. Otherwise `text/plain`. | +| target.id | The token of the conversation in which the message was posted. It can be used to react or reply to the given message. | +| target.name | The name of the conversation in which the message was posted. | + +## Sending a chat message + +Bots can also send message. On the sending process the same signature/verification method is applied. + +* Required capability: `bots-v1` +* Method: `POST` +* Endpoint: `/bot/{token}/message` +* Header: + +| Name | Description | +|----------------------------------|-----------------------------------------------------------------| +| `X-Nextcloud-Talk-Bot-Random` | The random value used when signing the request | +| `X-Nextcloud-Talk-Bot-Signature` | The signature to validate the request comes from an enabled bot | +| `OCS-APIRequest` | Needs to be set to `true` to access the ocs/vX.php endpoint | + +* Data: + +| field | type | Description | +|--------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `message` | string | The message the user wants to say | +| `replyTo` | int | The message ID this message is a reply to (only allowed for messages from the same conversation and when the message type is not `system` or `command`) | +| `referenceId` | string | A reference string to be able to identify the message again in a "get messages" request, should be a random sha256 | +| `silent` | bool | If sent silent the message will not create chat notifications even for users | + +* Response: + - Status code: + + `201 Created` When the message was posted successfully + + `400 Bad Request` When the provided replyTo parameter is invalid + + `401 Unauthenticated` When the bot could not be verified for the conversation + + `404 Not Found` When the conversation could not be found + + `413 Payload Too Large` When the message was longer than the allowed limit of 32000 characters (or 1000 until Nextcloud 16.0.1, check the `spreed => config => chat => max-length` capability for the limit) + + `429 Too Many Requests` When `401 Unauthenticated` was triggered too often + +## Reacting to a chat message + +Bots can also react to a message. The same signature/verification method is applied. + +* Required capability: `bots-v1` +* Method: `POST` +* Endpoint: `/bot/{token}/reaction/{messageId}` +* Header: + +| Name | Description | +|----------------------------------|-----------------------------------------------------------------| +| `X-Nextcloud-Talk-Bot-Random` | The random value used when signing the request | +| `X-Nextcloud-Talk-Bot-Signature` | The signature to validate the request comes from an enabled bot | +| `OCS-APIRequest` | Needs to be set to `true` to access the ocs/vX.php endpoint | + +* Data: + +| field | type | Description | +|------------|--------|----------------| +| `reaction` | string | A single emoji | + +* Response: + - Status code: + + `201 Created` When the reaction was created successfully + + `400 Bad Request` When the provided emoji was invalid + + `401 Unauthenticated` When the bot could not be verified for the conversation + + `404 Not Found` When the conversation or message could not be found + + `429 Too Many Requests` When `401 Unauthenticated` was triggered too often + +## Delete a reaction + +Bots can also remove their previous reaction from amessage. The same signature/verification method is applied. + +* Required capability: `bots-v1` +* Method: `DELETE` +* Endpoint: `/bot/{token}/reaction/{messageId}` +* Header: + +| Name | Description | +|----------------------------------|-----------------------------------------------------------------| +| `X-Nextcloud-Talk-Bot-Random` | The random value used when signing the request | +| `X-Nextcloud-Talk-Bot-Signature` | The signature to validate the request comes from an enabled bot | +| `OCS-APIRequest` | Needs to be set to `true` to access the ocs/vX.php endpoint | + +* Data: + +| field | type | Description | +|------------|--------|----------------| +| `reaction` | string | A single emoji | + +* Response: + - Status code: + + `200 OK` When the reaction was deleted successfully + + `400 Bad Request` When the provided emoji was invalid + + `401 Unauthenticated` When the bot could not be verified for the conversation + + `404 Not Found` When the conversation or message could not be found + + `429 Too Many Requests` When `401 Unauthenticated` was triggered too often diff --git a/docs/botscreenshots/call-summary.png b/docs/botscreenshots/call-summary.png new file mode 100644 index 00000000000..b0b3cf87810 Binary files /dev/null and b/docs/botscreenshots/call-summary.png differ diff --git a/docs/capabilities.md b/docs/capabilities.md index 3922b54d520..2fb160374f2 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -124,3 +124,4 @@ ## 17.1 * `remind-me-later` - Support for "Remind me later" for chat messages exists +* `bots-v1` - Support of the first version for Bots and Webhooks is available diff --git a/docs/commands.md b/docs/commands.md index 3aca4e06390..629abb682b1 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,11 @@ # Chat commands +!!! warning + + **Deprecation:** Commands are deprecated in favor of [Bots](bots.md). + +--- + !!! note For security reasons commands can only be added via the diff --git a/docs/constants.md b/docs/constants.md index 729fa33fbe8..b469770368c 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -147,6 +147,13 @@ * `0` - Public: Participants can see the result immediately and also who voted for which option * `1` - Hidden: The result is hidden until the poll is closed and then only the number of votes for each option are displayed +## Bots + +### Bot states +* `0` Disabled +* `1` Enabled +* `2` No setup - The bot can neither be enabled nor disabled by a moderator + ## Signaling modes * `internal` - No external signaling server is used * `external` - A single external signaling server is used diff --git a/lib/Controller/BotController.php b/lib/Controller/BotController.php index 3e763dc13db..24dbe149e32 100644 --- a/lib/Controller/BotController.php +++ b/lib/Controller/BotController.php @@ -238,6 +238,21 @@ public function deleteReaction(string $token, int $messageId, string $reaction): return new DataResponse([], Http::STATUS_OK); } + /** + * Admin required + */ + public function adminListBots(): DataResponse { + $data = []; + $bots = $this->botServerMapper->getAllBots(); + foreach ($bots as $bot) { + $botData = $bot->jsonSerialize(); + unset($botData['secret']); + $data[] = $botData; + } + + return new DataResponse($data); + } + #[NoAdminRequired] #[RequireLoggedInModeratorParticipant] public function listBots(): DataResponse { diff --git a/mkdocs.yml b/mkdocs.yml index 31f29074e79..0dd650be541 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,9 +23,11 @@ nav: - 'Server system requirements': 'system-requirements.md' - 'Configuring coTURN': 'TURN.md' - 'Occ commands': 'occ.md' - - 'Commands': 'commands.md' + - 'Bots': 'bot-list.md' + - 'Commands (deprecated)': 'commands.md' - 'Matterbridge integration': 'matterbridge.md' - 'Developer documentation': + - 'Bots and webhooks': 'bots.md' - 'Constants': 'constants.md' - 'Capabilities': 'capabilities.md' - 'PHP events': 'events.md' @@ -39,6 +41,7 @@ nav: - 'Reaction management': 'reaction.md' - 'Poll management': 'poll.md' - 'Breakout rooms management': 'breakout-rooms.md' + - 'Bots management': 'bot-management.md' - 'Webinar management': 'webinar.md' - 'Settings': 'settings.md' - 'Integration by other apps': 'integration.md' diff --git a/package-lock.json b/package-lock.json index 6d06bc2f15a..deffe4c9d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "talk", - "version": "17.0.3", + "version": "17.1.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "talk", - "version": "17.0.3", + "version": "17.1.0-rc.1", "license": "agpl", "dependencies": { "@linusborg/vue-simple-portal": "^0.1.5", diff --git a/package.json b/package.json index d8777f6b6e2..44208c2b72a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "17.0.3", + "version": "17.1.0-rc.1", "private": true, "description": "", "author": "Joas Schilling ", diff --git a/src/components/AdminSettings/BotsSettings.vue b/src/components/AdminSettings/BotsSettings.vue new file mode 100644 index 00000000000..3cd7926eaa7 --- /dev/null +++ b/src/components/AdminSettings/BotsSettings.vue @@ -0,0 +1,209 @@ + + + + + + + diff --git a/src/components/AdminSettings/Commands.vue b/src/components/AdminSettings/Commands.vue index dd4167d67be..71ac59fe6e3 100644 --- a/src/components/AdminSettings/Commands.vue +++ b/src/components/AdminSettings/Commands.vue @@ -24,7 +24,7 @@

{{ t('spreed', 'Commands') }} - {{ t('spreed', 'Beta') }} + {{ t('spreed', 'Deprecated') }}

diff --git a/src/components/ConversationSettings/BotsSettings.vue b/src/components/ConversationSettings/BotsSettings.vue index 4902171d513..6732a540405 100644 --- a/src/components/ConversationSettings/BotsSettings.vue +++ b/src/components/ConversationSettings/BotsSettings.vue @@ -40,7 +40,7 @@
{{ toggleButtonTitle(bot) }} @@ -108,12 +108,12 @@ export default { }, methods: { - isBotForceEnabled(bot) { - return bot.state === BOT.STATE.FORCE_ENABLED + isBotLocked(bot) { + return bot.state === BOT.STATE.NO_SETUP }, async toggleBotState(bot) { - if (this.isBotForceEnabled(bot)) { + if (this.isBotLocked(bot)) { return } this.isLoading[bot.id] = true @@ -122,7 +122,7 @@ export default { }, toggleButtonTitle(bot) { - if (this.isBotForceEnabled(bot)) { + if (this.isBotLocked(bot)) { return t('spreed', 'Enabled') } diff --git a/src/constants.js b/src/constants.js index 09b05f2913b..5681b8064d9 100644 --- a/src/constants.js +++ b/src/constants.js @@ -221,6 +221,6 @@ export const BOT = { STATE: { DISABLED: 0, ENABLED: 1, - FORCE_ENABLED: 2, + NO_SETUP: 2, }, } diff --git a/src/services/botsService.js b/src/services/botsService.js index 0eb39018eb2..09c653256f8 100644 --- a/src/services/botsService.js +++ b/src/services/botsService.js @@ -23,6 +23,15 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' +/** + * Get information about available bots for this instance + * + * @return {object} The axios response + */ +const getAllBots = async function() { + return axios.get(generateOcsUrl('/apps/spreed/api/v1/bot/admin')) +} + /** * Get information about available bots for this conversation * @@ -55,29 +64,9 @@ const disableBotForConversation = async function(token, id) { return axios.delete(generateOcsUrl('/apps/spreed/api/v1/bot/{token}/{id}', { token, id })) } -/** - * Send a message to bot in conversation - * - * @param {string} token The conversation token - * @param {object} object Object with arguments - * @param {string} object.message The message to send - * @param {string} object.referenceId for the message to be able to later identify it again - * @param {number} object.replyTo Parent id which this message is a reply to - * @param {boolean} object.silent If sent silent the chat message will not create any notifications - * @return {object} The axios response - */ -const sendMessageToBot = async function(token, { message, referenceId, replyTo, silent }) { - return axios.post(generateOcsUrl('/apps/spreed/api/v1/bot/{token}/message', { token }), { - message, - referenceId, - replyTo, - silent, - }) -} - export { + getAllBots, getConversationBots, enableBotForConversation, disableBotForConversation, - sendMessageToBot, } diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index bd65f638493..199fc0b6824 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -25,6 +25,7 @@ + @@ -38,6 +39,7 @@