From 1f2e3614fc1c52e737b2011f59a08ef5c12efaf4 Mon Sep 17 00:00:00 2001 From: Pascal Holy Date: Wed, 11 Aug 2021 11:10:04 +0200 Subject: [PATCH 01/40] Bump version to 0.29.0-alpha --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 697f087f37..233917a48e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.28.0 +0.29.0-alpha From cd60f855a33d4eff7e1a3c81a863a3589c566292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 11:27:42 +0200 Subject: [PATCH 02/40] Bump url-parse from 1.5.1 to 1.5.3 in /docs (#2270) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3) --- updated-dependencies: - dependency-name: url-parse 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 938ecfdafb..e56592f51a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -9663,9 +9663,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.4.7: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" From 589b2ed227991ad8c2eda7e9f79611b1e84baef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 11:28:07 +0200 Subject: [PATCH 03/40] Bump @types/react from 17.0.15 to 17.0.17 (#2266) Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.15 to 17.0.17. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) --- updated-dependencies: - dependency-name: "@types/react" 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 ebbafc3ea3..4a09b763ad 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@reduxjs/toolkit": "^1.6.1", "@stomp/stompjs": "^6.1.0", "@types/node": "16.4.10", - "@types/react": "17.0.15", + "@types/react": "17.0.17", "@types/react-dom": "17.0.9", "@types/react-redux": "7.1.18", "@types/react-router-dom": "^5.1.8", diff --git a/yarn.lock b/yarn.lock index 5a9a72148c..3a64af71e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1521,10 +1521,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@17.0.15": - version "17.0.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.15.tgz#c7533dc38025677e312606502df7656a6ea626d0" - integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw== +"@types/react@*", "@types/react@17.0.17": + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.17.tgz#1772d3d5425128e0635a716f49ef57c2955df055" + integrity sha512-nrfi7I13cAmrd0wje8czYpf5SFbryczCtPzFc6ijqvdjKcyA3tCvGxwchOUlxb2ucBPuJ9Y3oUqKrRqZvrz0lw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" From d8bc7b88aaf106b19002ac29f46d420acaeda3b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 11:28:37 +0200 Subject: [PATCH 04/40] Bump path-parse from 1.0.6 to 1.0.7 in /docs (#2269) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/jbgutierrez/path-parse/releases) - [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7) --- updated-dependencies: - dependency-name: path-parse 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 e56592f51a..2ae1eb233c 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -6929,9 +6929,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.7: version "0.1.7" From bb6f54786faf968f69165f8b208a1377151dfd86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 11:28:54 +0200 Subject: [PATCH 05/40] Bump path-parse from 1.0.6 to 1.0.7 (#2267) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/jbgutierrez/path-parse/releases) - [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7) --- updated-dependencies: - dependency-name: path-parse 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 3a64af71e7..72a1bc2ff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5736,9 +5736,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.7: version "0.1.7" From b1dd87e30365195345581314566d62c60b87310a Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Wed, 11 Aug 2021 12:39:11 +0200 Subject: [PATCH 06/40] [#2228] Introduce Viber as a source (#2242) --- .../co/airy/model/metadata/MetadataKeys.java | 9 +- .../airy/core/sources/facebook/Connector.java | 2 +- .../co/airy/core/sources/facebook/Stores.java | 2 +- .../core/sources/facebook/EventsRouter.java | 2 +- .../co/airy/core/sources/google/Stores.java | 2 +- .../co/airy/core/sources/twilio/Stores.java | 2 +- backend/sources/viber/connector/BUILD | 61 ++++++ .../sources/viber/ChannelsController.java | 138 ++++++++++++ .../co/airy/core/sources/viber/Connector.java | 78 +++++++ .../airy/core/sources/viber/EventsRouter.java | 119 +++++++++++ .../co/airy/core/sources/viber/Stores.java | 148 +++++++++++++ .../core/sources/viber/WebhookController.java | 82 ++++++++ .../core/sources/viber/config/Account.java | 81 +++++++ .../core/sources/viber/dto/AccountInfo.java | 19 ++ .../sources/viber/dto/SendMessageRequest.java | 19 ++ .../viber/dto/SendMessageResponse.java | 14 ++ .../airy/core/sources/viber/services/Api.java | 84 ++++++++ .../src/main/resources/application.properties | 3 + .../airy/core/sources/viber/ChannelsTest.java | 121 +++++++++++ .../core/sources/viber/EventsRouterTest.java | 103 +++++++++ .../core/sources/viber/SendMessageTest.java | 115 ++++++++++ .../sources/viber/WebhookControllerTest.java | 108 ++++++++++ .../sources/viber/lib/MockAccountInfo.java | 15 ++ .../airy/core/sources/viber/lib/Topics.java | 18 ++ .../events/conversation_started.json | 16 ++ .../resources/events/message_delivered.json | 6 + .../resources/events/message_received.json | 23 ++ .../test/resources/events/message_seen.json | 6 + .../src/test/resources/test.properties | 9 + docs/docs/api/endpoints/connect-viber.mdx | 26 +++ docs/docs/sources/introduction.md | 8 + docs/docs/sources/viber.md | 199 ++++++++++++++++++ docs/sidebars.js | 1 + docs/static/icons/viber.svg | 3 + .../img/sources/viber/account-creation.jpg | Bin 0 -> 124316 bytes .../lib/src/components/chat/index.tsx | 2 +- .../ui/src/components/IconChannel/index.tsx | 6 + .../ui/src/pages/Inbox/MessageInput/index.tsx | 4 +- .../Inbox/Messenger/MessageList/index.tsx | 69 +++--- .../Inbox/SuggestedReplySelector/index.tsx | 2 +- .../pages/Inbox/TemplateSelector/index.tsx | 2 +- .../charts/sources/charts/viber/Chart.yaml | 5 + .../charts/viber/templates/deployments.yaml | 173 +++++++++++++++ .../charts/viber/templates/service.yaml | 16 ++ .../charts/sources/charts/viber/values.yaml | 4 + .../templates/kafka-create-topics.yaml | 2 + .../charts/core/templates/ingress.yaml | 14 ++ .../charts/core/templates/ngrok.yaml | 7 + .../co/airy/kafka/schema/SourceFacebook.java | 13 -- .../co/airy/kafka/schema/SourceGoogle.java | 13 -- .../schema/source/SourceFacebookEvents.java | 12 +- .../schema/source/SourceGoogleEvents.java | 12 +- .../schema/source/SourceTwilioEvents.java | 12 +- .../schema/source/SourceViberEvents.java | 18 ++ .../co/airy/kafka/test/KafkaTestHelper.java | 1 - lib/typescript/assets/images/icons/viber.svg | 32 +++ lib/typescript/render/SourceMessage.tsx | 2 +- lib/typescript/render/outbound/index.ts | 3 + lib/typescript/render/outbound/viber.ts | 14 ++ lib/typescript/render/props.ts | 5 +- .../providers/chatplugin/ChatPluginRender.tsx | 8 +- .../chatplugin/components/RichText/index.tsx | 7 +- .../providers/facebook/FacebookRender.tsx | 49 ++--- .../render/providers/google/GoogleRender.tsx | 11 +- .../google/components/RichText/index.tsx | 7 +- .../render/providers/twilio/TwilioRender.tsx | 9 +- .../render/providers/viber/ViberRender.tsx | 42 ++++ .../render/providers/viber/viberModel.ts | 34 +++ lib/typescript/render/renderProviders.ts | 2 + maven_install.json | 82 +++++++- repositories.bzl | 1 + 71 files changed, 2187 insertions(+), 150 deletions(-) create mode 100644 backend/sources/viber/connector/BUILD create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Connector.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/EventsRouter.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Stores.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/WebhookController.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/config/Account.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/AccountInfo.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageRequest.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageResponse.java create mode 100644 backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/services/Api.java create mode 100644 backend/sources/viber/connector/src/main/resources/application.properties create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/ChannelsTest.java create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/EventsRouterTest.java create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/SendMessageTest.java create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/WebhookControllerTest.java create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/MockAccountInfo.java create mode 100644 backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/Topics.java create mode 100644 backend/sources/viber/connector/src/test/resources/events/conversation_started.json create mode 100644 backend/sources/viber/connector/src/test/resources/events/message_delivered.json create mode 100644 backend/sources/viber/connector/src/test/resources/events/message_received.json create mode 100644 backend/sources/viber/connector/src/test/resources/events/message_seen.json create mode 100644 backend/sources/viber/connector/src/test/resources/test.properties create mode 100644 docs/docs/api/endpoints/connect-viber.mdx create mode 100644 docs/docs/sources/viber.md create mode 100644 docs/static/icons/viber.svg create mode 100644 docs/static/img/sources/viber/account-creation.jpg create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/Chart.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/deployments.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/service.yaml create mode 100644 infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/values.yaml delete mode 100644 lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceFacebook.java delete mode 100644 lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceGoogle.java create mode 100644 lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceViberEvents.java create mode 100644 lib/typescript/assets/images/icons/viber.svg create mode 100644 lib/typescript/render/outbound/viber.ts create mode 100644 lib/typescript/render/providers/viber/ViberRender.tsx create mode 100644 lib/typescript/render/providers/viber/viberModel.ts 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 4c44f2009c..b23b64ebd3 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 @@ -15,6 +15,9 @@ public static class ConversationKeys { public static class Contact { public static final String DISPLAY_NAME = "contact.display_name"; public static final String AVATAR_URL = "contact.avatar_url"; + public static final String LANGUAGE = "contact.lang"; + public static final String COUNTRY = "contact.country"; + public static final String FETCH_STATE = "contact.fetch_state"; } @@ -47,7 +50,11 @@ public static class ChannelKeys { public static class MessageKeys { public static final String SUGGESTIONS = "suggestions"; - public static final String SOURCE_ID = "source_id"; + + public static class Source { + public static final String ID = "source.id"; + public static final String DELIVERY_STATE = "source.delivery_state"; + } } } 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 4519270e04..40ccbd3fc4 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 @@ -64,7 +64,7 @@ public List> sendMessage(SendMessageRequest final SendMessagePayload fbSendMessagePayload = mapper.fromSendMessageRequest(sendMessageRequest); final SendMessageResponse response = api.sendMessage(pageToken, fbSendMessagePayload); - final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.SOURCE_ID, response.getMessageId()); + final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.Source.ID, response.getMessageId()); updateDeliveryState(message, DeliveryState.DELIVERED); return List.of(KeyValue.pair(message.getId(), message), KeyValue.pair(getId(metadata).toString(), metadata)); diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java index 88bb636032..7dbec011ed 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java @@ -109,7 +109,7 @@ public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) .build()); // Send outbound messages - messageStream.filter((messageId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) + messageStream.filter((conversationId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) .join(contextTable, (message, conversation) -> new SendMessageRequest(conversation, message)) .flatMap((conversationId, sendMessageRequest) -> connector.sendMessage(sendMessageRequest)) .to((recordId, record, context) -> { 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 2648830b45..79561a5850 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_ID)) + .filter((metadataId, metadata) -> metadata.getKey().equals(MetadataKeys.MessageKeys.Source.DELIVERY_STATE)) .groupBy((metadataId, metadata) -> KeyValue.pair(metadata.getValue(), metadata)) .reduce((oldValue, newValue) -> newValue, (oldValue, reduceValue) -> reduceValue, Materialized.as(metadataStore)); diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java index b2a59d45d4..0054ee1b51 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java @@ -68,7 +68,7 @@ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { return requestBuilder.build(); }); - messageStream.filter((messageId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) + messageStream.filter((conversationId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) .join(contextTable, (message, sendMessageRequest) -> sendMessageRequest.toBuilder().message(message).build()) .map((conversationId, sendMessageRequest) -> { final Message message = connector.sendMessage(sendMessageRequest); diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java index 6b009ab60d..39222b8219 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java @@ -83,7 +83,7 @@ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { .join(channelsTable, SendMessageRequest::getChannelId, (aggregate, channel) -> aggregate.toBuilder().channel(channel).build()); - messageStream.filter((messageId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) + messageStream.filter((conversationId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) .join(contextTable, (message, sendMessageRequest) -> sendMessageRequest.toBuilder().message(message).build()) .map((conversationId, sendMessageRequest) -> { final Message message = connector.sendMessage(sendMessageRequest); diff --git a/backend/sources/viber/connector/BUILD b/backend/sources/viber/connector/BUILD new file mode 100644 index 0000000000..d6ce4635a5 --- /dev/null +++ b/backend/sources/viber/connector/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", + "//:springboot_actuator", + "//backend/model/channel", + "//backend/model/message", + "//backend/model/metadata", + "//lib/java/log", + "//lib/java/uuid", + "//lib/java/spring/kafka/core:spring-kafka-core", + "//lib/java/spring/kafka/streams:spring-kafka-streams", + "//lib/java/kafka/schema:source-viber-events", + "//lib/java/spring/web:spring-web", + "//lib/java/spring/auth:spring-auth", + "@maven//:com_viber_viber_bot", +] + +springboot( + name = "connector", + srcs = glob(["src/main/java/**/*.java"]), + main_class = "co.airy.spring.core.AirySpringBootApplication", + deps = app_deps, +) + +custom_java_library( + name = "test-lib", + srcs = glob(["src/test/java/co/airy/core/sources/viber/lib/*.java"]), + deps = [ + ":app", + "//backend:base_test", + ] + app_deps, +) + +[ + junit5( + size = "medium", + file = file, + resources = glob(["src/test/resources/**/*"]), + deps = [ + ":app", + ":test-lib", + "//backend:base_test", + "@maven//:javax_xml_bind_jaxb_api", + "//lib/java/kafka/test:kafka-test", + "//lib/java/spring/test:spring-test", + ] + app_deps, + ) + for file in glob(["src/test/java/**/*Test.java"]) +] + +container_release( + registry = "ghcr.io/airyhq/sources", + repository = "viber-connector", +) + +check_pkg(name = "buildifier") 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 new file mode 100644 index 0000000000..7140718450 --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/ChannelsController.java @@ -0,0 +1,138 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.Channel; +import co.airy.avro.communication.ChannelConnectionState; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.core.sources.viber.services.Api; +import co.airy.log.AiryLoggerFactory; +import co.airy.model.channel.dto.ChannelContainer; +import co.airy.model.metadata.MetadataKeys; +import co.airy.model.metadata.dto.MetadataMap; +import co.airy.spring.web.payload.RequestErrorResponsePayload; +import co.airy.uuid.UUIDv5; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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 javax.validation.constraints.NotNull; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static co.airy.model.channel.ChannelPayload.fromChannelContainer; +import static co.airy.model.metadata.MetadataRepository.newChannelMetadata; + +@RestController +public class ChannelsController { + private static final Logger log = AiryLoggerFactory.getLogger(ChannelsController.class); + private final Stores stores; + private final URL webhookUrl; + private final Api api; + private final AccountInfo accountInfo; + + public ChannelsController(Stores stores, @Value("${host}") String webhookUrl, @Value("${ngrok:#{null}}") String ngrok, Api api, AccountInfo accountInfo) { + this.stores = stores; + this.api = api; + this.accountInfo = accountInfo; + + String configHost = Optional.ofNullable(ngrok).orElse(webhookUrl); + try { + this.webhookUrl = new URL(configHost + "/viber"); + } catch (MalformedURLException e) { + log.error("Host from config was not a valid url {}", configHost, e); + throw new RuntimeException(e); + } + + } + + @PostMapping("/channels.viber.connect") + ResponseEntity connect(@RequestBody(required = false) @Valid ConnectChannelRequestPayload payload) { + final String channelId = UUIDv5.fromName(accountInfo.getId()).toString(); + payload = payload != null ? payload : new ConnectChannelRequestPayload(); + + try { + api.setWebhook(webhookUrl.toString()); + + List metadataList = new ArrayList<>(); + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.NAME, + Optional.ofNullable(payload.getName()).orElse(accountInfo.getName()))); + String avatarUrl = Optional.ofNullable(payload.getImageUrl()).orElse(accountInfo.getIcon()); + if (avatarUrl != null) { + metadataList.add(newChannelMetadata(channelId, MetadataKeys.ChannelKeys.IMAGE_URL, avatarUrl)); + } + + final ChannelContainer container = ChannelContainer.builder() + .channel( + Channel.newBuilder() + .setId(channelId) + .setConnectionState(ChannelConnectionState.CONNECTED) + .setSource("viber") + .setSourceChannelId(accountInfo.getId()) + .build() + ) + .metadataMap(MetadataMap.from(metadataList)).build(); + + stores.storeChannelContainer(container); + return ResponseEntity.ok(fromChannelContainer(container)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestErrorResponsePayload("Failed to connect the viber webhook. Error: " + e)); + } + } + + @PostMapping("/channels.viber.disconnect") + ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload requestPayload) { + final String channelId = requestPayload.getChannelId().toString(); + final Channel channel = stores.getChannelsStore().get(channelId); + + if (channel == null) { + return ResponseEntity.notFound().build(); + } + + if (channel.getConnectionState().equals(ChannelConnectionState.DISCONNECTED)) { + return ResponseEntity.noContent().build(); + } + + channel.setConnectionState(ChannelConnectionState.DISCONNECTED); + channel.setToken(null); + + try { + api.removeWebhook(); + stores.storeChannel(channel); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestErrorResponsePayload("Failed to connect the viber webhook. Error: " + e)); + } + + return ResponseEntity.noContent().build(); + } +} + +@Data +@NoArgsConstructor +@AllArgsConstructor +class ConnectChannelRequestPayload { + @NotNull + private String name; + private String imageUrl; +} + +@Data +@NoArgsConstructor +@AllArgsConstructor +class DisconnectChannelRequestPayload { + @NotNull + private UUID channelId; +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Connector.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Connector.java new file mode 100644 index 0000000000..1cef6b59b6 --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Connector.java @@ -0,0 +1,78 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.viber.dto.SendMessageRequest; +import co.airy.core.sources.viber.dto.SendMessageResponse; +import co.airy.core.sources.viber.services.Api; +import co.airy.log.AiryLoggerFactory; +import co.airy.model.metadata.MetadataKeys; +import co.airy.spring.auth.IgnoreAuthPattern; +import co.airy.spring.web.filters.RequestLoggingIgnorePatterns; +import com.viber.bot.api.ViberBot; +import org.apache.avro.specific.SpecificRecordBase; +import org.apache.kafka.streams.KeyValue; +import org.slf4j.Logger; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +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; + +@Component +public class Connector { + private static final Logger log = AiryLoggerFactory.getLogger(Connector.class); + + private final long messageStaleAfterSec = 300L; // 5 minutes + private final ViberBot viberBot; + private final Api api; + + public Connector(ViberBot viberBot, Api api) { + this.viberBot = viberBot; + this.api = api; + } + + public List> sendMessage(SendMessageRequest sendMessageRequest) { + final Message message = sendMessageRequest.getMessage(); + final String to = sendMessageRequest.getSourceConversationId(); + + if (isMessageStale(message)) { + updateDeliveryState(message, DeliveryState.FAILED); + return List.of(KeyValue.pair(message.getId(), message)); + } + + try { + final SendMessageResponse response = api.sendMessage(to, viberBot.getBotProfile(), message.getContent()); + + final Metadata metadata = newMessageMetadata(message.getId(), MetadataKeys.MessageKeys.Source.ID, response.getMessageToken().toString()); + updateDeliveryState(message, DeliveryState.DELIVERED); + + return List.of(KeyValue.pair(message.getId(), message), KeyValue.pair(getId(metadata).toString(), metadata)); + } catch (Exception e) { + log.error(String.format("Failed to send a message to viber \n SendMessageRequest: %s", sendMessageRequest), e); + } + + updateDeliveryState(message, DeliveryState.FAILED); + return List.of(KeyValue.pair(message.getId(), message)); + } + + private boolean isMessageStale(Message message) { + return ChronoUnit.SECONDS.between(Instant.ofEpochMilli(message.getSentAt()), Instant.now()) > messageStaleAfterSec; + } + + @Bean + public IgnoreAuthPattern ignoreAuthPattern() { + return new IgnoreAuthPattern("/viber"); + } + + @Bean + public RequestLoggingIgnorePatterns requestLoggingIgnorePatterns() { + return new RequestLoggingIgnorePatterns(List.of("/viber")); + } +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/EventsRouter.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/EventsRouter.java new file mode 100644 index 0000000000..9207d7e762 --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/EventsRouter.java @@ -0,0 +1,119 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.model.metadata.MetadataKeys; +import co.airy.uuid.UUIDv5; +import com.viber.bot.Request; +import com.viber.bot.event.incoming.IncomingConversationStartedEvent; +import com.viber.bot.event.incoming.IncomingDeliveredEvent; +import com.viber.bot.event.incoming.IncomingMessageEvent; +import com.viber.bot.event.incoming.IncomingSeenEvent; +import com.viber.bot.profile.UserProfile; +import org.apache.avro.specific.SpecificRecord; +import org.apache.kafka.streams.KeyValue; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static co.airy.model.metadata.MetadataRepository.getId; +import static co.airy.model.metadata.MetadataRepository.newConversationMetadata; +import static co.airy.model.metadata.MetadataRepository.newMessageMetadata; + +@Component +public class EventsRouter { + + public List> onEvent(String key, String payload) { + final Request request = Request.fromJsonString(payload); + final String channelId = UUIDv5.fromName(key).toString(); + + switch (request.getEvent().getEvent()) { + case CONVERSATION_STARTED: { + final IncomingConversationStartedEvent event = (IncomingConversationStartedEvent) request.getEvent(); + final String senderId = event.getUser().getId(); + final String conversationId = UUIDv5.fromNamespaceAndName(channelId, senderId).toString(); + final String messageId = getMessageId(event.getToken()); + + return new ArrayList<>() { + { + add(KeyValue.pair(messageId, Message.newBuilder() + .setChannelId(channelId) + .setContent(payload) + .setConversationId(conversationId) + .setDeliveryState(DeliveryState.DELIVERED) + .setHeaders(Map.of()) + .setId(messageId) + .setIsFromContact(true) + .setSenderId(senderId) + .setSentAt(event.getTimestamp()) + .setSource("viber").build())); + addAll(getMetadata(conversationId, event.getUser())); + } + }; + } + + case MESSAGE_RECEIVED: { + final IncomingMessageEvent event = (IncomingMessageEvent) request.getEvent(); + final String senderId = event.getSender().getId(); + final String conversationId = UUIDv5.fromNamespaceAndName(channelId, senderId).toString(); + final String messageId = getMessageId(event.getToken()); + + return new ArrayList<>() { + { + add(KeyValue.pair(messageId, Message.newBuilder() + .setChannelId(channelId) + .setContent(payload) + .setConversationId(conversationId) + .setDeliveryState(DeliveryState.DELIVERED) + .setHeaders(Map.of()) + .setId(messageId) + .setIsFromContact(true) + .setSenderId(senderId) + .setSentAt(event.getTimestamp()) + .setSource("viber").build())); + addAll(getMetadata(conversationId, event.getSender())); + } + }; + } + + case MESSAGE_DELIVERED: { + final IncomingDeliveredEvent event = (IncomingDeliveredEvent) request.getEvent(); + final String messageId = getMessageId(event.getToken()); + final Metadata metadata = newMessageMetadata(messageId, MetadataKeys.MessageKeys.Source.DELIVERY_STATE, "delivered"); + return List.of(KeyValue.pair(getId(metadata).toString(), metadata)); + } + + case MESSAGE_SEEN: { + final IncomingSeenEvent event = (IncomingSeenEvent) request.getEvent(); + final String messageId = getMessageId(event.getToken()); + final Metadata metadata = newMessageMetadata(messageId, MetadataKeys.MessageKeys.Source.DELIVERY_STATE, "seen"); + return List.of(KeyValue.pair(getId(metadata).toString(), metadata)); + } + + default: return List.of(); + } + } + + private String getMessageId(Long messageToken) { + return UUIDv5.fromNamespaceAndName("viber", messageToken.toString()).toString(); + } + + private List> getMetadata(String conversationId, UserProfile userProfile) { + final ArrayList> result = new ArrayList<>(); + + if (userProfile.getName() != null) { + final Metadata name = newConversationMetadata(conversationId, MetadataKeys.ConversationKeys.Contact.DISPLAY_NAME, userProfile.getName()); + result.add(KeyValue.pair(getId(name).toString(), name)); + } + + if (userProfile.getAvatar() != null) { + final Metadata avatar = newConversationMetadata(conversationId, MetadataKeys.ConversationKeys.Contact.AVATAR_URL, userProfile.getAvatar()); + result.add(KeyValue.pair(getId(avatar).toString(), avatar)); + } + + return result; + } +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Stores.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Stores.java new file mode 100644 index 0000000000..aefd8a373a --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/Stores.java @@ -0,0 +1,148 @@ +package co.airy.core.sources.viber; + +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.core.sources.viber.dto.SendMessageRequest; +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.source.SourceViberEvents; +import co.airy.kafka.streams.KafkaStreamsWrapper; +import co.airy.model.channel.dto.ChannelContainer; +import org.apache.avro.specific.SpecificRecordBase; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.streams.KafkaStreams; +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.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutionException; + +import static co.airy.model.metadata.MetadataRepository.getId; + +@Component +public class Stores implements ApplicationListener, DisposableBean, HealthIndicator { + + private static final String applicationCommunicationChannels = new ApplicationCommunicationChannels().name(); + private static final String applicationCommunicationMessages = new ApplicationCommunicationMessages().name(); + private static final String applicationCommunicationMetadata = new ApplicationCommunicationMetadata().name(); + private static final String appId = "sources.viber.ConnectorStores"; + private final String channelsStore = "channels-store"; + + private final KafkaStreamsWrapper streams; + private final KafkaProducer producer; + private final Connector connector; + private final EventsRouter eventsRouter; + + Stores(KafkaStreamsWrapper streams, Connector connector, KafkaProducer producer, EventsRouter eventsRouter) { + this.streams = streams; + this.connector = connector; + this.producer = producer; + this.eventsRouter = eventsRouter; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + final StreamsBuilder builder = new StreamsBuilder(); + + // Channels table + builder.table(applicationCommunicationChannels) + .filter((sourceChannelId, channel) -> channel.getSource().startsWith("viber"), Materialized.as(channelsStore)); + + final KStream messageStream = builder.stream(new ApplicationCommunicationMessages().name()) + .filter((messageId, message) -> message != null && message.getSource().startsWith("viber")) + .selectKey((messageId, message) -> message.getConversationId()); + + final KTable contextTable = messageStream + .groupByKey() + .aggregate(SendMessageRequest::new, + (conversationId, message, aggregate) -> { + SendMessageRequest.SendMessageRequestBuilder sendMessageRequestBuilder = aggregate.toBuilder(); + if (message.getIsFromContact()) { + sendMessageRequestBuilder.sourceConversationId(message.getSenderId()); + } + + sendMessageRequestBuilder.channelId(message.getChannelId()); + + return sendMessageRequestBuilder.build(); + }); + + messageStream.filter((conversationId, message) -> DeliveryState.PENDING.equals(message.getDeliveryState())) + .join(contextTable, (message, sendMessageRequest) -> sendMessageRequest.toBuilder().message(message).build()) + .flatMap((conversationId, sendMessageRequest) -> connector.sendMessage(sendMessageRequest)) + .to((recordId, record, context) -> { + if (record instanceof Metadata) { + return applicationCommunicationMetadata; + } + if (record instanceof Message) { + return applicationCommunicationMessages; + } + + throw new IllegalStateException("Unknown type for record " + record); + }); + + + builder.stream(new SourceViberEvents().name()) + .flatMap(eventsRouter::onEvent) + .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 ReadOnlyKeyValueStore getChannelsStore() { + return streams.acquireLocalStore(channelsStore); + } + + public void storeChannelContainer(ChannelContainer container) throws ExecutionException, InterruptedException { + final Channel channel = container.getChannel(); + storeChannel(channel); + + for (Metadata metadata : container.getMetadataMap().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(); + } + + @Override + public void destroy() { + if (streams != null) { + streams.close(); + } + } + + @Override + public Health health() { + getChannelsStore(); + + return Health.up().build(); + } + + // visible for testing + KafkaStreams.State getStreamState() { + return streams.state(); + } +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/WebhookController.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/WebhookController.java new file mode 100644 index 0000000000..8b06297f60 --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/WebhookController.java @@ -0,0 +1,82 @@ +package co.airy.core.sources.viber; + +import co.airy.core.sources.viber.config.Account.WelcomeMessage; +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.kafka.schema.source.SourceViberEvents; +import com.viber.bot.Request; +import com.viber.bot.ViberSignatureValidator; +import com.viber.bot.event.Event; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.Optional; +import java.util.Properties; + +@RestController +public class WebhookController implements DisposableBean { + private final String sourceViberEvents = new SourceViberEvents().name(); + private final ViberSignatureValidator signatureValidator; + private final AccountInfo accountInfo; + private final Optional welcomeMessage; + + private final Producer producer; + + public WebhookController(@Value("${kafka.brokers}") String brokers, ViberSignatureValidator signatureValidator, AccountInfo accountInfo, Optional welcomeMessage) { + this.signatureValidator = signatureValidator; + this.accountInfo = accountInfo; + this.welcomeMessage = welcomeMessage; + + final Properties props = new Properties(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); + props.put(ProducerConfig.RETRIES_CONFIG, String.valueOf(Integer.MAX_VALUE)); + props.put(ProducerConfig.ACKS_CONFIG, "1"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producer = new KafkaProducer<>(props); + } + + + @PostMapping(value = "/viber", produces = "application/json") + public ResponseEntity accept(@RequestBody String json, + @RequestHeader("X-Viber-Content-Signature") String serverSideSignature) { + if (!signatureValidator.isSignatureValid(serverSideSignature, json)) { + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + final Request request = Request.fromJsonString(json); + + try { + ProducerRecord record = new ProducerRecord<>(sourceViberEvents, accountInfo.getId(), json); + producer.send(record).get(); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + + if (request.getEvent().getEvent().equals(Event.CONVERSATION_STARTED)) { + return welcomeMessage.map(ResponseEntity::ok).orElse(new ResponseEntity<>(HttpStatus.OK)); + } + + return new ResponseEntity<>(HttpStatus.OK); + } + + @Override + public void destroy() { + if (producer != null) { + producer.close(Duration.ofSeconds(10)); + } + } + +} + diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/config/Account.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/config/Account.java new file mode 100644 index 0000000000..9f2484761e --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/config/Account.java @@ -0,0 +1,81 @@ +package co.airy.core.sources.viber.config; + +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.log.AiryLoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.viber.bot.ViberSignatureValidator; +import com.viber.bot.api.ViberBot; +import com.viber.bot.message.Message; +import com.viber.bot.profile.BotProfile; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Configuration +public class Account { + private final Logger log = AiryLoggerFactory.getLogger(Account.class); + + @Value("${authToken}") + private String authToken; + + @Value("${welcomeMessage:#{null}}") + private String welcomeMessage; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Bean + WelcomeMessage welcomeMessage(AccountInfo accountInfo) { + return Optional.ofNullable(welcomeMessage) + .map((messageString) -> { + try { + return objectMapper.readValue(messageString, Message.class); + } catch (JsonProcessingException e) { + log.error("The specified viber.welcomeMessage is not a valid viber message json. Valid message types: https://developers.viber.com/docs/api/rest-bot-api/#message-types"); + throw new RuntimeException(e); + } + }) + .map((viberMessage) -> { + final WelcomeMessage result = new WelcomeMessage(); + final Map senderMap = new HashMap<>() {{ + put("name", accountInfo.getName()); + if (accountInfo.getIcon() != null) { + put("avatar", accountInfo.getIcon()); + } + }}; + result.put("sender", senderMap); + result.putAll(viberMessage.getMapRepresentation()); + return result; + }).orElse(null); + } + + @Bean + ViberBot viberBot(AccountInfo accountInfo) { + return new ViberBot(new BotProfile(accountInfo.getName(), accountInfo.getIcon()), authToken); + } + + @Bean + ViberSignatureValidator signatureValidator() { + return new ViberSignatureValidator(authToken); + } + + public static class WelcomeMessage extends HashMap { + } + + @Bean + @Qualifier("viberObjectMapper") + public ObjectMapper viberObjectMapper() { + return new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) + .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + } +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/AccountInfo.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/AccountInfo.java new file mode 100644 index 0000000000..9be19bf1e8 --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/AccountInfo.java @@ -0,0 +1,19 @@ +package co.airy.core.sources.viber.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccountInfo { + @NonNull + private String id; + @NonNull + private String name; + private String icon; +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageRequest.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageRequest.java new file mode 100644 index 0000000000..29ea19f18a --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageRequest.java @@ -0,0 +1,19 @@ +package co.airy.core.sources.viber.dto; + +import co.airy.avro.communication.Message; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class SendMessageRequest implements Serializable { + private String sourceConversationId; + private String channelId; + private Message message; +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageResponse.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageResponse.java new file mode 100644 index 0000000000..afabe65d8d --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/dto/SendMessageResponse.java @@ -0,0 +1,14 @@ +package co.airy.core.sources.viber.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SendMessageResponse { + private Integer status; + private String statusMessage; + private Long messageToken; +} diff --git a/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/services/Api.java b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/services/Api.java new file mode 100644 index 0000000000..a0436b645e --- /dev/null +++ b/backend/sources/viber/connector/src/main/java/co/airy/core/sources/viber/services/Api.java @@ -0,0 +1,84 @@ +package co.airy.core.sources.viber.services; + +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.core.sources.viber.dto.SendMessageResponse; +import co.airy.log.AiryLoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.viber.bot.profile.BotProfile; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class Api { + private final Logger log = AiryLoggerFactory.getLogger(Api.class); + private static final String API_URL = "https://chatapi.viber.com/pa"; + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper; + private final HttpHeaders authHeaders; + + public Api(@Value("${authToken}") String authToken, @Qualifier("viberObjectMapper") ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + authHeaders = new HttpHeaders(); + authHeaders.set("X-Viber-Auth-Token", authToken); + } + + @Bean + AccountInfo accountInfo() { + try { + final String apiResponse = getApiResponse("/get_account_info", null); + return objectMapper.readValue(apiResponse, AccountInfo.class); + } catch (Exception e) { + log.error("Could not fetch required account info using get_account_info and the provided viber.auth-token"); + throw new RuntimeException(e); + } + } + + private String getApiResponse(String path, @Nullable String postData) { + final HttpEntity request = new HttpEntity<>(postData, authHeaders); + return restTemplate.postForObject(String.format("%s%s", API_URL, path), request, String.class); + } + + public SendMessageResponse sendMessage(String receiver, BotProfile sender, String content) throws JsonProcessingException { + final ObjectNode payload = (ObjectNode) objectMapper.readTree(content); + + final ObjectNode senderPayload = JsonNodeFactory.instance.objectNode(); + senderPayload.put("name", sender.getName()); + if (sender.getAvatar() != null) { + senderPayload.put("avatar", sender.getAvatar()); + } + + payload.set("sender", senderPayload); + payload.put("receiver", receiver); + + final String response = getApiResponse("/send_message", payload.toString()); + return objectMapper.readValue(response, SendMessageResponse.class); + } + + public void setWebhook(String webhookUrl) throws Exception { + Map request = new HashMap<>() {{ + put("url", webhookUrl); + put("send_name", true); + put("send_photo", true); + }}; + + getApiResponse("/set_webhook", objectMapper.writeValueAsString(request)); + } + + public void removeWebhook() throws Exception { + getApiResponse("/set_webhook", objectMapper.writeValueAsString(Map.of("url", ""))); + } +} diff --git a/backend/sources/viber/connector/src/main/resources/application.properties b/backend/sources/viber/connector/src/main/resources/application.properties new file mode 100644 index 0000000000..3c1d68e8b2 --- /dev/null +++ b/backend/sources/viber/connector/src/main/resources/application.properties @@ -0,0 +1,3 @@ +kafka.brokers=${KAFKA_BROKERS} +kafka.schema-registry-url=${KAFKA_SCHEMA_REGISTRY_URL} +kafka.commit-interval-ms=${KAFKA_COMMIT_INTERVAL_MS} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/ChannelsTest.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/ChannelsTest.java new file mode 100644 index 0000000000..808d764ef5 --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/ChannelsTest.java @@ -0,0 +1,121 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.Channel; +import co.airy.avro.communication.ChannelConnectionState; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.core.sources.viber.lib.MockAccountInfo; +import co.airy.core.sources.viber.lib.Topics; +import co.airy.core.sources.viber.services.Api; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.model.metadata.MetadataKeys; +import co.airy.spring.core.AirySpringBootApplication; +import co.airy.spring.test.WebTestHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; +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.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +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.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(MockAccountInfo.class) +@SpringBootTest(classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +class ChannelsTest { + + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper kafkaTestHelper; + + @MockBean + private Api api; + + @Autowired + private AccountInfo accountInfo; + + @Autowired + @InjectMocks + private Connector worker; + + @Autowired + private WebTestHelper webTestHelper; + + @Autowired + private Stores stores; + + @Value("${ngrok}") + private String ngrokHost; + + @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 InterruptedException { + MockitoAnnotations.openMocks(this); + webTestHelper.waitUntilHealthy(); + } + + @Test + void canConnectChannels() throws Exception { + ArgumentCaptor webhookCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(api).setWebhook(webhookCaptor.capture()); + doNothing().when(api).removeWebhook(); + Thread.sleep(5000); + final String content = webTestHelper.post("/channels.viber.connect") + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + final JsonNode jsonNode = new ObjectMapper().readTree(content); + + final String expectedWebhook = ngrokHost + "/viber"; + assertEquals(expectedWebhook, webhookCaptor.getValue()); + + List channels = kafkaTestHelper.consumeValues(1, Topics.applicationCommunicationChannels.name()); + final Channel channel = channels.get(0); + + assertThat(accountInfo.getId(), equalTo(channel.getSourceChannelId())); + assertThat(ChannelConnectionState.CONNECTED, equalTo(channel.getConnectionState())); + + final List metadataList = kafkaTestHelper.consumeValues(1, Topics.applicationCommunicationMetadata.name()); + final Metadata metadata = metadataList.get(0); + assertThat(MetadataKeys.ChannelKeys.NAME, equalTo(metadata.getKey())); + assertThat(accountInfo.getName(), equalTo(metadata.getValue())); + + webTestHelper.post("/channels.viber.disconnect", "{\"channel_id\":\"" + jsonNode.get("id").textValue() + "\"}") + .andExpect(status().isNoContent()); + + channels = kafkaTestHelper.consumeValues(1, Topics.applicationCommunicationChannels.name()); + assertThat(ChannelConnectionState.DISCONNECTED, equalTo(channels.get(0).getConnectionState())); + } +} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/EventsRouterTest.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/EventsRouterTest.java new file mode 100644 index 0000000000..07ac8fa3b4 --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/EventsRouterTest.java @@ -0,0 +1,103 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.core.sources.viber.lib.MockAccountInfo; +import co.airy.core.sources.viber.lib.Topics; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.model.metadata.MetadataRepository; +import co.airy.spring.core.AirySpringBootApplication; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.StreamUtils; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static co.airy.model.metadata.MetadataRepository.getSubject; +import static co.airy.model.metadata.MetadataRepository.isMessageMetadata; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.IsEqual.equalTo; + +@Import(MockAccountInfo.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +class EventsRouterTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper testHelper; + + @Autowired + private AccountInfo accountInfo; + + @BeforeAll + static void beforeAll() throws Exception { + testHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + testHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + testHelper.afterAll(); + } + + @Test + void routesEvents() throws Exception { + final String conversationStarted = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("events/conversation_started.json"), StandardCharsets.UTF_8); + final String received = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("events/message_received.json"), StandardCharsets.UTF_8); + final String delivered = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("events/message_delivered.json"), StandardCharsets.UTF_8); + final String seen = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("events/message_seen.json"), StandardCharsets.UTF_8); + + testHelper.produceRecords(List.of( + new ProducerRecord<>(Topics.sourceViberEvents.name(), UUID.randomUUID().toString(), conversationStarted), + new ProducerRecord<>(Topics.sourceViberEvents.name(), UUID.randomUUID().toString(), received), + new ProducerRecord<>(Topics.sourceViberEvents.name(), UUID.randomUUID().toString(), delivered), + new ProducerRecord<>(Topics.sourceViberEvents.name(), UUID.randomUUID().toString(), seen) + )); + + final List messages = testHelper.consumeValues(2, Topics.applicationCommunicationMessages.name()); + assertThat(messages, hasSize(2)); + + assertThat(messages.stream() + .filter((message) -> message.getContent().contains("conversation_started")).count(), equalTo(1L)); + + final Message userMessage = messages.stream() + .filter((message) -> message.getContent().contains("I am a user message")) + .findAny().get(); + + assertThat(userMessage.getSenderId(), equalTo("01234567890A=")); + + final String messageId = userMessage.getId(); + + final List metadataList = testHelper.consumeValues(6, Topics.applicationCommunicationMetadata.name()); + assertThat(metadataList, hasSize(6)); + + // 4 contact metadata created + assertThat(metadataList.stream() + .filter(MetadataRepository::isConversationMetadata).count(), equalTo(4L)); + + // All message metadata points to the same message based on the message token + final List messageMetadata = metadataList.stream().filter((metadata) -> + isMessageMetadata(metadata) && getSubject(metadata).getIdentifier().equals(messageId)) + .collect(Collectors.toList()); + + assertThat(messageMetadata, hasSize(2)); + } +} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/SendMessageTest.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/SendMessageTest.java new file mode 100644 index 0000000000..39018cf468 --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/SendMessageTest.java @@ -0,0 +1,115 @@ +package co.airy.core.sources.viber; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.core.sources.viber.dto.SendMessageResponse; +import co.airy.core.sources.viber.lib.MockAccountInfo; +import co.airy.core.sources.viber.lib.Topics; +import co.airy.core.sources.viber.services.Api; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mockito; +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.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static co.airy.test.Timing.retryOnException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(MockAccountInfo.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +class SendMessageTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper testHelper; + + @Autowired + @MockBean + private Api api; + + @Autowired + @InjectMocks + private Connector connector; + + @BeforeAll + static void beforeAll() throws Exception { + testHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + testHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + testHelper.afterAll(); + } + + @Test + void sendsMessage() throws Exception { + final String conversationId = "conversationId"; + final String messageId = "message-id"; + final Long messageToken = 123L; + final String sourceConversationId = "9MVsH/2gRPr6pP72Eb6aXw=="; + final String content = "{\"type\":\"text\",\"text\":\"Hello\"}"; + + ArgumentCaptor receiverCaptor = ArgumentCaptor.forClass(String.class); + when(api.sendMessage(receiverCaptor.capture(), Mockito.any(), eq(content))) + .thenReturn(new SendMessageResponse(0, "ok", messageToken)); + + testHelper.produceRecords(List.of( + new ProducerRecord<>(Topics.applicationCommunicationMessages.name(), "other-message-id", + Message.newBuilder() + .setId("other-message-id") + .setSource("viber") + .setSentAt(Instant.now().toEpochMilli()) + .setSenderId(sourceConversationId) + .setDeliveryState(DeliveryState.DELIVERED) + .setConversationId(conversationId) + .setChannelId("channelId") + .setContent("{\"text\":\"Hello world\"}") + .setIsFromContact(true) + .build()) + )); + + TimeUnit.SECONDS.sleep(5); + + testHelper.produceRecord(new ProducerRecord<>(Topics.applicationCommunicationMessages.name(), messageId, + Message.newBuilder() + .setId(messageId) + .setSentAt(Instant.now().toEpochMilli()) + .setSenderId("user-id") + .setDeliveryState(DeliveryState.PENDING) + .setConversationId(conversationId) + .setChannelId("channelId") + .setSource("viber") + .setContent(content) + .setIsFromContact(false) + .build()) + ); + + retryOnException(() -> { + assertThat(receiverCaptor.getValue(), equalTo(sourceConversationId)); + }, "Viber API was not called"); + } +} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/WebhookControllerTest.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/WebhookControllerTest.java new file mode 100644 index 0000000000..ce4dbc2fd7 --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/WebhookControllerTest.java @@ -0,0 +1,108 @@ +package co.airy.core.sources.viber; + +import co.airy.core.sources.viber.dto.AccountInfo; +import co.airy.core.sources.viber.lib.MockAccountInfo; +import co.airy.core.sources.viber.lib.Topics; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +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.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(MockAccountInfo.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +class WebhookControllerTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper testHelper; + + @Autowired + private MockMvc mvc; + + @Autowired + private AccountInfo accountInfo; + + @Value("${authToken}") + private String authToken; + + @BeforeAll + static void beforeAll() throws Exception { + testHelper = new KafkaTestHelper(sharedKafkaTestResource, Topics.getTopics()); + testHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + testHelper.afterAll(); + } + + @Test + void failsForUnauthenticated() throws Exception { + final String validationSignature = getValidationSignature("invalid key", "DROP TABLE"); + + mvc.perform(post("/viber") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header("X-Viber-Content-Signature", validationSignature) + .content("DROP TABLE") + ).andExpect(status().isForbidden()); + } + + @Test + void repliesWithGreeting() throws Exception { + final String conversationStarted = "{\"event\":\"conversation_started\",\"timestamp\":1457764197627,\"message_token\":4912661846655238145,\"type\":\"open\",\"context\":\"context information\",\"user\":{\"id\":\"01234567890A=\",\"name\":\"John McClane\",\"avatar\":\"http://avatar.example.com\",\"country\":\"UK\",\"language\":\"en\",\"api_version\":1},\"subscribed\":false}"; + final String validationSignature = getValidationSignature(authToken, conversationStarted); + + mvc.perform(post("/viber") + .contentType(MediaType.APPLICATION_JSON) + .content(conversationStarted) + .header("X-Viber-Content-Signature", validationSignature) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.text", is("Welcome message text"))); + + List records = testHelper.consumeValues(1, Topics.sourceViberEvents.name()); + assertThat(records, hasSize(1)); + } + + private String getValidationSignature(String authToken, String content) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(authToken.getBytes(), "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hmac = mac.doFinal(content.getBytes()); + StringBuilder builder = new StringBuilder(); + for (byte b : hmac) { + builder.append(String.format("%02X", b).toLowerCase()); + } + return builder.toString(); + } catch (Exception e) { + throw new RuntimeException("Failed to calculate hmac-sha256", e); + } + } +} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/MockAccountInfo.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/MockAccountInfo.java new file mode 100644 index 0000000000..003d166046 --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/MockAccountInfo.java @@ -0,0 +1,15 @@ +package co.airy.core.sources.viber.lib; + +import co.airy.core.sources.viber.dto.AccountInfo; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class MockAccountInfo { + @Bean + @Primary + public AccountInfo mockAccountInfo() { + return new AccountInfo("Viber bot", "viber account id", null); + } +} diff --git a/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/Topics.java b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/Topics.java new file mode 100644 index 0000000000..ec2473eabf --- /dev/null +++ b/backend/sources/viber/connector/src/test/java/co/airy/core/sources/viber/lib/Topics.java @@ -0,0 +1,18 @@ +package co.airy.core.sources.viber.lib; + +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.source.SourceViberEvents; + +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 SourceViberEvents sourceViberEvents = new SourceViberEvents(); + + public static Topic[] getTopics() { + return new Topic[]{applicationCommunicationMessages, applicationCommunicationChannels, applicationCommunicationMetadata, sourceViberEvents}; + } +} diff --git a/backend/sources/viber/connector/src/test/resources/events/conversation_started.json b/backend/sources/viber/connector/src/test/resources/events/conversation_started.json new file mode 100644 index 0000000000..ba14f0f99f --- /dev/null +++ b/backend/sources/viber/connector/src/test/resources/events/conversation_started.json @@ -0,0 +1,16 @@ +{ + "event":"conversation_started", + "timestamp":1457764197627, + "message_token":12345, + "type":"open", + "context":"context information", + "user":{ + "id":"01234567890A=", + "name":"John McClane", + "avatar":"http://avatar.example.com", + "country":"UK", + "language":"en", + "api_version":1 + }, + "subscribed":false +} diff --git a/backend/sources/viber/connector/src/test/resources/events/message_delivered.json b/backend/sources/viber/connector/src/test/resources/events/message_delivered.json new file mode 100644 index 0000000000..1adc47a589 --- /dev/null +++ b/backend/sources/viber/connector/src/test/resources/events/message_delivered.json @@ -0,0 +1,6 @@ +{ + "event":"delivered", + "timestamp":1457764197627, + "message_token":4912661846655238145, + "user_id":"01234567890A=" +} diff --git a/backend/sources/viber/connector/src/test/resources/events/message_received.json b/backend/sources/viber/connector/src/test/resources/events/message_received.json new file mode 100644 index 0000000000..2e350f815f --- /dev/null +++ b/backend/sources/viber/connector/src/test/resources/events/message_received.json @@ -0,0 +1,23 @@ +{ + "event":"message", + "timestamp":1457764197627, + "message_token":4912661846655238145, + "sender":{ + "id":"01234567890A=", + "name":"John McClane", + "avatar":"http://avatar.example.com", + "country":"UK", + "language":"en", + "api_version":1 + }, + "message":{ + "type":"text", + "text":"I am a user message", + "media":"http://example.com", + "location":{ + "lat":50.76891, + "lon":6.11499 + }, + "tracking_data":"tracking data" + } +} diff --git a/backend/sources/viber/connector/src/test/resources/events/message_seen.json b/backend/sources/viber/connector/src/test/resources/events/message_seen.json new file mode 100644 index 0000000000..a464a906ea --- /dev/null +++ b/backend/sources/viber/connector/src/test/resources/events/message_seen.json @@ -0,0 +1,6 @@ +{ + "event":"seen", + "timestamp":1457764197627, + "message_token":4912661846655238145, + "user_id":"01234567890A=" +} diff --git a/backend/sources/viber/connector/src/test/resources/test.properties b/backend/sources/viber/connector/src/test/resources/test.properties new file mode 100644 index 0000000000..a32f4bd309 --- /dev/null +++ b/backend/sources/viber/connector/src/test/resources/test.properties @@ -0,0 +1,9 @@ +kafka.cleanup=true +kafka.commit-interval-ms=100 + +authToken=no +welcomeMessage={\"type\":\"text\",\"text\":\"Welcome message text\"} + +ngrok=http://ngrok_host.com +host=http://instance_host.com + diff --git a/docs/docs/api/endpoints/connect-viber.mdx b/docs/docs/api/endpoints/connect-viber.mdx new file mode 100644 index 0000000000..a2279666f1 --- /dev/null +++ b/docs/docs/api/endpoints/connect-viber.mdx @@ -0,0 +1,26 @@ +Connects your viber account to Airy. + +``` +POST /channels.viber.connect +``` + +- `name` (optional) overwrite the Viber account name +- `image_url` (optional) overwrite the Viber account image + +```json5 +{ + "name": "My custom name for this location", // optional + "image_url": "https://example.com/custom-image.jpg" // optional +} +``` + +**Sample response** + +```json5 +{ + "id": "channel-uuid-1", + "metadata": {"name": "My custom name for this location", "image_url": "https://example.com/custom-image.jpg"}, + "source": "viber", + "source_channel_id": "Viber account id" +} +``` diff --git a/docs/docs/sources/introduction.md b/docs/docs/sources/introduction.md index 37d4a9c909..1a9e286476 100644 --- a/docs/docs/sources/introduction.md +++ b/docs/docs/sources/introduction.md @@ -10,6 +10,7 @@ import AiryBubbleSVG from "@site/static/icons/airy-bubble.svg"; import FacebookMessengerSVG from "@site/static/icons/facebook-messenger.svg"; import GoogleSVG from "@site/static/icons/google.svg"; import WhatsAppSVG from "@site/static/icons/whatsapp.svg"; +import ViberSVG from "@site/static/icons/viber.svg"; import SmsSVG from "@site/static/icons/sms.svg"; import ChannelsUI from "@site/static/icons/channelsUi-icon.svg"; @@ -78,6 +79,13 @@ description='Connect Text Messaging to Airy & send and receive SMS' link='sources/sms-twilio' /> +} +title='Viber' +description='Connect the messaging app Viber to Airy' +link='sources/viber' +/> + ## How it works diff --git a/docs/docs/sources/viber.md b/docs/docs/sources/viber.md new file mode 100644 index 0000000000..08cbcd881d --- /dev/null +++ b/docs/docs/sources/viber.md @@ -0,0 +1,199 @@ +--- +title: Viber +sidebar_label: Viber +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import TLDR from "@site/src/components/TLDR"; +import ButtonBox from "@site/src/components/ButtonBox"; +import BoltSVG from "@site/static/icons/bolt.svg"; +import InboxSVG from "@site/static/icons/prototype.svg"; +import SuccessBox from "@site/src/components/SuccessBox"; + + + +Start receiving and sending messages using **the popular messaging app, Viber**. + + + +Viber allows you to configure a business account that you can use to send and receive messages +to and from users + +:::tip What you will learn + +- The required steps to configure the Viber messages source +- How to connect your Viber account to Airy + +::: + +## Configuration + +To get started with Viber we will complete the following steps + +- [Step 1: Registration](#step-1-registration) +- [Step 2: Editing of the yaml file in Airy Core](#step-2-updating-the-airy-config-file) +- [Step 3: Connecting the channel](#step-2-editing-of-the-yaml-file-in-airy-core) +- [Step 4: Send and receive a test message](#send-messages-from-googles-business-messages-source) + +Let's proceed step by step. + +### Step 1: Registration + +First you need a Viber bot account. You can register one by clicking on [this link](https://partners.viber.com/account/create-bot-account) and filling out the required form. At the end of the process you will be presented with your authentication token. Copy it for the next step. + +Viber successful account creation + +### Step 2: Updating the Airy config file + +Add a section for viber in your `airy.yaml` under `components > sources`. Add your authentication token so that your file looks like so: + +```yaml +components: + sources: + viber: + authToken: "" +``` + +Now run the `airy config apply` command and viber is ready to use. + +### Step 3: Connecting the channel + +The next step is to send a request to the [Channels endpoint](/api/endpoints/channels#viber) to connect Viber to your instance. + +} +title='Channels endpoint' +description='Connect a Viber source to your Airy Core instance through the Channels endpoint' +link='api/endpoints/channels#viber' +/> + +
+ +import ConnectViber from '../api/endpoints/connect-viber.mdx' + + + +## Step 4: Send and receive a test message + +Find your bot on Viber and send it a message. The message should show up in your Airy Inbox. +Now the conversation is created and you are ready to reply using the ui or the [Messages endpoint](/api/endpoints/messages#send). + + } +title="Messages endpoint" +description="Send messages to your Airy Core instance from different sources through the Messages endpoint" +link="api/endpoints/messages#send" +/> + +
+ +**Sending a text message** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#text-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "text": "Hello World", + "type": "text" + } +} +``` + +**Sending a picture** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#picture-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "picture", + "text": "Photo description", + "media": "http://www.images.com/img.jpg", + "thumbnail": "http://www.images.com/thumb.jpg" + } +} +``` + +**Sending a video** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#video-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "video", + "media": "http://www.images.com/video.mp4", + "thumbnail": "http://www.images.com/thumb.jpg", + "size": 10000, + "duration": 10 + } +} +``` + +**Sending a file** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#file-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "file", + "media": "http://www.images.com/file.doc", + "size": 10000, + "file_name": "name_of_file.doc" + } +} +``` + +**Sharing a contact** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#contact-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "contact", + "contact": { + "name": "Itamar", + "phone_number": "+972511123123" + } + } +} +``` + +**Sharing a location** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#location-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "location", + "location": { + "lat": "37.7898", + "lon": "-122.3942" + } + } +} +``` + +**Sending a URL** + +[Learn more](https://developers.viber.com/docs/api/rest-bot-api/#url-message) + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "type": "url", + "media": "http://www.website.com/go_here" + } +} +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 4bff3e2ce0..c4211fbec0 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -38,6 +38,7 @@ module.exports = { 'sources/google', 'sources/sms-twilio', 'sources/whatsapp-twilio', + 'sources/viber', 'ui/channels', ], }, diff --git a/docs/static/icons/viber.svg b/docs/static/icons/viber.svg new file mode 100644 index 0000000000..b5655fb15a --- /dev/null +++ b/docs/static/icons/viber.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/static/img/sources/viber/account-creation.jpg b/docs/static/img/sources/viber/account-creation.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a79cdcc5ebe87dd6b88efce95b21f845b4025d7e GIT binary patch literal 124316 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

nYA{}{q*TFXd}<{V95PDF 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�A`%tJl@obS&IszckV`g&iP@K~Grss4L=tkc<_Kk7GpLep z0nLhT`vUzk5AoO($OzcOpt=r(F!{#5zP@>k)ksko)LkhccTgbxU}GY?&VKIO=!-ZT zpnYTc?C4HTPPz<;FI{SaJO2m0hS2p!c-K_<1eJ~8{Sa@1gVW(9OzWCU@N6@Fo(eyj zQEZ|@jdFJ!S=-k6V{B~9D6TL;(0LhJTf$SISfxO;!iEbQFd4W9g6rz)nr#5A7=vb8 zl_fV`Viqx9-ESF%js4xZ^N{7DhX8C!WMbI8&&^i-Gc)vf#rNCGpT8K)pAU&g$p!*d zg|gxdhv7KyB4&WXep0#B*~z>`w=OE$p~k1FwCZqD`P@~$u{TvGp?3cDvyCq-?#1|y z3eX%B6CA~>9G4IX{Wbr36;yi z(2J#qn(LGIY+9#f_Me*M4qfJrF3Gf-Ywu7naB&{5jra+|$>2MkJa&D6m{c2PbcqaiP=>kn+Y4a^X8$d@+t~P>dg9|>Yf*W ztfduuWAf+}v-KQdAVP5k?jiK~6aQ`Z|7Y>mojd3LR@*hsD?gs3n)O3%eVdR32sPyj zTYZYl|9v+#lN2MUs>KZ8N^o#+fU2Fn+-`7^kF$(pn^cwW7 z5M%_6z|L2>bijS^g(LaTU8;TG>%cm6#gkDX?hEQ^$J6FT_%RBm?+3fP62OaS|Mk*7 zRb%+iB1}|NHv?l?N6Lxl$sEE!DV%BQICC19J|DNiQ2SA(s`%eAjVn zu0EgoQ;C}MHCdKV;nJ%bm)f;Sp9{Ws?%j$`t5zrNGa5^{7hGl)@u)#;=^(Mn)y>pw zOh0>McYCze*wkz(&Z=lYZ`Z>Qu|-Mn;@ba)qY@AVz;X9~zHXfV-^^2jMuz`q@+M&u zSVjN)x_|M1V_*sV#qIz1w**(-;KSShe*J&_rT_T_X`=*`kzndr16pkev`!c3tyBkr z0O)Mx&x{IM|Go30*Rzmd{n*nJ514f|mU(Kq#bml7k6qTr)u;?F+`M@c5{i~_(K7cg5E4>g zyvWLvta(S!-zID%r-MS%zPZdhS0Cgj3MG!$|15J-54)rsc0{1>TeUU9pJ-P80dlA? z+h1wZ6J_O`rouytbLGDd2qn4ex<;{K8X``y70x>uR;gdu%v7H~ZMSZKEmc-dPW8o$ z6jt>-XN`z?tSU(;9LAlQ(Pd%1Deu-M-p0RlF;FOCEiCrA@+)kKY>8J^Fw@sH6 zc=pOn$mLJU$+Yd{43~;Zq=b;0fXi}<+VQ_5hq_PRD7<`$TB;%?nC|$eH;ny zL$975H_vKk^|JIO--Ys9ItSX7hJi~C3&ndayG8pqW*UoJ*1R=Id2OAS_eVQkMm@5^*kSw;`pueb`J>2SDulcm>vA$oOJ{C>5HmvcQwIFB%$zA7xsViP} zblawA_d9CJ4ey>>Gx?dks8$%)-RVS2#mlZwt!d_^PjM)AE$ddmhA*Y8Vk-=m;Q^&5pOcp=Iyz-ahC1=g z=Q`8M^sxMK*(O={Jo=>U(!kVKoQ&NLRvj4;^=#=}?#Oa2G|*rAkkxlk&W-&(TqZK* zs}#G`bxEXf;XPN>zi+;1JyK5GB-N-W5;u_esZppBRXbhAPVdY3BwdOqrtLt;`^X+9 z+sH?jg7cxoQ}0wW+qn=YC>J@n`1*dG2utbc$i(NLC8g%F)m04^N>#T5iKePEt-6cl zo%{~p?-)dI;aAa(g=tO$kL&{UJ~8`{$#|M1ab{*_E>QX~b;I<1ZE!>Db*Z-sE=8pM zE+ganqYF)W22<6OTh(iC7v1YUjRo{QOy<@YdDN@ti_t7w=7hb(Y;HJ+sBQ^hZ#KS8V69ha+9(c{pxF;i8g zk}P`Da%2yFW_GYR_D|QPAw+VXLOf^f;;fCXPK~hY&rLaXqxnmPCuzUM4pSw-G@a}D zZOs9Z`ueU7hDA5PGb?XuV+BkYle(@AxJ~Y6Ga0n?v<$7lJp1Wl)R%OecXY0lXGWgP z=j&j&4>g_;)*POgO5BLX(EG-&9eQE+W7t!hHV)q&U%5xgRj%mO@#Qn6+dp@^bGxmI zc~ITX`aWaN!{`zVrU@*^|AV3Ks$c?Fs@Ak$!rd892Z(YV3{v2<>x&CHLqnGJg$WI zJ9?taZ90b{C2wXCl)HLlRl9tS<|NTn{+LfqXSjv)qpx8LU0ishR$<#T;W$-oc5vOQ zC*kDQV47rAHSRP`iLc1_W1eP3kC|z7sY8GZEq<9)lnWQl@Jh1qMsrfg8Q1Yx(J^>s z5)Ax{tvh_kZJ*-Dkt>F7qdLF0K8Iob=r;YGqP_zArBETB2C_qmYiM7JV|Gu^k;*mR zk-^dVJh8BtqvGq?KcTiO&21KwD6`D9s=_bswUOXFU!N^Z%Z#7gf`Q^Ajz{@}q$$&>-BvZx}l*fuS8C*L=E?6@NBFxZA0FxKOeCq@pJNQ(V}FPBH&&rCy_vqk?O7 zr=H<0!X7`^NB+61I+E4Od`g%g(w^{H3NF4*c}894N* zeB*XMS+NkhdN9Z@7+&fgpl{GOZVj^RO}~jg5g7r8Z*=bQZop8w)JGV>Bs$aZEKKkE zS&^d`IYC#xQ(|_ydR$kh+;%GnlLhxPHXFo>+XkqydX?id2KJ5L?UGfML>5UK&h{sO z@lL_e&&RhSQj}WJtnLD#>{a1wrqwUTY8iEFTxDYPHI6WKd^5D?N7*jqEOt*vQImQpxUOqO}J)(V6O}qYiVXVsX6Pm~G$2#w$(hF4))BHq1IHZq#` zz2@d^N6g^Ty-hW@& zkV#$jZooJ(A&`CaeqXw(lZ8g~XG8PpGx!*TD6}f`w4TQuDCXo1x@tBU_}p7a(6(i( zw)0FAJ57mWZqE6^R!v$Z*w>6xo6Kjvx>p zKfbB4A1$+r0ffLLlc!UbhbI?}q3Hy@Q89B@1}fER^lE-oe0)CeX6A5&jKhR$fCvTn z$$c>XGgSnK?9uvycvxrPu?KKgOSbx=!HSl=(m>v z7%3~AxZ}x4GHU94&3W4eX%VXq8j;T^2z> z9q1(l$-BW9y~1QOU8@ZgojQ!^Ie@pXxmdira0u12Af*bB?yt}N|GojDU5MLoYut_pA)KMNbeErgMX--W>rK80Dd_31gT>usW2;C9G(KsKiid+{0a&D&o%X3 zqL7D^u>7Bac@!1_`H{PkQCgDUsfRucdXC0rXJ@a$giPK;@OCbhksm@&Rw?*+W%@-9 zp{JwT2%7Kkjbe8-&$xkpKz09x|B9g)0w7i{9%d;ijTlFWm{z-Y3Rur50rD6NC0-(d^6sUkP&5i3bFUa@vQg=lKVv?0SSm6CLNU+anZ!X zwZYuaRdp~amB+j{#h*T+$`fS3y%pZWz%kf?iI)%sM+V_mxx1&)t{Q>Q-ia zm42_*RWQ~JfD%;z(x*q_0muh-F3|YY*eZ%%z7s|(q!9@NVRWI{Pz`A4QsHc{a%a0p zwieKNZJg6Oz9*twcj9lfD^3ZnDzqvuhfh zF@KfxcIO3Vj&5~j1vw7yIbQQ?FSO|`EX>6CVdQ6)l8emhIFEFV%2|ao=&4+${eHPy zmoKqw+KjnGwd8K!^2PE5C%@0EoJJp98v2YQCXF0HnXhSNBfmd3MV1@rt7eBEenjmq zDh~LBL)A4gPMD{Zd|!bUC0LJ)O-l81zS590VwfJZxkw4?d%8~X`gudg_j{wnCfvSn z*|fB9F3}o=msLY4Hb*it_<43+-1x`qy<&@MHcv>5hl^aas`aQI{Af70aQ)|#p$)$# z@yBthYr_JAr52BK6YwVisRI5ah+@<1dH7-t?^5@k_B-9BhcT5TMsV;~xGQZ3d3^uhrat^XGGS6Yp{`;4s=x)B598ld2Bw%b7$of2hTp?QR z>mw;2%)vetm2^M&EbyygamlmcsVD4K)iATRj-uwC=G^qi!YM!KUck1@J>veDf!ERl zh^|HtXctcrgI`MJ91-at^ikKsujjVtzw5HnF9{Z+eSx}uo$%8m=m&R!C5OI3a@PW6 zEKg`!j`}oLL;V>~ffAGS{1Q2QyFBby_ohBzF93>TXI^-wc{ztX90wpKUPySXu1<)7Iu$QQ~Qx(QgIEcSaZYW z1VxrdQ*}k>YTvpO0M?PDvkv0>F(c72N`@8ysi4Uf* z*r6>^&6sCNQ+Kd4tzXA0kRHzwVl`7bzm*iay*OV~I9Ko)y)DBu#2Azt*!-DvLb+{w zEy^C2uL8O{a6P?HXERP1XJZgqfAX+mu;=eCmUWjvxkI>SQ~+A6fw+EYAyN+=UUHQT zGN1?{@YZy&uVTS72Y-xYSqcYr42m&JIJ1)IMRTjS>M`t+;N-*rcxB9k?izgQ09`Wi zt$5Re=l<#nOg(1aHn|4p8XV7S)oVqn8XD1n4oz8Z9IlNZCpSmGr^EE{!aslPVScd$ z^rgpr#f$3ZAii5pR(>}@gSgoTE2awzHXM?h51+!2>;e1`OPIKeuQ5!iVCC(O7wb8? zP~MXPiUbb*LN)Me;^%Tg?>{dR^tTUCBxE)=KE@CE&RR>s8*f3zbjIdb4m< zO(sUhJXnT#+9g@8ThoO|$Wz4ISng^*-iIr+0uD?xTYGSEIRY-FZ4SD{=>52-{AK#_ z<_}vh+kvX8Do^N}<9MwS&ucRXP#e#n>XuvgL3cq{EvHVYG?Y~%_>tEmC%>ATbD>GL zc+C1TsVG*QiAnK_`aJ4u){r66fa%K@vU~I*5+2%1RdlUMjknj+ho5vLd(j9E95B8w zD;$uYo@4G8PPEdQqGCpVvZP!2l-C`9d++z>CP@P&%3|E zou?usjq!E60Ky{bOWWfaROk$^R_IWSfhD#m+V$2k4E}i3)^@ipt%pYWKf>pXOow1{8dip7#)R`6BRW z$6{dZ6pokMQ8n$q4OuS zY6i$QsQnb-*YMylbHViNP2%t<;O8wn0o#bi&%M2UXX;!9zRwbxZ--!!u*-NtyN%0) zO$GVrR1XWRykVF@ZT0n;o?e1BWCO8`*LhWN)~r7qLKw~n+kv+<>!Tsmyx?_wI8(}J zGyduli2*3RQx2`PoAkEu-~v$#{Y2(P!$&)vTnhHegNJNgAeylU9)kC58T>dr3k=-m zy^mqkkr=nb{8KDu1{ZzT2exrMFo5S`(P>;#JN&c~5OmjuY7>kLcsqgw5!$-M`>q9S z3&3w+6b_}KtErdPZ8BAg&f_U4yJ6b&Q0T-UAKZX`e8BK&-k8G`JiH34=rmVW(i~`A zmmm#uXO*}&Ctj-DimFxLSc@fQ#ZfZ z#E0_h&z~6@g-FFtEL3}8$r>6FQ){+KA|CQ_jCG%i{9G=nkJJPV6;vrSHBs8(=2|1< zM7)x|ZI*DgclT5!0~DfUUxU6;-EQ_a=gH@B-w^Y15TNWM)CLHd3#YclOc0m&V2Ut(?7;KeRBO^%3_bew~OO(NtKvs*9|#yhIT!2 zds$!t>v5-Eh+SbYw8(wOe5u`avm{tc>*(lE0Ude>5+tVF7sBvEvFlOiFI>ol%z~v; zKOAP}UtKA=X!ZvccW&S22Hfc#z%*ga3u{aoJTMP%Ufa!-K8h6?G3jDUFyefyh0cBU zwYM=Vh?wSJBMbSHLsz>$tSg!=wSrDxT+yD#^CB_a|RS2^Z)`^RX;U?$l_8KJgcwGfMJP;^V25>-bZkPPnHX+?0I zu~786kIfJ)6V2-AYvq3NB8ba;V4(xqGonPwQtbtN$fB^j z77j^l#Cj;t;g4yWc3T1Z{E5SCx6u0~6h( z*4M92aN5UO#vnPpFTF%wnMEeu@SXDUjv14Wb_x3c!q_ZXDE#X-N9|a}L`jo)U-*R> z!%g4Qrhro$RWr{bAn&yk$j?4z*{!X99GU5d#d}pfA5PWPjI4lLe6(Mr4`^XaTQFKS z{VTndD z&hFj-A)D>it<**fwIV%j=v%C-!pC)Z`iR+0-2l^!j!r7&FtULUzhQ$vCkO0HmVl-W zYF2U$>Z+?Ys(eFLZEcJ0@h`)!-+`_)J%LS`w?b7##k)sShO6}l-jLNV`3bQZ43Q** z4Ss%3e&E(INCrZF#EX{yM1Is7tbD_#`ItQ!RnO{%wSJw(i;>wj5k3Z%vMrY~(3 z=iI4YSv95-Wf*d7mOFg?l8T45@+s<`uwLAW>$_>(ZP|Uh9-P5gFpa0n?@RjH`#I>n z_THXERLetor5IDBBc+EIh4Qt{i4klAwAcOKy3cnfC|M12uoB%cTsWXshjwVz6plo^ zJL;4AbUE|8T`q#JW-7~T%Z*;bw5ts_c~7pu2RSo5jCtU*p%YAcuSz0_Q@?;?E?>PQ z_qo+!ZFfSHPg7y??|p&eax6@}cbRg@Wu^2`(;wFf=r*kgUarT~R(ykbSSgE@J1x7L zK1^vkDVq(rAIqC8LL+r^fp5w7Yi~6!(EMr$82p-qM<5jX~zHsaq5-99i{R2Q#PYaV!$Oij3u`EbL@od)BL zKuINYqzemJiJEjIQ2ST`r#JB7X6iV`=-m<$+4m#xYxEiUdluLc?bRcfXga^|AA+tn zxTCLEXkPC1+C8DZGXBrYfwsPX0!6nv#_O|PIt$NKFAiVCqOB?^*xQCDt8U6p7-Nf$ z%!g%sD)}Y0xLCY~t7{(a?#L8n4mq_R93KwUY>^I+&RHz+|Cw@AoN_mwoA3jACexbT zo35kv!ph9`pFzgAIX)I8{50XT+%$9lQu$0t6wNvv7lWr7J@8&7kTY*bI} z3wu{}5--Aml0(S(;x-%iN4+Uyauh?MrlHNikS5VuX3U%Yg$Pm( zgM`9dyJYU>X&^tUOGUEm{KBB?+YIqSbc5DSywVu??QCd8jQ$cUL9!re7j*Me+XQ_!zn1UDi9g!l_#CaAL z#aBir@->?-EQW8K+yB^#y&AKWBXt;>F8l&Pmh$%P^b<7nZ3iTd+rr%-PCBqYL7rY}4<+w1R{0e%@M zObCghgrAk~t}`;`z{W^m6a(ADoOUuS70N6!1rJZ_EX?2jyW?D~qcpDF94(o@U#d`Wq( zDCgzCLd3-Wa=N(Z;gp^T&;21`UQ_vM^xpeg22m7DplGN3BYgZSdgUxCV1$JF`e^F<{?|{{8|5k9twB4B>gH>fUrs&OX2jHxBo1K$56l#UTEa=5 zzesWE3aC9ti`s7r9zJ$ojQSiWTKDJ2k&x=6!E{WZQ@c3a_(B6(?MAnn->od=nm>bs zwJvWdMO&Uc)+dGTV-EZKR!T{lRin|_tx!exAq)r4w2UwVhn*Sj<(?w&z>Z^?p6-@5S`MN<%k7j^Bi@ER#7t{sH&|^XnZZSmqguUf^b!jJsfHjNUed&L ze0l!f8snkll*Xo~)wVe1m6w?m6TZFGi|U!XJOo~M zTU+QX9(6QP6yNbZD8Y`=dA1cD5pEV;H1IokJB*E8*yW)WtPFUA!|R>hca6u# zv*s}DPJ0nUcIo1htZH_W@WzNze00JxJqe943xrA0{9fU(_r*;l@yI55@J;p90)R4q|C zO+4cNm;tI-^O|#ifB#;u#MQ42s`^8}Ido98gU5PNA3wgZ^#e@r$lrsVp9Hnwr7hn0 zY@oXl(_(h%;zeb!Wp?=(2FS_v7n^jlAA&#Ir-u_&J9VW{>-!JXE?#EntiO5F!<8%M z>g7n&u5abz1~tM1D67-st5&Zq=70Slmi5El>vr$f|Gnidm19^);`5c4W29fq4UuG) z?Q)pxC%D&F@EHx|fr5Lp+iQ;&^YD0og5AV4uWfB%xv$@lex}G#s|9;>f??he)|p1l zls0dQ+VUdWhldL(4_wvs-UR4RolhLcHQ+=?T92cwlvh+O8406SJz<^%ElpQaRO(pC zRO?2IjRY9+4i~VxqCHlD;D{3COI?Hv1A0%i}87d@B0nz2g`L+1r(TsqMxeXotOXa{0w zq?mV!!yh4;`DtK^tqNuC>x({%h8m$x5d~)bVfge*fkpAbY*aAb=jU18U;)*U`OiLd zGoiF;yZCTdlL(g%z=6FFJt6Kso8OIFVI(~g8lChrAKINdi!E}b$Uvhmr7(FbhCBn9 z#H9mx`f3Ta1Ox=|WNOt6z+HXE_Is#x-y7uQnP3Qx#eh3v&y%MI9(MMoNwL%I2&ef+ zdM@hzL82%>`a#XXZ@FU|o3{rO_y)?Ha}O^+eip>N{M^cF6ti_zbmQ1XFjMBUBlr9* zH{2;$$_DdC##T6X^VkYGSnA5&C@IBc$vbcbKO33g@X;E{H{+Nj@nNs)HQHxNUR)BG zHXO~KgVBSbT-6Oe=UvDD=LJBj@S3jG9p6*^RGXSxX0kS*L}~EK)9-zm$$n-`^eOrM zMS~DM1f4GeyDfq!F&lU?iJ-4LQ^c02_?3@KVwy7}fv2+?F^r&B z&RZ+guX(&X3B`+O>}Gv+s$KFm1fsLuf&5fy#mz9bBc_5}(5}w2Dqi^Mydv)Qqc_7a%7cGR1xXU$FA}7@`n|Nodhebj zWY{P}&Cjk+1`_Y`SF@z{!#L;_6xsfDeF@q$clXDw5MXJkhrVtSTEW3d1ax%byA3^h zAP%%^1JC#Z!`G>KSHHoxF3EG1#R&(>N@RDs7%B44Tw%p@2j90{=w%!PpfN!AvGOi- zoQ^WPse9#j)(8?Mv zDx(uZ)VH9uH#d@_5uK67tqH4shCz#$-@9%U%*h<@@RHov+O|N<-n}+(6xs3YuU}ssf^HFFpP4+Y)seq9WPmd7^`1{J`{o2 zZUuGAn!^E$t^R6^(czz;uFXSkcGp!~vY@X0j9%kf+XW8i%(+M0mc5sD8)PquQwj4Q zeL$t={3XsfDbR(p5K2KHv$*3BYjv|4)tc@pXC8jB)w2rHxTn{;#D~ZQ8G@ybd_-y6tDtN$zR0X?TC5zMFpD%s?`4;fJxLr3T8>EIt~(HQN@emAGx4 zYN|5Q=iNsH7lOz4P|2i)TG19`qO||;iaffVF&NX0+0s3%=t;!lv|W)RO>xJb)!JE| zn!od~8|WvK{?6bmRFrFT?Bq>J>fA|Snkp^B&|C@8&y^d9M*V8KZ5 zL`nc@0YX4Zh}692?ON-d{r@-f&GWponQ<>Sn8KawzOJ(z$L~z{{=lGK5t`n$vFnub zp15{q@u@##3nGuk{ly0{{yAsfdQ5Mf*(4TT(JA;Q>e|il>S=~S=>@k*GPZr$1%8yb z;rD`@V}Gs#{@l*4cj)>~a>SCi-{Qz0&*?v(FaLd;oY+*q2THHy!7AtUPAqJdMF_f( zm$!}R3Y;o37^RWs0SwG@WUoHl2(7PX$_QY%)KX+k!1BWDWQa5dll?ZWdlkYf=Y;LP z--)=a(V-`ef!qR0mnuTgj<+_fd+Z#r>Rf}Lh9QxAoXO2AOwZGsZe!nRKpR zOzy^Y+Qh3tK`JV8ue-)S21FO#L_)C{kgZ-D z20@svkNSFFEF8Cm84Y7`Rd%60C^BJhSYK1Hxu#UgOLaGUXoF|ucG`NK!;r(F(!zPQ zc&(NzUa+aYUARg_zdvs zKJ~P+`h91VX3%fC!>FE-MJgupx%P%ys3SpFZk5Enw6FbsbmH+60mR2 z=KVp_F?G)H_Q7P3NZAZm6hc`q6KFmLoidcf&aUxDke$|p@Bp^O$jsVv8n8rq8O?m& zp+hcc0H@6hfO;a}Xx*Ew1)eda)d2S2<)H~bKyrC)FJr+@0%Eu$47?xQG*3ome`3&o zW?7!xNhV8nVi(q*`rih=%}pfs=uDR3!!O3Ni61{c6C4boAtdch>%|s?Z#tOdhVT zwC|##I!i~MZl7In`||RhjRg>y4F9@_~&)IhrU|kHtEk?>-G6L?qOHI)Cn3yo{7K zo8L)`xzVpBZ?q40K`IZ#Vheh+&s#1pUq@!C9qC_TOLK#YuxU&_1O_0I>0&$Bny(nP zT6>z2{$Xor$`=2LLA#!+;sMK^-fXLloz1j16|kZ}RrO5$s-J<#(&-7;EuZP6lA#8f zyKk6i69+szF4gW7nG8J1Q0z<->#V1kQk)lwbb_jkcTi!M*B?~<_0g3jsd-b9QP`dd zxU}hR)RV_eDL6g zGCe&>QQ0fb*S(etZ^I^Okn;Yz^>4*}O#7Uhq`{vwP7YG2l?;gTvLFe_f%+p-(|W2g z*(P!EqC6-O^$ZQ;p`4U=;Z%&Pt7|#1Z9zY61fZQe#>U10VCwFN-6kqLJPjPCIyrZ) z$AIbFn(uROR8$lQ;5|XXBVRKWu7aGp=<_}BZ_5EvfH-w5+v5?JP&Z{22U-UZ`w^pO!`j?0&cpoc_6kf2S}&G4P{nQ~JGOI1|(MBgjzTpN)D5VOzS*!Vs; zsGgZMr2YP>eeFc&9nD=GKcVxZJrmwF)FdZI^ zE^_A)G_bys<^NYEXrs1gH!3_x+AQDWKP&S*W$}*zdO5!O0o5spwKj;Dz|E%u+e*51G@#Ok{ugULg@!xgw_mcSkups>C;f8C>BqL;h z=mrAZ0&=&**x&pQPF-G)TDun|P#9uCkf9;nq$Rg+*~RVJ8(Ha0eLK^IIv zO1)=dP~(<+;o?O^^t)q=eV3eE1YJT+fGj|DUkB=|h)NRN*jsgGQW6pdU_^^%7Xe%+ zB!X27t-Qj*+Tp?=xg^i^W`FPJAuJ<3OGMVp&IO)I5D@DER-(ppiQml3Y{=}YwSP})fEtKo2>_7w0$c`ZV`yDQyIsBN(S#kIp0w2?s5h)M zTlxIbkQ;Q{u7bv$TPq*R@fQ*8VULeb|MfVbCAyrG{fi=t%evz@$E zyS~`o9VgVICbp5Mc`?MM?}PD&RR$=NAn*~yR{)ehe#4!YZ=k@TF-Veu3> zj9=wC0>2rBKbP z;O^rp1v1fy|JT-7nRNsZ8w(*z&V?yu1hQd=Ir)#bzix}z>eHXNpp!Gc%L)?-h+XFu zeQgJOCYG!6KL?RErx~#oKYlO_l6q478Ti)jO5V1?jbcfUH(@PeK7j%32_cl+LV7e!%USNcycS)1O0t!kNC`^rZ@(t}T}%C5_JFAUWW z^P9=lF3z`6XlbhK2KUf3bpVQa0T$glST(#sU)?a=urZThX=EH%Qc|)I7ZeJIQaf&6lOG&yL7uF!IcVgq%?~Q zTstOVnI%^{0?Ke^7uIR_l@O(^TIK?y$`01!$A6HS07rB`MaU{tMoMaU64gOe2Ew8Y zYinCunU$rb7v4-=Jyb?Ws5dz(%6%}Z|1(|oAdkNKcjV8QzZ`YavHL=k=|`7Oqt&JT z#7XFhmx6BWf$7o$M$fdl%)458(41N_y^(l0`s8}NNCXKiuHRSlR$xlX^p+0}L){hI zscRu(*<9Of3r&c-mUp#C>x`z~zPXNxjlO*l7E*hKV88Ia7`Z-M&>!*9Bx3W?mhqFF z-#$!N<6$60ZIYEEt3NN^ZgtqP!iSQOWM8RQZF%&E)6N$yYV6_#ncs*#e|)opGRtBd>-$*SL&g)OG%q#iS>6Eg%0ES6aAxj zQdY!A-^=%|;2o8N@zT`7%DR-TX`T|2i0vi9V1wKiZ*616&b5^7FqjhKO5~;)Uuj~3 z{e#{Ws0mt+A6J3am9VT2trd19gq@?A_Ju>P0*ci~nZ@rwcP0GcEhcfqi~zkQUfpR_ zZVNAF)kDH1sRXGc3Aa7y+T}k8eZyo{)YNfYNI@;<&*opoMT|{P`;&=&)}6g=dt-2BeTgVU z#*Mb78{1!JsFXCQ)O0WzgSRRvssv5`P|9d_Sc;9Jcnc{|Ow0Y(2MXo%Npsx1!W=Lx zOjL3#RM?(B`}MfbAGdy5`!jBG*|L2v{|)>%O-xa?h1$2P_H_z`6M274SPgkny)|ry z$`*Xg2>D^XWj@c}&`Ho#eJcJ6XG6@PgqYb5(AmS(ATjVJE6K{nG|0gy@>bPrKa)I1 zHNi*6t^{mB#Jj|_bQS|tx3-uB(ppEh@o{A}CX^62OINQ&>PS03&}hyIj=k}{k7HL?qv!ttm6)dHMO zRb*XeZDf;ci1b=-^IhmVU;DK_{{8cA>)?=PUCN?W*?oCy+Wn7Hj#)GX_W(P_rMcCf zEGEjAl5k0>Cf_mjk}79zOH@)=^c_`Iu5#T zI3!_+>By=eji5E(oZZT~8=Df^WC;unxB3<3Ysp${Al}NV-nr%|Ct$B0FDrv5HH6Jr zSg*}Kd0=Mh%gUYWImS1)>=!LinQJ!IyHY$v{u!8EE>)43r&~VTekjvzjHNkrec9$k zR-IPQq=+IW*3tp9ZkP(L!E9r4Fj?^g~ZK3VBbB0Tdcdio4#nIT@EnO*1*^hBJf?*VD9NeGF_=5MeD&G&?qHgI2my{gV6R;U5 zdo<+DA+m@U49}>FwNp!s&77Da9Idtxb+IE1h8j0 zFYfS2cvY$NEJ(zeR2*ShkHl=^3mdwd&hQ)UkUMHqB+KX z$+>Q(%-Xu8qd~g0>%qIlhG9k58HoY2<>H}okNR{UNw+o4h|tjEmTdJgSykecno{R< zH94QTUy0R@x9>FsP*h0I8C@czl*rQgaVYe^8VF*0M*8K56B3SqY`YjU!0eZ60fH1!sgvDdgitC__(i~1 z!S_cB)b5d%BTQ$s!Pk@{?oALV5fN~?%eB%ESi{p`l?3Z=p*u9U@}ZOu^R0>i>-!!! zoIDUYBG$BF%ZCp+;qQP4-kb;#kTxZPX5~s4NTIQl%g~CQ1urNL9KHq6sX&eb#3pJm zzG(;Y>wK^Eg{Y2ebzf|u#D z4f6v{0<_{ppj}ou;t~R;x%3Yot{})KU@o%`!{{vh!YbS#ULd^chi61orir-Hecp%% z9T1E~;O+HZtMX8vbSd`EhQPxJ0zw=Fv}ka&<$!jzx z7z}S10zxj11VbqAjSj;l$Fl+wvu_I9;9eg@>!}~yv0t2S=jH^ok0g?sd zYj*$~>ktMBg(9p8CMl1C-`olK`}|OO{$Lg;6b}Ph1NeP~Agih*al{FlR5~OL+amf? z5GBDltvk>&cSkIipn`)d9f!RNzW(%?Rp*H>uMlxCNKzA|iIdOo9Vz*S37?FYbh{T^ zmsljtXFOZANZbFjU!D%7VH3s6^@e%wbo01*IWapWm_eLPTHLcG+H+J%&-tuAnKD*w zFM<|vGZLogJpW5h=a1#yb$h+9->xLIAO8@Op~!lu@5!FfbGtFCy@1;zNPrV!y z0p&B}VCY3e9DBILurfY;% ztYgRf>#D$1sOaOlrU{|vN&VbHzCB5F`{!$jb(e)ded4XjM^Ek)Zt$9*rnQq^VVl@8 z0OEp7Y|!IAaS(3wvv3(fS}=1w159z?6|H}m_4e(D5-hv^lz1O<+YvYZfv(044@m=>Q<9 zZghpoX_<&>F1!yS5CdMuY%Cs;$CX-XfaGt0v5zy`AsTf=k*Vmo0v2=fBHjof^GU-yCoie~^dj8`FcHlDBZD-BA&-A&nAB+igE@}& zfOJr%#RdSkHe(I-is76x_~Y8(3r+wQYsM0WI!amwlFcmWoAg{8^Hd#&dLaOS-wGMG(96hIl&!(Cjil8~3r|-at$*voRHcTyyD`yDqQe0kmlI?$+hR z{PC8Cc!}2Ti=$W@;C*DCnn&g4hHgxA%zZqlG9&T)Eqf)`%K@~&P|g81wz=FN)%E?A zjsm7K>y9;CV&`^;Y)6S|Ni_^4ywNRhN_01i=jOaJ%rp=c^4!@+_bcB>%q(Wij@)?7L&7mR80C({r164h{~= z>dx1*d^NQx7pry$3xr97lQ~pd+;{rYuid-J%vy#3v`Ohca8_)Jl`79qGLKqk>*~x4Q#CrXe9GL`5hVb+3qZ}wNN?)Vk4MsA`Da8VR)zw zC^47-&(E&-q|IoleN|>)n_2qt9po5c$I@Q_1TYRzyyd7>OP~x)Jxxx4Ma0d`tsL$K z3ZLIdA{YRGoExykD}^?G5Q?>~943Q}S_L?wfryF|UIgKo>cH>|c8R_5;8@i}LU)MN zkr-#e5ePy<@EnRx?OSv;gYj3*UrZjHkf-B<+Y;#mgN3mW3N0$((7ERz;cIck)rjzL zVYk^k2+9Cb@Bu`UXU^vH^LoIRKOEs|PEzP=Ya5cz6VKprm-GAcfX&TkTy+ObOGCxx zK}g206x#iV)xkYS0R5Z+{-#Gj1|I@<9qhQ4UMd(MJ8YAUHRe zvt$F6g5SUzEcmO9rB+J4$TkS8Av^<4By0GrhTxOU(nxU_ojj=_eZS~Es8}`OCD;{9 zhzX7jns9@I*FFR6J+Z{DL8XL=@4Ijl0j5ABe*ZK~G(bM+9iY`9!WsC0AC?=E*LpPw z67CWGHvMNE>)t~#;}ImOUbF9m+IHY`;QDzQ!q_%@-1G8IEYvuphkC$Qij2s4bNcEH zi5j<)xUT3UL+<55@7lniHDrFg2E&6qVSYDh1_LDFp52|U&Qieqfe zz5=*sQ)~zF40tHgmzkIS>|wrM+AA$)t!RUjF2Xi_r$(!2#{KDEfe&vcUerDhw!5hf zAPoqkJBMM(A(|uzk?H|}fB?NLiuI8w5aIq@kd&kT*!$jnethf?JcICtK8}KrixcBQI ztRvjqgN`VnZ=4c)T>%@ieOX^i5mE15d>Ij&1Jn!^m;D8aABO;dKMy;2G#ZVL+xz)U z+XzTqP%L^-m^vgp3h^8hupj`2bUCC6p_W*K?8?#%McWS<7#h5!hB#x_Hz2;@hA0Tj zYiQVoiiHU6P&_Oe-2MgrGN=cQL=s?}Qh$}RISiZwsmxu3(vf0+>lcOG`V4gwa5cvSO9@VwO^|mFL9;`^EJO;Y zi0hXnvh<8Va>hOf>m7E#dhL>jN=UsL0*gx%|j(a z8?F<$D15uaJSV<{U#gOObSD@$XE`$Dd3SLVXKdo+NRT8meIt<9hkg3SqLus%U?*bNtuW%{?gz%&)fe z%=ij>7_I9`P$ofDa}Wzek|YzA;v2G zV-_+q>10An^%VynAD@7bcgd~AGt*#)jbTU7BJjD9A4{{dPd$w)c71pYYJl1mb_SJL z2+XY=qyp_Aq7kZZ)+;ie8@N~r0|1ar$76BC`W8nb4{mf3XiV#m>)_*?AH!hS714H% z>AHy*&l$s(tbTVlU=>otN8pMN&lq;7RRa*tfwxl~CS901|-l5QX()W2w3npgz9gRff7rMpc3nRsla{~xy zlg^_EA?o-Ym@D1~hawbV^h$4it#+}e!dME=v-MYjH%l6+1|ayNHk<<-&FoSf){rVW zgzOQs>HB8qk2CQ+HM~xkQk6YGON;U?1lmtO`-Ki!s}4evO#L00iJNQnT;c%Ex_0m0 zk$s_1wCIAcdwiL|tm%4HLhd50(BF?C6~mnvN13&kNr1C&ZayE)-{+@WppT@~-rJ|# z0~W^bS%0-tM2|wHOJ-GdHzm8MR3eJgz-c1YZqPrARXT_~X0k7xQ1VAnyN7eOvvpTv z+L#|t`nyS${2MPXz9pHL+cv+}CNIPmoOQU}Er2nW2 znGV&uX+)Z9f7-5|b2wrlpg;b76cZk1SiZizf_i`}N-5plXyf~#>zEY6j$unOEoimh z9dW~Z4WC)XeYhXo5+jkB%o5RFreQwDmHzgx&dtp>q1`OI%WphYGM4E9Orp( ztA=SeuiFn#tkl(2mg;BB^{Q0+ER}G}n9w}odC8_gL(?=22!>=w$971pQ3Y4WZJ^qK zz?c!RAq2T$8xI3#j&dDq}W*f zoO>#op&|lcyUJ!q9c4GW4Y9rmmV);ji`(&^+{FdiMU*5o<{R{^>OMpjN5Cf%jI^H$L1Uasgr)a40#3VUu35;r4e`i+d*> z%dTClX=NWjlCMZ-=If%Nge0sf?aEq9Tl@No>M6Ts946tk*w8`tX$&w%JQ7<4GecOv z$c4vx^R?N&D0LK#(IU@}n$`Mjgve37@1+X+RAG`)U(MvGcD>)|e| z(w-?;`*_1zNw+diiNPXAMya&MeFyfpbgE2^r-zjoH^@Vda*&Ppy+}$XkDp@ng9i)2 zugmCLT~>>S+Hdc19h0LD*iWIC(NTAYJ!DY>Kb$93Z2SCOaP3}1(p}Tm`5SX1m+zo4 z$(~bk911n0AuZFP8x9+nIg~bz?w6Vqd~}|Zle)@q;z4(8u7&Wt9G4_R0@(1|`*_g% zf9^X*Q}h=|EK+0hVKZ#Fp4o$X{k&qB#B}w+GX??Eg_UW!+t8_5SzTR~)bNDzts69S zt)Y;#3Cps-z(A<<@gq2-jqYtB^|i_)^X5PWDLgFehAoeffv+lQyyshEca;#XgmAdde zRba|0Mhomy!&Z&Z7?~6OWDaw6Ep0MGxMXDTb!Pef@k>^EJt*}y)>EgZi(@NcHn!1? zOwKHIzFb>9i*Jw{e-29ox3opQ1En)nRH^5^{Kn;D3m$Xd*h1g$)!|{3$?-nNFXntF zy@t|gL!OV8K6x-Eyy=37?M%95yRyxvVGe+Ph9;k#|6|?5F{0qh+R6q$yU7S?DC+FC zx^l_fTsB@YYvKD3@lILYTHT_yW&Notlq(yQsiH*AE3yfE(+&#dD4z%!xzFjl`FH>t zyK_4f7WWZ`huvVUToU5rtMqG2NXY&6?L^B23Zc#c|r#^X6BI_GsYC+Oz zC|@R62QPwBzLJy}0I1%-U^E~9n7Q-Ozf8WF%OpU^Y_KLIB0^4!M%ylpfm(Uy?1G&4 zW+p_x`Cu?dgae|EqXiIH#67V^$}mnpZj^GaRXdT+Bt=9_mV4$CU%rJ@&GfCH^U-TH z>p8M!qh7xb($dDc`d&htHM{UWCKhDJl&6R`fEXrzF!aG^5U4^b1s2nO!n zEI@|@kCaNG;ZpqB4SIGm91lp{z_8w{9AOzl1TzoYa33fiA{@_9jYyq3;D`eVrf5XQ z^4GX)+_RfLML+?*?|>AUky<$wZBkn?yh#Hc=q(N<<|_3#=Vg$1s#l7af=y6L{rnT~eSRW`mSyX+A!t5}X zJX!p`G#~FjR3|I2M9dU+wS0SK?&oc}d`ZvyeS2t(5B&~Uf5Q@)SKH{=Wb3**ZJ|KM zuIShXY1tRv)AN#Q?zClu2R>{B-04s(M_iM@ibOz-NF8Gl?0ib0A{-r;oyUht#Y9C1h;CdRw5HHU z_4?@LS7|@2i+Ch?8C2fg^7-?us48N5UMqTM2FQ+4}Cq@^>bmoB+>@j1>@Qy;8-Cm9S-%i$uPM**oK>m zcB-KcA2k(Q!pXzaYAfJLFs6beriRnK0{TIDuu@?b9au?)$&j>~7Y3@dW3kvixrBBP zQ_}2MS0y?{*7VMegoci;k=)V1GC#l2T-Aoel&nrU)!rY@94iY++aC{Bf1Ht8a{sa!-o-t|glv&Jbw$rWwZC@Ch?Hli)jL)_hfPf^=vOpqo7}Gzh(Ix4#JmgW$kgs)W;xrfTm8aX}j6_s~H-*X*6zF0u1=YA~S|f=HwV z!fp+9z3>dEO4gEDqg=cr!BA)oWj5BW55puwP;&uMl7Fb!Yx2*({2|;!`Y(hq2Z3If z{rmUFj^hD}GPV2*He%^T9+R4~xh7foj556qjygciX%mv-HYzX$^1)n;&^%btC}^a_ zCS!_RGiqB}xJSZ1K*z0PWEY0lO0#OHsrA1;Bg8)U^gsuQxAK4f{CS}>Vd>8@psdP6 z;7oqJ4X5?hN)yFnBQC9c;mk+Wyc5EM_TEm)@Q$CKpF@DZ6+RBC z@F)S;6+FB<5cIW!R77r5;ZF6zmk!ZWw`mae0&5r*N+(FrJ1nw#qg0>Q@@N1Mxscv6 z$|1p*pNBZp+rZ1;{h^fcqQ?LRX$~eG=(H&Z(5@^`pX9Na_@)aA8a$7^ii5VM4}_+i zN&Zp6Y2zSMVLQ*Rxbhy^CXJpQ-=`R+OeYhWQ1y6mFhq>c1YZ++&DB^H3S{d(Y^}1b zZq*eMqw;T<7)tMv3Jm5@>i7~eZp4*YxAO5eZ;@x=#emgzoPne_3#m_gu2q2fd1$@CDUe&VS6$6qYvI{rO1-4PH|;{A*txnB_1#Rg6vHj1-i|Ce$ap=z6a|}} z0rlR`@kXC@gC1U08CTr{W*t<8hK8QE=R+%)Fc(!I$-*r1>&KdB)dvS3*4EslmD$L^ z+_*58aF|B#1k!4H4*j>cQ?tvP(1{>I=~Nl|ly#_8VMvfm@erxWBzv!a%IMf=HgRA0 z-mMxV2zwFaQQ!7If@*OFj831;%}%}xr~2VP4<~gNd+k?c_4_`8l!a^jPmVs+2J;s2b_B_!4$$hjL)|Y4E~eb}&p;fl0|)j(S$G-DC}>ZT z!nGN|0N^FSKYtKtyH>+>RN)722Exn3 z&F_po8pJN+G+B}0D`Vu#YI>Ng4J)wJQY!2QFVy0T;Hn{27O40SK>BNcg3#37ucv}; zh@svXgwn)p$3bQi3Di(@z>rc;kpW|dQ`NkyouKNZZn2~A+!{_7%Y9~oL&U;kXHz{4#w-?45I9$W{){dN; z+MyVoI!pX6E285y9mQK{I`znijvtml-(79xPE&G21(7os+vo=1|{ya`;MxcFU*d)nkD07g)Mh+;l-^rSs);H8ZO! zwXo+Zj$dBV(Dpl(;TT_J&dy~EmDwklz`Jl4ycO2bM105?*dau@;pe9#4k2Ntv*t2J5Mq$OoYX*4Md$(RAeH3M<^+g=V`+L}-#5q+bm+}` zv$+l*lkDbgVBFjZVc?Tq2iBi0Qql`IU)(_E833N4AIflUa0k>xl}^RhOci5UU57Pb zp#vXX7n#ZdB2XmCfV&`OsTe6tL*cv!0px&|1rP+ItKt<9h9^>Pv=v1Qm=!^otrdRp z_@wF|r!rs>LnS5V_xK+uWFu~EtmyUY*I{gO%*Y~yY>A^@L_P%m4O&N}{H=%r;D$bo zi_bEmn^KuJ?ihYv;J9%{`tb*-4yHp76_Lil79mcUM4kf~RA5l33vViJb=r#+>5ElL z%EImh0g^IkB6MJ{kgiz5*Mgpk02?I^z}!5LFVDMPM+B3FW*AerQIH-13~(S7r+f@N zm_Yl?2vipQCN+;>x7vjE6B&MCFS{|2Nu$Z7;$@gp1S9}^gbs{)KzjYorbsUmQu2_R zw{;i{1}Qng9Qa3+MLZHEMDv@>yAMO&Z3{u=b1yv1a7AkDz(!Bv8iU4U0@=ejLJG7k zG3>}p2?W3kS$o>6lb7dVUIH4TP>8_DSezZq;7M z1h&+_m(3CXwBNlXZ_?nrL?ZnufT>L_U=nTH+7yQZ2NRhM<-6Exu3|g&ZJY6W{Z90h z51S*tFhh2$?RWv}rhl&FnV3QWISpKM@3cknZTsPk+2ovSzM?#8Qhsecd_R(H)4xCE z1tx&6WYT;29qk&oGcwCg!Mh`onGS^!?G+u*Uak7_8fofIw=#x(`S|kq9>Wjk8*6n_gm;>sdiJkQv(#jFxo?^76)jm>aIfe9DE)pK{;~x z@l1KeHBe)fL60|VqZ~FsH29TcCDxMaYfYdRG6(hfh^g6WlOkgY^cLjV4IsciTbcZ^ zqfs9eSLHAf1NbUgjTCQL?0h@S>d1mV7+zIwN;(T@hggIE_zrM^!)d?PCe-;jQXc9}FjiP*Z)%Y{X@L@l^DRnT{Rw$EO4VNH zEuxZR5Xo(>M0QlsCvz%y5?>9Z7a5}MU+#b zhu)2$q;S5c;Hb=mVmPvv6~o^{*>V%)1?@zF&ShiAF^D!1IT-?<3!|}v-w}b?h3!D! zND=lML7-r*!weuVIG{nwCSLi>96HG5Fajm)xervEU;@_?Qu$)V>VSP6IsO6Ue3jf% zodP{DWIhN4(Pe|OFewW9?L83b*{)9-ro8y`%tY~DKF&7&R!2ag1ZQR@E5ucaF|bA1 zLRHD(2tA`ETL(78g%@re9L#?t1HmhPb&6Y1@Gep$h%xhv>rED;49!&G+VC%1KG7GCeoxxW;O&)RGH4Ure z)Jmv2P-9f-u|IOygY$hM!uuXS<{Okj%SnzU$jA^I+j z(o&AWsA}7%k4R)7H=c6m+XE6phy;^leGTD5E{`+KyaEv;u=Y}+{6mPd zbB;<%Dy*s!2LUxAhj-}xnaBq|9pHk%>>QZ&D01W!vquyNB;ZMq$qR6bGV;+x$H(VA zdGchhV%*eah%6C_LY$z5Hk5L2;5e*Mo6snQRx#_LK>yDh$(2V8##X|iZUrxcJO$Ws z#FvPTa8Mx8A{=h{ZgWqwd2hq49teaHIqwa!pQq_X@c5f%OuAPDAQ}FR=Z9%40-(g& z1T!9h3j!j%6%-$hf9{Z(A7L6A9f$=89?CyNxON}C3}@kQOaKTur=EQqbQgmb$eulW z@=WV%uot?3X%ie4rsw4J0Z5Mv?qiOEMR~*cr^LZ-Bu1i#!|wQQ>I<|LZNAV zWw!s-$-7O#8~tpx$5)RWy*tHIo(of)hk%Xr1?3f!OVi4s~+Ics&@ zEK932!oGa^ZOYXhn^^TA|LzpLqAq%59v&%e{>me^$8nImqnKK*oc7B_d<9^FTUkS?q4i3~T5v z3@C%y2TJ#8Xq*mR(K!whhL+1IK4~C4Md~oHNvB`?{YaXQ8~@=8ltqkPe}6e$K;G=% zUw{7dGVuQz&;9q?zc0%FplgERnSQgd!=rT8Upz;qc6XrI+$u7{oUmXT;N!~&F;m$m zbbf)6QQzI9nve8{|M^UfuQAl({RS1k6FPU*h`5DmyIne){)*5|X_u3<|E>bYZx$)y zu`4rSL&YRcs}+C!n}OV33vn~_`~Q6wniw+E!Bs2jtsWZqH#sw%yKkQU^Rp*M#!J+g z;f=kTdv&kCtW?jNs+@N2pC67(n^e3Cd?Uv^wGbfre_!sqMS}SEQleuAYtG4k{-IL< z@(<0i9Zu`llK$^AFTgWTO?{W}PF4TsdyTX+^2h)AI}Od{|M4qUE;#k^Y+reUk1q=< z@!7A3J2fH!H~zg4#xCr~_&+p;@781**!SmX>YfTznY(#6Z(>57dp*ot(qkl^uv$Z| z8y%62e*EIgzYiNXyFM|#mrIBHnmlW*&JUakM@ZAex_-X@u8iAwWZGrNo-u64oqPXY z#RsoMj`2US>2I;wo|4j`ZYr}X?b8!M^P{gi;)}jk4y1Mcdp(;@yb4^MA`sFEU&}O` z!xsa^TwLrV-33i@bK@K+M!a|X-rnLilHYZrx?=l{Ou_$ z=}WjpA&QAf?Ne6&ia6yM{_{#JWODZht9YA*BV(FRem{@~W~e^@@^OPcIVH@&P8*Ca zvr1>_SBdv4w~l>D=`%8Kz=cdoeUpgLY>%JrKw-$A(06Yq1?SxvE;rI_9Kt>!=W&K3S87JhZV91={~E z>8hu1#l`9P41-$Sl>(=<=HQ}aKl_{=`J)G$4Yh6zg>fr}m=Nk1WBaH7G=*AY7 z_2*Jgc=;Jip_TS2ach3k(0Fb4UfqQFpD*|_aP>|@o%cBPj*m;ju>Zye+vduuTyR}V z7GIHOgk|~2VrxgmoT_qGOXShFXh(2$;LR-SW>xRJ3Elb52qr=b<)^4jnxeA*%Ca!(cAsVVXGv-&M#q-v9;YIR%8V1C&e zFZY3eet+Yyx0w@z8dGU1scih`oSlliD7D=rVCj0@TvFdQ2%nlX@BYSUM2wd_)jlbo zQm59rO;Ay~n{dp1Bui@uLwRP(Qa`XwZ7L>dug z^>Zi}>?!3?qN4KR9IA5jCdGaDymF}R@wy6I7fSmwsq9 zjNdiORJJayXncNmIqw=={O=$y^MDT+f8yYeA9`~^-rFU_-KAk z&_NOHm0Y)-?azCB>wUt8H~073Mml@%oZ#4A>mjnNpjG{3;WpP)4}H_}?>-g7mBlSd z$HleSyQ`gRI`eet;*~C$O4oVq8j86DaksH?WR!}o``$!FBCRGWkUISTm^^ zu*iGAa!MtscNvU5PlNw?!g1tJJY^OmD9yt=;9Q`PA2~_f!9Peg5UKQ@1>t zD;942d>&F;&G+>oMO>&d_q>rXe+{qYS9{(U_Fr-i8GYhE(5CzF)7LKwUk^vWF?ug= zBS6nt7@6$4o;GQ=YptNV^+`)M#j_LBN!^t8b!|W;vvITq2cDmOv=sWPe8-3~m(6hGrz?Q?f+`%-ca5^YLqRJ3u|o7z&dvlNdF1 zcdny;UJABlPnmQYX&y-L<*n%zu6y6J>jyPgrOCt6}|x(()IynHPR{l|l9PDM{Hf%;eD}=b!f!d8-;k ztqGRe*!7rWbh9z7I@v>fe3uWb&4~}N*rp5}NB&Ghf#X+JuJdZPrDBtUZ)arlP2Qbr z{C?zLA>BV+ROUz0N$4h-F+<0$-&g&XxY4PjA1exj(%vqiU7ePj97E`Q-M*&!q7ANG z<<8&KJF>RUkhVPDEmnt3eo>*l_0>Y_z_AAgmuC19-%74i+Ji1!n0Tkd7N%pX`z23= zXCfP8oJ=phNaFjFRa0NQX(f==k>F=xM$?V=mGtu7vOQ%i>gQ%Gl5g2%wLR6YhaK_6 ze17)(I-K6K|DgOGUIz2t6iQ}jVU>lt&tk}tiIVxN$Gi2e06!cZEZ{pmq>aH#8Fdg@ zz1Pcb8QJRfKyIw<<2k)`A%S$ZMl-#nc;zGAEfk7+r=r|L!PaO@SN>oJcUEG1bdwe4 zik$n-$m=!b=gFlNOhXNP^(8;@J^4a6j%?}4X@?fRl){v<%dWiQnQZlJ5MwYkvc3EJ zokb!SsXM~90=unHuIWBt^R!9jGIk;}B3+;N*380Ow|3>rFbgm(7wgzGH{gR{dko(#7FFK`LdWU&yek2wD+*Ilw7&sNB zb1Jtm;=%>d-CVgpP}_vz2OZu;w<`p>1q4E*8Y(!wycuu3zw;V?MWr#-OAj{qTptV{ zQM*Koyv1vWN+<4}9`VGzcw|4vZ?Dg7_ElR9s=h{_5jrRQOGDl^Pu~?5Y>IRphkBAS zqcp#OfXed&$B(xMX?$s^O%Z{X8hj}yj^8`&=9v<@gs+xOHo*O5Vj>f`J~!8l6(V|I zagJ1zZOYGOS5x#I&ST>1Sa$*J4rM^W+sKo9zj*q_ZL96s0L+*7Spl>$IO=^*>6Oof z^!h?6oroW=i;9xt+*uhJ zRK)MaochHgxap(c3qNA*jt~abDr+?TJvdgH)N|TZNuf+M<@L_i@L1pBz}2f3^Ho(3 zK5;nLrAr-KSbB_ad~oIDW$mvSxiPm0%eaZghR|7Q)Vx~#@LF7qf}`Vkj{z~dE$^fK zu)??Xen&@t28=&BuJAYE?uu~5KX|(nFz5yOI(HsYMda)M=Wl41e7r82aMdgJ@3{MO z!Nb5nu;MbwwEaFNp4`_h`S`KTK;#yk>+i$rUTl<|lN0_;#B0`>-;q<}6`sNIb7J+9 z(Y&o^u3xzDpt;0&Dobzcuiw9oytZx&oj=ij^}+f`>zfH$2l&tA!0^n*-#`9*P7BoA znY$-63PxEI=GU3pk937wNWB03;WS=hIu3L9zKqJ)7n3*Si@kCA-90^LHhj8gL$3|m zUHTfvc2>x$|BJhrdWp>M6rl0S*{dIaKbGc-n}zJ(e{*^<{`;-a-2MmO_T<_Bx^KLE z`SQm5bN{T6O=dYi51&57TjM;aPmCMG7BErWbryXoue%Pir--okYDtnbm^ z->Z5eA}ab7`V=r*P6~#CN11UFJFFpD5%MLTvJbv5oE=$ z!9A*AZ*Q-yqoZIDXY%^@bKT3}qCe~mPPRujHo>5uR{&DyL%}$B;PK?;5O literal 0 HcmV?d00001 diff --git a/frontend/chat-plugin/lib/src/components/chat/index.tsx b/frontend/chat-plugin/lib/src/components/chat/index.tsx index b6b7cf8e9f..59630b2302 100644 --- a/frontend/chat-plugin/lib/src/components/chat/index.tsx +++ b/frontend/chat-plugin/lib/src/components/chat/index.tsx @@ -233,7 +233,7 @@ const Chat = ({config, ...props}: Props) => { lastInGroup={lastInGroup}> , avatar: () => , }, + viber: { + text: 'Viber', + icon: () => , + avatar: () => , + }, }; const IconChannel: React.FC = ({ diff --git a/frontend/ui/src/pages/Inbox/MessageInput/index.tsx b/frontend/ui/src/pages/Inbox/MessageInput/index.tsx index 519a2976ec..b84eba67c8 100644 --- a/frontend/ui/src/pages/Inbox/MessageInput/index.tsx +++ b/frontend/ui/src/pages/Inbox/MessageInput/index.tsx @@ -306,7 +306,7 @@ const MessageInput = (props: Props) => { /> @@ -325,7 +325,7 @@ const MessageInput = (props: Props) => { /> diff --git a/frontend/ui/src/pages/Inbox/Messenger/MessageList/index.tsx b/frontend/ui/src/pages/Inbox/Messenger/MessageList/index.tsx index c355ebbde4..69b6b8b1c4 100644 --- a/frontend/ui/src/pages/Inbox/Messenger/MessageList/index.tsx +++ b/frontend/ui/src/pages/Inbox/Messenger/MessageList/index.tsx @@ -153,41 +153,40 @@ const MessageList = (props: MessageListProps) => { return (

- {messages && - messages.map((message: Message, index: number) => { - const prevMessage = messages[index - 1]; - const nextMessage = messages[index + 1]; - - const lastInGroup = nextMessage ? message.fromContact !== nextMessage.fromContact : true; - - const sentAt = lastInGroup ? formatTime(message.sentAt) : null; - - const messageDecoration = hasSuggestions(message) ? ( - - ) : null; - - return ( -
- {hasDateChanged(prevMessage, message) && ( -
- {formatDateOfMessage(message)} -
- )} - - - - -
- ); - })} + {messages?.map((message: Message, index: number) => { + const prevMessage = messages[index - 1]; + const nextMessage = messages[index + 1]; + + const lastInGroup = nextMessage ? message.fromContact !== nextMessage.fromContact : true; + + const sentAt = lastInGroup ? formatTime(message.sentAt) : null; + + const messageDecoration = hasSuggestions(message) ? ( + + ) : null; + + return ( +
+ {hasDateChanged(prevMessage, message) && ( +
+ {formatDateOfMessage(message)} +
+ )} + + + + +
+ ); + })}
); }; diff --git a/frontend/ui/src/pages/Inbox/SuggestedReplySelector/index.tsx b/frontend/ui/src/pages/Inbox/SuggestedReplySelector/index.tsx index e84c44ebd4..abb2e92cc2 100644 --- a/frontend/ui/src/pages/Inbox/SuggestedReplySelector/index.tsx +++ b/frontend/ui/src/pages/Inbox/SuggestedReplySelector/index.tsx @@ -48,7 +48,7 @@ const SuggestedReplySelector = ({onClose, suggestions, selectSuggestedReply, sou }}>
diff --git a/frontend/ui/src/pages/Inbox/TemplateSelector/index.tsx b/frontend/ui/src/pages/Inbox/TemplateSelector/index.tsx index 20671a5dd2..253135b734 100644 --- a/frontend/ui/src/pages/Inbox/TemplateSelector/index.tsx +++ b/frontend/ui/src/pages/Inbox/TemplateSelector/index.tsx @@ -121,7 +121,7 @@ const TemplateSelector = ({listTemplates, onClose, templates, selectTemplate, so selectTemplate(template); }}>
{template.name}
- +
); })} diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/Chart.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/Chart.yaml new file mode 100644 index 0000000000..b719648ddb --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for the Viber source +name: viber +version: 1.0 diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/deployments.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/deployments.yaml new file mode 100644 index 0000000000..13769df325 --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/deployments.yaml @@ -0,0 +1,173 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sources-viber-connector + namespace: {{ .Values.global.kubernetes.namespace }} + labels: + app: sources-viber-connector + 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: sources-viber-connector + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: sources-viber-connector + spec: + containers: + - name: app + image: "{{ .Values.global.kubernetes.containerRegistry}}/{{ .Values.imageConnector }}:{{ .Values.global.kubernetes.appImageTag }}" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + - configMapRef: + name: hostnames + env: + - 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 + - name: authToken + valueFrom: + configMapKeyRef: + name: "{{ .Values.component }}" + key: authToken + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 60 + 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 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sources-viber-events-router + namespace: {{ .Values.global.kubernetes.namespace }} + labels: + app: sources-viber-events-router + type: sources + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: "{{ .Values.component }}" + annotations: + core.airy.co/config-items-mandatory: "VIBER_AUTH_TOKEN VIBER_ACCOUNT_SID" +spec: + replicas: 0 + selector: + matchLabels: + app: sources-viber-events-router + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: sources-viber-events-router + spec: + containers: + - name: app + image: "{{ .Values.global.kubernetes.containerRegistry}}/{{ .Values.imageEventsRouter }}:{{ .Values.global.kubernetes.appImageTag }}" + imagePullPolicy: Always + env: + - 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 + - name: VIBER_AUTH_TOKEN + valueFrom: + configMapKeyRef: + name: "{{ .Values.component }}" + key: authToken + - name: VIBER_ACCOUNT_SID + valueFrom: + configMapKeyRef: + name: "{{ .Values.component }}" + key: accountSid + livenessProbe: + tcpSocket: + port: 6000 + 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/sources/charts/viber/templates/service.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/service.yaml new file mode 100644 index 0000000000..e09bbe1969 --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: sources-viber-connector + 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: sources-viber-connector diff --git a/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/values.yaml b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/values.yaml new file mode 100644 index 0000000000..dee7d9e75c --- /dev/null +++ b/infrastructure/helm-chart/charts/core/charts/components/charts/sources/charts/viber/values.yaml @@ -0,0 +1,4 @@ +component: sources-viber +mandatory: false +imageConnector: sources/viber-connector +imageEventsRouter: sources/viber-events-router 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 96cab10dec..4ec28cad20 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 @@ -51,3 +51,5 @@ data: kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}source.twilio.events" + kafka-topics.sh --create --if-not-exists --zookeeper "${ZOOKEEPER}" --replication-factor "${REPLICAS}" --partitions "${PARTITIONS}" --topic "${AIRY_CORE_NAMESPACE}source.viber.events" + diff --git a/infrastructure/helm-chart/charts/core/templates/ingress.yaml b/infrastructure/helm-chart/charts/core/templates/ingress.yaml index dc465751a5..6d59a4a9e4 100644 --- a/infrastructure/helm-chart/charts/core/templates/ingress.yaml +++ b/infrastructure/helm-chart/charts/core/templates/ingress.yaml @@ -289,6 +289,20 @@ spec: name: sources-facebook-connector port: number: 80 + - path: /channels.viber.connect + pathType: Prefix + backend: + service: + name: sources-viber-connector + port: + number: 80 + - path: /channels.viber.disconnect + pathType: Prefix + backend: + service: + name: sources-viber-connector + port: + number: 80 - path: /channels.facebook.explore pathType: Prefix backend: diff --git a/infrastructure/helm-chart/charts/core/templates/ngrok.yaml b/infrastructure/helm-chart/charts/core/templates/ngrok.yaml index c604607921..a6305f72cf 100644 --- a/infrastructure/helm-chart/charts/core/templates/ngrok.yaml +++ b/infrastructure/helm-chart/charts/core/templates/ngrok.yaml @@ -46,6 +46,13 @@ spec: name: sources-twilio-connector port: number: 80 + - path: /viber + pathType: Prefix + backend: + service: + name: sources-viber-connector + port: + number: 80 --- apiVersion: apps/v1 kind: Deployment diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceFacebook.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceFacebook.java deleted file mode 100644 index ee7730b6e3..0000000000 --- a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceFacebook.java +++ /dev/null @@ -1,13 +0,0 @@ -package co.airy.kafka.schema; - -public abstract class SourceFacebook extends AbstractTopic { - @Override - public String kind() { - return "source"; - } - - @Override - public String domain() { - return "facebook"; - } -} diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceGoogle.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceGoogle.java deleted file mode 100644 index f6d487a00b..0000000000 --- a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/SourceGoogle.java +++ /dev/null @@ -1,13 +0,0 @@ -package co.airy.kafka.schema; - -public abstract class SourceGoogle extends AbstractTopic { - @Override - public String kind() { - return "source"; - } - - @Override - public String domain() { - return "google"; - } -} diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceFacebookEvents.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceFacebookEvents.java index ef6cfb44f9..b0a900c4b3 100644 --- a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceFacebookEvents.java +++ b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceFacebookEvents.java @@ -1,8 +1,16 @@ package co.airy.kafka.schema.source; -import co.airy.kafka.schema.SourceFacebook; +import co.airy.kafka.schema.AbstractTopic; -public class SourceFacebookEvents extends SourceFacebook { +public class SourceFacebookEvents extends AbstractTopic { + @Override + public String kind() { + return "source"; + } + @Override + public String domain() { + return "facebook"; + } @Override public String dataset() { return "events"; diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceGoogleEvents.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceGoogleEvents.java index af9a4bf89d..bd48a06ff8 100644 --- a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceGoogleEvents.java +++ b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceGoogleEvents.java @@ -1,8 +1,16 @@ package co.airy.kafka.schema.source; -import co.airy.kafka.schema.SourceGoogle; +import co.airy.kafka.schema.AbstractTopic; -public class SourceGoogleEvents extends SourceGoogle { +public class SourceGoogleEvents extends AbstractTopic { + @Override + public String kind() { + return "source"; + } + @Override + public String domain() { + return "google"; + } @Override public String dataset() { return "events"; diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceTwilioEvents.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceTwilioEvents.java index 2384b7ad43..29d097f52e 100644 --- a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceTwilioEvents.java +++ b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceTwilioEvents.java @@ -1,8 +1,16 @@ package co.airy.kafka.schema.source; -import co.airy.kafka.schema.SourceTwilio; +import co.airy.kafka.schema.AbstractTopic; -public class SourceTwilioEvents extends SourceTwilio { +public class SourceTwilioEvents extends AbstractTopic { + @Override + public String kind() { + return "source"; + } + @Override + public String domain() { + return "twilio"; + } @Override public String dataset() { return "events"; diff --git a/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceViberEvents.java b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceViberEvents.java new file mode 100644 index 0000000000..943e22d941 --- /dev/null +++ b/lib/java/kafka/schema/src/main/java/co/airy/kafka/schema/source/SourceViberEvents.java @@ -0,0 +1,18 @@ +package co.airy.kafka.schema.source; + +import co.airy.kafka.schema.AbstractTopic; + +public class SourceViberEvents extends AbstractTopic { + @Override + public String kind() { + return "source"; + } + @Override + public String domain() { + return "viber"; + } + @Override + public String dataset() { + return "events"; + } +} diff --git a/lib/java/kafka/test/src/main/java/co/airy/kafka/test/KafkaTestHelper.java b/lib/java/kafka/test/src/main/java/co/airy/kafka/test/KafkaTestHelper.java index 810d1c5b61..f80f4e099b 100644 --- a/lib/java/kafka/test/src/main/java/co/airy/kafka/test/KafkaTestHelper.java +++ b/lib/java/kafka/test/src/main/java/co/airy/kafka/test/KafkaTestHelper.java @@ -131,5 +131,4 @@ public void produceRecords(List> records) throws Exe produceRecord(record); } } - } diff --git a/lib/typescript/assets/images/icons/viber.svg b/lib/typescript/assets/images/icons/viber.svg new file mode 100644 index 0000000000..b776d0bd90 --- /dev/null +++ b/lib/typescript/assets/images/icons/viber.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/lib/typescript/render/SourceMessage.tsx b/lib/typescript/render/SourceMessage.tsx index 6b356e2b4b..b2ab1173bd 100644 --- a/lib/typescript/render/SourceMessage.tsx +++ b/lib/typescript/render/SourceMessage.tsx @@ -23,7 +23,7 @@ export class SourceMessage extends React.Component; + return ; } render() { diff --git a/lib/typescript/render/outbound/index.ts b/lib/typescript/render/outbound/index.ts index df93b0f13c..944283e65f 100644 --- a/lib/typescript/render/outbound/index.ts +++ b/lib/typescript/render/outbound/index.ts @@ -2,6 +2,7 @@ import {FacebookMapper} from './facebook'; import {ChatpluginMapper} from './chatplugin'; import {GoogleMapper} from './google'; import {TwilioMapper} from './twilio'; +import {ViberMapper} from './viber'; export const getOutboundMapper = (source: string) => { switch (source) { @@ -15,6 +16,8 @@ export const getOutboundMapper = (source: string) => { case 'twilio.sms': case 'twilio.whatsapp': return new TwilioMapper(); + case 'viber': + return new ViberMapper(); default: { console.error('Unknown source ', source); } diff --git a/lib/typescript/render/outbound/viber.ts b/lib/typescript/render/outbound/viber.ts new file mode 100644 index 0000000000..1187e60beb --- /dev/null +++ b/lib/typescript/render/outbound/viber.ts @@ -0,0 +1,14 @@ +import {OutboundMapper} from './mapper'; + +export class ViberMapper extends OutboundMapper { + getTextPayload(text: string): any { + return { + text: text, + type: 'text', + }; + } + + isTextSupported(): boolean { + return true; + } +} diff --git a/lib/typescript/render/props.ts b/lib/typescript/render/props.ts index deb1482166..2802a92f6a 100644 --- a/lib/typescript/render/props.ts +++ b/lib/typescript/render/props.ts @@ -23,7 +23,10 @@ export type CommandUnion = SuggestedReplyCommand | QuickReplyCommand; interface RenderProps { contentType: 'message' | 'template' | 'suggestedReplies' | 'quickReplies'; - content: Content; + message: { + content: Content; + fromContact?: boolean; + }; source: string; } diff --git a/lib/typescript/render/providers/chatplugin/ChatPluginRender.tsx b/lib/typescript/render/providers/chatplugin/ChatPluginRender.tsx index fb8dfb31f9..7dbcc883cc 100644 --- a/lib/typescript/render/providers/chatplugin/ChatPluginRender.tsx +++ b/lib/typescript/render/providers/chatplugin/ChatPluginRender.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {RenderPropsUnion} from '../../props'; import {AttachmentUnion, ContentUnion, SimpleAttachment} from './chatPluginModel'; -import {Message} from 'model'; import {Text} from '../../components/Text'; import {RichText} from './components/RichText'; import {RichCard} from './components/RichCard'; @@ -10,12 +9,12 @@ import {RichCardCarousel} from './components/RichCardCarousel'; import {QuickReplies} from './components/QuickReplies'; export const ChatPluginRender = (props: RenderPropsUnion) => { - return render(mapContent(props.content), props); + return render(mapContent(props.message), props); }; function render(content: ContentUnion, props: RenderPropsUnion) { const defaultProps = { - fromContact: props.content.fromContact || false, + fromContact: props.message.fromContact || false, commandCallback: 'commandCallback' in props ? props.commandCallback : null, }; const invertedProps = {...defaultProps, fromContact: !defaultProps.fromContact}; @@ -30,7 +29,6 @@ function render(content: ContentUnion, props: RenderPropsUnion) { return ( { - const {message, text, fromContact} = props; + const {text, fromContact} = props; return ( -
+
{text} diff --git a/lib/typescript/render/providers/facebook/FacebookRender.tsx b/lib/typescript/render/providers/facebook/FacebookRender.tsx index 7238edbfae..530f349cb4 100644 --- a/lib/typescript/render/providers/facebook/FacebookRender.tsx +++ b/lib/typescript/render/providers/facebook/FacebookRender.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {Message} from 'model'; import {RenderPropsUnion} from '../../props'; import {Text} from '../../components/Text'; import {Image} from '../../components/Image'; @@ -19,7 +18,7 @@ import {MediaTemplate} from './components/MediaTemplate'; import {FallbackAttachment} from './components/FallbackAttachment'; export const FacebookRender = (props: RenderPropsUnion) => { - const message: Message = props.content; + const message = props.message; const content = message.fromContact ? facebookInbound(message) : facebookOutbound(message); return render(content, props); }; @@ -27,60 +26,36 @@ export const FacebookRender = (props: RenderPropsUnion) => { function render(content: ContentUnion, props: RenderPropsUnion) { switch (content.type) { case 'text': - return ; + return ; case 'fallback': - return ( - <> - - - ); + return ; case 'postback': - return ; + return ; case 'image': - return ( - <> - - - ); + return ; case 'images': - return ( - <> - - - ); + return ; case 'video': - return ( - <> -