diff --git a/.drone.jsonnet b/.drone.jsonnet
index d2f1103898c..d6b2bfbd958 100644
--- a/.drone.jsonnet
+++ b/.drone.jsonnet
@@ -1,8 +1,16 @@
## 1. Download/install drone binary:
-## curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
+## curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
## 2. Adjust the matrix as wished
-## 3. Run: ./drone jsonnet --stream --format yml
-## 4. Commit the result
+## 3. Transform jsonnet to yml:
+## ./drone jsonnet --stream --format yml
+## 4. Export your drone token and the server:
+## export DRONE_TOKEN=… export DRONE_SERVER=https://drone.nextcloud.com
+## 5. Sign off the changes:
+## ./drone sign nextcloud/spreed --save
+## 6. Copy the new signature from .drone.yml to `hmac` field in this file
+## 7. Transform jsonnet to yml again (to transfer the signature correctly):
+## ./drone jsonnet --stream --format yml
+## 8. Commit the result
local Pipeline(test_set, database, services) = {
kind: "pipeline",
@@ -16,6 +24,7 @@ local Pipeline(test_set, database, services) = {
APP_NAME: "spreed",
CORE_BRANCH: "master",
GUESTS_BRANCH: "master",
+ CSB_BRANCH: "main",
NOTIFICATIONS_BRANCH: "master",
DATABASEHOST: database
},
@@ -31,12 +40,11 @@ local Pipeline(test_set, database, services) = {
"cd ../..",
"./occ app:enable $APP_NAME",
"git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications apps/notifications",
- "./occ app:enable notifications"
- ] + (
- if test_set == "conversation" || test_set == "conversation-2" then [
- "git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests"
- ] else []
- ) + [
+ "./occ app:enable --force notifications",
+ "git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests",
+ "./occ app:enable --force guests",
+ "git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot apps/call_summary_bot",
+ "./occ app:enable --force call_summary_bot",
"cd apps/$APP_NAME/tests/integration/",
"bash run.sh features/"+test_set
]
@@ -155,6 +163,6 @@ local PipelinePostgreSQL(test_set) = Pipeline(
{
kind: "signature",
- hmac: "7d4f30bec296493e6f94fa268c8fb67ab927883875beda00b1d0f4c867bd825c"
+ hmac: "1f1f7e889531624fec63e4fa8b65abaa737acac57dd7aa01df34f40027fdef12"
},
]
diff --git a/.drone.yml b/.drone.yml
index c8a1c6d9457..84968aa7656 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -18,12 +18,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/callapi
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -55,12 +61,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -92,12 +104,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -129,12 +147,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/command
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -166,13 +190,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -204,13 +233,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -242,12 +276,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/federation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -279,12 +319,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/integration
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -316,12 +362,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -353,12 +405,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: sqlite
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -404,12 +462,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/callapi
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -455,12 +519,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -506,12 +576,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -557,12 +633,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/command
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -608,13 +690,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -660,13 +747,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -712,12 +804,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/federation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -763,12 +861,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/integration
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -814,12 +918,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -865,12 +975,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: mysql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -911,12 +1027,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/callapi
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -958,12 +1080,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1005,12 +1133,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/chat-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1052,12 +1186,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/command
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1099,13 +1239,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1147,13 +1292,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
- git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/conversation-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1195,12 +1345,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/federation
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1242,12 +1398,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/integration
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1289,12 +1451,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1336,12 +1504,18 @@ steps:
- ./occ app:enable $APP_NAME
- git clone --depth 1 -b $NOTIFICATIONS_BRANCH https://github.com/nextcloud/notifications
apps/notifications
- - ./occ app:enable notifications
+ - ./occ app:enable --force notifications
+ - git clone --depth 1 -b $GUESTS_BRANCH https://github.com/nextcloud/guests apps/guests
+ - ./occ app:enable --force guests
+ - git clone --depth 1 -b $CSB_BRANCH https://github.com/nextcloud/call_summary_bot
+ apps/call_summary_bot
+ - ./occ app:enable --force call_summary_bot
- cd apps/$APP_NAME/tests/integration/
- bash run.sh features/sharing-2
environment:
APP_NAME: spreed
CORE_BRANCH: master
+ CSB_BRANCH: main
DATABASEHOST: pgsql
GUESTS_BRANCH: master
NOTIFICATIONS_BRANCH: master
@@ -1355,5 +1529,5 @@ trigger:
- pull_request
- push
---
-hmac: 7d4f30bec296493e6f94fa268c8fb67ab927883875beda00b1d0f4c867bd825c
+hmac: 1f1f7e889531624fec63e4fa8b65abaa737acac57dd7aa01df34f40027fdef12
kind: signature
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 3230b8574b6..bea4033df47 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
]]>
- 18.0.0-dev.1
+ 18.0.0-dev.2
agpl
Daniel Calviño Sánchez
@@ -81,6 +81,12 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
+ OCA\Talk\Command\Bot\Install
+ OCA\Talk\Command\Bot\ListBots
+ OCA\Talk\Command\Bot\Remove
+ OCA\Talk\Command\Bot\State
+ OCA\Talk\Command\Bot\Setup
+ OCA\Talk\Command\Bot\Uninstall
OCA\Talk\Command\Command\Add
OCA\Talk\Command\Command\AddSamples
OCA\Talk\Command\Command\Delete
diff --git a/appinfo/routes.php b/appinfo/routes.php
index e75d8b2c87e..be859e962ea 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -26,6 +26,7 @@
return array_merge_recursive(
include(__DIR__ . '/routes/routesAvatarController.php'),
+ include(__DIR__ . '/routes/routesBotController.php'),
include(__DIR__ . '/routes/routesBreakoutRoomController.php'),
include(__DIR__ . '/routes/routesCallController.php'),
include(__DIR__ . '/routes/routesCertificateController.php'),
diff --git a/appinfo/routes/routesBotController.php b/appinfo/routes/routesBotController.php
new file mode 100644
index 00000000000..f61d6aa1f10
--- /dev/null
+++ b/appinfo/routes/routesBotController.php
@@ -0,0 +1,48 @@
+
+ *
+ * @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 .
+ *
+ */
+
+$requirements = [
+ 'apiVersion' => 'v1',
+ 'token' => '[a-z0-9]{4,30}',
+];
+
+$requirementsWithBotId = [
+ 'apiVersion' => 'v1',
+ 'token' => '[a-z0-9]{4,30}',
+ 'botId' => '[0-9]+',
+];
+
+return [
+ 'ocs' => [
+ /** @see \OCA\Talk\Controller\BotController::sendMessage() */
+ ['name' => 'Bot#sendMessage', 'url' => '/api/{apiVersion}/bot/{token}/message', 'verb' => 'POST', '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() */
+ ['name' => 'Bot#enableBot', 'url' => '/api/{apiVersion}/bot/{token}/{botId}', 'verb' => 'POST', 'requirements' => $requirementsWithBotId],
+ /** @see \OCA\Talk\Controller\BotController::disableBot() */
+ ['name' => 'Bot#disableBot', 'url' => '/api/{apiVersion}/bot/{token}/{botId}', 'verb' => 'DELETE', 'requirements' => $requirementsWithBotId],
+ ],
+];
diff --git a/docs/occ.md b/docs/occ.md
index 99a330bd242..9b7a442bc72 100644
--- a/docs/occ.md
+++ b/docs/occ.md
@@ -1,5 +1,108 @@
# Talk occ commands
+## talk:bot:install
+
+Install a new bot on the server
+
+### Usage
+
+* `talk:bot:install [--output [OUTPUT]] [--no-setup] [--] []`
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `name` | The name under which the messages will be posted | yes | no | `NULL` |
+| `secret` | Secret used to validate API calls | yes | no | `NULL` |
+| `url` | Webhook endpoint to post messages to | yes | no | `NULL` |
+| `description` | Optional description shown in the admin settings | no | no | `NULL` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+| `--no-setup` | Prevent moderators from setting up the bot in a conversation | no | no | no | false` |
+
+## talk:bot:list
+
+List all installed bots of the server or a conversation
+
+### Usage
+
+* `talk:bot:list [--output [OUTPUT]] [--] []`
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `token` | Conversation token to limit the bot list for | no | no | `NULL` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+
+## talk:bot:remove
+
+Remove a bot from a conversation
+
+### Usage
+
+* `talk:bot:remove [--output [OUTPUT]] [--] [...]`
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `bot-id` | The ID of the bot to remove in a conversation | yes | no | `NULL` |
+| `token` | Conversation tokens to remove bot up for | no | yes | `array ()` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+
+## talk:bot:state
+
+List all installed bots of the server or a conversation
+
+### Usage
+
+* `talk:bot:state [--output [OUTPUT]] [--] `
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `bot-id` | Bot ID to change the state for | yes | no | `NULL` |
+| `state` | New state for the bot (0 = disabled, 1 = enabled, 2 = no setup via GUI) | yes | no | `NULL` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+
+## talk:bot:setup
+
+Add a bot to a conversation
+
+### Usage
+
+* `talk:bot:setup [--output [OUTPUT]] [--] [...]`
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `bot-id` | The ID of the bot to set up in a conversation | yes | no | `NULL` |
+| `token` | Conversation tokens to set the bot up for | no | yes | `array ()` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+
+## talk:bot:uninstall
+
+Uninstall a bot from the server
+
+### Usage
+
+* `talk:bot:uninstall [--output [OUTPUT]] [--] `
+
+| Arguments | Description | Is required | Is array | Default |
+|---|---|---|---|---|
+| `id` | The ID of the bot | yes | no | `NULL` |
+
+| Options | Accept value | Is value required | Is multiple | Default |
+|---|---|---|---|---|
+| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | 'plain'` |
+
## talk:command:add
Add a new command
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 7497254e258..d462604d0a2 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -45,6 +45,7 @@
use OCA\Talk\Deck\DeckPluginLoader;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Events\AttendeesRemovedEvent;
+use OCA\Talk\Events\BotInstallEvent;
use OCA\Talk\Events\RoomEvent;
use OCA\Talk\Events\SendCallNotificationEvent;
use OCA\Talk\Federation\CloudFederationProviderTalk;
@@ -52,6 +53,7 @@
use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader;
use OCA\Talk\Flow\RegisterOperationsListener;
use OCA\Talk\Listener\BeforeUserLoggedOutListener;
+use OCA\Talk\Listener\BotListener;
use OCA\Talk\Listener\CircleDeletedListener;
use OCA\Talk\Listener\CircleMembershipListener;
use OCA\Talk\Listener\CSPListener;
@@ -124,6 +126,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);
$context->registerEventListener(AddFeaturePolicyEvent::class, FeaturePolicyListener::class);
+ $context->registerEventListener(BotInstallEvent::class, BotListener::class);
$context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class);
$context->registerEventListener(GroupChangedEvent::class, DisplayNameListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
@@ -186,6 +189,7 @@ public function boot(IBootContext $context): void {
CollaboratorsListener::register($dispatcher);
ResourceListener::register($dispatcher);
ReferenceInvalidationListener::register($dispatcher);
+ BotListener::register($dispatcher);
// Register only when Talk Updates are not disabled
if ($server->getConfig()->getAppValue('spreed', 'changelog', 'yes') === 'yes') {
ChangelogListener::register($dispatcher);
diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php
index feb8bff8004..acbe29ac25d 100644
--- a/lib/Chat/ChatManager.php
+++ b/lib/Chat/ChatManager.php
@@ -236,7 +236,7 @@ public function addChangelogMessage(Room $chat, string $message): IComment {
* Sends a new message to the given chat.
*
* @param Room $chat
- * @param Participant $participant
+ * @param ?Participant $participant
* @param string $actorType
* @param string $actorId
* @param string $message
@@ -245,7 +245,7 @@ public function addChangelogMessage(Room $chat, string $message): IComment {
* @param string $referenceId
* @return IComment
*/
- public function sendMessage(Room $chat, Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, ?IComment $replyTo, string $referenceId, bool $silent): IComment {
+ public function sendMessage(Room $chat, ?Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, ?IComment $replyTo, string $referenceId, bool $silent): IComment {
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId());
$comment->setMessage($message, self::MAX_CHAT_LENGTH);
$comment->setCreationDateTime($creationDateTime);
@@ -263,17 +263,23 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT
}
$this->setMessageExpiration($chat, $comment);
- $event = new ChatParticipantEvent($chat, $comment, $participant, $silent);
+ if ($participant instanceof Participant) {
+ $event = new ChatParticipantEvent($chat, $comment, $participant, $silent);
+ } else {
+ $event = new ChatEvent($chat, $comment, false, $silent);
+ }
$this->dispatcher->dispatch(self::EVENT_BEFORE_MESSAGE_SEND, $event);
$shouldFlush = $this->notificationManager->defer();
try {
$this->commentsManager->save($comment);
- $this->participantService->updateLastReadMessage($participant, (int) $comment->getId());
+ if ($participant instanceof Participant) {
+ $this->participantService->updateLastReadMessage($participant, (int) $comment->getId());
+ }
// Update last_message
- if ($comment->getActorType() !== 'bots' || $comment->getActorId() === 'changelog') {
+ if ($comment->getActorType() !== Attendee::ACTOR_BOTS || $comment->getActorId() === 'changelog' || str_starts_with($comment->getActorId(), Attendee::ACTOR_BOT_PREFIX)) {
$this->roomService->setLastMessage($chat, $comment);
$this->unreadCountCache->clear($chat->getId() . '-');
} else {
diff --git a/lib/Chat/Command/Listener.php b/lib/Chat/Command/Listener.php
index 90b49fb93e4..d9a137c248d 100644
--- a/lib/Chat/Command/Listener.php
+++ b/lib/Chat/Command/Listener.php
@@ -24,6 +24,7 @@
namespace OCA\Talk\Chat\Command;
use OCA\Talk\Chat\ChatManager;
+use OCA\Talk\Events\ChatEvent;
use OCA\Talk\Events\ChatParticipantEvent;
use OCA\Talk\Model\Command;
use OCA\Talk\Service\CommandService;
@@ -43,7 +44,12 @@ public static function register(IEventDispatcher $dispatcher): void {
$dispatcher->addListener(ChatManager::EVENT_BEFORE_MESSAGE_SEND, [self::class, 'executeCommand']);
}
- public static function executeCommand(ChatParticipantEvent $event): void {
+ public static function executeCommand(ChatEvent $event): void {
+ if (!$event instanceof ChatParticipantEvent) {
+ // No commands for bots 🚓
+ return;
+ }
+
$message = $event->getComment();
$participant = $event->getParticipant();
diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php
index 08d65c679e8..521eb443154 100644
--- a/lib/Chat/MessageParser.php
+++ b/lib/Chat/MessageParser.php
@@ -31,6 +31,7 @@
use OCA\Talk\Model\Message;
use OCA\Talk\Participant;
use OCA\Talk\Room;
+use OCA\Talk\Service\BotService;
use OCA\Talk\Service\ParticipantService;
use OCP\Comments\IComment;
use OCP\EventDispatcher\IEventDispatcher;
@@ -44,15 +45,18 @@ class MessageParser {
public const EVENT_MESSAGE_PARSE = self::class . '::parseMessage';
protected array $guestNames = [];
+ protected array $bots = [];
+ protected array $botNames = [];
public function __construct(
- protected IEventDispatcher $dispatcher,
- protected IUserManager $userManager,
+ protected IEventDispatcher $dispatcher,
+ protected IUserManager $userManager,
protected ParticipantService $participantService,
+ protected BotService $botService,
) {
}
- public function createMessage(Room $room, Participant $participant, IComment $comment, IL10N $l): Message {
+ public function createMessage(Room $room, ?Participant $participant, IComment $comment, IL10N $l): Message {
return new Message($room, $participant, $comment, $l);
}
@@ -91,8 +95,17 @@ protected function setActor(Message $message): void {
}
$this->guestNames[$comment->getActorId()] = $displayName;
}
- } elseif ($comment->getActorType() === 'bots') {
+ } elseif ($comment->getActorType() === Attendee::ACTOR_BOTS) {
+ $actorId = $comment->getActorId();
$displayName = $comment->getActorId() . '-bot';
+ $token = $message->getRoom()->getToken();
+ if (str_starts_with($actorId, Attendee::ACTOR_BOT_PREFIX)) {
+ $urlHash = substr($actorId, strlen(Attendee::ACTOR_BOT_PREFIX));
+ $botName = $this->getBotNameByUrlHashForConversation($token, $urlHash);
+ if ($botName) {
+ $displayName = $botName . ' (Bot)';
+ }
+ }
}
$message->setActor(
@@ -101,4 +114,17 @@ protected function setActor(Message $message): void {
$displayName
);
}
+
+ protected function getBotNameByUrlHashForConversation(string $token, string $urlHash): ?string {
+ if (!isset($this->botNames[$token])) {
+ $this->botNames[$token] = [];
+ $bots = $this->botService->getBotsForToken($token);
+ foreach ($bots as $bot) {
+ $botServer = $bot->getBotServer();
+ $this->botNames[$token][$botServer->getUrlHash()] = $botServer->getName();
+ }
+ }
+
+ return $this->botNames[$token][$urlHash] ?? null;
+ }
}
diff --git a/lib/Chat/Parser/Command.php b/lib/Chat/Parser/Command.php
index 6a69f2c8223..e0fa1b2f753 100644
--- a/lib/Chat/Parser/Command.php
+++ b/lib/Chat/Parser/Command.php
@@ -45,6 +45,7 @@ public function parseMessage(Message $message): void {
$participant = $message->getParticipant();
if ($data['visibility'] !== \OCA\Talk\Model\Command::RESPONSE_ALL &&
+ $participant !== null &&
($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS
|| $data['user'] !== $participant->getAttendee()->getActorId())) {
$message->setVisibility(false);
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index 5fcc628f602..f85e45d8081 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -98,7 +98,10 @@ public function parseMessage(Message $chatMessage): void {
$parsedParameters = ['actor' => $this->getActorFromComment($room, $comment)];
$participant = $chatMessage->getParticipant();
- if (!$participant->isGuest()) {
+ if ($participant === null) {
+ $currentActorId = null;
+ $currentUserIsActor = false;
+ } elseif (!$participant->isGuest()) {
$currentActorId = $participant->getAttendee()->getActorId();
$currentUserIsActor = $parsedParameters['actor']['type'] === 'user' &&
$participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS &&
@@ -563,19 +566,20 @@ public function parseDeletedMessage(Message $chatMessage): void {
$parsedParameters = ['actor' => $this->getActor($room, $data['deleted_by_type'], $data['deleted_by_id'])];
$participant = $chatMessage->getParticipant();
- $currentActorId = $participant->getAttendee()->getActorId();
$authorIsActor = $data['deleted_by_type'] === $chatMessage->getComment()->getActorType()
&& $data['deleted_by_id'] === $chatMessage->getComment()->getActorId();
- if (!$participant->isGuest()) {
+ if ($participant === null) {
+ $currentUserIsActor = false;
+ } elseif (!$participant->isGuest()) {
$currentUserIsActor = $parsedParameters['actor']['type'] === 'user' &&
$participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS &&
- $currentActorId === $parsedParameters['actor']['id'];
+ $participant->getAttendee()->getActorId() === $parsedParameters['actor']['id'];
} else {
$currentUserIsActor = $parsedParameters['actor']['type'] === 'guest' &&
$participant->getAttendee()->getActorType() === 'guest' &&
- $currentActorId === $parsedParameters['actor']['id'];
+ $participant->getAttendee()->getActorId() === $parsedParameters['actor']['id'];
}
if ($chatMessage->getMessageType() === ChatManager::VERB_MESSAGE_DELETED) {
diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php
index 2ab5689137d..2db590f3db1 100644
--- a/lib/Chat/Parser/UserMention.php
+++ b/lib/Chat/Parser/UserMention.php
@@ -116,7 +116,7 @@ public function parseMessage(Message $chatMessage): void {
if ($mention['type'] === 'call') {
$userId = '';
- if ($chatMessage->getParticipant()->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
+ if ($chatMessage->getParticipant()?->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
$userId = $chatMessage->getParticipant()->getAttendee()->getActorId();
}
diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php
index 0327aa9d0cd..785262667c5 100644
--- a/lib/Chat/SystemMessage/Listener.php
+++ b/lib/Chat/SystemMessage/Listener.php
@@ -428,12 +428,12 @@ protected function sendSystemMessage(Room $room, string $message, array $paramet
} elseif (\OC::$CLI || $this->session->exists('talk-overwrite-actor-cli')) {
$actorType = Attendee::ACTOR_GUESTS;
$actorId = 'cli';
- } elseif ($this->session->exists('talk-overwrite-actor')) {
- $actorType = Attendee::ACTOR_USERS;
- $actorId = $this->session->get('talk-overwrite-actor');
} elseif ($this->session->exists('talk-overwrite-actor-type')) {
$actorType = $this->session->get('talk-overwrite-actor-type');
$actorId = $this->session->get('talk-overwrite-actor-id');
+ } elseif ($this->session->exists('talk-overwrite-actor-id')) {
+ $actorType = Attendee::ACTOR_USERS;
+ $actorId = $this->session->get('talk-overwrite-actor-id');
} else {
$actorType = Attendee::ACTOR_GUESTS;
$sessionId = $this->talkSession->getSessionForRoom($room->getToken());
diff --git a/lib/Command/Bot/Install.php b/lib/Command/Bot/Install.php
new file mode 100644
index 00000000000..1f0ba5160f7
--- /dev/null
+++ b/lib/Command/Bot/Install.php
@@ -0,0 +1,103 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotServer;
+use OCA\Talk\Model\BotServerMapper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Install extends Base {
+ public function __construct(
+ private BotServerMapper $botServerMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:install')
+ ->setDescription('Install a new bot on the server')
+ ->addArgument(
+ 'name',
+ InputArgument::REQUIRED,
+ 'The name under which the messages will be posted'
+ )
+ ->addArgument(
+ 'secret',
+ InputArgument::REQUIRED,
+ 'Secret used to validate API calls'
+ )
+ ->addArgument(
+ 'url',
+ InputArgument::REQUIRED,
+ 'Webhook endpoint to post messages to'
+ )
+ ->addArgument(
+ 'description',
+ InputArgument::OPTIONAL,
+ 'Optional description shown in the admin settings'
+ )
+ ->addOption(
+ 'no-setup',
+ null,
+ InputOption::VALUE_NONE,
+ 'Prevent moderators from setting up the bot in a conversation'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $name = $input->getArgument('name');
+ $secret = $input->getArgument('secret');
+ $url = $input->getArgument('url');
+ $description = $input->getArgument('description');
+ $noSetup = $input->getOption('no-setup');
+
+ $bot = new BotServer();
+ $bot->setName($name);
+ $bot->setSecret($secret);
+ $bot->setUrl($url);
+ $bot->setUrlHash(sha1($url));
+ $bot->setDescription($description);
+ $bot->setState($noSetup ? Bot::STATE_NO_SETUP : Bot::STATE_ENABLED);
+ try {
+ $this->botServerMapper->insert($bot);
+ } catch (\Exception $e) {
+ $output->writeln('' . get_class($e) . ': ' . $e->getMessage() . '');
+ return 1;
+ }
+
+
+ $output->writeln('Bot installed');
+ return 0;
+ }
+}
diff --git a/lib/Command/Bot/ListBots.php b/lib/Command/Bot/ListBots.php
new file mode 100644
index 00000000000..4f5251c93bb
--- /dev/null
+++ b/lib/Command/Bot/ListBots.php
@@ -0,0 +1,89 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Model\BotConversation;
+use OCA\Talk\Model\BotConversationMapper;
+use OCA\Talk\Model\BotServerMapper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListBots extends Base {
+ public function __construct(
+ private BotConversationMapper $botConversationMapper,
+ private BotServerMapper $botServerMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:list')
+ ->setDescription('List all installed bots of the server or a conversation')
+ ->addArgument(
+ 'token',
+ InputArgument::OPTIONAL,
+ 'Conversation token to limit the bot list for'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $bots = $this->botServerMapper->getAllBots();
+ $token = $input->getArgument('token');
+
+ if ($token) {
+ $botIds = array_map(static function (BotConversation $bot): int {
+ return $bot->getBotId();
+ }, $this->botConversationMapper->findForToken($token));
+ }
+
+ $data = [];
+ foreach ($bots as $bot) {
+ if ($token && !in_array($bot->getId(), $botIds, true)) {
+ continue;
+ }
+
+ $botData = $bot->jsonSerialize();
+
+ if (!$output->isVerbose()) {
+ unset($botData['url']);
+ unset($botData['url_hash']);
+ unset($botData['secret']);
+ unset($botData['last_error_date']);
+ unset($botData['last_error_message']);
+ }
+
+ $data[] = $botData;
+ }
+
+ $this->writeTableInOutputFormat($input, $output, $data);
+ return 0;
+ }
+}
diff --git a/lib/Command/Bot/Remove.php b/lib/Command/Bot/Remove.php
new file mode 100644
index 00000000000..fa6378ea9d9
--- /dev/null
+++ b/lib/Command/Bot/Remove.php
@@ -0,0 +1,68 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Model\BotConversationMapper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Remove extends Base {
+ public function __construct(
+ private BotConversationMapper $botConversationMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:remove')
+ ->setDescription('Remove a bot from a conversation')
+ ->addArgument(
+ 'bot-id',
+ InputArgument::REQUIRED,
+ 'The ID of the bot to remove in a conversation'
+ )
+ ->addArgument(
+ 'token',
+ InputArgument::IS_ARRAY,
+ 'Conversation tokens to remove bot up for'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $botId = (int) $input->getArgument('bot-id');
+ $tokens = $input->getArgument('token');
+
+ $this->botConversationMapper->deleteByBotIdAndTokens($botId, $tokens);
+
+ $output->writeln('Remove bot from given conversations');
+ return 0;
+ }
+}
diff --git a/lib/Command/Bot/Setup.php b/lib/Command/Bot/Setup.php
new file mode 100644
index 00000000000..00562e96553
--- /dev/null
+++ b/lib/Command/Bot/Setup.php
@@ -0,0 +1,103 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Exceptions\RoomNotFoundException;
+use OCA\Talk\Manager;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotConversation;
+use OCA\Talk\Model\BotConversationMapper;
+use OCA\Talk\Model\BotServerMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Setup extends Base {
+ public function __construct(
+ private Manager $roomManager,
+ private BotServerMapper $botServerMapper,
+ private BotConversationMapper $botConversationMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:setup')
+ ->setDescription('Add a bot to a conversation')
+ ->addArgument(
+ 'bot-id',
+ InputArgument::REQUIRED,
+ 'The ID of the bot to set up in a conversation'
+ )
+ ->addArgument(
+ 'token',
+ InputArgument::IS_ARRAY,
+ 'Conversation tokens to set the bot up for'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $botId = (int) $input->getArgument('bot-id');
+ $tokens = $input->getArgument('token');
+
+ try {
+ $this->botServerMapper->findById($botId);
+ } catch (DoesNotExistException) {
+ $output->writeln('Bot could not be found by id: ' . $botId . '');
+ return 1;
+ }
+
+ $returnCode = 0;
+ foreach ($tokens as $token) {
+ try {
+ $this->roomManager->getRoomByToken($token);
+ } catch (RoomNotFoundException) {
+ $output->writeln('Conversation could not be found by token: ' . $token . '');
+ return 1;
+ }
+
+ $bot = new BotConversation();
+ $bot->setBotId($botId);
+ $bot->setToken($token);
+ $bot->setState(Bot::STATE_ENABLED);
+
+ try {
+ $this->botConversationMapper->insert($bot);
+ $output->writeln('Successfully set up for conversation ' . $token . '');
+ } catch (\Exception $e) {
+ $output->writeln('' . get_class($e) . ': ' . $e->getMessage() . '');
+ $returnCode = 3;
+ }
+ }
+
+ return $returnCode;
+ }
+}
diff --git a/lib/Command/Bot/State.php b/lib/Command/Bot/State.php
new file mode 100644
index 00000000000..9c059a3ca86
--- /dev/null
+++ b/lib/Command/Bot/State.php
@@ -0,0 +1,83 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotServerMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class State extends Base {
+ public function __construct(
+ private BotServerMapper $botServerMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:state')
+ ->setDescription('List all installed bots of the server or a conversation')
+ ->addArgument(
+ 'bot-id',
+ InputArgument::REQUIRED,
+ 'Bot ID to change the state for'
+ )
+ ->addArgument(
+ 'state',
+ InputArgument::REQUIRED,
+ 'New state for the bot (0 = disabled, 1 = enabled, 2 = no setup via GUI)'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $botId = (int) $input->getArgument('bot-id');
+ $state = (int) $input->getArgument('state');
+
+ if (!in_array($state, [Bot::STATE_DISABLED, Bot::STATE_ENABLED, Bot::STATE_NO_SETUP], true)) {
+ $output->writeln('Provided state is invalid');
+ return 1;
+ }
+
+ try {
+ $bot = $this->botServerMapper->findById($botId);
+ } catch (DoesNotExistException) {
+ $output->writeln('Bot could not be found by id: ' . $botId . '');
+ return 1;
+ }
+
+ $bot->setState($state);
+ $this->botServerMapper->update($bot);
+
+ $output->writeln('Bot state set to ' . $state . '');
+ return 0;
+ }
+}
diff --git a/lib/Command/Bot/Uninstall.php b/lib/Command/Bot/Uninstall.php
new file mode 100644
index 00000000000..157559e7b57
--- /dev/null
+++ b/lib/Command/Bot/Uninstall.php
@@ -0,0 +1,65 @@
+
+ *
+ * @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\Command\Bot;
+
+use OC\Core\Command\Base;
+use OCA\Talk\Model\BotConversationMapper;
+use OCA\Talk\Model\BotServerMapper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Uninstall extends Base {
+ public function __construct(
+ private BotConversationMapper $botConversationMapper,
+ private BotServerMapper $botServerMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('talk:bot:uninstall')
+ ->setDescription('Uninstall a bot from the server')
+ ->addArgument(
+ 'id',
+ InputArgument::REQUIRED,
+ 'The ID of the bot'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $botId = (int) $input->getArgument('id');
+
+ $this->botConversationMapper->deleteByBotId($botId);
+ $this->botServerMapper->deleteById($botId);
+
+ $output->writeln('Bot uninstalled');
+ return 0;
+ }
+}
diff --git a/lib/Controller/BotController.php b/lib/Controller/BotController.php
new file mode 100644
index 00000000000..6492bd95e8a
--- /dev/null
+++ b/lib/Controller/BotController.php
@@ -0,0 +1,249 @@
+
+ *
+ * @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 OCA\Talk\Chat\ChatManager;
+use OCA\Talk\Exceptions\UnauthorizedException;
+use OCA\Talk\Manager;
+use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant;
+use OCA\Talk\Model\Attendee;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotConversation;
+use OCA\Talk\Model\BotConversationMapper;
+use OCA\Talk\Model\BotServer;
+use OCA\Talk\Model\BotServerMapper;
+use OCA\Talk\Service\BotService;
+use OCA\Talk\Service\ChecksumVerificationService;
+use OCA\Talk\Service\ParticipantService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Comments\MessageTooLongException;
+use OCP\Comments\NotFoundException;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+class BotController extends AEnvironmentAwareController {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ protected ChatManager $chatManager,
+ protected ParticipantService $participantService,
+ protected ITimeFactory $timeFactory,
+ protected ChecksumVerificationService $checksumVerificationService,
+ protected BotConversationMapper $botConversationMapper,
+ protected BotServerMapper $botServerMapper,
+ protected BotService $botService,
+ protected Manager $manager,
+ protected LoggerInterface $logger,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Sends a new chat message to the given room.
+ *
+ * The author and timestamp are automatically set to the current user/guest
+ * and time.
+ *
+ * @param string $token conversation token
+ * @param string $message the message to send
+ * @param string $referenceId for the message to be able to later identify it again
+ * @param int $replyTo Parent id which this message is a reply to
+ * @param bool $silent If sent silent the chat message will not create any notifications
+ * @return DataResponse the status code is "201 Created" if successful, and
+ * "404 Not found" if the room or session for a guest user was not
+ * found".
+ */
+ #[BruteForceProtection(action: 'bot')]
+ #[PublicPage]
+ public function sendMessage(string $token, string $message, string $referenceId = '', int $replyTo = 0, bool $silent = false): DataResponse {
+ $random = $this->request->getHeader('X-Nextcloud-Talk-Bot-Random');
+ if (empty($random) || strlen($random) < 32) {
+ $this->logger->error('Invalid Random received from bot response');
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+ $checksum = $this->request->getHeader('X-Nextcloud-Talk-Bot-Signature');
+ if (empty($checksum)) {
+ $this->logger->error('Invalid Signature received from bot response');
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ $bots = $this->botService->getBotsForToken($token);
+ $bot = null;
+ foreach ($bots as $botAttempt) {
+ try {
+ $this->checksumVerificationService->validateRequest(
+ $random,
+ $checksum,
+ $botAttempt->getBotServer()->getSecret(),
+ $message
+ );
+ $bot = $botAttempt;
+ break;
+ } catch (UnauthorizedException) {
+ }
+ }
+
+ if (!$bot instanceof Bot) {
+ $this->logger->debug('No valid Bot entry found');
+ $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ $response->throttle(['action' => 'bot']);
+ return $response;
+ }
+
+ $room = $this->manager->getRoomByToken($token);
+
+ $actorType = Attendee::ACTOR_BOTS;
+ $actorId = Attendee::ACTOR_BOT_PREFIX . $bot->getBotServer()->getUrlHash();
+
+ $parent = null;
+ if ($replyTo !== 0) {
+ try {
+ $parent = $this->chatManager->getParentComment($room, (string) $replyTo);
+ } catch (NotFoundException $e) {
+ // Someone is trying to reply cross-rooms or to a non-existing message
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+ }
+
+ $this->participantService->ensureOneToOneRoomIsFilled($room);
+ $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC'));
+
+ try {
+ $this->chatManager->sendMessage($room, $this->participant, $actorType, $actorId, $message, $creationDateTime, $parent, $referenceId, $silent);
+ } catch (MessageTooLongException) {
+ return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE);
+ } catch (\Exception) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ return new DataResponse([], Http::STATUS_CREATED);
+ }
+
+ #[NoAdminRequired]
+ #[RequireLoggedInModeratorParticipant]
+ public function listBots(): DataResponse {
+ $alreadyInstalled = array_map(static function (BotConversation $bot): int {
+ return $bot->getBotId();
+ }, $this->botConversationMapper->findForToken($this->room->getToken()));
+
+ $data = [];
+ $bots = $this->botServerMapper->getAllBots();
+ foreach ($bots as $bot) {
+ $botData = $this->formatBot($bot, in_array($bot->getId(), $alreadyInstalled, true));
+ if ($botData !== null) {
+ $data[] = $this->formatBot($bot, in_array($bot->getId(), $alreadyInstalled, true));
+ }
+ }
+
+ return new DataResponse($data);
+ }
+
+ #[NoAdminRequired]
+ #[RequireLoggedInModeratorParticipant]
+ public function enableBot(int $botId): DataResponse {
+ try {
+ $bot = $this->botServerMapper->findById($botId);
+ } catch (DoesNotExistException) {
+ return new DataResponse([
+ 'error' => 'bot',
+ ], Http::STATUS_BAD_REQUEST);
+ }
+
+ if ($bot->getState() !== Bot::STATE_ENABLED) {
+ return new DataResponse([
+ 'error' => 'bot',
+ ], Http::STATUS_BAD_REQUEST);
+ }
+
+ $alreadyInstalled = array_map(static function (BotConversation $bot): int {
+ return $bot->getBotId();
+ }, $this->botConversationMapper->findForToken($this->room->getToken()));
+
+ if (in_array($botId, $alreadyInstalled)) {
+ return new DataResponse($this->formatBot($bot, true), Http::STATUS_OK);
+ }
+
+ $conversationBot = new BotConversation();
+ $conversationBot->setBotId($botId);
+ $conversationBot->setToken($this->room->getToken());
+ $conversationBot->setState(Bot::STATE_ENABLED);
+
+ $this->botConversationMapper->insert($conversationBot);
+ return new DataResponse($this->formatBot($bot, true), Http::STATUS_CREATED);
+ }
+
+ #[NoAdminRequired]
+ #[RequireLoggedInModeratorParticipant]
+ public function disableBot(int $botId): DataResponse {
+ try {
+ $bot = $this->botServerMapper->findById($botId);
+ } catch (DoesNotExistException) {
+ return new DataResponse([
+ 'error' => 'bot',
+ ], Http::STATUS_BAD_REQUEST);
+ }
+
+ if ($bot->getState() !== Bot::STATE_ENABLED) {
+ return new DataResponse([
+ 'error' => 'bot',
+ ], Http::STATUS_BAD_REQUEST);
+ }
+
+ $this->botConversationMapper->deleteByBotIdAndTokens($botId, [$this->room->getToken()]);
+ return new DataResponse($this->formatBot($bot, false), Http::STATUS_OK);
+ }
+
+ /**
+ * @param BotServer $bot
+ * @param bool $conversationEnabled
+ * @return array|null
+ * @psalm-return array{id: int, name: string, description: null|string, state: int}
+ */
+ protected function formatBot(BotServer $bot, bool $conversationEnabled): ?array {
+ $state = $conversationEnabled ? Bot::STATE_ENABLED : Bot::STATE_DISABLED;
+
+ if ($bot->getState() === Bot::STATE_NO_SETUP) {
+ if ($state === Bot::STATE_DISABLED) {
+ return null;
+ }
+ $state = Bot::STATE_NO_SETUP;
+ }
+
+ return [
+ 'id' => $bot->getId(),
+ 'name' => $bot->getName(),
+ 'description' => $bot->getDescription(),
+ 'state' => $state,
+ ];
+ }
+}
diff --git a/lib/Events/BotInstallEvent.php b/lib/Events/BotInstallEvent.php
new file mode 100644
index 00000000000..fa05c44194b
--- /dev/null
+++ b/lib/Events/BotInstallEvent.php
@@ -0,0 +1,55 @@
+
+ *
+ * @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\Events;
+
+use OCP\EventDispatcher\Event;
+
+class BotInstallEvent extends Event {
+ public function __construct(
+ protected string $name,
+ protected string $secret,
+ protected string $url,
+ protected string $description = '',
+ ) {
+ parent::__construct();
+ }
+
+ public function getName(): string {
+ return $this->name;
+ }
+
+ public function getSecret(): string {
+ return $this->secret;
+ }
+
+ public function getUrl(): string {
+ return $this->url;
+ }
+
+ public function getDescription(): string {
+ return $this->description;
+ }
+}
diff --git a/lib/Events/ChatEvent.php b/lib/Events/ChatEvent.php
index 56e199c9632..129585d8918 100644
--- a/lib/Events/ChatEvent.php
+++ b/lib/Events/ChatEvent.php
@@ -31,6 +31,7 @@ public function __construct(
Room $room,
protected IComment $comment,
protected bool $skipLastActivityUpdate = false,
+ protected bool $silent = false,
) {
parent::__construct($room);
}
@@ -53,4 +54,8 @@ public function getComment(): IComment {
public function shouldSkipLastActivityUpdate(): bool {
return $this->skipLastActivityUpdate;
}
+
+ public function isSilentMessage(): bool {
+ return $this->silent;
+ }
}
diff --git a/lib/Events/ChatParticipantEvent.php b/lib/Events/ChatParticipantEvent.php
index fc7fac5a241..b26bf677232 100644
--- a/lib/Events/ChatParticipantEvent.php
+++ b/lib/Events/ChatParticipantEvent.php
@@ -32,16 +32,12 @@ public function __construct(
Room $room,
IComment $message,
protected Participant $participant,
- protected bool $silent,
+ bool $silent,
) {
- parent::__construct($room, $message);
+ parent::__construct($room, $message, false, $silent);
}
public function getParticipant(): Participant {
return $this->participant;
}
-
- public function isSilentMessage(): bool {
- return $this->silent;
- }
}
diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php
index d847f58f5e8..7e2d6816e0d 100644
--- a/lib/Federation/CloudFederationProviderTalk.php
+++ b/lib/Federation/CloudFederationProviderTalk.php
@@ -172,6 +172,7 @@ private function shareAccepted(int $id, array $notification): array {
$this->session->set('talk-overwrite-actor-type', $attendee->getActorType());
$this->session->set('talk-overwrite-actor-id', $attendee->getActorId());
+ $this->session->set('talk-overwrite-actor-displayname', $attendee->getDisplayName());
$room = $this->manager->getRoomById($attendee->getRoomId());
$event = new AttendeesAddedEvent($room, [$attendee]);
@@ -179,6 +180,7 @@ private function shareAccepted(int $id, array $notification): array {
$this->session->remove('talk-overwrite-actor-type');
$this->session->remove('talk-overwrite-actor-id');
+ $this->session->remove('talk-overwrite-actor-displayname');
return [];
}
@@ -193,6 +195,7 @@ private function shareDeclined(int $id, array $notification): array {
$this->session->set('talk-overwrite-actor-type', $attendee->getActorType());
$this->session->set('talk-overwrite-actor-id', $attendee->getActorId());
+ $this->session->set('talk-overwrite-actor-displayname', $attendee->getDisplayName());
$room = $this->manager->getRoomById($attendee->getRoomId());
$participant = new Participant($room, $attendee, null);
@@ -200,6 +203,7 @@ private function shareDeclined(int $id, array $notification): array {
$this->session->remove('talk-overwrite-actor-type');
$this->session->remove('talk-overwrite-actor-id');
+ $this->session->remove('talk-overwrite-actor-displayname');
return [];
}
diff --git a/lib/Listener/BotListener.php b/lib/Listener/BotListener.php
new file mode 100644
index 00000000000..c6e73f50930
--- /dev/null
+++ b/lib/Listener/BotListener.php
@@ -0,0 +1,101 @@
+
+ *
+ * @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\Listener;
+
+use OCA\Talk\Chat\ChatManager;
+use OCA\Talk\Chat\MessageParser;
+use OCA\Talk\Events\BotInstallEvent;
+use OCA\Talk\Events\ChatEvent;
+use OCA\Talk\Events\ChatParticipantEvent;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotServer;
+use OCA\Talk\Model\BotServerMapper;
+use OCA\Talk\Service\BotService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Server;
+
+/**
+ * @template-implements IEventListener
+ */
+class BotListener implements IEventListener {
+ public function __construct(
+ protected BotServerMapper $botServerMapper,
+ ) {
+ }
+
+ public static function register(IEventDispatcher $dispatcher): void {
+ $dispatcher->addListener(ChatManager::EVENT_AFTER_MESSAGE_SEND, [self::class, 'afterMessageSendStatic']);
+ $dispatcher->addListener(ChatManager::EVENT_AFTER_SYSTEM_MESSAGE_SEND, [self::class, 'afterSystemMessageSendStatic']);
+ }
+
+ public static function afterMessageSendStatic(ChatEvent $event): void {
+ if (!$event instanceof ChatParticipantEvent) {
+ // No bots for bots
+ return;
+ }
+
+ /** @var BotService $service */
+ $service = Server::get(BotService::class);
+ $messageParser = Server::get(MessageParser::class);
+ $service->afterChatMessageSent($event, $messageParser);
+ }
+
+ public static function afterSystemMessageSendStatic(ChatEvent $event): void {
+ /** @var BotService $service */
+ $service = Server::get(BotService::class);
+ $messageParser = Server::get(MessageParser::class);
+ $service->afterSystemMessageSent($event, $messageParser);
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof BotInstallEvent) {
+ $this->handleBotInstallEvent($event);
+ }
+ }
+
+ protected function handleBotInstallEvent(BotInstallEvent $event): void {
+ try {
+ $bot = $this->botServerMapper->findByUrlAndSecret($event->getUrl(), $event->getSecret());
+
+ $bot->setName($event->getName());
+ $bot->setDescription($event->getDescription());
+ $this->botServerMapper->update($bot);
+ } catch (DoesNotExistException) {
+ $bot = new BotServer();
+ $bot->setName($event->getName());
+ $bot->setDescription($event->getDescription());
+ $bot->setSecret($event->getSecret());
+ $bot->setUrl($event->getUrl());
+ $bot->setUrlHash(sha1($event->getUrl()));
+ $bot->setState(Bot::STATE_ENABLED);
+ $this->botServerMapper->insert($bot);
+ }
+ }
+}
diff --git a/lib/Listener/CircleMembershipListener.php b/lib/Listener/CircleMembershipListener.php
index 516114ed49f..905449413a6 100644
--- a/lib/Listener/CircleMembershipListener.php
+++ b/lib/Listener/CircleMembershipListener.php
@@ -105,7 +105,8 @@ protected function addingCircleMemberEvent(AddingCircleMemberEvent $event): void
$invitedBy = $newMember->getInvitedBy();
if ($invitedBy->getUserType() === Member::TYPE_USER && $invitedBy->getUserId() !== '') {
- $this->session->set('talk-overwrite-actor', $invitedBy->getUserId());
+ $this->session->set('talk-overwrite-actor-id', $invitedBy->getUserId());
+ $this->session->set('talk-overwrite-actor-displayname', $invitedBy->getDisplayName());
} elseif ($invitedBy->getUserType() === Member::TYPE_APP && $invitedBy->getBasedOn()->getSource() === Member::APP_OCC) {
$this->session->set('talk-overwrite-actor-cli', 'cli');
}
@@ -113,7 +114,8 @@ protected function addingCircleMemberEvent(AddingCircleMemberEvent $event): void
foreach ($userMembers as $userMember) {
$this->addNewMemberToRooms(array_values($roomsToAdd), $userMember);
}
- $this->session->remove('talk-overwrite-actor');
+ $this->session->remove('talk-overwrite-actor-displayname');
+ $this->session->remove('talk-overwrite-actor-id');
$this->session->remove('talk-overwrite-actor-cli');
}
@@ -161,7 +163,8 @@ protected function removeFormerMemberFromRooms(RemovingCircleMemberEvent $event)
$removedBy = $removedMember->getInvitedBy();
if ($removedBy->getUserType() === Member::TYPE_USER && $removedBy->getUserId() !== '') {
- $this->session->set('talk-overwrite-actor', $removedBy->getUserId());
+ $this->session->set('talk-overwrite-actor-id', $removedBy->getUserId());
+ $this->session->set('talk-overwrite-actor-displayname', $removedBy->getDisplayName());
} elseif ($removedBy->getUserType() === Member::TYPE_APP && $removedBy->getUserId() === 'occ') {
$this->session->set('talk-overwrite-actor-cli', 'cli');
}
@@ -173,7 +176,8 @@ protected function removeFormerMemberFromRooms(RemovingCircleMemberEvent $event)
$this->removeFromRoomsUnlessStillLinked($rooms, $user);
- $this->session->remove('talk-overwrite-actor');
+ $this->session->remove('talk-overwrite-actor-displayname');
+ $this->session->remove('talk-overwrite-actor-id');
$this->session->remove('talk-overwrite-actor-cli');
}
}
diff --git a/lib/Migration/Version18000Date20230504205823.php b/lib/Migration/Version18000Date20230504205823.php
new file mode 100644
index 00000000000..32cbc0c26f2
--- /dev/null
+++ b/lib/Migration/Version18000Date20230504205823.php
@@ -0,0 +1,115 @@
+
+ *
+ * @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\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version18000Date20230504205823 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ * @return ?ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if (!$schema->hasTable('talk_bots_server')) {
+ $table = $schema->createTable('talk_bots_server');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('name', Types::STRING, [
+ 'length' => 64,
+ ]);
+ $table->addColumn('url', Types::STRING, [
+ 'length' => 4000,
+ ]);
+ $table->addColumn('url_hash', Types::STRING, [
+ 'length' => 64,
+ ]);
+ $table->addColumn('description', Types::STRING, [
+ 'length' => 4000,
+ 'notnull' => false,
+ ]);
+ $table->addColumn('secret', Types::STRING, [
+ 'length' => 128,
+ ]);
+ $table->addColumn('error_count', Types::BIGINT, [
+ 'default' => 0,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('last_error_date', Types::DATETIME, [
+ 'notnull' => false,
+ ]);
+ $table->addColumn('last_error_message', Types::STRING, [
+ 'length' => 4000,
+ 'notnull' => false,
+ ]);
+ $table->addColumn('state', Types::SMALLINT, [
+ 'default' => 0,
+ 'notnull' => false,
+ 'unsigned' => true,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['state'], 'talk_bots_server_state');
+
+ $table = $schema->createTable('talk_bots_conversation');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('bot_id', Types::BIGINT, [
+ 'default' => 0,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('token', Types::STRING, [
+ 'length' => 64,
+ 'notnull' => false,
+ ]);
+ $table->addColumn('state', Types::SMALLINT, [
+ 'default' => 0,
+ 'notnull' => false,
+ 'unsigned' => true,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['token', 'state'], 'talk_bots_convo_token');
+ $table->addIndex(['bot_id'], 'talk_bots_convo_id');
+ return $schema;
+ }
+
+ return null;
+ }
+}
diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php
index cabda580faa..ffb84d9b57e 100644
--- a/lib/Model/Attendee.php
+++ b/lib/Model/Attendee.php
@@ -68,7 +68,9 @@ class Attendee extends Entity {
public const ACTOR_EMAILS = 'emails';
public const ACTOR_CIRCLES = 'circles';
public const ACTOR_BRIDGED = 'bridged';
+ public const ACTOR_BOTS = 'bots';
public const ACTOR_FEDERATED_USERS = 'federated_users';
+ public const ACTOR_BOT_PREFIX = 'bot-';
public const PERMISSIONS_DEFAULT = 0;
public const PERMISSIONS_CUSTOM = 1;
diff --git a/lib/Model/Bot.php b/lib/Model/Bot.php
new file mode 100644
index 00000000000..474fe1fb4e2
--- /dev/null
+++ b/lib/Model/Bot.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\Model;
+
+class Bot {
+ public const STATE_DISABLED = 0;
+ public const STATE_ENABLED = 1;
+ public const STATE_NO_SETUP = 2;
+
+ public function __construct(
+ protected BotServer $botServer,
+ protected BotConversation $botConversation,
+ ) {
+ }
+
+ public function getBotServer(): BotServer {
+ return $this->botServer;
+ }
+
+ public function getBotConversation(): BotConversation {
+ return $this->botConversation;
+ }
+
+ public function isEnabled(): bool {
+ return $this->botServer->getState() !== self::STATE_DISABLED
+ && $this->botConversation->getState() !== self::STATE_DISABLED;
+ }
+}
diff --git a/lib/Model/BotConversation.php b/lib/Model/BotConversation.php
new file mode 100644
index 00000000000..7d90d17a26e
--- /dev/null
+++ b/lib/Model/BotConversation.php
@@ -0,0 +1,55 @@
+
+ *
+ * @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 setBotId(int $botId)
+ * @method int getBotId()
+ * @method void setToken(string $token)
+ * @method string getToken()
+ * @method void setState(int $state)
+ * @method int getState()
+ */
+class BotConversation extends Entity implements \JsonSerializable {
+ protected int $botId = 0;
+ protected string $token = '';
+ protected int $state = Bot::STATE_DISABLED;
+
+ public function __construct() {
+ $this->addType('bot_id', 'int');
+ $this->addType('token', 'string');
+ $this->addType('state', 'int');
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'bot_id' => $this->getBotId(),
+ 'token' => $this->getToken(),
+ 'state' => $this->getState(),
+ ];
+ }
+}
diff --git a/lib/Model/BotConversationMapper.php b/lib/Model/BotConversationMapper.php
new file mode 100644
index 00000000000..fdaa638de7b
--- /dev/null
+++ b/lib/Model/BotConversationMapper.php
@@ -0,0 +1,74 @@
+
+ *
+ * @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\AppFramework\Db\TTransactional;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * @method BotConversation mapRowToEntity(array $row)
+ * @method BotConversation findEntity(IQueryBuilder $query)
+ * @method BotConversation[] findEntities(IQueryBuilder $query)
+ * @template-extends QBMapper
+ */
+class BotConversationMapper extends QBMapper {
+ use TTransactional;
+
+ public function __construct(
+ IDBConnection $db,
+ ) {
+ parent::__construct($db, 'talk_bots_conversation', BotConversation::class);
+ }
+
+ /**
+ * @return BotConversation[]
+ */
+ public function findForToken(string $token): array {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('token', $query->createNamedParameter($token)));
+
+ return $this->findEntities($query);
+ }
+
+ public function deleteByBotId(int $botId): int {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->getTableName())
+ ->where($query->expr()->eq('bot_id', $query->createNamedParameter($botId, IQueryBuilder::PARAM_INT)));
+
+ return $query->executeStatement();
+ }
+
+ public function deleteByBotIdAndTokens(int $botId, array $tokens): int {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->getTableName())
+ ->where($query->expr()->eq('bot_id', $query->createNamedParameter($botId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->in('token', $query->createNamedParameter($tokens, IQueryBuilder::PARAM_STR_ARRAY)));
+
+ return $query->executeStatement();
+ }
+}
diff --git a/lib/Model/BotServer.php b/lib/Model/BotServer.php
new file mode 100644
index 00000000000..265845daf8a
--- /dev/null
+++ b/lib/Model/BotServer.php
@@ -0,0 +1,85 @@
+
+ *
+ * @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 setName(string $name)
+ * @method string getName()
+ * @method void setUrl(string $url)
+ * @method string getUrl()
+ * @method void setUrlHash(string $urlHash)
+ * @method string getUrlHash()
+ * @method void setDescription(?string $description)
+ * @method null|string getDescription()
+ * @method void setSecret(string $secret)
+ * @method string getSecret()
+ * @method void setErrorCount(int $errorCount)
+ * @method int getErrorCount()
+ * @method void setLastErrorDate(?\DateTimeImmutable $lastErrorDate)
+ * @method ?\DateTimeImmutable getLastErrorDate()
+ * @method void setLastErrorMessage(string $lastErrorMessage)
+ * @method string getLastErrorMessage()
+ * @method void setState(int $state)
+ * @method int getState()
+ */
+class BotServer extends Entity implements \JsonSerializable {
+ protected string $name = '';
+ protected string $url = '';
+ protected string $urlHash = '';
+ protected ?string $description = null;
+ protected string $secret = '';
+ protected int $errorCount = 0;
+ protected ?\DateTimeImmutable $lastErrorDate = null;
+ protected ?string $lastErrorMessage = null;
+ protected int $state = Bot::STATE_DISABLED;
+
+ public function __construct() {
+ $this->addType('name', 'string');
+ $this->addType('url', 'string');
+ $this->addType('url_hash', 'string');
+ $this->addType('description', 'string');
+ $this->addType('secret', 'string');
+ $this->addType('error_count', 'int');
+ $this->addType('last_error_date', 'datetime');
+ $this->addType('last_error_message', 'string');
+ $this->addType('state', 'int');
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'name' => $this->getName(),
+ 'url' => $this->getUrl(),
+ 'url_hash' => $this->getUrlHash(),
+ 'description' => $this->getDescription(),
+ 'secret' => $this->getSecret(),
+ 'error_count' => $this->getErrorCount(),
+ 'last_error_date' => $this->getLastErrorDate() ? $this->getLastErrorDate()->getTimestamp() : 0,
+ 'last_error_message' => $this->getLastErrorMessage(),
+ 'state' => $this->getState(),
+ ];
+ }
+}
diff --git a/lib/Model/BotServerMapper.php b/lib/Model/BotServerMapper.php
new file mode 100644
index 00000000000..caee0e77928
--- /dev/null
+++ b/lib/Model/BotServerMapper.php
@@ -0,0 +1,104 @@
+
+ *
+ * @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\DoesNotExistException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\AppFramework\Db\TTransactional;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * @method BotServer mapRowToEntity(array $row)
+ * @method BotServer findEntity(IQueryBuilder $query)
+ * @method BotServer[] findEntities(IQueryBuilder $query)
+ * @template-extends QBMapper
+ */
+class BotServerMapper extends QBMapper {
+ use TTransactional;
+
+ public function __construct(
+ IDBConnection $db,
+ ) {
+ parent::__construct($db, 'talk_bots_server', BotServer::class);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function findById(int $botId): BotServer {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('id', $query->createNamedParameter($botId, IQueryBuilder::PARAM_INT)));
+
+ return $this->findEntity($query);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function findByUrlAndSecret(string $url, string $secret): BotServer {
+ $urlHash = sha1($url);
+
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->eq('url_hash', $query->createNamedParameter($urlHash)))
+ ->andWhere($query->expr()->eq('secret', $query->createNamedParameter($secret)));
+
+ return $this->findEntity($query);
+ }
+
+ public function deleteById(int $botId): int {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->getTableName())
+ ->where($query->expr()->eq('id', $query->createNamedParameter($botId, IQueryBuilder::PARAM_INT)));
+
+ return $query->executeStatement();
+ }
+
+ /**
+ * @return BotServer[]
+ */
+ public function findByIds(array $botIds): array {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName())
+ ->where($query->expr()->in('id', $query->createNamedParameter($botIds, IQueryBuilder::PARAM_INT_ARRAY)));
+
+ return $this->findEntities($query);
+ }
+
+ /**
+ * @return BotServer[]
+ */
+ public function getAllBots(): array {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from($this->getTableName());
+
+ return $this->findEntities($query);
+ }
+}
diff --git a/lib/Model/Message.php b/lib/Model/Message.php
index 0be5935dde1..c4c7864fd1a 100644
--- a/lib/Model/Message.php
+++ b/lib/Model/Message.php
@@ -57,7 +57,7 @@ class Message {
public function __construct(
protected Room $room,
- protected Participant $participant,
+ protected ?Participant $participant,
protected IComment $comment,
protected IL10N $l,
) {
@@ -79,7 +79,7 @@ public function getL10n(): IL10N {
return $this->l;
}
- public function getParticipant(): Participant {
+ public function getParticipant(): ?Participant {
return $this->participant;
}
diff --git a/lib/Service/BotService.php b/lib/Service/BotService.php
new file mode 100644
index 00000000000..e371c6ac02d
--- /dev/null
+++ b/lib/Service/BotService.php
@@ -0,0 +1,285 @@
+
+ *
+ * @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\Chat\MessageParser;
+use OCA\Talk\Events\ChatEvent;
+use OCA\Talk\Events\ChatParticipantEvent;
+use OCA\Talk\Model\Attendee;
+use OCA\Talk\Model\Bot;
+use OCA\Talk\Model\BotConversation;
+use OCA\Talk\Model\BotConversationMapper;
+use OCA\Talk\Model\BotServerMapper;
+use OCA\Talk\Room;
+use OCA\Talk\TalkSession;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
+
+class BotService {
+ public function __construct(
+ protected BotServerMapper $botServerMapper,
+ protected BotConversationMapper $botConversationMapper,
+ protected IClientService $clientService,
+ protected IConfig $serverConfig,
+ protected IUserSession $userSession,
+ protected TalkSession $talkSession,
+ protected ISession $session,
+ protected ISecureRandom $secureRandom,
+ protected IURLGenerator $urlGenerator,
+ protected IFactory $l10nFactory,
+ protected ITimeFactory $timeFactory,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ public function afterChatMessageSent(ChatParticipantEvent $event, MessageParser $messageParser): void {
+ $bots = $this->getBotsForToken($event->getRoom()->getToken());
+ if (empty($bots)) {
+ return;
+ }
+
+ $message = $messageParser->createMessage(
+ $event->getRoom(),
+ $event->getParticipant(),
+ $event->getComment(),
+ $this->l10nFactory->get('spreed', 'en', 'en')
+ );
+ $messageParser->parseMessage($message);
+ $messageData = [
+ 'message' => $message->getMessage(),
+ 'parameters' => $message->getMessageParameters(),
+ ];
+
+ $attendee = $event->getParticipant()->getAttendee();
+
+ $this->sendAsyncRequests($bots, [
+ 'type' => 'Create',
+ 'actor' => [
+ 'type' => 'Person',
+ 'id' => $attendee->getActorType() . '/' . $attendee->getActorId(),
+ 'name' => $attendee->getDisplayName(),
+ ],
+ 'object' => [
+ 'type' => 'Note',
+ 'id' => $event->getComment()->getId(),
+ 'name' => 'message',
+ 'content' => json_encode($messageData, JSON_THROW_ON_ERROR),
+ 'mediaType' => 'text/markdown', // FIXME or text/plain when markdown is disabled
+ ],
+ 'target' => [
+ 'type' => 'Collection',
+ 'id' => $event->getRoom()->getToken(),
+ 'name' => $event->getRoom()->getName(),
+ ]
+ ]);
+ }
+
+ public function afterSystemMessageSent(ChatEvent $event, MessageParser $messageParser): void {
+ $bots = $this->getBotsForToken($event->getRoom()->getToken());
+ if (empty($bots)) {
+ return;
+ }
+
+ $message = $messageParser->createMessage(
+ $event->getRoom(),
+ null,
+ $event->getComment(),
+ $this->l10nFactory->get('spreed', 'en', 'en')
+ );
+ $messageParser->parseMessage($message);
+ $messageData = [
+ 'message' => $message->getMessage(),
+ 'parameters' => $message->getMessageParameters(),
+ ];
+
+ $this->sendAsyncRequests($bots, [
+ 'type' => 'Activity',
+ 'actor' => [
+ 'type' => 'Person',
+ 'id' => $message->getActorType() . '/' . $message->getActorId(),
+ 'name' => $message->getActorDisplayName(),
+ ],
+ 'object' => [
+ 'type' => 'Note',
+ 'id' => $event->getComment()->getId(),
+ 'name' => $message->getMessageRaw(),
+ 'content' => json_encode($messageData),
+ 'mediaType' => 'text/markdown',
+ ],
+ 'target' => [
+ 'type' => 'Collection',
+ 'id' => $event->getRoom()->getToken(),
+ 'name' => $event->getRoom()->getName(),
+ ]
+ ]);
+ }
+
+ /**
+ * @param Bot[] $bots
+ * @param array $body
+ */
+ protected function sendAsyncRequests(array $bots, array $body): void {
+ $jsonBody = json_encode($body, JSON_THROW_ON_ERROR);
+
+ foreach ($bots as $bot) {
+ $botServer = $bot->getBotServer();
+ $random = $this->secureRandom->generate(64);
+ $hash = hash_hmac('sha256', $random . $jsonBody, $botServer->getSecret());
+ $headers = [
+ 'Content-Type' => 'application/json',
+ 'X-Nextcloud-Talk-Random' => $random,
+ 'X-Nextcloud-Talk-Signature' => $hash,
+ 'X-Nextcloud-Talk-Backend' => rtrim($this->serverConfig->getSystemValueString('overwrite.cli.url'), '/') . '/',
+ 'OCS-APIRequest' => 'true', // FIXME optional?
+ ];
+
+ $data = [
+ 'verify' => false,
+ 'nextcloud' => [
+ 'allow_local_address' => true, // FIXME don't enforce
+ ],
+ 'headers' => $headers,
+ 'timeout' => 5,
+ 'body' => json_encode($body),
+ ];
+
+ $client = $this->clientService->newClient();
+ $promise = $client->postAsync($botServer->getUrl(), $data);
+
+ $promise->then(function (IResponse $response) use ($botServer) {
+ if ($response->getStatusCode() !== Http::STATUS_OK && $response->getStatusCode() !== Http::STATUS_ACCEPTED) {
+ $this->logger->error('Bot responded with unexpected status code (Received: ' . $response->getStatusCode() . '), increasing error count');
+ $botServer->setErrorCount($botServer->getErrorCount() + 1);
+ $botServer->setLastErrorDate($this->timeFactory->now());
+ $botServer->setLastErrorMessage('UnexpectedStatusCode: ' . $response->getStatusCode());
+ $this->botServerMapper->update($botServer);
+ }
+ }, function (\Exception $exception) use ($botServer) {
+ $this->logger->error('Bot error occurred, increasing error count', ['exception' => $exception]);
+ $botServer->setErrorCount($botServer->getErrorCount() + 1);
+ $botServer->setLastErrorDate($this->timeFactory->now());
+ $botServer->setLastErrorMessage(get_class($exception) . ': ' . $exception->getMessage());
+ $this->botServerMapper->update($botServer);
+ });
+ }
+ }
+
+ /**
+ * @param Room $room
+ * @return array
+ * @psalm-return array{type: string, id: string, name: string}
+ */
+ protected function getActor(Room $room): array {
+ if (\OC::$CLI || $this->session->exists('talk-overwrite-actor-cli')) {
+ return [
+ 'type' => Attendee::ACTOR_GUESTS,
+ 'id' => 'cli',
+ 'name' => 'Administration',
+ ];
+ }
+
+ if ($this->session->exists('talk-overwrite-actor-type')) {
+ return [
+ 'type' => $this->session->get('talk-overwrite-actor-type'),
+ 'id' => $this->session->get('talk-overwrite-actor-id'),
+ 'name' => $this->session->get('talk-overwrite-actor-displayname'),
+ ];
+ }
+
+ if ($this->session->exists('talk-overwrite-actor-id')) {
+ return [
+ 'type' => Attendee::ACTOR_USERS,
+ 'id' => $this->session->get('talk-overwrite-actor-id'),
+ 'name' => $this->session->get('talk-overwrite-actor-displayname'),
+ ];
+ }
+
+ $user = $this->userSession->getUser();
+ if ($user instanceof IUser) {
+ return [
+ 'type' => Attendee::ACTOR_USERS,
+ 'id' => $user->getUID(),
+ 'name' => $user->getDisplayName(),
+ ];
+ }
+
+ $sessionId = $this->talkSession->getSessionForRoom($room->getToken());
+ $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session';
+ return [
+ 'type' => Attendee::ACTOR_GUESTS,
+ 'id' => $actorId,
+ 'name' => $user->getDisplayName(),
+ ];
+ }
+
+ /**
+ * @param string $token
+ * @return Bot[]
+ */
+ public function getBotsForToken(string $token): array {
+ $botConversations = $this->botConversationMapper->findForToken($token);
+
+ if (empty($botConversations)) {
+ return [];
+ }
+
+ $botIds = array_map(static fn (BotConversation $bot): int => $bot->getBotId(), $botConversations);
+
+ $serversMap = [];
+ $botServers = $this->botServerMapper->findByIds($botIds);
+ foreach ($botServers as $botServer) {
+ $serversMap[$botServer->getId()] = $botServer;
+ }
+
+ $bots = [];
+ foreach ($botConversations as $botConversation) {
+ if (!isset($serversMap[$botConversation->getBotId()])) {
+ $this->logger->warning('Can not find bot by ID ' . $botConversation->getBotId() . ' for token ' . $botConversation->getToken());
+ continue;
+ }
+
+ $bot = new Bot(
+ $serversMap[$botConversation->getBotId()],
+ $botConversation,
+ );
+
+ if ($bot->isEnabled()) {
+ $bots[] = $bot;
+ }
+ }
+
+ return $bots;
+ }
+}
diff --git a/tests/integration/features/bootstrap/CommandLineTrait.php b/tests/integration/features/bootstrap/CommandLineTrait.php
index e13a00d8327..968486a8bd7 100644
--- a/tests/integration/features/bootstrap/CommandLineTrait.php
+++ b/tests/integration/features/bootstrap/CommandLineTrait.php
@@ -86,6 +86,10 @@ public function invokingTheCommand($cmd) {
$this->runOcc($args);
}
+ public function getLastStdOut(): string {
+ return $this->lastStdOut;
+ }
+
/**
* Find exception texts in stderr
*/
@@ -160,6 +164,13 @@ public function theCommandOutputContainsTheText($text) {
Assert::assertStringContainsString($text, $this->lastStdOut, 'The command did not output the expected text on stdout');
}
+ /**
+ * @Then /^the command output is empty$/
+ */
+ public function theCommandOutputIsEmpty() {
+ Assert::assertEmpty($this->lastStdOut, 'The command did output unexpected text on stdout');
+ }
+
/**
* @Then /^the command output contains the list entry '([^']*)' with value '([^']*)'$/
*/
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 8e8dbfc6ffe..a93cc2257c5 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -64,6 +64,12 @@ class FeatureContext implements Context, SnippetAcceptingContext {
protected static $questionToPollId;
/** @var array[] */
protected static $lastNotifications;
+ /** @var array */
+ protected static $botIdToName;
+ /** @var array */
+ protected static $botNameToId;
+ /** @var array */
+ protected static $botNameToHash;
protected static $permissionsMap = [
@@ -1664,6 +1670,7 @@ public function userSeesPeersInCall(string $user, int $numPeers, string $identif
*/
public function userSendsMessageToRoom(string $user, string $sendingMode, string $message, string $identifier, string $statusCode, string $apiVersion = 'v1') {
$message = substr($message, 1, -1);
+ $message = str_replace('\n', "\n", $message);
if ($sendingMode === 'silent sends') {
$body = new TableNode([['message', $message], ['silent', true]]);
} else {
@@ -2296,6 +2303,23 @@ protected function compareDataResponse(TableNode $formData = null) {
$expected[$i]['messageParameters'] = str_replace($matches[0], json_encode($messages[$i]['messageParameters']['object']['icon-url']), $expected[$i]['messageParameters']);
}
}
+ $expected[$i]['message'] = str_replace('\n', "\n", $expected[$i]['message']);
+
+ if ($expected[$i]['actorType'] === 'bots') {
+ $result = preg_match('/BOT\(([^)]+)\)/', $expected[$i]['actorId'], $matches);
+ if ($result && isset(self::$botNameToHash[$matches[1]])) {
+ $expected[$i]['actorId'] = 'bot-' . self::$botNameToHash[$matches[1]];
+ }
+ }
+
+ // Replace the date/time line of the call summary because we can not know if we jumped a minute, hour or day on the execution.
+ if (str_contains($expected[$i]['message'], '{DATE}')) {
+ $messages[$i]['message'] = preg_replace(
+ '/[A-Za-z]+day, [A-Za-z]+ \d+, \d+ · \d+:\d+ [AP]M – \d+:\d+ [AP]M \(UTC\)/u',
+ '{DATE}',
+ $messages[$i]['message']
+ );
+ }
}
Assert::assertEquals($expected, array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf) {
@@ -3217,7 +3241,7 @@ public function userSetTheMessageExpirationToXWithStatusCode(string $user, int $
}
/**
- * @When wait for :seconds (second|seconds)
+ * @When /^wait for ([0-9]+) (second|seconds)$/
*/
public function waitForXSecond($seconds): void {
sleep($seconds);
@@ -3447,6 +3471,59 @@ public function userStoreRecordingFileInRoom(string $user, string $file, string
$this->assertStatusCode($this->response, $statusCode);
}
+ /**
+ * @Then /^read bot ids from OCC$/
+ */
+ public function readBotIds(): void {
+ $this->invokingTheCommand('talk:bot:list -v --output json');
+ $this->theCommandWasSuccessful();
+ $json = $this->getLastStdOut();
+
+ $botData = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
+ foreach ($botData as $bot) {
+ self::$botNameToId[$bot['name']] = $bot['id'];
+ self::$botNameToHash[$bot['name']] = $bot['url_hash'];
+ self::$botIdToName[$bot['id']] = $bot['name'];
+ }
+ }
+
+ /**
+ * @Then /^(setup|remove) bot "([^"]*)" for room "([^"]*)" via OCC$/
+ */
+ public function setupOrRemoveBotInRoom(string $action, string $botName, string $identifier): void {
+ $this->invokingTheCommand('talk:bot:' . $action . ' ' . self::$botNameToId[$botName] . ' ' . self::$identifierToToken[$identifier]);
+ $this->theCommandWasSuccessful();
+ }
+
+ /**
+ * @Then /^set state (enabled|disabled|no-setup) for bot "([^"]*)" via OCC$/
+ */
+ public function stateUpdateForBot(string $state, string $botName): void {
+ if ($state === 'enabled') {
+ $state = 1;
+ } elseif ($state === 'disabled') {
+ $state = 0;
+ } elseif ($state === 'no-setup') {
+ $state = 2;
+ }
+
+ $this->invokingTheCommand('talk:bot:state ' . self::$botNameToId[$botName] . ' ' . $state);
+ $this->theCommandWasSuccessful();
+ }
+
+ /**
+ * @Then /^user "([^"]*)" (sets up|removes) bot "([^"]*)" for room "([^"]*)" with (\d+)(?: \((v1)\))?$/
+ */
+ public function setupOrRemoveBotViaOCSAPI(string $user, string $action, string $botName, string $identifier, int $status, string $apiVersion): void {
+ $this->setCurrentUser($user);
+
+ $this->sendRequest(
+ $action === 'sets up' ? 'POST' : 'DELETE',
+ '/apps/spreed/api/' . $apiVersion . '/bot/' . self::$identifierToToken[$identifier] . '/' .self::$botNameToId[$botName]
+ );
+ $this->assertStatusCode($this->response, $status);
+ }
+
/**
* @Then /^user "([^"]*)" shares file from the (first|last) notification to room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
diff --git a/tests/integration/features/chat/bots.feature b/tests/integration/features/chat/bots.feature
new file mode 100644
index 00000000000..6a1024c00ae
--- /dev/null
+++ b/tests/integration/features/chat/bots.feature
@@ -0,0 +1,98 @@
+Feature: chat/bots
+ Background:
+ Given user "participant1" exists
+
+ Scenario: Installing the call summary 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"
+ And the command was successful
+ And invoking occ with "app:enable call_summary_bot"
+ 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"
+
+ Scenario: Simple call summary bot run
+ # Populate default options again
+ And invoking occ with "app:disable call_summary_bot"
+ And the command was successful
+ And invoking occ with "app:enable call_summary_bot"
+ 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"
+
+ # Set up in room
+ Given invoking occ with "talk:bot:list room-name:room"
+ And the command was successful
+ And the command output is empty
+ Given user "participant1" creates room "room" (v4)
+ | roomType | 2 |
+ | roomName | room |
+ And read bot ids from OCC
+ And setup bot "Call summary" 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"
+
+ # Call summary
+ Given the following call_summary_bot app config is set
+ | min-length | -1 |
+ And user "participant1" sends message "- Before call" to room "room" with 201
+ And wait for 2 seconds
+ Then user "participant1" joins room "room" with 200 (v4)
+ Then user "participant1" joins call "room" with 200 (v4)
+ | 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
+ Then user "participant1" sees the following messages in room "room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | room | users | participant1 | participant1-displayname | - Task 2\n-Task 3 | [] |
+ | room | users | participant1 | participant1-displayname | * Task 1 | [] |
+ | room | users | participant1 | participant1-displayname | - Before call | [] |
+ Then user "participant1" leaves call "room" with 200 (v4)
+ 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 | [] |
+ | room | users | participant1 | participant1-displayname | - Task 2\n-Task 3 | [] |
+ | room | users | participant1 | participant1-displayname | * Task 1 | [] |
+ | room | users | participant1 | participant1-displayname | - Before call | [] |
+
+ # Different states bot
+ # Already enabled
+ And user "participant1" sets up bot "Call summary" 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"
+ # Disabling
+ And user "participant1" removes bot "Call summary" 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)
+ Given invoking occ with "talk:bot:list room-name:room"
+ And the command was successful
+ And the command output contains the text "Call summary"
+
+ # No-setup
+ And set state no-setup for bot "Call summary" via OCC
+
+ ## Failed removing
+ And user "participant1" removes bot "Call summary" 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"
+
+ ## Failed adding
+ And remove bot "Call summary" 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)
+ Given invoking occ with "talk:bot:list room-name:room"
+ And the command was successful
+ And the command output is empty
diff --git a/tests/integration/run.sh b/tests/integration/run.sh
index b22beefe39a..b6dd66087ce 100755
--- a/tests/integration/run.sh
+++ b/tests/integration/run.sh
@@ -3,6 +3,7 @@
APP_NAME=spreed
NOTIFICATIONS_BRANCH="master"
GUESTS_BRANCH="master"
+CSB_BRANCH="main"
APP_INTEGRATION_DIR=$PWD
ROOT_DIR=${APP_INTEGRATION_DIR}/../../../..
@@ -16,7 +17,7 @@ echo ''
echo '#'
echo '# Starting PHP webserver'
echo '#'
-php -S localhost:8080 -t ${ROOT_DIR} &
+PHP_CLI_SERVER_WORKERS=3 php -S localhost:8080 -t ${ROOT_DIR} &
PHPPID1=$!
echo 'Running on process ID:'
echo $PHPPID1
@@ -41,6 +42,9 @@ export NEXTCLOUD_ROOT_DIR
export TEST_SERVER_URL="http://localhost:8080/"
export TEST_REMOTE_URL="http://localhost:8180/"
+OVERWRITE_CLI_URL=$(${ROOT_DIR}/occ config:system:get overwrite.cli.url)
+${ROOT_DIR}/occ config:system:set overwrite.cli.url --value "http://localhost:8080/"
+
echo ''
echo '#'
echo '# Setting up apps'
@@ -52,15 +56,18 @@ ${ROOT_DIR}/occ app:getpath spreedcheats
# 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 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 notifications
${ROOT_DIR}/occ app:list | grep guests
+${ROOT_DIR}/occ app:list | grep call_summary_bot
echo ''
echo '#'
@@ -88,6 +95,7 @@ kill $PHPPID1
kill $PHPPID2
${ROOT_DIR}/occ app:disable spreedcheats
+${ROOT_DIR}/occ config:system:set overwrite.cli.url --value $OVERWRITE_CLI_URL
rm -rf ../../../spreedcheats
wait $PHPPID1
diff --git a/tests/integration/spreedcheats/lib/Controller/ApiController.php b/tests/integration/spreedcheats/lib/Controller/ApiController.php
index c59896a3bae..a150342d3ee 100644
--- a/tests/integration/spreedcheats/lib/Controller/ApiController.php
+++ b/tests/integration/spreedcheats/lib/Controller/ApiController.php
@@ -55,6 +55,12 @@ public function resetSpreed(): DataResponse {
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_attendees')->executeStatement();
+ $delete = $this->db->getQueryBuilder();
+ $delete->delete('talk_bots_conversation')->executeStatement();
+
+ $delete = $this->db->getQueryBuilder();
+ $delete->delete('talk_bots_server')->executeStatement();
+
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_bridges')->executeStatement();
@@ -70,15 +76,15 @@ public function resetSpreed(): DataResponse {
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_invitations')->executeStatement();
- $delete = $this->db->getQueryBuilder();
- $delete->delete('talk_rooms')->executeStatement();
-
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_polls')->executeStatement();
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_poll_votes')->executeStatement();
+ $delete = $this->db->getQueryBuilder();
+ $delete->delete('talk_rooms')->executeStatement();
+
$delete = $this->db->getQueryBuilder();
$delete->delete('talk_sessions')->executeStatement();
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index eb29642983f..4ddfba16b8e 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -29,6 +29,36 @@
\OC_Util
+
+
+ Base
+
+
+
+
+ Base
+
+
+
+
+ Base
+
+
+
+
+ Base
+
+
+
+
+ Base
+
+
+
+
+ Base
+
+
Base