From 09c76d0e9cc5fef1ddaa70ec4120d88114dcb29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Pr=C3=B6schel?= Date: Wed, 25 Aug 2021 17:02:47 +0200 Subject: [PATCH 01/33] Bump version to 0.30.0-alpha --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ae6dd4e203..0236ce60b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.29.0 +0.30.0-alpha From b7f60159f368f6d5d80b9ea789441737bf4fee28 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Fri, 27 Aug 2021 09:24:41 +0200 Subject: [PATCH 02/33] [#2335] Update release process docs (#2336) --- .github/ISSUE_TEMPLATE/---docs.md | 11 +++++++++++ docs/docs/concepts/release-process.md | 7 +++++-- .../getting-started/installation/configuration.md | 8 +++++--- docs/src/css/custom.css | 10 ++++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/---docs.md diff --git a/.github/ISSUE_TEMPLATE/---docs.md b/.github/ISSUE_TEMPLATE/---docs.md new file mode 100644 index 0000000000..99b81c14bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---docs.md @@ -0,0 +1,11 @@ +--- +name: "\U0001F4DA Bug report" +about: Request a doc change in Airy. +title: "" +labels: docs +assignees: "" +--- + +## This is how we could improve the [documentation](https://docs.airy.co): + +Did you spot an error in the docs or do you think something is missing? Write a clear and concise description of your change request. diff --git a/docs/docs/concepts/release-process.md b/docs/docs/concepts/release-process.md index 3df81bf8b7..a8bb3fb410 100644 --- a/docs/docs/concepts/release-process.md +++ b/docs/docs/concepts/release-process.md @@ -19,9 +19,12 @@ permission to the Airy organization to function correctly Once a release day comes, we execute the following steps: -- We clean up the draft release +- We clean up the draft release. If the upgrade to the new version requires manual steps, we detail them. - We run `./scripts/release.sh start x.y.z` -- We test the release using `airy create --provider=minikube`. Note that: +- We wait for the release candidate CLI to be pushed and then download it by running: + - `wget https://airy-core-binaries.s3.amazonaws.com/$VERSION-rc/darwin/amd64/airy` + - `chmod +x airy` +- We test the release using `./airy create --provider=minikube`. Note that: - Any additional hot-fix is committed directly to the release branch - You must wait for all the images to be pushed via CI - Once we're satisfied with the release, we publish the release: diff --git a/docs/docs/getting-started/installation/configuration.md b/docs/docs/getting-started/installation/configuration.md index 360b19be40..1fff780512 100644 --- a/docs/docs/getting-started/installation/configuration.md +++ b/docs/docs/getting-started/installation/configuration.md @@ -31,7 +31,7 @@ are looking for. If you want to launch an older version refer to our [Releases](https://github.com/airyhq/airy/releases) for the correct version - number, or if you are feeling adventurous, try `develop` at your own risk. + number, or if you are feeling adventurous, try the [development version](https://github.com/airyhq/airy/blob/develop/VERSION) at your own risk. - `containerRegistry` the URL of the container registry @@ -75,10 +75,12 @@ cluster and Redis. - `webhookSecret` set this to a webhook secret of your choice (optional) - `google` - `saFile` copy here the content of your Google service account key file (one line json string) - - `partnerKey` set this to your Google parttner key + - `partnerKey` set this to your Google partner key - `twilio` - `authToken` set this to your Twilio authentication token - `accountSid` set this to your Twilio account SID + - `viber` + - `authToken` set this to your Viber authentication token The **Airy Controller** only starts configured sources. To keep system load to a minimum, only add the sources you are using. @@ -104,7 +106,7 @@ monitor or debug the **Airy Core**. ### Example airy.yaml file -For example, if you want to enable Facebook, Google and Twilio sources, as well as the webhook integration and the AKHQ tool, your `airy.yaml` file should look like this: +For example, if you want to enable Facebook and Google sources, as well as the webhook integration, and the AKHQ tool, your `airy.yaml` file should look like this: ```yaml kubernetes: diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 614ccda3ef..7c725e3003 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -169,3 +169,13 @@ html[data-theme='dark'] .DocSearch { .markdown > table td { min-width: 160px; } + +/* Fix alert danger color */ +.alert--danger a, +.alert--danger { + color: #000; +} + +.alert--danger svg { + fill: #000; +} From 515f964a4847f6afa3762a0f906d70a3b01f8ed6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Aug 2021 10:44:22 +0200 Subject: [PATCH 03/33] Bump @typescript-eslint/eslint-plugin from 4.29.1 to 4.29.2 (#2296) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.29.1 to 4.29.2. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.29.2/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 60 ++++++++++++---------------------------------------- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index f4710584d1..6945fa40f8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@types/lodash-es": "^4.17.4", "@types/react-window-infinite-loader": "^1.0.4", "@types/resize-observer-browser": "^0.1.6", - "@typescript-eslint/eslint-plugin": "^4.29.1", + "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.2", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index 3c9d53b78a..349b5e4a57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1531,28 +1531,28 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz#808d206e2278e809292b5de752a91105da85860b" - integrity sha512-AHqIU+SqZZgBEiWOrtN94ldR3ZUABV5dUG94j8Nms9rQnHFc8fvDOue/58K4CFz6r8OtDDc35Pw9NQPWo0Ayrw== +"@typescript-eslint/eslint-plugin@^4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" + integrity sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg== dependencies: - "@typescript-eslint/experimental-utils" "4.29.1" - "@typescript-eslint/scope-manager" "4.29.1" + "@typescript-eslint/experimental-utils" "4.29.2" + "@typescript-eslint/scope-manager" "4.29.2" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.1.tgz#0af2b17b0296b60c6b207f11062119fa9c5a8994" - integrity sha512-kl6QG6qpzZthfd2bzPNSJB2YcZpNOrP6r9jueXupcZHnL74WiuSjaft7WSu17J9+ae9zTlk0KJMXPUj0daBxMw== +"@typescript-eslint/experimental-utils@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz#5f67fb5c5757ef2cb3be64817468ba35c9d4e3b7" + integrity sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.29.1" - "@typescript-eslint/types" "4.29.1" - "@typescript-eslint/typescript-estree" "4.29.1" + "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/typescript-estree" "4.29.2" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -1566,14 +1566,6 @@ "@typescript-eslint/typescript-estree" "4.29.2" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.1.tgz#f25da25bc6512812efa2ce5ebd36619d68e61358" - integrity sha512-Hzv/uZOa9zrD/W5mftZa54Jd5Fed3tL6b4HeaOpwVSabJK8CJ+2MkDasnX/XK4rqP5ZTWngK1ZDeCi6EnxPQ7A== - dependencies: - "@typescript-eslint/types" "4.29.1" - "@typescript-eslint/visitor-keys" "4.29.1" - "@typescript-eslint/scope-manager@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz#442b0f029d981fa402942715b1718ac7fcd5aa1b" @@ -1582,29 +1574,11 @@ "@typescript-eslint/types" "4.29.2" "@typescript-eslint/visitor-keys" "4.29.2" -"@typescript-eslint/types@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.1.tgz#94cce6cf7cc83451df03339cda99d326be2feaf5" - integrity sha512-Jj2yu78IRfw4nlaLtKjVaGaxh/6FhofmQ/j8v3NXmAiKafbIqtAPnKYrf0sbGjKdj0hS316J8WhnGnErbJ4RCA== - "@typescript-eslint/types@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== -"@typescript-eslint/typescript-estree@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.1.tgz#7b32a25ff8e51f2671ccc6b26cdbee3b1e6c5e7f" - integrity sha512-lIkkrR9E4lwZkzPiRDNq0xdC3f2iVCUjw/7WPJ4S2Sl6C3nRWkeE1YXCQ0+KsiaQRbpY16jNaokdWnm9aUIsfw== - dependencies: - "@typescript-eslint/types" "4.29.1" - "@typescript-eslint/visitor-keys" "4.29.1" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219" @@ -1618,14 +1592,6 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.29.1": - version "4.29.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.1.tgz#0615be8b55721f5e854f3ee99f1a714f2d093e5d" - integrity sha512-zLqtjMoXvgdZY/PG6gqA73V8BjqPs4af1v2kiiETBObp+uC6gRYnJLmJHxC0QyUrrHDLJPIWNYxoBV3wbcRlag== - dependencies: - "@typescript-eslint/types" "4.29.1" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df" From 15d381951de9fc0f5def72c2ef0b9a36ff5e026b Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Fri, 27 Aug 2021 12:02:09 +0200 Subject: [PATCH 04/33] [#2337] Fix webhook publisher crash for deleted messages (#2340) --- .../java/co/airy/core/webhook/publisher/Stores.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/webhook/publisher/src/main/java/co/airy/core/webhook/publisher/Stores.java b/backend/webhook/publisher/src/main/java/co/airy/core/webhook/publisher/Stores.java index 035eb4aa4f..754e51ea20 100644 --- a/backend/webhook/publisher/src/main/java/co/airy/core/webhook/publisher/Stores.java +++ b/backend/webhook/publisher/src/main/java/co/airy/core/webhook/publisher/Stores.java @@ -10,7 +10,6 @@ import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; import co.airy.kafka.schema.application.ApplicationCommunicationWebhooks; import co.airy.kafka.streams.KafkaStreamsWrapper; -import co.airy.log.AiryLoggerFactory; import co.airy.model.conversation.Conversation; import co.airy.model.event.payload.ChannelUpdated; import co.airy.model.event.payload.ConversationUpdated; @@ -22,13 +21,10 @@ import org.apache.kafka.streams.KafkaStreams; import org.apache.kafka.streams.KeyValue; import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.Topology; -import org.apache.kafka.streams.kstream.Consumed; import org.apache.kafka.streams.kstream.KStream; import org.apache.kafka.streams.kstream.KTable; import org.apache.kafka.streams.kstream.Materialized; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; -import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; @@ -83,7 +79,11 @@ private void startStream() { // message.updated messageTable.toStream() - .foreach((messageId, messageContainer) -> onRecord(MessageUpdated.fromMessageContainer(messageContainer))); + .foreach((messageId, messageContainer) -> { + if (messageContainer != null) { + onRecord(MessageUpdated.fromMessageContainer(messageContainer)); + } + }); // conversation.updated messageTable.groupBy((messageId, messageContainer) -> KeyValue.pair(messageContainer.getMessage().getConversationId(), messageContainer)) From 822c8e076b93e68f9beccc29b4abf829e2e0ec16 Mon Sep 17 00:00:00 2001 From: Steffen Date: Fri, 27 Aug 2021 12:05:38 +0200 Subject: [PATCH 05/33] Fixing typos in webhook docs (#2338) --- docs/docs/api/webhook.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/api/webhook.md b/docs/docs/api/webhook.md index f0f0e45f1d..eef0c2e8c6 100644 --- a/docs/docs/api/webhook.md +++ b/docs/docs/api/webhook.md @@ -154,7 +154,7 @@ request with one the following payloads: ```json5 { - "type": "message:created", + "type": "message.created", "payload": { "conversation_id": "{UUID}", "channel_id": "{UUID}", @@ -176,7 +176,7 @@ Sent whenever a message is updated (e.g. delivery state) or its [metadata](conce ```json5 { - "type": "message:created", + "type": "message.updated", "payload": { "conversation_id": "{UUID}", "channel_id": "{UUID}", @@ -204,7 +204,7 @@ Sent whenever a message is updated (e.g. delivery state) or its [metadata](conce ```json5 { - "type": "conversation:updated", + "type": "conversation.updated", "payload": { "id": "2e1da639-7152-4595-b43e-2117a55ac260", "created_at": "2020-10-25T21:24:54.560Z", // ISO 8601 date string From 2879fac3bbdb19ffc9a3aa82a05f46cdb184e122 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Fri, 27 Aug 2021 14:11:48 +0200 Subject: [PATCH 06/33] [#2243] Improve upgrade docs (#2339) --- docs/docs/getting-started/installation/aws.md | 4 ++-- docs/docs/getting-started/upgrade.md | 13 ++++++++++--- .../core/charts/upgrading/templates/0.29.0.yaml | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/docs/getting-started/installation/aws.md b/docs/docs/getting-started/installation/aws.md index 86c09f786c..83ec10d04e 100644 --- a/docs/docs/getting-started/installation/aws.md +++ b/docs/docs/getting-started/installation/aws.md @@ -209,8 +209,8 @@ Update the existing ingress resources with the new hostname (for this you will a ```sh kubectl get ingress airy-core -o json | jq "(.spec.rules[0].host=\"${HOSTNAME}\")" | kubectl apply -f - -kubectl get ingress airy-core-ui -o json | jq "(.spec.rules[0].host=\"${HOSTNAME}\")" | kubectl -f - -kubectl get ingress airy-core-redirect -o json | jq "(.spec.rules[0].host=\"${HOSTNAME}\")" | kubectl -f - +kubectl get ingress airy-core-ui -o json | jq "(.spec.rules[0].host=\"${HOSTNAME}\")" | kubectl apply -f - +kubectl get ingress airy-core-redirect -o json | jq "(.spec.rules[0].host=\"${HOSTNAME}\")" | kubectl apply -f - ``` #### Setup your DNS diff --git a/docs/docs/getting-started/upgrade.md b/docs/docs/getting-started/upgrade.md index 93864449d5..4c1e0dd2ed 100644 --- a/docs/docs/getting-started/upgrade.md +++ b/docs/docs/getting-started/upgrade.md @@ -28,11 +28,11 @@ Version: 0.29.0, GitCommit: b47d7e46c884a45c4c2169f626ebd0ff9ff6ee8e The upgrade of your Airy Core cluster will lead to downtime. Usually it takes seconds, but in case there is a migration or a reset of some of the streaming apps, this process might take longer, depending on the amount of data you have. -For more information about specific upgrades, or if you have any questions, please join the [Airy Developers Community on Slack](https://airy-developers.slack.com/). +Before you proceed, refer to the Release documentation for any upgrade notes or steps. ::: -Use the `airy upgrade` command to perform the upgrade of your Airy Core instance. Run `airy upgrade` to return information about your current Airy Core version and the latest version available. You will be prompted to proceed with the upgrade (omit this prompt by using the `--approve` flag). +The upgrade process will not delete any of the persistent data that is kept inside the Kafka cluster. Use the `airy upgrade` command to perform the upgrade of your Airy Core instance. Run `airy upgrade` to return information about your current Airy Core version and the latest version available. You will be prompted to proceed with the upgrade (omit this prompt by using the `--approve` flag). ```sh $ airy upgrade @@ -72,6 +72,7 @@ The upgrade will create few additional resources (Kubernetes jobs and configMaps To cleanup those resources, run: ```sh +kubectl delete pods -l job-name=helm-runner kubectl delete configmap -l core.airy.co/upgrade="true" kubectl delete job -l core.airy.co/upgrade="true" kubectl delete job -l core.airy.co/upgrade="post-upgrade" @@ -79,7 +80,13 @@ kubectl delete job -l core.airy.co/upgrade="post-upgrade" ## Rollback -The upgrade process will not delete any of the persistent data that is kept inside the Kafka cluster. In case the upgrade fails, you can rollback to your previous Airy Core version. Clean-up the upgrade resources as instructed in the previous section and run: +In case the upgrade fails, first you can inspect the reason for the failure by running this command: + +``` +kubectl logs -l job-name=helm-runner +``` + +After that, you can rollback to your previous Airy Core version. Clean-up the upgrade resources as instructed in the previous section and run: ```sh helm rollback airy diff --git a/infrastructure/helm-chart/charts/core/charts/upgrading/templates/0.29.0.yaml b/infrastructure/helm-chart/charts/core/charts/upgrading/templates/0.29.0.yaml index ad324bd792..decf0b4e53 100644 --- a/infrastructure/helm-chart/charts/core/charts/upgrading/templates/0.29.0.yaml +++ b/infrastructure/helm-chart/charts/core/charts/upgrading/templates/0.29.0.yaml @@ -13,7 +13,7 @@ metadata: data: check-existing-wehook.sh: | #!/bin/sh - system_token=${SYSTEM_TOKEN} + system_token=${systemToken} curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${system_token}" api-admin/webhooks.info -w "\n%{http_code}\n" > /tmp/response.txt cat /tmp/response.txt status_code=$(cat /tmp/response.txt | tail -n 1) @@ -37,7 +37,7 @@ data: subscribe-wehoook.sh: | #!/bin/sh - system_token=${SYSTEM_TOKEN} + system_token=${systemToken} webhookUsed=$(kubectl get configmap upgrade-{{ .Values.global.kubernetes.appImageTag }} -o jsonpath='{.data.webhookUsed}') echo "Webhook used: ${webhookUsed}" if [ "${webhookUsed}" = "true" ]; then From 2f9b1541eb0aa6105abf7fa45bee08489f68dd56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 10:42:26 +0200 Subject: [PATCH 07/33] Bump sass from 1.38.0 to 1.38.2 (#2342) Bumps [sass](https://github.com/sass/dart-sass) from 1.38.0 to 1.38.2. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.38.0...1.38.2) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6945fa40f8..ff7c92a18d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "minimist": "^1.2.5", "prettier": "^2.3.2", "react-hot-loader": "^4.13.0", - "sass": "^1.38.0", + "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.2.1", "terser-webpack-plugin": "^5.1.4", diff --git a/yarn.lock b/yarn.lock index 349b5e4a57..9808bc17a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6653,10 +6653,10 @@ sass-loader@^12.1.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.0.tgz#2f3e60a1efdcdc910586fa79dc89d3399a145b4f" - integrity sha512-WBccZeMigAGKoI+NgD7Adh0ab1HUq+6BmyBUEaGxtErbUtWUevEbdgo5EZiJQofLUGcKtlNaO2IdN73AHEua5g== +sass@^1.38.2: + version "1.38.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.2.tgz#970045d9966180002a8c8f3820fc114cddb42822" + integrity sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA== dependencies: chokidar ">=3.0.0 <4.0.0" From df752d792ae9d9ee759621b8170190b882170a5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 12:15:38 +0200 Subject: [PATCH 08/33] Bump react-router-dom from 5.2.0 to 5.2.1 (#2346) Bumps [react-router-dom](https://github.com/ReactTraining/react-router) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/ReactTraining/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) - [Commits](https://github.com/ReactTraining/react-router/compare/v5.2.0...v5.2.1) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index ff7c92a18d..f9c83ea1e2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "react-markdown": "^7.0.0", "react-modal": "^3.14.3", "react-redux": "7.2.4", - "react-router-dom": "5.2.0", + "react-router-dom": "5.2.1", "redux": "^4.1.1", "regenerator-runtime": "^0.13.9", "reselect": "4.0.0", diff --git a/yarn.lock b/yarn.lock index 9808bc17a3..912551077e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -975,10 +975,10 @@ "@babel/helper-validator-option" "^7.14.5" "@babel/plugin-transform-typescript" "^7.15.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" - integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" + integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== dependencies: regenerator-runtime "^0.13.4" @@ -6265,25 +6265,25 @@ react-redux@7.2.4: prop-types "^15.7.2" react-is "^16.13.1" -react-router-dom@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" - integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== +react-router-dom@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.1.tgz#34af8b551a4ce17487d3f80e651b91651978dff6" + integrity sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w== dependencies: - "@babel/runtime" "^7.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.2.0" + react-router "5.2.1" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== +react-router@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" + integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ== dependencies: - "@babel/runtime" "^7.1.2" + "@babel/runtime" "^7.12.13" history "^4.9.0" hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" From b46a9f27ad8f0effcc043cb47cb54b533902c0f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:49:05 +0200 Subject: [PATCH 09/33] Bump @stomp/stompjs from 6.1.0 to 6.1.1 (#2347) Bumps [@stomp/stompjs](https://github.com/stomp-js/stompjs) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/stomp-js/stompjs/releases) - [Commits](https://github.com/stomp-js/stompjs/commits) --- updated-dependencies: - dependency-name: "@stomp/stompjs" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f9c83ea1e2..cfe6b7a719 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "dependencies": { "@crello/react-lottie": "^0.0.11", "@reduxjs/toolkit": "^1.6.1", - "@stomp/stompjs": "^6.1.0", + "@stomp/stompjs": "^6.1.1", "@types/node": "16.7.1", "@types/react": "17.0.19", "@types/react-dom": "17.0.9", diff --git a/yarn.lock b/yarn.lock index 912551077e..dc30edf1f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1193,10 +1193,10 @@ redux-thunk "^2.3.0" reselect "^4.0.0" -"@stomp/stompjs@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-6.1.0.tgz#b77ca240775a7a34289b2c09618e256be761d142" - integrity sha512-Bt+whL0V51rSKP35OkgM1yeVu/usunpz5l1VYuoWw8mGsDMNd3W3FpWa4/4eis6QfXO3uteNGfyE21XWn0pGAQ== +"@stomp/stompjs@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-6.1.1.tgz#acfb07f0a4928a4bcdb3b525c2c07d1624ec1dde" + integrity sha512-MHSfchoe9NC93i7hyvmz4zsKux786aOWzWxg8guULIFx+yoB0hO9JcpOL2677oRPDP1Hdrscxsg2lg8tjkd3SA== "@svgr/babel-plugin-add-jsx-attribute@^5.4.0": version "5.4.0" From f960de988adf461dc8495f75d35574243e5443a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:49:17 +0200 Subject: [PATCH 10/33] Bump cypress from 8.3.0 to 8.3.1 (#2344) Bumps [cypress](https://github.com/cypress-io/cypress) from 8.3.0 to 8.3.1. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js) - [Commits](https://github.com/cypress-io/cypress/compare/v8.3.0...v8.3.1) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index cfe6b7a719..b92d121f09 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "babel-loader": "^8.0.6", "copy-webpack-plugin": "^9.0.1", "css-loader": "^6.2.0", - "cypress": "^8.3.0", + "cypress": "^8.3.1", "eslint": "^7.32.0", "eslint-plugin-react": "^7.24.0", "file-loader": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index dc30edf1f0..8cf5b2925f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1031,10 +1031,10 @@ dependencies: lottie-web "^5.7.3" -"@cypress/request@^2.88.5": - version "2.88.5" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" - integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== +"@cypress/request@^2.88.6": + version "2.88.6" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9" + integrity sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -1049,13 +1049,12 @@ isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" - oauth-sign "~0.9.0" performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" tough-cookie "~2.5.0" tunnel-agent "^0.6.0" - uuid "^3.3.2" + uuid "^8.3.2" "@cypress/xvfb@^1.2.4": version "1.2.4" @@ -2784,12 +2783,12 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== -cypress@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.3.0.tgz#ba906d2170888073ad94b2be1b994a749bbb7c7d" - integrity sha512-zA5Rcq8AZIfRfPXU0CCcauofF+YpaU9HYbfqkunFTmFV0Kdlo14tNjH2E3++MkjXKFnv3/pXq+HgxWtw8CSe8Q== +cypress@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.3.1.tgz#c6760dbb907df2570b0e1ac235fa31c30f9260a6" + integrity sha512-1v6pfx+/5cXhaT5T6QKOvnkawmEHWHLiVzm3MYMoQN1fkX2Ma1C32STd3jBStE9qT5qPSTILjGzypVRxCBi40g== dependencies: - "@cypress/request" "^2.88.5" + "@cypress/request" "^2.88.6" "@cypress/xvfb" "^1.2.4" "@types/node" "^14.14.31" "@types/sinonjs__fake-timers" "^6.0.2" @@ -5533,11 +5532,6 @@ nth-check@^2.0.0: dependencies: boolbase "^1.0.0" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -7697,6 +7691,11 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" From 8b91bd263182579a7bf1ada06d31618363d542bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:49:27 +0200 Subject: [PATCH 11/33] Bump eslint-plugin-react from 7.24.0 to 7.25.1 (#2343) Bumps [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) from 7.24.0 to 7.25.1. - [Release notes](https://github.com/yannickcr/eslint-plugin-react/releases) - [Changelog](https://github.com/yannickcr/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/yannickcr/eslint-plugin-react/compare/v7.24.0...v7.25.1) --- updated-dependencies: - dependency-name: eslint-plugin-react dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b92d121f09..397afee6d3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "css-loader": "^6.2.0", "cypress": "^8.3.1", "eslint": "^7.32.0", - "eslint-plugin-react": "^7.24.0", + "eslint-plugin-react": "^7.25.1", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.3.2", "minimist": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index 8cf5b2925f..70ac48a26c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3252,14 +3252,15 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-react@^7.24.0: - version "7.24.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz#eadedfa351a6f36b490aa17f4fa9b14e842b9eb4" - integrity sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q== +eslint-plugin-react@^7.25.1: + version "7.25.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz#9286b7cd9bf917d40309760f403e53016eda8331" + integrity sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug== dependencies: array-includes "^3.1.3" array.prototype.flatmap "^1.2.4" doctrine "^2.1.0" + estraverse "^5.2.0" has "^1.0.3" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.0.4" From 642d855cd1253114a02a6046c230c9253266a933 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:03:10 +0200 Subject: [PATCH 12/33] Bump @typescript-eslint/parser from 4.29.2 to 4.29.3 (#2350) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.29.2 to 4.29.3. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.29.3/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 397afee6d3..9f8241d78c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@types/react-window-infinite-loader": "^1.0.4", "@types/resize-observer-browser": "^0.1.6", "@typescript-eslint/eslint-plugin": "^4.29.2", - "@typescript-eslint/parser": "^4.29.2", + "@typescript-eslint/parser": "^4.29.3", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^9.0.1", "css-loader": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index 70ac48a26c..ead15fd257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1555,14 +1555,14 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.2.tgz#1c7744f4c27aeb74610c955d3dce9250e95c370a" - integrity sha512-WQ6BPf+lNuwteUuyk1jD/aHKqMQ9jrdCn7Gxt9vvBnzbpj7aWEf+aZsJ1zvTjx5zFxGCt000lsbD9tQPEL8u6g== - dependencies: - "@typescript-eslint/scope-manager" "4.29.2" - "@typescript-eslint/types" "4.29.2" - "@typescript-eslint/typescript-estree" "4.29.2" +"@typescript-eslint/parser@^4.29.3": + version "4.29.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.3.tgz#2ac25535f34c0e98f50c0e6b28c679c2357d45f2" + integrity sha512-jrHOV5g2u8ROghmspKoW7pN8T/qUzk0+DITun0MELptvngtMrwUJ1tv5zMI04CYVEUsSrN4jV7AKSv+I0y0EfQ== + dependencies: + "@typescript-eslint/scope-manager" "4.29.3" + "@typescript-eslint/types" "4.29.3" + "@typescript-eslint/typescript-estree" "4.29.3" debug "^4.3.1" "@typescript-eslint/scope-manager@4.29.2": @@ -1573,11 +1573,24 @@ "@typescript-eslint/types" "4.29.2" "@typescript-eslint/visitor-keys" "4.29.2" +"@typescript-eslint/scope-manager@4.29.3": + version "4.29.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.3.tgz#497dec66f3a22e459f6e306cf14021e40ec86e19" + integrity sha512-x+w8BLXO7iWPkG5mEy9bA1iFRnk36p/goVlYobVWHyDw69YmaH9q6eA+Fgl7kYHmFvWlebUTUfhtIg4zbbl8PA== + dependencies: + "@typescript-eslint/types" "4.29.3" + "@typescript-eslint/visitor-keys" "4.29.3" + "@typescript-eslint/types@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== +"@typescript-eslint/types@4.29.3": + version "4.29.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.3.tgz#d7980c49aef643d0af8954c9f14f656b7fd16017" + integrity sha512-s1eV1lKNgoIYLAl1JUba8NhULmf+jOmmeFO1G5MN/RBCyyzg4TIOfIOICVNC06lor+Xmy4FypIIhFiJXOknhIg== + "@typescript-eslint/typescript-estree@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219" @@ -1591,6 +1604,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@4.29.3": + version "4.29.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.3.tgz#1bafad610015c4ded35c85a70b6222faad598b40" + integrity sha512-45oQJA0bxna4O5TMwz55/TpgjX1YrAPOI/rb6kPgmdnemRZx/dB0rsx+Ku8jpDvqTxcE1C/qEbVHbS3h0hflag== + dependencies: + "@typescript-eslint/types" "4.29.3" + "@typescript-eslint/visitor-keys" "4.29.3" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/visitor-keys@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df" @@ -1599,6 +1625,14 @@ "@typescript-eslint/types" "4.29.2" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.29.3": + version "4.29.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.3.tgz#c691760a00bd86bf8320d2a90a93d86d322f1abf" + integrity sha512-MGGfJvXT4asUTeVs0Q2m+sY63UsfnA+C/FDgBKV3itLBmM9H0u+URcneePtkd0at1YELmZK6HSolCqM4Fzs6yA== + dependencies: + "@typescript-eslint/types" "4.29.3" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" From 934b9e1b55fabf7b1cd06b394ea4e5041c9e8245 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:25:21 +0200 Subject: [PATCH 13/33] Bump core-js from 3.16.2 to 3.16.4 (#2357) Bumps [core-js](https://github.com/zloirock/core-js) from 3.16.2 to 3.16.4. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.16.2...v3.16.4) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9f8241d78c..c7a8c7fb73 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@types/react-redux": "7.1.18", "@types/react-router-dom": "^5.1.8", "camelcase-keys": "^7.0.0", - "core-js": "3.16.2", + "core-js": "3.16.4", "emoji-mart": "3.0.1", "linkifyjs": "^2.1.9", "lodash-es": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index ead15fd257..be05a8e761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2693,10 +2693,10 @@ core-js-compat@^3.16.0, core-js-compat@^3.9.1: browserslist "^4.16.6" semver "7.0.0" -core-js@3.16.2: - version "3.16.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.2.tgz#3f485822889c7fc48ef463e35be5cc2a4a01a1f4" - integrity sha512-P0KPukO6OjMpjBtHSceAZEWlDD1M2Cpzpg6dBbrjFqFhBHe/BwhxaP820xKOjRn/lZRQirrCusIpLS/n2sgXLQ== +core-js@3.16.4: + version "3.16.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.4.tgz#0fb1029a554fc2688c0963d7c900e188188a78e0" + integrity sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" From f6366d5c183ff062ebce7ffce392103097f87b09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:25:53 +0200 Subject: [PATCH 14/33] Bump @types/react-window-infinite-loader from 1.0.4 to 1.0.5 (#2348) Bumps [@types/react-window-infinite-loader](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-window-infinite-loader) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-window-infinite-loader) --- updated-dependencies: - dependency-name: "@types/react-window-infinite-loader" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c7a8c7fb73..620342da0f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@bazel/typescript": "^3.6.0", "@svgr/webpack": "^5.5.0", "@types/lodash-es": "^4.17.4", - "@types/react-window-infinite-loader": "^1.0.4", + "@types/react-window-infinite-loader": "^1.0.5", "@types/resize-observer-browser": "^0.1.6", "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.3", diff --git a/yarn.lock b/yarn.lock index be05a8e761..089025ba9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1474,10 +1474,10 @@ "@types/history" "*" "@types/react" "*" -"@types/react-window-infinite-loader@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.4.tgz#2130555d3037f842e6fff939a6563849fd4d1046" - integrity sha512-bfztJr6udQ/8+/KsomCDOh/bWQv9uQZzpvPwhZC/izKuVdilH+yv/ql4m9Dql5DgbomGiKPzphaZrqIf2ApyTA== +"@types/react-window-infinite-loader@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.5.tgz#32469a6a47438d9ebf1b5f5719943f494ea0abdf" + integrity sha512-3v45+4oBNJpSroULtb2EgTJyyK4pCjOMCg+RT/HnA3or5zY4jYufv6uH9NWPyLv0nx8dqt1s4nJqHilfthwKSw== dependencies: "@types/react" "*" "@types/react-window" "*" From 7d04de10a7364b9b3c0868ee1edc50e1e7e8e844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:28:48 +0200 Subject: [PATCH 15/33] Bump tar from 6.1.4 to 6.1.11 in /docs (#2359) Bumps [tar](https://github.com/npm/node-tar) from 6.1.4 to 6.1.11. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v6.1.4...v6.1.11) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 2ae1eb233c..554144b26e 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -9192,9 +9192,9 @@ tapable@^1.0.0, tapable@^1.1.3: integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar@^6.0.2: - version "6.1.4" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.4.tgz#9f0722b772a5e00dba7d52e1923b37a7ec3799b3" - integrity sha512-kcPWrO8S5ABjuZ/v1xQHP8xCEvj1dQ1d9iAb6Qs4jLYzaAIYWwST2IQpz7Ud8VNYRI+fGhFjrnzRKmRggKWg3g== + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" From 0dd96382fe3a35f7d3accdb124bcb2bf7ff26b89 Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Wed, 1 Sep 2021 11:54:01 +0200 Subject: [PATCH 16/33] [#2229] instagram source render story mention in inbox UI (#2363) * added story mention for Instagram * fixed linting * linting * linting * fixed linting * moved date function to lib --- .../assets/images/icons/external-link.svg | 1 + lib/typescript/dates/convert.ts | 4 ++ lib/typescript/dates/index.ts | 1 + .../providers/facebook/FacebookRender.tsx | 22 ++++++++++ .../InstagramStoryMention/index.module.scss | 43 +++++++++++++++++++ .../InstagramStoryMention/index.tsx | 30 +++++++++++++ .../providers/facebook/facebookModel.ts | 8 ++++ 7 files changed, 109 insertions(+) create mode 100644 lib/typescript/assets/images/icons/external-link.svg create mode 100644 lib/typescript/dates/convert.ts create mode 100644 lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss create mode 100644 lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx diff --git a/lib/typescript/assets/images/icons/external-link.svg b/lib/typescript/assets/images/icons/external-link.svg new file mode 100644 index 0000000000..afb9cb5f3a --- /dev/null +++ b/lib/typescript/assets/images/icons/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/typescript/dates/convert.ts b/lib/typescript/dates/convert.ts new file mode 100644 index 0000000000..7e6dd873f3 --- /dev/null +++ b/lib/typescript/dates/convert.ts @@ -0,0 +1,4 @@ +export const timeElapsedInHours = (sentAt: Date) => { + const millisecondsDiff = new Date().getTime() - new Date(sentAt).getTime(); + return (millisecondsDiff / (1000 * 60 * 60)) % 24; +}; diff --git a/lib/typescript/dates/index.ts b/lib/typescript/dates/index.ts index ac10189a8b..a07417ff69 100644 --- a/lib/typescript/dates/index.ts +++ b/lib/typescript/dates/index.ts @@ -1,2 +1,3 @@ export * from './format'; export * from './compare'; +export * from './convert'; diff --git a/lib/typescript/render/providers/facebook/FacebookRender.tsx b/lib/typescript/render/providers/facebook/FacebookRender.tsx index 530f349cb4..233cf2fb94 100644 --- a/lib/typescript/render/providers/facebook/FacebookRender.tsx +++ b/lib/typescript/render/providers/facebook/FacebookRender.tsx @@ -16,6 +16,7 @@ import {ButtonTemplate} from './components/ButtonTemplate'; import {GenericTemplate} from './components/GenericTemplate'; import {MediaTemplate} from './components/MediaTemplate'; import {FallbackAttachment} from './components/FallbackAttachment'; +import {StoryMention} from './components/InstagramStoryMention'; export const FacebookRender = (props: RenderPropsUnion) => { const message = props.message; @@ -62,6 +63,11 @@ function render(content: ContentUnion, props: RenderPropsUnion) { /> ); + case 'story_mention': + return ( + + ); + default: return null; } @@ -142,6 +148,14 @@ function facebookInbound(message): ContentUnion { }; } + if (messageJson.attachments?.[0].type === 'story_mention') { + return { + type: 'story_mention', + url: messageJson.attachment?.payload?.url || messageJson.attachments[0]?.payload?.url || null, + sentAt: message.sentAt, + }; + } + if (messageJson.attachment || messageJson.attachments) { return parseAttachment(messageJson.attachment || messageJson.attachments[0]); } @@ -190,6 +204,14 @@ function facebookOutbound(message): ContentUnion { }; } + if (messageJson.attachments?.[0].type === 'story_mention') { + return { + type: 'story_mention', + url: messageJson.attachment.url || messageJson.attachments[0].url, + sentAt: messageJson.sentAt, + }; + } + if (messageJson.attachment?.type === 'fallback' || messageJson.attachments?.[0].type === 'fallback') { return { text: messageJson.text ?? null, diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss b/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss new file mode 100644 index 0000000000..c125d75bdd --- /dev/null +++ b/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.module.scss @@ -0,0 +1,43 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.textMessage { + display: inline-block; + overflow-wrap: break-word; + max-width: 500px; + word-break: break-word; + white-space: pre-wrap; + padding: 10px; + margin-top: 5px; + border-radius: 8px; + font-family: 'Lato', sans-serif; +} + +.activeStory { + display: flex; + align-items: center; +} + +.activeStoryLink:link, +.activeStoryLink:hover, +.activeStoryLink:visited, +.activeStoryLink:active { + color: var(--color-text-contrast); + font-style: italic; +} + +.expiredStory { + font-style: italic; +} + +.contactContent { + @extend .textMessage; + background: var(--color-background-blue); + color: var(--color-text-contrast); +} + +.memberContent { + @extend .textMessage; + background: var(--color-airy-blue); + color: white; +} diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx b/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx new file mode 100644 index 0000000000..62cdd13fa6 --- /dev/null +++ b/lib/typescript/render/providers/facebook/components/InstagramStoryMention/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as LinkIcon} from 'assets/images/icons/external-link.svg'; +import {timeElapsedInHours} from 'dates'; + +type StoryMentionProps = { + url: string; + sentAt: Date; + fromContact: boolean; +}; + +export const StoryMention = ({url, sentAt, fromContact}: StoryMentionProps) => { + return ( + <> +
+ {timeElapsedInHours(sentAt) <= 24 ? ( + <> + + + ) : ( + mentioned in an expired Instagram story + )} +
+ + ); +}; diff --git a/lib/typescript/render/providers/facebook/facebookModel.ts b/lib/typescript/render/providers/facebook/facebookModel.ts index 98e221f67d..da92c9892c 100644 --- a/lib/typescript/render/providers/facebook/facebookModel.ts +++ b/lib/typescript/render/providers/facebook/facebookModel.ts @@ -148,6 +148,12 @@ export interface Fallback extends Content { url: string; } +export interface StoryMentionContent extends Content { + type: 'story_mention'; + url: string; + sentAt: Date; +} + // Add a new facebook content model here: export type ContentUnion = | TextContent @@ -159,6 +165,7 @@ export type ContentUnion = | GenericTemplate | QuickRepliesContent | MediaTemplate + | StoryMentionContent | Fallback; export type AttachmentUnion = @@ -168,4 +175,5 @@ export type AttachmentUnion = | ButtonTemplate | GenericTemplate | MediaTemplate + | StoryMentionContent | Fallback; From 79de3a7ff267e4e777a7a3155549c64aeedd8839 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:33:17 +0200 Subject: [PATCH 17/33] Bump @types/node from 16.7.1 to 16.7.10 (#2362) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.7.1 to 16.7.10. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 620342da0f..6485ea6a69 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@crello/react-lottie": "^0.0.11", "@reduxjs/toolkit": "^1.6.1", "@stomp/stompjs": "^6.1.1", - "@types/node": "16.7.1", + "@types/node": "16.7.10", "@types/react": "17.0.19", "@types/react-dom": "17.0.9", "@types/react-redux": "7.1.18", diff --git a/yarn.lock b/yarn.lock index 089025ba9f..c7c6fc20d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1410,10 +1410,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@16.7.1": - version "16.7.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.1.tgz#c6b9198178da504dfca1fd0be9b2e1002f1586f0" - integrity sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A== +"@types/node@*", "@types/node@16.7.10": + version "16.7.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc" + integrity sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA== "@types/node@^10.1.0": version "10.17.55" From c84327a5fa270db0f2cbbfd209bebfff9b748d35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:33:26 +0200 Subject: [PATCH 18/33] Bump @typescript-eslint/parser from 4.29.3 to 4.30.0 (#2361) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.29.3 to 4.30.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.30.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 6485ea6a69..9670b185cf 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@types/react-window-infinite-loader": "^1.0.5", "@types/resize-observer-browser": "^0.1.6", "@typescript-eslint/eslint-plugin": "^4.29.2", - "@typescript-eslint/parser": "^4.29.3", + "@typescript-eslint/parser": "^4.30.0", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^9.0.1", "css-loader": "^6.2.0", diff --git a/yarn.lock b/yarn.lock index c7c6fc20d9..5c3d5f29ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1555,14 +1555,14 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.29.3": - version "4.29.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.3.tgz#2ac25535f34c0e98f50c0e6b28c679c2357d45f2" - integrity sha512-jrHOV5g2u8ROghmspKoW7pN8T/qUzk0+DITun0MELptvngtMrwUJ1tv5zMI04CYVEUsSrN4jV7AKSv+I0y0EfQ== - dependencies: - "@typescript-eslint/scope-manager" "4.29.3" - "@typescript-eslint/types" "4.29.3" - "@typescript-eslint/typescript-estree" "4.29.3" +"@typescript-eslint/parser@^4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.30.0.tgz#6abd720f66bd790f3e0e80c3be77180c8fcb192d" + integrity sha512-HJ0XuluSZSxeboLU7Q2VQ6eLlCwXPBOGnA7CqgBnz2Db3JRQYyBDJgQnop6TZ+rsbSx5gEdWhw4rE4mDa1FnZg== + dependencies: + "@typescript-eslint/scope-manager" "4.30.0" + "@typescript-eslint/types" "4.30.0" + "@typescript-eslint/typescript-estree" "4.30.0" debug "^4.3.1" "@typescript-eslint/scope-manager@4.29.2": @@ -1573,23 +1573,23 @@ "@typescript-eslint/types" "4.29.2" "@typescript-eslint/visitor-keys" "4.29.2" -"@typescript-eslint/scope-manager@4.29.3": - version "4.29.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.3.tgz#497dec66f3a22e459f6e306cf14021e40ec86e19" - integrity sha512-x+w8BLXO7iWPkG5mEy9bA1iFRnk36p/goVlYobVWHyDw69YmaH9q6eA+Fgl7kYHmFvWlebUTUfhtIg4zbbl8PA== +"@typescript-eslint/scope-manager@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.30.0.tgz#1a3ffbb385b1a06be85cd5165a22324f069a85ee" + integrity sha512-VJ/jAXovxNh7rIXCQbYhkyV2Y3Ac/0cVHP/FruTJSAUUm4Oacmn/nkN5zfWmWFEanN4ggP0vJSHOeajtHq3f8A== dependencies: - "@typescript-eslint/types" "4.29.3" - "@typescript-eslint/visitor-keys" "4.29.3" + "@typescript-eslint/types" "4.30.0" + "@typescript-eslint/visitor-keys" "4.30.0" "@typescript-eslint/types@4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== -"@typescript-eslint/types@4.29.3": - version "4.29.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.3.tgz#d7980c49aef643d0af8954c9f14f656b7fd16017" - integrity sha512-s1eV1lKNgoIYLAl1JUba8NhULmf+jOmmeFO1G5MN/RBCyyzg4TIOfIOICVNC06lor+Xmy4FypIIhFiJXOknhIg== +"@typescript-eslint/types@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.30.0.tgz#fb9d9b0358426f18687fba82eb0b0f869780204f" + integrity sha512-YKldqbNU9K4WpTNwBqtAerQKLLW/X2A/j4yw92e3ZJYLx+BpKLeheyzoPfzIXHfM8BXfoleTdiYwpsvVPvHrDw== "@typescript-eslint/typescript-estree@4.29.2": version "4.29.2" @@ -1604,13 +1604,13 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@4.29.3": - version "4.29.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.3.tgz#1bafad610015c4ded35c85a70b6222faad598b40" - integrity sha512-45oQJA0bxna4O5TMwz55/TpgjX1YrAPOI/rb6kPgmdnemRZx/dB0rsx+Ku8jpDvqTxcE1C/qEbVHbS3h0hflag== +"@typescript-eslint/typescript-estree@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.30.0.tgz#ae57833da72a753f4846cd3053758c771670c2ac" + integrity sha512-6WN7UFYvykr/U0Qgy4kz48iGPWILvYL34xXJxvDQeiRE018B7POspNRVtAZscWntEPZpFCx4hcz/XBT+erenfg== dependencies: - "@typescript-eslint/types" "4.29.3" - "@typescript-eslint/visitor-keys" "4.29.3" + "@typescript-eslint/types" "4.30.0" + "@typescript-eslint/visitor-keys" "4.30.0" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" @@ -1625,12 +1625,12 @@ "@typescript-eslint/types" "4.29.2" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@4.29.3": - version "4.29.3" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.3.tgz#c691760a00bd86bf8320d2a90a93d86d322f1abf" - integrity sha512-MGGfJvXT4asUTeVs0Q2m+sY63UsfnA+C/FDgBKV3itLBmM9H0u+URcneePtkd0at1YELmZK6HSolCqM4Fzs6yA== +"@typescript-eslint/visitor-keys@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.30.0.tgz#a47c6272fc71b0c627d1691f68eaecf4ad71445e" + integrity sha512-pNaaxDt/Ol/+JZwzP7MqWc8PJQTUhZwoee/PVlQ+iYoYhagccvoHnC9e4l+C/krQYYkENxznhVSDwClIbZVxRw== dependencies: - "@typescript-eslint/types" "4.29.3" + "@typescript-eslint/types" "4.30.0" eslint-visitor-keys "^2.0.0" "@webassemblyjs/ast@1.11.1": From 625097c2726402af0cc724b88dc1dc91ab429978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 15:10:28 +0200 Subject: [PATCH 19/33] Bump react-markdown from 7.0.0 to 7.0.1 (#2365) Bumps [react-markdown](https://github.com/remarkjs/react-markdown) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/remarkjs/react-markdown/releases) - [Changelog](https://github.com/remarkjs/react-markdown/blob/main/changelog.md) - [Commits](https://github.com/remarkjs/react-markdown/compare/7.0.0...7.0.1) --- updated-dependencies: - dependency-name: react-markdown dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9670b185cf..5ace57c3cc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "react-autosize-textarea": "^7.1.0", "react-color": "^2.19.3", "react-dom": "16.14.0", - "react-markdown": "^7.0.0", + "react-markdown": "^7.0.1", "react-modal": "^3.14.3", "react-redux": "7.2.4", "react-router-dom": "5.2.1", diff --git a/yarn.lock b/yarn.lock index 5c3d5f29ae..4c0606f54e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6253,10 +6253,10 @@ react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-markdown@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-7.0.0.tgz#66565968e8513af070eeef65abc77e80fc1a9b87" - integrity sha512-qdWfKxMgdKF3kHAV5pmcB12fAvytPoTpYwKTO6O/I3HujrK7sKIv6j4RnXVNLrNUh+TaBk+KtqpGzIKslX2rDg== +react-markdown@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-7.0.1.tgz#c7365fcd7d1813b3ae68f2200e8f92d47d865627" + integrity sha512-pthNPaoiwg0q7hukoE04F2ENwSzijIlWHJ4UMs/96LUe/G/P3FnbP4qHzx3FoNqae+2SqDG8vzniTLnJDeWneg== dependencies: "@types/hast" "^2.0.0" "@types/unist" "^2.0.0" From 963064e11f5e768cec11ec1918ae0c5e1f315d14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 15:10:45 +0200 Subject: [PATCH 20/33] Bump @typescript-eslint/eslint-plugin from 4.29.2 to 4.30.0 (#2356) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.29.2 to 4.30.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.30.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 60 ++++++++++++---------------------------------------- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 5ace57c3cc..100aff01ba 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@types/lodash-es": "^4.17.4", "@types/react-window-infinite-loader": "^1.0.5", "@types/resize-observer-browser": "^0.1.6", - "@typescript-eslint/eslint-plugin": "^4.29.2", + "@typescript-eslint/eslint-plugin": "^4.30.0", "@typescript-eslint/parser": "^4.30.0", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index 4c0606f54e..46e515f3c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1530,28 +1530,28 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" - integrity sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg== +"@typescript-eslint/eslint-plugin@^4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.30.0.tgz#4a0c1ae96b953f4e67435e20248d812bfa55e4fb" + integrity sha512-NgAnqk55RQ/SD+tZFD9aPwNSeHmDHHe5rtUyhIq0ZeCWZEvo4DK9rYz7v9HDuQZFvn320Ot+AikaCKMFKLlD0g== dependencies: - "@typescript-eslint/experimental-utils" "4.29.2" - "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/experimental-utils" "4.30.0" + "@typescript-eslint/scope-manager" "4.30.0" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz#5f67fb5c5757ef2cb3be64817468ba35c9d4e3b7" - integrity sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A== +"@typescript-eslint/experimental-utils@4.30.0": + version "4.30.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.30.0.tgz#9e49704fef568432ae16fc0d6685c13d67db0fd5" + integrity sha512-K8RNIX9GnBsv5v4TjtwkKtqMSzYpjqAQg/oSphtxf3xxdt6T0owqnpojztjjTcatSteH3hLj3t/kklKx87NPqw== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.29.2" - "@typescript-eslint/types" "4.29.2" - "@typescript-eslint/typescript-estree" "4.29.2" + "@typescript-eslint/scope-manager" "4.30.0" + "@typescript-eslint/types" "4.30.0" + "@typescript-eslint/typescript-estree" "4.30.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -1565,14 +1565,6 @@ "@typescript-eslint/typescript-estree" "4.30.0" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz#442b0f029d981fa402942715b1718ac7fcd5aa1b" - integrity sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA== - dependencies: - "@typescript-eslint/types" "4.29.2" - "@typescript-eslint/visitor-keys" "4.29.2" - "@typescript-eslint/scope-manager@4.30.0": version "4.30.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.30.0.tgz#1a3ffbb385b1a06be85cd5165a22324f069a85ee" @@ -1581,29 +1573,11 @@ "@typescript-eslint/types" "4.30.0" "@typescript-eslint/visitor-keys" "4.30.0" -"@typescript-eslint/types@4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" - integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== - "@typescript-eslint/types@4.30.0": version "4.30.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.30.0.tgz#fb9d9b0358426f18687fba82eb0b0f869780204f" integrity sha512-YKldqbNU9K4WpTNwBqtAerQKLLW/X2A/j4yw92e3ZJYLx+BpKLeheyzoPfzIXHfM8BXfoleTdiYwpsvVPvHrDw== -"@typescript-eslint/typescript-estree@4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219" - integrity sha512-TJ0/hEnYxapYn9SGn3dCnETO0r+MjaxtlWZ2xU+EvytF0g4CqTpZL48SqSNn2hXsPolnewF30pdzR9a5Lj3DNg== - dependencies: - "@typescript-eslint/types" "4.29.2" - "@typescript-eslint/visitor-keys" "4.29.2" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@4.30.0": version "4.30.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.30.0.tgz#ae57833da72a753f4846cd3053758c771670c2ac" @@ -1617,14 +1591,6 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.29.2": - version "4.29.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df" - integrity sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag== - dependencies: - "@typescript-eslint/types" "4.29.2" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@4.30.0": version "4.30.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.30.0.tgz#a47c6272fc71b0c627d1691f68eaecf4ad71445e" From 51604fc8dd05e4cc0a37bd82ecb16f5820505322 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 17:51:48 +0200 Subject: [PATCH 21/33] Bump terser-webpack-plugin from 5.1.4 to 5.2.0 (#2364) Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.1.4 to 5.2.0. - [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases) - [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.1.4...v5.2.0) --- updated-dependencies: - dependency-name: terser-webpack-plugin dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 100aff01ba..e6be32b02b 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "sass": "^1.38.2", "sass-loader": "^12.1.0", "style-loader": "^3.2.1", - "terser-webpack-plugin": "^5.1.4", + "terser-webpack-plugin": "^5.2.0", "typescript": "4.2.4", "url-loader": "^4.1.1", "webpack": "5.51.1", diff --git a/yarn.lock b/yarn.lock index 46e515f3c6..f2e1b4ab95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4670,10 +4670,10 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -jest-worker@^27.0.2: - version "27.0.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.2.tgz#4ebeb56cef48b3e7514552f80d0d80c0129f0b05" - integrity sha512-EoBdilOTTyOgmHXtw/cPc+ZrCA0KJMrkXzkrPGNwLmnvvlN1nj7MPrxpT7m+otSv2e1TLaVffzDnE/LB14zJMg== +jest-worker@^27.0.6: + version "27.1.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.1.0.tgz#65f4a88e37148ed984ba8ca8492d6b376938c0aa" + integrity sha512-mO4PHb2QWLn9yRXGp7rkvXLAYuxwhq1ZYUo0LoDhg8wqvv4QizP1ZWEJOeolgbEgAWZLIEU0wsku8J+lGWfBhg== dependencies: "@types/node" "*" merge-stream "^2.0.0" @@ -6686,7 +6686,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0: +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -7245,17 +7245,17 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1" - integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA== +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.0.tgz#694c54fcdfa5f5cb2ceaf31929e7535b32a8a50c" + integrity sha512-FpR4Qe0Yt4knSQ5u2bA1wkM0R8VlVsvhyfSHvomXRivS4vPLk0dJV2IhRBIHRABh7AFutdMeElIA5y1dETwMBg== dependencies: - jest-worker "^27.0.2" + jest-worker "^27.0.6" p-limit "^3.1.0" - schema-utils "^3.0.0" + schema-utils "^3.1.1" serialize-javascript "^6.0.0" source-map "^0.6.1" - terser "^5.7.0" + terser "^5.7.2" terser@^4.6.3: version "4.8.0" @@ -7266,10 +7266,10 @@ terser@^4.6.3: source-map "~0.6.1" source-map-support "~0.5.12" -terser@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" - integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== +terser@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f" + integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw== dependencies: commander "^2.20.0" source-map "~0.7.2" From c37460a62a4aa9f87a2632a6ecac53352dac9bfd Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Thu, 2 Sep 2021 11:01:14 +0200 Subject: [PATCH 22/33] [#2227] instagram source show story reply in inbox UI (#2367) * story replies added * finished instagram story reply --- .../assets/images/icons/play-circle.svg | 4 ++ lib/typescript/dates/convert.ts | 17 ++++- .../providers/facebook/FacebookRender.tsx | 30 +++++++++ .../InstagramStoryReplies/index.module.scss | 65 +++++++++++++++++++ .../InstagramStoryReplies/index.tsx | 41 ++++++++++++ .../providers/facebook/facebookModel.ts | 7 ++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 lib/typescript/assets/images/icons/play-circle.svg create mode 100644 lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss create mode 100644 lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx diff --git a/lib/typescript/assets/images/icons/play-circle.svg b/lib/typescript/assets/images/icons/play-circle.svg new file mode 100644 index 0000000000..c93144ab06 --- /dev/null +++ b/lib/typescript/assets/images/icons/play-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/typescript/dates/convert.ts b/lib/typescript/dates/convert.ts index 7e6dd873f3..6e51f1cd61 100644 --- a/lib/typescript/dates/convert.ts +++ b/lib/typescript/dates/convert.ts @@ -1,4 +1,19 @@ export const timeElapsedInHours = (sentAt: Date) => { const millisecondsDiff = new Date().getTime() - new Date(sentAt).getTime(); - return (millisecondsDiff / (1000 * 60 * 60)) % 24; + const seconds = (millisecondsDiff / 1000).toFixed(0); + let minutes = Math.floor(Number(seconds) / 60).toString(); + let hours; + if (Number(minutes) > 59) { + hours = Math.floor(Number(minutes) / 60); + hours = hours >= 10 ? hours : '0' + hours; + minutes = (Number(minutes) - hours * 60).toString(); + minutes = Number(minutes) >= 10 ? minutes : '0' + minutes; + } + if (!hours) { + hours = '00'; + } + if (!minutes) { + minutes = '00'; + } + return parseFloat(hours + '.' + minutes); }; diff --git a/lib/typescript/render/providers/facebook/FacebookRender.tsx b/lib/typescript/render/providers/facebook/FacebookRender.tsx index 233cf2fb94..9afad78bc4 100644 --- a/lib/typescript/render/providers/facebook/FacebookRender.tsx +++ b/lib/typescript/render/providers/facebook/FacebookRender.tsx @@ -17,6 +17,7 @@ import {GenericTemplate} from './components/GenericTemplate'; import {MediaTemplate} from './components/MediaTemplate'; import {FallbackAttachment} from './components/FallbackAttachment'; import {StoryMention} from './components/InstagramStoryMention'; +import {StoryReplies} from './components/InstagramStoryReplies'; export const FacebookRender = (props: RenderPropsUnion) => { const message = props.message; @@ -63,11 +64,22 @@ function render(content: ContentUnion, props: RenderPropsUnion) { /> ); + //Instagram-specific case 'story_mention': return ( ); + case 'story_replies': + return ( + + ); + default: return null; } @@ -156,6 +168,15 @@ function facebookInbound(message): ContentUnion { }; } + if (messageJson.reply_to) { + return { + type: 'story_replies', + text: messageJson.text, + url: messageJson.reply_to?.story?.url, + sentAt: message.sentAt, + }; + } + if (messageJson.attachment || messageJson.attachments) { return parseAttachment(messageJson.attachment || messageJson.attachments[0]); } @@ -204,6 +225,15 @@ function facebookOutbound(message): ContentUnion { }; } + if (messageJson.reply_to) { + return { + type: 'story_replies', + text: messageJson.text, + url: messageJson.reply_to?.story?.url, + sentAt: message.sentAt, + }; + } + if (messageJson.attachments?.[0].type === 'story_mention') { return { type: 'story_mention', diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss new file mode 100644 index 0000000000..c5bf313e53 --- /dev/null +++ b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss @@ -0,0 +1,65 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.textMessage { + display: inline-block; + overflow-wrap: break-word; + max-width: 500px; + word-break: break-word; + white-space: pre-wrap; + padding: 10px; + margin-top: 5px; + border-radius: 8px; + font-family: 'Lato', sans-serif; +} + +.contactContent { + @extend .textMessage; + background: var(--color-background-blue); + color: var(--color-text-contrast); +} + +.memberContent { + @extend .textMessage; + background: var(--color-airy-blue); + color: white; +} + +.container { + margin-top: 10px; +} + +.storyReply { + display: flex; + align-items: center; + @include font-s; +} + +.storyResponse, +.expiredStory { + color: var(--color-text-gray); +} + +.storyLink { + display: 'flex'; + align-items: center; + justify-content: center; + display: inline-block; +} + +.icon { + margin-right: 2px; + margin-left: 2px; +} + +.activeStory { + &:link, + &:hover, + &:visited, + &:active { + font-weight: bold; + text-decoration: underline; + color: var(--color-text-contrast); + display: flex; + } +} diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx new file mode 100644 index 0000000000..17ef49330d --- /dev/null +++ b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as PlayCircleIcon} from 'assets/images/icons/play-circle.svg'; +import Linkify from 'linkifyjs/react'; +import {timeElapsedInHours} from 'dates'; + +type InstagramRepliesProps = { + url: string; + text: string; + sentAt: Date; + fromContact: boolean; +}; + +export const StoryReplies = ({url, text, sentAt, fromContact}: InstagramRepliesProps) => { + return ( +
+
+ In response to a  + {timeElapsedInHours(sentAt) <= 24 ? ( + + ) : ( + story (expired) + )} +
+ + {text} + +
+ ); +}; diff --git a/lib/typescript/render/providers/facebook/facebookModel.ts b/lib/typescript/render/providers/facebook/facebookModel.ts index da92c9892c..6d6309ddf9 100644 --- a/lib/typescript/render/providers/facebook/facebookModel.ts +++ b/lib/typescript/render/providers/facebook/facebookModel.ts @@ -154,6 +154,12 @@ export interface StoryMentionContent extends Content { sentAt: Date; } +export interface StoryRepliesContent extends Content { + type: 'story_replies'; + url: string; + sentAt: Date; +} + // Add a new facebook content model here: export type ContentUnion = | TextContent @@ -166,6 +172,7 @@ export type ContentUnion = | QuickRepliesContent | MediaTemplate | StoryMentionContent + | StoryRepliesContent | Fallback; export type AttachmentUnion = From aa3cb24d21a11858aa4856191e8e1ffcf5689486 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 09:29:21 +0200 Subject: [PATCH 23/33] Bump immer from 9.0.3 to 9.0.6 (#2369) Bumps [immer](https://github.com/immerjs/immer) from 9.0.3 to 9.0.6. - [Release notes](https://github.com/immerjs/immer/releases) - [Commits](https://github.com/immerjs/immer/compare/v9.0.3...v9.0.6) --- updated-dependencies: - dependency-name: immer dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f2e1b4ab95..0a8ba601c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4214,9 +4214,9 @@ ignore@^5.1.4: integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== immer@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.3.tgz#146e2ba8b84d4b1b15378143c2345559915097f4" - integrity sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A== + version "9.0.6" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" + integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" From 1b4257e683ef9945eacc97c9147759c980cc4548 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:15:57 +0200 Subject: [PATCH 24/33] Bump @babel/preset-env from 7.15.0 to 7.15.4 (#2372) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.15.0 to 7.15.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.15.4/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 323 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 245 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index e6be32b02b..e52fa88a65 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-object-rest-spread": "^7.14.5", "@babel/plugin-transform-spread": "^7.14.6", - "@babel/preset-env": "^7.15.0", + "@babel/preset-env": "^7.15.4", "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.15.0", "@bazel/typescript": "^3.6.0", diff --git a/yarn.lock b/yarn.lock index 0a8ba601c8..93132e200d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -51,6 +51,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.4.tgz#85acb159a267ca6324f9793986991ee2022a05b0" + integrity sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw== + dependencies: + "@babel/types" "^7.15.4" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" @@ -58,6 +67,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-annotate-as-pure@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz#3d0e43b00c5e49fdb6c57e421601a7a658d5f835" + integrity sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191" @@ -66,10 +82,10 @@ "@babel/helper-explode-assignable-expression" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0": - version "7.15.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818" - integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0", "@babel/helper-compilation-targets@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz#cf6d94f30fbefc139123e27dd6b02f65aeedb7b9" + integrity sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ== dependencies: "@babel/compat-data" "^7.15.0" "@babel/helper-validator-option" "^7.14.5" @@ -100,6 +116,18 @@ "@babel/helper-replace-supers" "^7.15.0" "@babel/helper-split-export-declaration" "^7.14.5" +"@babel/helper-create-class-features-plugin@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz#7f977c17bd12a5fba363cb19bea090394bf37d2e" + integrity sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.15.4" + "@babel/helper-function-name" "^7.15.4" + "@babel/helper-member-expression-to-functions" "^7.15.4" + "@babel/helper-optimise-call-expression" "^7.15.4" + "@babel/helper-replace-supers" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" + "@babel/helper-create-regexp-features-plugin@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4" @@ -138,6 +166,15 @@ "@babel/template" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/helper-function-name@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc" + integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw== + dependencies: + "@babel/helper-get-function-arity" "^7.15.4" + "@babel/template" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-get-function-arity@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815" @@ -145,6 +182,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-get-function-arity@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" + integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-hoist-variables@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d" @@ -152,6 +196,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-hoist-variables@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df" + integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-member-expression-to-functions@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz#d5c70e4ad13b402c95156c7a53568f504e2fb7b8" @@ -166,6 +217,13 @@ dependencies: "@babel/types" "^7.15.0" +"@babel/helper-member-expression-to-functions@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef" + integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-module-imports@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" @@ -180,6 +238,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-module-imports@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f" + integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08" @@ -194,6 +259,20 @@ "@babel/traverse" "^7.15.0" "@babel/types" "^7.15.0" +"@babel/helper-module-transforms@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz#962cc629a7f7f9a082dd62d0307fa75fe8788d7c" + integrity sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw== + dependencies: + "@babel/helper-module-imports" "^7.15.4" + "@babel/helper-replace-supers" "^7.15.4" + "@babel/helper-simple-access" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" + "@babel/helper-validator-identifier" "^7.14.9" + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-optimise-call-expression@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c" @@ -201,6 +280,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-optimise-call-expression@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" + integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" @@ -215,6 +301,15 @@ "@babel/helper-wrap-function" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/helper-remap-async-to-generator@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz#2637c0731e4c90fbf58ac58b50b2b5a192fc970f" + integrity sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.15.4" + "@babel/helper-wrap-function" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-replace-supers@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz#0ecc0b03c41cd567b4024ea016134c28414abb94" @@ -235,6 +330,16 @@ "@babel/traverse" "^7.15.0" "@babel/types" "^7.15.0" +"@babel/helper-replace-supers@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a" + integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.15.4" + "@babel/helper-optimise-call-expression" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helper-simple-access@^7.14.8": version "7.14.8" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" @@ -242,6 +347,13 @@ dependencies: "@babel/types" "^7.14.8" +"@babel/helper-simple-access@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz#ac368905abf1de8e9781434b635d8f8674bcc13b" + integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-skip-transparent-expression-wrappers@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4" @@ -249,6 +361,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-skip-transparent-expression-wrappers@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb" + integrity sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-split-export-declaration@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" @@ -256,6 +375,13 @@ dependencies: "@babel/types" "^7.14.5" +"@babel/helper-split-export-declaration@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz#aecab92dcdbef6a10aa3b62ab204b085f776e257" + integrity sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw== + dependencies: + "@babel/types" "^7.15.4" + "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" @@ -286,6 +412,16 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/helper-wrap-function@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz#6f754b2446cfaf3d612523e6ab8d79c27c3a3de7" + integrity sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw== + dependencies: + "@babel/helper-function-name" "^7.15.4" + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/helpers@^7.14.8": version "7.14.8" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77" @@ -318,22 +454,27 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.2.tgz#08d4ffcf90d211bf77e7cc7154c6f02d468d2b1d" integrity sha512-bMJXql1Ss8lFnvr11TZDH4ArtwlAS5NG9qBmdiFW2UHHm6MVoR+GDc5XE2b9K938cyjc9O6/+vjjcffLDtfuDg== -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz#4b467302e1548ed3b1be43beae2cc9cf45e0bb7e" - integrity sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ== +"@babel/parser@^7.15.4": + version "7.15.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.5.tgz#d33a58ca69facc05b26adfe4abebfed56c1c2dac" + integrity sha512-2hQstc6I7T6tQsWzlboMh3SgMRPaS4H6H7cPQsJkdzTzEGqQrpLDsE2BGASU5sBPoEQyHzeqU6C8uKbFeEk6sg== + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz#dbdeabb1e80f622d9f0b583efb2999605e0a567e" + integrity sha512-eBnpsl9tlhPhpI10kU06JHnrYXwg3+V6CaP2idsCXNef0aeslpqyITXQ74Vfk5uHgY7IG7XP0yIH8b42KSzHog== dependencies: "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4" "@babel/plugin-proposal-optional-chaining" "^7.14.5" -"@babel/plugin-proposal-async-generator-functions@^7.14.9": - version "7.14.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz#7028dc4fa21dc199bbacf98b39bab1267d0eaf9a" - integrity sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw== +"@babel/plugin-proposal-async-generator-functions@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.4.tgz#f82aabe96c135d2ceaa917feb9f5fca31635277e" + integrity sha512-2zt2g5vTXpMC3OmK6uyjvdXptbhBXfA77XGrd3gh93zwG8lZYBLOBImiGBEG0RANu3JqKEACCz5CGk73OJROBw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-remap-async-to-generator" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.15.4" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-proposal-class-properties@^7.14.5": @@ -344,12 +485,12 @@ "@babel/helper-create-class-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz#158e9e10d449c3849ef3ecde94a03d9f1841b681" - integrity sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg== +"@babel/plugin-proposal-class-static-block@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.15.4.tgz#3e7ca6128453c089e8b477a99f970c63fc1cb8d7" + integrity sha512-M682XWrrLNk3chXCjoPUQWOyYsB93B9z3mRyjtqqYJWDf2mfCdIYgDrA11cgNVhAQieaq6F2fn2f3wI0U4aTjA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" @@ -437,13 +578,13 @@ "@babel/helper-create-class-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz#9f65a4d0493a940b4c01f8aa9d3f1894a587f636" - integrity sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q== +"@babel/plugin-proposal-private-property-in-object@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.15.4.tgz#55c5e3b4d0261fd44fe637e3f624cfb0f484e3e5" + integrity sha512-X0UTixkLf0PCCffxgu5/1RQyGGbgZuKoI+vXP4iSbJSYwPb7hu06omsFGBvQ9lJEvwgrxHdS8B5nbfcd8GyUNA== dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.15.4" + "@babel/helper-create-class-features-plugin" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" @@ -590,24 +731,24 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-block-scoping@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz#8cc63e61e50f42e078e6f09be775a75f23ef9939" - integrity sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw== +"@babel/plugin-transform-block-scoping@^7.15.3": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz#94c81a6e2fc230bcce6ef537ac96a1e4d2b3afaf" + integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q== dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-classes@^7.14.9": - version "7.14.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz#2a391ffb1e5292710b00f2e2c210e1435e7d449f" - integrity sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A== +"@babel/plugin-transform-classes@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz#50aee17aaf7f332ae44e3bce4c2e10534d5d3bf1" + integrity sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg== dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.15.4" + "@babel/helper-function-name" "^7.15.4" + "@babel/helper-optimise-call-expression" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" - "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-replace-supers" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.14.5": @@ -647,10 +788,10 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-for-of@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb" - integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA== +"@babel/plugin-transform-for-of@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz#25c62cce2718cfb29715f416e75d5263fb36a8c2" + integrity sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA== dependencies: "@babel/helper-plugin-utils" "^7.14.5" @@ -685,25 +826,25 @@ "@babel/helper-plugin-utils" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.15.0": - version "7.15.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz#3305896e5835f953b5cdb363acd9e8c2219a5281" - integrity sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig== +"@babel/plugin-transform-modules-commonjs@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz#8201101240eabb5a76c08ef61b2954f767b6b4c1" + integrity sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA== dependencies: - "@babel/helper-module-transforms" "^7.15.0" + "@babel/helper-module-transforms" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.14.8" + "@babel/helper-simple-access" "^7.15.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz#c75342ef8b30dcde4295d3401aae24e65638ed29" - integrity sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA== +"@babel/plugin-transform-modules-systemjs@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz#b42890c7349a78c827719f1d2d0cd38c7d268132" + integrity sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw== dependencies: - "@babel/helper-hoist-variables" "^7.14.5" - "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-hoist-variables" "^7.15.4" + "@babel/helper-module-transforms" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.9" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.14.5": @@ -736,10 +877,10 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-replace-supers" "^7.14.5" -"@babel/plugin-transform-parameters@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3" - integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA== +"@babel/plugin-transform-parameters@^7.14.5", "@babel/plugin-transform-parameters@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz#5f2285cc3160bf48c8502432716b48504d29ed62" + integrity sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ== dependencies: "@babel/helper-plugin-utils" "^7.14.5" @@ -864,19 +1005,19 @@ "@babel/helper-create-regexp-features-plugin" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5" -"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.15.0": - version "7.15.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.0.tgz#e2165bf16594c9c05e52517a194bf6187d6fe464" - integrity sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q== +"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.4.tgz#197e7f99a755c488f0af411af179cbd10de6e815" + integrity sha512-4f2nLw+q6ht8gl3sHCmNhmA5W6b1ItLzbH3UrKuJxACHr2eCpk96jwjrAfCAaXaaVwTQGnyUYHY2EWXJGt7TUQ== dependencies: "@babel/compat-data" "^7.15.0" - "@babel/helper-compilation-targets" "^7.15.0" + "@babel/helper-compilation-targets" "^7.15.4" "@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5" - "@babel/plugin-proposal-async-generator-functions" "^7.14.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.15.4" + "@babel/plugin-proposal-async-generator-functions" "^7.15.4" "@babel/plugin-proposal-class-properties" "^7.14.5" - "@babel/plugin-proposal-class-static-block" "^7.14.5" + "@babel/plugin-proposal-class-static-block" "^7.15.4" "@babel/plugin-proposal-dynamic-import" "^7.14.5" "@babel/plugin-proposal-export-namespace-from" "^7.14.5" "@babel/plugin-proposal-json-strings" "^7.14.5" @@ -887,7 +1028,7 @@ "@babel/plugin-proposal-optional-catch-binding" "^7.14.5" "@babel/plugin-proposal-optional-chaining" "^7.14.5" "@babel/plugin-proposal-private-methods" "^7.14.5" - "@babel/plugin-proposal-private-property-in-object" "^7.14.5" + "@babel/plugin-proposal-private-property-in-object" "^7.15.4" "@babel/plugin-proposal-unicode-property-regex" "^7.14.5" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" @@ -906,25 +1047,25 @@ "@babel/plugin-transform-arrow-functions" "^7.14.5" "@babel/plugin-transform-async-to-generator" "^7.14.5" "@babel/plugin-transform-block-scoped-functions" "^7.14.5" - "@babel/plugin-transform-block-scoping" "^7.14.5" - "@babel/plugin-transform-classes" "^7.14.9" + "@babel/plugin-transform-block-scoping" "^7.15.3" + "@babel/plugin-transform-classes" "^7.15.4" "@babel/plugin-transform-computed-properties" "^7.14.5" "@babel/plugin-transform-destructuring" "^7.14.7" "@babel/plugin-transform-dotall-regex" "^7.14.5" "@babel/plugin-transform-duplicate-keys" "^7.14.5" "@babel/plugin-transform-exponentiation-operator" "^7.14.5" - "@babel/plugin-transform-for-of" "^7.14.5" + "@babel/plugin-transform-for-of" "^7.15.4" "@babel/plugin-transform-function-name" "^7.14.5" "@babel/plugin-transform-literals" "^7.14.5" "@babel/plugin-transform-member-expression-literals" "^7.14.5" "@babel/plugin-transform-modules-amd" "^7.14.5" - "@babel/plugin-transform-modules-commonjs" "^7.15.0" - "@babel/plugin-transform-modules-systemjs" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.15.4" + "@babel/plugin-transform-modules-systemjs" "^7.15.4" "@babel/plugin-transform-modules-umd" "^7.14.5" "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.9" "@babel/plugin-transform-new-target" "^7.14.5" "@babel/plugin-transform-object-super" "^7.14.5" - "@babel/plugin-transform-parameters" "^7.14.5" + "@babel/plugin-transform-parameters" "^7.15.4" "@babel/plugin-transform-property-literals" "^7.14.5" "@babel/plugin-transform-regenerator" "^7.14.5" "@babel/plugin-transform-reserved-words" "^7.14.5" @@ -936,7 +1077,7 @@ "@babel/plugin-transform-unicode-escapes" "^7.14.5" "@babel/plugin-transform-unicode-regex" "^7.14.5" "@babel/preset-modules" "^0.1.4" - "@babel/types" "^7.15.0" + "@babel/types" "^7.15.4" babel-plugin-polyfill-corejs2 "^0.2.2" babel-plugin-polyfill-corejs3 "^0.2.2" babel-plugin-polyfill-regenerator "^0.2.2" @@ -991,6 +1132,15 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" +"@babel/template@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" + integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.15.4" + "@babel/types" "^7.15.4" + "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8", "@babel/traverse@^7.15.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98" @@ -1006,10 +1156,25 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.12.13", "@babel/types@^7.12.6", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.15.0", "@babel/types@^7.4.4": - version "7.15.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" - integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== +"@babel/traverse@^7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" + integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.4" + "@babel/helper-function-name" "^7.15.4" + "@babel/helper-hoist-variables" "^7.15.4" + "@babel/helper-split-export-declaration" "^7.15.4" + "@babel/parser" "^7.15.4" + "@babel/types" "^7.15.4" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.12.13", "@babel/types@^7.12.6", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.15.0", "@babel/types@^7.15.4", "@babel/types@^7.4.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.4.tgz#74eeb86dbd6748d2741396557b9860e57fce0a0d" + integrity sha512-0f1HJFuGmmbrKTCZtbm3cU+b/AqdEYk5toj5iQur58xkVMlS0JWaKxTBSmCXd47uiN7vbcozAupm6Mvs80GNhw== dependencies: "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" From d810266845c979323f23ccdc543d11a4b90b1673 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 6 Sep 2021 16:43:37 +0200 Subject: [PATCH 25/33] [#2370] Fix inbox counter bug when paginating (#2371) --- .../src/reducers/data/conversations/index.ts | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/frontend/ui/src/reducers/data/conversations/index.ts b/frontend/ui/src/reducers/data/conversations/index.ts index eefe3d7e26..a5b11f0127 100644 --- a/frontend/ui/src/reducers/data/conversations/index.ts +++ b/frontend/ui/src/reducers/data/conversations/index.ts @@ -114,9 +114,7 @@ function setLoadingOfConversation(items: ConversationMap, conversationId: string return items; } -const lastMessageOf = (messages: Message[]): Message => { - return sortBy(messages, message => message.sentAt).pop(); -}; +const lastMessageOf = (messages: Message[]): Message => sortBy(messages, message => message.sentAt).pop(); const updateContact = (state: AllConversationsState, conversationId: string, displayName: string) => { const conversation: Conversation = state.items[conversationId]; @@ -165,27 +163,29 @@ const removeTagFromConversation = (state: AllConversationsState, conversationId: const mergeMessages = (state: AllConversationsState, conversationId: string, messages: Message[]) => { const conversation: Conversation = state.items[conversationId]; - - if (conversation) { - return { - ...state, - items: { - ...state.items, - [conversation.id]: { - ...conversation, - lastMessage: lastMessageOf(messages.concat([conversation.lastMessage])), - }, - }, - }; + if (!conversation) { + return state; } - return state; + return { + ...state, + items: { + ...state.items, + [conversation.id]: { + ...conversation, + lastMessage: lastMessageOf(messages.concat([conversation.lastMessage])), + }, + }, + }; }; +const getNewestConversation = (conversations: Conversation[]): Conversation => + sortBy(conversations, conversation => conversation.createdAt).pop(); + function allReducer( state: AllConversationsState = initialState, action: Action | MessageAction ): AllConversationsState { - let updatedConversationCount = state.paginationData.total; + let conversationCount = state.paginationData.total; switch (action.type) { case getType(actions.setStateConversationAction): @@ -224,9 +224,17 @@ function allReducer( case getType(actions.updateContactAction): { return updateContact(state, action.payload.conversationId, action.payload.displayName); } + case getType(actions.mergeConversationsAction): + /* eslint-disable no-case-declarations */ + const newestConversation = getNewestConversation(Object.values(state.items)); + const isNewConversation = ({createdAt}: Conversation) => + !newestConversation || new Date(createdAt).getTime() > new Date(newestConversation.createdAt).getTime(); + action.payload.conversations.forEach(conversation => { - if (!state.items[conversation.id]) updatedConversationCount++; + if (isNewConversation(conversation)) { + conversationCount++; + } }); if (action.payload.paginationData) { @@ -236,24 +244,23 @@ function allReducer( paginationData: { ...state.paginationData, ...action.payload.paginationData, - ...(state.paginationData.total < updatedConversationCount && - action.payload.paginationData.total < updatedConversationCount && {total: updatedConversationCount}), - loading: false, - loaded: true, - }, - }; - } else { - return { - ...state, - items: mergeConversations(state.items, action.payload.conversations as MergedConversation[]), - paginationData: { - ...state.paginationData, - ...(state.paginationData.total < updatedConversationCount && {total: updatedConversationCount}), + ...(state.paginationData.total < conversationCount && + action.payload.paginationData.total < conversationCount && {total: conversationCount}), loading: false, loaded: true, }, }; } + return { + ...state, + items: mergeConversations(state.items, action.payload.conversations as MergedConversation[]), + paginationData: { + ...state.paginationData, + ...(state.paginationData.total < conversationCount && {total: conversationCount}), + loading: false, + loaded: true, + }, + }; case getType(actions.loadingConversationsAction): return { From 695ae2708b64628f84f5156c4d5a78ad7b44bdda Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:04:12 +0200 Subject: [PATCH 26/33] [#2328] Display any source in the inbox (#2368) * added unknown source message type rendering * added display of source --- .../components/IconChannel/index.module.scss | 5 +- .../ui/src/components/IconChannel/index.tsx | 8 +++- lib/typescript/assets/images/icons/bubble.svg | 2 +- lib/typescript/render/SourceMessage.tsx | 28 +++++++++-- .../UnknownSourceText/index.module.scss | 47 +++++++++++++++++++ .../components/UnknownSourceText/index.tsx | 28 +++++++++++ .../InstagramStoryReplies/index.module.scss | 2 +- 7 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 lib/typescript/render/components/UnknownSourceText/index.module.scss create mode 100644 lib/typescript/render/components/UnknownSourceText/index.tsx diff --git a/frontend/ui/src/components/IconChannel/index.module.scss b/frontend/ui/src/components/IconChannel/index.module.scss index f69c552777..db0a0ce0b9 100644 --- a/frontend/ui/src/components/IconChannel/index.module.scss +++ b/frontend/ui/src/components/IconChannel/index.module.scss @@ -25,8 +25,9 @@ width: 20px; } svg { - height: 20px; - width: 20px; + height: 22px; + width: 22px; + margin-right: 2px; } p { overflow: hidden; diff --git a/frontend/ui/src/components/IconChannel/index.tsx b/frontend/ui/src/components/IconChannel/index.tsx index 301946d783..7b4543c342 100644 --- a/frontend/ui/src/components/IconChannel/index.tsx +++ b/frontend/ui/src/components/IconChannel/index.tsx @@ -14,6 +14,7 @@ import {ReactComponent as WhatsappAvatar} from 'assets/images/icons/whatsapp_ava import {ReactComponent as AiryAvatar} from 'assets/images/icons/airy_avatar.svg'; import {ReactComponent as AiryIcon} from 'assets/images/icons/airy-icon.svg'; import {ReactComponent as ViberIcon} from 'assets/images/icons/viber.svg'; +import {ReactComponent as BubbleIcon} from 'assets/images/icons/bubble.svg'; import styles from './index.module.scss'; @@ -71,6 +72,11 @@ const SOURCE_INFO = { icon: () => , avatar: () => , }, + unknown: { + text: 'Unknown Source', + icon: () => , + avatar: () => , + }, }; const IconChannel: React.FC = ({ @@ -84,7 +90,7 @@ const IconChannel: React.FC = ({ channel = PlaceholderChannelData; } - const channelInfo = SOURCE_INFO[channel.source]; + const channelInfo = SOURCE_INFO[channel.source] || SOURCE_INFO['unknown']; const fbFallback = SOURCE_INFO['facebook']; const isFromTwilioSource = channel.source === 'twilio.sms' || channel.source === 'twilio.whatsapp'; diff --git a/lib/typescript/assets/images/icons/bubble.svg b/lib/typescript/assets/images/icons/bubble.svg index 6d51a78f08..44d8152ac7 100644 --- a/lib/typescript/assets/images/icons/bubble.svg +++ b/lib/typescript/assets/images/icons/bubble.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/typescript/render/SourceMessage.tsx b/lib/typescript/render/SourceMessage.tsx index b2ab1173bd..155d17f0c6 100644 --- a/lib/typescript/render/SourceMessage.tsx +++ b/lib/typescript/render/SourceMessage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {renderProviders} from './renderProviders'; - import {Text} from './components/Text'; +import {UnknownSourceText} from './components/UnknownSourceText'; import {RenderPropsUnion} from './props'; type SourceMessageState = { @@ -22,13 +22,35 @@ export class SourceMessage extends React.Component + ); + } + errorFallback() { return ; } render() { const provider = renderProviders[this.props.source]; - if (this.state.hasError || this.props.source === undefined || provider === undefined) { + + if (provider === undefined || this.props.source === undefined) { + return this.unknownSource(); + } + + if (this.state.hasError) { return this.errorFallback(); } @@ -36,7 +58,7 @@ export class SourceMessage extends React.Component { + return ( +
+
+ {sourceName ?? 'Unknown'} Source +
+ + {text} + +
+ ); +}; diff --git a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss index c5bf313e53..2a2055547e 100644 --- a/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss +++ b/lib/typescript/render/providers/facebook/components/InstagramStoryReplies/index.module.scss @@ -41,7 +41,7 @@ } .storyLink { - display: 'flex'; + display: flex; align-items: center; justify-content: center; display: inline-block; From 73526d045c6738484714c43bbe1abeacd5d5e9f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 08:59:34 +0200 Subject: [PATCH 27/33] Bump core-js from 3.16.4 to 3.17.2 (#2375) Bumps [core-js](https://github.com/zloirock/core-js) from 3.16.4 to 3.17.2. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.16.4...v3.17.2) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e52fa88a65..734558a4b5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@types/react-redux": "7.1.18", "@types/react-router-dom": "^5.1.8", "camelcase-keys": "^7.0.0", - "core-js": "3.16.4", + "core-js": "3.17.2", "emoji-mart": "3.0.1", "linkifyjs": "^2.1.9", "lodash-es": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 93132e200d..e6f522d5a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2824,10 +2824,10 @@ core-js-compat@^3.16.0, core-js-compat@^3.9.1: browserslist "^4.16.6" semver "7.0.0" -core-js@3.16.4: - version "3.16.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.4.tgz#0fb1029a554fc2688c0963d7c900e188188a78e0" - integrity sha512-Tq4GVE6XCjE+hcyW6hPy0ofN3hwtLudz5ZRdrlCnsnD/xkm/PWQRudzYHiKgZKUcefV6Q57fhDHjZHJP5dpfSg== +core-js@3.17.2: + version "3.17.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.17.2.tgz#f960eae710dc62c29cca93d5332e3660e289db10" + integrity sha512-XkbXqhcXeMHPRk2ItS+zQYliAMilea2euoMsnpRRdDad6b2VY6CQQcwz1K8AnWesfw4p165RzY0bTnr3UrbYiA== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" From 0f3ab3554fbc41b0fe1f44d04fb32c858a1c84b3 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 7 Sep 2021 10:54:57 +0200 Subject: [PATCH 28/33] [#2274] Introduce the source API (#2327) --- .bazelignore | 2 + .../core/api/admin/ChannelsController.java | 35 +- .../java/co/airy/core/api/admin/Stores.java | 4 +- .../core/api/admin/WebhooksController.java | 3 +- .../payload/WebhookSubscribePayload.java | 4 +- .../payload/WebhookUnsubscribePayload.java | 2 - .../core/api/config/ServiceDiscovery.java | 4 - .../api/admin/WebhooksControllerTest.java | 3 - .../ConversationsController.java | 38 +- .../communication/SendMessageController.java | 2 +- .../airy/core/api/communication/Stores.java | 4 +- .../core/api/communication/dto/Messages.java | 5 - .../lucene/IndexingProcessor.java | 2 +- .../payload/ConversationResponsePayload.java | 2 +- .../core/api/communication/MessagesTest.java | 4 - .../SendMessageControllerTest.java | 2 - backend/avro/BUILD | 4 + backend/avro/message.avsc | 8 + backend/avro/source.avsc | 44 +++ .../airy/model/conversation/Conversation.java | 2 +- .../model/event/payload/MessageUpdated.java | 1 - .../co/airy/model/metadata/MetadataKeys.java | 11 +- .../model/metadata/MetadataRepository.java | 7 +- .../airy/model/metadata/dto/MetadataMap.java | 3 +- backend/sources/api/BUILD | 61 ++++ .../core/sources/api/ChannelsController.java | 117 +++++++ .../core/sources/api/SourcesController.java | 120 +++++++ .../java/co/airy/core/sources/api/Stores.java | 205 +++++++++++ .../core/sources/api/WebhookController.java | 105 ++++++ .../core/sources/api/actions/Actions.java | 42 +++ .../sources/api/actions/ApiException.java | 7 + .../core/sources/api/actions/Endpoint.java | 46 +++ .../sources/api/actions/EndpointConfig.java | 44 +++ .../sources/api/actions/dto/SendMessage.java | 23 ++ .../api/actions/payload/ActionPayload.java | 11 + .../actions/payload/ErrorResponsePayload.java | 8 + .../payload/SendMessageRequestPayload.java | 84 +++++ .../payload/SendMessageResponsePayload.java | 10 + .../api/payload/ChannelsResponsePayload.java | 15 + .../payload/CreateChannelRequestPayload.java | 19 + .../payload/CreateSourceRequestPayload.java | 19 + .../payload/DeleteSourceRequestPayload.java | 15 + .../DisconnectChannelRequestPayload.java | 16 + .../payload/ListSourceResponsePayload.java | 12 + .../api/payload/SourceResponsePayload.java | 14 + .../api/payload/WebhookRequestPayload.java | 58 ++++ .../sources/api/services/SourceToken.java | 73 ++++ .../sources/api/ChannelsControllerTest.java | 102 ++++++ .../core/sources/api/SendMessageTest.java | 145 ++++++++ .../sources/api/WebhookControllerTest.java | 113 ++++++ .../core/sources/api/util/TestSource.java | 48 +++ .../co/airy/core/sources/api/util/Topics.java | 18 + .../api/src/test/resources/test.properties | 2 + .../api/src/test/resources/webhook.json | 33 ++ .../airy/core/chat_plugin/ChatController.java | 20 +- .../sources/facebook/ChannelsController.java | 38 +- .../airy/core/sources/facebook/Connector.java | 4 +- .../airy/core/sources/facebook/api/Api.java | 18 +- .../sources/facebook/SendMessageTest.java | 1 - .../core/sources/facebook/EventsRouter.java | 2 +- .../core/sources/facebook/MessageMapper.java | 8 +- .../sources/facebook/EventsRouterTest.java | 3 +- .../sources/google/ChannelsController.java | 14 +- .../sources/twilio/ChannelsController.java | 26 +- .../airy/core/sources/twilio/Connector.java | 2 - .../core/sources/twilio/services/Api.java | 2 - .../core/sources/twilio/SendMessageTest.java | 3 - .../sources/viber/ChannelsController.java | 4 +- backend/webhook/consumer/BUILD | 1 + .../co/airy/core/webhook/consumer/Sender.java | 12 +- .../core/webhook/consumer/ConsumerTest.java | 8 +- docs/docs/api/endpoints/channels.md | 2 +- docs/docs/api/endpoints/messages-send.mdx | 11 + docs/docs/api/endpoints/messages.md | 2 +- docs/docs/api/introduction.md | 8 + docs/docs/api/source.md | 324 ++++++++++++++++++ docs/docs/api/websocket.md | 4 +- docs/docs/getting-started/glossary.md | 8 +- .../installation/configuration.md | 2 + docs/docs/sources/chatplugin/customization.md | 22 +- docs/docs/sources/introduction.md | 2 +- docs/sidebars.js | 1 + .../chat-plugin/lib/src/AiryChatPlugin.tsx | 21 +- frontend/chat-plugin/lib/src/api/index.tsx | 1 + .../chat-plugin/lib/src/websocket/index.ts | 2 +- .../integration/charts/source-api/Chart.yaml | 5 + .../source-api/templates/deployment.yaml | 90 +++++ .../charts/source-api/templates/service.yaml | 16 + .../integration/charts/source-api/values.yaml | 3 + .../templates/kafka-create-topics.yaml | 2 + .../charts/core/templates/ingress.yaml | 7 + lib/java/crypto/BUILD | 10 + .../main/java/co/airy/crypto}/Signature.java | 10 +- .../java/co/airy/date/format/DateFormat.java | 7 + .../ApplicationCommunicationSources.java | 17 + lib/java/spring/auth/BUILD | 2 +- .../java/co/airy/spring/auth/AuthConfig.java | 35 +- .../airy/spring/auth/{session => }/Jwt.java | 36 +- .../co/airy/spring/auth/PrincipalAccess.java | 8 +- .../CookieSecurityContextRepository.java | 7 +- .../session/{AiryAuth.java => UserAuth.java} | 4 +- .../airy/spring/auth/session/UserProfile.java | 1 - .../auth/token/AuthenticationFilter.java | 23 +- .../co/airy/spring/auth/token/TokenAuth.java | 20 +- .../airy/spring/auth/token/TokenProfile.java | 20 ++ .../airy/spring/auth/AuthenticationTest.java | 24 +- .../airy/spring/auth/test_app/Controller.java | 6 +- .../co/airy/spring/test/WebTestHelper.java | 16 +- 108 files changed, 2449 insertions(+), 277 deletions(-) create mode 100644 backend/avro/source.avsc create mode 100644 backend/sources/api/BUILD create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/ChannelsController.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/SourcesController.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/Stores.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/WebhookController.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Actions.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/ApiException.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Endpoint.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/EndpointConfig.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/dto/SendMessage.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ActionPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ErrorResponsePayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageResponsePayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ChannelsResponsePayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateChannelRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateSourceRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DeleteSourceRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DisconnectChannelRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ListSourceResponsePayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/SourceResponsePayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/payload/WebhookRequestPayload.java create mode 100644 backend/sources/api/src/main/java/co/airy/core/sources/api/services/SourceToken.java create mode 100644 backend/sources/api/src/test/java/co/airy/core/sources/api/ChannelsControllerTest.java create mode 100644 backend/sources/api/src/test/java/co/airy/core/sources/api/SendMessageTest.java create mode 100644 backend/sources/api/src/test/java/co/airy/core/sources/api/WebhookControllerTest.java create mode 100644 backend/sources/api/src/test/java/co/airy/core/sources/api/util/TestSource.java create mode 100644 backend/sources/api/src/test/java/co/airy/core/sources/api/util/Topics.java create mode 100644 backend/sources/api/src/test/resources/test.properties create mode 100644 backend/sources/api/src/test/resources/webhook.json create mode 100644 docs/docs/api/source.md create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/Chart.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/deployment.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/service.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/values.yaml create mode 100644 lib/java/crypto/BUILD rename {backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer => lib/java/crypto/src/main/java/co/airy/crypto}/Signature.java (79%) create mode 100644 lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/application/ApplicationCommunicationSources.java rename lib/java/spring/auth/src/main/java/co/airy/spring/auth/{session => }/Jwt.java (58%) rename lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/{AiryAuth.java => UserAuth.java} (90%) create mode 100644 lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenProfile.java diff --git a/.bazelignore b/.bazelignore index de76e80af4..56fb4fa620 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,2 +1,4 @@ docs/node_modules +docs/.docusaurus +docs/build node_modules diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java index 5946befbee..760dba2f49 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java @@ -8,7 +8,6 @@ import co.airy.model.channel.dto.ChannelContainer; import co.airy.model.metadata.MetadataKeys; import co.airy.model.metadata.dto.MetadataMap; -import co.airy.uuid.UUIDv5; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpStatus; @@ -37,9 +36,9 @@ public ChannelsController(Stores stores) { } @PostMapping("/channels.list") - ResponseEntity listChannels(@RequestBody(required = false) @Valid ListChannelRequestPayload requestPayload) { + ResponseEntity listChannels(@RequestBody(required = false) @Valid ListChannelRequestPayload payload) { final List channels = stores.getChannels(); - final String sourceToFilter = Optional.ofNullable(requestPayload).map(ListChannelRequestPayload::getSource).orElse(null); + final String sourceToFilter = Optional.ofNullable(payload).map(ListChannelRequestPayload::getSource).orElse(null); return ResponseEntity.ok(new ChannelsResponsePayload(channels.stream() .filter((container) -> sourceToFilter == null || sourceToFilter.equals(container.getChannel().getSource())) .map(ChannelPayload::fromChannelContainer) @@ -47,8 +46,8 @@ ResponseEntity listChannels(@RequestBody(required = fal } @PostMapping("/channels.info") - ResponseEntity getChannel(@RequestBody @Valid GetChannelRequestPayload requestPayload) { - final ChannelContainer container = stores.getChannel(requestPayload.getChannelId().toString()); + ResponseEntity getChannel(@RequestBody @Valid GetChannelRequestPayload payload) { + final ChannelContainer container = stores.getChannel(payload.getChannelId().toString()); if (container == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @@ -57,19 +56,19 @@ ResponseEntity getChannel(@RequestBody @Valid GetChannelRequestPayload reques } @PostMapping("/channels.update") - ResponseEntity updateChannel(@RequestBody @Valid UpdateChannelRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + ResponseEntity updateChannel(@RequestBody @Valid UpdateChannelRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final ChannelContainer container = stores.getChannel(channelId); if (container == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } final MetadataMap metadataMap = container.getMetadataMap(); - if (requestPayload.getName() != null) { - metadataMap.put(MetadataKeys.ChannelKeys.NAME, newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, requestPayload.getName())); + if (payload.getName() != null) { + metadataMap.put(MetadataKeys.ChannelKeys.NAME, newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, payload.getName())); } - if (requestPayload.getImageUrl() != null) { - metadataMap.put(MetadataKeys.ChannelKeys.IMAGE_URL, newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, requestPayload.getImageUrl())); + if (payload.getImageUrl() != null) { + metadataMap.put(MetadataKeys.ChannelKeys.IMAGE_URL, newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, payload.getImageUrl())); } try { @@ -82,17 +81,17 @@ ResponseEntity updateChannel(@RequestBody @Valid UpdateChannelRequestPayload } @PostMapping("/channels.chatplugin.connect") - ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload requestPayload) { - final String sourceChannelId = requestPayload.getName(); + ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload payload) { + final String sourceChannelId = payload.getName(); final String sourceIdentifier = "chatplugin"; final String channelId = UUID.randomUUID().toString(); List metadataList = new ArrayList<>(); - metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, requestPayload.getName())); + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, payload.getName())); - if (requestPayload.getImageUrl() != null) { - metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, requestPayload.getImageUrl())); + if (payload.getImageUrl() != null) { + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, payload.getImageUrl())); } final ChannelContainer container = ChannelContainer.builder() @@ -116,8 +115,8 @@ ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload reque } @PostMapping("/channels.chatplugin.disconnect") - ResponseEntity disconnect(@RequestBody @Valid ChannelDisconnectRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + ResponseEntity disconnect(@RequestBody @Valid ChannelDisconnectRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final ChannelContainer container = stores.getConnectedChannelsStore().get(channelId); diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/Stores.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/Stores.java index 91512a614c..89f0221861 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/Stores.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/Stores.java @@ -36,7 +36,6 @@ import static co.airy.model.metadata.MetadataRepository.getId; import static co.airy.model.metadata.MetadataRepository.getSubject; -import static co.airy.model.metadata.MetadataRepository.isChannelMetadata; @Component public class Stores implements HealthIndicator, ApplicationListener, DisposableBean { @@ -65,9 +64,8 @@ public Stores(KafkaStreamsWrapper streams, KafkaProducer metadataTable = builder.table(applicationCommunicationMetadata) - .filter((metadataId, metadata) -> isChannelMetadata(metadata)) .groupBy((metadataId, metadata) -> KeyValue.pair(getSubject(metadata).getIdentifier(), metadata)) .aggregate(MetadataMap::new, MetadataMap::adder, MetadataMap::subtractor); diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java index 30a42f8a08..c9f75b1b50 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java @@ -2,16 +2,15 @@ import co.airy.avro.communication.Status; import co.airy.avro.communication.Webhook; +import co.airy.core.api.admin.payload.WebhookInfoRequestPayload; import co.airy.core.api.admin.payload.WebhookListResponsePayload; import co.airy.core.api.admin.payload.WebhookResponsePayload; -import co.airy.core.api.admin.payload.WebhookInfoRequestPayload; import co.airy.core.api.admin.payload.WebhookSubscribePayload; import co.airy.core.api.admin.payload.WebhookUnsubscribePayload; import co.airy.core.api.config.ServiceDiscovery; import co.airy.core.api.config.dto.ComponentInfo; import co.airy.model.event.payload.EventType; import co.airy.spring.web.payload.RequestErrorResponsePayload; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java index 4f243570d8..b8f191a481 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; -import java.net.URI; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -19,7 +19,7 @@ public class WebhookSubscribePayload { private UUID id; @NotNull - private URI url; + private URL url; private Map headers = new HashMap<>(); private List events = new ArrayList<>(); private String signatureKey; diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUnsubscribePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUnsubscribePayload.java index d53cb27db6..0abee3ef92 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUnsubscribePayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUnsubscribePayload.java @@ -5,8 +5,6 @@ import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; @AllArgsConstructor diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java index 5505e58a9f..198f8db976 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java @@ -3,12 +3,8 @@ import co.airy.core.api.config.dto.ComponentInfo; import co.airy.core.api.config.dto.ServiceInfo; import co.airy.core.api.config.payload.ServicesResponsePayload; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; diff --git a/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java b/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java index 450abc920b..dd7e2acb7c 100644 --- a/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java +++ b/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java @@ -3,7 +3,6 @@ import co.airy.avro.communication.Status; import co.airy.avro.communication.Webhook; import co.airy.core.api.config.ServiceDiscovery; -import co.airy.core.api.config.dto.ComponentInfo; import co.airy.core.api.config.dto.ServiceInfo; import co.airy.kafka.schema.application.ApplicationCommunicationChannels; import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; @@ -23,7 +22,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; @@ -39,7 +37,6 @@ import java.util.UUID; import static co.airy.test.Timing.retryOnException; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java index 9f19d97eec..f1e6718b82 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java @@ -2,7 +2,6 @@ import co.airy.avro.communication.Metadata; import co.airy.avro.communication.ReadReceipt; -import co.airy.model.conversation.Conversation; import co.airy.core.api.communication.dto.LuceneQueryResult; import co.airy.core.api.communication.lucene.AiryAnalyzer; import co.airy.core.api.communication.lucene.ExtendedQueryParser; @@ -15,6 +14,7 @@ import co.airy.core.api.communication.payload.ConversationTagRequestPayload; import co.airy.core.api.communication.payload.ConversationUpdateContactRequestPayload; import co.airy.core.api.communication.payload.PaginationData; +import co.airy.model.conversation.Conversation; import co.airy.model.metadata.MetadataKeys; import co.airy.model.metadata.Subject; import co.airy.model.metadata.dto.MetadataMap; @@ -119,9 +119,9 @@ private ResponseEntity queryConversations(Query query, Integer cursor, int pa } @PostMapping("/conversations.info") - ResponseEntity conversationInfo(@RequestBody @Valid ConversationByIdRequestPayload requestPayload) { + ResponseEntity conversationInfo(@RequestBody @Valid ConversationByIdRequestPayload payload) { final ReadOnlyKeyValueStore store = stores.getConversationsStore(); - final Conversation conversation = store.get(requestPayload.getConversationId().toString()); + final Conversation conversation = store.get(payload.getConversationId().toString()); if (conversation == null) { return ResponseEntity.notFound().build(); @@ -145,9 +145,9 @@ private List fetchAllConversations() { } @PostMapping("/conversations.markRead") - ResponseEntity conversationMarkRead(@RequestBody @Valid ConversationByIdRequestPayload requestPayload) { + ResponseEntity conversationMarkRead(@RequestBody @Valid ConversationByIdRequestPayload payload) { final ReadOnlyKeyValueStore store = stores.getConversationsStore(); - final String conversationId = requestPayload.getConversationId().toString(); + final String conversationId = payload.getConversationId().toString(); final Conversation conversation = store.get(conversationId); if (conversation == null) { @@ -169,9 +169,9 @@ ResponseEntity conversationMarkRead(@RequestBody @Valid ConversationByIdReque } @PostMapping("/conversations.tag") - ResponseEntity conversationTag(@RequestBody @Valid ConversationTagRequestPayload requestPayload) { - final String conversationId = requestPayload.getConversationId().toString(); - final String tagId = requestPayload.getTagId().toString(); + ResponseEntity conversationTag(@RequestBody @Valid ConversationTagRequestPayload payload) { + final String conversationId = payload.getConversationId().toString(); + final String tagId = payload.getTagId().toString(); final ReadOnlyKeyValueStore store = stores.getConversationsStore(); final Conversation conversation = store.get(conversationId); @@ -191,9 +191,9 @@ ResponseEntity conversationTag(@RequestBody @Valid ConversationTagRequestPayl } @PostMapping("/conversations.untag") - ResponseEntity conversationUntag(@RequestBody @Valid ConversationTagRequestPayload requestPayload) { - final String conversationId = requestPayload.getConversationId().toString(); - final String tagId = requestPayload.getTagId().toString(); + ResponseEntity conversationUntag(@RequestBody @Valid ConversationTagRequestPayload payload) { + final String conversationId = payload.getConversationId().toString(); + final String tagId = payload.getTagId().toString(); final ReadOnlyKeyValueStore store = stores.getConversationsStore(); final Conversation conversation = store.get(conversationId); @@ -213,9 +213,9 @@ ResponseEntity conversationUntag(@RequestBody @Valid ConversationTagRequestPa } @PostMapping("/conversations.setState") - ResponseEntity conversationSetState(@RequestBody @Valid ConversationSetStateRequestPayload requestPayload) { - final String conversationId = requestPayload.getConversationId().toString(); - final String state = requestPayload.getState(); + ResponseEntity conversationSetState(@RequestBody @Valid ConversationSetStateRequestPayload payload) { + final String conversationId = payload.getConversationId().toString(); + final String state = payload.getState(); final ReadOnlyKeyValueStore store = stores.getConversationsStore(); final Conversation conversation = store.get(conversationId); @@ -235,8 +235,8 @@ ResponseEntity conversationSetState(@RequestBody @Valid ConversationSetStateR } @PostMapping("/conversations.removeState") - ResponseEntity conversationRemoveState(@RequestBody @Valid ConversationByIdRequestPayload requestPayload) { - final String conversationId = requestPayload.getConversationId().toString(); + ResponseEntity conversationRemoveState(@RequestBody @Valid ConversationByIdRequestPayload payload) { + final String conversationId = payload.getConversationId().toString(); final ReadOnlyKeyValueStore store = stores.getConversationsStore(); final Conversation conversation = store.get(conversationId); @@ -255,9 +255,9 @@ ResponseEntity conversationRemoveState(@RequestBody @Valid ConversationByIdRe } @PostMapping("/conversations.updateContact") - ResponseEntity conversationUpdateContact(@RequestBody @Valid ConversationUpdateContactRequestPayload requestPayload) { - final String conversationId = requestPayload.getConversationId().toString(); - final String displayName = requestPayload.getDisplayName(); + ResponseEntity conversationUpdateContact(@RequestBody @Valid ConversationUpdateContactRequestPayload payload) { + final String conversationId = payload.getConversationId().toString(); + final String displayName = payload.getDisplayName(); final ReadOnlyKeyValueStore store = stores.getConversationsStore(); final Conversation conversation = store.get(conversationId); diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/SendMessageController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/SendMessageController.java index a1bc536c55..3c334875db 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/SendMessageController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/SendMessageController.java @@ -4,9 +4,9 @@ import co.airy.avro.communication.ChannelConnectionState; import co.airy.avro.communication.DeliveryState; import co.airy.avro.communication.Message; -import co.airy.model.conversation.Conversation; import co.airy.core.api.communication.payload.SendMessageRequestPayload; import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.model.conversation.Conversation; import co.airy.model.message.dto.MessageContainer; import co.airy.model.message.dto.MessageResponsePayload; import co.airy.model.metadata.dto.MetadataMap; diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java index c5f999db25..fd89930e7d 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java @@ -4,7 +4,6 @@ import co.airy.avro.communication.Message; import co.airy.avro.communication.Metadata; import co.airy.avro.communication.ReadReceipt; -import co.airy.model.conversation.Conversation; import co.airy.core.api.communication.dto.CountAction; import co.airy.core.api.communication.dto.Messages; import co.airy.core.api.communication.dto.UnreadCountState; @@ -18,6 +17,7 @@ import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.streams.KafkaStreamsWrapper; import co.airy.model.channel.dto.ChannelContainer; +import co.airy.model.conversation.Conversation; import co.airy.model.message.dto.MessageContainer; import co.airy.model.metadata.MetadataKeys; import co.airy.model.metadata.Subject; @@ -39,7 +39,6 @@ import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.HashMap; @@ -58,7 +57,6 @@ import static java.util.stream.Collectors.toCollection; @Component -@RestController public class Stores implements HealthIndicator, ApplicationListener, DisposableBean { private static final String appId = "api.CommunicationStores"; diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Messages.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Messages.java index 361b671828..f6120ef28b 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Messages.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Messages.java @@ -3,13 +3,8 @@ import co.airy.model.message.dto.MessageContainer; import com.fasterxml.jackson.annotation.JsonCreator; -import java.util.Comparator; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListSet; -import static java.util.Comparator.comparing; - public class Messages extends ConcurrentSkipListSet { @JsonCreator public Messages() { diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/IndexingProcessor.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/IndexingProcessor.java index 3de0c490f5..53b98065d9 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/IndexingProcessor.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/IndexingProcessor.java @@ -1,7 +1,7 @@ package co.airy.core.api.communication.lucene; -import co.airy.model.conversation.Conversation; import co.airy.core.api.communication.dto.ConversationIndex; +import co.airy.model.conversation.Conversation; import org.apache.kafka.streams.processor.Processor; import org.apache.kafka.streams.processor.ProcessorContext; import org.apache.kafka.streams.processor.ProcessorSupplier; diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ConversationResponsePayload.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ConversationResponsePayload.java index c091be2fe8..2b28f4387c 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ConversationResponsePayload.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ConversationResponsePayload.java @@ -1,7 +1,7 @@ package co.airy.core.api.communication.payload; -import co.airy.model.conversation.Conversation; import co.airy.model.channel.ChannelPayload; +import co.airy.model.conversation.Conversation; import co.airy.model.message.dto.MessageResponsePayload; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java index 7a0b0e0a78..61f2aa6091 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java @@ -4,12 +4,10 @@ import co.airy.avro.communication.ChannelConnectionState; import co.airy.avro.communication.DeliveryState; import co.airy.avro.communication.Message; -import co.airy.core.api.communication.dto.Messages; import co.airy.core.api.communication.util.TestConversation; import co.airy.date.format.DateFormat; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; -import co.airy.model.message.dto.MessageContainer; import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.test.WebTestHelper; import com.fasterxml.jackson.databind.JsonNode; @@ -29,7 +27,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import java.time.Instant; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -44,7 +41,6 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; import static org.hamcrest.core.StringContains.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java index fdd5b6b742..6297eeff1d 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java @@ -35,9 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.fail; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) diff --git a/backend/avro/BUILD b/backend/avro/BUILD index d79dfd54d2..aa68df7714 100644 --- a/backend/avro/BUILD +++ b/backend/avro/BUILD @@ -26,6 +26,10 @@ avro_java_library( srcs = ["metadata.avsc"], ) +avro_java_library( + name = "source", +) + avro_java_library( name = "tag-avro", srcs = ["tag.avsc"], diff --git a/backend/avro/message.avsc b/backend/avro/message.avsc index 38489471d1..439b35ce43 100644 --- a/backend/avro/message.avsc +++ b/backend/avro/message.avsc @@ -38,6 +38,14 @@ "name": "senderId", "type": "string" }, + { + "name": "sourceRecipientId", + "type": [ + "null", + "string" + ], + "default": null + }, { "name": "conversationId", "type": "string" diff --git a/backend/avro/source.avsc b/backend/avro/source.avsc new file mode 100644 index 0000000000..bbaa3d1d3d --- /dev/null +++ b/backend/avro/source.avsc @@ -0,0 +1,44 @@ +{ + "namespace": "co.airy.avro.communication", + "name": "Source", + "type": "record", + "fields": [ + { + "name": "id", + "type": "string" + }, + { + "name": "token", + "type": "string" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "imageUrl", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "actionEndpoint", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "createdAt", + "type": "long", + "logicalType": "timestamp-millis" + } + ] +} diff --git a/backend/model/conversation/src/main/java/co/airy/model/conversation/Conversation.java b/backend/model/conversation/src/main/java/co/airy/model/conversation/Conversation.java index 6de9e4f225..fe3bd19ae2 100644 --- a/backend/model/conversation/src/main/java/co/airy/model/conversation/Conversation.java +++ b/backend/model/conversation/src/main/java/co/airy/model/conversation/Conversation.java @@ -18,8 +18,8 @@ import java.util.List; import java.util.Optional; -import static java.util.stream.Collectors.toList; import static co.airy.text.format.TextFormat.capitalize; +import static java.util.stream.Collectors.toList; @Data @Builder(toBuilder = true) diff --git a/backend/model/event/src/main/java/co/airy/model/event/payload/MessageUpdated.java b/backend/model/event/src/main/java/co/airy/model/event/payload/MessageUpdated.java index 5a90e0018b..234dd735a6 100644 --- a/backend/model/event/src/main/java/co/airy/model/event/payload/MessageUpdated.java +++ b/backend/model/event/src/main/java/co/airy/model/event/payload/MessageUpdated.java @@ -1,6 +1,5 @@ package co.airy.model.event.payload; -import co.airy.avro.communication.Message; import co.airy.model.message.dto.MessageContainer; import co.airy.model.message.dto.MessageResponsePayload; import lombok.AllArgsConstructor; diff --git a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataKeys.java b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataKeys.java index b23b64ebd3..b70944781a 100644 --- a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataKeys.java +++ b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataKeys.java @@ -21,11 +21,6 @@ public static class Contact { public static final String FETCH_STATE = "contact.fetch_state"; } - public static class Reaction { - public static final String EMOJI = "reaction.emoji"; - public static final String SENT_AT = "reaction.sent_at"; - } - public enum ContactFetchState { ok("ok"), failed("failed"); @@ -50,11 +45,17 @@ public static class ChannelKeys { public static class MessageKeys { public static final String SUGGESTIONS = "suggestions"; + public static final String ERROR = "error"; public static class Source { public static final String ID = "source.id"; public static final String DELIVERY_STATE = "source.delivery_state"; } + + public static class Reaction { + public static final String EMOJI = "reaction.emoji"; + public static final String SENT_AT = "reaction.sent_at"; + } } } diff --git a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java index 24b8e458e4..efad2b8d54 100644 --- a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java +++ b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java @@ -7,6 +7,10 @@ import java.util.UUID; public class MetadataRepository { + public interface MetadataConstructor { + Metadata apply(String id, String key, String value); + } + public static Metadata newConversationMetadata(String conversationId, String key, String value) { return Metadata.newBuilder() .setSubject(new Subject("conversation", conversationId).toString()) @@ -48,7 +52,7 @@ public static boolean isMessageMetadata(Metadata metadata) { public static Metadata newConversationTag(String conversationId, String tagId) { return Metadata.newBuilder() - .setSubject(new Subject("conversation",conversationId).toString()) + .setSubject(new Subject("conversation", conversationId).toString()) .setKey(String.format("%s.%s", MetadataKeys.ConversationKeys.TAGS, tagId)) .setValue("") .setTimestamp(Instant.now().toEpochMilli()) @@ -73,6 +77,7 @@ public static Subject getSubject(Metadata metadata) { public static UUID getId(Metadata metadata) { return UUIDv5.fromNamespaceAndName(metadata.getSubject(), metadata.getKey()); } + public static UUID getId(Subject subject, String key) { return UUIDv5.fromNamespaceAndName(subject.toString(), key); } diff --git a/backend/model/metadata/src/main/java/co/airy/model/metadata/dto/MetadataMap.java b/backend/model/metadata/src/main/java/co/airy/model/metadata/dto/MetadataMap.java index d6a2d9f98d..8b3e7b0923 100644 --- a/backend/model/metadata/src/main/java/co/airy/model/metadata/dto/MetadataMap.java +++ b/backend/model/metadata/src/main/java/co/airy/model/metadata/dto/MetadataMap.java @@ -2,7 +2,7 @@ import co.airy.avro.communication.Metadata; import co.airy.log.AiryLoggerFactory; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.slf4j.Logger; @@ -33,6 +33,7 @@ public static MetadataMap subtractor(String key, Metadata metadata, MetadataMap return aggregate; } + @JsonProperty public long getUpdatedAt() { return Optional.ofNullable(updatedAt) // Backwards compatible for maps that have not recorded this value yet diff --git a/backend/sources/api/BUILD b/backend/sources/api/BUILD new file mode 100644 index 0000000000..e75bada1f5 --- /dev/null +++ b/backend/sources/api/BUILD @@ -0,0 +1,61 @@ +load("@com_github_airyhq_bazel_tools//lint:buildifier.bzl", "check_pkg") +load("//tools/build:springboot.bzl", "springboot") +load("//tools/build:junit5.bzl", "junit5") +load("//tools/build:java_library.bzl", "custom_java_library") +load("//tools/build:container_release.bzl", "container_release") + +app_deps = [ + "//backend:base_app", + "//backend/avro:source", + "//backend/model/channel", + "//backend/model/message", + "//backend/model/conversation", + "//backend/model/metadata", + "//lib/java/crypto", + "//lib/java/date", + "//lib/java/uuid", + "//lib/java/spring/kafka/core:spring-kafka-core", + "//lib/java/spring/kafka/streams:spring-kafka-streams", + "//lib/java/spring/web:spring-web", + "//lib/java/spring/auth:spring-auth", + "//lib/java/kafka/schema:application-communication-sources", + "//:springboot_actuator", + "@maven//:javax_xml_bind_jaxb_api", +] + +springboot( + name = "api", + srcs = glob(["src/main/java/**/*.java"]), + main_class = "co.airy.spring.core.AirySpringBootApplication", + deps = app_deps, +) + +test_deps = app_deps + [ + ":app", + "//backend:base_test", + "//lib/java/kafka/test:kafka-test", + "//lib/java/spring/test:spring-test", +] + +custom_java_library( + name = "test-util", + srcs = glob(["src/test/java/co/airy/core/sources/api/util/**/*.java"]), + deps = test_deps, +) + +[ + junit5( + size = "medium", + file = file, + resources = glob(["src/test/resources/**/*"]), + deps = test_deps + [":test-util"], + ) + for file in glob(["src/test/java/**/*Test.java"]) +] + +container_release( + registry = "ghcr.io/airyhq/sources", + repository = "api", +) + +check_pkg(name = "buildifier") diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/ChannelsController.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/ChannelsController.java new file mode 100644 index 0000000000..6e55263932 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/ChannelsController.java @@ -0,0 +1,117 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.Channel; +import co.airy.avro.communication.ChannelConnectionState; +import co.airy.avro.communication.Metadata; +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.payload.ChannelsResponsePayload; +import co.airy.core.sources.api.payload.CreateChannelRequestPayload; +import co.airy.core.sources.api.payload.DisconnectChannelRequestPayload; +import co.airy.core.sources.api.services.SourceToken; +import co.airy.model.channel.ChannelPayload; +import co.airy.model.channel.dto.ChannelContainer; +import co.airy.model.metadata.MetadataKeys; +import co.airy.model.metadata.Subject; +import co.airy.model.metadata.dto.MetadataMap; +import co.airy.uuid.UUIDv5; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static co.airy.model.channel.ChannelPayload.fromChannelContainer; +import static co.airy.model.metadata.MetadataObjectMapper.getMetadataFromJson; +import static co.airy.model.metadata.MetadataRepository.newChannelMetadata; +import static java.util.stream.Collectors.toList; + +@RestController +public class ChannelsController { + private final Stores stores; + private final SourceToken sourceToken; + + public ChannelsController(Stores stores, SourceToken sourceToken) { + this.stores = stores; + this.sourceToken = sourceToken; + } + + @PostMapping("/sources.channels.create") + ResponseEntity createChannel(@RequestBody @Valid CreateChannelRequestPayload payload, Authentication authentication) throws Exception { + final Source source = sourceToken.getSource(authentication); + final String sourceChannelId = payload.getSourceChannelId(); + final String channelId = UUIDv5.fromNamespaceAndName(source.getId(), sourceChannelId).toString(); + + List metadataList = new ArrayList<>(); + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, payload.getName())); + + if (payload.getMetadata() != null) { + final Subject subject = new Subject("channel", channelId); + final List fromJson = getMetadataFromJson(subject, payload.getMetadata()); + metadataList.addAll(fromJson); + } + + final ChannelContainer container = ChannelContainer.builder() + .channel( + Channel.newBuilder() + .setId(channelId) + .setConnectionState(ChannelConnectionState.CONNECTED) + .setSource(source.getId()) + .setSourceChannelId(sourceChannelId) + .build() + ) + .metadataMap(MetadataMap.from(metadataList)).build(); + try { + stores.storeChannelContainer(container); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); + } + + return ResponseEntity.status(HttpStatus.CREATED).body(fromChannelContainer(container)); + } + + @PostMapping("/sources.channels.list") + ResponseEntity listChannels(Authentication authentication) { + final Source source = sourceToken.getSource(authentication); + final List channels = stores.getAllChannels().stream() + .filter((container -> source.getId().equals(container.getChannel().getSource()) + && container.getChannel().getConnectionState().equals(ChannelConnectionState.CONNECTED))) + .collect(Collectors.toList()); + return ResponseEntity.ok(new ChannelsResponsePayload(channels.stream() + .map(ChannelPayload::fromChannelContainer) + .collect(toList()))); + } + + @PostMapping("/sources.channels.disconnect") + ResponseEntity disconnectChannel(@RequestBody @Valid DisconnectChannelRequestPayload payload, Authentication authentication) { + final Source source = sourceToken.getSource(authentication); + final ChannelContainer container = stores.getChannel(payload.getChannelId().toString()); + if (container == null) { + return ResponseEntity.notFound().build(); + } + if (!container.getChannel().getSource().equals(source.getId())) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + final Channel channel = container.getChannel(); + if (channel.getConnectionState().equals(ChannelConnectionState.DISCONNECTED)) { + return ResponseEntity.noContent().build(); + } + + channel.setConnectionState(ChannelConnectionState.DISCONNECTED); + channel.setToken(null); + + try { + stores.storeChannel(channel); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); + } + + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/SourcesController.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/SourcesController.java new file mode 100644 index 0000000000..2d17ee91a9 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/SourcesController.java @@ -0,0 +1,120 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.payload.CreateSourceRequestPayload; +import co.airy.core.sources.api.payload.DeleteSourceRequestPayload; +import co.airy.core.sources.api.payload.ListSourceResponsePayload; +import co.airy.core.sources.api.payload.SourceResponsePayload; +import co.airy.core.sources.api.services.SourceToken; +import co.airy.spring.web.payload.RequestErrorResponsePayload; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.net.URL; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@RestController +public class SourcesController { + + private final Stores stores; + private final SourceToken sourceToken; + + public SourcesController(Stores stores, SourceToken sourceToken) { + this.stores = stores; + this.sourceToken = sourceToken; + } + + @PostMapping("/sources.create") + ResponseEntity createSource(@RequestBody @Valid CreateSourceRequestPayload payload, Authentication authentication) { + sourceToken.rejectSourceAuth(authentication); + if (stores.getSource(payload.getSourceId()) != null) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new RequestErrorResponsePayload("Source already exists")); + } + + final String token = sourceToken.getSourceToken(payload.getSourceId()); + final Source source = Source.newBuilder() + .setId(payload.getSourceId()) + .setCreatedAt(Instant.now().toEpochMilli()) + .setToken(token) + .setName(payload.getName()) + .setImageUrl(payload.getImageUrl()) + .setActionEndpoint(Optional.ofNullable(payload.getActionEndpoint()).map(URL::toString).orElse(null)) + .build(); + + try { + stores.storeSource(source); + return ResponseEntity.ok(SourceResponsePayload.builder() + .sourceId(source.getId()) + .token(source.getToken()) + .actionEndpoint(source.getActionEndpoint()) + .name(source.getName()) + .imageUrl(source.getImageUrl()) + .build() + ); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); + } + } + + @PostMapping("/sources.update") + ResponseEntity updateSource(@RequestBody @Valid CreateSourceRequestPayload payload, Authentication authentication) { + sourceToken.rejectSourceAuth(authentication); + + final Source source = stores.getSource(payload.getSourceId()); + if (source == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + + source.setActionEndpoint(payload.getActionEndpoint().toString()); + source.setImageUrl(payload.getImageUrl()); + source.setName(payload.getName()); + + try { + stores.storeSource(source); + return ResponseEntity.ok(SourceResponsePayload.builder() + .sourceId(source.getId()) + .actionEndpoint(source.getActionEndpoint()) + .name(source.getName()) + .imageUrl(source.getImageUrl()) + .build() + ); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); + } + } + + @PostMapping("/sources.list") + ResponseEntity listSources(Authentication authentication) { + sourceToken.rejectSourceAuth(authentication); + final List allSources = stores.getAllSources(); + return ResponseEntity.ok( + ListSourceResponsePayload.builder().data(allSources.stream().map((source -> + SourceResponsePayload.builder() + .sourceId(source.getId()) + .actionEndpoint(source.getActionEndpoint()) + .name(source.getName()) + .imageUrl(source.getImageUrl()) + .build() + )).collect(Collectors.toList())).build() + ); + } + + @PostMapping("/sources.delete") + ResponseEntity deleteSource(@RequestBody @Valid DeleteSourceRequestPayload payload, Authentication authentication) { + sourceToken.rejectSourceAuth(authentication); + try { + stores.deleteSource(payload.getSourceId()); + return ResponseEntity.accepted().build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); + } + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/Stores.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/Stores.java new file mode 100644 index 0000000000..73958bad82 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/Stores.java @@ -0,0 +1,205 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.Channel; +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.actions.Actions; +import co.airy.core.sources.api.actions.dto.SendMessage; +import co.airy.kafka.schema.application.ApplicationCommunicationChannels; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.schema.application.ApplicationCommunicationSources; +import co.airy.kafka.streams.KafkaStreamsWrapper; +import co.airy.model.channel.dto.ChannelContainer; +import co.airy.model.conversation.Conversation; +import co.airy.model.message.dto.MessageContainer; +import co.airy.model.metadata.dto.MetadataMap; +import org.apache.avro.specific.SpecificRecord; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.streams.KeyValue; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.KTable; +import org.apache.kafka.streams.kstream.Materialized; +import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static co.airy.model.metadata.MetadataRepository.getId; +import static co.airy.model.metadata.MetadataRepository.getSubject; + +@Component +public class Stores implements HealthIndicator, ApplicationListener, DisposableBean { + private static final String appId = "sources.Api"; + + private final String channelsStore = "channels-store"; + private final String sourcesStore = "sources-store"; + private final String metadataStore = "metadata-store"; + private final Actions actions; + private final KafkaStreamsWrapper streams; + private final KafkaProducer producer; + + private final String applicationCommunicationChannels = new ApplicationCommunicationChannels().name(); + private final String applicationCommunicationMessages = new ApplicationCommunicationMessages().name(); + private final String applicationCommunicationMetadata = new ApplicationCommunicationMetadata().name(); + private final String applicationCommunicationSources = new ApplicationCommunicationSources().name(); + + public Stores(Actions actions, KafkaStreamsWrapper streams, KafkaProducer producer) { + this.actions = actions; + this.streams = streams; + this.producer = producer; + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) { + final StreamsBuilder builder = new StreamsBuilder(); + + // Metadata table keyed by subject identifier id + final KTable metadataTable = builder.table(applicationCommunicationMetadata) + .groupBy((metadataId, metadata) -> KeyValue.pair(getSubject(metadata).getIdentifier(), metadata)) + .aggregate(MetadataMap::new, MetadataMap::adder, MetadataMap::subtractor, Materialized.as(metadataStore)); + + final KTable channelTable = builder.table(new ApplicationCommunicationChannels().name()) + .leftJoin(metadataTable, ChannelContainer::new, Materialized.as(channelsStore)); + + final KTable sourceTable = builder.table(new ApplicationCommunicationSources().name(), Materialized.as(sourcesStore)); + final KTable actionSources = sourceTable.filter((sourceId, source) -> source.getActionEndpoint() != null); + + + final KStream messageStream = builder.stream(new ApplicationCommunicationMessages().name()) + .selectKey((messageId, message) -> message.getConversationId()); + + // Conversation table + final KTable conversationTable = messageStream.toTable() + .groupBy((messageId, message) -> KeyValue.pair(message.getConversationId(), message)) + .aggregate(Conversation::new, + (conversationId, message, aggregate) -> { + if (aggregate.getLastMessageContainer() == null) { + aggregate = Conversation.builder() + .lastMessageContainer(new MessageContainer(message, new MetadataMap())) + .createdAt(message.getSentAt()) // Set this only once for the sent time of the first message + .build(); + } + + // equals because messages can be updated + if (message.getSentAt() >= aggregate.getLastMessageContainer().getMessage().getSentAt()) { + aggregate.setLastMessageContainer(new MessageContainer(message, new MetadataMap())); + } + + if (message.getIsFromContact()) { + aggregate.setSourceConversationId(message.getSenderId()); + } + + return aggregate; + }, (conversationId, container, aggregate) -> { + // If the deleted message was the last message we have no way of replacing it + // so we have no choice but to keep it + return aggregate; + }) + .join(channelTable, Conversation::getChannelId, + (conversation, channelContainer) -> conversation.toBuilder() + .channelContainer(channelContainer).build()); + + + + // Actions: + // Send messages + messageStream.filter((conversationId, message) -> message != null && message.getDeliveryState().equals(DeliveryState.PENDING)) + .leftJoin(conversationTable, (message, conversation) -> SendMessage.builder().message(message).conversation(conversation).build()) + .selectKey((messageId, message) -> message.getMessage().getSource()) + .join(actionSources, (sendMessage, source) -> sendMessage.toBuilder().source(source).build()) + .flatMap((conversationId, sendMessage) -> actions.sendMessage(sendMessage)) + .to((recordId, record, context) -> { + if (record instanceof Metadata) { + return applicationCommunicationMetadata; + } + if (record instanceof Message) { + return applicationCommunicationMessages; + } + + throw new IllegalStateException("Unknown type for record " + record); + }); + + + streams.start(builder.build(), appId); + } + + public void storeSource(Source source) throws ExecutionException, InterruptedException { + producer.send(new ProducerRecord<>(applicationCommunicationSources, source.getId(), source)).get(); + } + + public void deleteSource(String sourceId) throws ExecutionException, InterruptedException { + producer.send(new ProducerRecord<>(applicationCommunicationSources, sourceId, null)).get(); + } + + public void storeChannelContainer(ChannelContainer container) throws ExecutionException, InterruptedException { + storeChannel(container.getChannel()); + storeMetadataMap(container.getMetadataMap()); + } + + public void storeMetadataMap(MetadataMap metadataMap) throws ExecutionException, InterruptedException { + for (Metadata metadata : metadataMap.values()) { + producer.send(new ProducerRecord<>(applicationCommunicationMetadata, getId(metadata).toString(), metadata)).get(); + } + } + + public void storeChannel(Channel channel) throws ExecutionException, InterruptedException { + producer.send(new ProducerRecord<>(applicationCommunicationChannels, channel.getId(), channel)).get(); + } + + private ReadOnlyKeyValueStore getChannelsStore() { + return streams.acquireLocalStore(channelsStore); + } + + public ChannelContainer getChannel(String id) { + final ReadOnlyKeyValueStore store = getChannelsStore(); + return store.get(id); + } + + public List getAllChannels() { + final ReadOnlyKeyValueStore store = getChannelsStore(); + final ArrayList channels = new ArrayList<>(); + store.all().forEachRemaining((record) -> channels.add(record.value)); + return channels; + } + + private ReadOnlyKeyValueStore getSourcesStore() { + return streams.acquireLocalStore(sourcesStore); + } + + public Source getSource(String id) { + final ReadOnlyKeyValueStore store = getSourcesStore(); + return store.get(id); + } + + public List getAllSources() { + final ReadOnlyKeyValueStore store = getSourcesStore(); + final ArrayList sources = new ArrayList<>(); + store.all().forEachRemaining((record) -> sources.add(record.value)); + return sources; + } + + @Override + public void destroy() { + streams.close(); + } + + @Override + public Health health() { + getChannelsStore(); + getSourcesStore(); + return Health.up().build(); + } + +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/WebhookController.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/WebhookController.java new file mode 100644 index 0000000000..469ae38869 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/WebhookController.java @@ -0,0 +1,105 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.payload.WebhookRequestPayload; +import co.airy.core.sources.api.services.SourceToken; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.model.metadata.MetadataObjectMapper; +import co.airy.model.metadata.MetadataRepository; +import co.airy.model.metadata.Subject; +import co.airy.spring.web.payload.RequestErrorResponsePayload; +import co.airy.uuid.UUIDv5; +import org.apache.avro.specific.SpecificRecord; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static co.airy.model.metadata.MetadataRepository.getId; + +@RestController +public class WebhookController { + public static final String applicationCommunicationMessages = new ApplicationCommunicationMessages().name(); + public static final String applicationCommunicationMetadata = new ApplicationCommunicationMetadata().name(); + private final SourceToken sourceToken; + private final KafkaProducer producer; + private final List allowedMetadataNamespaces = List.of("conversation", "message"); + + public static Map metadataConstructorMap = Map.of( + "conversation", MetadataRepository::newConversationMetadata, + "message", MetadataRepository::newMessageMetadata + ); + + public WebhookController(SourceToken sourceToken, KafkaProducer producer) { + this.sourceToken = sourceToken; + this.producer = producer; + } + + @PostMapping("/sources.webhook") + ResponseEntity webhook(@RequestBody @Valid WebhookRequestPayload payload, Authentication authentication) throws Exception { + final Source source = sourceToken.getSource(authentication); + final String sourceId = source.getId(); + + List> records = new ArrayList<>(); + + for (WebhookRequestPayload.MessagePayload messagePayload : payload.getMessages()) { + final String messageId = UUIDv5.fromNamespaceAndName(sourceId, messagePayload.getSourceMessageId()).toString(); + final String conversationId = UUIDv5.fromNamespaceAndName(sourceId, messagePayload.getSourceConversationId()).toString(); + final String channelId = UUIDv5.fromNamespaceAndName(sourceId, messagePayload.getSourceChannelId()).toString(); + + final Message message = Message.newBuilder() + .setSource(sourceId) + .setSentAt(messagePayload.getSentAt()) + .setSenderId(messagePayload.getSourceSenderId()) + .setIsFromContact(messagePayload.isFromContact()) + .setId(messageId) + .setConversationId(conversationId) + .setHeaders(Map.of()) + .setDeliveryState(DeliveryState.DELIVERED) + .setContent(messagePayload.getContent().toString()) + .setChannelId(channelId) + .setUpdatedAt(null) + .build(); + + records.add(new ProducerRecord<>(applicationCommunicationMessages, message.getId(), message)); + } + + for (WebhookRequestPayload.MetadataPayload metadataPayload : payload.getMetadata()) { + final String namespace = metadataPayload.getNamespace(); + if (!allowedMetadataNamespaces.contains(namespace)) { + return ResponseEntity.badRequest().body(new RequestErrorResponsePayload("Unknown metadata namespace " + namespace)); + } + final String subjectId = UUIDv5.fromNamespaceAndName(sourceId, metadataPayload.getSourceId()).toString(); + final Subject subject = new Subject(namespace, subjectId); + + final List metadataList = MetadataObjectMapper.getMetadataFromJson(subject, metadataPayload.getMetadata()); + + for (Metadata metadata : metadataList) { + records.add(new ProducerRecord<>(applicationCommunicationMetadata, getId(metadata).toString(), metadata)); + } + } + + try { + for (ProducerRecord record : records) { + producer.send(record).get(); + } + + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); + } + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Actions.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Actions.java new file mode 100644 index 0000000000..5ba65791e1 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Actions.java @@ -0,0 +1,42 @@ +package co.airy.core.sources.api.actions; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.api.actions.dto.SendMessage; +import co.airy.core.sources.api.actions.payload.SendMessageResponsePayload; +import co.airy.model.metadata.MetadataKeys; +import org.apache.avro.specific.SpecificRecord; +import org.apache.kafka.streams.KeyValue; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static co.airy.model.message.MessageRepository.updateDeliveryState; +import static co.airy.model.metadata.MetadataRepository.getId; +import static co.airy.model.metadata.MetadataRepository.newMessageMetadata; + +@Service +public class Actions { + private final Endpoint endpoint; + + public Actions(Endpoint endpoint) { + this.endpoint = endpoint; + } + + public List> sendMessage(SendMessage sendMessage) { + final Message message = sendMessage.getMessage(); + + try { + final SendMessageResponsePayload response = endpoint.sendMessage(sendMessage); + final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.Source.ID, response.getSourceMessageId()); + updateDeliveryState(message, DeliveryState.DELIVERED); + + return List.of(KeyValue.pair(message.getId(), message), KeyValue.pair(getId(metadata).toString(), metadata)); + } catch (Exception e) { + final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.ERROR, e.getMessage()); + updateDeliveryState(message, DeliveryState.FAILED); + return List.of(KeyValue.pair(message.getId(), message), KeyValue.pair(getId(metadata).toString(), metadata)); + } + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/ApiException.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/ApiException.java new file mode 100644 index 0000000000..c6a694505f --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/ApiException.java @@ -0,0 +1,7 @@ +package co.airy.core.sources.api.actions; + +public class ApiException extends RuntimeException{ + public ApiException(String msg) { + super(msg); + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Endpoint.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Endpoint.java new file mode 100644 index 0000000000..03b26434ed --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/Endpoint.java @@ -0,0 +1,46 @@ +package co.airy.core.sources.api.actions; + +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.actions.dto.SendMessage; +import co.airy.core.sources.api.actions.payload.SendMessageRequestPayload; +import co.airy.core.sources.api.actions.payload.SendMessageResponsePayload; +import co.airy.crypto.Signature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; + +import static co.airy.crypto.Signature.getSignature; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +@Service +public class Endpoint { + private final ObjectMapper objectMapper; + private final RestTemplate restTemplate; + + public Endpoint(RestTemplate restTemplate, ObjectMapper objectMapper) { + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + public SendMessageResponsePayload sendMessage(SendMessage request) throws Exception { + final SendMessageRequestPayload requestPayload = SendMessageRequestPayload.fromSendMessage(request); + final String responseContent = sendRequest(requestPayload, request.getSource()); + return objectMapper.readValue(responseContent, SendMessageResponsePayload.class); + } + + private String sendRequest(Object payload, Source source) throws Exception { + final String content = objectMapper.writeValueAsString(payload); + final HttpHeaders headers = new HttpHeaders(); + headers.set(CONTENT_TYPE, APPLICATION_JSON.toString()); + final String signature = getSignature(source.getToken(), content); + headers.set(Signature.CONTENT_SIGNATURE_HEADER, signature); + + final HttpEntity entity = new HttpEntity<>(content, headers); + return restTemplate.postForObject(new URI(source.getActionEndpoint()), entity, String.class); + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/EndpointConfig.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/EndpointConfig.java new file mode 100644 index 0000000000..957ca4a323 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/EndpointConfig.java @@ -0,0 +1,44 @@ +package co.airy.core.sources.api.actions; + +import co.airy.core.sources.api.actions.payload.ErrorResponsePayload; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +@Configuration +public class EndpointConfig { + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, ObjectMapper objectMapper) { + return restTemplateBuilder + .errorHandler(new ResponseErrorHandler() { + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return response.getRawStatusCode() != HttpStatus.OK.value(); + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + final String responseContent = new String(response.getBody().readAllBytes()); + String errorMessage; + try { + final ErrorResponsePayload payload = objectMapper.readValue(responseContent, ErrorResponsePayload.class); + errorMessage = payload.getError(); + } catch (Exception e) { + errorMessage = String.format("Action endpoint returned invalid error: %s", e); + } + + throw new ApiException(errorMessage); + } + }) + .additionalMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) + .build(); + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/dto/SendMessage.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/dto/SendMessage.java new file mode 100644 index 0000000000..b1142ff7ad --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/dto/SendMessage.java @@ -0,0 +1,23 @@ +package co.airy.core.sources.api.actions.dto; + +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Source; +import co.airy.model.conversation.Conversation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +public class SendMessage implements Serializable { + private Conversation conversation; + private Source source; + @NotNull + private Message message; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ActionPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ActionPayload.java new file mode 100644 index 0000000000..e27366dc9d --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ActionPayload.java @@ -0,0 +1,11 @@ +package co.airy.core.sources.api.actions.payload; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = SendMessageRequestPayload.class, name = "message.send"), +}) +public abstract class ActionPayload { +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ErrorResponsePayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ErrorResponsePayload.java new file mode 100644 index 0000000000..fc4111ad66 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/ErrorResponsePayload.java @@ -0,0 +1,8 @@ +package co.airy.core.sources.api.actions.payload; + +import lombok.Data; + +@Data +public class ErrorResponsePayload { + private String error; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageRequestPayload.java new file mode 100644 index 0000000000..d46d8b74a9 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageRequestPayload.java @@ -0,0 +1,84 @@ +package co.airy.core.sources.api.actions.payload; + +import co.airy.avro.communication.Channel; +import co.airy.avro.communication.Message; +import co.airy.core.sources.api.actions.dto.SendMessage; +import co.airy.model.conversation.Conversation; +import co.airy.model.metadata.dto.MetadataMap; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Optional; + +import static co.airy.date.format.DateFormat.isoFromMillis; +import static co.airy.model.message.MessageRepository.resolveContent; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class SendMessageRequestPayload extends ActionPayload { + private Payload payload; + + @Data + @Builder + public static class Payload { + private MessagePayload message; + private ConversationPayload conversation; + } + + @Data + @Builder + public static class MessagePayload { + private String id; + private String sourceRecipientId; + private Object content; + private String sentAt; + } + + @Data + @Builder + public static class ConversationPayload { + private String id; + private String channelId; + private String sourceChannelId; + private String sourceConversationId; + private String createdAt; + + public static ConversationPayload fromConversation(Conversation conversation) { + return builder() + .id(conversation.getId()) + .channelId(conversation.getChannelId()) + .sourceConversationId(conversation.getSourceConversationId()) + .sourceChannelId(Optional.of(conversation.getChannel()).map(Channel::getSourceChannelId).orElse(null)) + .createdAt(isoFromMillis(conversation.getCreatedAt())) + .build(); + } + } + + public static SendMessageRequestPayload fromSendMessage(SendMessage sendMessage) { + final Message message = sendMessage.getMessage(); + final Conversation conversation = sendMessage.getConversation(); + + final MessagePayload messagePayload = MessagePayload.builder() + .id(message.getId()) + .sourceRecipientId(Optional.ofNullable(message.getSourceRecipientId()).orElseGet(() -> { + if (conversation != null) { + return conversation.getSourceConversationId(); + } + return null; + })) + .content(resolveContent(message, new MetadataMap())) + .sentAt(isoFromMillis(message.getSentAt())) + .build(); + + final Payload.PayloadBuilder builder = Payload.builder().message(messagePayload); + + if (conversation != null) { + builder.conversation(ConversationPayload.fromConversation(sendMessage.getConversation())); + } + + return new SendMessageRequestPayload(builder.build()); + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageResponsePayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageResponsePayload.java new file mode 100644 index 0000000000..65b6364e1a --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/actions/payload/SendMessageResponsePayload.java @@ -0,0 +1,10 @@ +package co.airy.core.sources.api.actions.payload; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class SendMessageResponsePayload { + private String sourceMessageId; + private JsonNode metadata; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ChannelsResponsePayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ChannelsResponsePayload.java new file mode 100644 index 0000000000..263a1b8835 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ChannelsResponsePayload.java @@ -0,0 +1,15 @@ +package co.airy.core.sources.api.payload; + +import co.airy.model.channel.ChannelPayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChannelsResponsePayload { + private List data; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateChannelRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateChannelRequestPayload.java new file mode 100644 index 0000000000..bd0115eba3 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateChannelRequestPayload.java @@ -0,0 +1,19 @@ +package co.airy.core.sources.api.payload; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateChannelRequestPayload { + @NotNull + private String name; + @NotNull + private String sourceChannelId; + private JsonNode metadata; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateSourceRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateSourceRequestPayload.java new file mode 100644 index 0000000000..7781f7d83d --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/CreateSourceRequestPayload.java @@ -0,0 +1,19 @@ +package co.airy.core.sources.api.payload; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.net.URL; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateSourceRequestPayload { + @NotNull + private String sourceId; + private String name; + private String imageUrl; + private URL actionEndpoint; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DeleteSourceRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DeleteSourceRequestPayload.java new file mode 100644 index 0000000000..b9d50a3471 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DeleteSourceRequestPayload.java @@ -0,0 +1,15 @@ +package co.airy.core.sources.api.payload; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeleteSourceRequestPayload { + @NotNull + private String sourceId; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DisconnectChannelRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DisconnectChannelRequestPayload.java new file mode 100644 index 0000000000..0a33ee67b7 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/DisconnectChannelRequestPayload.java @@ -0,0 +1,16 @@ +package co.airy.core.sources.api.payload; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DisconnectChannelRequestPayload { + @NotNull + private UUID channelId; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ListSourceResponsePayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ListSourceResponsePayload.java new file mode 100644 index 0000000000..7fabe41c07 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/ListSourceResponsePayload.java @@ -0,0 +1,12 @@ +package co.airy.core.sources.api.payload; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ListSourceResponsePayload { + private List data; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/SourceResponsePayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/SourceResponsePayload.java new file mode 100644 index 0000000000..a7ca9f16a2 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/SourceResponsePayload.java @@ -0,0 +1,14 @@ +package co.airy.core.sources.api.payload; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SourceResponsePayload { + private String sourceId; + private String token; + private String actionEndpoint; + private String name; + private String imageUrl; +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/WebhookRequestPayload.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/WebhookRequestPayload.java new file mode 100644 index 0000000000..22b9bb7fde --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/payload/WebhookRequestPayload.java @@ -0,0 +1,58 @@ +package co.airy.core.sources.api.payload; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +import static co.airy.date.format.DateFormat.instantFromIso; + +@Data +public class WebhookRequestPayload { + @Valid + @NotNull + private List messages = List.of(); + @Valid + @NotNull + private List metadata = List.of(); + + @Data + public static class MessagePayload { + @NotNull + private String sourceMessageId; + @NotNull + private String sourceConversationId; + @NotNull + private String sourceChannelId; + @NotNull + private String sourceSenderId; + @NotNull + private JsonNode content; + @NotNull + private boolean fromContact; + @NotNull + private JsonNode sentAt; + + public long getSentAt() { + if (sentAt.isNumber()) { + return sentAt.longValue(); + } + if (sentAt.isTextual()) { + return instantFromIso(sentAt.textValue()).toEpochMilli(); + } + throw new IllegalArgumentException("sentAt must be either a UNIX timestamp or an ISO8601 string."); + } + } + + @Data + public static class MetadataPayload { + @NotNull + private String namespace; + @NotNull + private String sourceId; + @NotNull + private JsonNode metadata; + } +} diff --git a/backend/sources/api/src/main/java/co/airy/core/sources/api/services/SourceToken.java b/backend/sources/api/src/main/java/co/airy/core/sources/api/services/SourceToken.java new file mode 100644 index 0000000000..452690de62 --- /dev/null +++ b/backend/sources/api/src/main/java/co/airy/core/sources/api/services/SourceToken.java @@ -0,0 +1,73 @@ +package co.airy.core.sources.api.services; + +import co.airy.avro.communication.Source; +import co.airy.core.sources.api.Stores; +import co.airy.spring.auth.Jwt; +import co.airy.spring.auth.token.TokenAuth; +import co.airy.spring.auth.token.TokenProfile; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Map; + +@Service +public class SourceToken { + private final Jwt jwt; + private final Stores stores; + private final String sourceKey = "source"; + private final String identityError = "Wrong identity used. Use the authentication token you obtained for your app from the /sources.create endpoint. See https://docs.airy.co/api/source#create-a-source"; + + public SourceToken(@Value("${jwtSecret}") String jwtSecret, Stores stores) { + jwt = new Jwt(jwtSecret); + this.stores = stores; + } + + public String getSourceToken(String sourceId) { + final TokenProfile tokenProfile = TokenProfile.builder() + .name(String.format("source_app:%s", sourceId)) + .data(Map.of(sourceKey, sourceId)).build(); + try { + // Source tokens don't expire + return jwt.getAuthToken(new TokenAuth(tokenProfile), null); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public void rejectSourceAuth(Authentication authentication) throws ResponseStatusException { + if (!(authentication instanceof TokenAuth)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + + Map tokenData = ((TokenAuth) authentication).getProfile().getData(); + if (tokenData.containsKey(sourceKey)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, identityError); + } + } + + public Source getSource(Authentication authentication) throws ResponseStatusException { + final String sourceId = getSourceId(authentication); + final Source source = stores.getSource(sourceId); + if (source == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Source not found"); + } + return source; + } + + private String getSourceId(Authentication authentication) throws ResponseStatusException { + if (!(authentication instanceof TokenAuth)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, identityError); + } + + Map tokenData = ((TokenAuth) authentication).getProfile().getData(); + if (!tokenData.containsKey(sourceKey)) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, identityError); + } + + return tokenData.get(sourceKey); + } +} diff --git a/backend/sources/api/src/test/java/co/airy/core/sources/api/ChannelsControllerTest.java b/backend/sources/api/src/test/java/co/airy/core/sources/api/ChannelsControllerTest.java new file mode 100644 index 0000000000..8d6e0abb21 --- /dev/null +++ b/backend/sources/api/src/test/java/co/airy/core/sources/api/ChannelsControllerTest.java @@ -0,0 +1,102 @@ +package co.airy.core.sources.api; + +import co.airy.core.sources.api.util.TestSource; +import co.airy.core.sources.api.util.Topics; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import co.airy.spring.test.WebTestHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static co.airy.test.Timing.retryOnException; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(properties = { + "systemToken=user-generated-api-token", + "jwtSecret=long-randomly-generated-secret-used-as-jwt-secret-key", +}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +public class ChannelsControllerTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + + @Autowired + private WebTestHelper webTestHelper; + + @Autowired + private MockMvc mvc; + + @Autowired + private TestSource testSource; + + @Value("${systemToken}") + private String systemToken; + + private static KafkaTestHelper kafkaTestHelper; + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @BeforeEach + void beforeEach() throws Exception { + webTestHelper.waitUntilHealthy(); + } + + @Test + void canCreateChannel() throws Exception { + final String sourceId = "my-source"; + final String token = testSource.createSourceAndGetToken(sourceId); + + final String channelPayload = "{\"name\":\"source channel\",\"source_channel_id\":\"my-source-channel-1\"," + + "\"metadata\":{\"my_key\":\"my value\"}}"; + + mvc.perform(MockMvcRequestBuilders.post("/sources.channels.create") + .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + .content(channelPayload)) + .andExpect(status().isForbidden()); + + // Cannot use system token + mvc.perform(MockMvcRequestBuilders.post("/sources.channels.create") + .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + .header(AUTHORIZATION, systemToken) + .content(channelPayload)) + .andExpect(status().isForbidden()); + + retryOnException(() -> mvc.perform(MockMvcRequestBuilders.post("/sources.channels.create") + .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + .header(AUTHORIZATION, token) + .content(channelPayload)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.metadata.name", equalTo("source channel"))) + .andExpect(jsonPath("$.metadata.my_key", equalTo("my value"))) + .andExpect(jsonPath("$.source", equalTo(sourceId))), "Channel was not created"); + } +} diff --git a/backend/sources/api/src/test/java/co/airy/core/sources/api/SendMessageTest.java b/backend/sources/api/src/test/java/co/airy/core/sources/api/SendMessageTest.java new file mode 100644 index 0000000000..d63f89356b --- /dev/null +++ b/backend/sources/api/src/test/java/co/airy/core/sources/api/SendMessageTest.java @@ -0,0 +1,145 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.core.sources.api.util.TestSource; +import co.airy.core.sources.api.util.Topics; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import co.airy.spring.test.WebTestHelper; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.security.InvalidKeyException; +import java.time.Instant; +import java.util.UUID; + +import static co.airy.crypto.Signature.CONTENT_SIGNATURE_HEADER; +import static co.airy.crypto.Signature.getSignature; +import static co.airy.test.Timing.retryOnException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.client.ExpectedCount.once; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +@SpringBootTest(properties = { + "systemToken=user-generated-api-token", + "jwtSecret=long-randomly-generated-secret-used-as-jwt-secret-key", +}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +public class SendMessageTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + + @Autowired + private WebTestHelper webTestHelper; + + @Autowired + private MockMvc mvc; + + @Autowired + private TestSource testSource; + + @Autowired + private RestTemplate restTemplate; + + private MockRestServiceServer mockServer; + + private static KafkaTestHelper kafkaTestHelper; + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @BeforeEach + void beforeEach() throws Exception { + webTestHelper.waitUntilHealthy(); + mockServer = MockRestServiceServer.createServer(restTemplate); + } + + @Test + void canSendMessage() throws Exception { + // Create source + final String actionEndpoint = "http://customer-endpoint.com/webhook"; + final String sourceId = "my-source"; + final String token = testSource.createSourceAndGetToken(sourceId, actionEndpoint); + final String messageId = UUID.randomUUID().toString(); + + final String sourceConversationId = "source-conversation-id"; + // Create conversation + kafkaTestHelper.produceRecord( + new ProducerRecord<>(Topics.applicationCommunicationMessages.name(), "other-message-id", + Message.newBuilder() + .setId("other-message-id") + .setSource(sourceId) + .setSentAt(Instant.now().toEpochMilli()) + .setSenderId(sourceConversationId) + .setDeliveryState(DeliveryState.DELIVERED) + .setConversationId("conversationId") + .setChannelId("channelId") + .setContent("Hi") + .setIsFromContact(true) + .build()) + ); + + mockServer.expect(once(), requestTo(new URI(actionEndpoint))) + .andExpect(method(HttpMethod.POST)) + .andExpect((request) -> { + MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; + final String expectedSignature = mustGetSignature(token, mockRequest.getBodyAsString()); + assertThat(mockRequest.getHeaders().get(CONTENT_SIGNATURE_HEADER).get(0), equalTo(expectedSignature)); + }) + .andRespond(withSuccess()); + + kafkaTestHelper.produceRecord(new ProducerRecord<>(Topics.applicationCommunicationMessages.name(), messageId, + Message.newBuilder() + .setId(messageId) + .setSource(sourceId) + .setSentAt(Instant.now().toEpochMilli()) + .setSenderId(sourceConversationId) + .setDeliveryState(DeliveryState.PENDING) + .setConversationId("conversationId") + .setChannelId("channelId") + .setContent("Hi") + .setIsFromContact(false) + .build())); + + retryOnException(() -> mockServer.verify(), "Endpoint was not called", 10_000); + } + + private String mustGetSignature(String key, String content) { + try { + return getSignature(key, content); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/sources/api/src/test/java/co/airy/core/sources/api/WebhookControllerTest.java b/backend/sources/api/src/test/java/co/airy/core/sources/api/WebhookControllerTest.java new file mode 100644 index 0000000000..7d618e7f76 --- /dev/null +++ b/backend/sources/api/src/test/java/co/airy/core/sources/api/WebhookControllerTest.java @@ -0,0 +1,113 @@ +package co.airy.core.sources.api; + +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.api.util.TestSource; +import co.airy.core.sources.api.util.Topics; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import co.airy.spring.test.WebTestHelper; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.StreamUtils; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static co.airy.model.metadata.MetadataRepository.getSubject; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(properties = { + "systemToken=user-generated-api-token", + "jwtSecret=long-randomly-generated-secret-used-as-jwt-secret-key", +}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +public class WebhookControllerTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + + @Autowired + private WebTestHelper webTestHelper; + + @Autowired + private MockMvc mvc; + + @Autowired + private TestSource testSource; + + private static KafkaTestHelper kafkaTestHelper; + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @BeforeEach + void beforeEach() throws Exception { + webTestHelper.waitUntilHealthy(); + } + + @Test + void canIngestData() throws Exception { + final String sourceId = "my-source"; + final String token = testSource.createSourceAndGetToken(sourceId); + + // Only accepts conversation and message metadata + final String invalidMetadata = "{\"metadata\":[{\"namespace\":\"channel\",\"source_id\":\"source-id\",\"metadata\":{}}]}"; + mvc.perform(MockMvcRequestBuilders.post("/sources.webhook") + .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + .header(AUTHORIZATION, token) + .content(invalidMetadata)) + .andExpect(status().isBadRequest()); + + final String webhookPayload = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("webhook.json"), StandardCharsets.UTF_8); + mvc.perform(MockMvcRequestBuilders.post("/sources.webhook") + .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + .header(AUTHORIZATION, token) + .content(webhookPayload)) + .andExpect(status().isOk()); + + final List> messageRecords = kafkaTestHelper.consumeRecords(1, Topics.applicationCommunicationMessages.name()); + assertThat(messageRecords, hasSize(1)); + final Message message = messageRecords.get(0).value(); + assertThat(message.getSource(), equalTo(sourceId)); + final String messageId = message.getId(); + final String conversationId = message.getConversationId(); + + final List> metadataRecords = kafkaTestHelper.consumeRecords(2, Topics.applicationCommunicationMetadata.name()); + assertThat(metadataRecords, hasSize(2)); + + final ConsumerRecord conversationMetadata = metadataRecords.stream().filter((metadata) -> getSubject(metadata.value()).getNamespace().equals("conversation")).findFirst().get(); + assertThat(getSubject(conversationMetadata.value()).getIdentifier(), equalTo(conversationId)); + + final ConsumerRecord messageMetadata = metadataRecords.stream().filter((metadata) -> getSubject(metadata.value()).getNamespace().equals("message")).findFirst().get(); + assertThat(getSubject(messageMetadata.value()).getIdentifier(), equalTo(messageId)); + } +} diff --git a/backend/sources/api/src/test/java/co/airy/core/sources/api/util/TestSource.java b/backend/sources/api/src/test/java/co/airy/core/sources/api/util/TestSource.java new file mode 100644 index 0000000000..9998373263 --- /dev/null +++ b/backend/sources/api/src/test/java/co/airy/core/sources/api/util/TestSource.java @@ -0,0 +1,48 @@ +package co.airy.core.sources.api.util; + +import co.airy.spring.test.WebTestHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static co.airy.test.Timing.retryOnException; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Component +public class TestSource { + @Autowired + WebTestHelper webTestHelper; + + public String createSourceAndGetToken(String sourceId) throws Exception { + return createSourceAndGetToken(sourceId, null); + } + + public String createSourceAndGetToken(String sourceId, String actionEndpoint) throws Exception { + String payload = String.format("{\"source_id\":\"%s\"}", sourceId); + if (actionEndpoint != null) { + payload = String.format("{\"source_id\":\"%s\",\"action_endpoint\":\"%s\"}", sourceId, actionEndpoint); + } + + final String response = webTestHelper.post("/sources.create", payload) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.source_id", is(not(nullValue())))) + .andExpect(jsonPath("$.token", is(not(nullValue())))) + .andReturn().getResponse().getContentAsString(); + + retryOnException(() -> webTestHelper.post("/sources.list") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[*].source_id", contains(equalTo(sourceId)))), + "Source did not show up in store" + ); + + final JsonNode node = new ObjectMapper().readTree(response); + return node.get("token").textValue(); + } +} diff --git a/backend/sources/api/src/test/java/co/airy/core/sources/api/util/Topics.java b/backend/sources/api/src/test/java/co/airy/core/sources/api/util/Topics.java new file mode 100644 index 0000000000..72ceabca47 --- /dev/null +++ b/backend/sources/api/src/test/java/co/airy/core/sources/api/util/Topics.java @@ -0,0 +1,18 @@ +package co.airy.core.sources.api.util; + +import co.airy.kafka.schema.Topic; +import co.airy.kafka.schema.application.ApplicationCommunicationChannels; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.schema.application.ApplicationCommunicationSources; + +public class Topics { + public static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); + public static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); + public static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); + public static final ApplicationCommunicationSources applicationCommunicationSources = new ApplicationCommunicationSources(); + + public static Topic[] getTopics() { + return new Topic[]{applicationCommunicationMessages, applicationCommunicationChannels, applicationCommunicationMetadata, applicationCommunicationSources}; + } +} diff --git a/backend/sources/api/src/test/resources/test.properties b/backend/sources/api/src/test/resources/test.properties new file mode 100644 index 0000000000..1d10a69841 --- /dev/null +++ b/backend/sources/api/src/test/resources/test.properties @@ -0,0 +1,2 @@ +kafka.cleanup=true +kafka.commit-interval-ms=100 diff --git a/backend/sources/api/src/test/resources/webhook.json b/backend/sources/api/src/test/resources/webhook.json new file mode 100644 index 0000000000..0abbc44561 --- /dev/null +++ b/backend/sources/api/src/test/resources/webhook.json @@ -0,0 +1,33 @@ +{ + "messages": [ + { + "source_message_id": "source-message-id", + "source_conversation_id": "source-conversation-id", + "source_channel_id": "source-channel-id", + "source_sender_id": "source-sender-id", + "content": "Hello world", + "from_contact": true, + "sent_at": 1603661094560 + } + ], + "metadata": [ + { + "namespace": "conversation", + "source_id": "source-conversation-id", + "metadata": { + "contact": { + "display_name": "Margaret Hamilton" + } + } + }, + { + "namespace": "message", + "source_id": "source-message-id", + "metadata": { + "reaction": { + "emoji": "❤️" + } + } + } + ] +} diff --git a/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/ChatController.java b/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/ChatController.java index 2dfe02db57..4b8e45dc2c 100644 --- a/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/ChatController.java +++ b/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/ChatController.java @@ -48,9 +48,9 @@ public ChatController(Stores stores, Jwt jwt, ObjectMapper objectMapper, @Value( } @PostMapping("/chatplugin.authenticate") - ResponseEntity authenticateVisitor(@RequestBody @Valid AuthenticationRequestPayload requestPayload) { - final UUID channelId = requestPayload.getChannelId(); - final String resumeToken = requestPayload.getResumeToken(); + ResponseEntity authenticateVisitor(@RequestBody @Valid AuthenticationRequestPayload payload) { + final UUID channelId = payload.getChannelId(); + final String resumeToken = payload.getResumeToken(); Principal principal; List messages = List.of(); @@ -86,27 +86,27 @@ private Principal createConversation(String channelId) { @PostMapping("/chatplugin.resumeToken") ResponseEntity getResumeToken( - @Valid @RequestBody(required = false) ResumeTokenRequestPayload requestPayload, + @Valid @RequestBody(required = false) ResumeTokenRequestPayload payload, @RequestHeader(value = "Authorization") String authHeader) { - final Principal principal = getUserOrSystemPrincipal(requestPayload, authHeader); + final Principal principal = getUserOrSystemPrincipal(payload, authHeader); final String resumeToken = jwt.getResumeToken(principal.getConversationId(), principal.getChannelId()); return ResponseEntity.ok(new ResumeTokenResponsePayload(resumeToken)); } - private Principal getUserOrSystemPrincipal(ResumeTokenRequestPayload requestPayload, String authHeader) { + private Principal getUserOrSystemPrincipal(ResumeTokenRequestPayload payload, String authHeader) { final String requestToken = getAuthToken(authHeader); if (apiToken != null && apiToken.equals(requestToken)) { - if (requestPayload == null) { + if (payload == null) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST); } - return new Principal(requestPayload.getChannelId(), requestPayload.getConversationId()); + return new Principal(payload.getChannelId(), payload.getConversationId()); } return jwt.authenticate(requestToken); } @PostMapping("/chatplugin.send") - ResponseEntity sendMessage(@RequestBody @Valid SendMessageRequestPayload requestPayload, Authentication authentication) { + ResponseEntity sendMessage(@RequestBody @Valid SendMessageRequestPayload payload, Authentication authentication) { final Principal principal = (Principal) authentication.getPrincipal(); final String channelId = principal.getChannelId(); final Channel channel = stores.getChannel(channelId); @@ -119,7 +119,7 @@ ResponseEntity sendMessage(@RequestBody @Valid SendMessageRequestPayload requ final Message message = Message.newBuilder() .setId(UUID.randomUUID().toString()) .setChannelId(channel.getId()) - .setContent(objectMapper.writeValueAsString(requestPayload.getMessage())) + .setContent(objectMapper.writeValueAsString(payload.getMessage())) .setConversationId(principal.getName()) .setHeaders(Map.of()) .setDeliveryState(DeliveryState.DELIVERED) diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java index 4d9c694af6..2ea23ea60a 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java @@ -45,9 +45,9 @@ public ChannelsController(Api api, Stores stores) { } @PostMapping("/channels.facebook.explore") - ResponseEntity explore(@RequestBody @Valid ExploreRequestPayload requestPayload) { + ResponseEntity explore(@RequestBody @Valid ExploreRequestPayload payload) { try { - final List pagesInfo = api.getPagesInfo(requestPayload.getAuthToken()); + final List pagesInfo = api.getPagesInfo(payload.getAuthToken()); final KeyValueIterator iterator = stores.getChannelsStore().all(); @@ -80,17 +80,17 @@ ResponseEntity explore(@RequestBody @Valid ExploreRequestPayload requestPaylo } @PostMapping("/channels.facebook.connect") - ResponseEntity connectFacebook(@RequestBody @Valid ConnectPageRequestPayload requestPayload) { - final String token = requestPayload.getPageToken(); - final String pageId = requestPayload.getPageId(); + ResponseEntity connectFacebook(@RequestBody @Valid ConnectPageRequestPayload payload) { + final String token = payload.getPageToken(); + final String pageId = payload.getPageId(); final String channelId = UUIDv5.fromNamespaceAndName("facebook", pageId).toString(); try { final String longLivingUserToken = api.exchangeToLongLivingUserAccessToken(token); - final PageWithConnectInfo fbPageWithConnectInfo = api.getPageForUser(pageId, longLivingUserToken); + final PageWithConnectInfo pageWithConnectInfo = api.getPageForUser(pageId, longLivingUserToken); - api.connectPageToApp(fbPageWithConnectInfo.getAccessToken()); + api.connectPageToApp(pageWithConnectInfo.getAccessToken()); final ChannelContainer container = ChannelContainer.builder() .channel( @@ -103,8 +103,8 @@ ResponseEntity connectFacebook(@RequestBody @Valid ConnectPageRequestPayload .build() ) .metadataMap(MetadataMap.from(List.of( - newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, Optional.ofNullable(requestPayload.getName()).orElse(fbPageWithConnectInfo.getNameWithLocationDescriptor())), - newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, Optional.ofNullable(requestPayload.getImageUrl()).orElse(fbPageWithConnectInfo.getPicture().getData().getUrl())) + newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, Optional.ofNullable(payload.getName()).orElse(pageWithConnectInfo.getNameWithLocationDescriptor())), + newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, Optional.ofNullable(payload.getImageUrl()).orElse(pageWithConnectInfo.getPicture().getData().getUrl())) ))).build(); stores.storeChannelContainer(container); @@ -118,24 +118,24 @@ ResponseEntity connectFacebook(@RequestBody @Valid ConnectPageRequestPayload } @PostMapping("/channels.instagram.connect") - ResponseEntity connectInstagram(@RequestBody @Valid ConnectInstagramRequestPayload requestPayload) { - final String token = requestPayload.getPageToken(); - final String pageId = requestPayload.getPageId(); - final String accountId = requestPayload.getAccountId(); + ResponseEntity connectInstagram(@RequestBody @Valid ConnectInstagramRequestPayload payload) { + final String token = payload.getPageToken(); + final String pageId = payload.getPageId(); + final String accountId = payload.getAccountId(); final String channelId = UUIDv5.fromNamespaceAndName("instagram", accountId).toString(); try { final String longLivingUserToken = api.exchangeToLongLivingUserAccessToken(token); - final PageWithConnectInfo fbPageWithConnectInfo = api.getPageForUser(pageId, longLivingUserToken); + final PageWithConnectInfo pageWithConnectInfo = api.getPageForUser(pageId, longLivingUserToken); - api.connectPageToApp(fbPageWithConnectInfo.getAccessToken()); + api.connectPageToApp(pageWithConnectInfo.getAccessToken()); final MetadataMap metadataMap = MetadataMap.from(List.of( - newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, Optional.ofNullable(requestPayload.getName()).orElse(String.format("%s Instagram account", fbPageWithConnectInfo.getNameWithLocationDescriptor()))) + newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, Optional.ofNullable(payload.getName()).orElse(String.format("%s Instagram account", pageWithConnectInfo.getNameWithLocationDescriptor()))) )); - Optional.ofNullable(requestPayload.getImageUrl()) + Optional.ofNullable(payload.getImageUrl()) .ifPresent((imageUrl) -> { final Metadata metadata = newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, imageUrl); metadataMap.put(metadata.getKey(), metadata); @@ -164,8 +164,8 @@ ResponseEntity connectInstagram(@RequestBody @Valid ConnectInstagramRequestPa } @PostMapping(path = {"/channels.facebook.disconnect", "/channels.instagram.disconnect"}) - ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final Channel channel = stores.getChannelsStore().get(channelId); diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Connector.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Connector.java index 40ccbd3fc4..419fd8ebf1 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Connector.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Connector.java @@ -61,9 +61,9 @@ public List> sendMessage(SendMessageRequest try { final String pageToken = conversation.getChannel().getToken(); - final SendMessagePayload fbSendMessagePayload = mapper.fromSendMessageRequest(sendMessageRequest); + final SendMessagePayload payload = mapper.fromSendMessageRequest(sendMessageRequest); - final SendMessageResponse response = api.sendMessage(pageToken, fbSendMessagePayload); + final SendMessageResponse response = api.sendMessage(pageToken, payload); final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.Source.ID, response.getMessageId()); updateDeliveryState(message, DeliveryState.DELIVERED); diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java index 1733a52bcc..33cb25e559 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java @@ -8,16 +8,11 @@ import co.airy.core.sources.facebook.api.model.SendMessagePayload; import co.airy.core.sources.facebook.api.model.SendMessageResponse; import co.airy.core.sources.facebook.api.model.UserProfile; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.ApplicationListener; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -46,7 +41,6 @@ public class Api implements ApplicationListener { private final RestTemplateBuilder restTemplateBuilder; private final ObjectMapper objectMapper; - private RestTemplate restTemplate; private static final String subscribedFields = "messages,messaging_postbacks,messaging_optins,message_deliveries,message_reads,messaging_payments,messaging_pre_checkouts,messaging_checkout_updates,messaging_account_linking,messaging_referrals,message_echoes,messaging_game_plays,standby,messaging_handovers,messaging_policy_enforcement,message_reactions,inbox_labels,message_reactions"; @@ -73,8 +67,8 @@ public Api(ObjectMapper objectMapper, RestTemplateBuilder restTemplateBuilder, } public SendMessageResponse sendMessage(final String pageToken, SendMessagePayload sendMessagePayload) { - String fbReqUrl = String.format(requestTemplate, pageToken); - final ResponseEntity responseEntity = restTemplate.postForEntity(fbReqUrl, new HttpEntity<>(sendMessagePayload, httpHeaders), SendMessageResponse.class); + String reqUrl = String.format(requestTemplate, pageToken); + final ResponseEntity responseEntity = restTemplate.postForEntity(reqUrl, new HttpEntity<>(sendMessagePayload, httpHeaders), SendMessageResponse.class); return responseEntity.getBody(); } @@ -84,13 +78,13 @@ public List getPagesInfo(String accessToken) throws Excepti boolean hasMorePages = true; List pageList = new ArrayList<>(); while (hasMorePages) { - Pages fbPages = apiResponse(pagesUrl, HttpMethod.GET, Pages.class); - if (fbPages.getPaging() != null && fbPages.getPaging().getNext() != null) { - pagesUrl = URLDecoder.decode(fbPages.getPaging().getNext(), StandardCharsets.UTF_8); + Pages pages = apiResponse(pagesUrl, HttpMethod.GET, Pages.class); + if (pages.getPaging() != null && pages.getPaging().getNext() != null) { + pagesUrl = URLDecoder.decode(pages.getPaging().getNext(), StandardCharsets.UTF_8); } else { hasMorePages = false; } - pageList.addAll(fbPages.getData()); + pageList.addAll(pages.getData()); } return pageList; } diff --git a/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java b/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java index 6e62a0db64..cbbd660c34 100644 --- a/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java +++ b/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java @@ -41,7 +41,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; @SpringBootTest(classes = AirySpringBootApplication.class) diff --git a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java index 79561a5850..b3e64d4124 100644 --- a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java +++ b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java @@ -65,7 +65,7 @@ public void startStream() { // Facebook to Airy message id lookup table builder.table(new ApplicationCommunicationMetadata().name()) - .filter((metadataId, metadata) -> metadata.getKey().equals(MetadataKeys.MessageKeys.Source.DELIVERY_STATE)) + .filter((metadataId, metadata) -> metadata.getKey().equals(MetadataKeys.MessageKeys.Source.ID)) .groupBy((metadataId, metadata) -> KeyValue.pair(metadata.getValue(), metadata)) .reduce((oldValue, newValue) -> newValue, (oldValue, reduceValue) -> reduceValue, Materialized.as(metadataStore)); diff --git a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/MessageMapper.java b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/MessageMapper.java index 8ffd534ecd..d497d7adde 100644 --- a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/MessageMapper.java +++ b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/MessageMapper.java @@ -136,8 +136,8 @@ private List> getReaction(String mess if (!reaction.get("action").textValue().equals("react")) { // unreact return List.of( - new ProducerRecord<>(applicationCommunicationMetadata, getId(new Subject("message", messageId), MetadataKeys.ConversationKeys.Reaction.EMOJI).toString(), null), - new ProducerRecord<>(applicationCommunicationMetadata, getId(new Subject("message", messageId), MetadataKeys.ConversationKeys.Reaction.SENT_AT).toString(), null) + new ProducerRecord<>(applicationCommunicationMetadata, getId(new Subject("message", messageId), MetadataKeys.MessageKeys.Reaction.EMOJI).toString(), null), + new ProducerRecord<>(applicationCommunicationMetadata, getId(new Subject("message", messageId), MetadataKeys.MessageKeys.Reaction.SENT_AT).toString(), null) ); } @@ -146,9 +146,9 @@ private List> getReaction(String mess throw new Exception(String.format("Could not convert reaction emoji \"%s\" to string.", emojiString)); } - Metadata emoji = newMessageMetadata(messageId, MetadataKeys.ConversationKeys.Reaction.EMOJI, emojiString); + Metadata emoji = newMessageMetadata(messageId, MetadataKeys.MessageKeys.Reaction.EMOJI, emojiString); - final Metadata sentAt = newMessageMetadata(messageId, MetadataKeys.ConversationKeys.Reaction.SENT_AT, + final Metadata sentAt = newMessageMetadata(messageId, MetadataKeys.MessageKeys.Reaction.SENT_AT, String.valueOf(rootNode.get("timestamp").longValue())); return List.of( diff --git a/backend/sources/facebook/events-router/src/test/java/co/airy/core/sources/facebook/EventsRouterTest.java b/backend/sources/facebook/events-router/src/test/java/co/airy/core/sources/facebook/EventsRouterTest.java index f010c841c6..036dbd2908 100644 --- a/backend/sources/facebook/events-router/src/test/java/co/airy/core/sources/facebook/EventsRouterTest.java +++ b/backend/sources/facebook/events-router/src/test/java/co/airy/core/sources/facebook/EventsRouterTest.java @@ -39,7 +39,6 @@ import static co.airy.test.Timing.retryOnException; import static org.apache.kafka.streams.KafkaStreams.State.RUNNING; import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -177,7 +176,7 @@ void parsesEventsCorrectly() throws Exception { List metadataList = kafkaTestHelper.consumeValues(2, applicationCommunicationMetadata.name()); assertThat(metadataList, hasSize(2)); assertTrue(metadataList.stream().anyMatch((metadata -> - metadata.getKey().equals(MetadataKeys.ConversationKeys.Reaction.EMOJI) && + metadata.getKey().equals(MetadataKeys.MessageKeys.Reaction.EMOJI) && metadata.getValue().equals("❤️") ))); } diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java index 6bc1fbb52c..9c4697f711 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java @@ -36,18 +36,18 @@ public ChannelsController(Stores stores) { } @PostMapping("/channels.google.connect") - ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload requestPayload) { - final String gbmId = requestPayload.getGbmId(); + ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload payload) { + final String gbmId = payload.getGbmId(); final String sourceIdentifier = "google"; final String channelId = UUIDv5.fromNamespaceAndName(sourceIdentifier, gbmId).toString(); try { List metadataList = new ArrayList<>(); - metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, requestPayload.getName())); + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, payload.getName())); - if (requestPayload.getImageUrl() != null) { - metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, requestPayload.getImageUrl())); + if (payload.getImageUrl() != null) { + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, payload.getImageUrl())); } final ChannelContainer container = ChannelContainer.builder() @@ -70,8 +70,8 @@ ResponseEntity connect(@RequestBody @Valid ConnectChannelRequestPayload reque } @PostMapping("/channels.google.disconnect") - ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final Channel channel = stores.getChannelsStore().get(channelId); diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java index 925a1d5956..edf6dbf62f 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java @@ -35,22 +35,22 @@ public ChannelsController(Stores stores) { } @PostMapping("/channels.twilio.sms.connect") - ResponseEntity connectSms(@RequestBody @Valid ConnectChannelRequestPayload requestPayload) { - final String channelId = UUIDv5.fromNamespaceAndName("twilio.sms", requestPayload.getPhoneNumber()).toString(); + ResponseEntity connectSms(@RequestBody @Valid ConnectChannelRequestPayload payload) { + final String channelId = UUIDv5.fromNamespaceAndName("twilio.sms", payload.getPhoneNumber()).toString(); final Channel channel = Channel.newBuilder() .setId(channelId) .setConnectionState(ChannelConnectionState.CONNECTED) .setSource("twilio.sms") - .setSourceChannelId(requestPayload.getPhoneNumber()) + .setSourceChannelId(payload.getPhoneNumber()) .build(); - return connectChannel(channel, requestPayload.getName(), requestPayload.getImageUrl()); + return connectChannel(channel, payload.getName(), payload.getImageUrl()); } @PostMapping("/channels.twilio.whatsapp.connect") - ResponseEntity connectWhatsapp(@RequestBody @Valid ConnectChannelRequestPayload requestPayload) { - final String phoneNumber = "whatsapp:" + requestPayload.getPhoneNumber(); + ResponseEntity connectWhatsapp(@RequestBody @Valid ConnectChannelRequestPayload payload) { + final String phoneNumber = "whatsapp:" + payload.getPhoneNumber(); final String channelId = UUIDv5.fromNamespaceAndName("twilio.whatsapp", phoneNumber).toString(); final Channel channel = Channel.newBuilder() @@ -60,7 +60,7 @@ ResponseEntity connectWhatsapp(@RequestBody @Valid ConnectChannelRequestPaylo .setSourceChannelId(phoneNumber) .build(); - return connectChannel(channel, requestPayload.getName(), requestPayload.getImageUrl()); + return connectChannel(channel, payload.getName(), payload.getImageUrl()); } private ResponseEntity connectChannel(Channel channel, String name, String imageUrl) { @@ -84,17 +84,17 @@ private ResponseEntity connectChannel(Channel channel, String name, String im } @PostMapping("/channels.twilio.sms.disconnect") - ResponseEntity disconnectSms(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - return disconnect(requestPayload); + ResponseEntity disconnectSms(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + return disconnect(payload); } @PostMapping("/channels.twilio.whatsapp.disconnect") - ResponseEntity disconnectWhatsapp(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - return disconnect(requestPayload); + ResponseEntity disconnectWhatsapp(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + return disconnect(payload); } - private ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + private ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final Channel channel = stores.getChannelsStore().get(channelId); diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java index 03f4b58629..782425b0d1 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java @@ -7,8 +7,6 @@ import co.airy.log.AiryLoggerFactory; import co.airy.spring.auth.IgnoreAuthPattern; import co.airy.spring.web.filters.RequestLoggingIgnorePatterns; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.twilio.exception.ApiException; import org.slf4j.Logger; import org.springframework.context.annotation.Bean; diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/services/Api.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/services/Api.java index 41031c69f2..96e6496878 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/services/Api.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/services/Api.java @@ -1,10 +1,8 @@ package co.airy.core.sources.twilio.services; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.twilio.Twilio; -import com.twilio.exception.ApiException; import com.twilio.rest.api.v2010.account.Message; import com.twilio.rest.api.v2010.account.MessageCreator; import com.twilio.type.PhoneNumber; diff --git a/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java b/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java index 4cfb7e2d6e..38a6423d88 100644 --- a/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java +++ b/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java @@ -1,7 +1,5 @@ package co.airy.core.sources.twilio; -import co.airy.avro.communication.Channel; -import co.airy.avro.communication.ChannelConnectionState; import co.airy.avro.communication.DeliveryState; import co.airy.avro.communication.Message; import co.airy.core.sources.twilio.services.Api; @@ -37,7 +35,6 @@ import java.util.concurrent.TimeUnit; import static co.airy.test.Timing.retryOnException; -import static org.apache.kafka.streams.KafkaStreams.State.RUNNING; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.doNothing; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java index 7140718450..60cb011a82 100644 --- a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java @@ -93,8 +93,8 @@ ResponseEntity connect(@RequestBody(required = false) @Valid ConnectChannelRe } @PostMapping("/channels.viber.disconnect") - ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { - final String channelId = requestPayload.getChannelId().toString(); + ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload payload) { + final String channelId = payload.getChannelId().toString(); final Channel channel = stores.getChannelsStore().get(channelId); if (channel == null) { diff --git a/backend/webhook/consumer/BUILD b/backend/webhook/consumer/BUILD index 2c3328754f..48d4e23419 100644 --- a/backend/webhook/consumer/BUILD +++ b/backend/webhook/consumer/BUILD @@ -11,6 +11,7 @@ app_deps = [ "//backend:webhook", "//lib/java/uuid", "//lib/java/date", + "//lib/java/crypto", "//lib/java/spring/kafka/core:spring-kafka-core", "//lib/java/spring/kafka/streams:spring-kafka-streams", "//lib/java/spring/web:spring-web", diff --git a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Sender.java b/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Sender.java index 3149349ae9..a1f9d172ec 100644 --- a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Sender.java +++ b/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Sender.java @@ -2,6 +2,7 @@ import co.airy.avro.communication.Webhook; import co.airy.core.webhook.WebhookEvent; +import co.airy.crypto.Signature; import co.airy.log.AiryLoggerFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,6 +18,7 @@ import java.security.InvalidKeyException; import static co.airy.core.webhook.WebhookEvent.shouldSendFor; +import static co.airy.crypto.Signature.getSignature; @Component public class Sender { @@ -24,15 +26,11 @@ public class Sender { private final RestTemplate restTemplate; private final Stores stores; private final ObjectMapper objectMapper; - private final Signature signature; - public static final String CONTENT_SIGNATURE_HEADER = "X-Airy-Content-Signature"; - - public Sender(RestTemplate restTemplate, Stores stores, ObjectMapper objectMapper, Signature signature) { + public Sender(RestTemplate restTemplate, Stores stores, ObjectMapper objectMapper) { this.restTemplate = restTemplate; this.stores = stores; this.objectMapper = objectMapper; - this.signature = signature; } public void sendRecord(WebhookEvent event) { @@ -47,8 +45,8 @@ public void sendRecord(WebhookEvent event) { try { final String content = objectMapper.writeValueAsString(event.getPayload()); if (webhook.getSignKey() != null) { - final String contentSignature = this.signature.getSignature(webhook.getSignKey(), content); - headers.set(CONTENT_SIGNATURE_HEADER, contentSignature); + final String contentSignature = getSignature(webhook.getSignKey(), content); + headers.set(Signature.CONTENT_SIGNATURE_HEADER, contentSignature); } final HttpEntity entity = new HttpEntity<>(content, headers); diff --git a/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java b/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java index 4815c9e9f4..c5153fd761 100644 --- a/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java +++ b/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java @@ -36,7 +36,8 @@ import java.util.Map; import java.util.UUID; -import static co.airy.core.webhook.consumer.Sender.CONTENT_SIGNATURE_HEADER; +import static co.airy.crypto.Signature.CONTENT_SIGNATURE_HEADER; +import static co.airy.crypto.Signature.getSignature; import static co.airy.test.Timing.retryOnException; import static org.apache.kafka.streams.KafkaStreams.State.RUNNING; import static org.hamcrest.CoreMatchers.is; @@ -75,9 +76,6 @@ static void afterAll() throws Exception { @Autowired Stores stores; - @Autowired - Signature signature; - @Autowired ObjectMapper objectMapper; @@ -117,7 +115,7 @@ void canSendOutEvents() throws Exception { mockServer.expect(once(), requestTo(new URI(endpoint))) .andExpect(method(HttpMethod.POST)) .andExpect(header("user-defined", equalTo("header"))) - .andExpect(header(CONTENT_SIGNATURE_HEADER, equalTo(signature.getSignature(signKey, content)))) + .andExpect(header(CONTENT_SIGNATURE_HEADER, equalTo(getSignature(signKey, content)))) .andRespond(withSuccess()); kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationWebhooks.name(), webhook.getId(), webhook)); diff --git a/docs/docs/api/endpoints/channels.md b/docs/docs/api/endpoints/channels.md index f69c671257..7ab8ea02ca 100644 --- a/docs/docs/api/endpoints/channels.md +++ b/docs/docs/api/endpoints/channels.md @@ -129,7 +129,7 @@ POST /channels.chatplugin.connect ```json5 { - "id": "1F679227-76C2-4302-BB12-703B2ADB0F66", + "id": "1f679227-76c2-4302-bb12-703b2adb0f66", "source": "chatplugin", "source_channel_id": "website-identifier-42", "metadata": { diff --git a/docs/docs/api/endpoints/messages-send.mdx b/docs/docs/api/endpoints/messages-send.mdx index 3aa696ccdf..fec7fb224c 100644 --- a/docs/docs/api/endpoints/messages-send.mdx +++ b/docs/docs/api/endpoints/messages-send.mdx @@ -6,6 +6,17 @@ the payload will look differently for each source. Refer to each [source's docum to see learn how to send text, media, and many more message types. +**Sample request** + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + // Source specific body + } +} +``` + **Sample response** ```json5 diff --git a/docs/docs/api/endpoints/messages.md b/docs/docs/api/endpoints/messages.md index ec177f80dc..cd33528932 100644 --- a/docs/docs/api/endpoints/messages.md +++ b/docs/docs/api/endpoints/messages.md @@ -36,7 +36,7 @@ are sorted from oldest to latest. // delivery state of message, one of PENDING, FAILED, DELIVERED "from_contact": true, "sent_at": "{string}", - //'yyyy-MM-dd'T'HH:mm:ss.SSSZ' date in UTC form, to be localized by clients + // ISO 8601 date string "source": "{String}", // one of the possible sources "metadata": { diff --git a/docs/docs/api/introduction.md b/docs/docs/api/introduction.md index 7b78ca4bb9..6f28cbe8c4 100644 --- a/docs/docs/api/introduction.md +++ b/docs/docs/api/introduction.md @@ -9,6 +9,7 @@ import ButtonBox from "@site/src/components/ButtonBox"; import HighVoltageSVG from "@site/static/icons/high-voltage.svg"; import ElectricPlugSVG from "@site/static/icons/electric-plug.svg"; import FishingPoleSVG from "@site/static/icons/fishing-pole.svg"; +import HammerAndWrenchSVG from "@site/static/icons/hammer-and-wrench.svg"; @@ -55,4 +56,11 @@ interacting with data: description='Participate programmatically in conversations by listening to events' link='api/webhook' /> +} + iconInvertible={true} + title='Sources' + description='Build your own messaging sources using simple ingestion and action http calls' + link='api/source' +/> diff --git a/docs/docs/api/source.md b/docs/docs/api/source.md new file mode 100644 index 0000000000..4ff076d35c --- /dev/null +++ b/docs/docs/api/source.md @@ -0,0 +1,324 @@ +--- +title: Source API +sidebar_label: Source +--- + +import TLDR from "@site/src/components/TLDR"; + + + +With the Source HTTP API you can build your own Airy messaging source in no time. + + + +:::note + +This feature is disabled by default. To enable it you need to provide values for the `security.jwtSecret`, `security.systemToken` and `integration.source-api.enabled` fields in your [airy.yaml config](getting-started/installation/configuration.md). + +::: + +The source API allows you to leverage Airy's message ingestion and real time delivery for any messaging platform +that is not yet officially supported. This is a typical usage pattern: + +1. Get a source access token +2. Create a channel +3. Forward incoming events to Airy +4. Handle outgoing messages or changes in metadata by [registering an action endpoint](#action-endpoint) + +**Note** For Airy there exists a 1 to 1 mapping between a source's channel/conversation/message id to the one stored in Airy. Therefore, the ids referenced in this payload must be identifiers used by the source. + +Take for instance the Facebook Messenger source. You should map these fields like so: + +- `source_id` → This is the identifier that will be written to all your messaging data (i.e. the `source` field on every [message](api/endpoints/messages.md#list)) and should thus not be changed later unless you want to partition your data. + +- `source_message_id` → the `mid` sent in every [webhook message event](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messages/). If your messaging source does not provide message ids, you can either create them yourself or use something like the hash of the message content and the timestamp as a proxy. + +- `source_channel_id` → For Messenger this is the Facebook page id. If your source only supports one channel per account, you can also use a constant value for this field. + +- `source_conversation_id` → Contacts for each Facebook page in Messenger are identified by a [Page-scoped ID](https://developers.facebook.com/docs/messenger-platform/identity/user-profile). Since in Messenger page conversations cannot have multiple participants, this uniquely identifies a conversation. + +## Managing sources + +Using your user [authentication](getting-started/installation/security.md) you [create](#create-a-source), [read](#list-sources), [update](#update-a-source), and [delete](#delete-a-source) sources. + +### Create a source + +`POST /sources.create` + +To ensure that apps using the source API can write their data in isolation of each other, every app receives a +JWT which encodes the source id. This JWT has to be set on the `Authorization` header of each request to authenticate +the source and write the correct identifier to the messaging data. + +**Sample request** + +```json5 +{ + "source_id": "my-crm-connector", + "action_endpoint": "http://my-app.com/action", // Optional + "name": "Human readable name for this source", // Optional + "image_url": "http://example.org/sourcIcon.jpg" // Optional +} +``` + +- `source_id` An unique identifier of your source that will be stored alongside all messaging data. +- `action_endpoint` (optional) If your source app should handle events such as outbound messages, you need to specify the [action http endpoint](#action-endpoint) here. +- `name` (optional) Human readable name for this source. +- `image_url` (optional) Icon presenting this source for display. + +**Sample response** + +```json5 +{ + "source_id": "my-crm-connector", + "token": "", + "action_endpoint": "http://my-app.com/action", // Optional + "name": "Human readable name for this source", // Optional + "image_url": "http://example.org/sourcIcon.jpg" // Optional +} +``` + +### List sources + +`POST /sources.list` + +**Sample response** + +```json5 +{ + "data": [ + { + "source_id": "my-crm-connector", + "action_endpoint": "http://my-app.com/action", // Optional + "name": "Human readable name for this source", // Optional + "image_url": "http://example.org/sourcIcon.jpg" // Optional + } + ] +} +``` + +### Delete a source + +`POST /sources.delete` + +Responds with `202 (Accepted)`. + +**Sample request** + +```json5 +{ + "source_id": "my-crm-connector" +} +``` + +### Update a source + +`POST /sources.update` + +**Sample request** + +```json5 +{ + "source_id": "my-crm-connector", + "action_endpoint": "http://my-app.com/action", // Optional + "name": "Human readable name for this source", // Optional + "image_url": "http://example.org/sourcIcon.jpg" // Optional +} +``` + +**Sample response** + +```json5 +{ + "source_id": "my-crm-connector", + "action_endpoint": "http://my-app.com/action", // Optional + "name": "Human readable name for this source", // Optional + "image_url": "http://example.org/sourcIcon.jpg" // Optional +} +``` + +## Manage source channels + +Using the token obtained during [creation](#create-a-source) a source can manage its own channels using the following endpoints. + +### Create a channel + +Creates a channel for the authenticated source. + +`POST /sources.channels.create` + +**Sample request** + +```json5 +{ + "source_channel_id": "Source identifier of the channel in use", + "name": "My source channel", // required + "metadata": { + "image_url": "https://example.com/custom-image.jpg", // optional + "token": "authentication string to use in your source app" // optional + } +} +``` + +- `source_channel_id` source identifier of the channel. Messages sent to [`/sources.webhook`](#messaging-data-webhook) must have a connected channel. +- `token` (optional) You can include a token and other keys in the metadata to make it easier to build stateless source apps. + +**Sample response** + +```json5 +{ + "id": "1f679227-76c2-4302-bb12-703b2adb0f66", // Airy channel id + "source_id": "my-source", + "source_channel_id": "Source identifier of the channel in use", + "metadata": { + "name": "My source channel", + "image_url": "https://example.com/custom-image.jpg", + "token": "authentication string to use in your source app" // optional + }, + "connected": true +} +``` + +### List channels + +List all connected channels of the authenticated source. + +`POST /sources.channels.list` + +**Sample response** + +```json5 +{ + data: [ + { + "id": "1f679227-76c2-4302-bb12-703b2adb0f66", // Airy channel id + "source_id": "my-source", + "source_channel_id": "Source identifier of the channel in use", + "metadata": { + "name": "My source channel", + "image_url": "https://example.com/custom-image.jpg" // optional + }, + "connected": true + } + ] +} +``` + +### Disconnect a channel + +Returns a `403` if the source does not have access to the given `channel_id` + +`POST /sources.channels.disconnect` + +**Sample request** + +```json5 +{ + "channel_id": "a688d36c-a85e-44af-bc02-4248c2c97622" +} +``` + +**Empty response (204)** + +## Inbound Webhook + +You can use this endpoint to ingest (inbound) messages and metadata into Airy. Not to be confused with the [(outbound) Webhook API](api/webhook.md). + +`POST /sources.webhook` + +**Sample request** + +```json5 +{ + "messages": [ + { + "source_message_id": "source message identifier", + "source_conversation_id": "source conversation identifier", + "source_channel_id": "source channel identifier", + "source_sender_id": "Unique identifier of the sender of the message", + "content": {"text": "Hello world"}, // Source specific content node (can be a plain string) + "from_contact": true, + "sent_at": 1603661094560 // Unix timestamp of event or ISO8601 date string + } + ], + "metadata": [ + { + "namespace": "conversation", // One of: conversation, message + "source_id": "conversation id", // Source message or conversation id + "metadata": { + "contact": { + "display_name": "Margaret Hamilton" + } + } + } + ] +} +``` + +## Action endpoint + +When you [create](#create-a-source) a source you can register an action endpoint. This way Airy will be able to map common messaging features to your source. The endpoint will be called with a `POST` request containing a JSON payload that will indicate, which action to perform. Each action requires a different response. See below for possible action payloads, and their expected responses. + +Each request includes an `X-Airy-Content-Signature` header that should be used to validate the authenticity of the request. To do so, compare the signature against the SHA256 HMAC of the source token you obtained during creation with the request content. Pseudocode: + +``` +isSignatureValid = hmac_sha256(key = source_token, message = request.body).to_lower_case() == request.headers['X-Airy-Content-Signature'] +``` + + + +[Our Java implementation](https://github.com/airyhq/airy/blob/develop/lib/java/crypto/src/main/java/co/airy/crypto/Signature.java#L21) + +### Send message + +When Airy users call the [`/messages.send`](api/endpoints/messages.md#send) endpoint to send a message to a conversation linked to your source, Airy will call your action endpoint with the following payload. Depending on the outcome you must respond with either a success or a failure payload. + +```json5 +{ + "type": "message.send", + "payload": { + "message": { + "id": "uuid", // Airy message id + "source_recipient_id": "source recipient identifier", // Depending on the source this can be the same as the source conversation id + "content": {"text": "Hello world"}, + "sent_at": "1603661094560" // Unix timestamp of event + }, + "conversation": { + // Optional + "id": "uuid", // Airy conversation id + "channel_id": "uuid", // Airy channel id + "source_channel_id": "source channel identifier", + "source_conversation_id": "source conversation identifier", + "created_at": "2021-08-31T09:27:46.528Z" + } + } +} +``` + +The conversation object is absent if there is no existing conversation. If your source does not support sending messages to new conversations, you should respond with a failure message. + +:::warning + +It's possible for users to send messages to your source before it was connected and before this API was installed in the cluster. In that case messages with older `send_at` timestamps can be sent. It is up to your source to decide when a message is too old to send and return an error. + +::: + +**Success response payload** + +Status code must be `200`. + +```json5 +{ + "source_message_id": "Source identifier of the sent message", + "metadata": {} // Additional message metadata +} +``` + +**Failure response payload** + +Status code must be in the range of `4xx`. For status codes above `500` the response Airy considers the network request failed, and the source unreachable. + +```json5 +{ + "error": "The content you attempted to send is malformed" // example message that will be included in the message metadata +} +``` diff --git a/docs/docs/api/websocket.md b/docs/docs/api/websocket.md index 5c7a077ae2..a60380c368 100644 --- a/docs/docs/api/websocket.md +++ b/docs/docs/api/websocket.md @@ -70,7 +70,7 @@ Includes the full and current state of a metadata object given a namespace-ident } ``` -### Channel +### `channel.updated` ```json5 { @@ -85,7 +85,7 @@ Includes the full and current state of a metadata object given a namespace-ident } ``` -### Tag +### `tag.updated` ```json5 { diff --git a/docs/docs/getting-started/glossary.md b/docs/docs/getting-started/glossary.md index 3e552c8fd4..a117ae0919 100644 --- a/docs/docs/getting-started/glossary.md +++ b/docs/docs/getting-started/glossary.md @@ -81,17 +81,17 @@ Indicates whether the message was sent by a contact or not. One of: - `pending` message to be sent out - - `delivered` message has been sent to source + - `delivered` message has been sent to source or has arrived at Airy - `failed` message sending has terminally failed - `sentAt` timestamp -- `updatedAt` timestamp null for messages that are inserted first time +- `updatedAt` timestamp `null` for messages that are inserted first time ### Headers -Header data contains information that is important for downstream processing. It -also includes the message preview and tags that are useful for use cases like routing and automations. +Header data contains information that is important for downstream processing and therefore cannot be separated from the message (as opposed to metadat). +It also includes the message preview and tags that are useful for use cases like routing and automations. ## Metadata diff --git a/docs/docs/getting-started/installation/configuration.md b/docs/docs/getting-started/installation/configuration.md index 1fff780512..dae5982fe4 100644 --- a/docs/docs/getting-started/installation/configuration.md +++ b/docs/docs/getting-started/installation/configuration.md @@ -129,6 +129,8 @@ components: integration: webhook: name: webhook + source-api: + enabled: true ``` ## Applying the configuration diff --git a/docs/docs/sources/chatplugin/customization.md b/docs/docs/sources/chatplugin/customization.md index c7cd811b2d..6014870533 100644 --- a/docs/docs/sources/chatplugin/customization.md +++ b/docs/docs/sources/chatplugin/customization.md @@ -75,16 +75,16 @@ Each Render Prop accepts a set of parameters that allows you to customize the in **Example of how to use Render Props with the Airy Chat Plugin library:** -```json5 +```jsx const airyChatPlugin = new AiryChatPlugin({ - organization_id: "YOUR_ORG_ID", - app_id: "YOUR_APP_ID", - app_secret: "YOUR_APP_SECRET", - headerBarProp: ({orgImage, toggleHideChat}) => -
-

Title

- orgImg -
- } -); + organization_id: "YOUR_ORG_ID", + app_id: "YOUR_APP_ID", + app_secret: "YOUR_APP_SECRET", + headerBarProp: ({orgImage, toggleHideChat}) => ( +
+

Title

+ orgImg +
+ ) +}); ``` diff --git a/docs/docs/sources/introduction.md b/docs/docs/sources/introduction.md index 1a9e286476..48c74ea3b2 100644 --- a/docs/docs/sources/introduction.md +++ b/docs/docs/sources/introduction.md @@ -27,7 +27,7 @@ Business Messages, Twilio.WhatsApp or Twilio.SMS. You can connect sources through API requests or using our [Channels UI](/ui/channels). Our Sources guides cover both options, step-by-step. -It's important to understand the difference between a [source](/getting-started/glossary/#source) and a [channel](/getting-started/glossary/#channel). A [channel](/getting-started/glossary/#channel) represents a connection between a [source](/getting-started/glossary/#source) and your Airy Core instance: multiple [channels](/getting-started/glossary/#channel) can thus use the same [source](/getting-started/glossary/#source) for different [conversations](/getting-started/glossary/#conversation). +It's important to understand the difference between a [source](/getting-started/glossary/#source) and a [channel](/getting-started/glossary/#channel). A channel represents a connection between a source and your Airy Core instance: multiple channels can thus use the same source for different [conversations](/getting-started/glossary/#conversation). Connecting a [channel](/getting-started/glossary/#channel) allows the possibility of starting a [conversation](/getting-started/glossary/#conversation) between a [source](/getting-started/glossary/#source) and your Airy Core instance. Once a [channel](/getting-started/glossary/#channel) has been connected, your Airy Core instance will start ingesting [messages](/getting-started/glossary/#message) and create new [conversations](/getting-started/glossary/#conversation) accordingly. diff --git a/docs/sidebars.js b/docs/sidebars.js index 7462e91a90..668fb14ef6 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -61,6 +61,7 @@ module.exports = { }, 'api/websocket', 'api/webhook', + 'api/source', ], }, { diff --git a/frontend/chat-plugin/lib/src/AiryChatPlugin.tsx b/frontend/chat-plugin/lib/src/AiryChatPlugin.tsx index 824330c8c5..86212b6dee 100644 --- a/frontend/chat-plugin/lib/src/AiryChatPlugin.tsx +++ b/frontend/chat-plugin/lib/src/AiryChatPlugin.tsx @@ -8,6 +8,9 @@ type AiryChatPluginProps = { className?: string; }; +const defaultWidth = 380; +const defaultHeight = 700; + export const AiryChatPlugin = (props: AiryChatPluginProps) => { const {config, className} = props; @@ -21,24 +24,10 @@ export const AiryChatPlugin = (props: AiryChatPluginProps) => { window.addEventListener('resize', handleResize); - const widgetHeight = (): number => { - if (config.config?.height) { - return config.config.height > windowHeight ? windowHeight : config.config.height; - } - return 700 > windowHeight ? windowHeight : 700; - }; - - const widgetWidth = (): number => { - if (config.config?.width) { - return config.config.width > windowWidth ? windowWidth : config.config.width; - } - return 380 > windowWidth ? windowWidth : 380; - }; - const customStyle = { background: 'transparent', - height: widgetHeight(), - width: widgetWidth(), + width: Math.min(config.config?.width ?? defaultWidth, windowWidth), + height: Math.min(config.config?.height ?? defaultHeight, windowHeight), ...(config.config?.primaryColor && { '--color-airy-blue': config.config?.primaryColor, }), diff --git a/frontend/chat-plugin/lib/src/api/index.tsx b/frontend/chat-plugin/lib/src/api/index.tsx index 8a2b58b0f8..8fe50c8af4 100644 --- a/frontend/chat-plugin/lib/src/api/index.tsx +++ b/frontend/chat-plugin/lib/src/api/index.tsx @@ -67,6 +67,7 @@ export const authenticate = async (channelId: string, resumeToken?: string) => return response.json(); }) .catch(error => { + // Get a fresh conversation in case the resume token expired if (resumeToken) { resetStorage(channelId); return authenticate(channelId); diff --git a/frontend/chat-plugin/lib/src/websocket/index.ts b/frontend/chat-plugin/lib/src/websocket/index.ts index 2ef371415d..97c3227337 100644 --- a/frontend/chat-plugin/lib/src/websocket/index.ts +++ b/frontend/chat-plugin/lib/src/websocket/index.ts @@ -62,7 +62,7 @@ class WebSocket { this.client.onConnect = this.onConnect; this.client.onWebSocketClose = this.onWebSocketClose; - this.client.onStompError = function (frame: IFrame) { + this.client.onStompError = (frame: IFrame) => { console.error('Broker reported error: ' + frame.headers['message']); console.error('Additional details: ' + frame.body); authenticate(this.channelId); diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/Chart.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/Chart.yaml new file mode 100644 index 0000000000..c33dbe3b6d --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for the Sources Chatplugin app +name: integration-source-api +version: 0-develop diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/deployment.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/deployment.yaml new file mode 100644 index 0000000000..3764ff05ae --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/deployment.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: source-api + namespace: {{ .Values.global.kubernetes.namespace }} + labels: + app: source-api + type: sources + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: "{{ .Values.component }}" +spec: + replicas: 0 + selector: + matchLabels: + app: source-api + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: source-api + spec: + containers: + - name: app + image: "{{ .Values.global.kubernetes.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.kubernetes.appImageTag }}" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + - configMapRef: + name: kafka-config + - configMapRef: + name: "{{ .Values.component }}" + env: + - name: jwtSecret + valueFrom: + configMapKeyRef: + name: security + key: jwtSecret + - name: KAFKA_BROKERS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_BROKERS + - name: KAFKA_SCHEMA_REGISTRY_URL + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_SCHEMA_REGISTRY_URL + - name: KAFKA_COMMIT_INTERVAL_MS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_COMMIT_INTERVAL_MS + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 3 + initContainers: + - name: wait + image: busybox + command: ["/bin/sh", "/opt/provisioning/wait-for-minimum-kafkas.sh"] + env: + - name: KAFKA_BROKERS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_BROKERS + - name: REPLICAS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_MINIMUM_REPLICAS + volumeMounts: + - name: provisioning-scripts + mountPath: /opt/provisioning + volumes: + - name: provisioning-scripts + configMap: + name: provisioning-scripts diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/service.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/service.yaml new file mode 100644 index 0000000000..caee0bd1ef --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: source-api + namespace: {{ .Values.global.kubernetes.namespace }} + labels: + core.airy.co/prometheus: spring +spec: + ports: + - name: web + port: 80 + targetPort: 8080 + protocol: TCP + type: NodePort + selector: + app: source-api diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/values.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/values.yaml new file mode 100644 index 0000000000..68810eaa98 --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/integration/charts/source-api/values.yaml @@ -0,0 +1,3 @@ +component: integration-source-api +mandatory: false +image: sources/api diff --git a/infrastructure/helm-chart/charts/core/charts/provisioning/templates/kafka-create-topics.yaml b/infrastructure/helm-chart/charts/core/charts/provisioning/templates/kafka-create-topics.yaml index 650f33cc59..9776c5e05f 100644 --- a/infrastructure/helm-chart/charts/core/charts/provisioning/templates/kafka-create-topics.yaml +++ b/infrastructure/helm-chart/charts/core/charts/provisioning/templates/kafka-create-topics.yaml @@ -38,6 +38,8 @@ data: kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}application.communication.read-receipt" --config cleanup.policy=compact min.compaction.lag.ms=86400000 segment.bytes=10485760 + kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}application.communication.sources" --config cleanup.policy=compact min.compaction.lag.ms=86400000 segment.bytes=10485760 + kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}application.communication.tags" --config cleanup.policy=compact min.compaction.lag.ms=86400000 segment.bytes=10485760 kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}application.communication.templates" --config cleanup.policy=compact min.compaction.lag.ms=86400000 segment.bytes=10485760 diff --git a/infrastructure/helm-chart/charts/core/templates/ingress.yaml b/infrastructure/helm-chart/charts/core/templates/ingress.yaml index c9e604adb8..257fdc1e1c 100644 --- a/infrastructure/helm-chart/charts/core/templates/ingress.yaml +++ b/infrastructure/helm-chart/charts/core/templates/ingress.yaml @@ -16,6 +16,13 @@ spec: name: api-websocket port: number: 80 + - path: /sources + pathType: Prefix + backend: + service: + name: sources-api + port: + number: 80 - path: /conversations pathType: Prefix backend: diff --git a/lib/java/crypto/BUILD b/lib/java/crypto/BUILD new file mode 100644 index 0000000000..33dd671011 --- /dev/null +++ b/lib/java/crypto/BUILD @@ -0,0 +1,10 @@ +load("@com_github_airyhq_bazel_tools//lint:buildifier.bzl", "check_pkg") +load("//tools/build:java_library.bzl", "custom_java_library") + +custom_java_library( + name = "crypto", + srcs = glob(["src/main/java/co/airy/crypto/**/*.java"]), + visibility = ["//visibility:public"], +) + +check_pkg(name = "buildifier") diff --git a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Signature.java b/lib/java/crypto/src/main/java/co/airy/crypto/Signature.java similarity index 79% rename from backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Signature.java rename to lib/java/crypto/src/main/java/co/airy/crypto/Signature.java index 1644589040..f8035def66 100644 --- a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Signature.java +++ b/lib/java/crypto/src/main/java/co/airy/crypto/Signature.java @@ -1,15 +1,14 @@ -package co.airy.core.webhook.consumer; - -import org.springframework.stereotype.Service; +package co.airy.crypto; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -@Service public class Signature { + public static final String CONTENT_SIGNATURE_HEADER = "X-Airy-Content-Signature"; + /** * Computes a signature of the content send to user webhooks so that they can verify its integrity and authenticity. * @@ -18,7 +17,7 @@ public class Signature { * @return hmac (sha256) of the content given the key in lowercase hex representation * @throws InvalidKeyException Malformed user secret key */ - public String getSignature(String key, String content) throws InvalidKeyException { + public static String getSignature(String key, String content) throws InvalidKeyException { Mac mac; try { mac = Mac.getInstance("HmacSHA256"); @@ -30,6 +29,7 @@ public String getSignature(String key, String content) throws InvalidKeyExceptio byte[] hmac = mac.doFinal(content.getBytes()); StringBuilder builder = new StringBuilder(); for (byte b : hmac) { + // TODO This is slow compared to the DataTypeConverter implementation builder.append(String.format("%02X", b).toLowerCase()); } return builder.toString(); diff --git a/lib/java/date/src/main/java/co/airy/date/format/DateFormat.java b/lib/java/date/src/main/java/co/airy/date/format/DateFormat.java index 62f3ed592a..92c2b84513 100644 --- a/lib/java/date/src/main/java/co/airy/date/format/DateFormat.java +++ b/lib/java/date/src/main/java/co/airy/date/format/DateFormat.java @@ -3,11 +3,18 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.TemporalAccessor; public class DateFormat { private static final DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder().parseCaseInsensitive().appendInstant(3).toFormatter(); + private static final DateTimeFormatter isoParser = DateTimeFormatter.ISO_DATE_TIME; public static String isoFromMillis(Long epochMilli) { return dateTimeFormatter.format(Instant.ofEpochMilli(epochMilli)); } + + public static Instant instantFromIso(String isoDateString) { + final TemporalAccessor parsed = isoParser.parse(isoDateString); + return Instant.from(parsed); + } } diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/application/ApplicationCommunicationSources.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/application/ApplicationCommunicationSources.java new file mode 100644 index 0000000000..285571b25d --- /dev/null +++ b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/application/ApplicationCommunicationSources.java @@ -0,0 +1,17 @@ +package co.airy.kafka.schema.application; + +import co.airy.kafka.schema.ApplicationCommunication; + +import java.util.Map; + +public class ApplicationCommunicationSources extends ApplicationCommunication { + @Override + public String dataset() { + return "sources"; + } + + @Override + public Map config() { + return Map.of("cleanup.policy", "compact", "segment.bytes", "10485760", "min.compaction.lag.ms", "86400000"); + } +} diff --git a/lib/java/spring/auth/BUILD b/lib/java/spring/auth/BUILD index 64351172e6..e98c815fce 100644 --- a/lib/java/spring/auth/BUILD +++ b/lib/java/spring/auth/BUILD @@ -32,7 +32,7 @@ custom_java_library( custom_java_library( name = "test-app", srcs = glob(["src/test/java/co/airy/spring/auth/test_app/*.java"]), - deps = lib_deps, + deps = [":spring-auth"] + lib_deps, ) [ diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java index e4f04c2403..78624f9d80 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java @@ -3,50 +3,31 @@ import co.airy.log.AiryLoggerFactory; import co.airy.spring.auth.oidc.ConfigProvider; import co.airy.spring.auth.oidc.EmailFilter; -import co.airy.spring.auth.oidc.UserService; import co.airy.spring.auth.session.AuthCookie; import co.airy.spring.auth.session.CookieSecurityContextRepository; -import co.airy.spring.auth.session.Jwt; import co.airy.spring.auth.token.AuthenticationFilter; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; -import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; -import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Optional; @Configuration @EnableWebSecurity @@ -85,19 +66,19 @@ protected void configure(final HttpSecurity http) throws Exception { .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - if (systemToken != null || configProvider.isPresent()) { + if (hasTokenAuth() || hasOidcAuth()) { http.authorizeRequests(authorize -> authorize .antMatchers("/actuator/**", "/login/**", "/logout/**", "/oauth/**").permitAll() .antMatchers(ignoreAuthPatterns).permitAll() .anyRequest().authenticated() ); - if (systemToken != null) { + if (hasTokenAuth()) { log.info("System token auth enabled"); - http.addFilterBefore(new AuthenticationFilter(systemToken), AnonymousAuthenticationFilter.class); + http.addFilterBefore(new AuthenticationFilter(systemToken, new Jwt(jwtSecret)), AnonymousAuthenticationFilter.class); } - if (configProvider.isPresent()) { + if (hasOidcAuth()) { final String registrationId = configProvider.getRegistration().getRegistrationId(); log.info("Oidc auth enabled with provider: {}", registrationId); @@ -122,6 +103,14 @@ protected void configure(final HttpSecurity http) throws Exception { } } + private boolean hasTokenAuth() { + return systemToken != null || jwtSecret != null; + } + + private boolean hasOidcAuth() { + return configProvider.isPresent(); + } + @Bean CorsConfigurationSource corsConfigurationSource(final Environment environment) { final String allowed = environment.getProperty("allowedOrigins", ""); diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/Jwt.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/Jwt.java similarity index 58% rename from lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/Jwt.java rename to lib/java/spring/auth/src/main/java/co/airy/spring/auth/Jwt.java index 039851d40b..3052ae92f2 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/Jwt.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/Jwt.java @@ -1,11 +1,16 @@ -package co.airy.spring.auth.session; +package co.airy.spring.auth; +import co.airy.spring.auth.session.UserAuth; +import co.airy.spring.auth.session.UserProfile; +import co.airy.spring.auth.token.TokenAuth; +import co.airy.spring.auth.token.TokenProfile; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.security.core.Authentication; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; @@ -27,7 +32,7 @@ public Jwt(String tokenKey) { this.objectMapper = new ObjectMapper(); } - public String getAuthToken(AiryAuth auth) throws JsonProcessingException { + public String getAuthToken(UserAuth auth) throws JsonProcessingException { Date now = Date.from(Instant.now()); Map claims = new HashMap<>(); @@ -46,11 +51,32 @@ public String getAuthToken(AiryAuth auth) throws JsonProcessingException { return builder.compact(); } - public AiryAuth loadFromToken(final String authHeader) throws Exception { + public String getAuthToken(TokenAuth auth, Duration expiry) throws JsonProcessingException { + Map claims = new HashMap<>(); + claims.put(PRINCIPAL_CLAIM, objectMapper.writeValueAsString(auth.getPrincipal())); + + JwtBuilder builder = Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setSubject(auth.getPrincipal().getName()) + .setIssuedAt(Date.from(Instant.now())) + .addClaims(claims) + .signWith(signingKey, SignatureAlgorithm.HS256); + + if (expiry != null) { + Date exp = Date.from(Instant.now().plus(expiry)); + builder.setExpiration(exp); + } + return builder.compact(); + } + + public Authentication loadFromToken(final String authHeader) throws Exception { Claims claims = extractClaims(authHeader); final String principalClaim = (String) claims.get(PRINCIPAL_CLAIM); - final UserProfile profile = objectMapper.readValue(principalClaim, UserProfile.class); - return new AiryAuth(profile); + try { + return new UserAuth(objectMapper.readValue(principalClaim, UserProfile.class)); + } catch (Exception e) { + return new TokenAuth(objectMapper.readValue(principalClaim, TokenProfile.class)); + } } diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/PrincipalAccess.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/PrincipalAccess.java index 4ee6a31c66..c891f1d6ce 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/PrincipalAccess.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/PrincipalAccess.java @@ -1,6 +1,6 @@ package co.airy.spring.auth; -import co.airy.spring.auth.session.AiryAuth; +import co.airy.spring.auth.session.UserAuth; import co.airy.spring.auth.session.UserProfile; import co.airy.spring.auth.token.TokenAuth; import org.springframework.security.core.Authentication; @@ -20,7 +20,7 @@ public String getUserId(Authentication authentication) { } if (authentication instanceof TokenAuth) { - return ((TokenAuth) authentication).getPrincipal(); + return ((TokenAuth) authentication).getPrincipal().getName(); } final UserProfile userProfile = getUserProfile(authentication); @@ -34,8 +34,8 @@ public String getUserId(Authentication authentication) { public UserProfile getUserProfile(Authentication authentication) { if (authentication instanceof OAuth2AuthenticationToken) { return UserProfile.from((OAuth2AuthenticationToken) authentication); - } else if (authentication instanceof AiryAuth) { - return ((AiryAuth) authentication).getPrincipal(); + } else if (authentication instanceof UserAuth) { + return ((UserAuth) authentication).getPrincipal(); } return null; diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/CookieSecurityContextRepository.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/CookieSecurityContextRepository.java index 044b8721bf..db37e41c6c 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/CookieSecurityContextRepository.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/CookieSecurityContextRepository.java @@ -1,6 +1,7 @@ package co.airy.spring.auth.session; import co.airy.log.AiryLoggerFactory; +import co.airy.spring.auth.Jwt; import co.airy.spring.auth.PrincipalAccess; import com.fasterxml.jackson.core.JsonProcessingException; import org.slf4j.Logger; @@ -62,7 +63,7 @@ public boolean containsContext(HttpServletRequest request) { return getStoredAuth(request).isPresent(); } - private Optional getStoredAuth(HttpServletRequest request) { + private Optional getStoredAuth(HttpServletRequest request) { return getCookie(request) .map((authCookie) -> { try { @@ -105,9 +106,9 @@ protected void saveContext(SecurityContext securityContext) { try { // Exchange the oauth2 session for an Airy JWT cookie session final UserProfile profile = principalAccess.getUserProfile(authentication); - final AiryAuth airyAuth = new AiryAuth(profile); + final UserAuth userAuth = new UserAuth(profile); - AuthCookie cookie = new AuthCookie(jwt.getAuthToken(airyAuth)); + AuthCookie cookie = new AuthCookie(jwt.getAuthToken(userAuth)); cookie.setSecure(request.isSecure()); response.addCookie(cookie); } catch (JsonProcessingException e) { diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/AiryAuth.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserAuth.java similarity index 90% rename from lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/AiryAuth.java rename to lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserAuth.java index 39d0138b24..4ac5b15194 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/AiryAuth.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserAuth.java @@ -8,11 +8,11 @@ import java.util.Collection; @NoArgsConstructor -public class AiryAuth implements Authentication, Serializable { +public class UserAuth implements Authentication, Serializable { private UserProfile userProfile; private boolean isAuthenticated = true; - public AiryAuth(UserProfile userProfile) { + public UserAuth(UserProfile userProfile) { this.userProfile = userProfile; } diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserProfile.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserProfile.java index f7643ce50a..25774e5a75 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserProfile.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/session/UserProfile.java @@ -5,7 +5,6 @@ import lombok.NoArgsConstructor; import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/AuthenticationFilter.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/AuthenticationFilter.java index 9608de905e..e7df58e467 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/AuthenticationFilter.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/AuthenticationFilter.java @@ -1,6 +1,8 @@ package co.airy.spring.auth.token; +import co.airy.spring.auth.Jwt; import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -9,12 +11,16 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.List; +import java.util.Map; public class AuthenticationFilter extends OncePerRequestFilter { private final String systemToken; + private final Jwt jwt; - public AuthenticationFilter(String systemToken) { + public AuthenticationFilter(String systemToken, Jwt jwt) { this.systemToken = systemToken; + this.jwt = jwt; } @Override @@ -31,9 +37,9 @@ protected void doFilterInternal(HttpServletRequest req, return; } - TokenAuth authentication = getAuthentication(authToken); + Authentication authentication = getAuthentication(authToken); if (authentication == null) { - res.sendError(403, "system token does not match"); + res.sendError(403); return; } @@ -42,11 +48,16 @@ protected void doFilterInternal(HttpServletRequest req, chain.doFilter(req, res); } - private TokenAuth getAuthentication(String token) { + private Authentication getAuthentication(String token) { if (systemToken != null && systemToken.equals(token)) { - return new TokenAuth(token); + final TokenProfile profile = new TokenProfile(String.format("system-token-%s", token.substring(0, Math.min(token.length(), 4))), Map.of(), List.of()); + return new TokenAuth(profile); } - return null; + try { + return jwt.loadFromToken(token); + } catch (Exception ignored) { + return null; + } } } diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenAuth.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenAuth.java index 3c4a8b3181..e8c5943fad 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenAuth.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenAuth.java @@ -3,23 +3,23 @@ import lombok.Data; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Collection; -import java.util.List; +import java.util.stream.Collectors; @Data public class TokenAuth implements Authentication { - private String token; - private String principal; + private final TokenProfile profile; private boolean isAuthenticated = false; - public TokenAuth(String token) { - this.principal = String.format("system-token-%s", token.substring(0, Math.min(token.length(), 4))); + public TokenAuth(TokenProfile profile) { + this.profile = profile; } @Override public Collection getAuthorities() { - return List.of(); + return profile.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } @Override @@ -32,6 +32,11 @@ public Object getDetails() { return null; } + @Override + public TokenProfile getPrincipal() { + return profile; + } + @Override public boolean isAuthenticated() { return this.isAuthenticated; @@ -42,9 +47,8 @@ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentExce this.isAuthenticated = true; } - @Override public String getName() { - return principal; + return profile.getName(); } } diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenProfile.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenProfile.java new file mode 100644 index 0000000000..e0c3cd28ac --- /dev/null +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/token/TokenProfile.java @@ -0,0 +1,20 @@ +package co.airy.spring.auth.token; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.AuthenticatedPrincipal; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TokenProfile implements AuthenticatedPrincipal { + private String name; + private Map data; + private List roles; +} diff --git a/lib/java/spring/auth/src/test/java/co/airy/spring/auth/AuthenticationTest.java b/lib/java/spring/auth/src/test/java/co/airy/spring/auth/AuthenticationTest.java index 50400616c0..0cb658508f 100644 --- a/lib/java/spring/auth/src/test/java/co/airy/spring/auth/AuthenticationTest.java +++ b/lib/java/spring/auth/src/test/java/co/airy/spring/auth/AuthenticationTest.java @@ -1,9 +1,12 @@ package co.airy.spring.auth; +import co.airy.spring.auth.token.TokenAuth; +import co.airy.spring.auth.token.TokenProfile; import co.airy.spring.core.AirySpringBootApplication; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; @@ -20,6 +23,7 @@ @SpringBootTest(properties = { "allowedOrigins=*", "systemToken=user-generated-api-token", + "jwtSecret=long-randomly-generated-secret-used-as-jwt-secret-key", }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) @@ -28,8 +32,14 @@ public class AuthenticationTest { @Autowired private MockMvc mvc; + @Value("${systemToken}") + private String systemToken; + + @Value("${jwtSecret}") + private String jwtSecret; + @Test - void rejectsMissingJwt() throws Exception { + void rejectsMissingAuth() throws Exception { mvc.perform(post("/principal.get")) .andExpect(status().isForbidden()) .andExpect(jsonPath("$").doesNotExist()); @@ -55,7 +65,17 @@ void authenticatesSystemToken() throws Exception { mvc.perform(post("/principal.get") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.AUTHORIZATION, "user-generated-api-token")) + .header(HttpHeaders.AUTHORIZATION, systemToken)) + .andExpect(status().isOk()); + } + + @Test + void authenticatesJwt() throws Exception { + final Jwt jwt = new Jwt(jwtSecret); + + mvc.perform(post("/principal.get") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, jwt.getAuthToken(new TokenAuth(new TokenProfile("Some name", null, null)), null))) .andExpect(status().isOk()); } } diff --git a/lib/java/spring/auth/src/test/java/co/airy/spring/auth/test_app/Controller.java b/lib/java/spring/auth/src/test/java/co/airy/spring/auth/test_app/Controller.java index 39770072a1..7208c8a571 100644 --- a/lib/java/spring/auth/src/test/java/co/airy/spring/auth/test_app/Controller.java +++ b/lib/java/spring/auth/src/test/java/co/airy/spring/auth/test_app/Controller.java @@ -1,5 +1,6 @@ package co.airy.spring.auth.test_app; +import co.airy.spring.auth.token.TokenProfile; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,11 +14,10 @@ public class Controller { @PostMapping("/principal.get") ResponseEntity echoPrincipal(Authentication authentication) { - final String userId = (String) authentication.getPrincipal(); - return ResponseEntity.ok(new PrincipalDetails(userId)); + TokenProfile profile = (TokenProfile) authentication.getPrincipal(); + return ResponseEntity.ok(new PrincipalDetails(profile.getName())); } - @PostMapping("/data.get") ResponseEntity getData() { return ResponseEntity.ok().build(); diff --git a/lib/java/spring/test/src/main/java/co/airy/spring/test/WebTestHelper.java b/lib/java/spring/test/src/main/java/co/airy/spring/test/WebTestHelper.java index 37a650eb7c..1f4e9ff59e 100644 --- a/lib/java/spring/test/src/main/java/co/airy/spring/test/WebTestHelper.java +++ b/lib/java/spring/test/src/main/java/co/airy/spring/test/WebTestHelper.java @@ -28,24 +28,28 @@ public void waitUntilHealthy() throws InterruptedException { } public ResultActions post(String url, String body) throws Exception { - return this.mvc.perform(MockMvcRequestBuilders.post(url) - .header(CONTENT_TYPE, APPLICATION_JSON.toString()) + return mvc.perform(MockMvcRequestBuilders.post(url) + .headers(buildHeaders()) .content(body)); } public ResultActions post(String url) throws Exception { - return this.mvc.perform(MockMvcRequestBuilders.post(url)); + HttpHeaders headers = new HttpHeaders(); + if (systemToken != null) { + headers.setBearerAuth(systemToken); + } + return mvc.perform(MockMvcRequestBuilders.post(url).headers(headers)); } public ResultActions get(String url) throws Exception { - return this.mvc.perform(MockMvcRequestBuilders.get(url)); + return mvc.perform(MockMvcRequestBuilders.get(url)); } private HttpHeaders buildHeaders() { HttpHeaders headers = new HttpHeaders(); headers.add(CONTENT_TYPE, APPLICATION_JSON.toString()); - if (this.systemToken != null) { - headers.setBearerAuth(this.systemToken); + if (systemToken != null) { + headers.setBearerAuth(systemToken); } return headers; } From c5c6fae4445f978744a058b9ae6f29e95396b92e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 11:18:37 +0200 Subject: [PATCH 29/33] Bump react-router-dom from 5.2.1 to 5.3.0 (#2373) Bumps [react-router-dom](https://github.com/ReactTraining/react-router) from 5.2.1 to 5.3.0. - [Release notes](https://github.com/ReactTraining/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) - [Commits](https://github.com/ReactTraining/react-router/compare/v5.2.1...v5.3.0) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 734558a4b5..22d230ae38 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "react-markdown": "^7.0.1", "react-modal": "^3.14.3", "react-redux": "7.2.4", - "react-router-dom": "5.2.1", + "react-router-dom": "5.3.0", "redux": "^4.1.1", "regenerator-runtime": "^0.13.9", "reselect": "4.0.0", diff --git a/yarn.lock b/yarn.lock index e6f522d5a4..5bd5c24b5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6425,10 +6425,10 @@ react-redux@7.2.4: prop-types "^15.7.2" react-is "^16.13.1" -react-router-dom@5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.1.tgz#34af8b551a4ce17487d3f80e651b91651978dff6" - integrity sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w== +react-router-dom@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" + integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" From bbe3ce4476eb2f1a8dab05da29052945f3ea3767 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 11:47:11 +0200 Subject: [PATCH 30/33] Bump sass from 1.38.2 to 1.39.0 (#2377) Bumps [sass](https://github.com/sass/dart-sass) from 1.38.2 to 1.39.0. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.38.2...1.39.0) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 22d230ae38..dd47fdc3cf 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "minimist": "^1.2.5", "prettier": "^2.3.2", "react-hot-loader": "^4.13.0", - "sass": "^1.38.2", + "sass": "^1.39.0", "sass-loader": "^12.1.0", "style-loader": "^3.2.1", "terser-webpack-plugin": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index 5bd5c24b5e..5035c87db4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6813,10 +6813,10 @@ sass-loader@^12.1.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.38.2: - version "1.38.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.2.tgz#970045d9966180002a8c8f3820fc114cddb42822" - integrity sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA== +sass@^1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.39.0.tgz#6c64695d1c437767c8f1a4e471288e831f81d035" + integrity sha512-F4o+RhJkNOIG0b6QudYU8c78ZADKZjKDk5cyrf8XTKWfrgbtyVVXImFstJrc+1pkQDCggyidIOytq6gS4gCCZg== dependencies: chokidar ">=3.0.0 <4.0.0" From 4df064e70988fb8c64be5eec2d190adcdec9344e Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Tue, 7 Sep 2021 12:34:51 +0200 Subject: [PATCH 31/33] Fixes #2380 --- VERSION | 2 +- docs/docs/changelog.md | 64 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 0236ce60b7..c25c8e5b74 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.30.0-alpha +0.30.0 diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 2c6454f812..0cc18235b8 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -3,10 +3,7 @@ title: Changelog sidebar_label: 📝 Changelog --- -## 0.29.0 - -[[#2282](https://github.com/airyhq/airy/issues/2282)] Hotfix for webhook registration failing despite all services healthy [[#2283](https://github.com/airyhq/airy/pull/2283)] -## +## 0.30.0 #### Changes @@ -52,9 +49,62 @@ sidebar_label: 📝 Changelog You can download the Airy CLI for your operating system from the following links: -[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.28.2/darwin/amd64/airy) -[Linux](https://airy-core-binaries.s3.amazonaws.com/0.28.2/linux/amd64/airy) -[Windows](https://airy-core-binaries.s3.amazonaws.com/0.28.2/windows/amd64/airy.exe) +[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.29.0/darwin/amd64/airy) +[Linux](https://airy-core-binaries.s3.amazonaws.com/0.29.0/linux/amd64/airy) +[Windows](https://airy-core-binaries.s3.amazonaws.com/0.29.0/windows/amd64/airy.exe) + +## 0.30.0 + +#### Changes + +#### 🚀 Features + +- [[#2274](https://github.com/airyhq/airy/issues/2274)] Introduce the source API [[#2327](https://github.com/airyhq/airy/pull/2327)] +- [[#2328](https://github.com/airyhq/airy/issues/2328)] Display any source in the inbox [[#2368](https://github.com/airyhq/airy/pull/2368)] +- [[#2227](https://github.com/airyhq/airy/issues/2227)] Instagram source show story reply in inbox UI [[#2367](https://github.com/airyhq/airy/pull/2367)] +- [[#2229](https://github.com/airyhq/airy/issues/2229)] Instagram source render story mention in inbox UI [[#2363](https://github.com/airyhq/airy/pull/2363)] + +#### 🐛 Bug Fixes + +- [[#2370](https://github.com/airyhq/airy/issues/2370)] Fix inbox counter bug when paginating [[#2371](https://github.com/airyhq/airy/pull/2371)] +- [[#2337](https://github.com/airyhq/airy/issues/2337)] Fix webhook publisher crash for deleted messages [[#2340](https://github.com/airyhq/airy/pull/2340)] +- [[#2275](https://github.com/airyhq/airy/issues/2275)] Fixing typos in webhook docs [[#2338](https://github.com/airyhq/airy/pull/2338)] + +#### 📚 Documentation + +- [[#2243](https://github.com/airyhq/airy/issues/2243)] Improve upgrade docs [[#2339](https://github.com/airyhq/airy/pull/2339)] +- [[#2335](https://github.com/airyhq/airy/issues/2335)] Update release process docs [[#2336](https://github.com/airyhq/airy/pull/2336)] + +#### 🧰 Maintenance + +- Bump sass from 1.38.2 to 1.39.0 [[#2377](https://github.com/airyhq/airy/pull/2377)] +- Bump react-router-dom from 5.2.1 to 5.3.0 [[#2373](https://github.com/airyhq/airy/pull/2373)] +- Bump core-js from 3.16.4 to 3.17.2 [[#2375](https://github.com/airyhq/airy/pull/2375)] +- Bump @babel/preset-env from 7.15.0 to 7.15.4 [[#2372](https://github.com/airyhq/airy/pull/2372)] +- Bump immer from 9.0.3 to 9.0.6 [[#2369](https://github.com/airyhq/airy/pull/2369)] +- Bump terser-webpack-plugin from 5.1.4 to 5.2.0 [[#2364](https://github.com/airyhq/airy/pull/2364)] +- Bump @typescript-eslint/eslint-plugin from 4.29.2 to 4.30.0 [[#2356](https://github.com/airyhq/airy/pull/2356)] +- Bump react-markdown from 7.0.0 to 7.0.1 [[#2365](https://github.com/airyhq/airy/pull/2365)] +- Bump @typescript-eslint/parser from 4.29.3 to 4.30.0 [[#2361](https://github.com/airyhq/airy/pull/2361)] +- Bump @types/node from 16.7.1 to 16.7.10 [[#2362](https://github.com/airyhq/airy/pull/2362)] +- Bump tar from 6.1.4 to 6.1.11 in /docs [[#2359](https://github.com/airyhq/airy/pull/2359)] +- Bump @types/react-window-infinite-loader from 1.0.4 to 1.0.5 [[#2348](https://github.com/airyhq/airy/pull/2348)] +- Bump core-js from 3.16.2 to 3.16.4 [[#2357](https://github.com/airyhq/airy/pull/2357)] +- Bump @typescript-eslint/parser from 4.29.2 to 4.29.3 [[#2350](https://github.com/airyhq/airy/pull/2350)] +- Bump eslint-plugin-react from 7.24.0 to 7.25.1 [[#2343](https://github.com/airyhq/airy/pull/2343)] +- Bump cypress from 8.3.0 to 8.3.1 [[#2344](https://github.com/airyhq/airy/pull/2344)] +- Bump @stomp/stompjs from 6.1.0 to 6.1.1 [[#2347](https://github.com/airyhq/airy/pull/2347)] +- Bump react-router-dom from 5.2.0 to 5.2.1 [[#2346](https://github.com/airyhq/airy/pull/2346)] +- Bump sass from 1.38.0 to 1.38.2 [[#2342](https://github.com/airyhq/airy/pull/2342)] +- Bump @typescript-eslint/eslint-plugin from 4.29.1 to 4.29.2 [[#2296](https://github.com/airyhq/airy/pull/2296)] + +#### Airy CLI + +You can download the Airy CLI for your operating system from the following links: + +[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.30.0/darwin/amd64/airy) +[Linux](https://airy-core-binaries.s3.amazonaws.com/0.30.0/linux/amd64/airy) +[Windows](https://airy-core-binaries.s3.amazonaws.com/0.30.0/windows/amd64/airy.exe) ## 0.28.0 From bcaa92730496a2b2464e37838283a2bedea07102 Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Tue, 7 Sep 2021 15:27:58 +0200 Subject: [PATCH 32/33] Fix changelog --- docs/docs/changelog.md | 281 ++++++++++++++++++++--------------------- 1 file changed, 139 insertions(+), 142 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 0cc18235b8..c8336bd362 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -1,61 +1,9 @@ ---- -title: Changelog -sidebar_label: 📝 Changelog ---- - -## 0.30.0 - -#### Changes - -- [[#2304](https://github.com/airyhq/airy/issues/2304)] Fixed broken link from Messages Send Section to Sources. [[#2307](https://github.com/airyhq/airy/pull/2307)] -- [[#2308](https://github.com/airyhq/airy/issues/2308)] Chatplugin Customize Section Not Working [[#2309](https://github.com/airyhq/airy/pull/2309)] - -#### 🚀 Features - -- [[#2275](https://github.com/airyhq/airy/issues/2275)] Upgrade api-communication [[#2331](https://github.com/airyhq/airy/pull/2331)] -- [[#2286](https://github.com/airyhq/airy/issues/2286)] Core upgrade scripts for 0.29.0 [[#2322](https://github.com/airyhq/airy/pull/2322)] -- [[#2324](https://github.com/airyhq/airy/issues/2324)] Added more configs to the ui [[#2326](https://github.com/airyhq/airy/pull/2326)] -- [[#2275](https://github.com/airyhq/airy/issues/2275)] Allow for multiple webhooks and event filtering [[#2286](https://github.com/airyhq/airy/pull/2286)] -- [[#2243](https://github.com/airyhq/airy/issues/2243)] Introduce airy upgrade [[#2292](https://github.com/airyhq/airy/pull/2292)] -- [[#2314](https://github.com/airyhq/airy/issues/2314)] fix docs + fix responsiveness of chatplugin [[#2315](https://github.com/airyhq/airy/pull/2315)] -- [[#2279](https://github.com/airyhq/airy/issues/2279)] [[#2280](https://github.com/airyhq/airy/issues/2280)] More configs for chatplugin + docs [[#2301](https://github.com/airyhq/airy/pull/2301)] - -#### 🐛 Bug Fixes - -- [[#2282](https://github.com/airyhq/airy/issues/2282)] Hotfix for webhook registration health check [[#2284](https://github.com/airyhq/airy/pull/2284)] - -#### 🧰 Maintenance - -- Bump react-markdown from 6.0.3 to 7.0.0 [[#2317](https://github.com/airyhq/airy/pull/2317)] -- Bump @types/react from 17.0.18 to 17.0.19 [[#2320](https://github.com/airyhq/airy/pull/2320)] -- Bump webpack from 5.50.0 to 5.51.1 [[#2316](https://github.com/airyhq/airy/pull/2316)] -- Bump @types/node from 16.6.1 to 16.7.1 [[#2318](https://github.com/airyhq/airy/pull/2318)] -- Bump cypress from 8.1.0 to 8.3.0 [[#2311](https://github.com/airyhq/airy/pull/2311)] -- Bump core-js from 3.16.1 to 3.16.2 [[#2312](https://github.com/airyhq/airy/pull/2312)] -- Bump @types/react from 17.0.17 to 17.0.18 [[#2313](https://github.com/airyhq/airy/pull/2313)] -- Bump react-markdown from 6.0.3 to 7.0.0 [[#2297](https://github.com/airyhq/airy/pull/2297)] -- Bump @babel/core from 7.14.8 to 7.15.0 [[#2303](https://github.com/airyhq/airy/pull/2303)] -- Bump sass from 1.37.5 to 1.38.0 [[#2306](https://github.com/airyhq/airy/pull/2306)] -- Bump webpack from 5.49.0 to 5.50.0 [[#2302](https://github.com/airyhq/airy/pull/2302)] -- Bump @typescript-eslint/parser from 4.29.1 to 4.29.2 [[#2295](https://github.com/airyhq/airy/pull/2295)] -- Bump core-js from 3.16.0 to 3.16.1 [[#2298](https://github.com/airyhq/airy/pull/2298)] -- Bump @types/node from 16.4.13 to 16.6.1 [[#2287](https://github.com/airyhq/airy/pull/2287)] -- Bump @babel/preset-typescript from 7.14.5 to 7.15.0 [[#2288](https://github.com/airyhq/airy/pull/2288)] -- Bump webpack-cli from 4.7.2 to 4.8.0 [[#2290](https://github.com/airyhq/airy/pull/2290)] -- Bump @typescript-eslint/parser from 4.29.0 to 4.29.1 [[#2291](https://github.com/airyhq/airy/pull/2291)] -- Bump url-parse from 1.5.1 to 1.5.3 [[#2268](https://github.com/airyhq/airy/pull/2268)] - -#### Airy CLI - -You can download the Airy CLI for your operating system from the following links: +--- +title: Changelog +sidebar_label: 📝 Changelog +--- -[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.29.0/darwin/amd64/airy) -[Linux](https://airy-core-binaries.s3.amazonaws.com/0.29.0/linux/amd64/airy) -[Windows](https://airy-core-binaries.s3.amazonaws.com/0.29.0/windows/amd64/airy.exe) - -## 0.30.0 - -#### Changes +## 0.30.0 #### 🚀 Features @@ -105,9 +53,58 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.30.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.30.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.30.0/windows/amd64/airy.exe) - -## 0.28.0 - + + +## 0.29.0 + +#### 🚀 Features + +- [[#2275](https://github.com/airyhq/airy/issues/2275)] Upgrade api-communication [[#2331](https://github.com/airyhq/airy/pull/2331)] +- [[#2286](https://github.com/airyhq/airy/issues/2286)] Core upgrade scripts for 0.29.0 [[#2322](https://github.com/airyhq/airy/pull/2322)] +- [[#2324](https://github.com/airyhq/airy/issues/2324)] Added more configs to the ui [[#2326](https://github.com/airyhq/airy/pull/2326)] +- [[#2275](https://github.com/airyhq/airy/issues/2275)] Allow for multiple webhooks and event filtering [[#2286](https://github.com/airyhq/airy/pull/2286)] +- [[#2243](https://github.com/airyhq/airy/issues/2243)] Introduce airy upgrade [[#2292](https://github.com/airyhq/airy/pull/2292)] +- [[#2314](https://github.com/airyhq/airy/issues/2314)] fix docs + fix responsiveness of chatplugin [[#2315](https://github.com/airyhq/airy/pull/2315)] +- [[#2279](https://github.com/airyhq/airy/issues/2279)] [[#2280](https://github.com/airyhq/airy/issues/2280)] More configs for chatplugin + docs [[#2301](https://github.com/airyhq/airy/pull/2301)] + +#### 🐛 Bug Fixes + +- [[#2282](https://github.com/airyhq/airy/issues/2282)] Hotfix for webhook registration health check [[#2284](https://github.com/airyhq/airy/pull/2284)] +- [[#2304](https://github.com/airyhq/airy/issues/2304)] Fixed broken link from Messages Send Section to Sources. [[#2307](https://github.com/airyhq/airy/pull/2307)] +- [[#2308](https://github.com/airyhq/airy/issues/2308)] Chatplugin Customize Section Not Working [[#2309](https://github.com/airyhq/airy/pull/2309)] + +#### 🧰 Maintenance + +- Bump react-markdown from 6.0.3 to 7.0.0 [[#2317](https://github.com/airyhq/airy/pull/2317)] +- Bump @types/react from 17.0.18 to 17.0.19 [[#2320](https://github.com/airyhq/airy/pull/2320)] +- Bump webpack from 5.50.0 to 5.51.1 [[#2316](https://github.com/airyhq/airy/pull/2316)] +- Bump @types/node from 16.6.1 to 16.7.1 [[#2318](https://github.com/airyhq/airy/pull/2318)] +- Bump cypress from 8.1.0 to 8.3.0 [[#2311](https://github.com/airyhq/airy/pull/2311)] +- Bump core-js from 3.16.1 to 3.16.2 [[#2312](https://github.com/airyhq/airy/pull/2312)] +- Bump @types/react from 17.0.17 to 17.0.18 [[#2313](https://github.com/airyhq/airy/pull/2313)] +- Bump react-markdown from 6.0.3 to 7.0.0 [[#2297](https://github.com/airyhq/airy/pull/2297)] +- Bump @babel/core from 7.14.8 to 7.15.0 [[#2303](https://github.com/airyhq/airy/pull/2303)] +- Bump sass from 1.37.5 to 1.38.0 [[#2306](https://github.com/airyhq/airy/pull/2306)] +- Bump webpack from 5.49.0 to 5.50.0 [[#2302](https://github.com/airyhq/airy/pull/2302)] +- Bump @typescript-eslint/parser from 4.29.1 to 4.29.2 [[#2295](https://github.com/airyhq/airy/pull/2295)] +- Bump core-js from 3.16.0 to 3.16.1 [[#2298](https://github.com/airyhq/airy/pull/2298)] +- Bump @types/node from 16.4.13 to 16.6.1 [[#2287](https://github.com/airyhq/airy/pull/2287)] +- Bump @babel/preset-typescript from 7.14.5 to 7.15.0 [[#2288](https://github.com/airyhq/airy/pull/2288)] +- Bump webpack-cli from 4.7.2 to 4.8.0 [[#2290](https://github.com/airyhq/airy/pull/2290)] +- Bump @typescript-eslint/parser from 4.29.0 to 4.29.1 [[#2291](https://github.com/airyhq/airy/pull/2291)] +- Bump url-parse from 1.5.1 to 1.5.3 [[#2268](https://github.com/airyhq/airy/pull/2268)] + +#### Airy CLI + +You can download the Airy CLI for your operating system from the following links: + +[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.29.0/darwin/amd64/airy) +[Linux](https://airy-core-binaries.s3.amazonaws.com/0.29.0/linux/amd64/airy) +[Windows](https://airy-core-binaries.s3.amazonaws.com/0.29.0/windows/amd64/airy.exe) + + +## 0.28.0 + #### 🚀 Features - [[#1911](https://github.com/airyhq/airy/issues/1911)] Reorganize the helm charts [[#2241](https://github.com/airyhq/airy/pull/2241)] @@ -145,21 +142,21 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.28.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.28.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.28.0/windows/amd64/airy.exe) - -## Hotfix 0.27.1 - -[[#2219](https://github.com/airyhq/airy/issues/2219)] fixed inbox ui overflow bug [[#2220](https://github.com/airyhq/airy/pull/2220)] -## Hotfix 0.26.3 - -[[#2192](https://github.com/airyhq/airy/issues/2192)] Inbox crashing when selecting conversations in filtered view [[#2193](https://github.com/airyhq/airy/pull/2193)] -## Hotfix 0.26.2 - -[[#2187](https://github.com/airyhq/airy/issues/2187)] Hotfix chat plugin async bundle loading failed on installed websites -## Hotfix 0.26.1 - -[[#2181](https://github.com/airyhq/airy/issues/2181)] Fixes chat plugin integration crashing with empty config -## 0.27.0 - + +## Hotfix 0.27.1 + +[[#2219](https://github.com/airyhq/airy/issues/2219)] fixed inbox ui overflow bug [[#2220](https://github.com/airyhq/airy/pull/2220)] +## Hotfix 0.26.3 + +[[#2192](https://github.com/airyhq/airy/issues/2192)] Inbox crashing when selecting conversations in filtered view [[#2193](https://github.com/airyhq/airy/pull/2193)] +## Hotfix 0.26.2 + +[[#2187](https://github.com/airyhq/airy/issues/2187)] Hotfix chat plugin async bundle loading failed on installed websites +## Hotfix 0.26.1 + +[[#2181](https://github.com/airyhq/airy/issues/2181)] Fixes chat plugin integration crashing with empty config +## 0.27.0 + #### Changes #### 🚀 Features @@ -202,9 +199,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.27.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.27.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.27.0/windows/amd64/airy.exe) - -## 0.26.0 - + +## 0.26.0 + #### Changes - Change endpoint for webhook to /twilio [[#2123](https://github.com/airyhq/airy/pull/2123)] @@ -272,9 +269,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.25.1/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.25.1/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.25.1/windows/amd64/airy.exe) - -## 0.25.0 - + +## 0.25.0 + #### 🚀 Features - [[#1752](https://github.com/airyhq/airy/issues/1752)] Add connect cluster chart [[#1961](https://github.com/airyhq/airy/pull/1961)] @@ -316,13 +313,13 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.24.1/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.24.1/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.24.1/windows/amd64/airy.exe) - -## 0.23.1 Hotfix - + +## 0.23.1 Hotfix + [[#1921](https://github.com/airyhq/airy/issues/1921)] Hotfix: Facebook echo ingestion [[#1922](https://github.com/airyhq/airy/issues/1922)] - -## 0.24.0 - + +## 0.24.0 + #### Changes - [[#1956](https://github.com/airyhq/airy/issues/1956)] Fix link to installation page [[#1957](https://github.com/airyhq/airy/pull/1957)] @@ -375,9 +372,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.24.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.24.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.24.0/windows/amd64/airy.exe) - -## 0.23.0 - + +## 0.23.0 + #### 🚀 Features - [[#1815](https://github.com/airyhq/airy/issues/1815)] Added emptyState for filtered items [[#1874](https://github.com/airyhq/airy/pull/1874)] @@ -449,9 +446,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.23.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.23.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.23.0/windows/amd64/airy.exe) - -## 0.22.0 - + +## 0.22.0 + #### 🚀 Features - [[#1743](https://github.com/airyhq/airy/issues/1743)] Return proper status code for unauthorized access [[#1785](https://github.com/airyhq/airy/pull/1785)] @@ -491,9 +488,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.22.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.22.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.22.0/windows/amd64/airy.exe) - -## 0.21.0 - + +## 0.21.0 + #### Changes - [[#1750](https://github.com/airyhq/airy/issues/1750)] Fix tags filter [[#1765](https://github.com/airyhq/airy/pull/1765)] @@ -548,9 +545,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.21.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.21.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.21.0/windows/amd64/airy.exe) - -## 0.20.0 - + +## 0.20.0 + #### Changes - Bump @types/react from 16.9.34 to 17.0.4 [[#1658](https://github.com/airyhq/airy/pull/1658)] @@ -601,9 +598,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.20.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.20.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.20.0/windows/amd64/airy.exe) - -## 0.19.0 - + +## 0.19.0 + #### Changes #### 🚀 Features @@ -656,9 +653,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.19.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.19.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.19.0/windows/amd64/airy.exe) - -## 0.18.0 - + +## 0.18.0 + #### 🚀 Features - [[#1524](https://github.com/airyhq/airy/issues/1524)] Added conversationState to conversationList [[#1560](https://github.com/airyhq/airy/pull/1560)] - [[#1515](https://github.com/airyhq/airy/issues/1515)] Create airy chat plugin library + use it in UI [[#1550](https://github.com/airyhq/airy/pull/1550)] @@ -707,9 +704,9 @@ You can download the Airy CLI for your operating system from the following links You can download the Airy CLI for your operating system from the following links: [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.18.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.18.0/linux/amd64/airy) -[Windows](https://airy-core-binaries.s3.amazonaws.com/0.18.0/windows/amd64/airy.exe) -## 0.17.0 - +[Windows](https://airy-core-binaries.s3.amazonaws.com/0.18.0/windows/amd64/airy.exe) +## 0.17.0 + #### 🚀 Features - [[#929](https://github.com/airyhq/airy/issues/929)] Implement the option to end chat [[#1508](https://github.com/airyhq/airy/pull/1508)] @@ -759,9 +756,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.17.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.17.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.17.0/windows/amd64/airy.exe) - -## 0.16.0 - + +## 0.16.0 + #### 🚀 Features - [[#1111](https://github.com/airyhq/airy/issues/1111)] Customize Chat Plugin [[#1456](https://github.com/airyhq/airy/pull/1456)] @@ -813,13 +810,13 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.16.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.16.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.16.0/windows/amd64/airy.exe) - -## 0.15.1 Hotfix - + +## 0.15.1 Hotfix + - [[#1427](https://github.com/airyhq/airy/issues/1427)] Fix broken UI pod config for AWS deployment - -## 0.15.0 - + +## 0.15.0 + #### 🚀 Features - [[#1299](https://github.com/airyhq/airy/issues/1299)] Video Fallback for the render library [[#1412](https://github.com/airyhq/airy/pull/1412)] @@ -866,9 +863,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.15.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.15.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.15.0/windows/amd64/airy.exe) - -## 0.14.0 - + +## 0.14.0 + #### Changes - Docs/1301 add docs for twilio sources [[#1332](https://github.com/airyhq/airy/pull/1332)] @@ -937,9 +934,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.14.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.14.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.14.0/windows/amd64/airy.exe) - -## 0.13.0 - + +## 0.13.0 + #### Changes - Bump typesafe-actions from 4.4.2 to 5.1.0 [[#1210](https://github.com/airyhq/airy/pull/1210)] - [[#783](https://github.com/airyhq/airy/issues/783)] Introduce changelog [[#1221](https://github.com/airyhq/airy/pull/1221)] @@ -995,9 +992,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.13.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.13.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.13.0/windows/amd64/airy.exe) - -## 0.12.0 - + +## 0.12.0 + #### Changes - [[#1132](https://github.com/airyhq/airy/issues/1132)] Fix missing , in nginx [[#1133](https://github.com/airyhq/airy/pull/1133)] @@ -1061,9 +1058,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.12.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.12.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.12.0/windows/amd64/airy.exe) - -## 0.11.0 - + +## 0.11.0 + #### 🚀 Features - Custom welcome message in Chat Plugin [[#1103](https://github.com/airyhq/airy/pull/1103)] @@ -1103,9 +1100,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.11.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.11.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.11.0/windows/amd64/airy.exe) - -## 0.10.0 - + +## 0.10.0 + #### Changes - [[#1007](https://github.com/airyhq/airy/issues/1007)] Bug: Cookies + 2 Chat Plugins [[#1027](https://github.com/airyhq/airy/pull/1027)] @@ -1155,9 +1152,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.10.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.10.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.10.0/windows/amd64/airy.exe) - -## 0.9.0 - + +## 0.9.0 + #### 🚀 Features - [[#807](https://github.com/airyhq/airy/issues/807)] Introduction to UI docs [[#973](https://github.com/airyhq/airy/pull/973)] @@ -1201,9 +1198,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.9.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.9.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.9.0/windows/amd64/airy.exe) - -## 0.8.1 - + +## 0.8.1 + #### Changes #### 🚀 Features @@ -1223,9 +1220,9 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.8.1/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.8.1/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.8.1/windows/amd64/airy.exe) - -## 0.8.0 - + +## 0.8.0 + #### Changes @@ -1274,4 +1271,4 @@ You can download the Airy CLI for your operating system from the following links [MacOS](https://airy-core-binaries.s3.amazonaws.com/0.8.0/darwin/amd64/airy) [Linux](https://airy-core-binaries.s3.amazonaws.com/0.8.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.8.0/windows/amd64/airy.exe) - + From 5fae51f65d4b6de0667678eaed75a111cf37b64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Pr=C3=B6schel?= Date: Wed, 8 Sep 2021 09:13:55 +0200 Subject: [PATCH 33/33] Fix auth test condition --- .../auth/src/main/java/co/airy/spring/auth/AuthConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java index 78624f9d80..0704db5331 100644 --- a/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java +++ b/lib/java/spring/auth/src/main/java/co/airy/spring/auth/AuthConfig.java @@ -104,7 +104,7 @@ protected void configure(final HttpSecurity http) throws Exception { } private boolean hasTokenAuth() { - return systemToken != null || jwtSecret != null; + return systemToken != null && jwtSecret != null; } private boolean hasOidcAuth() {