diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a8232df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +max_line_length = 100 +indent_size = 4 +indent_style = space +insert_final_newline = true +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +# Specific settings for YAML files +[{*.yml,*.yaml}] +indent_size = 2 + +[*.{kt,kts}] +ktlint_standard_annotation = disabled +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_string-template-indent = disabled +ktlint_standard_import-ordering = disabled diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..4b51128 --- /dev/null +++ b/.env.sample @@ -0,0 +1,14 @@ +# Database +DB_USER=wire-poll-app +DB_PASSWORD=super-secret-wire-pwd +DB_URL=jdbc:postgresql://db:5432/poll-app +DB_NAME=poll-app + +# Init +SDK_APP_ID=myAppUuid +SDK_APP_TOKEN=myApiToken +API_HOST_URL=https://staging-nginz-https.zinfra.io +CRYPTO_PASSWORD=myDummyPasswordmyDummyPassword01 + +# SDK +WIRE_SDK_ENVIRONMENT=staging.zinfra.io diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..42a03ab --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @wireapp/integrations \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7f0b9ad --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maven updates + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "monthly" + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index db25aa4..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: CI - -on: - push: - branches-ignore: - - master - - staging - - pull_request: - -jobs: - check: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: Setup JDK - uses: actions/setup-java@v1 - with: - java-version: 11.0.6 - - - name: Execute test with gradle - run: ./gradlew test - - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: Poll Bot bare metal CI pipeline - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} - # Send message only if previous step failed - if: failure() - - docker-build: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - # setup docker actions https://github.com/docker/build-push-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Build image - id: docker_build - uses: docker/build-push-action@v2 - with: - # https://github.com/docker/build-push-action/issues/220 - context: . - tags: wire/ci-test-image - push: false - - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: Docker CI pipeline - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} - # Send message only if previous step failed - if: failure() diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..ac0ba57 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,47 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened] + +jobs: + claude-review: + if: github.actor != 'dependabot[bot]' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..fcbb1b5 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options + # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml deleted file mode 100644 index 468a0b5..0000000 --- a/.github/workflows/prod.yml +++ /dev/null @@ -1,189 +0,0 @@ -name: Release Pipeline - -on: - release: - types: [ published ] - -env: - DOCKER_IMAGE: wire-bot/poll - SERVICE_NAME: poll - -jobs: - deploy: - name: Build and deploy service - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: Set Release Version - # use latest tag as release version - run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV - - # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta - - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 - with: - images: eu.gcr.io/${{ env.DOCKER_IMAGE }} - - # setup docker actions https://github.com/docker/build-push-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - # login to GCR repo - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - registry: eu.gcr.io - username: _json_key - password: ${{ secrets.GCR_ACCESS_JSON }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} - # push only if this is indeed a taged release - push: ${{ startsWith(github.ref, 'refs/tags/') }} - build-args: | - release_version=${{ env.RELEASE_VERSION }} - - # Checkout our Kubernetes configuration - - name: Checkout Rubicon - uses: actions/checkout@v2 - with: - repository: zinfra/rubicon - # currently main branch is develop - ref: develop - path: rubicon - # private repo so use different git token - token: ${{ secrets.RUBICON_GIT_TOKEN }} - - # Update version to the one that was just built - - name: Change Version in Rubicon - env: - IMAGE: ${{ env.DOCKER_IMAGE }} - SERVICE: ${{ env.SERVICE_NAME }} - VERSION: ${{ env.RELEASE_VERSION }} - run: | - # go to directory with configuration - cd "rubicon/prod/services/$SERVICE" - # escape literals for the sed and set output with GCR - export SED_PREPARED=$(echo $IMAGE | awk '{ gsub("/", "\\/", $1); print "eu.gcr.io\\/"$1 }') - # update final yaml - sed -i".bak" "s/image: $SED_PREPARED.*/image: $SED_PREPARED:$VERSION/g" "$SERVICE.yaml" - # delete bakup file - rm "$SERVICE.yaml.bak" - - - name: Enable auth plugin - run: | - echo "USE_GKE_GCLOUD_AUTH_PLUGIN=True" >> $GITHUB_ENV - # Auth to GKE - - name: Authenticate to GKE - uses: google-github-actions/auth@v1 - with: - project_id: wire-bot - credentials_json: ${{ secrets.GKE_SA_KEY }} - service_account: kubernetes-deployment-agent@wire-bot.iam.gserviceaccount.com - - # Setup gcloud CLI - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v1 - - # Prepare components - - name: Prepare gcloud components - run: | - gcloud components install gke-gcloud-auth-plugin - gcloud components update - gcloud --quiet auth configure-docker - - # Get the GKE credentials so we can deploy to the cluster - - name: Obtain k8s credentials - env: - GKE_CLUSTER: anayotto - GKE_ZONE: europe-west1-c - run: | - gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE" - - # K8s is set up, deploy the app - - name: Deploy the Service - env: - SERVICE: ${{ env.SERVICE_NAME }} - run: | - kubectl apply -f "rubicon/prod/services/$SERVICE/$SERVICE.yaml" - - # Commit all data to Rubicon and open PR - - name: Create Rubicon Pull Request - uses: peter-evans/create-pull-request@v3 - with: - path: rubicon - branch: ${{ env.SERVICE_NAME }}-release - token: ${{ secrets.RUBICON_GIT_TOKEN }} - labels: version-bump, automerge - title: ${{ env.SERVICE_NAME }} release ${{ env.RELEASE_VERSION }} - commit-message: ${{ env.SERVICE_NAME }} version bump to ${{ env.RELEASE_VERSION }} - body: | - This is automatic version bump from the pipeline. - - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: ${{ env.SERVICE_NAME }} release pipeline - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_RELEASE }} - # Notify every release - if: always() - - quay_publish: - name: Quay Publish Pipeline - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - name: Set Release Version - # use latest tag as release version - run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV - - # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta - - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 - with: - images: quay.io/wire/poll-bot - - # setup docker actions https://github.com/docker/build-push-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - # login to GCR repo - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - registry: quay.io - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} - push: true - build-args: | - release_version=${{ env.RELEASE_VERSION }} - - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: ${{ env.SERVICE_NAME }} Quay Production Publish - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_RELEASE }} - # Send message only if previous step failed - if: always() diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..1c76738 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,32 @@ +name: Build + +on: + workflow_dispatch: + pull_request: + +jobs: + tests: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '21' + - name: Lint + run: | + ./gradlew ktlintCheck + - name: Detekt + run: | + ./gradlew detekt + - name: Build + run: | + ./gradlew build --info + - name: Store reports + if: failure() + uses: actions/upload-artifact@v7 + with: + name: reports + path: | + **/build/reports/ + **/build/test-results/ diff --git a/.github/workflows/quay.yml b/.github/workflows/quay.yml deleted file mode 100644 index 0f8d033..0000000 --- a/.github/workflows/quay.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Quay Deployment - -on: - # manual dispatch - workflow_dispatch: - inputs: - tag: - description: 'Docker image tag.' - required: true -jobs: - publish: - name: Deploy to staging - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta - - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 - with: - images: quay.io/wire/poll-bot - - # setup docker actions https://github.com/docker/build-push-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - # login to GCR repo - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - registry: quay.io - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - tags: quay.io/wire/poll-bot:${{ github.event.inputs.tag }} - labels: ${{ steps.docker_meta.outputs.labels }} - push: true - build-args: | - release_version=${{ github.event.inputs.tag }} - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: Poll Bot Quay Custom Tag Pipeline - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} - # Send message only if previous step failed - if: always() diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 0eae132..db927ac 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -1,101 +1,45 @@ -name: Staging Deployment +name: Build on: + workflow_dispatch: push: branches: - - staging + - main env: - DOCKER_IMAGE: wire-bot/poll - SERVICE_NAME: polls + REGISTRY: quay.io + IMAGE_NAME: wire/poll-app jobs: - publish: - name: Deploy to staging - runs-on: ubuntu-20.04 + deploy-staging: + runs-on: ubuntu-22.04 + if: github.actor != 'github-actions[bot]' steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v7 - # use latest tag as release version in the docker container - - name: Set Release Version - run: echo "RELEASE_VERSION=${GITHUB_SHA}" >> $GITHUB_ENV + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 - # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta - - name: Docker meta - id: docker_meta - uses: crazy-max/ghaction-docker-meta@v1 + - name: Login to Quay.io + uses: docker/login-action@v4 with: - images: eu.gcr.io/${{ env.DOCKER_IMAGE }} + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_ROBOT_TOKEN }} - # setup docker actions https://github.com/docker/build-push-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - # login to GCR repo - - name: Login to DockerHub - uses: docker/login-action@v1 + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v6 with: - registry: eu.gcr.io - username: _json_key - password: ${{ secrets.GCR_ACCESS_JSON }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: latest=false + tags: | + type=sha,format=short - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v7 with: - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} push: true - build-args: | - release_version=${{ env.RELEASE_VERSION }} - - - name: Enable auth plugin - run: | - echo "USE_GKE_GCLOUD_AUTH_PLUGIN=True" >> $GITHUB_ENV - # Auth to GKE - - name: Authenticate to GKE - uses: google-github-actions/auth@v1 - with: - project_id: wire-bot - credentials_json: ${{ secrets.GKE_SA_KEY }} - service_account: kubernetes-deployment-agent@wire-bot.iam.gserviceaccount.com - - # Setup gcloud CLI - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v1 - - # Prepare components - - name: Prepare gcloud components - run: | - gcloud components install gke-gcloud-auth-plugin - gcloud components update - gcloud --quiet auth configure-docker - - # Get the GKE credentials so we can deploy to the cluster - - name: Obtain k8s credentials - env: - GKE_CLUSTER: dagobah - GKE_ZONE: europe-west1-c - run: | - gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE" - - # K8s is set up, deploy the app - - name: Deploy the Service - env: - SERVICE: ${{ env.SERVICE_NAME }} - run: | - kubectl delete pod -l app=$SERVICE -n staging - kubectl describe pod -l app=$SERVICE -n staging - - # Send webhook to Wire using Slack Bot - - name: Webhook to Wire - uses: 8398a7/action-slack@v2 - with: - status: ${{ job.status }} - author_name: ${{ env.SERVICE_NAME }} staging pipeline - env: - SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} - # Send message only if previous step failed - if: always() - + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index fff8d5b..d7d6bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ .env +# Ignore Storage directory content but keep the directory +storage/* +!storage/.gitkeep + # Created by https://www.gitignore.io/api/java,kotlin,jetbrains+all # Edit at https://www.gitignore.io/?templates=java,kotlin,jetbrains+all @@ -99,6 +103,9 @@ cmake-build-*/ # IntelliJ out/ +# Mac OS +.DS_Store + # mpeltonen/sbt-idea plugin .idea_modules/ @@ -136,6 +143,8 @@ modules.xml # Sonarlint plugin .idea/sonarlint +CLAUDE.md + ### Kotlin ### # Compiled class file diff --git a/Dockerfile b/Dockerfile index 25d6a89..bd10c86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,23 @@ -FROM adoptopenjdk/openjdk11:alpine AS build -LABEL description="Wire Poll Bot" -LABEL project="wire-bots:polls" - -ENV PROJECT_ROOT /src -WORKDIR $PROJECT_ROOT - -# Copy gradle settings -COPY build.gradle.kts settings.gradle.kts gradle.properties gradlew $PROJECT_ROOT/ -# Make sure gradlew is executable -RUN chmod +x gradlew -# Copy gradle specification -COPY gradle $PROJECT_ROOT/gradle -# Download gradle -RUN ./gradlew --version -# download and cache dependencies -RUN ./gradlew resolveDependencies --no-daemon - -# Copy project and build -COPY . $PROJECT_ROOT -RUN ./gradlew distTar --no-daemon +FROM gradle:8.14.3-jdk21 AS build +LABEL description="Wire Poll App" +LABEL project="wire-apps:polls" -# Runtime -FROM adoptopenjdk/openjdk11:alpine-jre +WORKDIR /setup + +COPY . ./ -ENV APP_ROOT /app -WORKDIR $APP_ROOT +RUN ./gradlew clean shadowJar --no-daemon + +# Runtime +FROM eclipse-temurin:21-jre -# Obtain built from the base -COPY --from=build /src/build/distributions/app.tar $APP_ROOT/ +WORKDIR /app -# Extract executables -RUN mkdir $APP_ROOT/run -RUN tar -xvf app.tar --strip-components=1 -C $APP_ROOT/run +# Copy the fat jar from the build stage +COPY --from=build /setup/build/libs/poll-app*.jar /app/app.jar -# ------------------- Wire common ----------------- -# create version file -ARG release_version=development -ENV RELEASE_FILE_PATH=$APP_ROOT/run/release.txt -RUN echo $release_version > $RELEASE_FILE_PATH -# enable json logging -# TODO enable this once we fully migrate to JSON logging everywhere -# ENV JSON_LOGGING=true -# /------------------ Wire common ----------------- +RUN mkdir -p storage +# Run the application EXPOSE 8080 -ENTRYPOINT ["/bin/sh", "-c", "/app/run/bin/polls"] +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/Makefile b/Makefile index ecbefc8..86cd8ba 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,4 @@ db: docker-compose up -d db up: - docker-compose up -d db && docker-compose up bot - -docker-run: - docker run --rm -p 8080:8080 quay.io/wire/poll-bot - -docker-build: - docker build -t quay.io/wire/poll-bot:latest . - -publish: docker-build - docker push quay.io/wire/poll-bot:latest + docker-compose up diff --git a/README.md b/README.md index 1acec54..eaf800d 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,39 @@ -# Wire Poll Bot -[![GitHub version](https://badge.fury.io/gh/wireapp%2Fpoll-bot.svg)](https://badge.fury.io/gh/wireapp%2Fpoll-bot) -![CI](https://github.com/wireapp/poll-bot/workflows/CI/badge.svg) -![Docker Latest build](https://github.com/wireapp/poll-bot/workflows/Docker%20Latest%20build/badge.svg) -![Staging Deployment](https://github.com/wireapp/poll-bot/workflows/Staging%20Deployment/badge.svg) -![Release Pipeline](https://github.com/wireapp/poll-bot/workflows/Release%20Pipeline/badge.svg) +# Wire Poll App +[![GitHub version](https://badge.fury.io/gh/wireapp%2Fpoll-app.svg)](https://badge.fury.io/gh/wireapp%2Fpoll-app) +[![Build](https://github.com/wireapp/poll-app/actions/workflows/pull-request.yml/badge.svg)](https://github.com/wireapp/poll-app/actions/workflows/pull-request.yml) -[Wire](https://wire.com/) bot for the polls. This version is based on Roman therefore does not employ E2EE directly - -Service code to enable Poll bot in your team: -```bash -3131a0af-89d4-4569-b36d-50bcced4b1fb:163a422b-c314-4e34-95af-10b6b36cde18 -``` +[Wire](https://wire.com/) app for the polls. +Based on [Wire Applications JVM SDK](https://github.com/wireapp/wire-apps-jvm-sdk), which offers full support for MLS. +Previous [Poll Bot](https://github.com/wireapp/poll-bot/) implementation was based on [Roman](https://github.com/wireapp/roman) therefore did not employ E2EE directly, and worked with Proteus protocol only. ## Commands Basic usage * `/poll "Question" "Option 1" "Option 2"` will create poll -* `/stats` will send result of the **latest** poll in the conversation -* `/help` to show help -* `/version` prints the current version of the poll bot +* `/poll help` to show help +* `/poll version` prints the current version of the poll app ## Technologies used -* HTTP Server - [Ktor](https://ktor.io/) -* HTTP Client - [Apache](https://ktor.io/clients/http-client/engines.html) under [Ktor](https://ktor.io/) +* Kotlin + JDK 21 +* Ktor HTTP Server - [Ktor](https://ktor.io/) * Dependency Injection - [Kodein](https://github.com/Kodein-Framework/Kodein-DI) * Build system - [Gradle](https://gradle.org/) -* Communication with [Wire](https://wire.com/) - [Roman](https://github.com/dkovacevic/roman) - -Bot is using webhooks coming from Roman, for that, the bot needs to have public URL or IP address. +* Communication with [Wire Applications JVM SDK](https://github.com/wireapp/wire-apps-jvm-sdk) ## Usage -* The bot needs Postgres database up & running - we use one in [docker-compose.yml](docker-compose.yml), to start it up, you can use +* The app needs Postgres database up & running - we use one in [docker-compose.yml](docker-compose.yml), to start it up, you can use command `make db`. * To run the application execute `make run` or `./gradlew run`. * To run the application inside the docker compose environment run `make up`. For more details see [Makefile](Makefile). -## Docker Images - -Poll bot has public [docker image](https://quay.io/wire/poll-bot). -```bash -quay.io/wire/poll-bot -``` - -Tag `latest` is the latest release. [Releases](https://github.com/wireapp/poll-bot/releases) have then images with corresponding tag, so you -can always roll back. Tag `staging` is build from the latest commit in `staging` branch. - - -## Bot configuration +## App configuration Configuration is currently being loaded from the environment variables. -```kotlin - /** - * Username for the database. - */ - const val DB_USER = "DB_USER" - - /** - * Password for the database. - */ - const val DB_PASSWORD = "DB_PASSWORD" - - /** - * URL for the database. - * - * Example: - * `jdbc:postgresql://localhost:5432/bot-database` - */ - const val DB_URL = "DB_URL" - - /** - * Token which is used for the auth of proxy. - */ - const val SERVICE_TOKEN = "SERVICE_TOKEN" - - /** - * Domain used for sending the messages from the bot to proxy eg. "https://proxy.services.zinfra.io/api" - */ - const val PROXY_DOMAIN = "PROXY_DOMAIN" -``` - -Via the system variables - see [complete list](src/main/kotlin/com/wire/bots/polls/setup/EnvConfigVariables.kt). +Via the system variables - see [complete list](src/main/kotlin/com/wire/apps/polls/setup/EnvConfigVariables.kt). +And also the env variables required by `Wire Applications JVM SDK` ## Docker Compose -To run bot inside docker compose environment with default PostgreSQL database, -please create `.env` file in the root directory with the following variables: -```bash -# database -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_DB= - -# application -DB_USER= -DB_PASSWORD= -DB_URL= -SERVICE_TOKEN= -PROXY_DOMAIN= -``` - -Such configuration can look for example like that: - -```bash -# database -POSTGRES_USER=wire-poll-bot -POSTGRES_PASSWORD=super-secret-wire-pwd -POSTGRES_DB=poll-bot - -# application -DB_USER=wire-poll-bot -DB_PASSWORD=super-secret-wire-pwd -DB_URL=jdbc:postgresql://db:5432/poll-bot -SERVICE_TOKEN=x6jsd5vets967dsA01dz1cOl -APP_KEY=eyJhbGciOiJIUzM4NCJ9....... -PROXY_DOMAIN=https://proxy.services.zinfra.io/api -``` +To run app inside docker compose environment with default PostgreSQL database, +please create `.env` file in the root directory, starting from the .env.sample file. diff --git a/build.gradle.kts b/build.gradle.kts index fdd6482..5422f19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,19 @@ +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType + plugins { - kotlin("jvm") version "1.5.30" + kotlin("jvm") version "2.4.0" application - distribution - id("net.nemerosa.versioning") version "3.1.0" + id("com.gradleup.shadow") version "9.4.3" + id("org.jlleitschuh.gradle.ktlint") version "14.2.0" + id("io.gitlab.arturbosch.detekt") version "1.23.8" + id("net.nemerosa.versioning") version "4.0.1" } -group = "com.wire.bots.polls" +group = "com.wire.apps.polls" version = versioning.info?.tag ?: versioning.info?.lastTag ?: "development" -val mClass = "com.wire.bots.polls.PollBotKt" - application { - mainClass.set(mClass) + mainClass.set("com.wire.apps.polls.PollAppKt") } repositories { @@ -19,41 +21,28 @@ repositories { } dependencies { + implementation("com.wire", "wire-apps-jvm-sdk", "0.2.0") // stdlib implementation(kotlin("stdlib-jdk8")) // extension functions implementation("pw.forst", "katlib", "2.0.3") // Ktor server dependencies - val ktorVersion = "1.6.3" + val ktorVersion = "3.2.3" implementation("io.ktor", "ktor-server-core", ktorVersion) implementation("io.ktor", "ktor-server-netty", ktorVersion) - implementation("io.ktor", "ktor-jackson", ktorVersion) - implementation("io.ktor", "ktor-websockets", ktorVersion) - // explicitly set the reflect library to same version as the kotlin - implementation("org.jetbrains.kotlin", "kotlin-reflect", "1.5.0") - // Ktor client dependencies - implementation("io.ktor", "ktor-client-json", ktorVersion) - implementation("io.ktor", "ktor-client-jackson", ktorVersion) { - exclude("org.jetbrains.kotlin", "kotlin-reflect") - } - implementation("io.ktor", "ktor-client-apache", ktorVersion) - implementation("io.ktor", "ktor-client-logging-jvm", ktorVersion) - - // Prometheus metrics - implementation("io.ktor", "ktor-metrics-micrometer", ktorVersion) - implementation("io.micrometer", "micrometer-registry-prometheus", "1.6.6") // logging - implementation("io.github.microutils", "kotlin-logging", "2.0.6") - // if-else in logback.xml - implementation("org.codehaus.janino", "janino", "3.1.2") - implementation("ch.qos.logback", "logback-classic", "1.2.3") + implementation("net.logstash.logback", "logstash-logback-encoder", "8.1") + implementation("ch.qos.logback", "logback-classic", "1.5.19") + implementation("io.github.oshai", "kotlin-logging", "8.0.4") + + // metrics + implementation("io.ktor", "ktor-server-metrics-micrometer", ktorVersion) + implementation("io.micrometer", "micrometer-registry-prometheus", "1.16.0") // DI - val kodeinVersion = "7.5.0" - implementation("org.kodein.di", "kodein-di-jvm", kodeinVersion) - implementation("org.kodein.di", "kodein-di-framework-ktor-server-jvm", kodeinVersion) + implementation("org.kodein.di", "kodein-di-framework-ktor-server-jvm", "7.28.0") // database implementation("org.postgresql", "postgresql", "42.2.20") @@ -67,50 +56,51 @@ dependencies { // database migrations from the code implementation("org.flywaydb", "flyway-core", "7.8.2") + + // testing + testImplementation(kotlin("test")) + testImplementation("io.kotest", "kotest-assertions-core", "5.8.1") + testImplementation("io.mockk", "mockk", "1.13.16") + testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-test", "1.7.3") + testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.10.0") + testImplementation("io.ktor", "ktor-server-test-host", ktorVersion) } -tasks { - val jvmTarget = "1.8" - compileKotlin { - kotlinOptions.jvmTarget = jvmTarget - } - compileJava { - targetCompatibility = jvmTarget - } - compileTestKotlin { - kotlinOptions.jvmTarget = jvmTarget - } - compileTestJava { - targetCompatibility = jvmTarget +ktlint { + verbose.set(true) + outputToConsole.set(true) + coloredOutput.set(true) + reporters { + reporter(ReporterType.CHECKSTYLE) + reporter(ReporterType.JSON) + reporter(ReporterType.HTML) } +} - distTar { - archiveFileName.set("app.tar") - } +detekt { + toolVersion = "1.23.7" + config.setFrom(file("$rootDir/config/detekt/detekt.yml")) + baseline = file("$rootDir/config/detekt/baseline.xml") + parallel = true + buildUponDefaultConfig = true + source.setFrom("src/main/kotlin") +} - withType { - useJUnitPlatform() - } +kotlin { + jvmToolchain(21) +} - register("fatJar") { - manifest { - attributes["Main-Class"] = mClass - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - archiveFileName.set("polls.jar") - from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - from(sourceSets.main.get().output) - } +tasks.withType().configureEach { + useJUnitPlatform() +} - register("resolveDependencies") { - doLast { - project.allprojects.forEach { subProject -> - with(subProject) { - buildscript.configurations.forEach { if (it.isCanBeResolved) it.resolve() } - configurations.compileClasspath.get().resolve() - configurations.testCompileClasspath.get().resolve() - } - } - } +tasks { + named("shadowJar") { + mergeServiceFiles() + duplicatesStrategy = DuplicatesStrategy.INCLUDE + archiveBaseName = "poll-app" + } + build { + dependsOn(shadowJar) } } diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml new file mode 100644 index 0000000..5324b2b --- /dev/null +++ b/config/detekt/baseline.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..7465534 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,783 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: true + threshold: 2 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: false + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/commonTest/**', '**/jvmTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/docker-compose.yml b/docker-compose.yml index 704ba61..fa3206a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,27 +1,33 @@ -version: '3.8' services: - bot: + app: + platform: linux/amd64 # Allows using core-crypto on macOs build: context: . dockerfile: Dockerfile args: - release_version=docker-compose - image: lukaswire/polls - container_name: poll-bot + image: com.wire/poll-app + container_name: poll-app env_file: .env ports: - 8080:8080 + volumes: + - poll-app:/app/storage depends_on: - db db: image: postgres:13 - container_name: poll-bot-db - env_file: .env + container_name: poll-app-db + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} ports: - 5432:5432 volumes: - - poll-bot-db:/var/lib/postgresql/data/ + - poll-app-db:/var/lib/postgresql/data/ volumes: - poll-bot-db: + poll-app-db: + poll-app: diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..db6d194 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,114 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd3..8508ef6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/helm/pollapp/Chart.yaml b/helm/pollapp/Chart.yaml new file mode 100644 index 0000000..d9a912f --- /dev/null +++ b/helm/pollapp/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: pollapp +description: Wire Poll-App - A Helm chart for deploying the Wire Poll app +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/wireapp/poll-app +maintainers: + - name: Wire Integrations Team diff --git a/helm/pollapp/templates/NOTES.txt b/helm/pollapp/templates/NOTES.txt new file mode 100644 index 0000000..803a2db --- /dev/null +++ b/helm/pollapp/templates/NOTES.txt @@ -0,0 +1,68 @@ +Wire poll-app has been deployed successfully! + +The Wire Poll App is now running and will respond to these commands in Wire: + - /poll "Question" "Option 1" "Option 2" - Create a new poll + - /poll help - Show help information + - /poll version - Show the current version of the poll app + +AVAILABLE ENDPOINTS: + - GET /health - Server running + +DEPLOYMENT INFORMATION: +{{- if .Values.persistence.enabled }} +- Persistent storage: {{ .Values.persistence.size }} ({{ .Values.persistence.storageClass | default "default" }} storage class) +{{- end }} +- Health checks: Startup, liveness, and readiness probes configured +- Resource limits: {{ .Values.resources.limits.cpu }} CPU, {{ .Values.resources.limits.memory }} memory +- Service: {{ .Values.service.type }} on port {{ .Values.service.port }} -> {{ .Values.service.targetPort }} + +MONITORING & DEBUGGING: + +1. Check application status: + kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "pollapp.name" . }} + +2. View application logs: + kubectl logs -f deployment/{{ include "pollapp.fullname" . }} -n {{ .Release.Namespace }} + +3. Access health endpoint: +{{- if contains "ClusterIP" .Values.service.type }} + kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "pollapp.fullname" . }} 8080:{{ .Values.service.port }} + # Then visit: http://localhost:8080/health +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get -n {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pollapp.fullname" . }}) + export NODE_IP=$(kubectl get nodes -n {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + # Visit: http://$NODE_IP:$NODE_PORT/health +{{- else if contains "LoadBalancer" .Values.service.type }} + export SERVICE_IP=$(kubectl get svc -n {{ .Release.Namespace }} {{ include "pollapp.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + # Visit: http://$SERVICE_IP:{{ .Values.service.port }}/health +{{- end }} + +4. Check configuration: + kubectl describe configmap/{{ include "pollapp.fullname" . }} -n {{ .Release.Namespace }} +{{- if .Values.secrets.secretName }} + kubectl describe secret/{{ .Values.secrets.secretName }} -n {{ .Release.Namespace }} +{{- end }} + +CONFIGURATION: +{{- range .Values.env }} +{{- if eq .name "WIRE_SDK_ENVIRONMENT" }} +- Wire SDK Environment: {{ .value | default "Not configured" }} +{{- end }} +{{- if eq .name "DB_USER" }} +- Database user: {{ .value | default "Not configured" }} +{{- end }} +{{- if eq .name "DB_URL" }} +- Database Url: {{ .value | default "Not configured" }} +{{- end }} +{{- if eq .name "SDK_APP_ID" }} +- Wire SDK App Id: {{ .value | default "Not configured" }} +{{- end }} +{{- if eq .name "API_HOST_URL" }} +- Wire API Host Url: {{ .value | default "Not configured" }} +{{- end }} +{{- end }} +{{- if .Values.secrets.secretName }} +- Secrets mounted at: {{ .Values.secrets.mountPath }} +{{- end }} + +The app is ready to create and manage polls in Wire conversations! diff --git a/helm/pollapp/templates/_helpers.tpl b/helm/pollapp/templates/_helpers.tpl new file mode 100644 index 0000000..1c38c2b --- /dev/null +++ b/helm/pollapp/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "pollapp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "pollapp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "pollapp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "pollapp.labels" -}} +helm.sh/chart: {{ include "pollapp.chart" . }} +{{ include "pollapp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "pollapp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "pollapp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "pollapp.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "pollapp.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/pollapp/templates/configmap.yaml b/helm/pollapp/templates/configmap.yaml new file mode 100644 index 0000000..1e2e4f6 --- /dev/null +++ b/helm/pollapp/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "pollapp.fullname" . }}-config + labels: + {{- include "pollapp.labels" . | nindent 4 }} diff --git a/helm/pollapp/templates/pvc.yaml b/helm/pollapp/templates/pvc.yaml new file mode 100644 index 0000000..4289caa --- /dev/null +++ b/helm/pollapp/templates/pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "pollapp.fullname" . }}-pvc + annotations: + "helm.sh/resource-policy": keep + labels: + {{- include "pollapp.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} + storageClassName: {{ .Values.persistence.storageClass }} diff --git a/helm/pollapp/templates/service.yaml b/helm/pollapp/templates/service.yaml new file mode 100644 index 0000000..047292a --- /dev/null +++ b/helm/pollapp/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pollapp.fullname" . }} + labels: + {{- include "pollapp.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pollapp.selectorLabels" . | nindent 4 }} diff --git a/helm/pollapp/templates/serviceaccount.yaml b/helm/pollapp/templates/serviceaccount.yaml new file mode 100644 index 0000000..dd4dbf2 --- /dev/null +++ b/helm/pollapp/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "pollapp.serviceAccountName" . }} + labels: + {{- include "pollapp.labels" . | nindent 4 }} diff --git a/helm/pollapp/templates/servicemonitor.yaml b/helm/pollapp/templates/servicemonitor.yaml new file mode 100644 index 0000000..8345552 --- /dev/null +++ b/helm/pollapp/templates/servicemonitor.yaml @@ -0,0 +1,13 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "pollapp.fullname" . }} + labels: + {{- include "pollapp.labels" . | nindent 4 }} +spec: + endpoints: + - port: http + path: /metrics + selector: + matchLabels: + {{- include "pollapp.selectorLabels" . | nindent 6 }} diff --git a/helm/pollapp/templates/statefulset.yaml b/helm/pollapp/templates/statefulset.yaml new file mode 100644 index 0000000..d3d3fa2 --- /dev/null +++ b/helm/pollapp/templates/statefulset.yaml @@ -0,0 +1,103 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "pollapp.fullname" . }} + labels: + {{- include "pollapp.labels" . | nindent 4 }} +spec: + serviceName: {{ include "pollapp.fullname" . }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "pollapp.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "pollapp.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "pollapp.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + {{- if .Values.livenessProbe }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + {{- end }} + {{- if .Values.readinessProbe }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + {{- end }} + {{- if .Values.startupProbe }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + env: + {{- range .Values.env }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- if .Values.secrets.secretName }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.secrets.secretName }} + key: DB_PASSWORD + - name: SDK_APP_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.secrets.secretName }} + key: SDK_APP_TOKEN + - name: CRYPTO_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.secrets.secretName }} + key: CRYPTO_PASSWORD + {{- end }} + volumeMounts: + - name: data + mountPath: /app/storage + {{- if .Values.secrets.secretName }} + - name: secrets + mountPath: {{ .Values.secrets.mountPath }} + readOnly: true + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "pollapp.fullname" . }}-pvc + {{- if .Values.secrets.secretName }} + - name: secrets + secret: + secretName: {{ .Values.secrets.secretName }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/pollapp/values.yaml b/helm/pollapp/values.yaml new file mode 100644 index 0000000..344bf32 --- /dev/null +++ b/helm/pollapp/values.yaml @@ -0,0 +1,105 @@ +replicaCount: 1 + +image: + repository: pollapp + pullPolicy: IfNotPresent + tag: "latest" + +imagePullSecrets: [] +nameOverride: "poll-app" +fullnameOverride: "poll-app" + +serviceAccount: + create: true + annotations: {} + name: "poll-app-account" + +podAnnotations: {} + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + port: 80 + targetPort: 8080 + +# External secrets configuration +secrets: + secretName: "pollapp-secrets" + mountPath: "/etc/secrets" + +# Persistent storage configuration +persistence: + enabled: true + storageClass: "gp3-automode-nodepool" + accessMode: ReadWriteOnce + size: 1Gi + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 200m + memory: 256Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 1 + targetCPUUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Environment variables for the application +env: + - name: WIRE_SDK_ENVIRONMENT + value: "" + - name: DB_USER + value: "" + - name: DB_URL + value: "" + - name: SDK_APP_ID + value: "" + - name: API_HOST_URL + value: "" + +# Health check configuration (latest Kubernetes spec) +livenessProbe: + httpGet: + path: /health + port: http + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: /health + port: http + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + +startupProbe: + httpGet: + path: /health + port: http + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 18 diff --git a/settings.gradle.kts b/settings.gradle.kts index a3ce49f..7e45544 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1 @@ rootProject.name = "polls" - diff --git a/src/main/kotlin/com/wire/apps/polls/PollApp.kt b/src/main/kotlin/com/wire/apps/polls/PollApp.kt new file mode 100644 index 0000000..71eedd9 --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/PollApp.kt @@ -0,0 +1,14 @@ +package com.wire.apps.polls + +import com.wire.apps.polls.setup.init +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty + +fun main() { + embeddedServer( + factory = Netty, + port = 8080 + ) { + init() + }.start(wait = true) +} diff --git a/src/main/kotlin/com/wire/apps/polls/dao/DaoConstants.kt b/src/main/kotlin/com/wire/apps/polls/dao/DaoConstants.kt new file mode 100644 index 0000000..4764c07 --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/dao/DaoConstants.kt @@ -0,0 +1,5 @@ +package com.wire.apps.polls.dao + +internal const val UUID_LENGTH = 36 + +internal const val DOMAIN_LENGTH = 255 diff --git a/src/main/kotlin/com/wire/apps/polls/dao/DatabaseSetup.kt b/src/main/kotlin/com/wire/apps/polls/dao/DatabaseSetup.kt new file mode 100644 index 0000000..77395d1 --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/dao/DatabaseSetup.kt @@ -0,0 +1,28 @@ +package com.wire.apps.polls.dao + +import com.wire.apps.polls.setup.conf.DatabaseConfiguration +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.transaction + +/** + * Configure and check database + */ +object DatabaseSetup { + fun connect(dbConfiguration: DatabaseConfiguration) = + Database.connect( + url = dbConfiguration.url, + user = dbConfiguration.userName, + password = dbConfiguration.password, + driver = "org.postgresql.Driver" + ) + + /** + * Ensure DB connection is alive before performing operations + */ + fun isConnected() = + runCatching { + transaction { + this.connection.isClosed + } + }.isSuccess +} diff --git a/src/main/kotlin/com/wire/bots/polls/dao/Mentions.kt b/src/main/kotlin/com/wire/apps/polls/dao/Mentions.kt similarity index 50% rename from src/main/kotlin/com/wire/bots/polls/dao/Mentions.kt rename to src/main/kotlin/com/wire/apps/polls/dao/Mentions.kt index b970f9a..d83384e 100644 --- a/src/main/kotlin/com/wire/bots/polls/dao/Mentions.kt +++ b/src/main/kotlin/com/wire/apps/polls/dao/Mentions.kt @@ -1,21 +1,26 @@ -package com.wire.bots.polls.dao +package com.wire.apps.polls.dao import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.Column /** - * Table storing votes of the users. + * Table storing mentions of the users. */ object Mentions : IntIdTable("mentions") { /** * Id of the poll. */ - val pollId: Column = varchar("poll_id", 36) references Polls.id + val pollId: Column = varchar("poll_id", UUID_LENGTH) references Polls.id /** - * If of user that is mentioned. + * Id of user that is mentioned. */ - val userId: Column = varchar("user_id", 36) + val userId: Column = varchar("user_id", UUID_LENGTH) + + /** + * Domain of user that is mentioned. + */ + val domain: Column = varchar("domain", DOMAIN_LENGTH) /** * Where mention begins. diff --git a/src/main/kotlin/com/wire/bots/polls/dao/PollOptions.kt b/src/main/kotlin/com/wire/apps/polls/dao/PollOptions.kt similarity index 75% rename from src/main/kotlin/com/wire/bots/polls/dao/PollOptions.kt rename to src/main/kotlin/com/wire/apps/polls/dao/PollOptions.kt index d68db3f..377b144 100644 --- a/src/main/kotlin/com/wire/bots/polls/dao/PollOptions.kt +++ b/src/main/kotlin/com/wire/apps/polls/dao/PollOptions.kt @@ -1,4 +1,4 @@ -package com.wire.bots.polls.dao +package com.wire.apps.polls.dao import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table @@ -7,11 +7,10 @@ import org.jetbrains.exposed.sql.Table * Poll options. */ object PollOptions : Table("poll_option") { - /** * Id of the poll this option is for. UUID. */ - val pollId: Column = varchar("poll_id", 36) references Polls.id + val pollId: Column = varchar("poll_id", UUID_LENGTH) references Polls.id /** * Option order or option id. @@ -21,7 +20,8 @@ object PollOptions : Table("poll_option") { /** * Option content, the text inside the button/choice. */ - val optionContent: Column = varchar("option_content", 256) + private const val OPTION_LENGTH = 256 + val optionContent: Column = varchar("option_content", OPTION_LENGTH) override val primaryKey: PrimaryKey get() = PrimaryKey(pollId, optionOrder) diff --git a/src/main/kotlin/com/wire/apps/polls/dao/PollRepository.kt b/src/main/kotlin/com/wire/apps/polls/dao/PollRepository.kt new file mode 100644 index 0000000..3ac048e --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/dao/PollRepository.kt @@ -0,0 +1,182 @@ +package com.wire.apps.polls.dao + +import com.wire.apps.polls.dto.PollAction.VoteAction +import com.wire.apps.polls.dto.PollDto +import com.wire.apps.polls.dto.common.Mention +import com.wire.apps.polls.dto.common.Text +import org.jetbrains.exposed.sql.JoinType +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.or +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.update +import pw.forst.exposed.insertOrUpdate +import pw.forst.katlib.mapToSet + +/** + * Simple repository for handling database transactions on one place. + */ +@Suppress("TooManyFunctions") +class PollRepository { + /** + * Saves given poll to database and returns its id (same as the [pollId] parameter, + * but this design supports fluent style in the services.) + */ + suspend fun savePoll( + poll: PollDto, + pollId: String, + userId: String, + userDomain: String, + conversationId: String + ) = newSuspendedTransaction { + Polls.insert { + it[this.id] = pollId + it[this.ownerId] = userId + it[this.domain] = userDomain + it[this.isActive] = true + it[this.conversationId] = conversationId + it[this.body] = poll.question.data + it[this.overviewMessageId] = null + it[this.isResultVisible] = false + } + + Mentions.batchInsert(poll.question.mentions) { + this[Mentions.pollId] = pollId + this[Mentions.userId] = it.userId + this[Mentions.domain] = it.userDomain + this[Mentions.offset] = it.offset + this[Mentions.length] = it.length + } + + PollOptions.batchInsert( + poll.options + ) { + this[PollOptions.pollId] = pollId + this[PollOptions.optionOrder] = it.optionOrder + this[PollOptions.optionContent] = it.content + } + pollId + } + + suspend fun getPollQuestion(pollId: String) = + newSuspendedTransaction { + (Polls leftJoin Mentions) + .select { Polls.id eq pollId } + .groupBy( + { it[Polls.body] }, + { + if (it.getOrNull(Mentions.userId) != null) { + Mention( + userId = it[Mentions.userId], + userDomain = it[Mentions.domain], + offset = it[Mentions.offset], + length = it[Mentions.length] + ) + } else { + null + } + } + ).map { (pollBody, mentions) -> + Text(data = pollBody, mentions = mentions.filterNotNull()) + }.singleOrNull() + } + + suspend fun isPollMessage(messageId: String) = + newSuspendedTransaction { + Polls + .select { (Polls.id eq messageId) or (Polls.overviewMessageId eq messageId) } + .limit(1) + .any() + } + + /** + * Register new vote to the poll. If the poll with provided pollId does not exist, + * database contains foreign key to an option and poll so the SQL exception is thrown. + */ + suspend fun saveVote(voteAction: VoteAction) = + newSuspendedTransaction { + Votes.insertOrUpdate(Votes.pollId, Votes.userId) { + it[pollId] = voteAction.pollId + it[pollOption] = voteAction.optionIndex + it[userId] = voteAction.userId.id.toString() + } + } + + /** + * Retrieves stats for given pollId. + * + * Offset/option/button content as keys and count of the votes as values. + */ + suspend fun stats(pollId: String) = + newSuspendedTransaction { + PollOptions + .join( + otherTable = Votes, + joinType = JoinType.LEFT, + additionalConstraint = { + (Votes.pollId eq PollOptions.pollId) and + (Votes.pollOption eq PollOptions.optionOrder) + } + ).slice(PollOptions.optionOrder, PollOptions.optionContent, Votes.userId) + .select { PollOptions.pollId eq pollId } + // left join so sender can be null + .groupBy({ + it[PollOptions.optionOrder] to it[PollOptions.optionContent] + }, { it.getOrNull(Votes.userId) }) + .mapValues { (_, votingUsers) -> votingUsers.count { !it.isNullOrBlank() } } + } + + /** + * Returns set of user ids that voted in the poll with given pollId. + */ + suspend fun votingUsers(pollId: String) = + newSuspendedTransaction { + Votes + .slice(Votes.userId) + .select { Votes.pollId eq pollId } + .mapToSet { it[Votes.userId] } + } + + suspend fun getPollId(pollOverviewMessageId: String) = + newSuspendedTransaction { + Polls + .select { Polls.overviewMessageId eq pollOverviewMessageId } + .singleOrNull() + ?.get(Polls.id) + } + + suspend fun setResultVisibilityToTrue(pollId: String) = + newSuspendedTransaction { + Polls.update({ + Polls.id eq pollId + }) { it[this.isResultVisible] = true } + } + + suspend fun isResultVisible(pollId: String) = + newSuspendedTransaction { + Polls + .select { Polls.id eq pollId } + .single()[Polls.isResultVisible] + } + + suspend fun setOverviewMessageId( + pollId: String, + overviewMessageId: String? + ) = newSuspendedTransaction { + overviewMessageId ?: return@newSuspendedTransaction + + Polls.update({ + Polls.id eq pollId + }) { it[this.overviewMessageId] = overviewMessageId } + } + + suspend fun getOverviewMessageId(pollId: String) = + newSuspendedTransaction { + Polls + .select { Polls.id eq pollId } + .singleOrNull() + ?.get(Polls.overviewMessageId) + } +} diff --git a/src/main/kotlin/com/wire/bots/polls/dao/Polls.kt b/src/main/kotlin/com/wire/apps/polls/dao/Polls.kt similarity index 52% rename from src/main/kotlin/com/wire/bots/polls/dao/Polls.kt rename to src/main/kotlin/com/wire/apps/polls/dao/Polls.kt index 049e23c..1c00313 100644 --- a/src/main/kotlin/com/wire/bots/polls/dao/Polls.kt +++ b/src/main/kotlin/com/wire/apps/polls/dao/Polls.kt @@ -1,4 +1,4 @@ -package com.wire.bots.polls.dao +package com.wire.apps.polls.dao import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table @@ -12,19 +12,22 @@ object Polls : Table("polls") { /** * Id of the poll. UUID. */ - val id: Column = varchar("id", 36).uniqueIndex() + val id: Column = varchar("id", UUID_LENGTH).uniqueIndex() /** * Id of the user who created this poll. UUID. */ - val ownerId: Column = varchar("owner_id", 36) - + val ownerId: Column = varchar("owner_id", UUID_LENGTH) /** - * Id of the bot that created this poll. UUID. + * Id of the conversation where poll was created. UUID. */ - val botId: Column = varchar("bot_id", 36) + val conversationId: Column = varchar("conversation_id", UUID_LENGTH) + /** + * Domain of conversation. + */ + val domain: Column = varchar("domain", DOMAIN_LENGTH) /** * Determines whether is the pool active and whether new votes should be accepted. @@ -36,6 +39,17 @@ object Polls : Table("polls") { */ val body: Column = text("body", collate = null) + /** + * ID of a message storing how many users voted out of group size + */ + val overviewMessageId: Column = + varchar("overview_message_id", UUID_LENGTH).nullable() + + /** + * Determine if results are visible or hidden + */ + val isResultVisible: Column = bool("is_result_visible") + /** * Timestamp when was this poll created. */ diff --git a/src/main/kotlin/com/wire/bots/polls/dao/Votes.kt b/src/main/kotlin/com/wire/apps/polls/dao/Votes.kt similarity index 73% rename from src/main/kotlin/com/wire/bots/polls/dao/Votes.kt rename to src/main/kotlin/com/wire/apps/polls/dao/Votes.kt index 71e9323..2d45c33 100644 --- a/src/main/kotlin/com/wire/bots/polls/dao/Votes.kt +++ b/src/main/kotlin/com/wire/apps/polls/dao/Votes.kt @@ -1,4 +1,4 @@ -package com.wire.bots.polls.dao +package com.wire.apps.polls.dao import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table @@ -10,7 +10,7 @@ object Votes : Table("votes") { /** * Id of the poll. */ - val pollId: Column = varchar("poll_id", 36) + val pollId: Column = varchar("poll_id", UUID_LENGTH) /** * Id of the option. @@ -20,7 +20,7 @@ object Votes : Table("votes") { /** * User who voted for this option. */ - val userId: Column = varchar("user_id", 36) + val userId: Column = varchar("user_id", UUID_LENGTH) override val primaryKey: PrimaryKey get() = PrimaryKey(pollId, userId) diff --git a/src/main/kotlin/com/wire/apps/polls/dto/DtoToApiMappers.kt b/src/main/kotlin/com/wire/apps/polls/dto/DtoToApiMappers.kt new file mode 100644 index 0000000..c80546e --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/dto/DtoToApiMappers.kt @@ -0,0 +1,119 @@ +package com.wire.apps.polls.dto + +import com.wire.apps.polls.dto.common.Mention +import com.wire.apps.polls.dto.common.Text +import com.wire.apps.polls.dto.common.toWireMention +import com.wire.apps.polls.utils.RESULTS_BUTTON_ID +import com.wire.sdk.model.QualifiedId +import com.wire.sdk.model.WireMessage +import java.util.UUID +import pw.forst.katlib.newLine + +/** + * Creates message for poll. + */ +fun newPoll( + conversationId: QualifiedId, + body: String, + buttons: List