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