diff --git a/.github/workflows/integration-mariadb.yml b/.github/workflows/integration-mariadb.yml
index a1bd12d2df8..f49e34173b7 100644
--- a/.github/workflows/integration-mariadb.yml
+++ b/.github/workflows/integration-mariadb.yml
@@ -56,7 +56,6 @@ jobs:
php-versions: ['8.2']
server-versions: ['stable29']
guests-versions: ['stable29']
- call-summary-bot-versions: ['main']
notifications-versions: ['stable29']
services:
@@ -86,13 +85,6 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}
- - name: Checkout call_summary_bot app
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: nextcloud/call_summary_bot
- path: apps/call_summary_bot
- ref: ${{ matrix.call-summary-bot-versions }}
-
- name: Checkout guests app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
@@ -140,7 +132,6 @@ jobs:
./occ config:system:set debug --value=true --type=boolean
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ app:enable --force ${{ env.APP_NAME }}
- ./occ app:enable --force call_summary_bot
./occ app:enable --force guests
./occ app:enable --force notifications
diff --git a/.github/workflows/integration-mysql.yml b/.github/workflows/integration-mysql.yml
index a05d0f1fdb8..637241ad07d 100644
--- a/.github/workflows/integration-mysql.yml
+++ b/.github/workflows/integration-mysql.yml
@@ -56,7 +56,6 @@ jobs:
php-versions: ['8.2']
server-versions: ['stable29']
guests-versions: ['stable29']
- call-summary-bot-versions: ['main']
notifications-versions: ['stable29']
services:
@@ -86,13 +85,6 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}
- - name: Checkout call_summary_bot app
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: nextcloud/call_summary_bot
- path: apps/call_summary_bot
- ref: ${{ matrix.call-summary-bot-versions }}
-
- name: Checkout guests app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
@@ -140,7 +132,6 @@ jobs:
./occ config:system:set debug --value=true --type=boolean
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ app:enable --force ${{ env.APP_NAME }}
- ./occ app:enable --force call_summary_bot
./occ app:enable --force guests
./occ app:enable --force notifications
diff --git a/.github/workflows/integration-oci.yml b/.github/workflows/integration-oci.yml
index 82bff3b022d..19be36f12e9 100644
--- a/.github/workflows/integration-oci.yml
+++ b/.github/workflows/integration-oci.yml
@@ -56,7 +56,6 @@ jobs:
php-versions: ['8.2']
server-versions: ['stable29']
guests-versions: ['stable29']
- call-summary-bot-versions: ['main']
notifications-versions: ['stable29']
services:
@@ -98,13 +97,6 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}
- - name: Checkout call_summary_bot app
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: nextcloud/call_summary_bot
- path: apps/call_summary_bot
- ref: ${{ matrix.call-summary-bot-versions }}
-
- name: Checkout guests app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
@@ -147,7 +139,6 @@ jobs:
./occ config:system:set debug --value=true --type=boolean
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ app:enable --force ${{ env.APP_NAME }}
- ./occ app:enable --force call_summary_bot
./occ app:enable --force guests
./occ app:enable --force notifications
diff --git a/.github/workflows/integration-pgsql.yml b/.github/workflows/integration-pgsql.yml
index 2734a77b02f..e4091def695 100644
--- a/.github/workflows/integration-pgsql.yml
+++ b/.github/workflows/integration-pgsql.yml
@@ -53,7 +53,6 @@ jobs:
php-versions: ['8.3']
server-versions: ['stable29']
guests-versions: ['stable29']
- call-summary-bot-versions: ['main']
notifications-versions: ['stable29']
services:
@@ -89,13 +88,6 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}
- - name: Checkout call_summary_bot app
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: nextcloud/call_summary_bot
- path: apps/call_summary_bot
- ref: ${{ matrix.call-summary-bot-versions }}
-
- name: Checkout guests app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
@@ -142,7 +134,6 @@ jobs:
./occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
./occ config:system:set memcache.distributed --value="\\OC\\Memcache\\APCu"
./occ app:enable --force ${{ env.APP_NAME }}
- ./occ app:enable --force call_summary_bot
./occ app:enable --force guests
./occ app:enable --force notifications
diff --git a/.github/workflows/integration-sqlite.yml b/.github/workflows/integration-sqlite.yml
index eeb99afd736..3e2002a6f14 100644
--- a/.github/workflows/integration-sqlite.yml
+++ b/.github/workflows/integration-sqlite.yml
@@ -56,7 +56,6 @@ jobs:
php-versions: ['8.2']
server-versions: ['stable29']
guests-versions: ['stable29']
- call-summary-bot-versions: ['main']
notifications-versions: ['stable29']
steps:
@@ -77,13 +76,6 @@ jobs:
with:
path: apps/${{ env.APP_NAME }}
- - name: Checkout call_summary_bot app
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: nextcloud/call_summary_bot
- path: apps/call_summary_bot
- ref: ${{ matrix.call-summary-bot-versions }}
-
- name: Checkout guests app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
@@ -126,7 +118,6 @@ jobs:
./occ config:system:set debug --value=true --type=boolean
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ app:enable --force ${{ env.APP_NAME }}
- ./occ app:enable --force call_summary_bot
./occ app:enable --force guests
./occ app:enable --force notifications
diff --git a/composer.lock b/composer.lock
index 2ddbe57842d..36e5f017a86 100644
--- a/composer.lock
+++ b/composer.lock
@@ -258,12 +258,12 @@
"source": {
"type": "git",
"url": "https://github.com/nextcloud-deps/ocp.git",
- "reference": "5d14cd5c2d92046ce268033a8e420f4acf054403"
+ "reference": "a0e83a946dbc06986094e7e9ccb68fcb05de473a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/5d14cd5c2d92046ce268033a8e420f4acf054403",
- "reference": "5d14cd5c2d92046ce268033a8e420f4acf054403",
+ "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/a0e83a946dbc06986094e7e9ccb68fcb05de473a",
+ "reference": "a0e83a946dbc06986094e7e9ccb68fcb05de473a",
"shasum": ""
},
"require": {
@@ -294,7 +294,7 @@
"issues": "https://github.com/nextcloud-deps/ocp/issues",
"source": "https://github.com/nextcloud-deps/ocp/tree/stable29"
},
- "time": "2025-01-08T00:43:58+00:00"
+ "time": "2025-01-21T00:42:15+00:00"
},
{
"name": "psr/clock",
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 58e62631689..2e37147ed8c 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -188,6 +188,7 @@ public function setUp() {
self::$lastNotifications = [];
self::$phoneNumberToActorId = [];
self::$modifiedSince = [];
+ self::$botNameToId = [];
$this->createdUsers = [];
$this->createdGroups = [];
diff --git a/tests/integration/features/chat-1/bots.feature b/tests/integration/features/chat-1/bots.feature
index 2d784f32197..ba58d329919 100644
--- a/tests/integration/features/chat-1/bots.feature
+++ b/tests/integration/features/chat-1/bots.feature
@@ -2,32 +2,32 @@ Feature: chat/bots
Background:
Given user "participant1" exists
- Scenario: Installing the call summary bot
+ Scenario: Installing the Webhook Demo bot
Given invoking occ with "talk:bot:list"
Then the command was successful
And the command output is empty
- Given invoking occ with "app:disable call_summary_bot"
+ Given invoking occ with "app:disable talk_webhook_demo"
And the command was successful
- And invoking occ with "app:enable call_summary_bot"
+ And invoking occ with "app:enable talk_webhook_demo"
And the command was successful
When invoking occ with "talk:bot:list"
Then the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
And read bot ids from OCC
- And set state no-setup for bot "Call summary" via OCC
+ And set state no-setup for bot "Webhook Demo" via OCC
| feature |
| webhook |
| response |
- Scenario: Simple call summary bot run
+ Scenario: Simple Webhook Demo bot run
# Populate default options again
- And invoking occ with "app:disable call_summary_bot"
+ And invoking occ with "app:disable talk_webhook_demo"
And the command was successful
- And invoking occ with "app:enable call_summary_bot"
+ And invoking occ with "app:enable talk_webhook_demo"
And the command was successful
And invoking occ with "talk:bot:list"
And the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
# Set up in room
Given invoking occ with "talk:bot:list room-name:room"
@@ -37,13 +37,13 @@ Feature: chat/bots
| roomType | 2 |
| roomName | room |
And read bot ids from OCC
- And setup bot "Call summary" for room "room" via OCC
+ And setup bot "Webhook Demo" for room "room" via OCC
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
- # Call summary
- Given the following call_summary_bot app config is set
+ # Webhook Demo
+ Given the following talk_webhook_demo app config is set
| min-length | -1 |
And user "participant1" sends message "- [ ] Before call" to room "room" with 201
And wait for 2 seconds
@@ -52,15 +52,15 @@ Feature: chat/bots
| flags | 1 |
And user "participant1" sends message "- [ ] Task 1" to room "room" with 201
And user "participant1" sends message "- [ ] Task 2\n- [ ] Task 3" to room "room" with 201
- And set state enabled for bot "Call summary" via OCC
+ And set state enabled for bot "Webhook Demo" via OCC
| feature |
| webhook |
And user "participant1" sends message "- [ ] Received but no reaction permission" to room "room" with 201
- And set state enabled for bot "Call summary" via OCC
+ And set state enabled for bot "Webhook Demo" via OCC
| feature |
| none |
And user "participant1" sends message "- [ ] Not received due to permission" to room "room" with 201
- And set state enabled for bot "Call summary" via OCC
+ And set state enabled for bot "Webhook Demo" via OCC
| feature |
| webhook |
| response |
@@ -75,7 +75,7 @@ Feature: chat/bots
Then user "participant1" leaves room "room" with 200 (v4)
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters |
- | room | bots | BOT(Call summary) | Call summary (Bot) | # Call summary - room\n\n{DATE}\n\n## Attendees\n- participant1-displayname\n\n## Tasks\n- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3\n- [ ] Received but no reaction permission | [] |
+ | room | bots | BOT(Webhook Demo) | Webhook Demo (Bot) | # Call summary - room\n\n{DATE}\n\n## Attendees\n- participant1-displayname\n\n## Tasks\n- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3\n- [ ] Received but no reaction permission | [] |
| room | users | participant1 | participant1-displayname | - [ ] Not received due to permission | [] |
| room | users | participant1 | participant1-displayname | - [ ] Received but no reaction permission | [] |
| room | users | participant1 | participant1-displayname | - [ ] Task 2\n- [ ] Task 3 | [] |
@@ -85,10 +85,10 @@ Feature: chat/bots
| actorType | actorId | actorDisplayName | reaction |
Then user "participant1" retrieve reactions "๐" of message "- [ ] Task 1" in room "room" with 200
| actorType | actorId | actorDisplayName | reaction |
- | bots | BOT(Call summary) | Call summary (Bot) | ๐ |
+ | bots | BOT(Webhook Demo) | Webhook Demo (Bot) | ๐ |
Then user "participant1" retrieve reactions "๐" of message "- [ ] Task 2\n- [ ] Task 3" in room "room" with 200
| actorType | actorId | actorDisplayName | reaction |
- | bots | BOT(Call summary) | Call summary (Bot) | ๐ |
+ | bots | BOT(Webhook Demo) | Webhook Demo (Bot) | ๐ |
Then user "participant1" retrieve reactions "๐" of message "- [ ] Received but no reaction permission" in room "room" with 200
| actorType | actorId | actorDisplayName | reaction |
Then user "participant1" retrieve reactions "๐" of message "- [ ] Not received due to permission" in room "room" with 200
@@ -96,36 +96,36 @@ Feature: chat/bots
# Different states bot
# Already enabled
- And user "participant1" sets up bot "Call summary" for room "room" with 200 (v1)
+ And user "participant1" sets up bot "Webhook Demo" for room "room" with 200 (v1)
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
# Disabling
- And user "participant1" removes bot "Call summary" for room "room" with 200 (v1)
+ And user "participant1" removes bot "Webhook Demo" for room "room" with 200 (v1)
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
And the command output is empty
# Enabling
- And user "participant1" sets up bot "Call summary" for room "room" with 201 (v1)
+ And user "participant1" sets up bot "Webhook Demo" for room "room" with 201 (v1)
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
# No-setup
- And set state no-setup for bot "Call summary" via OCC
+ And set state no-setup for bot "Webhook Demo" via OCC
## Failed removing
- And user "participant1" removes bot "Call summary" for room "room" with 400 (v1)
+ And user "participant1" removes bot "Webhook Demo" for room "room" with 400 (v1)
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
- And the command output contains the text "Call summary"
+ And the command output contains the text "Webhook Demo"
## Failed adding
- And remove bot "Call summary" for room "room" via OCC
+ And remove bot "Webhook Demo" for room "room" via OCC
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
And the command output is empty
- And user "participant1" sets up bot "Call summary" for room "room" with 400 (v1)
+ And user "participant1" sets up bot "Webhook Demo" for room "room" with 400 (v1)
Given invoking occ with "talk:bot:list room-name:room"
And the command was successful
And the command output is empty
@@ -356,4 +356,3 @@ Feature: chat/bots
When invoking occ with "talk:bot:uninstall --url=https://example.tld"
Then the command failed with exit code 1
And the command output contains the text "Bot not found"
-
diff --git a/tests/integration/run.sh b/tests/integration/run.sh
index d17538127cc..b27729619e3 100755
--- a/tests/integration/run.sh
+++ b/tests/integration/run.sh
@@ -5,7 +5,6 @@ PROCESS_ID=$$
APP_NAME=spreed
NOTIFICATIONS_BRANCH="master"
GUESTS_BRANCH="master"
-CSB_BRANCH="main"
APP_INTEGRATION_DIR=$PWD
ROOT_DIR=${APP_INTEGRATION_DIR}/../../../..
@@ -63,23 +62,24 @@ echo -e "\033[0;36m# Setting up apps\033[0m"
echo -e "\033[0;36m#\033[0m"
cp -R ./spreedcheats ../../../spreedcheats
${ROOT_DIR}/occ app:getpath spreedcheats
+cp -R ./talk_webhook_demo ../../../talk_webhook_demo
+${ROOT_DIR}/occ app:getpath talk_webhook_demo
# Add apps to the parent directory of "spreed" (unless they are
# already there or in "apps").
${ROOT_DIR}/occ app:getpath notifications || (cd ../../../ && git clone --depth 1 --branch ${NOTIFICATIONS_BRANCH} https://github.com/nextcloud/notifications)
${ROOT_DIR}/occ app:getpath guests || (cd ../../../ && git clone --depth 1 --branch ${GUESTS_BRANCH} https://github.com/nextcloud/guests)
-${ROOT_DIR}/occ app:getpath call_summary_bot || (cd ../../../ && git clone --depth 1 --branch ${CSB_BRANCH} https://github.com/nextcloud/call_summary_bot)
${ROOT_DIR}/occ app:enable spreed || exit 1
${ROOT_DIR}/occ app:enable --force spreedcheats || exit 1
+${ROOT_DIR}/occ app:enable --force talk_webhook_demo || exit 1
${ROOT_DIR}/occ app:enable --force notifications || exit 1
${ROOT_DIR}/occ app:enable --force guests || exit 1
-${ROOT_DIR}/occ app:enable --force call_summary_bot || exit 1
${ROOT_DIR}/occ app:list | grep spreed
+${ROOT_DIR}/occ app:list | grep talk_webhook_demo
${ROOT_DIR}/occ app:list | grep notifications
${ROOT_DIR}/occ app:list | grep guests
-${ROOT_DIR}/occ app:list | grep call_summary_bot
echo ''
echo -e "\033[0;36m#\033[0m"
@@ -125,11 +125,13 @@ echo -e "\033[0;36m#\033[0m"
echo -e "\033[0;36m# Reverting configuration changes and disabling spreedcheats\033[0m"
echo -e "\033[0;36m#\033[0m"
${ROOT_DIR}/occ app:disable spreedcheats
+${ROOT_DIR}/occ app:disable talk_webhook_demo
${ROOT_DIR}/occ config:system:set overwrite.cli.url --value $OVERWRITE_CLI_URL
if [[ "$SKELETON_DIR" ]]; then
${ROOT_DIR}/occ config:system:set skeletondirectory --value "$SKELETON_DIR"
fi
rm -rf ../../../spreedcheats
+rm -rf ../../../talk_webhook_demo
wait $PHPPID1
wait $PHPPID2
diff --git a/tests/integration/talk_webhook_demo/appinfo/info.xml b/tests/integration/talk_webhook_demo/appinfo/info.xml
new file mode 100644
index 00000000000..74add6ce48f
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/appinfo/info.xml
@@ -0,0 +1,33 @@
+
+
+
+ talk_webhook_demo
+ Talk Webhook demo
+
+
+
+ 1.0.0
+ agpl
+
+ Joas Schilling
+ TalkWebhookDemo
+ workflow
+ https://github.com/nextcloud/spreed/issues
+
+
+
+
+
+
+
+ OCA\TalkWebhookDemo\Migration\InstallBot
+
+
+ OCA\TalkWebhookDemo\Migration\UninstallBot
+
+
+
diff --git a/tests/integration/talk_webhook_demo/appinfo/routes.php b/tests/integration/talk_webhook_demo/appinfo/routes.php
new file mode 100644
index 00000000000..2698efe9c40
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/appinfo/routes.php
@@ -0,0 +1,14 @@
+ [
+ /** @see \OCA\TalkWebhookDemo\Controller\BotController::receiveWebhook() */
+ ['name' => 'Bot#receiveWebhook', 'url' => '/api/v1/bot/{lang}', 'verb' => 'POST'],
+ ],
+];
diff --git a/tests/integration/talk_webhook_demo/lib/Controller/BotController.php b/tests/integration/talk_webhook_demo/lib/Controller/BotController.php
new file mode 100644
index 00000000000..ead15fd9a0a
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Controller/BotController.php
@@ -0,0 +1,304 @@
+logger->warning('Request for unsupported language was sent');
+ $response = new DataResponse([], Http::STATUS_BAD_REQUEST);
+ $response->throttle(['action' => 'webhook']);
+ return $response;
+ }
+
+ $signature = $this->request->getHeader('X_NEXTCLOUD_TALK_SIGNATURE');
+ $random = $this->request->getHeader('X_NEXTCLOUD_TALK_RANDOM');
+ $server = rtrim($this->request->getHeader('X_NEXTCLOUD_TALK_BACKEND'), '/') . '/';
+
+ $secretData = $this->config->getAppValue('talk_webhook_demo', 'secret_' . sha1($server));
+ if ($secretData === '') {
+ $this->logger->warning('No matching secret found for server: ' . $server);
+ $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ $response->throttle(['action' => 'webhook']);
+ return $response;
+ }
+
+ try {
+ $config = json_decode($secretData, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\JsonException) {
+ $this->logger->error('Could not json_decode config');
+ return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ $body = $this->getInputStream();
+ $secret = $config['secret'] . str_replace('_', '', $lang);
+ $generatedDigest = hash_hmac('sha256', $random . $body, $secret);
+
+ if (!hash_equals($generatedDigest, strtolower($signature))) {
+ $generatedLegacyDigest = hash_hmac('sha256', $random . $body, $config['secret']);
+ if (!hash_equals($generatedLegacyDigest, strtolower($signature))) {
+ $this->logger->warning('Message signature could not be verified');
+ $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ $response->throttle(['action' => 'webhook']);
+ return $response;
+ }
+ // Installed before final release, when the secret was not unique
+ $secret = $config['secret'];
+ $this->legacySecret = true;
+ }
+
+ $this->logger->debug($body);
+ $data = json_decode($body, true);
+
+ if ($data['type'] === 'Create' && $data['object']['name'] === 'message') {
+ $messageData = json_decode($data['object']['content'], true);
+ $message = $messageData['message'];
+
+ if (!$this->logEntryMapper->hasActiveCall($server, $data['target']['id'])) {
+ $agendaDetected = $this->summaryService->readAgendaFromMessage($message, $messageData, $server, $data);
+
+ if ($agendaDetected) {
+ // React with thumbs up as we detected an agenda item
+ $this->sendReaction($server, $secret, $data);
+ }
+ return new DataResponse();
+ }
+
+ $taskDetected = $this->summaryService->readTasksFromMessage($message, $messageData, $server, $data);
+
+ if ($taskDetected) {
+ // React with thumbs up as we detected a task
+ $this->sendReaction($server, $secret, $data);
+ // Sample: $this->removeReaction($server, $secret, $data);
+ }
+ } elseif ($data['type'] === 'Activity') {
+ if ($data['object']['name'] === 'call_joined' || $data['object']['name'] === 'call_started') {
+ if ($data['object']['name'] === 'call_started') {
+ $this->postAgenda($server, $secret, $random, $data, $lang);
+
+ $logEntry = new LogEntry();
+ $logEntry->setServer($server);
+ $logEntry->setToken($data['target']['id']);
+ $logEntry->setType(LogEntry::TYPE_START);
+ $logEntry->setDetails((string)$this->timeFactory->now()->getTimestamp());
+ $this->logEntryMapper->insert($logEntry);
+
+ $logEntry = new LogEntry();
+ $logEntry->setServer($server);
+ $logEntry->setToken($data['target']['id']);
+ $logEntry->setType(LogEntry::TYPE_ELEVATOR);
+ $logEntry->setDetails((string)$data['object']['id']);
+ $this->logEntryMapper->insert($logEntry);
+ }
+
+ $logEntry = new LogEntry();
+ $logEntry->setServer($server);
+ $logEntry->setToken($data['target']['id']);
+ $logEntry->setType(LogEntry::TYPE_ATTENDEE);
+
+ $displayName = $data['actor']['name'];
+ if (str_starts_with($data['actor']['id'], 'guests/') || str_starts_with($data['actor']['id'], 'emails/')) {
+ if ($displayName === '') {
+ return new DataResponse();
+ }
+ $l = $this->l10nFactory->get('talk_webhook_demo', $lang);
+ $displayName = $l->t('%s (guest)', $displayName);
+ } elseif (str_starts_with($data['actor']['id'], 'federated_users/')) {
+ $cloudIdServer = explode('@', $data['actor']['id']);
+ $displayName .= ' (' . array_pop($cloudIdServer) . ')';
+ }
+
+ $logEntry->setDetails($displayName);
+ if ($logEntry->getDetails()) {
+ // Only store when not empty
+ $this->logEntryMapper->insert($logEntry);
+ }
+ } elseif ($data['object']['name'] === 'call_ended' || $data['object']['name'] === 'call_ended_everyone') {
+ $summary = $this->summaryService->summarize($server, $data['target']['id'], $data['target']['name'], $lang);
+ if ($summary !== null) {
+ $body = [
+ 'message' => $summary['summary'],
+ 'referenceId' => sha1($random),
+ ];
+
+ if (!empty($summary['elevator'])) {
+ $body['replyTo'] = $summary['elevator'];
+ }
+
+ // Generate and post summary
+ $this->sendResponse($server, $secret, $body, $data);
+ }
+ }
+ }
+ return new DataResponse();
+ }
+
+ protected function postAgenda(string $server, string $secret, string $random, array $data, string $lang): void {
+ $agenda = $this->summaryService->agenda($server, $data['target']['id'], $lang);
+ if ($agenda !== null) {
+ $body = [
+ 'message' => $agenda,
+ 'referenceId' => sha1($random),
+ ];
+
+ // Generate and post summary
+ $this->sendResponse($server, $secret, $body, $data);
+ }
+ }
+
+ protected function sendResponse(string $server, string $secret, array $body, array $data): void {
+ $jsonBody = json_encode($body, JSON_THROW_ON_ERROR);
+
+ $random = bin2hex(random_bytes(32));
+ $hash = hash_hmac('sha256', $random . $body['message'], $secret);
+ $this->logger->debug('Reply: Random ' . $random);
+ $this->logger->debug('Reply: Hash ' . $hash);
+
+ try {
+ $options = [
+ 'headers' => [
+ 'OCS-APIRequest' => 'true',
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ 'X-Nextcloud-Talk-Bot-Random' => $random,
+ 'X-Nextcloud-Talk-Bot-Signature' => $hash,
+ 'User-Agent' => 'nextcloud-call-summary-bot/1.0',
+ ],
+ 'body' => $jsonBody,
+ 'verify' => $this->certificateManager->getAbsoluteBundlePath(),
+ 'nextcloud' => [
+ 'allow_local_address' => true,
+ ],
+ ];
+
+ $client = $this->clientService->newClient();
+ $response = $client->post(rtrim($server, '/') . '/ocs/v2.php/apps/spreed/api/v1/bot/' . $data['target']['id'] . '/message', $options);
+ $this->logger->info('Response: ' . $response->getBody());
+ } catch (\Exception $exception) {
+ $this->logger->info($exception::class . ': ' . $exception->getMessage());
+ }
+ }
+
+ protected function sendReaction(string $server, string $secret, array $data): void {
+ $body = [
+ 'reaction' => '๐',
+ ];
+ $jsonBody = json_encode($body, JSON_THROW_ON_ERROR);
+
+ $random = bin2hex(random_bytes(32));
+ $hash = hash_hmac('sha256', $random . $body['reaction'], $secret);
+ $this->logger->debug('Reaction: Random ' . $random);
+ $this->logger->debug('Reaction: Hash ' . $hash);
+
+ try {
+ $options = [
+ 'headers' => [
+ 'OCS-APIRequest' => 'true',
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ 'X-Nextcloud-Talk-Bot-Random' => $random,
+ 'X-Nextcloud-Talk-Bot-Signature' => $hash,
+ 'User-Agent' => 'nextcloud-call-summary-bot/1.0',
+ ],
+ 'body' => $jsonBody,
+ 'verify' => $this->certificateManager->getAbsoluteBundlePath(),
+ 'nextcloud' => [
+ 'allow_local_address' => true,
+ ],
+ ];
+
+ $client = $this->clientService->newClient();
+ $response = $client->post(rtrim($server, '/') . '/ocs/v2.php/apps/spreed/api/v1/bot/' . $data['target']['id'] . '/reaction/' . $data['object']['id'], $options);
+ $this->logger->info('Response: ' . $response->getBody());
+ } catch (\Exception $exception) {
+ $this->logger->info($exception::class . ': ' . $exception->getMessage());
+ }
+ }
+
+ protected function removeReaction(string $server, string $secret, array $data): void {
+ $body = [
+ 'reaction' => '๐',
+ ];
+ $jsonBody = json_encode($body, JSON_THROW_ON_ERROR);
+
+ $random = bin2hex(random_bytes(32));
+ $hash = hash_hmac('sha256', $random . $body['reaction'], $secret);
+ $this->logger->debug('RemoveReaction: Random ' . $random);
+ $this->logger->debug('RemoveReaction: Hash ' . $hash);
+
+ try {
+ $options = [
+ 'headers' => [
+ 'OCS-APIRequest' => 'true',
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ 'X-Nextcloud-Talk-Bot-Random' => $random,
+ 'X-Nextcloud-Talk-Bot-Signature' => $hash,
+ 'User-Agent' => 'nextcloud-call-summary-bot/1.0',
+ ],
+ 'body' => $jsonBody,
+ 'verify' => $this->certificateManager->getAbsoluteBundlePath(),
+ 'nextcloud' => [
+ 'allow_local_address' => true,
+ ],
+ ];
+
+ $client = $this->clientService->newClient();
+ $response = $client->delete(rtrim($server, '/') . '/ocs/v2.php/apps/spreed/api/v1/bot/' . $data['target']['id'] . '/reaction/' . $data['object']['id'], $options);
+ $this->logger->info('Response: ' . $response->getBody());
+ } catch (\Exception $exception) {
+ $this->logger->info($exception::class . ': ' . $exception->getMessage());
+ }
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Migration/InstallBot.php b/tests/integration/talk_webhook_demo/lib/Migration/InstallBot.php
new file mode 100644
index 00000000000..9621b2b30ef
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Migration/InstallBot.php
@@ -0,0 +1,38 @@
+warning('Talk not found, not installing bots');
+ return;
+ }
+
+ $backend = $this->url->getAbsoluteURL('');
+ $this->service->installBot($backend);
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Migration/UninstallBot.php b/tests/integration/talk_webhook_demo/lib/Migration/UninstallBot.php
new file mode 100644
index 00000000000..db4f01fbe29
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Migration/UninstallBot.php
@@ -0,0 +1,48 @@
+warning('Talk not found, not removing the bots');
+ return;
+ }
+
+ $backend = $this->url->getAbsoluteURL('');
+ $id = sha1($backend);
+
+ $secretData = $this->config->getAppValue('talk_webhook_demo', 'secret_' . $id);
+ if ($secretData) {
+ $secretArray = json_decode($secretData, true, 512, JSON_THROW_ON_ERROR);
+ if ($secretArray['secret']) {
+ $this->service->uninstallBot($secretArray['secret'], $backend);
+ }
+ }
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Migration/Version1000Date20230719061613.php b/tests/integration/talk_webhook_demo/lib/Migration/Version1000Date20230719061613.php
new file mode 100644
index 00000000000..40d13681e75
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Migration/Version1000Date20230719061613.php
@@ -0,0 +1,76 @@
+hasTable('twd_log_entries')) {
+ $table = $schema->createTable('twd_log_entries');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 11,
+ ]);
+
+ $table->addColumn('server', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 512,
+ ]);
+ $table->addColumn('token', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+
+ $table->addColumn('type', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 32,
+ ]);
+ $table->addColumn('details', Types::TEXT, [
+ 'notnull' => false,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['server', 'token'], 'twd_log_entry_origin');
+ return $schema;
+ }
+ return null;
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ */
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Model/Bot.php b/tests/integration/talk_webhook_demo/lib/Model/Bot.php
new file mode 100644
index 00000000000..adee141172c
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Model/Bot.php
@@ -0,0 +1,23 @@
+addType('server', 'string');
+ $this->addType('token', 'string');
+ $this->addType('type', 'string');
+ $this->addType('details', 'string');
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Model/LogEntryMapper.php b/tests/integration/talk_webhook_demo/lib/Model/LogEntryMapper.php
new file mode 100644
index 00000000000..ddcbe517f4b
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Model/LogEntryMapper.php
@@ -0,0 +1,60 @@
+ findEntities(IQueryBuilder $query)
+ * @template-extends QBMapper
+ */
+class LogEntryMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'twd_log_entries', LogEntry::class);
+ }
+
+ /**
+ * @return LogEntry[]
+ */
+ public function findByConversation(string $server, string $token): array {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('server', $query->createNamedParameter($server)))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token)));
+ return $this->findEntities($query);
+ }
+
+ public function hasActiveCall(string $server, string $token): bool {
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->expr()->literal(1))
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('server', $query->createNamedParameter($server)))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter(LogEntry::TYPE_ATTENDEE)))
+ ->setMaxResults(1);
+ $result = $query->executeQuery();
+ $hasAttendee = (bool)$result->fetchOne();
+ $result->closeCursor();
+
+ return $hasAttendee;
+ }
+
+ public function deleteByConversation(string $server, string $token): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->getTableName())
+ ->where($query->expr()->eq('server', $query->createNamedParameter($server)))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token)));
+ $query->executeStatement();
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Service/BotService.php b/tests/integration/talk_webhook_demo/lib/Service/BotService.php
new file mode 100644
index 00000000000..c4cef05cd42
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Service/BotService.php
@@ -0,0 +1,101 @@
+config->getAppValue('talk_webhook_demo', 'secret_' . $id);
+ if ($secretData) {
+ $secretArray = json_decode($secretData, true, 512, JSON_THROW_ON_ERROR);
+ $secret = $secretArray['secret'] ?? $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
+ } else {
+ $secret = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
+ }
+ foreach (Bot::SUPPORTED_LANGUAGES as $lang) {
+ $this->installLanguage($secret, $lang);
+ }
+
+ $this->config->setAppValue('talk_webhook_demo', 'secret_' . $id, json_encode([
+ 'id' => $id,
+ 'secret' => $secret,
+ 'backend' => $backend,
+ ], JSON_THROW_ON_ERROR));
+ }
+
+ protected function installLanguage(string $secret, string $lang): void {
+ $libL10n = $this->l10nFactory->get('lib', $lang);
+ $langName = $libL10n->t('__language_name__');
+ if ($langName === '__language_name__') {
+ $langName = $lang === 'en' ? 'British English' : $lang;
+ }
+
+ $l = $this->l10nFactory->get('talk_webhook_demo', $lang);
+
+ $event = new BotInstallEvent(
+ $l->t('Webhook Demo'),
+ $secret . str_replace('_', '', $lang),
+ $this->url->linkToOCSRouteAbsolute('talk_webhook_demo.Bot.receiveWebhook', ['lang' => $lang]),
+ $l->t('Call summary (%s)', $langName) . ' - ' . $l->t('The call summary bot posts an overview message after the call listing all participants and outlining tasks'),
+ );
+ try {
+ $this->dispatcher->dispatchTyped($event);
+ } catch (\Throwable) {
+ }
+ }
+
+ public function uninstallBot(string $secret, string $backend): void {
+ foreach (Bot::SUPPORTED_LANGUAGES as $lang) {
+ $this->uninstallLanguage($secret, $backend, $lang);
+ }
+ }
+
+ protected function uninstallLanguage(string $secret, string $backend, string $lang): void {
+ $absoluteUrl = $this->url->getAbsoluteURL('');
+ $backendUrl = rtrim($backend, '/') . '/' . substr($this->url->linkToOCSRouteAbsolute('talk_webhook_demo.Bot.receiveWebhook', ['lang' => $lang]), strlen($absoluteUrl));
+
+ $event = new BotUninstallEvent(
+ $secret . str_replace('_', '', $lang),
+ $backendUrl,
+ );
+ try {
+ $this->dispatcher->dispatchTyped($event);
+ } catch (\Throwable $e) {
+ }
+
+ // Also remove legacy secret bots
+ $event = new BotUninstallEvent(
+ $secret,
+ $backendUrl,
+ );
+ try {
+ $this->dispatcher->dispatchTyped($event);
+ } catch (\Throwable) {
+ }
+ }
+}
diff --git a/tests/integration/talk_webhook_demo/lib/Service/SummaryService.php b/tests/integration/talk_webhook_demo/lib/Service/SummaryService.php
new file mode 100644
index 00000000000..6b556e36377
--- /dev/null
+++ b/tests/integration/talk_webhook_demo/lib/Service/SummaryService.php
@@ -0,0 +1,319 @@
+ $parameter) {
+ $placeholders[] = '{' . $placeholder . '}';
+ if ($parameter['type'] === 'user') {
+ if (str_contains($parameter['id'], ' ') || str_contains($parameter['id'], '/')) {
+ $replacements[] = '@"' . $parameter['id'] . '"';
+ } else {
+ $replacements[] = '@' . $parameter['id'];
+ }
+ } elseif ($parameter['type'] === 'call') {
+ $replacements[] = '@all';
+ } elseif ($parameter['type'] === 'guest') {
+ $replacements[] = '@' . $parameter['name'];
+ } else {
+ $replacements[] = $parameter['name'];
+ }
+ }
+
+ $parsedMessage = str_replace($placeholders, $replacements, $message);
+ $parsedMessage = preg_replace(self::TODO_SOLVED_PATTERN, '- solved: ', $parsedMessage);
+ $parsedMessage = preg_replace(self::TODO_UNSOLVED_PATTERN, '- todo: ', $parsedMessage);
+
+ if (preg_match(self::SUMMARY_PATTERN, $parsedMessage)) {
+ $todos = preg_split(self::SUMMARY_PATTERN, $parsedMessage, flags: PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+ $nextEntry = null;
+ foreach ($todos as $todo) {
+ if (preg_match(self::TODO_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_TODO;
+ } elseif (preg_match(self::SOLVED_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_SOLVED;
+ } elseif (preg_match(self::SOLVED_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_SOLVED;
+ } elseif (preg_match(self::NOTE_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_NOTE;
+ } elseif (preg_match(self::REPORT_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_REPORT;
+ } elseif (preg_match(self::DECISION_PATTERN, $todo)) {
+ $nextEntry = LogEntry::TYPE_DECISION;
+ } elseif ($nextEntry !== null) {
+ $todoText = trim($todo);
+ if ($todoText) {
+ // Only store when not empty
+ $this->saveTask($server, $data['target']['id'], $todoText, $nextEntry);
+ }
+ $nextEntry = null;
+ }
+ }
+
+ // React with thumbs up as we detected a task
+ return true;
+ }
+
+ return false;
+ }
+
+ public function readAgendaFromMessage(string $message, array $messageData, string $server, array $data): bool {
+ $endOfFirstLine = strpos($message, "\n") ?: -1;
+ $firstLowerLine = strtolower(substr($message, 0, $endOfFirstLine));
+
+ if (!preg_match(self::AGENDA_PATTERN, $firstLowerLine)) {
+ return false;
+ }
+
+ $placeholders = $replacements = [];
+ foreach ($messageData['parameters'] as $placeholder => $parameter) {
+ $placeholders[] = '{' . $placeholder . '}';
+ if ($parameter['type'] === 'user') {
+ if (str_contains($parameter['id'], ' ') || str_contains($parameter['id'], '/')) {
+ $replacements[] = '@"' . $parameter['id'] . '"';
+ } else {
+ $replacements[] = '@' . $parameter['id'];
+ }
+ } elseif ($parameter['type'] === 'call') {
+ $replacements[] = '@all';
+ } elseif ($parameter['type'] === 'guest') {
+ $replacements[] = '@' . $parameter['name'];
+ } else {
+ $replacements[] = $parameter['name'];
+ }
+ }
+
+ $parsedMessage = str_replace($placeholders, $replacements, $message);
+ $agendas = preg_split(self::AGENDA_PATTERN, $parsedMessage, flags: PREG_SPLIT_NO_EMPTY);
+ foreach ($agendas as $agenda) {
+ $agendaText = trim($agenda);
+ if ($agendaText) {
+ // Only store when not empty
+ $this->saveTask($server, $data['target']['id'], $agendaText, LogEntry::TYPE_AGENDA);
+ }
+ }
+
+ // React with thumbs up as we detected a task
+ return true;
+ }
+
+ protected function saveTask(string $server, string $token, string $text, string $type): void {
+ $logEntry = new LogEntry();
+ $logEntry->setServer($server);
+ $logEntry->setToken($token);
+ $logEntry->setType($type);
+ $logEntry->setDetails($text);
+ $this->logEntryMapper->insert($logEntry);
+ }
+
+ /**
+ * @param string $server
+ * @param string $token
+ * @param string $roomName
+ * @param string $lang
+ * @return array{summary: string, elevator: ?int}|null
+ */
+ public function summarize(string $server, string $token, string $roomName, string $lang = 'en'): ?array {
+ $logEntries = $this->logEntryMapper->findByConversation($server, $token);
+ $this->logEntryMapper->deleteByConversation($server, $token);
+
+ $libL10N = $this->l10nFactory->get('lib', $lang);
+ $l = $this->l10nFactory->get('talk_webhook_demo', $lang);
+
+ $endDateTime = $this->timeFactory->now();
+ $endTimestamp = $endDateTime->getTimestamp();
+ $startTimestamp = $endTimestamp;
+
+ $attendees = $todos = $solved = $notes = $decisions = $reports = [];
+ $elevator = null;
+
+ foreach ($logEntries as $logEntry) {
+ if ($logEntry->getType() === LogEntry::TYPE_START) {
+ $time = (int)$logEntry->getDetails();
+ if ($startTimestamp > $time) {
+ $startTimestamp = $time;
+ }
+ } elseif ($logEntry->getType() === LogEntry::TYPE_ATTENDEE) {
+ $attendees[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_TODO) {
+ $todos[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_SOLVED) {
+ $solved[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_NOTE) {
+ $notes[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_DECISION) {
+ $decisions[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_REPORT) {
+ $reports[] = $logEntry->getDetails();
+ } elseif ($logEntry->getType() === LogEntry::TYPE_ELEVATOR) {
+ $elevator = (int)$logEntry->getDetails();
+ }
+ }
+
+ if (($endTimestamp - $startTimestamp) < (int)$this->config->getAppValue('talk_webhook_demo', 'min-length', '60')) {
+ // No call summary for short calls
+ return null;
+ }
+
+ $attendees = array_unique($attendees);
+ sort($attendees);
+
+ $systemDefault = $this->config->getSystemValueString('default_timezone', 'UTC');
+ $timezoneString = $this->config->getAppValue('talk_webhook_demo', 'timezone', $systemDefault);
+ $timezone = null;
+ if ($timezoneString !== 'UTC') {
+ try {
+ $timezone = new \DateTimeZone($timezoneString);
+ } catch (\Throwable) {
+ }
+ }
+
+ $startDate = $this->dateTimeFormatter->formatDate($startTimestamp, 'full', $timezone, $libL10N);
+ $startTime = $this->dateTimeFormatter->formatTime($startTimestamp, 'short', $timezone, $libL10N);
+ $endTime = $this->dateTimeFormatter->formatTime($endTimestamp, 'short', $timezone, $libL10N);
+
+ $summary = '# ' . $this->getTitle($l, $roomName) . "\n\n";
+ $summary .= $startDate . 'โยทโ' . $startTime . ' โ ' . $endTime;
+ if ($timezone !== null) {
+ $summary .= ' (' . $timezone->getName() . ")\n";
+ } else {
+ $summary .= ' (' . $endDateTime->getTimezone()->getName() . ")\n";
+ }
+
+ $summary .= "\n";
+ $summary .= '## ' . $l->t('Attendees') . "\n";
+ foreach ($attendees as $attendee) {
+ $summary .= '- ' . $attendee . "\n";
+ }
+
+ if (!empty($todos) || !empty($solved)) {
+ $summary .= "\n";
+ $summary .= '## ' . $l->t('Tasks') . "\n";
+ foreach ($solved as $todo) {
+ $summary .= '- [x] ' . $todo . "\n";
+ }
+ foreach ($todos as $todo) {
+ $summary .= '- [ ] ' . $todo . "\n";
+ }
+ }
+
+ if (!empty($notes)) {
+ $summary .= "\n";
+ $summary .= '## ' . $l->t('Notes') . "\n";
+ foreach ($notes as $note) {
+ $summary .= '- ' . $note . "\n";
+ }
+ }
+
+ if (!empty($reports)) {
+ $summary .= "\n";
+ $summary .= '## ' . $l->t('Reports') . "\n";
+ foreach ($reports as $report) {
+ $summary .= '- ' . $report . "\n";
+ }
+ }
+
+ if (!empty($decisions)) {
+ $summary .= "\n";
+ $summary .= '## ' . $l->t('Decisions') . "\n";
+ foreach ($decisions as $decision) {
+ $summary .= '- ' . $decision . "\n";
+ }
+ }
+
+ return ['summary' => $summary, 'elevator' => $elevator];
+ }
+
+ /**
+ * @param string $server
+ * @param string $token
+ * @param string $lang
+ * @return ?string
+ */
+ public function agenda(string $server, string $token, string $lang = 'en'): ?string {
+ $logEntries = $this->logEntryMapper->findByConversation($server, $token);
+ $this->logEntryMapper->deleteByConversation($server, $token);
+
+
+ $agenda = [];
+ foreach ($logEntries as $logEntry) {
+ if ($logEntry->getType() === LogEntry::TYPE_AGENDA) {
+ $agenda[] = $logEntry->getDetails();
+ }
+ }
+
+ if (empty($agenda)) {
+ return null;
+ }
+ $agenda = array_unique($agenda);
+
+ $l = $this->l10nFactory->get('talk_webhook_demo', $lang);
+ $summary = '# ' . $l->t('Agenda') . "\n\n";
+ foreach ($agenda as $item) {
+ $summary .= '- [ ] ' . $item . "\n";
+ }
+
+ return $summary;
+ }
+
+ protected function getTitle(IL10N $l, string $roomName): string {
+ try {
+ $data = json_decode($roomName, true, flags: JSON_THROW_ON_ERROR);
+ if (is_array($data) && count($data) === 2 && isset($data[0]) && is_string($data[0]) && isset($data[1]) && is_string($data[1])) {
+ // Seems like the room name is a JSON map with the 2 user IDs of a 1-1 conversation,
+ // so we don't add it to the title to avoid things like:
+ // `Call summary - ["2991c735-4f9e-46e2-a107-7569dd19fdf8","42e6a9c2-a833-43f6-ab47-6b7004094912"]`
+ return $l->t('Call summary');
+ }
+ } catch (\JsonException) {
+ }
+
+ return str_replace('{title}', $roomName, $l->t('Call summary - {title}'));
+ }
+}