diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..cac54165 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,65 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. + +# Common formats +*.html text +*.xml text +*.css text +*.scss text +*.js text +*.properties text +*.rtf text +*.yaml text +*.yml text +*.md text +*.json text + +LICENSE text + +# SQL scripts +*.sql text + +# Java sources +*.java text + +# Kotlin sources +*.kt text +*.kts text + +# Python sources +*.py text + +# Gradle build files +*.gradle text + +# Google protocol buffers +*.proto text + +# Miscellaneous +*.rb text +*.http text + +# Declare files that will always have CRLF line endings on checkout. +*.bat text eol=crlf + +# Declare files that will always have LF line endings on checkout. +*.sh text eol=lf +gradlew text eol=lf +pull text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.swf binary +*.jar binary +*.desc binary + +*.scpt binary +*.scssc binary + +# Encoded files +*.enc binary diff --git a/.gitignore b/.gitignore index 983275cd..2707e789 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,29 @@ +# +# Copyright 2020, TeamDev. All rights reserved. +# +# Redistribution and use in source and/or binary forms, with or without +# modification, must retain the above copyright notice and the following +# disclaimer. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + Thumbs.db .DS_Store .gradle -build/ +/build/ +/buildSrc/build/ +/google-chat-bot/build/ target/ out/ .idea @@ -219,3 +241,13 @@ gradle-app.setting # .nfs files are created when an open file is removed but is still being accessed .nfs* +# Local gradle properties that overrides default gradle.properties and are private +gradle-local.properties + +**/generated/** + +# Local credentials folder +.credentials + +# IDEA HTTP client private env files +http-client.private.env.json diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..7cd03408 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,52 @@ + + + + + + diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..92dbb2b9 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,75 @@ + + + + diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..0f7bc519 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + diff --git a/.idea/copyright/TeamDev_Open_Source.xml b/.idea/copyright/TeamDev_Open_Source.xml new file mode 100644 index 00000000..10ab8291 --- /dev/null +++ b/.idea/copyright/TeamDev_Open_Source.xml @@ -0,0 +1,6 @@ + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..0b8f9a19 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/dictionaries/common.xml b/.idea/dictionaries/common.xml new file mode 100644 index 00000000..c6d06b80 --- /dev/null +++ b/.idea/dictionaries/common.xml @@ -0,0 +1,65 @@ + + + + afghani + arraybuffer + aspx + bytebuffer + closeables + cqrs + dartdocs + dataset + datastore + datastores + deserialized + dirham + enrichable + enrichments + escaper + flushables + googleapis + gradle + grpc + handshaker + hohpe + idempotency + lempira + liskov + melnik + memoized + memoizes + memoizing + mergeable + mikhaylov + millisecs + multitenancy + multitenant + nullable + onclose + oneof + onmessage + onopen + parameterizing + plugable + processmanager + procman + proto's + protos + sfixed + stderr + stringifier + stringifiers + switchman + testutil + threeten + tuples + unregister + unregistering + unregisters + unregistration + websocket + workflows + yevsyukov + + + diff --git a/.idea/filetypes/Google Protobuf.xml b/.idea/filetypes/Google Protobuf.xml new file mode 100644 index 00000000..270fe82c --- /dev/null +++ b/.idea/filetypes/Google Protobuf.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..bf686044 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,860 @@ + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..c2b0bbbe --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..5ace414d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md new file mode 100644 index 00000000..21b01fe9 --- /dev/null +++ b/ENVIRONMENT.md @@ -0,0 +1,201 @@ +Cloud Environment setup +------------ + +The ChatBot application is working in the cloud environment on the Google Cloud Platform (GCP) and +this document provides an overview of the currently configured environment. + +# Cloud Run + +The [Cloud Run][cloud-run] is used as the main compute platform for the ChatBot application. +Cloud Run is a managed serverless solution that works with Docker images and is able to scale +upon the load when needed. + +The `chat-bot-server` service is configured in Cloud Run with the +`gcr.io//chat-bot-server` container image. The image is built automatically using +[`jib`][jib] Gradle plugin and deployed to the [Container Registry][container-registry] as part +of the build process. + +Upon pushes to the `master` branch, the Cloud Build performs the automatic deployment of the +new Cloud Run revision (see [Cloud Build](#cloud-build) section for details). + +[cloud-run]: https://cloud.google.com/run +[jib]: https://github.com/GoogleContainerTools/jib +[container-registry]: https://cloud.google.com/container-registry + +# Cloud Build + +The [Cloud Build][cloud-build] CI/CD solution is used to continuously build and deploy +the application. + +The Cloud Build configuration is available as [`cloudbuild.yaml`](./cloudbuild.yaml) and does +the following: + +1. Starts Gradle build for the project. +2. Deploys a new revision of the Cloud Run service. + +In addition to the configuration, the Cloud Build trigger is configured to automatically start build +and deploy process upon commits to the `master` branch. In order to allow Cloud Build to +fetch code from the GitHub, the Cloud Build GitHub [application][cloud-build-github-app] +is [configured][run-builds-on-github] for the organization. + +The Cloud Build itself uses GCP service accounts in order to access the APIs and should be +configured to allow the Cloud Run deployment (see the [IAM](#iam) section for details). + +[cloud-build]: https://cloud.google.com/cloud-build +[cloud-build-trigger]: https://cloud.google.com/cloud-build/docs/automating-builds/create-manage-triggers#console +[cloud-build-github-app]: https://github.com/marketplace/google-cloud-build +[run-builds-on-github]: https://cloud.google.com/cloud-build/docs/automating-builds/run-builds-on-github + +# Hangouts Chat API + +The bot uses [Hangout Chat API][chat-api] and is linked to the GCP project. It is only possible +to have a single bot per GCP project. + +The bot configuration is only available via the web UI console. The bot is published in accordance +to the publishing [guide][publishing-guide] where the essential configurations are: + +1. `Functionality` — `Bot works in rooms` is checked. The bot is not expected to work + in direct messages. +2. `Connection settings` — [`Cloud Pub/Sub`][pubsub-bot] is choosen and a Pub/Sub topic name + that is used to deliver messages from users to the bot is configured (for the Pub/Sub details + see [Pub/Sub](#pubsub) section). +3. `Permissions` — a list of individuals in the domain who are able to install the bot + is configured in the `Specific people and groups in your domain` field. + +[chat-api]: https://developers.google.com/hangouts/chat +[publishing-guide]: https://developers.google.com/hangouts/chat/how-tos/bots-publish +[pubsub-bot]: https://developers.google.com/hangouts/chat/how-tos/pub-sub + +# Pub/Sub + +The application is built with resilience in mind and even though it exposes some REST APIs, +it is not intended to handle to load directly due to the security and performance considerations. +Instead, it relies on the Google [Pub/Sub][pubsub] async messaging service to receive +the incoming messages and then stream them into the app. The Pub/Sub uses dedicated IAM +configuration and is able to handle ultimate load at any pace. + +The bot requires the following Pub/Sub topics to be configured: + +1. `incoming-bot-messages` — the topic that is used in the `Connection settings` of the + bot configuration. The Hangouts Chat system takes care of propagating user messages to this + topic. + + For the topic the `incoming-bot-messages-cloud-run` [push subscription][push-subscription] + is created with a backoff retry policy and the acknowledgement deadline of 600 seconds. + The subscription delivers messages to the `/chat/incoming/event` endpoint of + the Cloud Run [service](#cloud-run). + + Also, the `dead lettering` is configured for the subscription, so all the undelivered + messages are sent to the `dead-incoming-bot-messages` topic. + + The subscription uses `cloud-run-pubsub-invoker` service account to implement service2service + authentication (see [IAM](#iam) section for details). + +2. `dead-incoming-bot-messages` — the topic that holds undelivered incoming messages. + + For the topic, the `dead-incoming-bot-messages` [pull subscription][pull-subscription] + that never expires is configured. + In case of an undelivered message, it can be pulled from the subscription for the further + analysis and investigation. + +3. `repository-checks` — the topic that delivers scheduled tasks to check the build state of + the watched resources (see [Cloud Scheduler](#cloud-scheduler) section for details). + + The `repository-checks-cloud-run` subscription is configured for the topic. The subscription + delivers messages to the `/repositories/builds/check` endpoint of the Cloud Run + [service](#cloud-run). + The subscription uses the same `cloud-run-pubsub-invoker` service account as the + `incoming-bot-messages-cloud-run` (see [IAM](#iam) section for details). + +[pubsub]: https://cloud.google.com/pubsub +[push-subscription]: https://cloud.google.com/pubsub/docs/push +[pull-subscription]: https://cloud.google.com/pubsub/docs/pull + +# Cloud Scheduler + +The [Cloud Scheduler][scheduler] service allows configuring multiple scheduled tasks that deliver +the payload to a particular target (HTTP endpoint, Pub/Sub topic or AppEngine endpoint). + +The CRON task `repositories-check-trigger` is configured for the bot. The task emits a Pub/Sub +message with an empty payload to the `repository-checks` topic and is +[configured][configure-schedules] to run every hour using the following unix-cron format +expression: `0 * * * *`. + +[scheduler]: https://cloud.google.com/scheduler +[configure-schedules]: https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules + +# Secret Manager + +The [Secret Manager][secret-manager] service is used to supply application secrets like API tokens +and service accounts securely. + +The secrets are [managed][managing-secrets] in the Secret Manager Web UI, but in order to be able +to [read][reading-secrets] the secrets, developers and service accounts should have the +`roles/secretmanager.viewer` role that is not configured by default (see [IAM](#iam) section +for details). + +The following secrets are configured for the bot: + +1. `ChatServiceAccount` — the private key of the chatbot actor that is used by the + [Hangouts Chat API](#hangouts-chat-api). + + The key is stored in JSON format as string value. + +2. `TravisApiToken` — the Travis CI API token. + + The API token is used to authenticate calls to the Travis CI v3 API. + +[secret-manager]: https://cloud.google.com/secret-manager +[managing-secrets]: https://cloud.google.com/secret-manager/docs/managing-secrets +[reading-secrets]: https://cloud.google.com/secret-manager/docs/managing-secret-versions#get + +# IAM + +The [Cloud Identity and Access Management][iam] (IAM) service is used to fine-tune the authorization +and access management for the application. + +In order to run the application, the following service accounts, and their respective roles +are configured: + +1. `chat-api-push@system.gserviceaccount.com` — a special Chat API service account used by the + Chat to publish messages to the Pub/Sub topic. + + The [`Pub/Sub Publisher`][publisher-role] role [must][grant-publish-rights] be assigned + to the service account in order to grant the Chat permission to publish user messages + to the defined topic. + +2. `spine-chat-bot-actor@.iam.gserviceaccount.com` — a custom service account that is + used as the credentials for the Chat API. + + The Hangouts Chat API works only with dedicated service account keys and + [could not][chat-api-with-default-sa] be used with default credentials. The API itself does + not require any specific AIM role, but during the authentication the + [`chat.bot`][applying-chatbot-credentials] scope should be set. + +3. `-compute@developer.gserviceaccount.com` — default service account used by Cloud Run. + + It is not required to set a custom service account for the Cloud Run, but in order to use + the [Secret Manager](#secret-manager) API the service account should have the + `Secret Manager Viewer` role applied. + +4. `@cloudbuild.gserviceaccount.com` — [Cloud Build](#cloud-build) service account + used by the Cloud Build service to build and deploy the application. + + The Cloud Build service account is configured to act as a `Service Account User` and should + have the `Cloud Run Admin` role in order to be able to [deploy][cloud-build-deploy-cloud-run] + the application. + +5. `cloud-run-pubsub-invoker@.iam.gserviceaccount.com` — a custom service account + used to call the Cloud Run service from the [Pub/Sub](#pubsub) subscriptions. + + The Cloud Run is not accepting unauthenticated calls by default and is not exposed + to the internet. In order to be able to call the service, the caller + [must][cloud-run-service-to-service-auth] have the `Cloud Run Invoker` role. + +[iam]: https://cloud.google.com/iam +[grant-publish-rights]: https://developers.google.com/hangouts/chat/how-tos/pub-sub#grant_publish_rights_on_your_topic +[publisher-role]: https://cloud.google.com/pubsub/docs/access-control#roles +[chat-api-with-default-sa]: https://stackoverflow.com/questions/62571412/hangout-chat-api-authentication-fails-with-default-service-account +[applying-chatbot-credentials]: https://developers.google.com/hangouts/chat/how-tos/service-accounts#step_2_applying_credentials_to_http_request_headers +[cloud-build-deploy-cloud-run]: https://cloud.google.com/cloud-build/docs/deploying-builds/deploy-cloud-run +[cloud-run-service-to-service-auth]: https://cloud.google.com/run/docs/authenticating/service-to-service diff --git a/README.md b/README.md index 7e4a7f72..09893d31 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,71 @@ -Micronaut-based Google Chat-Bot +Spine ChatBot ------- +The ChatBot application is a Spine-based Google Chat [bot][chatbot-concepts] that +monitors the build statuses of Spine repositories and notifies the developers +via the [Google Chat][google-chat]. + +[chatbot-concepts]: https://developers.google.com/hangouts/chat/concepts/bots +[google-chat]: https://chat.google.com/ + +# Prerequisites + +* [JDK 11][jdk11] or newer. +* [Docker SE][docker] v19.03 or newer. + +[docker]: https://docs.docker.com/get-docker/ +[jdk11]: https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html + +# Build + +In order to build the application, run: + +```bash +./gradlew clean build +``` + +Also, it is possible to build a Docker image using [`jib`][jib]: + +```bash +./gradlew clean build jib +``` + +[jib]: https://github.com/GoogleContainerTools/jib + +# Running locally + +It is possible to run the application as a docker image locally. In order to do that, please make +sure you have saved the appropriate GCP credentials at `.credentials/gcp-adc.json`. + +Then run the following script from the repository root folder: + +```bash +export APP_PORT=8080 +export LOCAL_PORT=9090 +export CONTAINER_CREDENTIALS_PATH="/tmp/keys/gcp-adc.json" +export LOCAL_CREDENTIALS_PATH="${PWD}/.credentials/gcp-adc.json" +export GCP_PROJECT_ID="spine-chat-bot" +docker run \ + --tty \ + --rm \ + -p "${LOCAL_PORT}:${APP_PORT}" \ + -e "PORT=${APP_PORT}" \ + -e "MICRONAUT_SERVER_PORT=${APP_PORT}" \ + -e "GOOGLE_APPLICATION_CREDENTIALS=${CONTAINER_CREDENTIALS_PATH}" \ + -e "GCP_PROJECT_ID=${GCP_PROJECT_ID}" \ + -v "${LOCAL_CREDENTIALS_PATH}:${CONTAINER_CREDENTIALS_PATH}" \ + gcr.io/${GCP_PROJECT_ID}/chat-bot-server +``` + +The application will be available at `127.0.0.1:${LOCAL_PORT}` (e.g. `127.0.0.1:9090`). +Locally-supplied GCP credentials are mounted into the image directly. + +For detailed Application Default Credentials (ADC) guide for Docker see example +Cloud Run [guide][cloud-run-local-guide]. + +[cloud-run-local-guide]: https://cloud.google.com/run/docs/testing/local#running_locally_using_docker_with_access_to_services + +# Running in the Cloud + +The application is deployed in the Google Cloud Platform cloud, and the overview of the +cloud deployment is available in a separate [document](ENVIRONMENT.md). diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b839bdfb..00000000 --- a/build.gradle +++ /dev/null @@ -1,83 +0,0 @@ -plugins { - id "net.ltgt.apt-eclipse" version "0.21" - id "com.github.johnrengelman.shadow" version "5.2.0" - id "application" - id "com.google.cloud.tools.jib" version "2.3.0" -} - -version "0.1" -group "io.spine" - - -repositories { - jcenter() - mavenCentral() -} - -configurations { - // for dependencies that are needed for development only - developmentOnly - invoker -} - -dependencies { - annotationProcessor(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) - annotationProcessor("io.micronaut:micronaut-inject-java") - annotationProcessor("io.micronaut:micronaut-validation") - - compileOnly(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) - - implementation(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) - implementation("io.micronaut:micronaut-inject") - implementation("io.micronaut:micronaut-validation") - implementation("io.micronaut:micronaut-runtime") - - implementation "io.micronaut:micronaut-http-server-netty" - implementation("javax.annotation:javax.annotation-api") - - implementation("org.apache.logging.log4j:log4j-core:2.13.3") - runtimeOnly("org.apache.logging.log4j:log4j-api:2.13.3") - runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:2.13.3") - - testAnnotationProcessor(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) - testAnnotationProcessor("io.micronaut:micronaut-inject-java") - testImplementation(enforcedPlatform("io.micronaut:micronaut-bom:$micronautVersion")) - testImplementation("io.micronaut:micronaut-http-client") - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation("io.micronaut.test:micronaut-test-junit5") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") -} - -test.classpath += configurations.developmentOnly - -mainClassName = "io.spine.chatbot.Application" - -test { - useJUnitPlatform() -} - -java { - sourceCompatibility = JavaVersion.toVersion('11') - targetCompatibility = JavaVersion.toVersion('11') -} - -tasks.withType(JavaCompile) { - options.encoding = "UTF-8" - options.compilerArgs.add('-parameters') -} - -tasks.withType(JavaExec) { - classpath += configurations.developmentOnly - jvmArgs('-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote') -} - -shadowJar { - minimize() - mergeServiceFiles() -} - -jib { - to { - image = 'gcr.io/chat-bot/jib-image' - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..33c40115 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import net.saliman.gradle.plugin.properties.PropertiesPlugin + +plugins { + idea +} + +apply(from = "version.gradle.kts") +val botVersion: String by extra + +allprojects { + apply() + apply() + + group = "io.spine" + version = botVersion +} + +subprojects { + apply() +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..c07f4329 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +plugins { + `kotlin-dsl` +} + +repositories { + mavenLocal() + jcenter() + gradlePluginPortal() +} + +dependencies { + implementation("net.ltgt.gradle:gradle-errorprone-plugin:1.2.1") + implementation("net.ltgt.gradle:gradle-apt-plugin:0.21") + implementation("com.github.jengelman.gradle.plugins:shadow:6.0.0") + implementation("gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:2.4.0") + implementation("io.spine.tools:spine-bootstrap:1.5.24") + implementation("net.saliman:gradle-properties-plugin:1.5.1") +} + +kotlinDslPluginOptions { + experimentalWarning.set(false) +} diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt new file mode 100644 index 00000000..bcceae7b --- /dev/null +++ b/buildSrc/src/main/kotlin/deps.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +object Versions { + const val checkerFramework = "3.4.1" + const val errorProne = "2.4.0" + const val pmd = "6.24.0" + const val checkstyle = "8.33" + const val findBugs = "3.0.2" + const val guava = "29.0-jre" + const val flogger = "0.5.1" + const val junit5 = "5.6.2" + const val truth = "1.0.1" + const val micronaut = "2.0.0" + const val spineGcloud = "1.5.22" + const val googleSecretManager = "1.1.0" + const val googleChat = "v1-rev20200617-1.30.9" + const val googleAuth = "0.20.0" + const val log4j2 = "2.13.3" +} + +object Build { + val errorProneAnnotations = listOf( + "com.google.errorprone:error_prone_annotations:${Versions.errorProne}", + "com.google.errorprone:error_prone_type_annotations:${Versions.errorProne}" + ) + const val errorProneCheckApi = "com.google.errorprone:error_prone_check_api:${Versions.errorProne}" + const val errorProneCore = "com.google.errorprone:error_prone_core:${Versions.errorProne}" + const val errorProneTestHelpers = "com.google.errorprone:error_prone_test_helpers:${Versions.errorProne}" + const val checkerAnnotations = "org.checkerframework:checker-qual:${Versions.checkerFramework}" + val checkerDataflow = listOf( + "org.checkerframework:dataflow:${Versions.checkerFramework}", + "org.checkerframework:javacutil:${Versions.checkerFramework}" + ) + const val jsr305Annotations = "com.google.code.findbugs:jsr305:${Versions.findBugs}" + const val guava = "com.google.guava:guava:${Versions.guava}" + const val flogger = "com.google.flogger:flogger:${Versions.flogger}" + val ci = "true".equals(System.getenv("CI")) + val micronaut = Micronaut + val google = Google + val log4j2 = Log4j2 + var spine = Spine +} + +object Spine { + const val datastore = "io.spine.gcloud:spine-datastore:${Versions.spineGcloud}" + const val pubsub = "io.spine.gcloud:spine-pubsub:${Versions.spineGcloud}" +} + +object Log4j2 { + const val core = "org.apache.logging.log4j:log4j-core:${Versions.log4j2}" + const val api = "org.apache.logging.log4j:log4j-api:${Versions.log4j2}" + const val slf4jBridge = "org.apache.logging.log4j:log4j-slf4j-impl:${Versions.log4j2}" +} + +object Google { + const val secretManager = "com.google.cloud:google-cloud-secretmanager:${Versions.googleSecretManager}" + const val chat = "com.google.apis:google-api-services-chat:${Versions.googleChat}" + const val auth = "com.google.auth:google-auth-library-oauth2-http:${Versions.googleAuth}" +} + +object Micronaut { + const val bom = "io.micronaut:micronaut-bom:${Versions.micronaut}" + const val inject = "io.micronaut:micronaut-inject" + const val injectJava = "io.micronaut:micronaut-inject-java" + const val validation = "io.micronaut:micronaut-validation" + const val runtime = "io.micronaut:micronaut-runtime" + const val netty = "io.micronaut:micronaut-http-server-netty" + const val testJUnit5 = "io.micronaut.test:micronaut-test-junit5" + const val httpClient = "io.micronaut:micronaut-http-client" + const val annotationApi = "javax.annotation:javax.annotation-api" +} + +object Test { + val junit5Api = listOf( + "org.junit.jupiter:junit-jupiter-api:${Versions.junit5}", + "org.junit.jupiter:junit-jupiter-params:${Versions.junit5}" + ) + const val junit5Runner = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit5}" + const val guavaTestlib = "com.google.guava:guava-testlib:${Versions.guava}" + val truth = listOf( + "com.google.truth:truth:${Versions.truth}", + "com.google.truth.extensions:truth-java8-extension:${Versions.truth}", + "com.google.truth.extensions:truth-proto-extension:${Versions.truth}" + ) +} + +object Deps { + val build = Build + val test = Test + val versions = Versions +} diff --git a/buildSrc/src/main/kotlin/java-convention.gradle.kts b/buildSrc/src/main/kotlin/java-convention.gradle.kts new file mode 100644 index 00000000..11eca08e --- /dev/null +++ b/buildSrc/src/main/kotlin/java-convention.gradle.kts @@ -0,0 +1,76 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import net.ltgt.gradle.errorprone.errorprone + +plugins { + `java-library` + id("net.ltgt.errorprone") + id("net.ltgt.apt-idea") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +dependencies { + errorprone(Deps.build.errorProneCore) + implementation(Deps.build.guava) + implementation(Deps.build.jsr305Annotations) + implementation(Deps.build.checkerAnnotations) + Deps.build.errorProneAnnotations.forEach { implementation(it) } + + testImplementation(Deps.test.guavaTestlib) + Deps.test.junit5Api.forEach { testImplementation(it) } + Deps.test.truth.forEach { testImplementation(it) } + testRuntimeOnly(Deps.test.junit5Runner) +} + +tasks.test { + useJUnitPlatform { + includeEngines("junit-jupiter") + } +} + +tasks.compileJava { + options.errorprone.disableWarningsInGeneratedCode.set(true) + // Explicitly states the encoding of the source and test source files, ensuring + // correct execution of the `javac` task. + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation", "-Werror")) + + // Configure Error Prone: + // 1. Exclude generated sources from being analyzed by Error Prone. + // 2. Turn the check off until Error Prone can handle `@Nested` JUnit classes. + // See issue: https://github.com/google/error-prone/issues/956 + // 3. Turn off checks which report unused methods and unused method parameters. + // See issue: https://github.com/SpineEventEngine/config/issues/61 + // + // For more config details see: + // https://github.com/tbroyer/gradle-errorprone-plugin/tree/master#usage + options.errorprone.errorproneArgs.addAll(listOf( + "-XepExcludedPaths:.*/generated/.*", + "-Xep:ClassCanBeStatic:OFF", + "-Xep:UnusedMethod:OFF", + "-Xep:UnusedVariable:OFF", + "-Xep:CheckReturnValue:OFF" + )) +} diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 00000000..0da28f53 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,22 @@ +steps: + # The following step starts the build process using Gradle official image, performs the build + # and runs `jib` for the specified `gcpProject` in order to build and deploy the container + # image to the Google Container Registry. + - name: 'gradle:6.4.1-jdk11' + entrypoint: 'gradle' + args: [ 'build', 'jib', '-PgcpProject=${PROJECT_ID}' ] + # The following step deploys the previously produced container image to the Cloud Run environment + # with the pre-configured `GCP_PROJECT_ID` environment variable. + - name: 'google/cloud-sdk:slim' + entrypoint: 'gcloud' + args: [ + 'run', 'deploy', '${_SERVICE_NAME}', + '--image', 'gcr.io/${PROJECT_ID}/${_SERVICE_NAME}', + '--set-env-vars', 'GCP_PROJECT_ID=${PROJECT_ID}', + '--region', 'europe-west1', + '--platform', 'managed', + '--project', '${PROJECT_ID}' + ] +timeout: 1200s +substitutions: + _SERVICE_NAME: "chat-bot-server" diff --git a/google-chat-bot/build.gradle.kts b/google-chat-bot/build.gradle.kts new file mode 100644 index 00000000..a79d9134 --- /dev/null +++ b/google-chat-bot/build.gradle.kts @@ -0,0 +1,87 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +plugins { + application + id("com.github.johnrengelman.shadow") + id("com.google.cloud.tools.jib") + id("io.spine.tools.gradle.bootstrap") +} + +/** The GCP project ID used for deployment of the application. **/ +val gcpProject: String by project + +spine { + enableJava().server() +} + +dependencies { + annotationProcessor(enforcedPlatform(Deps.build.micronaut.bom)) + annotationProcessor(Deps.build.micronaut.injectJava) + annotationProcessor(Deps.build.micronaut.validation) + + compileOnly(enforcedPlatform(Deps.build.micronaut.bom)) + + implementation(enforcedPlatform(Deps.build.micronaut.bom)) + implementation(Deps.build.micronaut.inject) + implementation(Deps.build.micronaut.validation) + implementation(Deps.build.micronaut.runtime) + implementation(Deps.build.micronaut.netty) + implementation(Deps.build.micronaut.annotationApi) + + implementation(Deps.build.log4j2.core) + runtimeOnly(Deps.build.log4j2.api) + runtimeOnly(Deps.build.log4j2.slf4jBridge) + implementation(Deps.build.flogger) + implementation("com.google.flogger:flogger-log4j2-backend:${Deps.versions.flogger}") { + exclude("org.apache.logging.log4j:log4j-api") + exclude("org.apache.logging.log4j:log4j-core") + } + + implementation(Deps.build.spine.datastore) + implementation(Deps.build.spine.pubsub) + + implementation(Deps.build.google.secretManager) + + implementation(Deps.build.google.chat) + implementation(Deps.build.google.auth) + + testAnnotationProcessor(enforcedPlatform(Deps.build.micronaut.bom)) + testAnnotationProcessor(Deps.build.micronaut.injectJava) + + testImplementation("io.spine:spine-testutil-server:${spine.version()}") + testImplementation(enforcedPlatform(Deps.build.micronaut.bom)) + testImplementation(Deps.build.micronaut.testJUnit5) + testImplementation(Deps.build.micronaut.httpClient) +} + +application { + mainClassName = "io.spine.chatbot.Application" +} + +jib { + to { + image = "gcr.io/${gcpProject}/chat-bot-server" + tags = setOf("latest") + } + container { + mainClass = application.mainClassName + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/Application.java b/google-chat-bot/src/main/java/io/spine/chatbot/Application.java new file mode 100644 index 00000000..5e7c2061 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/Application.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import io.micronaut.runtime.Micronaut; +import io.spine.chatbot.server.Server; +import io.spine.chatbot.server.github.GitHubContext; +import io.spine.chatbot.server.google.chat.GoogleChatContext; +import io.spine.logging.Logging; + +/** + * The entry point to the Spine ChatBot application. + * + *

The application exposes a number of REST endpoints accessible for the clients such as: + * + *

    + *
  • {@code /chat/incoming/event} — handles incoming events from the Google Chat space; + *
  • {@code /repositories/check} — triggers checking of the repositories build statuses. + *
+ * + * @see IncomingEventsController + * @see RepositoriesController + **/ +public final class Application implements Logging { + + static { + useLog4j2FloggerBackend(); + } + + /** + * Prevents direct instantiation. + */ + private Application() { + } + + /** + * Starts the application. + * + *

Performs bounded contexts initialization, starts Spine {@link Server} and runs + * the {@link Micronaut}. + */ + public static void main(String[] args) { + var application = new Application(); + application.start(); + } + + private void start() { + Server.withContexts(GitHubContext.newInstance(), GoogleChatContext.newInstance()) + .start(); + _config().log("Starting Micronaut application."); + Micronaut.run(Application.class); + } + + /** + * Configures Log4j2 as the Flogger backend. + */ + private static void useLog4j2FloggerBackend() { + System.setProperty( + "flogger.backend_factory", + "com.google.common.flogger.backend.log4j2.Log4j2BackendFactory#getInstance" + ); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/BeanFactory.java b/google-chat-bot/src/main/java/io/spine/chatbot/BeanFactory.java new file mode 100644 index 00000000..c49a1989 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/BeanFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.pubsub.v1.PubsubMessage; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.spine.json.Json; +import io.spine.pubsub.PubsubPushRequest; + +import javax.inject.Singleton; +import java.io.IOException; + +import static io.spine.util.Exceptions.newIllegalArgumentException; + +/** + * Creates Micronaut context bean definitions. + */ +@Factory +final class BeanFactory { + + /** + * Registers {@linkplain PubsubPushRequest push request} Jackson deserializer. + */ + @Singleton + @Bean + PubsubPushRequestDeserializer pubsubDeserializer() { + return new PubsubPushRequestDeserializer(); + } + + /** + * Deserializes JSON arriving with {@link PubsubPushRequest} into Spine-compatible + * data structures. + * + * @see + * Jackson Deserialization + */ + @VisibleForTesting + static final class PubsubPushRequestDeserializer extends JsonDeserializer { + + /** + * Deserializes {@link PubsubPushRequest} JSON string into a Protobuf message. + * + *

While Protobuf JSON parser is not able to handle same fields that are set using + * {@code lowerCamelCase} and {@code snake_case} notations, we manually drop duplicate + * fields. + * + * @see + * JsonFormat fails to parse JSON with both `lowerCamelCase` and `snake_case` + * fields + */ + @Override + public PubsubPushRequest deserialize(JsonParser parser, DeserializationContext ctxt) { + try { + var jsonNode = ctxt.readTree(parser); + var messageNode = (ObjectNode) jsonNode.get("message"); + messageNode.remove("message_id"); + messageNode.remove("publish_time"); + var pubsubMessage = Json.fromJson(messageNode.toString(), PubsubMessage.class); + var subscription = jsonNode.get("subscription") + .asText(); + var result = PubsubPushRequest + .newBuilder() + .setMessage(pubsubMessage) + .setSubscription(subscription) + .vBuild(); + return result; + } catch (IOException e) { + throw newIllegalArgumentException( + e, "Unable to deserialize `%s` json.", + PubsubPushRequest.class.getSimpleName() + ); + } + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/IncomingEventsController.java b/google-chat-bot/src/main/java/io/spine/chatbot/IncomingEventsController.java new file mode 100644 index 00000000..705ac0c8 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/IncomingEventsController.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import io.micronaut.context.event.ShutdownEvent; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.runtime.event.annotation.EventListener; +import io.spine.chatbot.google.chat.incoming.ChatEvent; +import io.spine.chatbot.google.chat.incoming.User; +import io.spine.chatbot.google.chat.incoming.event.ChatEventReceived; +import io.spine.core.UserId; +import io.spine.json.Json; +import io.spine.logging.Logging; +import io.spine.pubsub.PubsubPushRequest; +import io.spine.server.integration.ThirdPartyContext; + +import static io.micronaut.http.MediaType.APPLICATION_JSON; + +/** + * A REST controller for handling incoming events from Google Chat. + */ +@Controller("/chat") +final class IncomingEventsController implements Logging { + + private static final String GOOGLE_CHAT_SERVER_CONTEXT_NAME = "GoogleChatServer"; + private static final ThirdPartyContext GOOGLE_CHAT_SERVER = + ThirdPartyContext.singleTenant(GOOGLE_CHAT_SERVER_CONTEXT_NAME); + + /** + * Processes an incoming Google Chat event. + * + *

Dispatches the event using {@link ThirdPartyContext}. + */ + @Post(value = "/incoming/event", consumes = APPLICATION_JSON) + String on(@Body PubsubPushRequest pushRequest) { + var message = pushRequest.getMessage(); + var chatEventJson = message.getData() + .toStringUtf8(); + _debug().log("Received a new chat event:%n%s", chatEventJson); + ChatEvent chatEvent = Json.fromJson(chatEventJson, ChatEvent.class); + var actor = eventActor(chatEvent.getUser()); + var chatEventReceived = ChatEventReceived + .newBuilder() + .setEvent(chatEvent) + .vBuild(); + GOOGLE_CHAT_SERVER.emittedEvent(chatEventReceived, actor); + return "OK"; + } + + private static UserId eventActor(User user) { + return UserId + .newBuilder() + .setValue(user.getName()) + .vBuild(); + } + + /** + * Cleans up resources of the {@link #GOOGLE_CHAT_SERVER context}. + */ + @EventListener + void on(ShutdownEvent event) { + _info().log("Closing `%s` third-party context.", GOOGLE_CHAT_SERVER_CONTEXT_NAME); + try { + GOOGLE_CHAT_SERVER.close(); + } catch (Exception e) { + _error().withCause(e) + .log("Unable to gracefully close `%s` context.", + GOOGLE_CHAT_SERVER_CONTEXT_NAME); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/RepositoriesController.java b/google-chat-bot/src/main/java/io/spine/chatbot/RepositoriesController.java new file mode 100644 index 00000000..f092ed99 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/RepositoriesController.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.spine.chatbot.client.Client; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.organization.Organization; +import io.spine.chatbot.github.repository.build.command.CheckRepositoryBuild; +import io.spine.logging.Logging; + +/** + * A REST controller handling Repository commands. + */ +@Controller("/repositories") +final class RepositoriesController implements Logging { + + /** + * Sends {@link CheckRepositoryBuild} commands to all repositories registered in the system. + */ + @Post("/builds/check") + String checkBuildStatuses() { + _debug().log("Checking repositories build statuses."); + try (var client = Client.newInstance()) { + var organizations = client.listOrganizations(); + for (var org : organizations) { + var repos = client.listOrgRepos(org.getId()); + repos.forEach(repo -> checkBuildStatus(client, repo, org)); + } + return "success"; + } + } + + private void checkBuildStatus(Client client, RepositoryId repo, Organization org) { + _debug().log("Sending `%s` command for the repository `%s`.", + CheckRepositoryBuild.class.getSimpleName(), repo.getValue()); + var checkRepositoryBuild = checkRepoBuildCommand(repo, org); + client.post(checkRepositoryBuild); + } + + private static CheckRepositoryBuild checkRepoBuildCommand(RepositoryId repo, Organization org) { + return CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setOrganization(org.getId()) + .setSpace(org.space()) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/client/Client.java b/google-chat-bot/src/main/java/io/spine/chatbot/client/Client.java new file mode 100644 index 00000000..8818a2f0 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/client/Client.java @@ -0,0 +1,149 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.client; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.spine.base.CommandMessage; +import io.spine.base.EventMessage; +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.organization.Organization; +import io.spine.chatbot.github.organization.OrganizationRepositories; +import io.spine.chatbot.server.Server; +import io.spine.client.CommandRequest; +import io.spine.client.Subscription; + +import java.util.concurrent.CountDownLatch; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * A ChatBot application's Spine client. + * + *

Abstracts working with Spine's {@link io.spine.client.Client client}. + */ +public final class Client implements AutoCloseable { + + private final io.spine.client.Client client; + + private Client(io.spine.client.Client client) { + this.client = client; + } + + /** + * Creates a new in-process client linked to the {@link Server}. + */ + public static Client newInstance() { + io.spine.client.Client client = io.spine.client.Client + .inProcess(Server.name()) + .build(); + return new Client(client); + } + + /** + * Retrieves all registered organizations. + */ + public ImmutableList listOrganizations() { + return client.asGuest() + .select(Organization.class) + .run(); + } + + /** + * Returns list of all registered repositories for the {@code organization}. + */ + public ImmutableList listOrgRepos(OrganizationId org) { + checkNotNull(org); + var orgRepos = client.asGuest() + .select(OrganizationRepositories.class) + .byId(org) + .run(); + checkState(orgRepos.size() == 1); + return ImmutableList.copyOf(orgRepos.get(0) + .getRepositoryList()); + } + + @Override + public void close() { + this.client.close(); + } + + /** + * Posts a command and waits synchronously till the expected outcome event is published. + */ + public void post(CommandMessage command, Class expectedOutcome) { + checkNotNull(command); + checkNotNull(expectedOutcome); + post(command, expectedOutcome, 1); + } + + /** + * Posts a command asynchronously. + * + * @see #post(CommandMessage, Class) + */ + public void post(CommandMessage command) { + checkNotNull(command); + var subscriptions = client.asGuest() + .command(command) + .onStreamingError(Client::throwProcessingError) + .post(); + subscriptions.forEach(this::cancelSubscription); + } + + private void + post(CommandMessage command, Class expectedOutcome, int expectedEvents) { + var latch = new CountDownLatch(expectedEvents); + var subscriptions = client.asGuest() + .command(command) + .onStreamingError(Client::throwProcessingError) + .observe(expectedOutcome, event -> latch.countDown()) + .post(); + try { + latch.await(); + } catch (InterruptedException e) { + newIllegalStateException(e, "Processing of command interrupted:%n%s.", command); + } + subscriptions.forEach(this::cancelSubscription); + } + + /** + * Cancels the passed subscription. + * + * @see io.spine.client.Subscriptions#cancel(Subscription) + * @see CommandRequest#post() + */ + @CanIgnoreReturnValue + private boolean cancelSubscription(Subscription subscription) { + checkNotNull(subscription); + return client.subscriptions() + .cancel(subscription); + } + + private static void throwProcessingError(Throwable throwable) { + throw newIllegalStateException( + throwable, "An error while processing the command." + ); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/client/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/client/package-info.java new file mode 100644 index 00000000..4915e9fc --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/client/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the ChatBot client. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.client; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/delivery/DistributedDelivery.java b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/DistributedDelivery.java new file mode 100644 index 00000000..f33df478 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/DistributedDelivery.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.delivery; + +import io.spine.server.delivery.Delivery; +import io.spine.server.delivery.ShardedWorkRegistry; +import io.spine.server.delivery.UniformAcrossAllShards; +import io.spine.server.storage.datastore.DatastoreStorageFactory; +import io.spine.server.storage.datastore.DsShardedWorkRegistry; + +/** + * Delivers messages using Datastore as the underlying storage. + * + *

The delivery is based on the {@link DatastoreStorageFactory Datastore} and uses + * {@link DsShardedWorkRegistry} as the {@linkplain ShardedWorkRegistry work registry}. + */ +final class DistributedDelivery { + + /** The number of shards used for the signal delivery. **/ + private static final int NUMBER_OF_SHARDS = 50; + + /** + * Prevents instantiation of this utility class. + */ + private DistributedDelivery() { + } + + /** + * Creates a new Datastore-based delivery using the supplied Datastore {@code storageFactory}. + * + *

Assigns the targets uniformly across shards. Configures the inbox storage + * to be single-tenant. + */ + public static Delivery instance(DatastoreStorageFactory storageFactory) { + var workRegistry = new DsShardedWorkRegistry(storageFactory); + var inboxStorage = storageFactory.createInboxStorage(false); + var delivery = Delivery + .newBuilder() + .setStrategy(UniformAcrossAllShards.forNumber(NUMBER_OF_SHARDS)) + .setWorkRegistry(workRegistry) + .setInboxStorage(inboxStorage) + .build(); + return delivery; + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/delivery/LocalDelivery.java b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/LocalDelivery.java new file mode 100644 index 00000000..0033eac6 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/LocalDelivery.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.delivery; + +import com.google.protobuf.util.Durations; +import io.spine.server.delivery.CatchUpStorage; +import io.spine.server.delivery.Delivery; +import io.spine.server.delivery.InboxStorage; +import io.spine.server.delivery.UniformAcrossAllShards; +import io.spine.server.delivery.memory.InMemoryShardedWorkRegistry; +import io.spine.server.storage.memory.InMemoryCatchUpStorage; +import io.spine.server.storage.memory.InMemoryInboxStorage; + +/** + * A {@link Delivery} factory that creates deliveries for local or test environments. + */ +public final class LocalDelivery { + + /** A singleton instance of the local delivery. **/ + public static final Delivery instance = delivery(); + + /** + * Prevents instantiation of this class. + */ + private LocalDelivery() { + } + + /** + * Creates a new instance of an in-memory local delivery. + */ + private static Delivery delivery() { + var delivery = Delivery + .newBuilder() + .setInboxStorage(singleTenantInboxStorage()) + .setCatchUpStorage(singleTenantCatchupStorage()) + .setWorkRegistry(new InMemoryShardedWorkRegistry()) + .setStrategy(UniformAcrossAllShards.singleShard()) + .setDeduplicationWindow(Durations.fromSeconds(0)) + .build(); + delivery.subscribe(ShardDelivery::deliver); + return delivery; + } + + @SuppressWarnings("TestOnlyProblems") // we do want the in-memory delivery in local-dev env + private static InboxStorage singleTenantInboxStorage() { + return new InMemoryInboxStorage(false); + } + + @SuppressWarnings("TestOnlyProblems") // we do want the in-memory delivery in local-dev env + private static CatchUpStorage singleTenantCatchupStorage() { + return new InMemoryCatchUpStorage(false); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/delivery/ShardDelivery.java b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/ShardDelivery.java new file mode 100644 index 00000000..e8548cc0 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/ShardDelivery.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.delivery; + +import io.spine.logging.Logging; +import io.spine.server.ServerEnvironment; +import io.spine.server.delivery.DeliveryStats; +import io.spine.server.delivery.ShardIndex; +import io.spine.server.delivery.ShardedRecord; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Delivers messages from a particular shard. + * + *

Wraps the {@link io.spine.server.delivery.Delivery#deliverMessagesFrom(ShardIndex) + * Delivery#deliverMessagesFrom} with server environment-specific logging and provides helpers + * that unifies the usage of the delivery. + */ +final class ShardDelivery implements Logging { + + private final ShardIndex shard; + + private ShardDelivery(ShardIndex shard) { + this.shard = shard; + } + + /** + * Delivers the {@code message}. + */ + static void deliver(ShardedRecord message) { + checkNotNull(message); + deliverFrom(message.shardIndex()); + } + + /** + * Delivers messages from the {@code shard}. + */ + private static void deliverFrom(ShardIndex shard) { + checkNotNull(shard); + var delivery = new ShardDelivery(shard); + delivery.deliverNow(); + } + + private void deliverNow() { + var trace = _trace(); + var server = ServerEnvironment.instance(); + var nodeId = server.nodeId() + .getValue(); + var indexValue = shard.getIndex(); + trace.log("Delivering messages from the shard with index `%d`. NodeId=%s.", + indexValue, nodeId); + var stats = server.delivery() + .deliverMessagesFrom(shard); + if (stats.isPresent()) { + DeliveryStats deliveryStats = stats.get(); + trace.log("`%d` messages delivered from the shard with index `%s`. NodeId=%s.", + deliveryStats.deliveredCount(), indexValue, nodeId); + } else { + trace.log("No messages delivered from the shard with index `%d`. NodeId=%s.", + indexValue, nodeId); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/delivery/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/package-info.java new file mode 100644 index 00000000..dc021563 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/delivery/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package is devoted to configuring the delivery mechanism for ChatBot signals. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.delivery; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/GitHubIdentifiers.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/GitHubIdentifiers.java new file mode 100644 index 00000000..9eeeb5f8 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/GitHubIdentifiers.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github; + +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A utility for working with {@code GitHub} context identifiers. + */ +public final class GitHubIdentifiers { + + /** + * Prevents instantiation of this utility class. + */ + private GitHubIdentifiers() { + } + + /** + * Creates a new {@code OrganizationId} out of the specified {@code name}. + */ + public static OrganizationId organization(String name) { + checkNotEmptyOrBlank(name); + return OrganizationId + .newBuilder() + .setValue(name) + .vBuild(); + } + + /** + * Creates a new {@code RepositoryId} out of the specified {@code slug}. + */ + public static RepositoryId repository(String slug) { + checkNotEmptyOrBlank(slug); + return RepositoryId + .newBuilder() + .setValue(slug) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/SlugMixin.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/SlugMixin.java new file mode 100644 index 00000000..4c3ef629 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/SlugMixin.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github; + +import io.spine.annotation.GeneratedMixin; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * Augments {@link Slug} with useful methods. + */ +@GeneratedMixin +public interface SlugMixin extends SlugOrBuilder { + + /** + * Returns the slug {@code value}. + */ + default String value() { + return getValue(); + } + + /** + * Returns URL-encoded slug {@code value}. + */ + default String encodedValue() { + return encode(getValue()); + } + + /** + * Encodes passed value using {@link URLEncoder} and + * {@link StandardCharsets#UTF_8 UTF_8} charset. + */ + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/Slugs.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/Slugs.java new file mode 100644 index 00000000..8222cec1 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/Slugs.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A utility for working with {@link Slug}s. + */ +public final class Slugs { + + /** + * Prevents instantiation of this utility class. + */ + private Slugs() { + } + + /** + * Creates a new {@code Slug} for the {@code repository}. + */ + public static Slug repoSlug(RepositoryId repo) { + checkNotNull(repo); + return newSlug(repo.getValue()); + } + + /** + * Creates a new {@code Slug} for the {@code organization}. + */ + public static Slug orgSlug(OrganizationId org) { + checkNotNull(org); + return newSlug(org.getValue()); + } + + /** + * Creates a new {@code Slug} with the specified {@code value}. + */ + public static Slug newSlug(String value) { + checkNotEmptyOrBlank(value); + return Slug.newBuilder() + .setValue(value) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/OrgHeaderAware.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/OrgHeaderAware.java new file mode 100644 index 00000000..f737b9fe --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/OrgHeaderAware.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.organization; + +import io.spine.annotation.GeneratedMixin; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.net.Url; + +/** + * Common interface for messages aware of the {@link OrgHeader}. + */ +@GeneratedMixin +public interface OrgHeaderAware { + + /** + * Returns the organization {@code header}. + * + * @implNote This method is implemented in the deriving Protobuf messages. + */ + OrgHeader getHeader(); + + /** + * Returns the organization {@code name}. + */ + default String name() { + return getHeader().getName(); + } + + /** + * Returns the organization {@code website}. + */ + default Url website() { + return getHeader().getWebsite(); + } + + /** + * Returns the organization {@code githubProfile}. + */ + default Url githubProfile() { + return getHeader().getGithubProfile(); + } + + /** + * Returns the organization {@code travisProfile}. + */ + default Url travisProfile() { + return getHeader().getTravisProfile(); + } + + /** + * Returns the {@code space} associated with the organization. + */ + default SpaceId space() { + return getHeader().getSpace(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/package-info.java new file mode 100644 index 00000000..69b88571 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/organization/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the GitHub organization-specific language. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.github.organization; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/package-info.java new file mode 100644 index 00000000..682d9d72 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the GitHub-specific language. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.github; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepoHeaderAware.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepoHeaderAware.java new file mode 100644 index 00000000..91acc055 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepoHeaderAware.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.repository; + +import io.spine.annotation.GeneratedMixin; +import io.spine.chatbot.github.OrganizationId; +import io.spine.net.Url; + +/** + * Common interface for messages aware of the {@link RepoHeader}. + */ +@GeneratedMixin +public interface RepoHeaderAware { + + /** + * Returns the repository {@code header}. + * + * @implNote This method is implemented in the deriving Protobuf messages. + */ + RepoHeader getHeader(); + + /** + * Returns the repository {@code name}. + */ + default String name() { + return getHeader().getName(); + } + + /** + * Returns the repository {@code githubProfile}. + */ + default Url githubProfile() { + return getHeader().getGithubProfile(); + } + + /** + * Returns the repository {@code travisProfile}. + */ + default Url travisProfile() { + return getHeader().getTravisProfile(); + } + + /** + * Returns the organization associated with the repository. + */ + default OrganizationId organization() { + return getHeader().getOrganization(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAware.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAware.java new file mode 100644 index 00000000..5febfc89 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAware.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.repository; + +import io.spine.annotation.GeneratedMixin; +import io.spine.chatbot.github.RepositoryId; + +/** + * Common interface for messages aware of the {@link RepositoryId repository}. + */ +@GeneratedMixin +public interface RepositoryAware { + + /** + * Obtains the repository ID. + */ + default RepositoryId repository() { + return getRepository(); + } + + /** + * Obtains the repository ID. + * + * @implNote This method is implemented in the deriving Protobuf messages. + */ + RepositoryId getRepository(); +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAwareEvent.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAwareEvent.java new file mode 100644 index 00000000..f9d35bd7 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/RepositoryAwareEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.repository; + +import com.google.errorprone.annotations.Immutable; +import io.spine.annotation.GeneratedMixin; +import io.spine.base.EventMessage; + +/** + * A repository-aware event message. + */ +@GeneratedMixin +@Immutable +public interface RepositoryAwareEvent extends RepositoryAware, EventMessage { +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/BuildStateMixin.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/BuildStateMixin.java new file mode 100644 index 00000000..00cf8fad --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/BuildStateMixin.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.repository.build; + +import io.spine.annotation.GeneratedMixin; + +import java.util.EnumSet; + +import static io.spine.chatbot.github.repository.build.Build.State.BS_UNKNOWN; +import static io.spine.chatbot.github.repository.build.Build.State.PASSED; +import static io.spine.chatbot.github.repository.build.BuildStateChange.Type.FAILED; +import static io.spine.chatbot.github.repository.build.BuildStateChange.Type.RECOVERED; +import static io.spine.chatbot.github.repository.build.BuildStateChange.Type.STABLE; +import static io.spine.util.Exceptions.newIllegalStateException; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * Augments {@link Build} with useful methods. + */ +@GeneratedMixin +public interface BuildStateMixin extends BuildOrBuilder { + + /** + * Determines whether the build is failed. + * + * @return {@code true} if the build is failed, {@code false} otherwise + * @see #failed(Build.State) + */ + default boolean failed() { + return failed(getState()); + } + + /** + * Returns a capitalized label of the {@linkplain Build.State build state}. + */ + default String stateLabel() { + var state = getState(); + var name = state.name(); + var result = name.charAt(0) + name.substring(1) + .toLowerCase(); + return result; + } + + /** + * Creates an instance of the {@linkplain Build.State build state} of out its + * string representation, ignoring the case. + */ + static Build.State buildStateFrom(String state) { + checkNotEmptyOrBlank(state); + return Build.State.valueOf(state.toUpperCase()); + } + + /** + * Determines the {@linkplain BuildStateChange state change} of the build comparing to the + * {@code previousState}. + * + * @see #stateChange(BuildStateMixin, BuildStateMixin) + */ + default BuildStateChange.Type stateChangeFrom(BuildStateMixin previousState) { + return stateChange(this, previousState); + } + + /** + * Determines the {@linkplain BuildStateChange state change} between build states. + * + *

The status is considered: + * + *

    + *
  • {@code failed} if the new state is {@link #failed() failed}; + *
  • {@code recovered} if the new state is {@code passed} and the previous is + * {@link #failed() failed}; + *
  • {@code stable} if the new state is {@code passed} and the previous is either + * {@code unknown} meaning that there were no previous states or {@code passed} as well. + *
+ */ + private static BuildStateChange.Type stateChange(BuildStateMixin newBuildState, + BuildStateMixin previousBuildState) { + var currentState = newBuildState.getState(); + var previousState = previousBuildState.getState(); + if (newBuildState.failed()) { + return FAILED; + } + if (currentState == PASSED && previousBuildState.failed()) { + return RECOVERED; + } + if (currentState == PASSED && (previousState == PASSED || previousState == BS_UNKNOWN)) { + return STABLE; + } + throw newIllegalStateException( + "Build is in an unpredictable state. Current state `%s`. Previous state `%s`.", + currentState.name(), previousState.name() + ); + } + + /** + * Determines whether the build state denotes a failed status. + * + *

The {@code cancelled}, {@code failed} and {@code errored} statuses are considered + * failed statuses. + * + * @return {@code true} if the build status is failed, {@code false} otherwise + */ + private static boolean failed(Build.State state) { + var failedStatuses = EnumSet.of( + Build.State.CANCELLED, Build.State.FAILED, Build.State.ERRORED + ); + return failedStatuses.contains(state); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/package-info.java new file mode 100644 index 00000000..9c6fb54a --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/build/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the GitHub repository build-specific language. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.github.repository.build; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/package-info.java new file mode 100644 index 00000000..ada78377 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/github/repository/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the GitHub repository-specific language. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.github.repository; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/BuildStateUpdates.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/BuildStateUpdates.java new file mode 100644 index 00000000..88a36ebb --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/BuildStateUpdates.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.api.services.chat.v1.model.CardHeader; +import com.google.api.services.chat.v1.model.KeyValue; +import com.google.api.services.chat.v1.model.Message; +import com.google.api.services.chat.v1.model.Section; +import com.google.api.services.chat.v1.model.Thread; +import com.google.api.services.chat.v1.model.WidgetMarkup; +import com.google.common.collect.ImmutableList; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.google.chat.thread.ThreadResource; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.chatbot.google.chat.ChatWidgets.cardWith; +import static io.spine.chatbot.google.chat.ChatWidgets.linkButton; +import static io.spine.chatbot.google.chat.ChatWidgets.sectionWithWidget; +import static io.spine.chatbot.google.chat.ChatWidgets.textParagraph; +import static io.spine.protobuf.Messages.isNotDefault; +import static io.spine.validate.Validate.checkValid; + +/** + * A utility class that creates {@link Build} update messages. + */ +final class BuildStateUpdates { + + private static final String FAILURE_ICON = + "https://storage.googleapis.com/spine-chat-bot.appspot.com/failure-icon.png"; + private static final String SUCCESS_ICON = + "https://storage.googleapis.com/spine-chat-bot.appspot.com/success-icon.png"; + + /** + * Prevents instantiation of this utility class. + */ + private BuildStateUpdates() { + } + + /** + * Creates a new {@link Build} update message from the supplied {@code build} + * and {@code thread}. + * + *

If the thread has no name set, assumes that the update message should be + * sent to a new thread. + */ + static Message buildStateMessage(Build build, ThreadResource thread) { + checkValid(build); + checkNotNull(thread); + var headerIcon = build.failed() ? FAILURE_ICON : SUCCESS_ICON; + var cardHeader = new CardHeader() + .setTitle(build.getRepository() + .value()) + .setImageUrl(headerIcon); + var sections = ImmutableList.of( + buildStateSection(build), + commitSection(build), + actions(build) + ); + var message = new Message().setCards(cardWith(cardHeader, sections)); + if (isNotDefault(thread)) { + message.setThread(new Thread().setName(thread.getName())); + } + return message; + } + + private static Section commitSection(Build build) { + var commit = build.getLastCommit(); + var commitInfo = String.format( + "Authored by %s at %s.", commit.getAuthoredBy(), commit.getCommittedAt() + ); + var section = new Section() + .setHeader("Commit " + commit.getSha()) + .setWidgets(ImmutableList.of( + textParagraph(commit.getMessage()), + textParagraph(commitInfo) + )); + return section; + } + + private static Section actions(Build build) { + var commit = build.getLastCommit(); + var actionButtons = new WidgetMarkup().setButtons(ImmutableList.of( + linkButton("Build", build.getTravisCiUrl()), + linkButton("Changeset", commit.getCompareUrl()) + )); + return sectionWithWidget(actionButtons); + } + + private static Section buildStateSection(Build build) { + return sectionWithWidget(buildStateWidget(build)); + } + + private static WidgetMarkup buildStateWidget(Build build) { + var keyValue = new KeyValue() + .setTopLabel("Build No.") + .setContent(build.getNumber()) + .setBottomLabel(build.stateLabel()); + return new WidgetMarkup().setKeyValue(keyValue); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/ChatWidgets.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/ChatWidgets.java new file mode 100644 index 00000000..ce9620cc --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/ChatWidgets.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.api.services.chat.v1.model.Button; +import com.google.api.services.chat.v1.model.Card; +import com.google.api.services.chat.v1.model.CardHeader; +import com.google.api.services.chat.v1.model.OnClick; +import com.google.api.services.chat.v1.model.OpenLink; +import com.google.api.services.chat.v1.model.Section; +import com.google.api.services.chat.v1.model.TextButton; +import com.google.api.services.chat.v1.model.TextParagraph; +import com.google.api.services.chat.v1.model.WidgetMarkup; +import com.google.common.collect.ImmutableList; +import io.spine.net.Url; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * Provides building blocks to empower the rich messages sent to Google Chat. + * + * @see + * Google Chat Cards + */ +final class ChatWidgets { + + /** + * Prevents instantiation of this utility class. + */ + private ChatWidgets() { + } + + /** + * Creates a new button with an on-click open link action with the specified {@code title} + * and {@code url} to open upon a click. + */ + static Button linkButton(String title, Url url) { + checkNotEmptyOrBlank(title); + checkNotNull(url); + var button = new TextButton().setText(title) + .setOnClick(openLink(url)); + return new Button().setTextButton(button); + } + + private static OnClick openLink(Url url) { + return new OnClick().setOpenLink(new OpenLink().setUrl(url.getSpec())); + } + + /** + * Creates a singleton card list with a new {@link Card} with a specified {@code header} + * and {@code sections}. + */ + static ImmutableList cardWith(CardHeader header, List

sections) { + checkNotNull(header); + checkNotNull(sections); + return ImmutableList.of(new Card().setHeader(header) + .setSections(sections)); + } + + /** + * Creates a new {@link Section} with a single {@code widget}. + */ + static Section sectionWithWidget(WidgetMarkup widget) { + checkNotNull(widget); + return new Section().setWidgets(List.of(widget)); + } + + /** + * Creates a new {@link TextParagraph} widget with the supplied {@code formattedText}. + * + * @see + * Card text formatting + */ + static WidgetMarkup textParagraph(String formattedText) { + checkNotEmptyOrBlank(formattedText); + return new WidgetMarkup().setTextParagraph(new TextParagraph().setText(formattedText)); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChat.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChat.java new file mode 100644 index 00000000..64687b55 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChat.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.api.services.chat.v1.HangoutsChat; +import com.google.api.services.chat.v1.model.Message; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.google.chat.thread.ThreadResource; +import io.spine.logging.Logging; + +import java.io.IOException; + +import static com.google.api.client.util.Preconditions.checkNotNull; +import static io.spine.chatbot.google.chat.BuildStateUpdates.buildStateMessage; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.thread; +import static io.spine.chatbot.server.google.chat.ThreadResources.threadResource; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * Google Chat API client. + * + * @see Google Chat API + */ +final class GoogleChat implements GoogleChatClient, Logging { + + private final HangoutsChat chat; + + GoogleChat(HangoutsChat chat) { + this.chat = checkNotNull(chat); + } + + @Override + public BuildStateUpdate sendBuildStateUpdate(Build build, ThreadResource thread) { + checkNotNull(build); + checkNotNull(thread); + var repo = build.getRepository(); + var trace = _trace(); + trace.log("Building state update message for the repository `%s`.", repo); + var message = buildStateMessage(build, thread); + trace.log("Sending state update message for the repository `%s`.", repo); + var sentMessage = sendMessage(build.getSpace(), message); + trace.log( + "Build state update message with ID `%s` for the repository `%s` sent to the thread `%s`.", + sentMessage.getName(), repo, sentMessage.getThread() + .getName() + ); + return BuildStateUpdate + .newBuilder() + .setMessage(message(message.getName())) + .setResource(threadResource(message.getThread() + .getName())) + .setSpace(build.getSpace()) + .setThread(thread(repo.value())) + .vBuild(); + } + + @CanIgnoreReturnValue + private Message sendMessage(SpaceId space, Message message) { + try { + return chat + .spaces() + .messages() + .create(space.getValue(), message) + .execute(); + } catch (IOException e) { + throw newIllegalStateException(e, "Unable to send message to the space `%s`.", space); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatClient.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatClient.java new file mode 100644 index 00000000..469d38cf --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatClient.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.api.services.chat.v1.HangoutsChat; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.google.chat.thread.ThreadResource; + +/** + * A client to the Google Chat server. + * + *

Abstracts out usage of the chat API by exposing only ready-to-use ChatBot-specific + * methods. + */ +public interface GoogleChatClient { + + /** + * Sends {@link Build} state update message to the related space and thread. + * + *

If the {@code thread} has no name specified the message is sent to a new thread. + * + * @return a sent build state update message + */ + BuildStateUpdate sendBuildStateUpdate(Build build, ThreadResource thread); + + /** + * Creates a new Google Chat client. + * + *

The client is backed by {@link HangoutsChat} API. + */ + static GoogleChatClient newInstance() { + return new GoogleChat(HangoutsChatFactory.newInstance()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatIdentifiers.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatIdentifiers.java new file mode 100644 index 00000000..11c9a7f0 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatIdentifiers.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A utility for working with {@code Google Chat} context identifiers. + */ +public final class GoogleChatIdentifiers { + + /** + * Prevents instantiation of this utility class. + */ + private GoogleChatIdentifiers() { + } + + /** + * Creates a new {@code ThreadId} out of the specified {@code value}. + */ + public static ThreadId thread(String value) { + checkNotEmptyOrBlank(value); + return ThreadId + .newBuilder() + .setValue(value) + .vBuild(); + } + + /** + * Creates a new {@code SpaceId} out of the specified {@code value}. + */ + public static SpaceId space(String value) { + checkNotEmptyOrBlank(value); + return SpaceId + .newBuilder() + .setValue(value) + .vBuild(); + } + + /** + * Creates a new {@code MessageId} out of the specified {@code value}. + */ + public static MessageId message(String value) { + checkNotEmptyOrBlank(value); + return MessageId + .newBuilder() + .setValue(value) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatKey.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatKey.java new file mode 100644 index 00000000..4f7fadef --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/GoogleChatKey.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.auth.oauth2.GoogleCredentials; +import io.spine.chatbot.google.secret.Secret; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import static io.spine.util.Exceptions.newIllegalStateException; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A service account key configured for Google Chat. + */ +final class GoogleChatKey extends Secret { + + private static final String CHAT_BOT_SCOPE = "https://www.googleapis.com/auth/chat.bot"; + private static final String CHAT_SERVICE_ACCOUNT = "ChatServiceAccount"; + + private final String value; + + private GoogleChatKey(String value) { + this.value = value; + } + + /** + * Creates a new Google Chat service account key. + */ + static GoogleChatKey chatServiceAccountKey() { + var value = checkNotEmptyOrBlank(retrieveSecret(CHAT_SERVICE_ACCOUNT)); + return new GoogleChatKey(value); + } + + /** + * Converts the key to respective scoped {@code GoogleCredentials}. + */ + GoogleCredentials toCredentials() { + try { + return GoogleCredentials + .fromStream(streamFrom(value)) + .createScoped(CHAT_BOT_SCOPE); + } catch (IOException e) { + throw newIllegalStateException(e, "Unable to read `GoogleCredentials`."); + } + } + + private static InputStream streamFrom(String data) { + return new ByteArrayInputStream(data.getBytes(Charset.defaultCharset())); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/HangoutsChatFactory.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/HangoutsChatFactory.java new file mode 100644 index 00000000..418aacd5 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/HangoutsChatFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.chat.v1.HangoutsChat; +import com.google.auth.http.HttpCredentialsAdapter; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import static io.spine.chatbot.google.chat.GoogleChatKey.chatServiceAccountKey; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * Provides fully-configured {@link HangoutsChat chat} client. + */ +final class HangoutsChatFactory { + + private static final String BOT_NAME = "Spine ChatBot"; + + /** + * Prevents direct instantiation of the utility class. + */ + private HangoutsChatFactory() { + } + + /** + * Creates a new instance of the {@link HangoutsChat} client. + */ + static HangoutsChat newInstance() { + var credentials = chatServiceAccountKey().toCredentials(); + var credentialsAdapter = new HttpCredentialsAdapter(credentials); + var chat = chatWithCredentials(credentialsAdapter) + .setApplicationName(BOT_NAME) + .build(); + return chat; + } + + private static HangoutsChat.Builder + chatWithCredentials(HttpCredentialsAdapter credentialsAdapter) { + var transport = newTrustedTransport(); + var jacksonFactory = JacksonFactory.getDefaultInstance(); + return new HangoutsChat.Builder(transport, jacksonFactory, credentialsAdapter); + } + + private static HttpTransport newTrustedTransport() { + try { + return GoogleNetHttpTransport.newTrustedTransport(); + } catch (GeneralSecurityException | IOException e) { + throw newIllegalStateException(e, "Unable to instantiate trusted transport."); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/SpaceHeaderAware.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/SpaceHeaderAware.java new file mode 100644 index 00000000..c5a7a09c --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/SpaceHeaderAware.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import io.spine.annotation.GeneratedMixin; + +/** + * Common interface for messages aware of the {@link SpaceHeader}. + */ +@GeneratedMixin +public interface SpaceHeaderAware { + + /** + * Returns the space header. + * + * @implNote This method is implemented in the deriving Protobuf messages. + */ + SpaceHeader getHeader(); + + /** + * Determines whether a space is a Direct Message (DM) between a bot and a human. + */ + default boolean directMessage() { + return getHeader().getSingleUserBotDm(); + } + + /** + * Determines whether the messages are threaded in the space. + */ + default boolean isThreaded() { + return getHeader().getThreaded(); + } + + /** + * Returns the display name of the space. + */ + default String displayName() { + return getHeader().getDisplayName(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/SpaceMixin.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/SpaceMixin.java new file mode 100644 index 00000000..947cc78f --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/SpaceMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat.incoming; + +import io.spine.annotation.GeneratedMixin; +import io.spine.chatbot.google.chat.SpaceId; + +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; + +/** + * Provides utility helpers for the {@link io.spine.chatbot.google.chat.incoming.Space Space} type. + */ +@GeneratedMixin +public interface SpaceMixin extends SpaceOrBuilder { + + /** + * Determines whether the space is threaded. + * + * @return {@code true} if the space is threaded, {@code false} otherwise + */ + default boolean isThreaded() { + return getType() == SpaceType.ROOM; + } + + /** + * Returns the space ID. + */ + default SpaceId id() { + return space(getName()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/package-info.java new file mode 100644 index 00000000..81c75e6a --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/incoming/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains entities related to the Google Chat incoming events handling. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.google.chat.incoming; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/package-info.java new file mode 100644 index 00000000..a2382aea --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/chat/package-info.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains Google Chat API facade. + * + *

The usage of the Chat API itself it not straightforward. That's why it is recommended to + * use the {@linkplain io.spine.chatbot.google.chat.GoogleChatClient facade} instead. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.google.chat; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/Secret.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/Secret.java new file mode 100644 index 00000000..e7b0dda1 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/Secret.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.secret; + +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretVersionName; + +import java.io.IOException; + +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * The abstract base for utilities that access application secrets stored in Google Secret Manager. + * + * @see Google Secret Manager + */ +public abstract class Secret { + + @SuppressWarnings("CallToSystemGetenv") + private static final String PROJECT_ID = System.getenv("GCP_PROJECT_ID"); + + protected Secret() { + } + + /** + * Retrieves the secret with the specified {@code name}. + * + *

The latest version of the secret available in the current {@link #PROJECT_ID project} + * is retrieved. + */ + protected static String retrieveSecret(String name) { + try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { + var secretVersion = SecretVersionName.of(PROJECT_ID, name, "latest"); + var secret = client.accessSecretVersion(secretVersion) + .getPayload() + .getData() + .toStringUtf8(); + return secret; + } catch (IOException e) { + throw newIllegalStateException(e, "Unable to retrieve secret `%s`.", name); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/package-info.java new file mode 100644 index 00000000..92224ac0 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/google/secret/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains Google Secret Manager API facade. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.google.secret; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/net/MoreUrls.java b/google-chat-bot/src/main/java/io/spine/chatbot/net/MoreUrls.java new file mode 100644 index 00000000..52fefc56 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/net/MoreUrls.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.net; + +import io.spine.chatbot.github.Slug; +import io.spine.net.Url; +import io.spine.net.Urls; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.util.Preconditions2.checkPositive; +import static java.lang.String.format; + +/** + * Static factories for creating project-specific {@link Url}s. + */ +public final class MoreUrls { + + private static final String TRAVIS_GITHUB_ENDPOINT = "https://travis-ci.com/github"; + private static final String GITHUB = "https://github.com"; + + /** + * Prevents instantiation of this utility class. + */ + private MoreUrls() { + } + + /** + * Creates a new Travis CI build URL for a build with the specified {@code buildId} of + * the repository with the specified {@code slug}. + */ + public static Url travisBuildUrlFor(Slug slug, long buildId) { + checkNotNull(slug); + var spec = format( + "%s/%s/builds/%d", TRAVIS_GITHUB_ENDPOINT, slug.getValue(), checkPositive(buildId) + ); + return Urls.create(spec); + } + + /** + * Creates a new Travis CI URL. + */ + public static Url travisUrlFor(Slug slug) { + checkNotNull(slug); + var spec = format("%s/%s", TRAVIS_GITHUB_ENDPOINT, slug.getValue()); + return Urls.create(spec); + } + + /** + * Creates a new GitHub URL. + */ + public static Url githubUrlFor(Slug slug) { + checkNotNull(slug); + var spec = format("%s/%s", GITHUB, slug.getValue()); + return Urls.create(spec); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/net/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/net/package-info.java new file mode 100644 index 00000000..f825e52b --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/net/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains utilities for working with {@link io.spine.net.Url URL}s. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.net; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/package-info.java new file mode 100644 index 00000000..f74c9fd4 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This contains package the application {@linkplain io.spine.chatbot.Application entry point}. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/ContextBuilderAware.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/ContextBuilderAware.java new file mode 100644 index 00000000..d7737bca --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/ContextBuilderAware.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server; + +import io.spine.server.BoundedContextBuilder; + +/** + * Common interface for {@link io.spine.server.BoundedContext BoundedContext} providers + * aware of the {@link BoundedContextBuilder}. + */ +public interface ContextBuilderAware { + + /** + * Returns the context builder associated with context. + */ + BoundedContextBuilder builder(); +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/DiagnosticEventLogger.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/DiagnosticEventLogger.java new file mode 100644 index 00000000..f660e9d6 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/DiagnosticEventLogger.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server; + +import io.spine.base.Identifier; +import io.spine.core.Subscribe; +import io.spine.logging.Logging; +import io.spine.server.event.AbstractEventSubscriber; +import io.spine.system.server.CannotDispatchDuplicateCommand; +import io.spine.system.server.CannotDispatchDuplicateEvent; +import io.spine.system.server.ConstraintViolated; +import io.spine.system.server.HandlerFailedUnexpectedly; +import io.spine.system.server.RoutingFailed; +import io.spine.validate.diags.ViolationText; + +import java.util.stream.Collectors; + +/** + * Logs internal diagnostic events to ease applications management. + */ +public final class DiagnosticEventLogger extends AbstractEventSubscriber implements Logging { + + /** + * Logs entity constraint violation rejection. + */ + @Subscribe + void on(ConstraintViolated e) { + var entity = e.getEntity(); + var violations = e.getViolationList() + .stream() + .map(ViolationText::of) + .map(ViolationText::toString) + .collect(Collectors.joining()); + _error().log( + "Entity `%s` with value `%s` validation constraints are violated. The violations are:%n%s", + entity.getTypeUrl(), Identifier.toString(entity.id()), violations + ); + } + + /** + * Logs duplicate command delivery rejection. + */ + @Subscribe + void on(CannotDispatchDuplicateCommand e) { + var command = e.getDuplicateCommand(); + var entity = e.getEntity(); + _warn().log( + "Duplicate delivery of the command `%s` with ID `%s` to the entity `%s` with ID `%s` prevented.", + command.getTypeUrl(), Identifier.toString(command.getId()), + entity.getTypeUrl(), Identifier.toString(entity.id()) + ); + } + + /** + * Logs duplicate event delivery rejection. + */ + @Subscribe + void on(CannotDispatchDuplicateEvent e) { + var event = e.getDuplicateEvent(); + var entity = e.getEntity(); + _warn().log( + "Duplicate delivery of the event `%s` with ID `%s` to the entity `%s` with ID `%s` prevented.", + event.getTypeUrl(), Identifier.toString(entity.getId()), + entity.getTypeUrl(), Identifier.toString(entity.id()) + ); + } + + /** + * Logs unexpected signal handler exception. + */ + @Subscribe + void on(HandlerFailedUnexpectedly e) { + var signal = e.getHandledSignal(); + var entity = e.getEntity(); + var error = e.getError(); + _error().log( + "Signal `%s` with ID `%s` handler of the entity `%s` with ID `%s` failed with error `%s`.%n%s", + signal.getTypeUrl(), Identifier.toString(signal.id()), + entity.getTypeUrl(), Identifier.toString(entity.id()), + error.getMessage(), error.getStacktrace() + ); + } + + /** + * Logs routing failures. + */ + @Subscribe + void on(RoutingFailed e) { + var entityType = e.getEntityType() + .getJavaClassName(); + var signal = e.getHandledSignal(); + var error = e.getError(); + _error().log( + "Signal `%s` with ID `%s` routing to the entity `%s` failed with the error `%s`.%n%s", + signal.getTypeUrl(), Identifier.toString(signal.id()), + entityType, error.getMessage(), error.getStacktrace() + ); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/Server.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/Server.java new file mode 100644 index 00000000..0604d420 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/Server.java @@ -0,0 +1,130 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server; + +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.concurrent.LazyInit; +import io.spine.logging.Logging; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.io.IOException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * A ChatBot application's Spine server. + * + *

Abstracts working with Spine's {@link io.spine.server.Server server}. + */ +public final class Server implements Logging { + + /** + * The name of the GRPC {@link io.spine.server.Server Server}. + */ + private static final String SERVER_NAME = "ChatBotServer"; + + private final ImmutableSet contexts; + + @LazyInit + private io.spine.server.@MonotonicNonNull Server grpcServer; + + private Server(ImmutableSet contexts) { + this.contexts = contexts; + } + + /** + * Creates a new in-process GRPC server with the supplied {@code contexts}. + */ + public static Server withContexts(ContextBuilderAware... contexts) { + checkNotNull(contexts); + checkArgument(contexts.length > 0, "At least a single Bounded Context is required."); + return new Server(ImmutableSet.copyOf(contexts)); + } + + /** + * Returns the name of the server. + */ + public static String name() { + return SERVER_NAME; + } + + /** + * Starts the server. + * + *

Performs {@linkplain #init() initialization} of the server if it was not + * previously initialized. + */ + public void start() { + if (grpcServer == null) { + init(); + } + try { + _config().log("Starting GRPC server."); + grpcServer.start(); + Runtime.getRuntime() + .addShutdownHook(ShutdownHook.newInstance(grpcServer)); + } catch (IOException e) { + throw newIllegalStateException( + e, "Unable to start Spine GRPC server `%s`.", SERVER_NAME + ); + } + } + + /** + * Initializes the server and its {@link ServerEnvironment environment}. + */ + public void init() { + _config().log("Initializing server environment."); + ServerEnvironment.init(); + _config().log("Bootstrapping server."); + io.spine.server.Server.Builder serverBuilder = io.spine.server.Server.inProcess( + SERVER_NAME); + for (ContextBuilderAware contextAware : contexts) { + serverBuilder.add(contextAware.builder()); + } + grpcServer = serverBuilder.build(); + } + + /** + * Gracefully stops the {@link #server}. + */ + private static final class ShutdownHook implements Runnable, Logging { + + private final io.spine.server.Server server; + + private ShutdownHook(io.spine.server.Server server) { + this.server = server; + } + + private static Thread newInstance(io.spine.server.Server server) { + checkNotNull(server); + return new Thread(new ShutdownHook(server)); + } + + @Override + public void run() { + _info().log("Shutting down the GRPC server."); + server.shutdownAndWait(); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/ServerEnvironment.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/ServerEnvironment.java new file mode 100644 index 00000000..d4015503 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/ServerEnvironment.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server; + +import com.google.cloud.datastore.DatastoreOptions; +import io.spine.base.Environment; +import io.spine.base.Production; +import io.spine.base.Tests; +import io.spine.chatbot.delivery.LocalDelivery; +import io.spine.server.storage.StorageFactory; +import io.spine.server.storage.datastore.DatastoreStorageFactory; +import io.spine.server.storage.memory.InMemoryStorageFactory; +import io.spine.server.transport.memory.InMemoryTransportFactory; + +/** + * Initializes the {@link io.spine.server.ServerEnvironment ServerEnvironment}. + * + *

Configures the {@link StorageFactory} depending on the current {@link Environment}. + * Uses the Datastore storage factory for the production mode and in-memory storage for tests. + * + *

Configures the inbox delivery through the Datastore work registry while + * in Production environment, otherwise uses local synchronous delivery. + */ +final class ServerEnvironment { + + /** + * Prevents instantiation of this utility class. + */ + private ServerEnvironment() { + } + + /** + * Initializes {@link io.spine.server.ServerEnvironment ServerEnvironment} for ChatBot. + */ + static void init() { + //TODO:2020-06-21:yuri-sergiichuk: switch to io.spine.chatbot.delivery.DistributedDelivery + // for Production environment after implementing the delivery strategy. + // see https://github.com/SpineEventEngine/chat-bot/issues/5. + io.spine.server.ServerEnvironment + .instance() + .use(InMemoryTransportFactory.newInstance(), Production.class) + .use(LocalDelivery.instance, Production.class) + .use(LocalDelivery.instance, Tests.class) + .use(dsStorageFactory(), Production.class) + .use(InMemoryStorageFactory.newInstance(), Tests.class); + } + + private static DatastoreStorageFactory dsStorageFactory() { + var datastore = DatastoreOptions.getDefaultInstance() + .getService(); + return DatastoreStorageFactory + .newBuilder() + .setDatastore(datastore) + .build(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/GitHubContext.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/GitHubContext.java new file mode 100644 index 00000000..4dbbb951 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/GitHubContext.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.server.ContextBuilderAware; +import io.spine.chatbot.server.DiagnosticEventLogger; +import io.spine.chatbot.travis.TravisClient; +import io.spine.server.BoundedContext; +import io.spine.server.BoundedContextBuilder; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Provides {@link BoundedContextBuilder} for the GitHub context. + */ +public final class GitHubContext implements ContextBuilderAware { + + /** + * The name of the GitHub Context. + */ + static final String GIT_HUB_CONTEXT_NAME = "GitHub"; + + private final BoundedContextBuilder builder; + + private GitHubContext(TravisClient client) { + this.builder = configureBuilder(client); + } + + /** + * Returns the context builder associated with the GitHub context. + */ + @Override + public BoundedContextBuilder builder() { + return this.builder; + } + + private static BoundedContextBuilder configureBuilder(TravisClient client) { + return BoundedContext + .singleTenant(GIT_HUB_CONTEXT_NAME) + .add(OrganizationAggregate.class) + .add(RepositoryAggregate.class) + .add(new OrgReposRepository()) + .add(new SpineOrgInitRepository(client)) + .add(new RepoBuildRepository(client)) + .addEventDispatcher(new DiagnosticEventLogger()); + } + + /** + * Creates a new builder of the GitHub context. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Creates a new GitHub context. + */ + public static GitHubContext newInstance() { + return newBuilder().build(); + } + + /** + * A Builder for configuring GitHub context. + */ + public static final class Builder { + + private TravisClient client; + + /** + * Prevents direct instantiation. + */ + private Builder() { + } + + /** + * Sets Travis CI client to be used within the context. + */ + public Builder setTravis(TravisClient client) { + this.client = checkNotNull(client); + return this; + } + + /** + * Finishes configuration of the context and builds a new instance. + * + *

If the {@link #client} was not explicitly configured, uses the + * {@link TravisClient#newInstance() default} client. + */ + public GitHubContext build() { + if (client == null) { + client = TravisClient.newInstance(); + } + return new GitHubContext(client); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposProjection.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposProjection.java new file mode 100644 index 00000000..435fe6b8 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposProjection.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.organization.OrganizationRepositories; +import io.spine.chatbot.github.organization.event.OrganizationRegistered; +import io.spine.chatbot.github.repository.event.RepositoryRegistered; +import io.spine.core.Subscribe; +import io.spine.server.projection.Projection; + +/** + * Organization repositories projection. + * + *

Repositories are only referenced by their identifiers. + * See {@link io.spine.chatbot.github.repository.Repository Repository} for the details + * on each repository. + */ +final class OrgReposProjection + extends Projection { + + /** + * Registers the organization to watch the repositories for. + */ + @Subscribe + void on(OrganizationRegistered e) { + builder().setOrganization(e.getOrganization()); + } + + /** + * Registers the organization repository. + */ + @Subscribe + void on(RepositoryRegistered e) { + var newRepo = e.getRepository(); + if (!hasRepository(newRepo)) { + builder().addRepository(newRepo); + } + } + + private boolean hasRepository(RepositoryId repo) { + return builder() + .getRepositoryList() + .contains(repo); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposRepository.java new file mode 100644 index 00000000..61302ed6 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrgReposRepository.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.organization.OrganizationRepositories; +import io.spine.chatbot.github.repository.event.RepositoryRegistered; +import io.spine.server.projection.ProjectionRepository; +import io.spine.server.route.EventRouting; + +import static io.spine.protobuf.Messages.isNotDefault; +import static io.spine.server.route.EventRoute.noTargets; +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for {@link OrganizationRepositories}. + */ +final class OrgReposRepository + extends ProjectionRepository { + + @OverridingMethodsMustInvokeSuper + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(RepositoryRegistered.class, (event, context) -> + isNotDefault(event.organization()) + ? withId(event.organization()) + : noTargets()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrganizationAggregate.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrganizationAggregate.java new file mode 100644 index 00000000..62df58d2 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/OrganizationAggregate.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.organization.Organization; +import io.spine.chatbot.github.organization.command.RegisterOrganization; +import io.spine.chatbot.github.organization.event.OrganizationRegistered; +import io.spine.logging.Logging; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.command.Assign; + +/** + * A GitHub organization. + * + *

The ChatBot watches organization resources and the organization is the root entity + * the resources are organized around. + */ +final class OrganizationAggregate + extends Aggregate + implements Logging { + + /** + * Registers a new organization. + */ + @Assign + OrganizationRegistered handle(RegisterOrganization c) { + _info().log("Registering organization `%s`.", idAsString()); + return OrganizationRegistered + .newBuilder() + .setOrganization(c.getId()) + .setHeader(c.getHeader()) + .vBuild(); + } + + @Apply + private void on(OrganizationRegistered e) { + builder().setHeader(e.getHeader()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildProcess.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildProcess.java new file mode 100644 index 00000000..8645ecff --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildProcess.java @@ -0,0 +1,207 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import com.google.common.annotations.VisibleForTesting; +import com.google.errorprone.annotations.concurrent.LazyInit; +import io.spine.base.Time; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.github.repository.build.BuildStateChange; +import io.spine.chatbot.github.repository.build.BuildStateMixin; +import io.spine.chatbot.github.repository.build.Commit; +import io.spine.chatbot.github.repository.build.RepositoryBuild; +import io.spine.chatbot.github.repository.build.command.CheckRepositoryBuild; +import io.spine.chatbot.github.repository.build.event.BuildFailed; +import io.spine.chatbot.github.repository.build.event.BuildRecovered; +import io.spine.chatbot.github.repository.build.event.BuildSucceededAgain; +import io.spine.chatbot.github.repository.build.rejection.NoBuildsFound; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.travis.BuildsQuery; +import io.spine.chatbot.travis.RepoBranchBuildResponse; +import io.spine.chatbot.travis.TravisClient; +import io.spine.logging.Logging; +import io.spine.net.Urls; +import io.spine.server.command.Assign; +import io.spine.server.procman.ProcessManager; +import io.spine.server.tuple.EitherOf3; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import static io.spine.chatbot.github.Slugs.newSlug; +import static io.spine.chatbot.github.Slugs.repoSlug; +import static io.spine.chatbot.net.MoreUrls.travisBuildUrlFor; +import static io.spine.protobuf.Messages.isDefault; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * Verifies a status of a build of a repository. + * + *

Performs repository build checks and acknowledges the state of the repository builds. + * As a result, emits build status events such as: + * + *

    + *
  • {@link BuildFailed} — whenever the build is failed; + *
  • {@link BuildRecovered} — whenever the build state changes from {@code failed} + * to {@code passing}; + *
  • {@link BuildSucceededAgain} — whenever the build state is {@code passing} and was + * {@code passing} previously. + *
+ * + * Or, if the repository builds cannot be retrieved, throws {@link NoBuildsFound} rejection. + */ +final class RepoBuildProcess + extends ProcessManager + implements Logging { + + @LazyInit + private @MonotonicNonNull TravisClient client; + + /** + * Checks the repository build state and propagates the respective events. + * + *

If the repository build state cannot be retrieved, throws {@link NoBuildsFound} rejection. + */ + @Assign + EitherOf3 handle(CheckRepositoryBuild c) + throws NoBuildsFound { + var repo = c.getRepository(); + _info().log("Checking build status for the repository `%s`.", repo.getValue()); + var branchBuild = client.execute(BuildsQuery.forRepo(repoSlug(repo))); + if (isDefault(branchBuild.getLastBuild())) { + _warn().log("No builds found for the repository `%s`.", repo.getValue()); + throw NoBuildsFound + .newBuilder() + .setRepository(repo) + .build(); + } + var build = buildFrom(branchBuild, c.getSpace()); + builder().setWhenLastChecked(Time.currentTime()) + .setBuild(build) + .setCurrentState(build.getState()); + var stateChange = BuildStateChange + .newBuilder() + .setPreviousValue(state().getBuild()) + .setNewValue(build) + .vBuild(); + var result = determineOutcome(repo, stateChange); + return result; + } + + private EitherOf3 + determineOutcome(RepositoryId repo, BuildStateChange stateChange) { + var newBuildState = stateChange.getNewValue(); + var previousBuildState = stateChange.getPreviousValue(); + var stateStatusChange = newBuildState.stateChangeFrom(previousBuildState); + switch (stateStatusChange) { + case FAILED: + return onFailed(repo, stateChange); + case RECOVERED: + return onRecovered(repo, stateChange); + case STABLE: + return onStable(repo, stateChange); + case BSCT_UNKNOWN: + case UNRECOGNIZED: + default: + throw newIllegalStateException( + "Unexpected state status change `%s` for the repository `%s`.", + stateStatusChange, repo.getValue() + ); + } + } + + private EitherOf3 + onStable(RepositoryId repo, BuildStateChange stateChange) { + _info().log("The build for the repository `%s` is stable.", repo.getValue()); + var buildSucceededAgain = BuildSucceededAgain + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + return EitherOf3.withC(buildSucceededAgain); + } + + private EitherOf3 + onRecovered(RepositoryId repo, BuildStateChange stateChange) { + _info().log("The build for the repository `%s` is recovered.", repo.getValue()); + var buildRecovered = BuildRecovered + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + return EitherOf3.withB(buildRecovered); + } + + private EitherOf3 + onFailed(RepositoryId repo, BuildStateChange stateChange) { + var newBuildState = stateChange.getNewValue(); + _info().log("A build for the repository `%s` failed with the status `%s`.", + repo.getValue(), newBuildState.getState()); + var buildFailed = BuildFailed + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + return EitherOf3.withA(buildFailed); + } + + @VisibleForTesting + static Build buildFrom(RepoBranchBuildResponse branchBuild, SpaceId space) { + var branchBuildName = branchBuild.getName(); + var slug = newSlug(branchBuild.getRepository() + .getSlug()); + var build = branchBuild.getLastBuild(); + return Build + .newBuilder() + .setNumber(build.getNumber()) + .setSpace(space) + .setState(BuildStateMixin.buildStateFrom(build.getState())) + .setPreviousState(BuildStateMixin.buildStateFrom(build.getPreviousState())) + .setBranch(branchBuildName) + .setLastCommit(from(build.getCommit())) + .setCreatedBy(build.getCreatedBy() + .getLogin()) + .setRepository(slug) + .setTravisCiUrl(travisBuildUrlFor(slug, build.getId())) + .vBuild(); + } + + private static Commit from(io.spine.chatbot.travis.Commit commit) { + return Commit + .newBuilder() + .setSha(commit.getSha()) + .setMessage(commit.getMessage()) + .setCommittedAt(commit.getCommittedAt()) + .setAuthoredBy(commit.getAuthor() + .getName()) + .setCompareUrl(Urls.create(commit.getCompareUrl())) + .vBuild(); + } + + /** + * Sets {@link #client} to be used during handling of signals. + * + * @implNote the method is intended to be used as part of the entity configuration + * done through the repository + */ + void setClient(TravisClient client) { + this.client = client; + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildRepository.java new file mode 100644 index 00000000..2d658b4a --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepoBuildRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.build.RepositoryBuild; +import io.spine.chatbot.travis.TravisClient; +import io.spine.server.procman.ProcessManagerRepository; + +/** + * The repository for {@link RepoBuildProcess}es. + */ +final class RepoBuildRepository + extends ProcessManagerRepository { + + private final TravisClient client; + + RepoBuildRepository(TravisClient client) { + this.client = client; + } + + @Override + protected void configure(RepoBuildProcess processManager) { + super.configure(processManager); + processManager.setClient(client); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepositoryAggregate.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepositoryAggregate.java new file mode 100644 index 00000000..15cf5639 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/RepositoryAggregate.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.Repository; +import io.spine.chatbot.github.repository.command.RegisterRepository; +import io.spine.chatbot.github.repository.event.RepositoryRegistered; +import io.spine.logging.Logging; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.command.Assign; + +/** + * A GitHub repository. + * + *

The ChatBot watches for the repository build status. + */ +final class RepositoryAggregate extends Aggregate + implements Logging { + + /** + * Registers the repository. + */ + @Assign + RepositoryRegistered handle(RegisterRepository c) { + var repository = c.getId(); + _info().log("Registering repository `%s`.", repository.getValue()); + var result = RepositoryRegistered + .newBuilder() + .setRepository(repository) + .setHeader(c.getHeader()) + .vBuild(); + return result; + } + + @Apply + private void on(RepositoryRegistered e) { + builder().setHeader(e.getHeader()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitProcess.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitProcess.java new file mode 100644 index 00000000..0466824b --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitProcess.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.concurrent.LazyInit; +import io.spine.base.CommandMessage; +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.organization.OrgHeader; +import io.spine.chatbot.github.organization.command.RegisterOrganization; +import io.spine.chatbot.github.organization.init.OrganizationInit; +import io.spine.chatbot.github.repository.RepoHeader; +import io.spine.chatbot.github.repository.command.RegisterRepository; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.event.SpaceRegistered; +import io.spine.chatbot.travis.ReposQuery; +import io.spine.chatbot.travis.Repository; +import io.spine.chatbot.travis.TravisClient; +import io.spine.core.External; +import io.spine.logging.Logging; +import io.spine.net.Urls; +import io.spine.server.command.Command; +import io.spine.server.procman.ProcessManager; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import static io.spine.chatbot.github.GitHubIdentifiers.organization; +import static io.spine.chatbot.github.GitHubIdentifiers.repository; +import static io.spine.chatbot.github.Slugs.newSlug; +import static io.spine.chatbot.github.Slugs.orgSlug; +import static io.spine.chatbot.net.MoreUrls.githubUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisUrlFor; + +/** + * Spine organization init process. + * + *

Registers Spine organization and the watched {@link #WATCHED_REPOS repositories} upon adding + * the ChatBot to the space. + */ +final class SpineOrgInitProcess + extends ProcessManager + implements Logging { + + private static final ImmutableList WATCHED_REPOS = ImmutableList.of( + "base", "time", "core-java", "web", "gcloud-java", "bootstrap", "money", "jdbc-storage" + ); + + /** + * The initialization process ID. + */ + static final OrganizationId ORGANIZATION = organization("SpineEventEngine"); + + @LazyInit + private @MonotonicNonNull TravisClient client; + + /** + * Registers {@link #ORGANIZATION Spine} organization and watched resources that are + * currently available in the Travis CI. + * + *

If a particular repository is not available in Travis, it is then skipped + * and not registered. + */ + @Command + Iterable on(@External SpaceRegistered e) { + if (state().getInitialized()) { + _info().log("Spine organization is already initialized. Skipping the process."); + return ImmutableSet.of(); + } + var space = e.getSpace(); + _info().log("Starting Spine organization initialization process in the space `%s`.", space); + var commands = ImmutableSet.builder(); + commands.add(registerOrgCommand(ORGANIZATION, space)); + client.execute(ReposQuery.forOwner(orgSlug(ORGANIZATION))) + .getRepositoriesList() + .stream() + .filter(repository -> WATCHED_REPOS.contains(repository.getName())) + .map(repository -> registerRepoCommand(repository, ORGANIZATION)) + .forEach(commands::add); + builder().setSpace(space) + .setInitialized(true); + return commands.build(); + } + + private RegisterRepository registerRepoCommand(Repository repo, OrganizationId org) { + var slug = newSlug(repo.getSlug()); + _info().log("Registering `%s` repository.", slug.getValue()); + var header = RepoHeader + .newBuilder() + .setOrganization(org) + .setGithubProfile(githubUrlFor(slug)) + .setName(repo.getName()) + .setTravisProfile(travisUrlFor(slug)) + .vBuild(); + return RegisterRepository + .newBuilder() + .setId(repository(slug.getValue())) + .setHeader(header) + .vBuild(); + } + + private RegisterOrganization registerOrgCommand(OrganizationId spineOrg, SpaceId space) { + var slug = orgSlug(spineOrg); + _info().log("Registering `%s` organization.", spineOrg.getValue()); + var header = OrgHeader + .newBuilder() + .setName("Spine Event Engine") + .setWebsite(Urls.create("https://spine.io/")) + .setTravisProfile(travisUrlFor(slug)) + .setGithubProfile(githubUrlFor(slug)) + .setSpace(space) + .vBuild(); + return RegisterOrganization + .newBuilder() + .setId(spineOrg) + .setHeader(header) + .vBuild(); + } + + /** + * Sets {@link #client} to be used during handling of signals. + * + * @implNote the method is intended to be used as part of the entity configuration + * done through the repository + */ + void setClient(TravisClient client) { + this.client = client; + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitRepository.java new file mode 100644 index 00000000..4685c839 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/SpineOrgInitRepository.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.organization.init.OrganizationInit; +import io.spine.chatbot.google.chat.event.SpaceRegistered; +import io.spine.chatbot.travis.TravisClient; +import io.spine.server.procman.ProcessManagerRepository; +import io.spine.server.route.EventRouting; + +import static io.spine.chatbot.server.github.SpineOrgInitProcess.ORGANIZATION; +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for {@link SpineOrgInitProcess}. + */ +final class SpineOrgInitRepository + extends ProcessManagerRepository { + + private final TravisClient client; + + SpineOrgInitRepository(TravisClient client) { + this.client = client; + } + + @OverridingMethodsMustInvokeSuper + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(SpaceRegistered.class, (event, context) -> withId(ORGANIZATION)); + } + + @Override + protected void configure(SpineOrgInitProcess processManager) { + super.configure(processManager); + processManager.setClient(client); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/github/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/package-info.java new file mode 100644 index 00000000..91fd9c98 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/github/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains server-side implementation of the GitHub Context. + */ +@BoundedContext(GitHubContext.GIT_HUB_CONTEXT_NAME) +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.server.github; + +import com.google.errorprone.annotations.CheckReturnValue; +import io.spine.core.BoundedContext; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ChatEvents.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ChatEvents.java new file mode 100644 index 00000000..023b684e --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ChatEvents.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.incoming.ChatEvent; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import io.spine.chatbot.google.chat.incoming.event.BotRemovedFromSpace; +import io.spine.chatbot.google.chat.incoming.event.MessageReceived; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; + +/** + * A utility for working with {@link ChatEvent}s. + */ +final class ChatEvents { + + /** + * Prevents instantiation of this utility class. + */ + private ChatEvents() { + } + + /** + * Creates a new {@code BotRemovedFromSpace} message out of the supplied {@code event}. + */ + static BotRemovedFromSpace toBotRemovedFromSpace(ChatEvent event) { + checkNotNull(event); + var space = event.getSpace(); + return BotRemovedFromSpace + .newBuilder() + .setEvent(event) + .setSpace(space.id()) + .vBuild(); + } + + /** + * Creates a new {@code BotAddedToSpace} message out of the supplied {@code event}. + */ + static BotAddedToSpace toBotAddedToSpace(ChatEvent event) { + checkNotNull(event); + var space = event.getSpace(); + return BotAddedToSpace + .newBuilder() + .setEvent(event) + .setSpace(space.id()) + .vBuild(); + } + + /** + * Creates a new {@code MessageReceived} message out of the supplied {@code event}. + */ + static MessageReceived toMessageReceived(ChatEvent event) { + checkNotNull(event); + var message = event.getMessage(); + return MessageReceived + .newBuilder() + .setEvent(event) + .setMessage(message(message.getName())) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/GoogleChatContext.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/GoogleChatContext.java new file mode 100644 index 00000000..af96266b --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/GoogleChatContext.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.GoogleChatClient; +import io.spine.chatbot.server.ContextBuilderAware; +import io.spine.chatbot.server.DiagnosticEventLogger; +import io.spine.server.BoundedContext; +import io.spine.server.BoundedContextBuilder; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Provides {@link BoundedContextBuilder} for the Google Chat context. + */ +public final class GoogleChatContext implements ContextBuilderAware { + + /** + * The name of the Google Chat Context. + */ + static final String GOOGLE_CHAT_CONTEXT_NAME = "GoogleChat"; + + private final BoundedContextBuilder builder; + + private GoogleChatContext(GoogleChatClient client) { + this.builder = configureBuilder(client); + } + + /** + * Returns the context builder associated with the Google Chat context. + */ + @Override + public BoundedContextBuilder builder() { + return this.builder; + } + + /** + * Creates a new instance of the Google Chat context builder. + */ + private static BoundedContextBuilder + configureBuilder(GoogleChatClient client) { + return BoundedContext + .singleTenant(GOOGLE_CHAT_CONTEXT_NAME) + .add(new SpaceRepository()) + .add(new ThreadRepository()) + .add(new ThreadChatRepository(client)) + .addEventDispatcher(new IncomingEventsHandler()) + .addEventDispatcher(new DiagnosticEventLogger()); + } + + /** + * Creates a new builder of the Google Chat context. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Creates a new Google Chat context. + */ + public static GoogleChatContext newInstance() { + return newBuilder().build(); + } + + /** + * A Builder for configuring Google Chat context. + */ + public static final class Builder { + + private GoogleChatClient client; + + /** + * Prevents direct instantiation. + */ + private Builder() { + } + + /** + * Sets Google Chat client to be used within the context. + */ + public Builder setClient(GoogleChatClient client) { + this.client = checkNotNull(client); + return this; + } + + /** + * Finishes configuration of the context and builds a new instance. + */ + public GoogleChatContext build() { + if (client == null) { + client = GoogleChatClient.newInstance(); + } + return new GoogleChatContext(client); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/IncomingEventsHandler.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/IncomingEventsHandler.java new file mode 100644 index 00000000..d5471d56 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/IncomingEventsHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.incoming.ChatEvent; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import io.spine.chatbot.google.chat.incoming.event.BotRemovedFromSpace; +import io.spine.chatbot.google.chat.incoming.event.ChatEventReceived; +import io.spine.chatbot.google.chat.incoming.event.MessageReceived; +import io.spine.core.External; +import io.spine.logging.Logging; +import io.spine.server.event.AbstractEventReactor; +import io.spine.server.event.React; +import io.spine.server.model.Nothing; +import io.spine.server.tuple.EitherOf4; + +import static io.spine.chatbot.server.google.chat.ChatEvents.toBotAddedToSpace; +import static io.spine.chatbot.server.google.chat.ChatEvents.toBotRemovedFromSpace; +import static io.spine.chatbot.server.google.chat.ChatEvents.toMessageReceived; + +/** + * Processes incoming {@link ChatEvent} messages and emits one of the following domain events: + * + *

    + *
  • {@link BotAddedToSpace} — the ChatBot is added to a space; + *
  • {@link BotRemovedFromSpace} — the ChatBot is removed from the space; + *
  • {@link MessageReceived} — the ChatBot received a new incoming message from a user + * within a space. + *
+ * + *

If the bot receives a chat event with a not supported currently event type, + * {@link Nothing} is emitted. + */ +final class IncomingEventsHandler extends AbstractEventReactor implements Logging { + + /** + * Processes an incoming external {@link ChatEvent}. + * + *

If the event type is not supported, returns {@link #nothing() nothing}. + */ + @React + EitherOf4 + on(@External ChatEventReceived e) { + var chatEvent = e.getEvent(); + var space = chatEvent.getSpace(); + switch (chatEvent.getType()) { + case MESSAGE: + _debug().log("A new user message received in the space `%s` (%s).", + space.getDisplayName(), space.getName()); + return EitherOf4.withC(toMessageReceived(chatEvent)); + case ADDED_TO_SPACE: + _info().log("ChatBot added to the space `%s` (%s).", + space.getDisplayName(), space.getName()); + return EitherOf4.withA(toBotAddedToSpace(chatEvent)); + case REMOVED_FROM_SPACE: + _info().log("ChatBot removed from the space `%s` (%s).", + space.getDisplayName(), space.getName()); + return EitherOf4.withB(toBotRemovedFromSpace(chatEvent)); + case CARD_CLICKED: + _debug().log("Skipping card clicks."); + return EitherOf4.withD(nothing()); + case UNRECOGNIZED: + case ET_UNKNOWN: + default: + _error().log("Unsupported chat event type received: `%s`.", chatEvent.getType()); + return EitherOf4.withD(nothing()); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceAggregate.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceAggregate.java new file mode 100644 index 00000000..41b0d8fa --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceAggregate.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.Space; +import io.spine.chatbot.google.chat.SpaceHeader; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.command.RegisterSpace; +import io.spine.chatbot.google.chat.event.SpaceRegistered; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import io.spine.logging.Logging; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.command.Assign; +import io.spine.server.event.React; + +/** + * A room or direct message chat in the Google Chat. + * + *

Whenever the ChatBot is added to the space, the space is registered in the context. + */ +final class SpaceAggregate extends Aggregate implements Logging { + + /** + * Registers a new space when the ChatBot is added to the space. + */ + @React + SpaceRegistered on(BotAddedToSpace e) { + var space = e.getEvent() + .getSpace(); + var displayName = space.getDisplayName(); + var spaceId = e.getSpace(); + _info().log("Registering the space `%s` (`%s`) because the bot is added the space.", + displayName, spaceId.getValue()); + var header = SpaceHeader + .newBuilder() + .setDisplayName(displayName) + .setThreaded(space.isThreaded()) + .vBuild(); + return SpaceRegistered + .newBuilder() + .setSpace(spaceId) + .setHeader(header) + .vBuild(); + } + + /** + * Registers the space in the context. + */ + @Assign + SpaceRegistered handle(RegisterSpace c) { + var header = c.getHeader(); + var space = c.getId(); + _info().log("Registering the space `%s` (`%s`) on direct registration request.", + header.getDisplayName(), space.getValue()); + var result = SpaceRegistered + .newBuilder() + .setSpace(space) + .setHeader(header) + .vBuild(); + return result; + } + + @Apply + private void on(SpaceRegistered e) { + builder().setHeader(e.getHeader()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceRepository.java new file mode 100644 index 00000000..53422983 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/SpaceRepository.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import io.spine.server.aggregate.AggregateRepository; +import io.spine.server.route.EventRouting; + +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for {@link SpaceAggregate}s. + */ +final class SpaceRepository extends AggregateRepository { + + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(BotAddedToSpace.class, (event, context) -> withId(event.getSpace())); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadAggregate.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadAggregate.java new file mode 100644 index 00000000..91f22811 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadAggregate.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.event.MessageCreated; +import io.spine.chatbot.google.chat.event.ThreadCreated; +import io.spine.chatbot.google.chat.thread.Thread; +import io.spine.chatbot.google.chat.thread.event.MessageAdded; +import io.spine.chatbot.google.chat.thread.event.ThreadInitialized; +import io.spine.logging.Logging; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.event.React; + +/** + * A thread in a chat room. + * + *

A new thread is initialized as early as a new conversation is started in the room. + * It happens once the first message is posted to the conversation. + */ +final class ThreadAggregate extends Aggregate implements Logging { + + /** + * Initializes the thread information upon the creation of the thread. + */ + @React + ThreadInitialized on(ThreadCreated e) { + _info().log("A new thread `%s` created.", idAsString()); + return ThreadInitialized + .newBuilder() + .setThread(e.getThread()) + .setResource(e.getResource()) + .setSpace(e.getSpace()) + .vBuild(); + } + + @Apply + private void on(ThreadInitialized e) { + builder().setResource(e.getResource()) + .setSpace(e.getSpace()); + } + + /** + * Acknowledges creation of a new thread message. + */ + @React + MessageAdded on(MessageCreated e) { + var message = e.getMessage(); + var thread = e.getThread(); + _info().log("A new message `%s` added to the thread `%s`.", + message.getValue(), thread.getValue()); + return MessageAdded + .newBuilder() + .setMessage(message) + .setThread(thread) + .vBuild(); + } + + @Apply + private void on(MessageAdded e) { + builder().addMessage(e.getMessage()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatProcess.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatProcess.java new file mode 100644 index 00000000..94e07727 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatProcess.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import com.google.errorprone.annotations.concurrent.LazyInit; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.github.repository.build.event.BuildFailed; +import io.spine.chatbot.github.repository.build.event.BuildRecovered; +import io.spine.chatbot.google.chat.GoogleChatClient; +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.event.MessageCreated; +import io.spine.chatbot.google.chat.event.ThreadCreated; +import io.spine.chatbot.google.chat.thread.ThreadChat; +import io.spine.core.External; +import io.spine.logging.Logging; +import io.spine.protobuf.Messages; +import io.spine.server.event.React; +import io.spine.server.procman.ProcessManager; +import io.spine.server.tuple.Pair; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.util.Optional; + +/** + * A process of notifying thread members about the changes in the watched resouces. + */ +final class ThreadChatProcess extends ProcessManager + implements Logging { + + @LazyInit + private @MonotonicNonNull GoogleChatClient client; + + /** + * Notifies thread members about a failed CI build. + */ + @React + Pair> on(@External BuildFailed e) { + var change = e.getChange(); + var build = change.getNewValue(); + var repo = e.getRepository(); + _info().log("A build for the repository `%s` failed.", repo.getValue()); + return processBuildStateUpdate(build, repo); + } + + /** + * Notifies thread members about a recovered CI build. + * + *

The build is considered a recovered when it changes its state from + * {@code failed} to {@code passing}. + */ + @React + Pair> on(@External BuildRecovered e) { + var change = e.getChange(); + var build = change.getNewValue(); + var repo = e.getRepository(); + _info().log("A build for the repository `%s` recovered.", repo.getValue()); + return processBuildStateUpdate(build, repo); + } + + private Pair> + processBuildStateUpdate(Build build, RepositoryId repo) { + var sentUpdate = client.sendBuildStateUpdate(build, state().getResource()); + var space = sentUpdate.getSpace(); + var thread = sentUpdate.getThread(); + var messageCreated = MessageCreated + .newBuilder() + .setMessage(sentUpdate.getMessage()) + .setSpace(space) + .setThread(thread) + .vBuild(); + if (shouldCreateThread()) { + var resource = sentUpdate.getResource(); + _debug().log("A new thread `%s` created for the repository `%s`.", + resource.getName(), repo.getValue()); + builder().setResource(resource) + .setSpace(space); + var threadCreated = ThreadCreated + .newBuilder() + .setThread(thread) + .setResource(resource) + .setSpace(space) + .vBuild(); + return Pair.withNullable(messageCreated, threadCreated); + } + return Pair.withNullable(messageCreated, null); + } + + private boolean shouldCreateThread() { + return Messages.isDefault(state().getResource()); + } + + /** + * Sets {@link #client} to be used during handling of signals. + * + * @implNote the method is intended to be used as part of the entity configuration + * done through the repository + */ + void setClient(GoogleChatClient client) { + this.client = client; + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatRepository.java new file mode 100644 index 00000000..92bac486 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadChatRepository.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.chatbot.github.repository.RepositoryAwareEvent; +import io.spine.chatbot.github.repository.build.event.BuildFailed; +import io.spine.chatbot.github.repository.build.event.BuildRecovered; +import io.spine.chatbot.google.chat.GoogleChatClient; +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.thread.ThreadChat; +import io.spine.core.EventContext; +import io.spine.server.procman.ProcessManagerRepository; +import io.spine.server.route.EventRoute; +import io.spine.server.route.EventRouting; + +import java.util.Set; + +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.thread; +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for {@link ThreadChatProcess}es. + */ +final class ThreadChatRepository + extends ProcessManagerRepository { + + private final GoogleChatClient client; + + ThreadChatRepository(GoogleChatClient client) { + this.client = client; + } + + @OverridingMethodsMustInvokeSuper + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(BuildFailed.class, new RepositoryEventRoute<>()) + .route(BuildRecovered.class, new RepositoryEventRoute<>()); + } + + @Override + protected void configure(ThreadChatProcess processManager) { + processManager.setClient(client); + } + + private static class RepositoryEventRoute + implements EventRoute { + + private static final long serialVersionUID = 0L; + + @Override + public Set apply(M event, EventContext context) { + var repository = event.repository(); + return withId(thread(repository.getValue())); + } + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadRepository.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadRepository.java new file mode 100644 index 00000000..50665445 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.event.MessageCreated; +import io.spine.chatbot.google.chat.event.ThreadCreated; +import io.spine.server.aggregate.AggregateRepository; +import io.spine.server.route.EventRouting; + +import static io.spine.server.route.EventRoute.withId; + +/** + * The repository for {@link ThreadAggregate}s. + */ +final class ThreadRepository extends AggregateRepository { + + @Override + protected void setupEventRouting(EventRouting routing) { + super.setupEventRouting(routing); + routing.route(ThreadCreated.class, (event, context) -> withId(event.getThread())) + .route(MessageCreated.class, (event, context) -> withId(event.getThread())); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadResources.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadResources.java new file mode 100644 index 00000000..e3754057 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/ThreadResources.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.thread.ThreadResource; + +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A utility for working with {@link ThreadResource}s. + */ +public final class ThreadResources { + + /** + * Prevents instantiation of this utility class. + */ + private ThreadResources() { + } + + /** + * Creates a new {@code ThreadResource} with the specified {@code name}. + */ + public static ThreadResource threadResource(String name) { + checkNotEmptyOrBlank(name); + return ThreadResource + .newBuilder() + .setName(name) + .vBuild(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/package-info.java new file mode 100644 index 00000000..ea2b8728 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/google/chat/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains server-side implementation of the Google Chat Context. + */ +@BoundedContext(GoogleChatContext.GOOGLE_CHAT_CONTEXT_NAME) +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.server.google.chat; + +import com.google.errorprone.annotations.CheckReturnValue; +import io.spine.core.BoundedContext; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/server/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/server/package-info.java new file mode 100644 index 00000000..bf59b25b --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/server/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains the ChatBot server. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.server; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/BuildsQuery.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/BuildsQuery.java new file mode 100644 index 00000000..4ffeeb7e --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/BuildsQuery.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import io.spine.chatbot.github.Slug; + +/** + * A branch builds query to the Travis CI API. + * + * @see Finding the branch build + */ +public final class BuildsQuery extends Query { + + private BuildsQuery(String request) { + super(request, RepoBranchBuildResponse.class); + } + + /** + * Creates a query for the {@code repository}. + * + *

Requests the latest build from the {@code master} branch. + */ + public static BuildsQuery forRepo(Slug repo) { + var encodedSlug = repo.encodedValue(); + var request = "/repo/" + + encodedSlug + + "/branch/master?&include=build.commit,build.created_by"; + return new BuildsQuery(request); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/JsonProtoBodyHandler.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/JsonProtoBodyHandler.java new file mode 100644 index 00000000..9ae2667e --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/JsonProtoBodyHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import com.google.protobuf.Message; +import io.spine.json.Json; + +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.charset.StandardCharsets; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Converts the incoming JSON strings into Protobuf messages relying on the Spine + * {@linkplain Json conversion functionality}. + * + * @param + * the Protobuf message supplied in the response body + */ +final class JsonProtoBodyHandler implements HttpResponse.BodyHandler { + + private final Class type; + + private JsonProtoBodyHandler(Class type) { + this.type = type; + } + + /** + * Creates a body handler for a specified Protobuf message. + */ + static JsonProtoBodyHandler jsonBodyHandler(Class type) { + checkNotNull(type); + return new JsonProtoBodyHandler<>(type); + } + + @Override + public HttpResponse.BodySubscriber apply(ResponseInfo response) { + return BodySubscribers.mapping(BodySubscribers.ofString(StandardCharsets.UTF_8), + this::parseJson); + } + + private T parseJson(String json) { + return Json.fromJson(json, type); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/Query.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Query.java new file mode 100644 index 00000000..d197acce --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Query.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A query to the Travis CI API. + * + * @param + * type of the expected query execution response + */ +abstract class Query { + + private final Class responseType; + private final String request; + + /** + * Creates a new API query with the specified {@code request}. + */ + Query(String request, Class responseType) { + this.request = checkNotNull(request); + this.responseType = checkNotNull(responseType); + } + + /** + * Returns the request URL to the REST endpoint. + */ + final String request() { + return request; + } + + /** + * Returns query response type. + */ + final Class responseType() { + return responseType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Query)) { + return false; + } + Query query = (Query) o; + return Objects.equal(responseType, query.responseType) && + Objects.equal(request, query.request); + } + + @Override + public int hashCode() { + return Objects.hashCode(responseType, request); + } + + @Override + public String toString() { + return MoreObjects + .toStringHelper(this) + .add("request", request) + .add("responseType", responseType) + .toString(); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/ReposQuery.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/ReposQuery.java new file mode 100644 index 00000000..8cf5513d --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/ReposQuery.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import io.spine.chatbot.github.Slug; + +/** + * A repositories query to the Travis CI API. + * + * @see + * Repos for owner + */ +public final class ReposQuery extends Query { + + private ReposQuery(String request) { + super(request, RepositoriesResponse.class); + } + + /** + * Creates a repository query for repositories of the specified {@code owner} + * (either a user or an organization). + */ + public static ReposQuery forOwner(Slug owner) { + var encodedOwner = owner.encodedValue(); + var request = "/owner/" + encodedOwner + "/repos"; + return new ReposQuery(request); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/Token.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Token.java new file mode 100644 index 00000000..236b0df8 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Token.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import io.spine.chatbot.google.secret.Secret; + +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; + +/** + * A Travis CI API access token. + */ +final class Token extends Secret { + + private static final String TRAVIS_API_TOKEN = "TravisApiToken"; + + private final String value; + + private Token(String value) { + this.value = value; + } + + /** + * Returns the token value. + */ + String value() { + return value; + } + + /** + * Creates the Travis CI API access token. + */ + static Token privateToken() { + var value = checkNotEmptyOrBlank(retrieveSecret(TRAVIS_API_TOKEN)); + return new Token(value); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/Travis.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Travis.java new file mode 100644 index 00000000..4fe36729 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/Travis.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import io.spine.logging.Logging; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; + +import static com.google.api.client.util.Preconditions.checkNotNull; +import static io.spine.chatbot.travis.JsonProtoBodyHandler.jsonBodyHandler; +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * A client to the Travis CI REST API. + * + * @see Travis CI API + */ +final class Travis implements TravisClient, Logging { + + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + private static final String BASE_URL = "https://api.travis-ci.com"; + private static final String API_HEADER = "Travis-API-Version"; + private static final String API_VERSION = "3"; + private static final String AUTH_HEADER = "Authorization"; + + private final Token apiToken; + + /** + * Creates a new Travis client with the specified API token. + */ + Travis(Token apiToken) { + this.apiToken = checkNotNull(apiToken); + } + + @Override + public T execute(Query query) { + var result = execute(query.request(), query.responseType()); + return result; + } + + private T execute(String request, Class responseType) { + var apiRequest = apiRequest(request, apiToken); + try { + _trace().log("Executing Travis API request `%s` for response `%s`.", + request, responseType.getSimpleName()); + var result = CLIENT.send(apiRequest, jsonBodyHandler(responseType)); + return result.body(); + } catch (IOException | InterruptedException e) { + throw newIllegalStateException( + e, "Unable to query data for response of type '%s' using request '%s'.", + responseType, request + ); + } + } + + private static HttpRequest apiRequest(String request, Token token) { + return authorizedApiRequest(token) + .uri(URI.create(BASE_URL + request)) + .build(); + } + + private static HttpRequest.Builder authorizedApiRequest(Token token) { + return HttpRequest + .newBuilder() + .GET() + .header(API_HEADER, API_VERSION) + .header(AUTH_HEADER, "token " + token.value()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisClient.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisClient.java new file mode 100644 index 00000000..dcd181be --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisClient.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import static io.spine.chatbot.travis.Token.privateToken; + +/** + * A Travis CI API client. + * + * @see Travis CI API + */ +public interface TravisClient { + + /** + * Executes the supplied {@code query} and returns the response of type {@code T}. + * + * @param query + * query to execute + * @param + * type of the query response + * @return query execution result + */ + T execute(Query query); + + /** + * Creates a new Travis client with the default Travis token. + */ + static TravisClient newInstance() { + return new Travis(privateToken()); + } +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisResponse.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisResponse.java new file mode 100644 index 00000000..e4a1e665 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/TravisResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import com.google.protobuf.Message; + +/** + * Travis CI API response marker. + */ +public interface TravisResponse extends Message { +} diff --git a/google-chat-bot/src/main/java/io/spine/chatbot/travis/package-info.java b/google-chat-bot/src/main/java/io/spine/chatbot/travis/package-info.java new file mode 100644 index 00000000..b6a0bb63 --- /dev/null +++ b/google-chat-bot/src/main/java/io/spine/chatbot/travis/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains Travis CI v3 API client. + * + *

The travis itself does not provide an idiomatic Java client, so the API contains only + * specific required functionality. + * + * @see Travis CI API + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.chatbot.travis; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/identifiers.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/identifiers.proto new file mode 100644 index 00000000..9b28ac42 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/identifiers.proto @@ -0,0 +1,48 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github"; +option java_outer_classname = "IdentifiersProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +// GitHub organization ID. +message OrganizationId { + + // The name of the GitHub organization. + string value = 1 [(required) = true]; +} + +// GitHub repository ID. +message RepositoryId { + + // The repository slug. + // + // E.g. `SpineEventEngine/base`. The slug is used as a human-friendly unique identifier. + // + string value = 1 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization.proto new file mode 100644 index 00000000..26877789 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization.proto @@ -0,0 +1,66 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization"; +option java_outer_classname = "OrganizationProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/net/url.proto"; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/google/chat/identifiers.proto"; + +// A GitHub organization. +message Organization { + option (entity) = {kind: AGGREGATE visibility: QUERY}; + option (is).java_type = "OrgHeaderAware"; + + OrganizationId id = 1; + + // The organization header. + OrgHeader header = 2; +} + +// The GitHub organization header. +message OrgHeader { + + // The name of the GitHub organization. + string name = 1 [(required) = true]; + + // The URL of the official organization-related website. + spine.net.Url website = 2; + + // The URL of the organization GitHub profile. + spine.net.Url github_profile = 3 [(required) = true]; + + // The URL of the organization Travis CI profile. + spine.net.Url travis_profile = 4 [(required) = true]; + + // The Google Chat space associated with the organization. + google.chat.SpaceId space = 5 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization_commands.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_commands.proto new file mode 100644 index 00000000..85ce7644 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_commands.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization.command"; +option java_outer_classname = "OrganizationCommandsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/organization.proto"; + +// A request to register an organization. +message RegisterOrganization { + option (is).java_type = "io.spine.chatbot.github.organization.OrgHeaderAware"; + + OrganizationId id = 1 [(required) = true]; + + // The organization header. + OrgHeader header = 2 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_events.proto new file mode 100644 index 00000000..4a62e9d5 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_events.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization.event"; +option java_outer_classname = "OrganizationEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/organization.proto"; + +// An organization is registered. +message OrganizationRegistered { + option (is).java_type = "io.spine.chatbot.github.organization.OrgHeaderAware"; + + OrganizationId organization = 1 [(required) = true]; + + // The organization header. + OrgHeader header = 2 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init.proto new file mode 100644 index 00000000..ca5d4f2b --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init.proto @@ -0,0 +1,52 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization.init"; +option java_outer_classname = "OrganizationInitProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/google/chat/identifiers.proto"; + +// The initialization process of the default watched organization resources. +// +// Ensures that watched resources organization and its repositories are initialized +// for a particular Chat space. +// +message OrganizationInit { + option (entity) = {kind: PROCESS_MANAGER visibility: NONE}; + + // The organization for which the initialization is performed. + OrganizationId organization = 1; + + // The Google Chat space associated with the organization. + google.chat.SpaceId space = 2; + + // Determines whether the organization resources are already initialized. + bool initialized = 3; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init_commands.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init_commands.proto new file mode 100644 index 00000000..c9406565 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_init_commands.proto @@ -0,0 +1,47 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization.init.command"; +option java_outer_classname = "OrganizationInitCommandsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/google/chat/identifiers.proto"; + +// A request to initialize watched organization resources. +// +// Registers the organization itself and the related repositories to be watched by the ChatBot. +// +message InitializeOrganization { + + // The organization to perform initialization for. + OrganizationId organization = 1 [(required) = true]; + + // The Google Chat space associated with the organization. + google.chat.SpaceId space = 2 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/organization_repositories.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_repositories.proto new file mode 100644 index 00000000..0f366192 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/organization_repositories.proto @@ -0,0 +1,43 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.organization"; +option java_outer_classname = "OrganizationRepositoriesProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; + +// Lists repositories of an organization. +message OrganizationRepositories { + option (entity) = {kind: PROJECTION visibility: FULL}; + + OrganizationId organization = 1; + + // Linked organization repositories. + repeated RepositoryId repository = 2; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository.proto new file mode 100644 index 00000000..119e5015 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository.proto @@ -0,0 +1,61 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository"; +option java_outer_classname = "RepositoryProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/net/url.proto"; +import "spine/chatbot/github/identifiers.proto"; + +// A GitHub repository. +message Repository { + option (entity) = {kind: AGGREGATE visibility: NONE}; + option (is).java_type = "RepoHeaderAware"; + + RepositoryId id = 1; + + // The repository header. + RepoHeader header = 2; +} + +// The GitHub repository header. +message RepoHeader { + + // The name of the repository. + string name = 1 [(required) = true]; + + // The URL of the repository GitHub profile. + spine.net.Url github_profile = 2 [(required) = true]; + + // The URL of the Travis CI profile. + spine.net.Url travis_profile = 3 [(required) = true]; + + // The organization repository is related to, if any. + OrganizationId organization = 4; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build.proto new file mode 100644 index 00000000..c69ffbb6 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build.proto @@ -0,0 +1,166 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.build"; +option java_outer_classname = "RepositoryBuildProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + + +import "google/protobuf/timestamp.proto"; + +import "spine/net/url.proto"; +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/slug.proto"; +import "spine/chatbot/google/chat/identifiers.proto"; + +// A build process of a GitHub repository. +message RepositoryBuild { + option (entity) = {kind: PROCESS_MANAGER visibility: NONE}; + + RepositoryId repository = 1; + + // The time of the last build status check. + .google.protobuf.Timestamp when_last_checked = 2; + + // The current build. + Build build = 3; + + // The current repository state. + Build.State current_state = 4 [(column) = true]; +} + +// A repository branch build. +message Build { + option (is).java_type = "BuildStateMixin"; + + // Incremental number for a repository's builds. + string number = 1; + + // Travis CI URL of the build. + spine.net.Url travis_ci_url = 2; + + // Current state of the build. + State state = 3; + + // State of the previous build. + State previous_state = 4; + + // The build state. + enum State { + BS_UNKNOWN = 0; + + // The build is created. + CREATED = 1; + + // The build is received by the CI. + RECEIVED = 2; + + // The build is in progress. + STARTED = 3; + + // The build has passed successfully. + PASSED = 4; + + // The build has failed. + // + // Denotes that the developer code could not be successfully built. + // + FAILED = 5; + + // The build configuration has failed. + // + // Denotes that the build configuration or pre-build steps failed. + // + ERRORED = 6; + + // The build is cancelled. + CANCELLED = 7; + } + + // The branch the build is associated with. + string branch = 5; + + // The commit the build is associated with. + Commit last_commit = 6; + + // The User or Organization that created the build. + string created_by = 7; + + // The repository slug the build is associated with. + Slug repository = 8; + + // The Google Chat space associated with the organization. + google.chat.SpaceId space = 9 [(required) = true, (validate) = true]; +} + +// A git commit. +message Commit { + + // Checksum the commit has in git and is identified by. + string sha = 1; + + // Commit message. + string message = 2; + + // URL to the commit's diff on GitHub. + spine.net.Url compare_url = 3; + + // Commit date from git. + string committed_at = 4; + + // Commit author from git. + string authored_by = 5; +} + +// Definition of a change in a `build` field. +message BuildStateChange { + + // The value of the field that's changing. + Build previous_value = 1; + + // The new value of the field. + Build new_value = 2 [(required) = true]; + + // The type of the build state change. + enum Type { + + BSCT_UNKNOWN = 0; + + // The build has failed. + // + // It could be either a build configuration failure or the actual code build issue. + // + FAILED = 1; + + // The build has recovered from the failed state. + RECOVERED = 2; + + // The build is stable. + STABLE = 3; + } +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_commands.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_commands.proto new file mode 100644 index 00000000..a2658b3b --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_commands.proto @@ -0,0 +1,47 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.build.command"; +option java_outer_classname = "RepositoryBuildCommandsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/google/chat/identifiers.proto"; + +// Check repository CI build state command. +message CheckRepositoryBuild { + + // The repository to perform a check for. + RepositoryId repository = 1 [(required) = true]; + + // The organization the repository belongs to. + OrganizationId organization = 2 [(required) = true]; + + // The Google Chat space associated with the organization. + google.chat.SpaceId space = 3 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_events.proto new file mode 100644 index 00000000..9a904d54 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_events.proto @@ -0,0 +1,69 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.build.event"; +option java_outer_classname = "RepositoryBuildEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +option (every_is).java_type = "io.spine.chatbot.github.repository.RepositoryAwareEvent"; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/repository_build.proto"; + +// The build has failed for a repository. +message BuildFailed { + + RepositoryId repository = 1 [(required) = true]; + + // The change of the build state. + BuildStateChange change = 2 [(required) = true]; +} + +// The build has recovered from the failed state. +message BuildRecovered { + + RepositoryId repository = 1 [(required) = true]; + + // The change of the build state. + BuildStateChange change = 2 [(required) = true]; +} + +// The build is stable and passing. +message BuildSucceededAgain { + + RepositoryId repository = 1 [(required) = true]; + + // The change of the build state. + BuildStateChange change = 2 [(required) = true]; +} + +// The build is stable and passing. +message BuildInProgress { + + RepositoryId repository = 1 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_rejections.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_rejections.proto new file mode 100644 index 00000000..77a20878 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_build_rejections.proto @@ -0,0 +1,40 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.build.rejection"; +option java_multiple_files = false; +option java_generate_equals_and_hash = true; + +option (every_is).java_type = "io.spine.chatbot.github.repository.RepositoryAwareEvent"; + +import "spine/chatbot/github/identifiers.proto"; + +// No CI builds found for the repository. +message NoBuildsFound { + + RepositoryId repository = 1 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_commands.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_commands.proto new file mode 100644 index 00000000..7ee15b4e --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_commands.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.command"; +option java_outer_classname = "RepositoryCommandsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/repository.proto"; + +// A request to register a repository. +message RegisterRepository { + option (is).java_type = "io.spine.chatbot.github.repository.RepoHeaderAware"; + + RepositoryId id = 1 [(required) = true]; + + // The repository header. + RepoHeader header = 2 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/repository_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_events.proto new file mode 100644 index 00000000..d0a012c7 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/repository_events.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github.repository.event"; +option java_outer_classname = "RepositoryEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/github/identifiers.proto"; +import "spine/chatbot/github/repository.proto"; + +// A repository is registered. +message RepositoryRegistered { + option (is).java_type = "io.spine.chatbot.github.repository.RepoHeaderAware"; + + RepositoryId repository = 1 [(required) = true]; + + // The repository header. + RepoHeader header = 2 [(required) = true, (validate) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/github/slug.proto b/google-chat-bot/src/main/proto/spine/chatbot/github/slug.proto new file mode 100644 index 00000000..9a2b2e1e --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/github/slug.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package spine.chatbot.github; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.github"; +option java_outer_classname = "SlugProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +// A unique human-readable identifier. +message Slug { + option (is).java_type = "SlugMixin"; + + // The slug value. + // + // E.g. it could be a GitHub repository slug in the format `SpineEventEngine/base`. + // + string value = 1 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat.proto new file mode 100644 index 00000000..be42b776 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat.proto @@ -0,0 +1,50 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat"; +option java_outer_classname = "ChatProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/thread.proto"; + +// A build state update message that was sent to the Chat. +message BuildStateUpdate { + + // The message that was sent to the chat. + MessageId message = 1 [(required) = true]; + + // The space to which the message was sent. + SpaceId space = 2 [(required) = true]; + + // The thread to which the message was sent. + ThreadId thread = 3 [(required) = true]; + + // The resource name of the thread. + ThreadResource resource = 4 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat_events.proto new file mode 100644 index 00000000..3874d5f9 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/chat_events.proto @@ -0,0 +1,58 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.event"; +option java_outer_classname = "ChatEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/thread.proto"; + +// A message created in the space. +message MessageCreated { + + MessageId message = 1 [(required) = true]; + + // The space within which the message is created. + SpaceId space = 2 [(required) = true]; + + // The thread within which the message is created, if created in a threaded space. + ThreadId thread = 3; +} + +// A recently created message created a new thread. +message ThreadCreated { + + ThreadId thread = 1 [(required) = true]; + + // Chat thread. + ThreadResource resource = 2 [(required) = true]; + + // The space within with the thread is available. + SpaceId space = 3 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/identifiers.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/identifiers.proto new file mode 100644 index 00000000..94d79846 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/identifiers.proto @@ -0,0 +1,56 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat"; +option java_outer_classname = "IdentifiersProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +// Chat Space identifier. +message SpaceId { + + // Resource name of the space, in the form `spaces/`. + string value = 1 [(required) = true, (pattern).regex = "spaces/.+"]; +} + +// Chat Room Thread identifier. +message ThreadId { + + // The thread identifier is linked to the topic discussed in the thread. + // + // E.g. if the thread is denoted to the build status of a GitHub repository, + // the GitHub repository slug could be used as the thread ID. + // + string value = 1 [(required) = true]; +} + +// Chat Message identifier. +message MessageId { + + // Resource name of the message, in the form `spaces//messages/`. + string value = 1 [(required) = true, (pattern).regex = "spaces/.+/messages/.+"]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_events.proto new file mode 100644 index 00000000..1ce41363 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_events.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package spine.chatbot.google.chat.incoming; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.incoming.event"; +option java_outer_classname = "IncomingChatEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/incoming/incoming_chat_messages.proto"; + +// A new event received from the Chat. +message ChatEventReceived { + + // The received event. + ChatEvent event = 1 [(required) = true]; +} + +// The ChatBot is added to a Chat space. +message BotAddedToSpace { + + // The ID of the Space to which the ChatBot is added. + SpaceId space = 1 [(required) = true]; + + // The actual chat event message. + ChatEvent event = 2 [(required) = true]; +} + +// The ChatBot is removed from the Chat space. +message BotRemovedFromSpace { + + // The ID of the Space from which the ChatBot is removed. + SpaceId space = 1 [(required) = true]; + + // The actual chat event message. + ChatEvent event = 2 [(required) = true]; +} + +// The ChatBot received a new incoming Chat message. +message MessageReceived { + + // The ID of the new message. + MessageId message = 1 [(required) = true]; + + // The actual chat event message. + ChatEvent event = 2 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_messages.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_messages.proto new file mode 100644 index 00000000..150721ff --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/incoming/incoming_chat_messages.proto @@ -0,0 +1,212 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +// This file contains the proto definitions that conform to the +// +// event formats of the Hangouts Chat. +// +// The layout of types and fields make them compatible with the Travis JSON output. +// This way we don't have to create custom conversion. +// + +package spine.chatbot.google.chat.incoming; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.incoming"; +option java_outer_classname = "IncomingChatMessagesProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +// An incoming Chat event. +// +// The definition conforms to the JSON representation defined in the +// . +// +message ChatEvent { + + // The type of the event + EventType type = 1 [(required) = true]; + + // The timestamp indicating when the event was dispatched. + string event_time = 2 [(required) = true]; + + // A secret value that bots can use to verify if a request is from Google. + // + // The token is randomly generated by Google, remains static, and can be obtained from + // the Chat API configuration page in the Cloud Console. Developers can revoke/regenerate + // it if needed from the same page. + // + string token = 3; + + // The bot-defined key for the thread related to the event. + string thread_key = 4; + + // The room or DM in which the event occurred. + Space space = 5; + + // The message that triggered the event, if applicable. + Message message = 6; + + // The user that triggered the event. + User user = 7 [(required) = true]; +} + +// A message in Chat. +// +// See +// reference declaration for more details. +// +message Message { + + // The resource name, in the form "spaces/*/messages/*". + string name = 1 [(required) = true, (pattern).regex = "spaces/.+/messages/.+"]; + + // The user who created the message. + User sender = 2; + + // The time at which the message was created in Hangouts Chat server. + string create_time = 3; + + // The plain-text body of the message. + string text = 4; + + // The plain-text body of the message with all bot mentions stripped out. + string argument_text = 5; + + // The thread the message belongs to. + Thread thread = 6; + + // Annotations associated with the text in this message. + repeated Annotation annotations = 7; +} + +// Annotation metadata of the message. +message Annotation { + + // The length of the substring in the plain-text message body this annotation corresponds to. + uint32 length = 1; + + // The start index (0-based, inclusive) in the plain-text message body this annotation + // corresponds to. + // + uint32 start_index = 2; + + // The metadata of user mention. + UserMention user_mention = 3; + + // The type of the annotation. + string type = 4; +} + +// Annotation metadata for user mentions (@). +message UserMention { + + // The type of user mention. + string type = 1; + + // The user mentioned. + User user = 2; +} + +// A thread in Chat. +message Thread { + + // Resource name, in the form "spaces/*/threads/*". + string name = 1 [(required) = true, (pattern).regex = "spaces/.+/threads/.+"]; +} + +// A room or DM in Chat. +// +// See +// reference declaration for more details. +// +message Space { + option (is).java_type = "SpaceMixin"; + + // The resource name of the space, in the form "spaces/*". + string name = 1 [(required) = true, (pattern).regex = "spaces/.+"]; + + // The display name (only if the space is a room). + string display_name = 2; + + // The type of a space + SpaceType type = 3; +} + +// A user in Chat. +// +// See reference +// declaration for more details. +// +message User { + + // The resource name, in the format "users/*". + string name = 1 [(required) = true, (pattern).regex = "users/.+"]; + + // The user's display name. + string displayName = 2; + + // The user's avatar URL. + string avatarUrl = 3; + + // The user's email. + string email = 4; + + // The user's type. + string type = 5; +} + +// The type of a space. +enum SpaceType { + + ST_UNKNOWN = 0; + + // Multi-user spaces such as rooms and DMs between humans. + ROOM = 1; + + // 1:1 Direct Message between a human and a bot, where all messages are flat. + DM = 2; +} + +// The type of the event the bot is receiving. +// +// See reference +// declaration for more details. +// +enum EventType { + + ET_UNKNOWN = 0; + + // A message was sent in a room or direct message. + MESSAGE = 1; + + // The bot was added to a room or DM. + ADDED_TO_SPACE = 2; + + // The bot was removed from a room or DM. + REMOVED_FROM_SPACE = 3; + + // The bot's interactive card was clicked. + CARD_CLICKED = 4; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space.proto new file mode 100644 index 00000000..2f8d080b --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space.proto @@ -0,0 +1,57 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat"; +option java_outer_classname = "SpaceProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; + +// A room or DM in Chat. +message Space { + option (entity) = {kind: AGGREGATE visibility: NONE}; + option (is).java_type = "SpaceHeaderAware"; + + SpaceId id = 1; + + // The space header. + SpaceHeader header = 2; +} + +// The Chat space header. +message SpaceHeader { + + // Whether the space is a DM between a bot and a single human. + bool single_user_bot_dm = 1; + + // Whether the messages are threaded in this space. + bool threaded = 2; + + // The display name (only if the space is a room). + string display_name = 3; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_commands.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_commands.proto new file mode 100644 index 00000000..c56b9224 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_commands.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.command"; +option java_outer_classname = "SpaceCommandsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/space.proto"; + +// A request to register a Chat space. +message RegisterSpace { + option (is).java_type = "io.spine.chatbot.google.chat.SpaceHeaderAware"; + + SpaceId id = 1 [(required) = true]; + + // The space header. + SpaceHeader header = 2 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_events.proto new file mode 100644 index 00000000..e2d6421e --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/space_events.proto @@ -0,0 +1,44 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.event"; +option java_outer_classname = "SpaceEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/space.proto"; + +// A new Chat space registered. +message SpaceRegistered { + option (is).java_type = "io.spine.chatbot.google.chat.SpaceHeaderAware"; + + SpaceId space = 1 [(required) = true]; + + // The space header. + SpaceHeader header = 2 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread.proto new file mode 100644 index 00000000..e0ad8ad4 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread.proto @@ -0,0 +1,56 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.thread"; +option java_outer_classname = "ThreadProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; + +// A thread in a room. +message Thread { + option (entity) = {kind: AGGREGATE visibility: NONE}; + + ThreadId id = 1; + + // The resource name of the thread. + ThreadResource resource = 2; + + // The space within with the thread is available. + SpaceId space = 3; + + // Messages posted by the bot to the thread. + repeated MessageId message = 4; +} + +// Chat Thread resource. +message ThreadResource { + + // The resource name of the thread, in the form `spaces//threads/`. + string name = 1 [(required) = true, (pattern).regex = "spaces/.+/threads/.+"]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_chat.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_chat.proto new file mode 100644 index 00000000..c5fa0890 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_chat.proto @@ -0,0 +1,50 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.thread"; +option java_outer_classname = "ThreadChatProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/thread.proto"; + +// A thread chatting process. +// +// Acknowledges incoming events and publishes messages to a respective thread if needed. +// +message ThreadChat { + option (entity) = {kind: PROCESS_MANAGER visibility: FULL}; + + ThreadId thread = 1; + + // Chat thread. + ThreadResource resource = 2; + + // Space within with the thread is available. + SpaceId space = 3; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_events.proto b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_events.proto new file mode 100644 index 00000000..ac0e2686 --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/google/chat/thread_events.proto @@ -0,0 +1,55 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +package spine.chatbot.google.chat; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.google.chat.thread.event"; +option java_outer_classname = "ThreadEventsProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +import "spine/chatbot/google/chat/identifiers.proto"; +import "spine/chatbot/google/chat/thread.proto"; + +// A Chat thread initialized. +message ThreadInitialized { + + ThreadId thread = 1 [(required) = true]; + + // Chat thread. + ThreadResource resource = 2 [(required) = true]; + + // Space within which the thread is available. + SpaceId space = 3 [(required) = true]; +} + +// A message added to the thead. +message MessageAdded { + + MessageId message = 1 [(required) = true]; + + // Thread to which the message is added. + ThreadId thread = 2 [(required) = true]; +} diff --git a/google-chat-bot/src/main/proto/spine/chatbot/travis/travis.proto b/google-chat-bot/src/main/proto/spine/chatbot/travis/travis.proto new file mode 100644 index 00000000..dc4af61e --- /dev/null +++ b/google-chat-bot/src/main/proto/spine/chatbot/travis/travis.proto @@ -0,0 +1,199 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +syntax = "proto3"; + +// This file contains the proto definitions that conform to the +// Travis CI API v3 data types. +// +// The layout of types and fields make them compatible with the Travis JSON output. +// This way we don't have to create custom conversion. +// + +package spine.chatbot.travis; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io.chatbot"; +option java_package = "io.spine.chatbot.travis"; +option java_outer_classname = "TravisCiApiProto"; +option java_multiple_files = true; +option java_generate_equals_and_hash = true; + +// The owner of a resource. +// +// This will be either a user or an organization. +// See reference declaration +// for more details. +// +message Owner { + + // Value uniquely identifying the owner. + uint64 id = 1; + + // User or organization login set on GitHub. + string login = 2; +} + +// An individual repository. +// +// See reference +// declaration for more details. +// +message Repository { + + // Value uniquely identifying the repository. + uint64 id = 1; + + // The repository's name on GitHub. + string name = 2; + + // The repository's slug. + // + // Same as {repository.owner.name}/{repository.name}. + // + string slug = 3; +} + +// The branch of a repository. +// +// See reference declaration +// for more details. +// +message Branch { + + // Name of the git branch. + string name = 1; +} + +// Commit information is obtained by requesting a build. +// +// See reference declaration +// for more details. +// +message Commit { + + // Value uniquely identifying the commit. + uint64 id = 1; + + // Checksum the commit has in git and is identified by. + string sha = 2; + + // Named reference the commit has in git. + string ref = 3; + + // Commit message. + string message = 4; + + // URL to the commit's diff on GitHub. + string compare_url = 5; + + // Commit date from git. + string committed_at = 6; + + // Commit author. + Author author = 7; +} + +// Git commit author information. +message Author { + + // Git name of the commit author. + string name = 1; +} + +// Minimal Travis CI build representation. +// +// See reference declaration +// for more details. +// +message Build { + + // Value uniquely identifying the build. + uint64 id = 1; + + // Incremental number for a repository's builds. + string number = 2; + + // Current state of the build. + string state = 3; + + // Wall clock time in seconds. + uint64 duration = 4; + + // Event that triggered the build. + string event_type = 5; + + // State of the previous build. + string previous_state = 6; + + // The repository the build is associated with. + Repository repository = 7; + + // The branch the build is associated with. + Branch branch = 8; + + // The build's tag. + string tag = 9; + + // The commit the build is associated with. + Commit commit = 10; + + // The User or Organization that created the build. + Owner created_by = 11; +} + +// A Travis `branch` API endpoint response. +// +// See API reference for more +// details. +// +message RepoBranchBuildResponse { + + option (is).java_type = "TravisResponse"; + + // Name of the git branch. + string name = 1; + + // The repository. + Repository repository = 2; + + // Whether or not this is the repository's default branch. + bool default_branch = 3; + + // Whether or not the branch still exists on GitHub. + bool exists_on_github = 4; + + // Last build on the branch. + Build last_build = 5; +} + +// A Travis `repos` API endpoint response. +// +// See API reference +// for more details. +// +message RepositoriesResponse { + + option (is).java_type = "TravisResponse"; + + // Repositories fetched by the API call. + repeated Repository repositories = 1; +} diff --git a/google-chat-bot/src/main/resources/application.yml b/google-chat-bot/src/main/resources/application.yml new file mode 100644 index 00000000..b554a48e --- /dev/null +++ b/google-chat-bot/src/main/resources/application.yml @@ -0,0 +1,23 @@ +# +# Copyright 2020, TeamDev. All rights reserved. +# +# Redistribution and use in source and/or binary forms, with or without +# modification, must retain the above copyright notice and the following +# disclaimer. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +micronaut: + application: + name: ChatBot diff --git a/google-chat-bot/src/main/resources/index.yaml b/google-chat-bot/src/main/resources/index.yaml new file mode 100644 index 00000000..ffa8850d --- /dev/null +++ b/google-chat-bot/src/main/resources/index.yaml @@ -0,0 +1,94 @@ +# This file is the configuration of the Cloud Datastore DB indexes. +# To update the index, modify the file and run: +# +# $ gcloud datastore indexes create google-chat-bot/src/main/resources/index.yaml --project +# + +indexes: + + # Common index for all the applications. + - kind: spine.core.Event + ancestor: no + properties: + - name: type + - name: created + + # Index required for `DsInboxStorage.readAll` query. + + - kind: spine.server.delivery.InboxMessage + ancestor: yes + properties: + - name: inbox_shard + - name: of_total_inbox_shards + - name: received_at + - name: version + + - kind: spine.server.delivery.InboxMessage + properties: + - name: inbox_shard + - name: of_total_inbox_shards + - name: received_at + - name: version + + # Index required for `DsInboxStorage.newestMessageToDeliver` query. + + - kind: spine.server.delivery.InboxMessage + ancestor: yes + properties: + - name: inbox_shard + - name: of_total_inbox_shards + - name: status + + - kind: spine.system.server.CommandLifecycle + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot + + - kind: spine.system.server.EntityHistory + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot + + - kind: spine.chatbot.github.Organization + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot + + - kind: spine.chatbot.github.Repository + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot + + - kind: spine.chatbot.google.chat.Space + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot + + - kind: spine.chatbot.google.chat.Thread + properties: + - name: aggregate_id + - name: version + direction: desc + - name: created + direction: desc + - name: snapshot diff --git a/google-chat-bot/src/main/resources/log4j2.xml b/google-chat-bot/src/main/resources/log4j2.xml new file mode 100644 index 00000000..9e22026e --- /dev/null +++ b/google-chat-bot/src/main/resources/log4j2.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/CanFailFast.java b/google-chat-bot/src/test/java/io/spine/chatbot/CanFailFast.java new file mode 100644 index 00000000..f2be68d2 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/CanFailFast.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import io.spine.logging.Logging; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static io.spine.util.Exceptions.newIllegalStateException; + +/** + * An abstract API that exposes the {@code fail-fast} concept. + */ +public abstract class CanFailFast implements Logging { + + /** + * Determines whether the client should fail if a particular response is not preconfigured. + */ + private final boolean failFast; + + /** + * Creates a new client with the specified {@code failFast} behavior. + */ + protected CanFailFast(boolean failFast) { + this.failFast = failFast; + } + + /** + * Applies the fail-fast approach to the supplied value if the client is configured so, + * otherwise returns the {@code defaultValue} if the supplied {@code value} is {@code null}. + * + * @param value + * the value under check + * @param key + * the key using which the {@code value} was obtained + * @param defaultValue + * the default value to be used if the client is not using the fail-fast approach and + * the value is {@code null} + * @param + * the type of the key the API client was called with + * @param + * the type of the value the API client is expected to return + * @throws IllegalStateException + * if the client is configured to use the fail-fast approach and the supplied + * {@code value} is {@code null} + */ + protected @NonNull V failOrDefault(@Nullable V value, + @NonNull K key, + @NonNull V defaultValue) { + if (failFast && value == null) { + throw newIllegalStateException( + "Response of type `%s` is not configured for the key `%s`.", + defaultValue.getClass() + .getSimpleName(), String.valueOf(key) + ); + } + if (!failFast && value == null) { + return defaultValue; + } + return value; + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/IncomingEventsControllerTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/IncomingEventsControllerTest.java new file mode 100644 index 00000000..88bff3cd --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/IncomingEventsControllerTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import com.google.common.io.Resources; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.PubsubMessage; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.annotation.MicronautTest; +import io.spine.chatbot.google.chat.InMemoryGoogleChatClient; +import io.spine.chatbot.server.Server; +import io.spine.chatbot.server.github.GitHubContext; +import io.spine.chatbot.server.google.chat.GoogleChatContext; +import io.spine.chatbot.travis.InMemoryTravisClient; +import io.spine.json.Json; +import io.spine.pubsub.PubsubPushRequest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static io.micronaut.http.HttpRequest.POST; +import static io.spine.util.Exceptions.newIllegalStateException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@MicronautTest +@DisplayName("`IncomingEventsController` should") +final class IncomingEventsControllerTest { + + @Inject + @Client("/") + private HttpClient client; + + @BeforeAll + static void setupServer() { + var chatContext = GoogleChatContext + .newBuilder() + .setClient(InMemoryGoogleChatClient.lenientClient()) + .build(); + var gitHubContext = GitHubContext + .newBuilder() + .setTravis(InMemoryTravisClient.lenientClient()) + .build(); + Server.withContexts(chatContext, gitHubContext) + .start(); + } + + @Test + @DisplayName("receive and decode a Pub/Sub message with Google Chat event") + void receiveAndDecode() { + var pubsubMessage = PubsubMessage + .newBuilder() + .setMessageId("129y418y4houfhiuehwr") + .setData(ByteString.copyFromUtf8(chatEventJson())) + .build(); + var pushRequest = PubsubPushRequest + .newBuilder() + .setMessage(pubsubMessage) + .setSubscription("projects/test-project/subscriptions/test-subscription") + .vBuild(); + var request = POST("/chat/incoming/event", Json.toJson(pushRequest)) + .contentType(MediaType.APPLICATION_JSON); + String actual = client.toBlocking() + .retrieve(request); + assertEquals("OK", actual); + } + + private static String chatEventJson() { + try { + var resource = Resources.getResource("chat_event.json"); + return Resources.toString(resource, StandardCharsets.UTF_8); + } catch (IOException e) { + throw newIllegalStateException(e, "Unable to load ChatEvent message JSON definition."); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/PubsubPushRequestDeserializerTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/PubsubPushRequestDeserializerTest.java new file mode 100644 index 00000000..96753796 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/PubsubPushRequestDeserializerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.Resources; +import com.google.common.truth.extensions.proto.ProtoTruth; +import com.google.protobuf.ByteString; +import com.google.protobuf.util.Timestamps; +import com.google.pubsub.v1.PubsubMessage; +import io.micronaut.jackson.ObjectMapperFactory; +import io.micronaut.test.annotation.MicronautTest; +import io.spine.pubsub.PubsubPushRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; + +import static io.spine.testing.Tests.nullRef; +import static io.spine.util.Exceptions.newIllegalStateException; + +@MicronautTest +@DisplayName("`PubsubPushRequestDeserializer` should") +final class PubsubPushRequestDeserializerTest { + + @Inject + private ObjectMapperFactory mapperFactory; + + @Test + @DisplayName("deserialize Pub/Sub message") + void deserializePubsubMessage() throws JsonProcessingException, ParseException { + var pubsubMessage = PubsubMessage + .newBuilder() + .setMessageId("450292511223766") + .setPublishTime(Timestamps.parse("2020-06-21T20:48:25.908Z")) + .setData(ByteString.copyFromUtf8("{\"key\":\"value\"}")) + .buildPartial(); + var expectedResult = PubsubPushRequest + .newBuilder() + .setSubscription("projects/test-project/subscriptions/test-subscription") + .setMessage(pubsubMessage) + .vBuild(); + + var mapper = mapperFactory.objectMapper(nullRef(), nullRef()); + + var pushRequest = mapper.readValue(pushRequestJson(), PubsubPushRequest.class); + ProtoTruth.assertThat(pushRequest) + .isEqualTo(expectedResult); + } + + private static String pushRequestJson() { + try { + var resource = Resources.getResource("pubsub_push_request.json"); + return Resources.toString(resource, StandardCharsets.UTF_8); + } catch (IOException e) { + throw newIllegalStateException( + e, "Unable to load PubsubPushRequest message JSON definition." + ); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugMixinTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugMixinTest.java new file mode 100644 index 00000000..f5140060 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugMixinTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("`SlugMixin` should") +final class SlugMixinTest { + + @Test + @DisplayName("encode slug value") + void encodeSlugValue() { + var slug = Slugs.newSlug("TestOrganization/test-repository"); + assertEquals("TestOrganization%2Ftest-repository", slug.encodedValue()); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugsTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugsTest.java new file mode 100644 index 00000000..172ebf3d --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/github/SlugsTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github; + +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`Slugs` should") +final class SlugsTest extends UtilityClassTest { + + SlugsTest() { + super(Slugs.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/github/repository/build/BuildStateMixinTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/github/repository/build/BuildStateMixinTest.java new file mode 100644 index 00000000..7aa6f35a --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/github/repository/build/BuildStateMixinTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.github.repository.build; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static io.spine.chatbot.github.repository.build.BuildStateMixin.buildStateFrom; +import static io.spine.testing.Tests.nullRef; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; + +@DisplayName("`BuildStateMixin` should") +final class BuildStateMixinTest { + + @Test + @DisplayName("not accept `null` build states") + void notAcceptNull() { + assertThrows(NullPointerException.class, () -> buildStateFrom(nullRef())); + } + + @Test + @DisplayName("not accept unknown build states") + void notAcceptUnknownStates() { + assertThrows(IllegalArgumentException.class, () -> buildStateFrom("unknown")); + } + + @DisplayName("accept valid `Build.State` value") + @ParameterizedTest + @EnumSource(mode = EXCLUDE, value = Build.State.class, names = {"BS_UNKNOWN", "UNRECOGNIZED"}) + void processValidValues(Build.State state) { + assertDoesNotThrow(() -> { + buildStateFrom(state.name()); + }); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/BuildStateUpdatesTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/BuildStateUpdatesTest.java new file mode 100644 index 00000000..259621ba --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/BuildStateUpdatesTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`BuildStateUpdates` should") +final class BuildStateUpdatesTest extends UtilityClassTest { + + BuildStateUpdatesTest() { + super(BuildStateUpdates.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/ChatWidgetsTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/ChatWidgetsTest.java new file mode 100644 index 00000000..4dcd900c --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/ChatWidgetsTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`ChatWidgets` should") +final class ChatWidgetsTest extends UtilityClassTest { + + ChatWidgetsTest() { + super(ChatWidgets.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/InMemoryGoogleChatClient.java b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/InMemoryGoogleChatClient.java new file mode 100644 index 00000000..0ae0ab98 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/google/chat/InMemoryGoogleChatClient.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.google.chat; + +import io.spine.chatbot.CanFailFast; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.google.chat.thread.ThreadResource; + +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.synchronizedMap; + +/** + * An in-memory test-only implementation of the Google Chat client. + */ +public final class InMemoryGoogleChatClient extends CanFailFast implements GoogleChatClient { + + private final Map sentMessages = synchronizedMap(new HashMap<>()); + + private InMemoryGoogleChatClient(boolean failFast) { + super(failFast); + } + + /** + * Creates a {@link #CanFailFast#failFast failFast} in-memory Google Chat client. + */ + public static InMemoryGoogleChatClient strictClient() { + return new InMemoryGoogleChatClient(true); + } + + /** + * Creates a lenient in-memory Google Chat client. + */ + public static InMemoryGoogleChatClient lenientClient() { + return new InMemoryGoogleChatClient(false); + } + + @Override + public BuildStateUpdate sendBuildStateUpdate(Build build, ThreadResource thread) { + var stubbedValue = sentMessages.get(build.getNumber()); + var result = failOrDefault(stubbedValue, + build.getNumber(), + BuildStateUpdate.getDefaultInstance()); + return result; + } + + /** + * Sets up a stub {@code update} for a build state with the specified {@code buildNumber}. + */ + public void setBuildStateUpdate(String buildNumber, BuildStateUpdate update) { + checkNotNull(buildNumber); + checkNotNull(update); + sentMessages.put(buildNumber, update); + } + + /** + * Resets state of the configured responses. + */ + public void reset() { + sentMessages.clear(); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/net/MoreUrlsTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/net/MoreUrlsTest.java new file mode 100644 index 00000000..0b615e3e --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/net/MoreUrlsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.net; + +import io.spine.chatbot.github.Slug; +import io.spine.chatbot.github.Slugs; +import io.spine.net.Urls; +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static io.spine.chatbot.net.MoreUrls.githubUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisBuildUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisUrlFor; + +@DisplayName("`MoreUrls` should") +final class MoreUrlsTest extends UtilityClassTest { + + private static final Slug REPO_SLUG = Slugs.newSlug("SpineEventEngine/chat-bot"); + + MoreUrlsTest() { + super(MoreUrls.class); + } + + @DisplayName("compose URL for") + @Nested + @SuppressWarnings("ClassCanBeStatic") // jUnit Jupiter cannot work with static classes + final class Compose { + + @DisplayName("Travis CI repository page") + @Test + void travisRepo() { + assertThat(travisUrlFor(REPO_SLUG)).isEqualTo(Urls.create( + "https://travis-ci.com/github/SpineEventEngine/chat-bot" + )); + } + + @DisplayName("Travis CI repository build page") + @Test + void travisBuild() { + assertThat(travisBuildUrlFor(REPO_SLUG, 331)).isEqualTo(Urls.create( + "https://travis-ci.com/github/SpineEventEngine/chat-bot/builds/331" + )); + } + + @DisplayName("GitHub repository page") + @Test + void githubRepo() { + assertThat(githubUrlFor(REPO_SLUG)).isEqualTo(Urls.create( + "https://github.com/SpineEventEngine/chat-bot" + )); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextAwareTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextAwareTest.java new file mode 100644 index 00000000..9e79b271 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextAwareTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.chatbot.travis.InMemoryTravisClient; +import io.spine.server.BoundedContextBuilder; +import io.spine.testing.server.blackbox.ContextAwareTest; +import org.junit.jupiter.api.AfterEach; + +/** + * An abstract test-base for GitHub context-based tests. + */ +abstract class GitHubContextAwareTest extends ContextAwareTest { + + private final InMemoryTravisClient client = InMemoryTravisClient.strictClient(); + + @Override + protected final BoundedContextBuilder contextBuilder() { + return GitHubContext + .newBuilder() + .setTravis(client) + .build() + .builder(); + } + + @AfterEach + @OverridingMethodsMustInvokeSuper + @Override + protected final void closeContext() { + super.closeContext(); + client.reset(); + } + + /** + * Returns configured for the {@link #context() context} Travis client. + */ + final InMemoryTravisClient travisClient() { + return client; + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextTest.java new file mode 100644 index 00000000..18337961 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubContextTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.travis.InMemoryTravisClient; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.spine.testing.Tests.nullRef; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("`GitHubContext` should") +final class GitHubContextTest { + + @Test + @DisplayName("allow configuring Travis CI client") + void allowConfiguringTravisClient() { + assertDoesNotThrow( + () -> GitHubContext.newBuilder() + .setTravis(InMemoryTravisClient.lenientClient()) + .build() + ); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Test + @DisplayName("not allow passing `null` value as Travis CI client") + void notAllowNullTravisClients() { + assertThrows( + NullPointerException.class, () -> GitHubContext.newBuilder() + .setTravis(nullRef()) + ); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubIdentifiersTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubIdentifiersTest.java new file mode 100644 index 00000000..4c279195 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/GitHubIdentifiersTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.google.chat.GoogleChatIdentifiers; +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`GitHubIdentifiers` should") +final class GitHubIdentifiersTest extends UtilityClassTest { + + GitHubIdentifiersTest() { + super(GoogleChatIdentifiers.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrgReposProjectionTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrgReposProjectionTest.java new file mode 100644 index 00000000..4b5dc47a --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrgReposProjectionTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.organization.OrgHeader; +import io.spine.chatbot.github.organization.OrganizationRepositories; +import io.spine.chatbot.github.organization.command.RegisterOrganization; +import io.spine.chatbot.github.repository.RepoHeader; +import io.spine.chatbot.github.repository.command.RegisterRepository; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.net.Url; +import io.spine.net.Urls; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.GitHubIdentifiers.organization; +import static io.spine.chatbot.github.GitHubIdentifiers.repository; +import static io.spine.chatbot.github.Slugs.orgSlug; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.net.MoreUrls.githubUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisUrlFor; + +@DisplayName("`OrgReposProjection` should") +final class OrgReposProjectionTest extends GitHubContextAwareTest { + + @Nested + @DisplayName("register an organization") + final class RegisterOrg { + + private static final String orgName = "Our Org"; + + private final SpaceId googleChatSpace = space("spaces/qwdp123tt1"); + private final OrganizationId organization = organization("OurOrg"); + + private final Url githubUrl = githubUrlFor(orgSlug(organization)); + private final Url travisCiUrl = travisUrlFor(orgSlug(organization)); + private final Url websiteUrl = Urls.create("https://our-organization.com"); + + private final OrgHeader header = OrgHeader + .newBuilder() + .setGithubProfile(githubUrl) + .setTravisProfile(travisCiUrl) + .setWebsite(websiteUrl) + .setName(orgName) + .setSpace(googleChatSpace) + .vBuild(); + + @BeforeEach + void registerOrg() { + var registerOrganization = RegisterOrganization + .newBuilder() + .setId(organization) + .setHeader(header) + .vBuild(); + context().receivesCommand(registerOrganization); + } + + @Test + @DisplayName("setting organization to the state") + void settingState() { + var expectedState = OrganizationRepositories + .newBuilder() + .setOrganization(organization) + .vBuild(); + context().assertState(organization, OrganizationRepositories.class) + .isEqualTo(expectedState); + } + } + + @Nested + @DisplayName("register repositories") + final class RegisterRepo { + + private static final String orgName = "Multi Repo Org"; + + private final SpaceId space = space("spaces/qqwp123ttQ"); + private final OrganizationId org = organization("MultiRepoOrg"); + private final RepositoryId repo = repository("main-repo"); + + private final Url githubUrl = githubUrlFor(orgSlug(org)); + private final Url travisCiUrl = travisUrlFor(orgSlug(org)); + private final Url websiteUrl = Urls.create("https://multi-repo-organization.com"); + + private final OrgHeader orgHeader = OrgHeader + .newBuilder() + .setGithubProfile(githubUrl) + .setTravisProfile(travisCiUrl) + .setWebsite(websiteUrl) + .setName(orgName) + .setSpace(space) + .vBuild(); + + private final RepoHeader repoHeader = RepoHeader + .newBuilder() + .setGithubProfile(githubUrl) + .setTravisProfile(travisCiUrl) + .setName("Main Repo") + .setOrganization(org) + .vBuild(); + + @BeforeEach + void registerOrg() { + var registerOrganization = RegisterOrganization + .newBuilder() + .setId(org) + .setHeader(orgHeader) + .vBuild(); + context().receivesCommand(registerOrganization); + } + + @Test + @DisplayName("setting repository to the state") + void settingState() { + var registerRepository = RegisterRepository + .newBuilder() + .setId(repo) + .setHeader(repoHeader) + .vBuild(); + context().receivesCommand(registerRepository); + var expectedState = OrganizationRepositories + .newBuilder() + .setOrganization(org) + .addRepository(repo) + .vBuild(); + context().assertState(org, OrganizationRepositories.class) + .isEqualTo(expectedState); + } + + @Test + @DisplayName("handling duplicate repos gracefully") + void handleDuplicateRepos() { + var registerRepository = RegisterRepository + .newBuilder() + .setId(repo) + .setHeader(repoHeader) + .vBuild(); + context().receivesCommand(registerRepository); + context().receivesCommand(registerRepository); + var expectedState = OrganizationRepositories + .newBuilder() + .setOrganization(org) + .addRepository(repo) + .vBuild(); + context().assertState(org, OrganizationRepositories.class) + .isEqualTo(expectedState); + } + + @Test + @DisplayName("ignoring repos without organization") + void ignoreRepoWithoutOrg() { + var registerRepository = RegisterRepository + .newBuilder() + .setId(repo) + .setHeader(repoHeader.toBuilder() + .clearOrganization()) + .vBuild(); + context().receivesCommand(registerRepository); + var expectedState = OrganizationRepositories + .newBuilder() + .setOrganization(org) + .vBuild(); + context().assertState(org, OrganizationRepositories.class) + .isEqualTo(expectedState); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrganizationAggregateTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrganizationAggregateTest.java new file mode 100644 index 00000000..9910c7cc --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/OrganizationAggregateTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.organization.OrgHeader; +import io.spine.chatbot.github.organization.Organization; +import io.spine.chatbot.github.organization.command.RegisterOrganization; +import io.spine.chatbot.github.organization.event.OrganizationRegistered; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.net.Url; +import io.spine.net.Urls; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.GitHubIdentifiers.organization; +import static io.spine.chatbot.github.Slugs.orgSlug; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.net.MoreUrls.githubUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisUrlFor; + +@DisplayName("`OrganizationAggregate` should") +final class OrganizationAggregateTest extends GitHubContextAwareTest { + + @Nested + @DisplayName("register an organization") + final class Register { + + private static final String orgName = "Test Organization"; + + private final SpaceId googleChatSpace = space("spaces/qwdp123ttQ"); + private final OrganizationId organization = organization("TestOrganization"); + + private final Url githubUrl = githubUrlFor(orgSlug(organization)); + private final Url travisCiUrl = travisUrlFor(orgSlug(organization)); + private final Url websiteUrl = Urls.create("https://test-organization.com"); + + private final OrgHeader header = OrgHeader + .newBuilder() + .setGithubProfile(githubUrl) + .setTravisProfile(travisCiUrl) + .setWebsite(websiteUrl) + .setName(orgName) + .setSpace(googleChatSpace) + .vBuild(); + + @BeforeEach + void registerOrganization() { + var registerOrganization = RegisterOrganization + .newBuilder() + .setId(organization) + .setHeader(header) + .vBuild(); + context().receivesCommand(registerOrganization); + } + + @Test + @DisplayName("producing `OrganizationRegistered` event") + void producingEvent() { + var organizationRegistered = OrganizationRegistered + .newBuilder() + .setOrganization(organization) + .setHeader(header) + .vBuild(); + context().assertEvent(organizationRegistered); + } + + @Test + @DisplayName("setting organization state") + void settingState() { + var expectedState = Organization + .newBuilder() + .setId(organization) + .setHeader(header) + .vBuild(); + context().assertState(organization, Organization.class) + .isEqualTo(expectedState); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepoBuildProcessTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepoBuildProcessTest.java new file mode 100644 index 00000000..f8cffe66 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepoBuildProcessTest.java @@ -0,0 +1,369 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.github.repository.build.BuildStateChange; +import io.spine.chatbot.github.repository.build.RepositoryBuild; +import io.spine.chatbot.github.repository.build.command.CheckRepositoryBuild; +import io.spine.chatbot.github.repository.build.event.BuildFailed; +import io.spine.chatbot.github.repository.build.event.BuildRecovered; +import io.spine.chatbot.github.repository.build.event.BuildSucceededAgain; +import io.spine.chatbot.github.repository.build.rejection.RepositoryBuildRejections; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.travis.Author; +import io.spine.chatbot.travis.Commit; +import io.spine.chatbot.travis.RepoBranchBuildResponse; +import io.spine.chatbot.travis.Repository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.GitHubIdentifiers.organization; +import static io.spine.chatbot.github.GitHubIdentifiers.repository; +import static io.spine.chatbot.github.Slugs.repoSlug; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.server.github.RepoBuildProcess.buildFrom; + +@DisplayName("`RepoBuildProcess` should") +final class RepoBuildProcessTest extends GitHubContextAwareTest { + + private static final OrganizationId org = organization("SpineEventEngine"); + private static final RepositoryId repo = repository("SpineEventEngine/web"); + private static final SpaceId space = space("spaces/1245wrq"); + + @Test + @DisplayName("throw `NoBuildsFound` rejection when Travis API cannot return builds for a repo") + void throwNoBuildsFoundRejection() { + travisClient().setBuildsFor(repoSlug(repo), RepoBranchBuildResponse.getDefaultInstance()); + var checkRepoBuild = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setOrganization(org) + .setSpace(space) + .vBuild(); + context().receivesCommand(checkRepoBuild); + + var noBuildsFound = RepositoryBuildRejections.NoBuildsFound + .newBuilder() + .setRepository(repo) + .vBuild(); + context().assertEvent(noBuildsFound); + } + + @SuppressWarnings("ClassCanBeStatic") // nested tests do not work with static classes + @Nested + @DisplayName("handle build failure") + final class FailedBuild { + + private final io.spine.chatbot.travis.Build build = failedBuild(); + private final RepoBranchBuildResponse branchBuild = branchBuildOf(build); + private final Build buildState = buildFrom(branchBuild, space); + + @BeforeEach + void sendCheckCommand() { + travisClient().setBuildsFor(repoSlug(repo), branchBuild); + var checkRepoBuild = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoBuild); + } + + @Test + @DisplayName("producing `BuildFailed` event") + void producingEvent() { + var stateChange = BuildStateChange + .newBuilder() + .setNewValue(buildState) + .vBuild(); + var buildFailed = BuildFailed + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + context().assertEvent(buildFailed); + } + + @Test + @DisplayName("setting process state") + void settingState() { + var expectedState = RepositoryBuild + .newBuilder() + .setRepository(repo) + .setBuild(buildState) + .setCurrentState(buildState.getState()) + .vBuild(); + context().assertState(repo, RepositoryBuild.class) + .isEqualTo(expectedState); + } + } + + @SuppressWarnings("ClassCanBeStatic") // nested tests do not work with static classes + @Nested + @DisplayName("handle build recovery") + final class RecoveredBuild { + + private final io.spine.chatbot.travis.Build previousBuild = failedBuild(); + private final RepoBranchBuildResponse previousBranchBuild = branchBuildOf(previousBuild); + private final Build previousBuildState = buildFrom(previousBranchBuild, + space); + + private final io.spine.chatbot.travis.Build newBuild = passingBuild(); + private final RepoBranchBuildResponse newBranchBuild = branchBuildOf(newBuild); + private final Build newBuildState = buildFrom(newBranchBuild, space); + + @BeforeEach + void sendCheckCommands() { + travisClient().setBuildsFor(repoSlug(repo), previousBranchBuild); + var checkRepoFailure = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoFailure); + travisClient().setBuildsFor(repoSlug(repo), newBranchBuild); + var checkRepoRecovery = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoRecovery); + } + + @Test + @DisplayName("producing `BuildRecovered` event") + void producingEvent() { + var stateChange = BuildStateChange + .newBuilder() + .setPreviousValue(previousBuildState) + .setNewValue(newBuildState) + .vBuild(); + var buildFailed = BuildRecovered + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + context().assertEvent(buildFailed); + } + + @Test + @DisplayName("setting process state") + void settingState() { + var expectedState = RepositoryBuild + .newBuilder() + .setRepository(repo) + .setBuild(newBuildState) + .setCurrentState(newBuildState.getState()) + .vBuild(); + context().assertState(repo, RepositoryBuild.class) + .isEqualTo(expectedState); + } + } + + @SuppressWarnings("ClassCanBeStatic") // nested tests do not work with static classes + @Nested + @DisplayName("handle stable builds") + final class StableBuild { + + private final io.spine.chatbot.travis.Build initialFailedBuild = failedBuild(); + + private final io.spine.chatbot.travis.Build previousBuild = passingBuild(); + private final RepoBranchBuildResponse previousBranchBuild = branchBuildOf(previousBuild); + private final Build previousBuildState = buildFrom(previousBranchBuild, + space); + + private final io.spine.chatbot.travis.Build newBuild = nextPassingBuild(); + private final RepoBranchBuildResponse newBranchBuild = branchBuildOf(newBuild); + private final Build newBuildState = buildFrom(newBranchBuild, space); + + @BeforeEach + void sendCheckCommands() { + travisClient().setBuildsFor(repoSlug(repo), branchBuildOf(initialFailedBuild)); + var checkRepoFailure = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoFailure); + travisClient().setBuildsFor(repoSlug(repo), previousBranchBuild); + var checkRepoRecovery = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoRecovery); + travisClient().setBuildsFor(repoSlug(repo), newBranchBuild); + var checkRepoStable = CheckRepositoryBuild + .newBuilder() + .setRepository(repo) + .setSpace(space) + .setOrganization(org) + .vBuild(); + context().receivesCommand(checkRepoStable); + } + + @Test + @DisplayName("producing `BuildSucceededAgain` event") + void producingEvent() { + var stateChange = BuildStateChange + .newBuilder() + .setPreviousValue(previousBuildState) + .setNewValue(newBuildState) + .vBuild(); + var buildSucceededAgain = BuildSucceededAgain + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + context().assertEvent(buildSucceededAgain); + } + + @Test + @DisplayName("setting process state") + void settingState() { + var expectedState = RepositoryBuild + .newBuilder() + .setRepository(repo) + .setBuild(newBuildState) + .setCurrentState(newBuildState.getState()) + .vBuild(); + context().assertState(repo, RepositoryBuild.class) + .isEqualTo(expectedState); + } + } + + private static RepoBranchBuildResponse branchBuildOf(io.spine.chatbot.travis.Build build) { + return RepoBranchBuildResponse + .newBuilder() + .setLastBuild(build) + .setName("master") + .setRepository(Repository.newBuilder() + .setSlug(repo.getValue())) + .buildPartial(); + } + + private static io.spine.chatbot.travis.Build passingBuild() { + return io.spine.chatbot.travis.Build + .newBuilder() + .setId(123153L) + .setNumber("42") + .setState("passed") + .setPreviousState("failed") + .setRepository(webRepository()) + .setCommit(luckyCommit()) + .buildPartial(); + } + + private static io.spine.chatbot.travis.Build nextPassingBuild() { + return io.spine.chatbot.travis.Build + .newBuilder() + .setId(123154L) + .setNumber("43") + .setState("passed") + .setPreviousState("passed") + .setRepository(webRepository()) + .setCommit(stableCommit()) + .buildPartial(); + } + + private static io.spine.chatbot.travis.Build failedBuild() { + return io.spine.chatbot.travis.Build + .newBuilder() + .setId(123152L) + .setNumber("41") + .setState("failed") + .setPreviousState("failed") + .setRepository(webRepository()) + .setCommit(fatefulCommit()) + .buildPartial(); + } + + private static Commit stableCommit() { + var compareUrl = "https://github.com/SpineEventEngine/web/compare/04694f26f24a...afc1b76bf93c"; + var author = Author + .newBuilder() + .setName("God") + .buildPartial(); + return Commit + .newBuilder() + .setId(668) + .setCompareUrl(compareUrl) + .setSha("afc1b76bf93c4dadf86075280da623f947e1434b") + .setAuthor(author) + .setCommittedAt("2020-06-06T18:30:00Z") + .setMessage("May the heaven be on earth.") + .buildPartial(); + } + + private static Commit luckyCommit() { + var compareUrl = "https://github.com/SpineEventEngine/web/compare/6b4d32cadd9c...6b0a31d033a2"; + var author = Author + .newBuilder() + .setName("God") + .buildPartial(); + return Commit + .newBuilder() + .setId(667) + .setCompareUrl(compareUrl) + .setSha("6b0a31d033a2fc8d29d49baad600bc31789d9615") + .setAuthor(author) + .setCommittedAt("2020-06-06T18:00:00Z") + .setMessage("No you're not. Fixing the world.") + .buildPartial(); + } + + private static Commit fatefulCommit() { + var compareUrl = "https://github.com/SpineEventEngine/web/compare/5cbfa7423708...8fcf5d98e50f"; + var author = Author + .newBuilder() + .setName("Lucifer") + .buildPartial(); + return Commit + .newBuilder() + .setId(666) + .setCompareUrl(compareUrl) + .setSha("8fcf5d98e50f8ffa6daa8c81746181c72bd09a50") + .setAuthor(author) + .setCommittedAt("2020-06-06T06:06:66Z") + .setMessage("I am going to conquer the world!") + .buildPartial(); + + } + + private static Repository webRepository() { + return Repository + .newBuilder() + .setId(1112) + .setName("web") + .setSlug(repo.getValue()) + .buildPartial(); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepositoryAggregateTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepositoryAggregateTest.java new file mode 100644 index 00000000..6b0b7764 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/RepositoryAggregateTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.OrganizationId; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.RepoHeader; +import io.spine.chatbot.github.repository.Repository; +import io.spine.chatbot.github.repository.command.RegisterRepository; +import io.spine.chatbot.github.repository.event.RepositoryRegistered; +import io.spine.net.Url; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.GitHubIdentifiers.organization; +import static io.spine.chatbot.github.GitHubIdentifiers.repository; +import static io.spine.chatbot.github.Slugs.orgSlug; +import static io.spine.chatbot.github.Slugs.repoSlug; +import static io.spine.chatbot.net.MoreUrls.githubUrlFor; +import static io.spine.chatbot.net.MoreUrls.travisUrlFor; + +@DisplayName("`RepositoryAggregate` should") +final class RepositoryAggregateTest extends GitHubContextAwareTest { + + @Nested + @DisplayName("register a repository") + final class Register { + + private static final String ORG_SLUG = "SpineEventEngine"; + private static final String REPO_SLUG = ORG_SLUG + "/base"; + private static final String REPO_NAME = "Spine Base"; + + private final RepositoryId repo = repository(REPO_SLUG); + private final OrganizationId org = organization(ORG_SLUG); + + private final Url githubProfile = githubUrlFor(orgSlug(org)); + private final Url travisProfile = travisUrlFor(repoSlug(repo)); + private final RepoHeader repoHeader = RepoHeader + .newBuilder() + .setGithubProfile(githubProfile) + .setTravisProfile(travisProfile) + .setName(REPO_NAME) + .setOrganization(org) + .vBuild(); + + @BeforeEach + void registerRepository() { + var registerRepository = RegisterRepository + .newBuilder() + .setId(repo) + .setHeader(repoHeader) + .vBuild(); + context().receivesCommand(registerRepository); + } + + @Test + @DisplayName("producing `RepositoryRegistered` event") + void producingEvent() { + var repositoryRegistered = RepositoryRegistered + .newBuilder() + .setRepository(repo) + .setHeader(repoHeader) + .vBuild(); + context().assertEvent(repositoryRegistered); + } + + @Test + @DisplayName("setting repository state") + void settingState() { + var expectedState = Repository + .newBuilder() + .setId(repo) + .setHeader(repoHeader) + .vBuild(); + context().assertState(repo, Repository.class) + .isEqualTo(expectedState); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/github/SpineOrgInitProcessTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/SpineOrgInitProcessTest.java new file mode 100644 index 00000000..d5b26584 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/github/SpineOrgInitProcessTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.github; + +import io.spine.chatbot.github.organization.init.OrganizationInit; +import io.spine.chatbot.google.chat.SpaceHeader; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.event.SpaceRegistered; +import io.spine.chatbot.travis.RepositoriesResponse; +import io.spine.chatbot.travis.Repository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.Slugs.orgSlug; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.server.github.SpineOrgInitProcess.ORGANIZATION; + +@DisplayName("`SpineOrgInitProcess` should") +final class SpineOrgInitProcessTest extends GitHubContextAwareTest { + + @Nested + @DisplayName("perform initialization of watched Spine resources") + final class Init { + + private final SpaceId space = space("spaces/qjwrp1441"); + private final Repository repo = Repository + .newBuilder() + .setId(123312L) + .setName("time") + .setSlug("SpineEventEngine/time") + .vBuild(); + private final SpaceHeader spaceHeader = SpaceHeader + .newBuilder() + .setThreaded(true) + .setDisplayName("Test Space") + .vBuild(); + + @BeforeEach + void registerSpace() { + var repositoriesResponse = RepositoriesResponse + .newBuilder() + .addRepositories(repo) + .vBuild(); + travisClient().setRepositoriesFor(orgSlug(ORGANIZATION), repositoriesResponse); + var spaceRegistered = SpaceRegistered + .newBuilder() + .setSpace(space) + .setHeader(spaceHeader) + .vBuild(); + context().receivesExternalEvent(spaceRegistered); + } + + @Test + @DisplayName("setting process state") + void settingState() { + var expectedState = OrganizationInit + .newBuilder() + .setSpace(space) + .setInitialized(true) + .setOrganization(ORGANIZATION) + .vBuild(); + context().assertState(ORGANIZATION, OrganizationInit.class) + .isEqualTo(expectedState); + } + + @Test + @DisplayName("producing commands to register organization and repositories") + void producingCommands() { + context().assertCommands() + .hasSize(2); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ChatEventsTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ChatEventsTest.java new file mode 100644 index 00000000..849111a4 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ChatEventsTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`ChatEvents` should") +final class ChatEventsTest extends UtilityClassTest { + + ChatEventsTest() { + super(ChatEvents.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextAwareTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextAwareTest.java new file mode 100644 index 00000000..a9881d4f --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextAwareTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper; +import io.spine.chatbot.google.chat.InMemoryGoogleChatClient; +import io.spine.server.BoundedContextBuilder; +import io.spine.testing.server.blackbox.ContextAwareTest; +import org.junit.jupiter.api.AfterEach; + +/** + * An abstract test-base for Google Chat context-based tests. + */ +abstract class GoogleChatContextAwareTest extends ContextAwareTest { + + private final InMemoryGoogleChatClient client = InMemoryGoogleChatClient.strictClient(); + + @Override + protected final BoundedContextBuilder contextBuilder() { + return GoogleChatContext + .newBuilder() + .setClient(client) + .build() + .builder(); + } + + @AfterEach + @OverridingMethodsMustInvokeSuper + @Override + protected final void closeContext() { + super.closeContext(); + client.reset(); + } + + /** + * Returns configured for the {@link #context() context} Google Chat client. + */ + final InMemoryGoogleChatClient googleChatClient() { + return client; + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextTest.java new file mode 100644 index 00000000..89d8ccf9 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatContextTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.InMemoryGoogleChatClient; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.spine.testing.Tests.nullRef; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("`GoogleChatContext` should") +final class GoogleChatContextTest { + + @Test + @DisplayName("allow configuring Google Chat client") + void allowConfiguringTravisClient() { + assertDoesNotThrow( + () -> GoogleChatContext + .newBuilder() + .setClient(InMemoryGoogleChatClient.lenientClient()) + .build() + ); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Test + @DisplayName("not allow passing `null` value as Google Chat client") + void notAllowNullTravisClients() { + assertThrows( + NullPointerException.class, () -> GoogleChatContext.newBuilder() + .setClient(nullRef()) + ); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatIdentifiersTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatIdentifiersTest.java new file mode 100644 index 00000000..f7cebf95 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/GoogleChatIdentifiersTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.GoogleChatIdentifiers; +import io.spine.chatbot.google.chat.MessageId; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.testing.UtilityClassTest; +import io.spine.validate.ValidationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@DisplayName("`GoogleChatIdentifiers` should") +final class GoogleChatIdentifiersTest extends UtilityClassTest { + + GoogleChatIdentifiersTest() { + super(GoogleChatIdentifiers.class); + } + + // nested tests do not work with static classes + @SuppressWarnings({"ClassCanBeStatic", "ResultOfMethodCallIgnored"}) + @TestInstance(PER_CLASS) + @Nested + @DisplayName("not accept invalid") + final class NotAcceptInvalid { + + @ParameterizedTest + @MethodSource("spaceIdsSource") + @DisplayName("space IDs") + void spaceIds(String value) { + assertThrows(ValidationException.class, () -> space(value)); + } + + @ParameterizedTest + @MethodSource("messageIdsSource") + @DisplayName("space IDs") + void messageIds(String value) { + assertThrows(ValidationException.class, () -> message(value)); + } + + @SuppressWarnings("unused") // method is used as parameterized test source + private Stream spaceIdsSource() { + return Stream.of("spaces/", "spacs/12415"); + } + + @SuppressWarnings("unused") // method is used as parameterized test source + private Stream messageIdsSource() { + return Stream.of("spaces/", "spaces/qwe124", "spaces/eqwt23/messages/"); + } + } + + @Test + @DisplayName("create space ID") + void createSpaceId() { + var spaceId = "spaces/qew21466"; + var expectedSpace = SpaceId + .newBuilder() + .setValue(spaceId) + .buildPartial(); + assertThat(space(spaceId)) + .isEqualTo(expectedSpace); + } + + @Test + @DisplayName("create message ID") + void createMessageId() { + var messageId = "spaces/qew21466/messages/123112111"; + var expectedMessage = MessageId + .newBuilder() + .setValue(messageId) + .buildPartial(); + assertThat(message(messageId)) + .isEqualTo(expectedMessage); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/IncomingEventsHandlerTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/IncomingEventsHandlerTest.java new file mode 100644 index 00000000..b4f18fee --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/IncomingEventsHandlerTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.MessageId; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.incoming.ChatEvent; +import io.spine.chatbot.google.chat.incoming.EventType; +import io.spine.chatbot.google.chat.incoming.Message; +import io.spine.chatbot.google.chat.incoming.User; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import io.spine.chatbot.google.chat.incoming.event.BotRemovedFromSpace; +import io.spine.chatbot.google.chat.incoming.event.ChatEventReceived; +import io.spine.chatbot.google.chat.incoming.event.MessageReceived; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.google.chat.incoming.EventType.ADDED_TO_SPACE; +import static io.spine.chatbot.google.chat.incoming.EventType.MESSAGE; +import static io.spine.chatbot.google.chat.incoming.EventType.REMOVED_FROM_SPACE; +import static io.spine.chatbot.google.chat.incoming.SpaceType.ROOM; + +@DisplayName("`IncomingEventsHandler` should") +final class IncomingEventsHandlerTest extends GoogleChatContextAwareTest { + + private static final SpaceId space = space("spaces/fqeq325661a"); + private static final MessageId message = message("spaces/fqeq325661a/messages/422"); + + @Test + @DisplayName("add bot to a space") + void addBot() { + // given + var chatEventReceived = chatEventReceived(ADDED_TO_SPACE); + var botAddedToSpace = BotAddedToSpace + .newBuilder() + .setEvent(chatEventReceived.getEvent()) + .setSpace(space) + .vBuild(); + // when + context().receivesExternalEvent(chatEventReceived); + // then + context().assertEvent(botAddedToSpace); + } + + @Test + @DisplayName("remove bot from the space") + void removeBot() { + // given + var chatEventReceived = chatEventReceived(REMOVED_FROM_SPACE); + var botRemovedFromSpace = BotRemovedFromSpace + .newBuilder() + .setEvent(chatEventReceived.getEvent()) + .setSpace(space) + .vBuild(); + // when + context().receivesExternalEvent(chatEventReceived); + // then + context().assertEvent(botRemovedFromSpace); + } + + @Test + @DisplayName("receive incoming message") + void receiveIncomingMessage() { + // given + var chatEventReceived = chatEventReceived(MESSAGE); + var messageReceived = MessageReceived + .newBuilder() + .setEvent(chatEventReceived.getEvent()) + .setMessage(message) + .vBuild(); + // when + context().receivesExternalEvent(chatEventReceived); + // then + context().assertEvent(messageReceived); + } + + private static ChatEventReceived chatEventReceived(EventType type) { + return ChatEventReceived + .newBuilder() + .setEvent(chatEventOfType(type)) + .vBuild(); + } + + private static ChatEvent chatEventOfType(EventType type) { + return ChatEvent + .newBuilder() + .setSpace(chatSpace()) + .setType(type) + .setUser(sender()) + .setEventTime("2020-06-20T15:42:02Z") + .setMessage(chatMessage()) + .vBuild(); + } + + private static Message chatMessage() { + return Message + .newBuilder() + .setName(message.getValue()) + .setSender(sender()) + .setText("To be, or not to be, that is the question.") + .vBuild(); + } + + private static User sender() { + return User + .newBuilder() + .setName("users/00qwe123") + .vBuild(); + } + + private static io.spine.chatbot.google.chat.incoming.Space chatSpace() { + return io.spine.chatbot.google.chat.incoming.Space + .newBuilder() + .setName(space.getValue()) + .setType(ROOM) + .vBuild(); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/SpaceAggregateTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/SpaceAggregateTest.java new file mode 100644 index 00000000..f19ba354 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/SpaceAggregateTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.Space; +import io.spine.chatbot.google.chat.SpaceHeader; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.command.RegisterSpace; +import io.spine.chatbot.google.chat.event.SpaceRegistered; +import io.spine.chatbot.google.chat.incoming.ChatEvent; +import io.spine.chatbot.google.chat.incoming.User; +import io.spine.chatbot.google.chat.incoming.event.BotAddedToSpace; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.google.chat.incoming.EventType.ADDED_TO_SPACE; +import static io.spine.chatbot.google.chat.incoming.SpaceType.ROOM; + +@DisplayName("`SpaceAggregate` should") +final class SpaceAggregateTest extends GoogleChatContextAwareTest { + + private static final SpaceId SPACE = space("spaces/poqwdpQ21"); + private static final SpaceHeader HEADER = SpaceHeader + .newBuilder() + .setThreaded(true) + .setDisplayName("Spine Developers") + .vBuild(); + + @Nested + @DisplayName("register a space") + final class Register { + + @BeforeEach + void registerSpace() { + var registerSpace = RegisterSpace + .newBuilder() + .setId(SPACE) + .setHeader(HEADER) + .vBuild(); + context().receivesCommand(registerSpace); + } + + @Test + @DisplayName("producing `SpaceRegistered` event") + void producingEvent() { + var spaceRegistered = SpaceRegistered + .newBuilder() + .setSpace(SPACE) + .setHeader(HEADER) + .vBuild(); + context().assertEvent(spaceRegistered); + } + + @Test + @DisplayName("setting space state") + void settingState() { + var expectedState = Space + .newBuilder() + .setId(SPACE) + .setHeader(HEADER) + .vBuild(); + context().assertState(SPACE, Space.class) + .isEqualTo(expectedState); + } + } + + @Nested + @DisplayName("register a space when a bot is added to the space") + final class RegisterOnAddedBot { + + @BeforeEach + void addBotToSpace() { + var chatEvent = ChatEvent + .newBuilder() + .setSpace(chatSpace()) + .setUser(User.newBuilder() + .setName("users/12e1ojep1")) + .setType(ADDED_TO_SPACE) + .setEventTime("2020-06-19T15:39:01Z") + .vBuild(); + var botAddedToSpace = BotAddedToSpace + .newBuilder() + .setSpace(SPACE) + .setEvent(chatEvent) + .vBuild(); + context().receivesEvent(botAddedToSpace); + } + + @Test + @DisplayName("producing `SpaceRegistered` event") + void producingEvent() { + var spaceRegistered = SpaceRegistered + .newBuilder() + .setSpace(SPACE) + .setHeader(HEADER) + .vBuild(); + context().assertEvent(spaceRegistered); + } + + @Test + @DisplayName("setting space state") + void settingState() { + var expectedState = Space + .newBuilder() + .setId(SPACE) + .setHeader(HEADER) + .vBuild(); + context().assertState(SPACE, Space.class) + .isEqualTo(expectedState); + } + + private io.spine.chatbot.google.chat.incoming.Space chatSpace() { + return io.spine.chatbot.google.chat.incoming.Space + .newBuilder() + .setName(SPACE.getValue()) + .setDisplayName(HEADER.getDisplayName()) + .setType(ROOM) + .vBuild(); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadAggregateTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadAggregateTest.java new file mode 100644 index 00000000..37a2359c --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadAggregateTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.chatbot.google.chat.MessageId; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.event.MessageCreated; +import io.spine.chatbot.google.chat.event.ThreadCreated; +import io.spine.chatbot.google.chat.thread.Thread; +import io.spine.chatbot.google.chat.thread.ThreadResource; +import io.spine.chatbot.google.chat.thread.event.MessageAdded; +import io.spine.chatbot.google.chat.thread.event.ThreadInitialized; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.thread; +import static io.spine.chatbot.server.google.chat.ThreadResources.threadResource; + +@DisplayName("`ThreadAggregate` should") +final class ThreadAggregateTest extends GoogleChatContextAwareTest { + + @Nested + @DisplayName("initialize a thread") + final class InitThread { + + private final ThreadId thread = thread("SpineEventEngine/base"); + private final SpaceId space = space("spaces/qpojdwpiq1241"); + private final ThreadResource resource = + threadResource("spaces/qpojdwpiq1241/threads/qwdojp12"); + + @BeforeEach + void createThread() { + var threadCreated = ThreadCreated + .newBuilder() + .setThread(thread) + .setSpace(space) + .setResource(resource) + .vBuild(); + context().receivesEvent(threadCreated); + } + + @Test + @DisplayName("producing ThreadInitialized event") + void producingEvent() { + var threadInitialized = ThreadInitialized + .newBuilder() + .setThread(thread) + .setSpace(space) + .setResource(resource) + .vBuild(); + context().assertEvent(threadInitialized); + } + + @Test + @DisplayName("setting aggregate state") + void settingState() { + var state = Thread + .newBuilder() + .setId(thread) + .setSpace(space) + .setResource(resource) + .vBuild(); + context().assertState(thread, Thread.class) + .isEqualTo(state); + } + } + + @Nested + @DisplayName("add created message") + final class AddMessage { + + private final ThreadId thread = thread("SpineEventEngine/base"); + private final SpaceId space = space("spaces/qpojdwpiq1241"); + private final MessageId message = message("spaces/qpojdwpiq1241/messages/dqpwjpop12"); + private final ThreadResource threadResource = + threadResource("spaces/qpojdwpiq1241/threads/qwdojp12"); + + @BeforeEach + void createThreadAndMessage() { + var threadCreated = ThreadCreated + .newBuilder() + .setThread(thread) + .setSpace(space) + .setResource(threadResource) + .vBuild(); + var messageCreated = MessageCreated + .newBuilder() + .setMessage(message) + .setSpace(space) + .setThread(thread) + .vBuild(); + context().receivesEvent(threadCreated) + .receivesEvent(messageCreated); + } + + @Test + @DisplayName("producing `MessageAdded` event") + void producingEvent() { + var messageAdded = MessageAdded + .newBuilder() + .setMessage(message) + .setThread(thread) + .vBuild(); + context().assertEvent(messageAdded); + } + + @Test + @DisplayName("setting aggregate state") + void settingState() { + var state = Thread + .newBuilder() + .setId(thread) + .setSpace(space) + .setResource(threadResource) + .addMessage(message) + .vBuild(); + context().assertState(thread, Thread.class) + .isEqualTo(state); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadChatProcessTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadChatProcessTest.java new file mode 100644 index 00000000..ae10c71f --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadChatProcessTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.base.EventMessage; +import io.spine.chatbot.github.RepositoryId; +import io.spine.chatbot.github.repository.build.Build; +import io.spine.chatbot.github.repository.build.BuildStateChange; +import io.spine.chatbot.github.repository.build.event.BuildFailed; +import io.spine.chatbot.github.repository.build.event.BuildRecovered; +import io.spine.chatbot.google.chat.BuildStateUpdate; +import io.spine.chatbot.google.chat.SpaceId; +import io.spine.chatbot.google.chat.ThreadId; +import io.spine.chatbot.google.chat.event.MessageCreated; +import io.spine.chatbot.google.chat.event.ThreadCreated; +import io.spine.chatbot.google.chat.thread.ThreadChat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.spine.chatbot.github.GitHubIdentifiers.repository; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.message; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.space; +import static io.spine.chatbot.google.chat.GoogleChatIdentifiers.thread; +import static io.spine.chatbot.server.google.chat.ThreadResources.threadResource; + +@DisplayName("`ThreadChatProcess` should") +final class ThreadChatProcessTest { + + @SuppressWarnings("ClassCanBeStatic") // nested tests do not work with static classes + @Nested + @DisplayName("sent a message to the Google Chat room when build failed") + final class BuildIsFailed extends BuildStateChanged { + + @Override + EventMessage buildStateChangeEvent(RepositoryId repo, BuildStateChange stateChange) { + return BuildFailed + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + } + } + + @SuppressWarnings("ClassCanBeStatic") // nested tests do not work with static classes + @Nested + @DisplayName("sent a message to the Google Chat room when build recovered from failure") + final class BuildIsRecovered extends BuildStateChanged { + + @Override + EventMessage buildStateChangeEvent(RepositoryId repo, BuildStateChange stateChange) { + return BuildRecovered + .newBuilder() + .setRepository(repo) + .setChange(stateChange) + .vBuild(); + } + } + + private abstract static class BuildStateChanged extends GoogleChatContextAwareTest { + + private static final String buildNumber = "551"; + + private final RepositoryId repo = repository("SpineEventEngine/money"); + private final ThreadId thread = thread(repo.getValue()); + private final SpaceId space = space("spaces/1241pjwqe"); + + private final BuildStateUpdate stateUpdate = BuildStateUpdate + .newBuilder() + .setSpace(space) + .setMessage(message("spaces/1241pjwqe/messages/12154363643624")) + .setThread(thread) + .setResource(threadResource("spaces/1241pjwqe/threads/k12d1o2r1")) + .vBuild(); + + @BeforeEach + void receiveBuildStateChange() { + googleChatClient().setBuildStateUpdate(buildNumber, stateUpdate); + var newBuildState = Build + .newBuilder() + .setSpace(space) + .setNumber(buildNumber) + .vBuild(); + var buildStateChange = BuildStateChange + .newBuilder() + .setNewValue(newBuildState) + .vBuild(); + var buildFailed = buildStateChangeEvent(repo, buildStateChange); + context().receivesExternalEvent(buildFailed); + } + + abstract EventMessage buildStateChangeEvent(RepositoryId repo, + BuildStateChange stateChange); + + @Test + @DisplayName("producing `MessageCreated` and `ThreadCreated` events") + void producingEvents() { + var messageCreated = MessageCreated + .newBuilder() + .setMessage(stateUpdate.getMessage()) + .setThread(thread) + .setSpace(space) + .vBuild(); + var threadCreated = ThreadCreated + .newBuilder() + .setThread(thread) + .setResource(stateUpdate.getResource()) + .setSpace(space) + .vBuild(); + context().assertEvent(messageCreated); + context().assertEvent(threadCreated); + } + + @Test + @DisplayName("setting process state") + void settingState() { + var expectedState = ThreadChat + .newBuilder() + .setThread(thread) + .setSpace(space) + .setResource(stateUpdate.getResource()) + .vBuild(); + context().assertState(thread, ThreadChat.class) + .isEqualTo(expectedState); + } + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadResourcesTest.java b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadResourcesTest.java new file mode 100644 index 00000000..7c549ad7 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/server/google/chat/ThreadResourcesTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.server.google.chat; + +import io.spine.testing.UtilityClassTest; +import org.junit.jupiter.api.DisplayName; + +@DisplayName("`ThreadResources` should") +final class ThreadResourcesTest extends UtilityClassTest { + + ThreadResourcesTest() { + super(ThreadResources.class); + } +} diff --git a/google-chat-bot/src/test/java/io/spine/chatbot/travis/InMemoryTravisClient.java b/google-chat-bot/src/test/java/io/spine/chatbot/travis/InMemoryTravisClient.java new file mode 100644 index 00000000..b12d3573 --- /dev/null +++ b/google-chat-bot/src/test/java/io/spine/chatbot/travis/InMemoryTravisClient.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chatbot.travis; + +import io.spine.chatbot.CanFailFast; +import io.spine.chatbot.github.Slug; + +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.spine.protobuf.Messages.defaultInstance; +import static java.util.Collections.synchronizedMap; + +/** + * An in-memory test-only implementation of the Travis CI API client. + */ +public final class InMemoryTravisClient extends CanFailFast implements TravisClient { + + private final Map, TravisResponse> responses = synchronizedMap(new HashMap<>()); + + private InMemoryTravisClient(boolean failFast) { + super(failFast); + } + + /** + * Creates a {@link #CanFailFast#failFast failFast} in-memory Travis CI client. + */ + public static InMemoryTravisClient strictClient() { + return new InMemoryTravisClient(true); + } + + /** + * Creates a lenient in-memory Travis CI client. + */ + public static InMemoryTravisClient lenientClient() { + return new InMemoryTravisClient(false); + } + + @Override + public T execute(Query query) { + checkNotNull(query); + var stubbedValue = responses.get(query); + var responseType = query.responseType(); + var result = failOrDefault(stubbedValue, query, defaultInstance(responseType)); + return responseType.cast(result); + } + + /** + * Sets up a stub {@code branchBuild} response for a specified {@code repository}. + */ + public void setBuildsFor(Slug repository, RepoBranchBuildResponse branchBuild) { + checkNotNull(repository); + checkNotNull(branchBuild); + responses.put(BuildsQuery.forRepo(repository), branchBuild); + } + + /** + * Sets up a stub {@code repositories} response for a specified {@code owner}. + */ + public void setRepositoriesFor(Slug owner, RepositoriesResponse repos) { + checkNotNull(owner); + checkNotNull(repos); + responses.put(ReposQuery.forOwner(owner), repos); + } + + /** + * Resets state of the configured responses. + */ + public void reset() { + responses.clear(); + } +} diff --git a/google-chat-bot/src/test/resources/chat_event.json b/google-chat-bot/src/test/resources/chat_event.json new file mode 100644 index 00000000..4f0cb697 --- /dev/null +++ b/google-chat-bot/src/test/resources/chat_event.json @@ -0,0 +1,46 @@ +{ + "type": "MESSAGE", + "eventTime": "2017-03-02T19:02:59.910959Z", + "space": { + "name": "spaces/AAAAAAAAAAA", + "displayName": "Chuck Norris Discussion Room", + "type": "ROOM" + }, + "message": { + "name": "spaces/AAAAAAAAAAA/messages/CCCCCCCCCCC", + "sender": { + "name": "users/12345678901234567890", + "displayName": "Chuck Norris", + "avatarUrl": "https://lh3.googleusercontent.com/.../photo.jpg", + "email": "chuck@example.com" + }, + "createTime": "2017-03-02T19:02:59.910959Z", + "text": "@TestBot Violence is my last option.", + "argumentText": " Violence is my last option.", + "thread": { + "name": "spaces/AAAAAAAAAAA/threads/BBBBBBBBBBB" + }, + "annotations": [ + { + "length": 8, + "startIndex": 0, + "userMention": { + "type": "MENTION", + "user": { + "avatarUrl": "https://.../avatar.png", + "displayName": "TestBot", + "name": "users/1234567890987654321", + "type": "BOT" + } + }, + "type": "USER_MENTION" + } + ] + }, + "user": { + "name": "users/12345678901234567890", + "displayName": "Chuck Norris", + "avatarUrl": "https://lh3.googleusercontent.com/.../photo.jpg", + "email": "chuck@example.com" + } +} diff --git a/google-chat-bot/src/test/resources/pubsub_push_request.json b/google-chat-bot/src/test/resources/pubsub_push_request.json new file mode 100644 index 00000000..af5a0293 --- /dev/null +++ b/google-chat-bot/src/test/resources/pubsub_push_request.json @@ -0,0 +1,10 @@ +{ + "message": { + "data": "eyJrZXkiOiJ2YWx1ZSJ9", + "messageId": "450292511223766", + "message_id": "450292511223766", + "publishTime": "2020-06-21T20:48:25.908Z", + "publish_time": "2020-06-21T20:48:25.908Z" + }, + "subscription": "projects/test-project/subscriptions/test-subscription" +} diff --git a/gradle.properties b/gradle.properties index 988b791e..084555fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,20 @@ -micronautVersion=2.0.0.M3 +# +# Copyright 2020, TeamDev. All rights reserved. +# +# Redistribution and use in source and/or binary forms, with or without +# modification, must retain the above copyright notice and the following +# disclaimer. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +gcpProject='' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9ecfb344..7c0e0d60 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Mon May 25 21:30:17 EEST 2020 +#Mon Jun 15 15:39:45 EEST 2020 distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/http-client.env.json b/http-client.env.json new file mode 100644 index 00000000..68c063e6 --- /dev/null +++ b/http-client.env.json @@ -0,0 +1,6 @@ +{ + "travis": { + "host": "api.travis-ci.com", + "token": "" + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 61ffebc0..00000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name="chat-bot" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..e1f0c05a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +rootProject.name = "ChatBot" +include("google-chat-bot") diff --git a/src/main/java/io/spine/chatbot/Application.java b/src/main/java/io/spine/chatbot/Application.java deleted file mode 100644 index 45281e3b..00000000 --- a/src/main/java/io/spine/chatbot/Application.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.spine.chatbot; - -import io.micronaut.runtime.Micronaut; - -public class Application { - - public static void main(String[] args) { - Micronaut.run(Application.class, args); - } -} diff --git a/src/main/java/io/spine/chatbot/HelloController.java b/src/main/java/io/spine/chatbot/HelloController.java deleted file mode 100644 index e41de7bf..00000000 --- a/src/main/java/io/spine/chatbot/HelloController.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.spine.chatbot; - -import io.micronaut.http.annotation.*; - -@Controller("/hello") -public class HelloController { - - @Get(uri="/", produces="text/plain") - public String index() { - return "Example Response"; - } -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 8a4def16..00000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -micronaut: - application: - name: chatBot diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml deleted file mode 100644 index 42f90d8e..00000000 --- a/src/main/resources/log4j2.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/java/io.spine/.gitkeep b/src/test/java/io.spine/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/io/spine/chatbot/HelloFunctionTest.java b/src/test/java/io/spine/chatbot/HelloFunctionTest.java deleted file mode 100644 index 39a16676..00000000 --- a/src/test/java/io/spine/chatbot/HelloFunctionTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.spine.chatbot; - -import io.micronaut.context.ApplicationContext; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.client.HttpClient; -import io.micronaut.runtime.server.EmbeddedServer; -import io.micronaut.test.annotation.MicronautTest; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@MicronautTest -final class HelloFunctionTest { - - private static EmbeddedServer server; - private static HttpClient client; - - @BeforeAll - public static void setupServer() { - server = ApplicationContext.run(EmbeddedServer.class); - client = server - .getApplicationContext() - .createBean(HttpClient.class, server.getURL()); - } - - @AfterAll - public static void stopServer() { - if (server != null) { - server.stop(); - } - if (client != null) { - client.stop(); - } - } - - @Test - public void testFunction() { - String actual = client.toBlocking().retrieve(HttpRequest.GET("/hello")); - assertEquals("Example Response", actual); - } -} diff --git a/travis-ci.http b/travis-ci.http new file mode 100644 index 00000000..7b02c0a3 --- /dev/null +++ b/travis-ci.http @@ -0,0 +1,20 @@ +### List Travis CI builds for a repository + +GET https://{{host}}/repo/SpineEventEngine%2Fcore-java/builds?limit=3&branch.name=master&include=build.commit +Accept: application/json +Authorization: token {{token}} +Travis-API-Version: 3 + +### List Travis CI repositories for a user + +GET https://{{host}}/owner/SpineEventEngine/repos +Accept: application/json +Authorization: token {{token}} +Travis-API-Version: 3 + +### List Travis CI repository branch builds + +GET https://{{host}}/repo/SpineEventEngine%2Fcore-java/branch/master?include=build.commit,build.created_by +Accept: application/json +Authorization: token {{token}} +Travis-API-Version: 3 diff --git a/version.gradle.kts b/version.gradle.kts new file mode 100644 index 00000000..79c72ebb --- /dev/null +++ b/version.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * The version of the application. + */ + +val botVersion: String by extra("1.0.0")