From 788ea470d24e0fb3e29c0944fc67198c36fa2ee3 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 01:54:01 +0300 Subject: [PATCH 01/59] feat: wip --- .github/actions/setup-terraform/action.yaml | 91 +++++ .github/workflow/deploy.yaml | 361 ++++++++++++++++++ .gitignore | 4 + _scripts/hook.sh | 16 +- _scripts/init.sh | 58 --- _scripts/migrate.sh | 47 --- _scripts/sync.sh | 26 -- domain/prayer.go | 1 + main.tf | 155 ++------ .../20251025195706_create_chats_table.sql | 21 + .../20251025195728_create_prayers_table.sql | 20 + revive.toml | 118 ++++++ .../dispatcher/internal/handler/command.go | 30 +- .../dispatcher/internal/handler/helper.go | 7 +- serverless/dispatcher/internal/service/db.go | 1 + 15 files changed, 686 insertions(+), 270 deletions(-) create mode 100644 .github/actions/setup-terraform/action.yaml create mode 100644 .github/workflow/deploy.yaml delete mode 100755 _scripts/init.sh delete mode 100644 _scripts/migrate.sh delete mode 100644 _scripts/sync.sh create mode 100644 migrations/20251025195706_create_chats_table.sql create mode 100644 migrations/20251025195728_create_prayers_table.sql create mode 100644 revive.toml diff --git a/.github/actions/setup-terraform/action.yaml b/.github/actions/setup-terraform/action.yaml new file mode 100644 index 0000000..b7addfb --- /dev/null +++ b/.github/actions/setup-terraform/action.yaml @@ -0,0 +1,91 @@ +name: 'Setup Terraform Environment' +description: 'Initialize Terraform with workspace, config, and SA key' +author: 'Your Name' + +inputs: + terraform_version: + description: 'Terraform version to install' + required: false + default: '1.6.3' + terraform_wrapper: + description: 'Enable Terraform wrapper' + required: false + default: 'false' + service_account_key: + description: 'Yandex Cloud Service Account Key (JSON)' + required: true + app_config: + description: 'Application config.json content' + required: true + config_file_path: + description: 'Path where config.json will be created' + required: false + default: 'config.json' + workspace: + description: 'Terraform workspace name' + required: true + aws_access_key_id: + description: 'AWS access key for S3 backend' + required: true + aws_secret_access_key: + description: 'AWS secret key for S3 backend' + required: true + cache_key: + description: 'Cache key for Terraform providers' + required: true + +outputs: + cache-hit: + description: 'Whether the cache was hit' + value: ${{ steps.cache.outputs.cache-hit }} + +runs: + using: 'composite' + steps: + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ inputs.terraform_version }} + terraform_wrapper: ${{ inputs.terraform_wrapper }} + + - name: Write service account key + shell: bash + run: | + echo "::add-mask::${{ inputs.service_account_key }}" + printf '%s' "${{ inputs.service_account_key }}" > iam.json + + - name: Write config.json + shell: bash + run: | + echo "::add-mask::${{ inputs.app_config }}" + printf '%s' "${{ inputs.app_config }}" > "${{ inputs.config_file_path }}" + echo "::notice::Application config written to: ${{ inputs.config_file_path }}" + + - name: Cache Terraform plugins + id: cache + uses: actions/cache@v3 + with: + path: | + .terraform + .terraform.lock.hcl + key: ${{ inputs.cache_key }} + restore-keys: | + ${{ inputs.cache_key }} + + - name: Terraform Init + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + AWS_ACCESS_KEY_ID: ${{ inputs.aws_access_key_id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.aws_secret_access_key }} + run: | + terraform init \ + -backend-config="access_key=$AWS_ACCESS_KEY_ID" \ + -backend-config="secret_key=$AWS_SECRET_ACCESS_KEY" + echo "::notice::Terraform initialized" + + - name: Select or Create Terraform Workspace + shell: bash + run: | + terraform workspace select ${{ inputs.workspace }} || terraform workspace new ${{ inputs.workspace }} + echo "::notice::Terraform workspace: ${{ inputs.workspace }}" \ No newline at end of file diff --git a/.github/workflow/deploy.yaml b/.github/workflow/deploy.yaml new file mode 100644 index 0000000..44a2dfc --- /dev/null +++ b/.github/workflow/deploy.yaml @@ -0,0 +1,361 @@ +name: Deploy to Yandex Cloud + +on: + push: + branches: + - main + - dev + workflow_dispatch: + inputs: + run_hook: + description: 'Run webhook setup' + required: false + type: boolean + default: false + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +env: + TERRAFORM_VERSION: 1.6.3 + GOOSE_VERSION: 3.16.0 + GO_VERSION: 1.21 + CACHE_PREFIX: ${{ runner.os }}-${{ github.ref_name }} + CONFIG_FILE_PATH: config.json + +jobs: + setup: + name: Setup Environment + runs-on: ubuntu-latest + outputs: + workspace: ${{ steps.set-workspace.outputs.workspace }} + environment: ${{ steps.set-workspace.outputs.environment }} + cache_key: ${{ steps.set-cache-key.outputs.cache_key }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Determine workspace and environment + id: set-workspace + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "workspace=prod" >> $GITHUB_OUTPUT + echo "environment=prod" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "workspace=dev" >> $GITHUB_OUTPUT + echo "environment=dev" >> $GITHUB_OUTPUT + fi + + - name: Set cache key + id: set-cache-key + run: | + CACHE_KEY="${{ env.CACHE_PREFIX }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" + echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT + echo "::notice::Cache key: $CACHE_KEY" + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + config_file_path: ${{ env.CONFIG_FILE_PATH }} + workspace: ${{ steps.set-workspace.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ steps.set-cache-key.outputs.cache_key }} + + validate: + name: Terraform Validate + runs-on: ubuntu-latest + needs: setup + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + config_file_path: ${{ env.CONFIG_FILE_PATH }} + workspace: ${{ needs.setup.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ needs.setup.outputs.cache_key }} + + - name: Terraform Format Check + run: terraform fmt -check -recursive + continue-on-error: false + + - name: Terraform Validate + run: terraform validate + + lint: + name: Code Linting + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ env.CACHE_PREFIX }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ env.CACHE_PREFIX }}-go- + + - name: Install revive + run: go install github.com/mgechev/revive@latest + + - name: Run revive - loader + run: | + cd serverless/loader + revive -config ../../revive.toml -formatter friendly ./... + + - name: Run revive - dispatcher + run: | + cd serverless/dispatcher + revive -config ../../revive.toml -formatter friendly ./... + + - name: Run revive - reminder + run: | + cd serverless/reminder + revive -config ../../revive.toml -formatter friendly ./... + + migrate: + name: Database Migration + runs-on: ubuntu-latest + needs: [setup, lint] + if: contains(github.event.head_commit.modified, 'migrations/') || contains(github.event.head_commit.added, 'migrations/') + environment: ${{ needs.setup.outputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check for migration changes + id: check-migrations + run: | + if git diff --name-only HEAD^ HEAD | grep -q '^migrations/'; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Install Goose + if: steps.check-migrations.outputs.changed == 'true' + run: | + curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | \ + GOOSE_INSTALL=$HOME/.goose sh -s v${{ env.GOOSE_VERSION }} + echo "$HOME/.goose" >> $GITHUB_PATH + + - name: Run Goose migrations + if: steps.check-migrations.outputs.changed == 'true' + env: + GOOSE_DRIVER: ydb + GOOSE_DBSTRING: ${{ secrets.DB_CONNECTION_STRING }} + run: | + if [ -z "$GOOSE_DBSTRING" ]; then + echo "::error::DB_CONNECTION_STRING secret is not set for ${{ needs.setup.outputs.environment }} environment" + exit 1 + fi + cd migrations + goose up + echo "Migration completed successfully" + + plan: + name: Terraform Plan + runs-on: ubuntu-latest + needs: [setup, lint] + environment: ${{ needs.setup.outputs.environment }} + outputs: + plan-exists: ${{ steps.plan.outputs.exitcode }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + config_file_path: ${{ env.CONFIG_FILE_PATH }} + workspace: ${{ needs.setup.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ needs.setup.outputs.cache_key }} + + - name: Terraform Plan + id: plan + env: + TF_VAR_cloud_id: ${{ secrets.CLOUD_ID }} + TF_VAR_folder_id: ${{ secrets.FOLDER_ID }} + run: | + terraform plan -out=tfplan -detailed-exitcode || EXIT_CODE=$? + echo "exitcode=$EXIT_CODE" >> $GITHUB_OUTPUT + + if [ $EXIT_CODE -eq 2 ]; then + echo "::notice::Changes detected in Terraform plan" + elif [ $EXIT_CODE -eq 0 ]; then + echo "::notice::No changes detected in Terraform plan" + else + echo "::error::Terraform plan failed" + exit 1 + fi + + - name: Upload plan artifact + if: steps.plan.outputs.exitcode == '2' + uses: actions/upload-artifact@v3 + with: + name: tfplan-${{ needs.setup.outputs.workspace }} + path: tfplan + retention-days: 7 + + apply: + name: Terraform Apply + runs-on: ubuntu-latest + needs: [setup, plan, migrate] + if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') + environment: ${{ needs.setup.outputs.environment }} + outputs: + dispatcher_fn_id: ${{ steps.outputs.outputs.dispatcher_fn_id }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + terraform_wrapper: 'false' + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + config_file_path: ${{ env.CONFIG_FILE_PATH }} + workspace: ${{ needs.setup.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ needs.setup.outputs.cache_key }} + + - name: Backup current state + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + mkdir -p state-backups + terraform state pull > state-backups/terraform-${{ needs.setup.outputs.workspace }}-${TIMESTAMP}-${{ github.run_id }}.tfstate + + - name: Upload state backup + uses: actions/upload-artifact@v3 + with: + name: terraform-state-backup-${{ needs.setup.outputs.workspace }}-${{ github.run_id }} + path: state-backups/ + retention-days: 90 + + - name: Download plan artifact + if: needs.plan.outputs.plan-exists == '2' + uses: actions/download-artifact@v3 + with: + name: tfplan-${{ needs.setup.outputs.workspace }} + + - name: Terraform Apply + env: + TF_VAR_cloud_id: ${{ secrets.CLOUD_ID }} + TF_VAR_folder_id: ${{ secrets.FOLDER_ID }} + run: | + if [ -f tfplan ]; then + terraform apply -auto-approve tfplan + else + echo "No changes to apply" + fi + + - name: Export Terraform Outputs + id: outputs + run: | + DISPATCHER_FN_ID=$(terraform output -raw dispatcher_fn_id) + echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT + echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" + + hook: + name: Setup Telegram Webhooks + runs-on: ubuntu-latest + needs: [setup, apply] + if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_hook == 'true' && needs.apply.result == 'success' + environment: ${{ needs.setup.outputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + terraform_wrapper: 'false' + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + config_file_path: ${{ env.CONFIG_FILE_PATH }} + workspace: ${{ needs.setup.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ needs.setup.outputs.cache_key }} + + - name: Execute webhook setup script + env: + DISPATCHER_FUNCTION_ID: ${{ needs.apply.outputs.dispatcher_fn_id }} + run: | + echo "::notice::Config file: $CONFIG_FILE_PATH" + echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" + + chmod +x _scripts/hook.sh + bash _scripts/hook.sh + continue-on-error: false + + - name: Verify webhook setup + run: | + echo "::notice::Webhook setup completed successfully for ${{ needs.setup.outputs.workspace }} environment" + + summary: + name: Deployment Summary + runs-on: ubuntu-latest + needs: [setup, validate, lint, migrate, plan, apply, hook] + if: always() + steps: + - name: Generate summary + run: | + echo "## Deployment Summary - ${{ needs.setup.outputs.workspace }} environment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Validate | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Code Linting | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Database Migration | ${{ needs.migrate.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Terraform Plan | ${{ needs.plan.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Terraform Apply | ${{ needs.apply.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Webhook Setup | ${{ needs.hook.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Workspace:** ${{ needs.setup.outputs.workspace }}" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "**Cache Key:** ${{ needs.setup.outputs.cache_key }}" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.apply.outputs.dispatcher_fn_id }}" != "" ]; then + echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_fn_id }}" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "**Manual Trigger:** Webhook setup requested: ${{ github.event.inputs.run_hook }}" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 100bebf..79acaa1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ _config *.tfstate.* *.tfvars *.zip + +# secret +iam.json +key.json diff --git a/_scripts/hook.sh b/_scripts/hook.sh index 63decdf..a9edd4c 100644 --- a/_scripts/hook.sh +++ b/_scripts/hook.sh @@ -1,11 +1,21 @@ #!/bin/bash -DISPATCHER_ENDPOINT="https://functions.yandexcloud.net/$(terraform output -raw dispatcher_fn_id)" -export DISPATCHER_ENDPOINT +if [[ -z "$CONFIG_FILE_PATH" ]]; then + echo "[ERROR] CONFIG_FILE_PATH is not set" + exit 1 +fi -CONFIG_FILE="_config/$(terraform workspace show)/config.json" +if [[ -z "$DISPATCHER_FUNCTION_ID" ]]; then + echo "[ERROR] DISPATCHER_FUNCTION_ID is not set" + exit 1 +fi + +export DISPATCHER_ENDPOINT="https://functions.yandexcloud.net/${DISPATCHER_FUNCTION_ID}" + +CONFIG_FILE="$CONFIG_FILE_PATH" echo "[INFO] using config file: \"$CONFIG_FILE\"" +echo "[INFO] using dispatcher function ID: \"$DISPATCHER_FUNCTION_ID\"" jq -c 'to_entries[]' "$CONFIG_FILE" | while read -r entry; do BOT_ID=$(echo "$entry" | jq -r '.value.bot_id') diff --git a/_scripts/init.sh b/_scripts/init.sh deleted file mode 100755 index d7fe655..0000000 --- a/_scripts/init.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# This script fetches the `access token`, `cloudID`, and `folderID` from Yandex Cloud -# and stores them in a `terraform.tfvars` file. - -set -e - -# check input arguments -if [ "$#" -ne 2 ]; then - echo "usage: $0 " - exit 1 -fi - -CLOUD_NAME="$1" -FOLDER_NAME="$2" -TFVARS_FILE="terraform.tfvars" - - -# get access token -echo "fetching access token" -ACCESS_TOKEN=$(yc iam create-token) -if [ -z "$ACCESS_TOKEN" ]; then - echo "error: failed to get access token" - exit 1 -fi - -# get cloud_id by cloud name -echo "fetching cloud id for cloud name '$CLOUD_NAME'" -CLOUD_ID=$(curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - https://resource-manager.api.cloud.yandex.net/resource-manager/v1/clouds \ - | jq -r --arg CLOUD_NAME "$CLOUD_NAME" '.clouds[] | select(.name == $CLOUD_NAME) | .id') - -if [ -z "$CLOUD_ID" ]; then - echo "error: could not find cloud with name '$CLOUD_NAME'" - exit 1 -fi - -# get folder_id by folder name -echo "fetching folder id for folder name '$FOLDER_NAME' in cloud '$CLOUD_NAME'" -FOLDER_ID=$(curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" \ - -G https://resource-manager.api.cloud.yandex.net/resource-manager/v1/folders \ - --data-urlencode "cloudId=${CLOUD_ID}" \ - | jq -r --arg FOLDER_NAME "$FOLDER_NAME" '.folders[] | select(.name == $FOLDER_NAME) | .id') - -if [ -z "$FOLDER_ID" ]; then - echo "error: could not find folder with name '$FOLDER_NAME' in cloud '$CLOUD_NAME'" - exit 1 -fi - -# replace values in terraform.tfvars -echo "updating $TFVARS_FILE" - -echo "access_token = \"$ACCESS_TOKEN\"" > terraform.tfvars -echo "cloud_id = \"$CLOUD_ID\"" >> terraform.tfvars -echo "folder_id = \"$FOLDER_ID\"" >> terraform.tfvars - -echo "terraform.tfvars updated successfully" - diff --git a/_scripts/migrate.sh b/_scripts/migrate.sh deleted file mode 100644 index f51f69f..0000000 --- a/_scripts/migrate.sh +++ /dev/null @@ -1,47 +0,0 @@ -ENV="dev" -ENDPOINT="grpcs://ydb.serverless.yandexcloud.net:2135" - -DB_PATH=" -{ - \"dev\": { - \"source\": \"/REGION/XXX/YYY\", - \"target\": \"/REGION/XXX/YYY\" - }, - \"prod\": { - \"source\": \"/REGION/XXX/YYY\", - \"target\": \"/REGION/XXX/YYY\" - } -} -" - -echo "[INFO] Using environment: $ENV" - -DB_PATH_SOURCE=$(echo -n "$DB_PATH" | jq -r ".\"$ENV\".source") -DB_PATH_TARGET=$(echo -n "$DB_PATH" | jq -r ".\"$ENV\".target") - -if [ -z "$DB_PATH_SOURCE" ] || [ -z "$DB_PATH_TARGET" ]; then - echo "[ERROR] Invalid environment: $ENV" - exit 1 -fi - -echo "[INFO] Source database path: \"$DB_PATH_SOURCE\"" -echo "[INFO] Target database path: \"$DB_PATH_TARGET\"" - -ydb config profile replace source-db -d "$DB_PATH_SOURCE" -e "$ENDPOINT" --token-file "./source.txt" -echo "[INFO] Source database profile created" - -ydb config profile replace target-db -d "$DB_PATH_TARGET" -e "$ENDPOINT" --token-file "./target.txt" -echo "[INFO] Target database profile created" - -DIR="$(eval echo ~$USER)/ydb_dump/$ENV/chats" -echo "[INFO] Using directory: $DIR" - -echo "[INFO] Recreating directory: $DIR" -rm -rf "$DIR" -mkdir -p "$DIR" - -echo "[INFO] Dumping source database..." -ydb --profile source-db tools dump --path chats --output "$DIR" - -echo "[INFO] Restoring to target database..." -ydb --profile target-db tools restore --path "$DB_PATH_TARGET" --input "$DIR" diff --git a/_scripts/sync.sh b/_scripts/sync.sh deleted file mode 100644 index c24587f..0000000 --- a/_scripts/sync.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -set -euo pipefail - -TERRAFORM_WORKSPACE=$(terraform workspace show) -REGION=$(terraform output -raw region) - -BUCKET_NAME="prayer-bot-bucket-${TERRAFORM_WORKSPACE}" -S3_ENDPOINT="https://storage.yandexcloud.net" - -LOCAL_DIR="_config/${TERRAFORM_WORKSPACE}" - -mkdir -p "$LOCAL_DIR" - -echo "[INFO] Syncing bucket '${BUCKET_NAME}' into '${LOCAL_DIR}'..." - -export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" -export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" - -if [[ -z "$AWS_ACCESS_KEY_ID" || -z "$AWS_SECRET_ACCESS_KEY" ]]; then - echo "[ERROR] AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set as environment variables or have default values." - exit 1 -fi - -aws --endpoint-url "$S3_ENDPOINT" --region "$REGION" s3 sync "s3://${BUCKET_NAME}/" "$LOCAL_DIR/" - -echo "[INFO] sync complete!" diff --git a/domain/prayer.go b/domain/prayer.go index 230380d..a0a092a 100644 --- a/domain/prayer.go +++ b/domain/prayer.go @@ -27,6 +27,7 @@ type PrayerDay struct { Isha time.Time `json:"isha"` } +//revive:disable:argument-limit func NewPrayerDay( date time.Time, fajr time.Time, diff --git a/main.tf b/main.tf index 4476924..eae25fd 100644 --- a/main.tf +++ b/main.tf @@ -4,12 +4,30 @@ terraform { source = "yandex-cloud/yandex" } } + + backend "s3" { + endpoints = { + s3 = "https://storage.yandexcloud.net" + } + region = "ru-central1" + + bucket = "escalopa-tfstate" + key = "prayer-bot/terraform.tfstate" + + workspace_key_prefix = "" + + skip_region_validation = true + skip_credentials_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + skip_s3_checksum = true + } + required_version = ">= 0.13" } -variable "access_token" { - type = string - sensitive = true +locals { + env = terraform.workspace } variable "cloud_id" { @@ -25,12 +43,8 @@ variable "region" { default = "ru-central1-d" } -output "region" { - value = var.region -} - provider "yandex" { - token = var.access_token + service_account_key_file = "iam.json" cloud_id = var.cloud_id folder_id = var.folder_id zone = var.region @@ -41,17 +55,21 @@ provider "yandex" { ########################### resource "yandex_storage_bucket" "bucket" { - bucket = "prayer-bot-bucket-${terraform.workspace}" + bucket = "prayer-bot-bucket-${local.env}" folder_id = var.folder_id max_size = 1073741824 # 1 GB in bytes } +output "bucket_name" { + value = yandex_storage_bucket.bucket.bucket +} + ########################### ### ydb ########################### resource "yandex_ydb_database_serverless" "ydb" { - name = "prayer-bot-ydb-${terraform.workspace}" + name = "prayer-bot-ydb-${local.env}" location_id = "ru-central1" folder_id = var.folder_id deletion_protection = true @@ -62,108 +80,9 @@ resource "yandex_ydb_database_serverless" "ydb" { } } -resource "yandex_ydb_table" "ydb_table_chats" { - connection_string = yandex_ydb_database_serverless.ydb.ydb_full_endpoint - path = "chats" - - primary_key = ["bot_id", "chat_id"] - - column { - name = "chat_id" - type = "Int64" - } - - column { - name = "bot_id" - type = "Int64" - } - - column { - name = "language_code" - type = "Utf8" - } - - column { - name = "state" - type = "Utf8" - } - - column { - name = "reminder_offset" - type = "Int32" - } - - column { - name = "reminder_message_id" - type = "Int32" - } - - column { - name = "jamaat" - type = "Bool" - } - - column { - name = "jamaat_message_id" - type = "Int32" - } - - column { - name = "subscribed" - type = "Bool" - } - - column { - name = "subscribed_at" - type = "Datetime" - } - - column { - name = "created_at" - type = "Datetime" - } -} - -resource "yandex_ydb_table" "ydb_table_prayers" { - connection_string = yandex_ydb_database_serverless.ydb.ydb_full_endpoint - path = "prayers" - - primary_key = ["bot_id", "prayer_date"] - - column { - name = "bot_id" - type = "Int64" - } - - column { - name = "prayer_date" - type = "Date" - } - - column { - name = "fajr" - type = "Datetime" - } - column { - name = "shuruq" - type = "Datetime" - } - column { - name = "dhuhr" - type = "Datetime" - } - column { - name = "asr" - type = "Datetime" - } - column { - name = "maghrib" - type = "Datetime" - } - column { - name = "isha" - type = "Datetime" - } +output "ydb_connection_string" { + value = yandex_ydb_database_serverless.ydb.ydb_full_endpoint + sensitive = true } ########################### @@ -222,10 +141,10 @@ resource "yandex_function" "loader_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.loader_sa.id folder_id = var.folder_id - user_hash = "v1" + user_hash = filemd5(data.archive_file.loader_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/_config/${terraform.workspace}/config.json") + APP_CONFIG = file("${path.module}/config.json") S3_ENDPOINT = "https://storage.yandexcloud.net" YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint @@ -299,10 +218,10 @@ resource "yandex_function" "dispatcher_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.dispatcher_sa.id folder_id = var.folder_id - user_hash = "v3" + user_hash = filemd5(data.archive_file.dispatcher_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/_config/${terraform.workspace}/config.json") + APP_CONFIG = file("${path.module}/config.json") YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint @@ -356,10 +275,10 @@ resource "yandex_function" "reminder_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.reminder_sa.id folder_id = var.folder_id - user_hash = "v1" + user_hash = filemd5(data.archive_file.reminder_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/_config/${terraform.workspace}/config.json") + APP_CONFIG = file("${path.module}/config.json") YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint diff --git a/migrations/20251025195706_create_chats_table.sql b/migrations/20251025195706_create_chats_table.sql new file mode 100644 index 0000000..e3ea6e6 --- /dev/null +++ b/migrations/20251025195706_create_chats_table.sql @@ -0,0 +1,21 @@ +-- +goose Up +CREATE TABLE chats ( + chat_id Int64 NOT NULL, + bot_id Int64 NOT NULL, + language_code Utf8, + state Utf8, + reminder Json, + is_group Bool, + subscribed Bool, + subscribed_at Datetime, + created_at Datetime, + PRIMARY KEY (bot_id, chat_id) +); + +-- +goose StatementBegin +CREATE INDEX idx_chats_bot_id ON chats (bot_id); +CREATE INDEX idx_chats_chat_id ON chats (chat_id); +-- +goose StatementEnd + +-- +goose Down +DROP TABLE IF EXISTS chats; \ No newline at end of file diff --git a/migrations/20251025195728_create_prayers_table.sql b/migrations/20251025195728_create_prayers_table.sql new file mode 100644 index 0000000..176ad79 --- /dev/null +++ b/migrations/20251025195728_create_prayers_table.sql @@ -0,0 +1,20 @@ +-- +goose Up +CREATE TABLE prayers ( + bot_id Int64 NOT NULL, + prayer_date Date NOT NULL, + fajr Datetime, + shuruq Datetime, + dhuhr Datetime, + asr Datetime, + maghrib Datetime, + isha Datetime, + PRIMARY KEY (bot_id, prayer_date) +); + +-- +goose StatementBegin +CREATE INDEX idx_prayers_bot_id ON prayers (bot_id); +CREATE INDEX idx_prayers_date ON prayers (prayer_date); +-- +goose StatementEnd + +-- +goose Down +DROP TABLE IF EXISTS prayers; \ No newline at end of file diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000..d500e70 --- /dev/null +++ b/revive.toml @@ -0,0 +1,118 @@ +# Revive configuration file for Go linting +# Place this file in the root of your repository + +# When set to false, ignores files with "GENERATED" header, similar to golint +ignoreGeneratedHeader = true + +# Sets the default severity to "warning" +severity = "warning" + +# Sets the default confidence to 0.8 +confidence = 0.8 + +# Sets the error code for failures with severity "error" +errorCode = 1 + +# Sets the error code for failures with severity "warning" +warningCode = 0 + +# Formatting rules +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +disabled = true +[rule.if-return] +[rule.increment-decrement] +[rule.var-naming] +[rule.var-declaration] +[rule.package-comments] +disabled = true +[rule.range] +[rule.receiver-naming] +[rule.time-naming] +[rule.unexported-return] +[rule.indent-error-flow] +[rule.errorf] +[rule.empty-block] +[rule.superfluous-else] +[rule.unused-parameter] +[rule.unreachable-code] +[rule.redefines-builtin-id] + +# Code complexity rules +[rule.function-result-limit] +arguments = [3] + +[rule.argument-limit] +arguments = [5] + +[rule.cyclomatic] +arguments = [15] + +[rule.max-public-structs] +arguments = [10] + +[rule.file-header] +disabled = true + +# Naming conventions +[rule.confusing-naming] +severity = "warning" + +[rule.confusing-results] +severity = "warning" + +[rule.bool-literal-in-expr] +[rule.constant-logical-expr] + +# Comments and documentation +[rule.comment-spacings] +arguments = ["//"] + +# Security and best practices +[rule.defer] +arguments = [["call-chain", "loop"]] + +[rule.string-of-int] + +# Code style +[rule.modifies-parameter] +[rule.modifies-value-receiver] +[rule.early-return] +[rule.unconditional-recursion] +[rule.identical-branches] +[rule.struct-tag] +[rule.range-val-in-closure] +[rule.range-val-address] +[rule.waitgroup-by-value] +[rule.atomic] +[rule.empty-lines] +[rule.cognitive-complexity] +arguments = [20] + +# Performance +[rule.string-format] +[rule.deep-exit] +[rule.unnecessary-stmt] + +# Optional rules (uncomment to enable) +# [rule.line-length-limit] +# arguments = [120] + +# [rule.function-length] +# arguments = [50, 0] + +# [rule.imports-blacklist] +# arguments = ["crypto/md5", "crypto/sha1"] + +# [rule.add-constant] +# arguments = [{maxLitCount = "3", allowStrs = "\"\"", allowInts = "0,1,2", allowFloats = "0.0,1.0"}] + +# Exclude patterns (if needed) +# [rule.exclude] +# arguments = ["^//go:generate"] \ No newline at end of file diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index 1ee6920..38cd4e7 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -66,7 +66,7 @@ func (h *Handler) start(ctx context.Context, b *bot.Bot, update *models.Update) return h.help(ctx, b, update) } -func (h *Handler) help(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) help(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -84,7 +84,7 @@ func (h *Handler) help(ctx context.Context, b *bot.Bot, update *models.Update) e return nil } -func (h *Handler) today(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) today(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, h.nowUTC(chat.BotID)) @@ -106,7 +106,7 @@ func (h *Handler) today(ctx context.Context, b *bot.Bot, update *models.Update) return nil } -func (h *Handler) date(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) date(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -123,7 +123,7 @@ func (h *Handler) date(ctx context.Context, b *bot.Bot, update *models.Update) e return nil } -func (h *Handler) next(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) next(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) var ( @@ -184,7 +184,7 @@ func (h *Handler) next(ctx context.Context, b *bot.Bot, update *models.Update) e return nil } -func (h *Handler) remind(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) remind(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -201,7 +201,7 @@ func (h *Handler) remind(ctx context.Context, b *bot.Bot, update *models.Update) return nil } -func (h *Handler) bug(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) bug(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -222,7 +222,7 @@ func (h *Handler) bug(ctx context.Context, b *bot.Bot, update *models.Update) er return nil } -func (h *Handler) feedback(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) feedback(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -243,7 +243,7 @@ func (h *Handler) feedback(ctx context.Context, b *bot.Bot, update *models.Updat return nil } -func (h *Handler) language(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) language(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -260,7 +260,7 @@ func (h *Handler) language(ctx context.Context, b *bot.Bot, update *models.Updat return nil } -func (h *Handler) subscribe(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) subscribe(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) err := h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, true) @@ -282,7 +282,7 @@ func (h *Handler) subscribe(ctx context.Context, b *bot.Bot, update *models.Upda return nil } -func (h *Handler) unsubscribe(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) unsubscribe(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) err := h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, false) @@ -304,7 +304,7 @@ func (h *Handler) unsubscribe(ctx context.Context, b *bot.Bot, update *models.Up return nil } -func (h *Handler) admin(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) admin(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ @@ -321,7 +321,7 @@ func (h *Handler) admin(ctx context.Context, b *bot.Bot, update *models.Update) return nil } -func (h *Handler) reply(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) reply(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) err := h.db.SetState(ctx, chat.BotID, chat.ChatID, string(replyState)) @@ -343,7 +343,7 @@ func (h *Handler) reply(ctx context.Context, b *bot.Bot, update *models.Update) return nil } -func (h *Handler) stats(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) stats(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) stats, err := h.db.GetStats(ctx, chat.BotID) @@ -377,7 +377,7 @@ func (h *Handler) stats(ctx context.Context, b *bot.Bot, update *models.Update) return nil } -func (h *Handler) announce(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) announce(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) err := h.db.SetState(ctx, chat.BotID, chat.ChatID, string(announceState)) @@ -399,7 +399,7 @@ func (h *Handler) announce(ctx context.Context, b *bot.Bot, update *models.Updat return nil } -func (h *Handler) cancel(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) cancel(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) if chat.State == string(defaultState) { diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index c4ed3be..2b14394 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -67,12 +67,13 @@ func daysInMonth(month time.Month, year int) int { } // layoutRowsInfo calculates number of rows needed to display input count and number of empty cells in the last row. -func layoutRowsInfo(totalItems, itemsPerRow int) (int, int) { +func layoutRowsInfo(totalItems, itemsPerRow int) (filled int, empty int) { if totalItems%itemsPerRow == 0 { return totalItems / itemsPerRow, 0 } - empty := itemsPerRow - (totalItems % itemsPerRow) - return (totalItems / itemsPerRow) + 1, empty + empty = itemsPerRow - (totalItems % itemsPerRow) + filled = (totalItems / itemsPerRow) + 1 + return } func isJamaat(chat models.Chat) bool { diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index b657a40..357b7d8 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -45,6 +45,7 @@ func NewDB(ctx context.Context) (*DB, error) { return &DB{client: sdk.Table()}, nil } +//revive:disable:argument-limit func (db *DB) CreateChat( ctx context.Context, botID int64, From 682d72dea58b5b6f098cd32a113525d282af3e5a Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 01:56:52 +0300 Subject: [PATCH 02/59] feat: rename workflow directory --- .github/{workflow => workflows}/deploy.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflow => workflows}/deploy.yaml (100%) diff --git a/.github/workflow/deploy.yaml b/.github/workflows/deploy.yaml similarity index 100% rename from .github/workflow/deploy.yaml rename to .github/workflows/deploy.yaml From 719f5ecc31416bc1433fefece150abc2a1e671b9 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 02:03:29 +0300 Subject: [PATCH 03/59] feat: fix workflow --- .github/workflows/deploy.yaml | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 44a2dfc..cbd6975 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -21,7 +21,6 @@ env: TERRAFORM_VERSION: 1.6.3 GOOSE_VERSION: 3.16.0 GO_VERSION: 1.21 - CACHE_PREFIX: ${{ runner.os }}-${{ github.ref_name }} CONFIG_FILE_PATH: config.json jobs: @@ -50,7 +49,7 @@ jobs: - name: Set cache key id: set-cache-key run: | - CACHE_KEY="${{ env.CACHE_PREFIX }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" + CACHE_KEY="${{ runner.os }}-${{ github.ref_name }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT echo "::notice::Cache key: $CACHE_KEY" @@ -112,9 +111,9 @@ jobs: path: | ~/.cache/go-build ~/go/pkg/mod - key: ${{ env.CACHE_PREFIX }}-go-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-${{ github.ref_name }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | - ${{ env.CACHE_PREFIX }}-go- + ${{ runner.os }}-${{ github.ref_name }}-go- - name: Install revive run: go install github.com/mgechev/revive@latest @@ -138,32 +137,18 @@ jobs: name: Database Migration runs-on: ubuntu-latest needs: [setup, lint] - if: contains(github.event.head_commit.modified, 'migrations/') || contains(github.event.head_commit.added, 'migrations/') environment: ${{ needs.setup.outputs.environment }} steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - name: Check for migration changes - id: check-migrations - run: | - if git diff --name-only HEAD^ HEAD | grep -q '^migrations/'; then - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "changed=false" >> $GITHUB_OUTPUT - fi - name: Install Goose - if: steps.check-migrations.outputs.changed == 'true' run: | curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | \ GOOSE_INSTALL=$HOME/.goose sh -s v${{ env.GOOSE_VERSION }} echo "$HOME/.goose" >> $GITHUB_PATH - name: Run Goose migrations - if: steps.check-migrations.outputs.changed == 'true' env: GOOSE_DRIVER: ydb GOOSE_DBSTRING: ${{ secrets.DB_CONNECTION_STRING }} @@ -174,7 +159,7 @@ jobs: fi cd migrations goose up - echo "Migration completed successfully" + echo "::notice::Migration completed successfully for ${{ needs.setup.outputs.workspace }} environment" plan: name: Terraform Plan From 6bedb70c5f3399b5e0a6aa6d671a25182e579534 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 02:17:23 +0300 Subject: [PATCH 04/59] feat: read env correctly --- .github/workflows/deploy.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index cbd6975..2209460 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -27,6 +27,7 @@ jobs: setup: name: Setup Environment runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: workspace: ${{ steps.set-workspace.outputs.workspace }} environment: ${{ steps.set-workspace.outputs.environment }} @@ -69,6 +70,7 @@ jobs: name: Terraform Validate runs-on: ubuntu-latest needs: setup + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -137,7 +139,7 @@ jobs: name: Database Migration runs-on: ubuntu-latest needs: [setup, lint] - environment: ${{ needs.setup.outputs.environment }} + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -165,7 +167,7 @@ jobs: name: Terraform Plan runs-on: ubuntu-latest needs: [setup, lint] - environment: ${{ needs.setup.outputs.environment }} + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: plan-exists: ${{ steps.plan.outputs.exitcode }} steps: @@ -215,7 +217,7 @@ jobs: runs-on: ubuntu-latest needs: [setup, plan, migrate] if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') - environment: ${{ needs.setup.outputs.environment }} + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: dispatcher_fn_id: ${{ steps.outputs.outputs.dispatcher_fn_id }} steps: @@ -280,7 +282,7 @@ jobs: runs-on: ubuntu-latest needs: [setup, apply] if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_hook == 'true' && needs.apply.result == 'success' - environment: ${{ needs.setup.outputs.environment }} + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: - name: Checkout code uses: actions/checkout@v4 From 1c3ef37347004753c467edee2863ff14dd24b59a Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 02:19:39 +0300 Subject: [PATCH 05/59] feat: fix format main.tf --- main.tf | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/main.tf b/main.tf index eae25fd..4e46993 100644 --- a/main.tf +++ b/main.tf @@ -10,10 +10,10 @@ terraform { s3 = "https://storage.yandexcloud.net" } region = "ru-central1" - + bucket = "escalopa-tfstate" key = "prayer-bot/terraform.tfstate" - + workspace_key_prefix = "" skip_region_validation = true @@ -27,7 +27,7 @@ terraform { } locals { - env = terraform.workspace + env = terraform.workspace } variable "cloud_id" { @@ -45,9 +45,9 @@ variable "region" { provider "yandex" { service_account_key_file = "iam.json" - cloud_id = var.cloud_id - folder_id = var.folder_id - zone = var.region + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.region } ########################### @@ -141,7 +141,7 @@ resource "yandex_function" "loader_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.loader_sa.id folder_id = var.folder_id - user_hash = filemd5(data.archive_file.loader_zip.output_path) + user_hash = filemd5(data.archive_file.loader_zip.output_path) environment = { APP_CONFIG = file("${path.module}/config.json") @@ -218,7 +218,7 @@ resource "yandex_function" "dispatcher_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.dispatcher_sa.id folder_id = var.folder_id - user_hash = filemd5(data.archive_file.dispatcher_zip.output_path) + user_hash = filemd5(data.archive_file.dispatcher_zip.output_path) environment = { APP_CONFIG = file("${path.module}/config.json") @@ -275,7 +275,7 @@ resource "yandex_function" "reminder_fn" { execution_timeout = 5 service_account_id = yandex_iam_service_account.reminder_sa.id folder_id = var.folder_id - user_hash = filemd5(data.archive_file.reminder_zip.output_path) + user_hash = filemd5(data.archive_file.reminder_zip.output_path) environment = { APP_CONFIG = file("${path.module}/config.json") From d9702276704cdc087c62aefe9ecd9d2e32b57eb0 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 02:32:17 +0300 Subject: [PATCH 06/59] feat: update deploy.yaml --- .github/workflows/deploy.yaml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 2209460..9de0e29 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -147,9 +147,14 @@ jobs: - name: Install Goose run: | curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | \ - GOOSE_INSTALL=$HOME/.goose sh -s v${{ env.GOOSE_VERSION }} + sh -s -- -b $HOME/.goose v${{ env.GOOSE_VERSION }} echo "$HOME/.goose" >> $GITHUB_PATH + - name: Verify Goose installation + run: | + export PATH="$HOME/.goose:$PATH" + goose --version + - name: Run Goose migrations env: GOOSE_DRIVER: ydb @@ -159,6 +164,7 @@ jobs: echo "::error::DB_CONNECTION_STRING secret is not set for ${{ needs.setup.outputs.environment }} environment" exit 1 fi + export PATH="$HOME/.goose:$PATH" cd migrations goose up echo "::notice::Migration completed successfully for ${{ needs.setup.outputs.workspace }} environment" @@ -206,7 +212,7 @@ jobs: - name: Upload plan artifact if: steps.plan.outputs.exitcode == '2' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: tfplan-${{ needs.setup.outputs.workspace }} path: tfplan @@ -247,7 +253,7 @@ jobs: terraform state pull > state-backups/terraform-${{ needs.setup.outputs.workspace }}-${TIMESTAMP}-${{ github.run_id }}.tfstate - name: Upload state backup - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: terraform-state-backup-${{ needs.setup.outputs.workspace }}-${{ github.run_id }} path: state-backups/ @@ -255,7 +261,7 @@ jobs: - name: Download plan artifact if: needs.plan.outputs.plan-exists == '2' - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: tfplan-${{ needs.setup.outputs.workspace }} @@ -280,7 +286,7 @@ jobs: hook: name: Setup Telegram Webhooks runs-on: ubuntu-latest - needs: [setup, apply] + needs: [apply] if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_hook == 'true' && needs.apply.result == 'success' environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: From 2b14646bbcfab4025139d670058a4b5749332ccb Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 02:45:30 +0300 Subject: [PATCH 07/59] feat: try fix workflow --- .github/actions/setup-terraform/action.yaml | 34 ++++++++++++++++++--- .github/workflows/deploy.yaml | 10 ++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-terraform/action.yaml b/.github/actions/setup-terraform/action.yaml index b7addfb..06f69a9 100644 --- a/.github/actions/setup-terraform/action.yaml +++ b/.github/actions/setup-terraform/action.yaml @@ -51,14 +51,40 @@ runs: - name: Write service account key shell: bash run: | - echo "::add-mask::${{ inputs.service_account_key }}" - printf '%s' "${{ inputs.service_account_key }}" > iam.json + if [ -z "${{ inputs.service_account_key }}" ]; then + echo "::error::SERVICE_ACCOUNT_KEY is empty" + exit 1 + fi + + # Write the key and validate it's valid JSON + echo '${{ inputs.service_account_key }}' > iam.json + + # Validate JSON format + if ! jq empty iam.json 2>/dev/null; then + echo "::error::SERVICE_ACCOUNT_KEY is not valid JSON" + cat iam.json + exit 1 + fi + + echo "::notice::Service account key file created and validated" - name: Write config.json shell: bash run: | - echo "::add-mask::${{ inputs.app_config }}" - printf '%s' "${{ inputs.app_config }}" > "${{ inputs.config_file_path }}" + if [ -z "${{ inputs.app_config }}" ]; then + echo "::error::APP_CONFIG is empty" + exit 1 + fi + + # Write the config and validate it's valid JSON + echo '${{ inputs.app_config }}' > "${{ inputs.config_file_path }}" + + # Validate JSON format + if ! jq empty "${{ inputs.config_file_path }}" 2>/dev/null; then + echo "::error::APP_CONFIG is not valid JSON" + exit 1 + fi + echo "::notice::Application config written to: ${{ inputs.config_file_path }}" - name: Cache Terraform plugins diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9de0e29..30752bb 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -146,10 +146,14 @@ jobs: - name: Install Goose run: | - curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | \ - sh -s -- -b $HOME/.goose v${{ env.GOOSE_VERSION }} + # Download and install Goose binary directly + GOOSE_VERSION="${{ env.GOOSE_VERSION }}" + wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose + chmod +x goose + mkdir -p $HOME/.goose + mv goose $HOME/.goose/ echo "$HOME/.goose" >> $GITHUB_PATH - + - name: Verify Goose installation run: | export PATH="$HOME/.goose:$PATH" From aa6d1393a396eadf3521a615fe6a57318e04b33b Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 03:07:00 +0300 Subject: [PATCH 08/59] feat: try fix workflow v1 --- .github/actions/setup-terraform/action.yaml | 17 ++++---- .github/workflows/deploy.yaml | 46 ++++++++++----------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/.github/actions/setup-terraform/action.yaml b/.github/actions/setup-terraform/action.yaml index 06f69a9..c68251f 100644 --- a/.github/actions/setup-terraform/action.yaml +++ b/.github/actions/setup-terraform/action.yaml @@ -14,10 +14,14 @@ inputs: service_account_key: description: 'Yandex Cloud Service Account Key (JSON)' required: true + service_account_key_path: + description: 'Path where iam.json will be created' + required: false + default: 'iam.json' app_config: description: 'Application config.json content' required: true - config_file_path: + app_config_path: description: 'Path where config.json will be created' required: false default: 'config.json' @@ -57,12 +61,11 @@ runs: fi # Write the key and validate it's valid JSON - echo '${{ inputs.service_account_key }}' > iam.json + echo '${{ inputs.service_account_key }}' > "${{ inputs.service_account_key_path }}" # Validate JSON format - if ! jq empty iam.json 2>/dev/null; then + if ! jq empty "${{ inputs.service_account_key_path }}" 2>/dev/null; then echo "::error::SERVICE_ACCOUNT_KEY is not valid JSON" - cat iam.json exit 1 fi @@ -77,15 +80,15 @@ runs: fi # Write the config and validate it's valid JSON - echo '${{ inputs.app_config }}' > "${{ inputs.config_file_path }}" + echo '${{ inputs.app_config }}' > "${{ inputs.app_config_path }}" # Validate JSON format - if ! jq empty "${{ inputs.config_file_path }}" 2>/dev/null; then + if ! jq empty "${{ inputs.app_config_path }}" 2>/dev/null; then echo "::error::APP_CONFIG is not valid JSON" exit 1 fi - echo "::notice::Application config written to: ${{ inputs.config_file_path }}" + echo "::notice::Application config written to: ${{ inputs.app_config_path }}" - name: Cache Terraform plugins id: cache diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 30752bb..c0be5db 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -21,7 +21,7 @@ env: TERRAFORM_VERSION: 1.6.3 GOOSE_VERSION: 3.16.0 GO_VERSION: 1.21 - CONFIG_FILE_PATH: config.json + APP_CONFIG_PATH: config.json jobs: setup: @@ -60,7 +60,7 @@ jobs: terraform_version: ${{ env.TERRAFORM_VERSION }} service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} - config_file_path: ${{ env.CONFIG_FILE_PATH }} + app_config_path: ${{ env.APP_CONFIG_PATH }} workspace: ${{ steps.set-workspace.outputs.workspace }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -81,7 +81,7 @@ jobs: terraform_version: ${{ env.TERRAFORM_VERSION }} service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} - config_file_path: ${{ env.CONFIG_FILE_PATH }} + app_config_path: ${{ env.APP_CONFIG_PATH }} workspace: ${{ needs.setup.outputs.workspace }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -146,30 +146,39 @@ jobs: - name: Install Goose run: | - # Download and install Goose binary directly GOOSE_VERSION="${{ env.GOOSE_VERSION }}" wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose chmod +x goose mkdir -p $HOME/.goose mv goose $HOME/.goose/ echo "$HOME/.goose" >> $GITHUB_PATH - + - name: Verify Goose installation run: | export PATH="$HOME/.goose:$PATH" goose --version + - name: Configure YDB Credentials + run: | + echo '${{ secrets.SERVICE_ACCOUNT_KEY }}' > /tmp/ydb-iam.json + chmod 600 /tmp/ydb-iam.json + - name: Run Goose migrations env: GOOSE_DRIVER: ydb GOOSE_DBSTRING: ${{ secrets.DB_CONNECTION_STRING }} + YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS: /tmp/ydb-iam.json run: | - if [ -z "$GOOSE_DBSTRING" ]; then - echo "::error::DB_CONNECTION_STRING secret is not set for ${{ needs.setup.outputs.environment }} environment" - exit 1 - fi export PATH="$HOME/.goose:$PATH" cd migrations + + # Ensure DB string uses TLS (grpcs://) + if ! echo "$GOOSE_DBSTRING" | grep -q "^grpcs://"; then + echo "::error::DB_CONNECTION_STRING must start with grpcs:// (secure)" + exit 1 + fi + + echo "::notice::Running Goose migrations on $GOOSE_DBSTRING" goose up echo "::notice::Migration completed successfully for ${{ needs.setup.outputs.workspace }} environment" @@ -190,7 +199,7 @@ jobs: terraform_version: ${{ env.TERRAFORM_VERSION }} service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} - config_file_path: ${{ env.CONFIG_FILE_PATH }} + app_config_path: ${{ env.APP_CONFIG_PATH }} workspace: ${{ needs.setup.outputs.workspace }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -241,7 +250,7 @@ jobs: terraform_wrapper: 'false' service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} - config_file_path: ${{ env.CONFIG_FILE_PATH }} + app_config_path: ${{ env.APP_CONFIG_PATH }} workspace: ${{ needs.setup.outputs.workspace }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -297,24 +306,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Terraform Environment - uses: ./.github/actions/setup-terraform - with: - terraform_version: ${{ env.TERRAFORM_VERSION }} - terraform_wrapper: 'false' - service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} - app_config: ${{ secrets.APP_CONFIG }} - config_file_path: ${{ env.CONFIG_FILE_PATH }} - workspace: ${{ needs.setup.outputs.workspace }} - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - cache_key: ${{ needs.setup.outputs.cache_key }} - - name: Execute webhook setup script env: DISPATCHER_FUNCTION_ID: ${{ needs.apply.outputs.dispatcher_fn_id }} run: | - echo "::notice::Config file: $CONFIG_FILE_PATH" + echo "::notice::Config file: $APP_CONFIG_PATH" echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" chmod +x _scripts/hook.sh From f8a0b2950fd5bb1165073d02d27c052c088581fe Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 04:33:43 +0300 Subject: [PATCH 09/59] feat: try ydb unauthenticated --- .github/workflows/deploy.yaml | 64 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c0be5db..cd0967d 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -19,7 +19,7 @@ concurrency: env: TERRAFORM_VERSION: 1.6.3 - GOOSE_VERSION: 3.16.0 + GOOSE_VERSION: 3.26.0 GO_VERSION: 1.21 APP_CONFIG_PATH: config.json @@ -66,38 +66,9 @@ jobs: aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} - validate: - name: Terraform Validate - runs-on: ubuntu-latest - needs: setup - environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Terraform Environment - uses: ./.github/actions/setup-terraform - with: - terraform_version: ${{ env.TERRAFORM_VERSION }} - service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} - app_config: ${{ secrets.APP_CONFIG }} - app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ needs.setup.outputs.workspace }} - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - cache_key: ${{ needs.setup.outputs.cache_key }} - - - name: Terraform Format Check - run: terraform fmt -check -recursive - continue-on-error: false - - - name: Terraform Validate - run: terraform validate - lint: name: Code Linting runs-on: ubuntu-latest - needs: validate steps: - name: Checkout code uses: actions/checkout@v4 @@ -181,11 +152,40 @@ jobs: echo "::notice::Running Goose migrations on $GOOSE_DBSTRING" goose up echo "::notice::Migration completed successfully for ${{ needs.setup.outputs.workspace }} environment" + continue-on-error: true + + validate: + name: Terraform Validate + runs-on: ubuntu-latest + needs: setup + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + app_config_path: ${{ env.APP_CONFIG_PATH }} + workspace: ${{ needs.setup.outputs.workspace }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ needs.setup.outputs.cache_key }} + + - name: Terraform Format Check + run: terraform fmt -check -recursive + continue-on-error: false + + - name: Terraform Validate + run: terraform validate plan: name: Terraform Plan runs-on: ubuntu-latest - needs: [setup, lint] + needs: [setup, lint, validate] environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: plan-exists: ${{ steps.plan.outputs.exitcode }} @@ -234,7 +234,7 @@ jobs: apply: name: Terraform Apply runs-on: ubuntu-latest - needs: [setup, plan, migrate] + needs: [plan, migrate] if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: From d6495dcce94004c2b8b071a14e093282cd41b14d Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 04:36:03 +0300 Subject: [PATCH 10/59] feat: try ydb unauthenticated --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index cd0967d..0c1028d 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -234,7 +234,7 @@ jobs: apply: name: Terraform Apply runs-on: ubuntu-latest - needs: [plan, migrate] + needs: [setup, plan, migrate] if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: From 054f7dd4ceeb68db12bcc0a71dc436f52b3a4758 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 04:41:36 +0300 Subject: [PATCH 11/59] feat: try ydb unauthenticated --- .github/workflows/deploy.yaml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0c1028d..597fb25 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -223,14 +223,6 @@ jobs: exit 1 fi - - name: Upload plan artifact - if: steps.plan.outputs.exitcode == '2' - uses: actions/upload-artifact@v4 - with: - name: tfplan-${{ needs.setup.outputs.workspace }} - path: tfplan - retention-days: 7 - apply: name: Terraform Apply runs-on: ubuntu-latest @@ -272,19 +264,13 @@ jobs: path: state-backups/ retention-days: 90 - - name: Download plan artifact - if: needs.plan.outputs.plan-exists == '2' - uses: actions/download-artifact@v4 - with: - name: tfplan-${{ needs.setup.outputs.workspace }} - - name: Terraform Apply env: TF_VAR_cloud_id: ${{ secrets.CLOUD_ID }} TF_VAR_folder_id: ${{ secrets.FOLDER_ID }} run: | if [ -f tfplan ]; then - terraform apply -auto-approve tfplan + terraform apply -auto-approve else echo "No changes to apply" fi From 8d623fc8b4521d2ab8689ae45495ff411a58d2da Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 05:30:21 +0300 Subject: [PATCH 12/59] feat: fix hook --- .github/workflows/deploy.yaml | 34 +++++++++++++------ .../20251025195706_create_chats_table.sql | 5 --- .../20251025195728_create_prayers_table.sql | 5 --- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 597fb25..d96554f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -115,6 +115,27 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install YC CLI + run: | + curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash + echo "${HOME}/yandex-cloud/bin" >> $GITHUB_PATH + + - name: Authenticate YC CLI + env: + YC_SERVICE_ACCOUNT_KEY: ${{ secrets.YC_SERVICE_ACCOUNT_KEY }} + YC_CLOUD_ID: ${{ secrets.YC_CLOUD_ID }} + YC_FOLDER_ID: ${{ secrets.YC_FOLDER_ID }} + run: | + echo "$YC_SERVICE_ACCOUNT_KEY" > sa-key.json + yc config set service-account-key sa-key.json + yc config set cloud-id "$YC_CLOUD_ID" + yc config set folder-id "$YC_FOLDER_ID" + + - name: Generate short-lived IAM token + run: | + export YDB_ACCESS_TOKEN_CREDENTIALS=$(yc iam create-token) + echo "YDB_ACCESS_TOKEN_CREDENTIALS=$YDB_ACCESS_TOKEN_CREDENTIALS" >> $GITHUB_ENV + - name: Install Goose run: | GOOSE_VERSION="${{ env.GOOSE_VERSION }}" @@ -124,21 +145,12 @@ jobs: mv goose $HOME/.goose/ echo "$HOME/.goose" >> $GITHUB_PATH - - name: Verify Goose installation - run: | - export PATH="$HOME/.goose:$PATH" - goose --version - - - name: Configure YDB Credentials - run: | - echo '${{ secrets.SERVICE_ACCOUNT_KEY }}' > /tmp/ydb-iam.json - chmod 600 /tmp/ydb-iam.json - - name: Run Goose migrations env: GOOSE_DRIVER: ydb + # ydb connection string format + # grpcs://:/?database=&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric GOOSE_DBSTRING: ${{ secrets.DB_CONNECTION_STRING }} - YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS: /tmp/ydb-iam.json run: | export PATH="$HOME/.goose:$PATH" cd migrations diff --git a/migrations/20251025195706_create_chats_table.sql b/migrations/20251025195706_create_chats_table.sql index e3ea6e6..f3054a5 100644 --- a/migrations/20251025195706_create_chats_table.sql +++ b/migrations/20251025195706_create_chats_table.sql @@ -12,10 +12,5 @@ CREATE TABLE chats ( PRIMARY KEY (bot_id, chat_id) ); --- +goose StatementBegin -CREATE INDEX idx_chats_bot_id ON chats (bot_id); -CREATE INDEX idx_chats_chat_id ON chats (chat_id); --- +goose StatementEnd - -- +goose Down DROP TABLE IF EXISTS chats; \ No newline at end of file diff --git a/migrations/20251025195728_create_prayers_table.sql b/migrations/20251025195728_create_prayers_table.sql index 176ad79..88bc5ba 100644 --- a/migrations/20251025195728_create_prayers_table.sql +++ b/migrations/20251025195728_create_prayers_table.sql @@ -11,10 +11,5 @@ CREATE TABLE prayers ( PRIMARY KEY (bot_id, prayer_date) ); --- +goose StatementBegin -CREATE INDEX idx_prayers_bot_id ON prayers (bot_id); -CREATE INDEX idx_prayers_date ON prayers (prayer_date); --- +goose StatementEnd - -- +goose Down DROP TABLE IF EXISTS prayers; \ No newline at end of file From 1036ff0f8920bd907f9b774c837ddc9143660326 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 05:40:54 +0300 Subject: [PATCH 13/59] feat: fix hook --- .github/workflows/deploy.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d96554f..0d4e92e 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -122,12 +122,12 @@ jobs: - name: Authenticate YC CLI env: - YC_SERVICE_ACCOUNT_KEY: ${{ secrets.YC_SERVICE_ACCOUNT_KEY }} - YC_CLOUD_ID: ${{ secrets.YC_CLOUD_ID }} - YC_FOLDER_ID: ${{ secrets.YC_FOLDER_ID }} + YC_SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }} + YC_CLOUD_ID: ${{ secrets.CLOUD_ID }} + YC_FOLDER_ID: ${{ secrets.FOLDER_ID }} run: | - echo "$YC_SERVICE_ACCOUNT_KEY" > sa-key.json - yc config set service-account-key sa-key.json + echo "$YC_SERVICE_ACCOUNT_KEY" > iam.json + yc config set service-account-key iam.json yc config set cloud-id "$YC_CLOUD_ID" yc config set folder-id "$YC_FOLDER_ID" From 6145addfd32ca51fe6ffa67ad00a57e31ce87a1b Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 11:18:49 +0300 Subject: [PATCH 14/59] feat: update workflow --- .github/workflows/check.yaml | 44 +++++++++ .github/workflows/deploy.yaml | 161 ++----------------------------- .github/workflows/hook.yaml | 77 +++++++++++++++ .github/workflows/migration.yaml | 81 ++++++++++++++++ 4 files changed, 209 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/check.yaml create mode 100644 .github/workflows/hook.yaml create mode 100644 .github/workflows/migration.yaml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 0000000..c760206 --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,44 @@ +name: Code Linting + +on: + push: + branches: + - main + - dev + workflow_dispatch: + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: 1.21 + +jobs: + lint: + name: Code Linting + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-${{ github.ref_name }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-${{ github.ref_name }}-go- + + - name: Install revive + run: go install github.com/mgechev/revive@latest + + - name: Run revive + run: revive -config revive.toml -formatter friendly ./... \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0d4e92e..5aa4d5c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -5,13 +5,6 @@ on: branches: - main - dev - workflow_dispatch: - inputs: - run_hook: - description: 'Run webhook setup' - required: false - type: boolean - default: false concurrency: group: deploy-${{ github.ref }} @@ -19,8 +12,6 @@ concurrency: env: TERRAFORM_VERSION: 1.6.3 - GOOSE_VERSION: 3.26.0 - GO_VERSION: 1.21 APP_CONFIG_PATH: config.json jobs: @@ -30,7 +21,6 @@ jobs: environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: workspace: ${{ steps.set-workspace.outputs.workspace }} - environment: ${{ steps.set-workspace.outputs.environment }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} steps: - name: Checkout code @@ -41,10 +31,8 @@ jobs: run: | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then echo "workspace=prod" >> $GITHUB_OUTPUT - echo "environment=prod" >> $GITHUB_OUTPUT elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then echo "workspace=dev" >> $GITHUB_OUTPUT - echo "environment=dev" >> $GITHUB_OUTPUT fi - name: Set cache key @@ -66,106 +54,6 @@ jobs: aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} - lint: - name: Code Linting - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-${{ github.ref_name }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-${{ github.ref_name }}-go- - - - name: Install revive - run: go install github.com/mgechev/revive@latest - - - name: Run revive - loader - run: | - cd serverless/loader - revive -config ../../revive.toml -formatter friendly ./... - - - name: Run revive - dispatcher - run: | - cd serverless/dispatcher - revive -config ../../revive.toml -formatter friendly ./... - - - name: Run revive - reminder - run: | - cd serverless/reminder - revive -config ../../revive.toml -formatter friendly ./... - - migrate: - name: Database Migration - runs-on: ubuntu-latest - needs: [setup, lint] - environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install YC CLI - run: | - curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash - echo "${HOME}/yandex-cloud/bin" >> $GITHUB_PATH - - - name: Authenticate YC CLI - env: - YC_SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }} - YC_CLOUD_ID: ${{ secrets.CLOUD_ID }} - YC_FOLDER_ID: ${{ secrets.FOLDER_ID }} - run: | - echo "$YC_SERVICE_ACCOUNT_KEY" > iam.json - yc config set service-account-key iam.json - yc config set cloud-id "$YC_CLOUD_ID" - yc config set folder-id "$YC_FOLDER_ID" - - - name: Generate short-lived IAM token - run: | - export YDB_ACCESS_TOKEN_CREDENTIALS=$(yc iam create-token) - echo "YDB_ACCESS_TOKEN_CREDENTIALS=$YDB_ACCESS_TOKEN_CREDENTIALS" >> $GITHUB_ENV - - - name: Install Goose - run: | - GOOSE_VERSION="${{ env.GOOSE_VERSION }}" - wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose - chmod +x goose - mkdir -p $HOME/.goose - mv goose $HOME/.goose/ - echo "$HOME/.goose" >> $GITHUB_PATH - - - name: Run Goose migrations - env: - GOOSE_DRIVER: ydb - # ydb connection string format - # grpcs://:/?database=&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric - GOOSE_DBSTRING: ${{ secrets.DB_CONNECTION_STRING }} - run: | - export PATH="$HOME/.goose:$PATH" - cd migrations - - # Ensure DB string uses TLS (grpcs://) - if ! echo "$GOOSE_DBSTRING" | grep -q "^grpcs://"; then - echo "::error::DB_CONNECTION_STRING must start with grpcs:// (secure)" - exit 1 - fi - - echo "::notice::Running Goose migrations on $GOOSE_DBSTRING" - goose up - echo "::notice::Migration completed successfully for ${{ needs.setup.outputs.workspace }} environment" - continue-on-error: true - validate: name: Terraform Validate runs-on: ubuntu-latest @@ -197,7 +85,7 @@ jobs: plan: name: Terraform Plan runs-on: ubuntu-latest - needs: [setup, lint, validate] + needs: [setup, validate] environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: plan-exists: ${{ steps.plan.outputs.exitcode }} @@ -226,6 +114,9 @@ jobs: terraform plan -out=tfplan -detailed-exitcode || EXIT_CODE=$? echo "exitcode=$EXIT_CODE" >> $GITHUB_OUTPUT + # Print the plan output + terraform show tfplan + if [ $EXIT_CODE -eq 2 ]; then echo "::notice::Changes detected in Terraform plan" elif [ $EXIT_CODE -eq 0 ]; then @@ -238,8 +129,7 @@ jobs: apply: name: Terraform Apply runs-on: ubuntu-latest - needs: [setup, plan, migrate] - if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') + needs: [setup, plan] environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: dispatcher_fn_id: ${{ steps.outputs.outputs.dispatcher_fn_id }} @@ -280,12 +170,7 @@ jobs: env: TF_VAR_cloud_id: ${{ secrets.CLOUD_ID }} TF_VAR_folder_id: ${{ secrets.FOLDER_ID }} - run: | - if [ -f tfplan ]; then - terraform apply -auto-approve - else - echo "No changes to apply" - fi + run: terraform apply -auto-approve - name: Export Terraform Outputs id: outputs @@ -294,35 +179,10 @@ jobs: echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" - hook: - name: Setup Telegram Webhooks - runs-on: ubuntu-latest - needs: [apply] - if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_hook == 'true' && needs.apply.result == 'success' - environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Execute webhook setup script - env: - DISPATCHER_FUNCTION_ID: ${{ needs.apply.outputs.dispatcher_fn_id }} - run: | - echo "::notice::Config file: $APP_CONFIG_PATH" - echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" - - chmod +x _scripts/hook.sh - bash _scripts/hook.sh - continue-on-error: false - - - name: Verify webhook setup - run: | - echo "::notice::Webhook setup completed successfully for ${{ needs.setup.outputs.workspace }} environment" - summary: name: Deployment Summary runs-on: ubuntu-latest - needs: [setup, validate, lint, migrate, plan, apply, hook] + needs: [setup, validate, plan, apply] if: always() steps: - name: Generate summary @@ -332,11 +192,8 @@ jobs: echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY echo "| Validate | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Code Linting | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Database Migration | ${{ needs.migrate.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Terraform Plan | ${{ needs.plan.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Terraform Apply | ${{ needs.apply.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Webhook Setup | ${{ needs.hook.result }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY echo "**Workspace:** ${{ needs.setup.outputs.workspace }}" >> $GITHUB_STEP_SUMMARY @@ -345,8 +202,4 @@ jobs: if [ "${{ needs.apply.outputs.dispatcher_fn_id }}" != "" ]; then echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_fn_id }}" >> $GITHUB_STEP_SUMMARY - fi - - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - echo "**Manual Trigger:** Webhook setup requested: ${{ github.event.inputs.run_hook }}" >> $GITHUB_STEP_SUMMARY fi \ No newline at end of file diff --git a/.github/workflows/hook.yaml b/.github/workflows/hook.yaml new file mode 100644 index 0000000..3a8c719 --- /dev/null +++ b/.github/workflows/hook.yaml @@ -0,0 +1,77 @@ +name: Setup Telegram Webhooks + +on: + workflow_dispatch: + +concurrency: + group: hook-${{ github.ref }} + cancel-in-progress: true + +env: + TERRAFORM_VERSION: 1.6.3 + APP_CONFIG_PATH: config.json + +jobs: + hook: + name: Setup Telegram Webhooks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Determine workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + + - name: Set cache key + id: set-cache-key + run: | + CACHE_KEY="${{ runner.os }}-${{ github.ref_name }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" + echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + terraform_wrapper: 'false' + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + app_config_path: ${{ env.APP_CONFIG_PATH }} + workspace: ${{ env.WORKSPACE }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ steps.set-cache-key.outputs.cache_key }} + + - name: Get Dispatcher Function ID + id: get-fn-id + run: | + DISPATCHER_FUNCTION_ID=$(terraform output -raw dispatcher_function_id) + echo "dispatcher_function_id=$DISPATCHER_FUNCTION_ID" >> $GITHUB_OUTPUT + echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" + + - name: Execute webhook setup script + env: + DISPATCHER_FUNCTION_ID: ${{ steps.get-fn-id.outputs.dispatcher_function_id }} + run: | + echo "::notice::Config file: $APP_CONFIG_PATH" + echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" + + chmod +x _scripts/hook.sh + bash _scripts/hook.sh + continue-on-error: false + + - name: Verify webhook setup + run: | + echo "::notice::Webhook setup completed successfully for $WORKSPACE environment" + + - name: Generate summary + if: always() + run: | + echo "## Webhook Setup Summary - $WORKSPACE environment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "**Dispatcher Function ID:** ${{ steps.get-fn-id.outputs.dispatcher_function_id }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/migration.yaml b/.github/workflows/migration.yaml new file mode 100644 index 0000000..4ea61d9 --- /dev/null +++ b/.github/workflows/migration.yaml @@ -0,0 +1,81 @@ +name: Database Migration + +on: + push: + branches: + - main + - dev + +concurrency: + group: migrate-${{ github.ref }} + cancel-in-progress: true + +env: + GOOSE_VERSION: 3.26.0 + +jobs: + migrate: + name: Database Migration + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install YC CLI + run: | + curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash + echo "${HOME}/yandex-cloud/bin" >> $GITHUB_PATH + + - name: Authenticate YC CLI + env: + YC_SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }} + YC_CLOUD_ID: ${{ secrets.CLOUD_ID }} + YC_FOLDER_ID: ${{ secrets.FOLDER_ID }} + run: | + echo "$YC_SERVICE_ACCOUNT_KEY" > iam.json + yc config set service-account-key iam.json + yc config set cloud-id "$YC_CLOUD_ID" + yc config set folder-id "$YC_FOLDER_ID" + + - name: Generate short-lived IAM token + run: | + export YDB_ACCESS_TOKEN=$(yc iam create-token) + echo "YDB_ACCESS_TOKEN=$YDB_ACCESS_TOKEN" >> $GITHUB_ENV + + - name: Install Goose + run: | + GOOSE_VERSION="${{ env.GOOSE_VERSION }}" + wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose + chmod +x goose + mkdir -p $HOME/.goose + mv goose $HOME/.goose/ + echo "$HOME/.goose" >> $GITHUB_PATH + + - name: Run Goose migrations + env: + GOOSE_DRIVER: ydb + GOOSE_DBSTRING_BASE: ${{ secrets.DB_CONNECTION_STRING }} + run: | + export PATH="$HOME/.goose:$PATH" + cd migrations + + # Add token and query options to connection string + GOOSE_DBSTRING="${GOOSE_DBSTRING_BASE}&token=${YDB_ACCESS_TOKEN}&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" + export GOOSE_DBSTRING + + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "::notice::Running Goose migrations for $WORKSPACE environment" + goose up + echo "::notice::Migration completed successfully for $WORKSPACE environment" + continue-on-error: true + + - name: Generate summary + if: always() + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "## Database Migration Summary - $WORKSPACE environment" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file From e1e817cbda809347d42358fe39ae783471a8f1bb Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 11:25:52 +0300 Subject: [PATCH 15/59] feat: update workflow --- .github/workflows/check.yaml | 44 --------- .github/workflows/deploy.yaml | 160 ++++++++++++++++++++++++++++++- .github/workflows/hook.yaml | 77 --------------- .github/workflows/migration.yaml | 81 ---------------- 4 files changed, 156 insertions(+), 206 deletions(-) delete mode 100644 .github/workflows/check.yaml delete mode 100644 .github/workflows/hook.yaml delete mode 100644 .github/workflows/migration.yaml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml deleted file mode 100644 index c760206..0000000 --- a/.github/workflows/check.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: Code Linting - -on: - push: - branches: - - main - - dev - workflow_dispatch: - -concurrency: - group: check-${{ github.ref }} - cancel-in-progress: true - -env: - GO_VERSION: 1.21 - -jobs: - lint: - name: Code Linting - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-${{ github.ref_name }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-${{ github.ref_name }}-go- - - - name: Install revive - run: go install github.com/mgechev/revive@latest - - - name: Run revive - run: revive -config revive.toml -formatter friendly ./... \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5aa4d5c..02fbce8 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -5,6 +5,7 @@ on: branches: - main - dev + workflow_dispatch: concurrency: group: deploy-${{ github.ref }} @@ -12,6 +13,8 @@ concurrency: env: TERRAFORM_VERSION: 1.6.3 + GOOSE_VERSION: 3.26.0 + GO_VERSION: 1.21 APP_CONFIG_PATH: config.json jobs: @@ -54,6 +57,90 @@ jobs: aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} + lint: + name: Code Linting + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-${{ github.ref_name }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-${{ github.ref_name }}-go- + + - name: Install revive + run: go install github.com/mgechev/revive@latest + + - name: Run revive + run: revive -config revive.toml -formatter friendly ./... + + migrate: + name: Database Migration + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install YC CLI + run: | + curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash + echo "${HOME}/yandex-cloud/bin" >> $GITHUB_PATH + + - name: Authenticate YC CLI + env: + YC_SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }} + YC_CLOUD_ID: ${{ secrets.CLOUD_ID }} + YC_FOLDER_ID: ${{ secrets.FOLDER_ID }} + run: | + echo "$YC_SERVICE_ACCOUNT_KEY" > iam.json + yc config set service-account-key iam.json + yc config set cloud-id "$YC_CLOUD_ID" + yc config set folder-id "$YC_FOLDER_ID" + + - name: Generate short-lived IAM token + run: | + export YDB_ACCESS_TOKEN=$(yc iam create-token) + echo "YDB_ACCESS_TOKEN=$YDB_ACCESS_TOKEN" >> $GITHUB_ENV + + - name: Install Goose + run: | + GOOSE_VERSION="${{ env.GOOSE_VERSION }}" + wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose + chmod +x goose + mkdir -p $HOME/.goose + mv goose $HOME/.goose/ + echo "$HOME/.goose" >> $GITHUB_PATH + + - name: Run Goose migrations + env: + GOOSE_DRIVER: ydb + GOOSE_DBSTRING_BASE: ${{ secrets.DB_CONNECTION_STRING }} + run: | + export PATH="$HOME/.goose:$PATH" + cd migrations + + # Add token and query options to connection string + GOOSE_DBSTRING="${GOOSE_DBSTRING_BASE}&token=${YDB_ACCESS_TOKEN}&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" + export GOOSE_DBSTRING + + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "::notice::Running Goose migrations for $WORKSPACE environment" + goose up + echo "::notice::Migration completed successfully for $WORKSPACE environment" + continue-on-error: true + validate: name: Terraform Validate runs-on: ubuntu-latest @@ -85,7 +172,7 @@ jobs: plan: name: Terraform Plan runs-on: ubuntu-latest - needs: [setup, validate] + needs: [setup, lint, validate] environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: plan-exists: ${{ steps.plan.outputs.exitcode }} @@ -129,7 +216,8 @@ jobs: apply: name: Terraform Apply runs-on: ubuntu-latest - needs: [setup, plan] + needs: [setup, plan, migrate] + if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: dispatcher_fn_id: ${{ steps.outputs.outputs.dispatcher_fn_id }} @@ -179,10 +267,67 @@ jobs: echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" + hook: + name: Setup Telegram Webhooks + runs-on: ubuntu-latest + needs: [setup, apply] + if: needs.apply.result == 'success' + environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Determine workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + + - name: Set cache key + id: set-cache-key + run: | + CACHE_KEY="${{ runner.os }}-${{ github.ref_name }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" + echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT + + - name: Setup Terraform Environment + uses: ./.github/actions/setup-terraform + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + terraform_wrapper: 'false' + service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} + app_config: ${{ secrets.APP_CONFIG }} + app_config_path: ${{ env.APP_CONFIG_PATH }} + workspace: ${{ env.WORKSPACE }} + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + cache_key: ${{ steps.set-cache-key.outputs.cache_key }} + + - name: Get Dispatcher Function ID + id: get-fn-id + run: | + DISPATCHER_FN_ID=$(terraform output -raw dispatcher_fn_id) + echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT + echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" + + - name: Execute webhook setup script + env: + DISPATCHER_FUNCTION_ID: ${{ steps.get-fn-id.outputs.dispatcher_fn_id }} + run: | + echo "::notice::Config file: $APP_CONFIG_PATH" + echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" + + chmod +x _scripts/hook.sh + bash _scripts/hook.sh + continue-on-error: false + + - name: Verify webhook setup + run: | + echo "::notice::Webhook setup completed successfully for $WORKSPACE environment" + summary: name: Deployment Summary runs-on: ubuntu-latest - needs: [setup, validate, plan, apply] + needs: [setup, validate, lint, migrate, plan, apply, hook] if: always() steps: - name: Generate summary @@ -192,8 +337,11 @@ jobs: echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY echo "| Validate | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Code Linting | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Database Migration | ${{ needs.migrate.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Terraform Plan | ${{ needs.plan.result }} |" >> $GITHUB_STEP_SUMMARY echo "| Terraform Apply | ${{ needs.apply.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Webhook Setup | ${{ needs.hook.result }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY echo "**Workspace:** ${{ needs.setup.outputs.workspace }}" >> $GITHUB_STEP_SUMMARY @@ -202,4 +350,8 @@ jobs: if [ "${{ needs.apply.outputs.dispatcher_fn_id }}" != "" ]; then echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_fn_id }}" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + fi + + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "**Manual Trigger:** Yes" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/hook.yaml b/.github/workflows/hook.yaml deleted file mode 100644 index 3a8c719..0000000 --- a/.github/workflows/hook.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: Setup Telegram Webhooks - -on: - workflow_dispatch: - -concurrency: - group: hook-${{ github.ref }} - cancel-in-progress: true - -env: - TERRAFORM_VERSION: 1.6.3 - APP_CONFIG_PATH: config.json - -jobs: - hook: - name: Setup Telegram Webhooks - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Determine workspace - run: | - WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV - echo "::notice::Workspace: $WORKSPACE" - - - name: Set cache key - id: set-cache-key - run: | - CACHE_KEY="${{ runner.os }}-${{ github.ref_name }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" - echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT - - - name: Setup Terraform Environment - uses: ./.github/actions/setup-terraform - with: - terraform_version: ${{ env.TERRAFORM_VERSION }} - terraform_wrapper: 'false' - service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} - app_config: ${{ secrets.APP_CONFIG }} - app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ env.WORKSPACE }} - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - cache_key: ${{ steps.set-cache-key.outputs.cache_key }} - - - name: Get Dispatcher Function ID - id: get-fn-id - run: | - DISPATCHER_FUNCTION_ID=$(terraform output -raw dispatcher_function_id) - echo "dispatcher_function_id=$DISPATCHER_FUNCTION_ID" >> $GITHUB_OUTPUT - echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" - - - name: Execute webhook setup script - env: - DISPATCHER_FUNCTION_ID: ${{ steps.get-fn-id.outputs.dispatcher_function_id }} - run: | - echo "::notice::Config file: $APP_CONFIG_PATH" - echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" - - chmod +x _scripts/hook.sh - bash _scripts/hook.sh - continue-on-error: false - - - name: Verify webhook setup - run: | - echo "::notice::Webhook setup completed successfully for $WORKSPACE environment" - - - name: Generate summary - if: always() - run: | - echo "## Webhook Setup Summary - $WORKSPACE environment" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "**Dispatcher Function ID:** ${{ steps.get-fn-id.outputs.dispatcher_function_id }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/migration.yaml b/.github/workflows/migration.yaml deleted file mode 100644 index 4ea61d9..0000000 --- a/.github/workflows/migration.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: Database Migration - -on: - push: - branches: - - main - - dev - -concurrency: - group: migrate-${{ github.ref }} - cancel-in-progress: true - -env: - GOOSE_VERSION: 3.26.0 - -jobs: - migrate: - name: Database Migration - runs-on: ubuntu-latest - environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install YC CLI - run: | - curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash - echo "${HOME}/yandex-cloud/bin" >> $GITHUB_PATH - - - name: Authenticate YC CLI - env: - YC_SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }} - YC_CLOUD_ID: ${{ secrets.CLOUD_ID }} - YC_FOLDER_ID: ${{ secrets.FOLDER_ID }} - run: | - echo "$YC_SERVICE_ACCOUNT_KEY" > iam.json - yc config set service-account-key iam.json - yc config set cloud-id "$YC_CLOUD_ID" - yc config set folder-id "$YC_FOLDER_ID" - - - name: Generate short-lived IAM token - run: | - export YDB_ACCESS_TOKEN=$(yc iam create-token) - echo "YDB_ACCESS_TOKEN=$YDB_ACCESS_TOKEN" >> $GITHUB_ENV - - - name: Install Goose - run: | - GOOSE_VERSION="${{ env.GOOSE_VERSION }}" - wget -q "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_x86_64" -O goose - chmod +x goose - mkdir -p $HOME/.goose - mv goose $HOME/.goose/ - echo "$HOME/.goose" >> $GITHUB_PATH - - - name: Run Goose migrations - env: - GOOSE_DRIVER: ydb - GOOSE_DBSTRING_BASE: ${{ secrets.DB_CONNECTION_STRING }} - run: | - export PATH="$HOME/.goose:$PATH" - cd migrations - - # Add token and query options to connection string - GOOSE_DBSTRING="${GOOSE_DBSTRING_BASE}&token=${YDB_ACCESS_TOKEN}&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" - export GOOSE_DBSTRING - - WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - echo "::notice::Running Goose migrations for $WORKSPACE environment" - goose up - echo "::notice::Migration completed successfully for $WORKSPACE environment" - continue-on-error: true - - - name: Generate summary - if: always() - run: | - WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - echo "## Database Migration Summary - $WORKSPACE environment" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file From 22e448fa306191a096be83d418c30d4dafb990f7 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 11:29:42 +0300 Subject: [PATCH 16/59] feat: update workflow --- _scripts/hook.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_scripts/hook.sh b/_scripts/hook.sh index a9edd4c..868b0e4 100644 --- a/_scripts/hook.sh +++ b/_scripts/hook.sh @@ -1,7 +1,7 @@ #!/bin/bash -if [[ -z "$CONFIG_FILE_PATH" ]]; then - echo "[ERROR] CONFIG_FILE_PATH is not set" +if [[ -z "$APP_CONFIG_PATH" ]]; then + echo "[ERROR] APP_CONFIG_PATH is not set" exit 1 fi @@ -12,7 +12,7 @@ fi export DISPATCHER_ENDPOINT="https://functions.yandexcloud.net/${DISPATCHER_FUNCTION_ID}" -CONFIG_FILE="$CONFIG_FILE_PATH" +CONFIG_FILE="$APP_CONFIG_PATH" echo "[INFO] using config file: \"$CONFIG_FILE\"" echo "[INFO] using dispatcher function ID: \"$DISPATCHER_FUNCTION_ID\"" From 23ef02c28d5eebb0b74a8f9f72054e05267e1b3f Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 11:42:34 +0300 Subject: [PATCH 17/59] feat: update workflow + add load-config --- .github/actions/load-config/action.yaml | 34 ++++++++ .github/workflows/deploy.yaml | 104 +++++++++++++----------- 2 files changed, 90 insertions(+), 48 deletions(-) create mode 100644 .github/actions/load-config/action.yaml diff --git a/.github/actions/load-config/action.yaml b/.github/actions/load-config/action.yaml new file mode 100644 index 0000000..b70e765 --- /dev/null +++ b/.github/actions/load-config/action.yaml @@ -0,0 +1,34 @@ +name: 'Write Application Config' +description: 'Write application config.json file' +author: 'Your Name' + +inputs: + app_config: + description: 'Application config.json content' + required: true + app_config_path: + description: 'Path where config.json will be created' + required: false + default: 'config.json' + +runs: + using: 'composite' + steps: + - name: Write config.json + shell: bash + run: | + if [ -z "${{ inputs.app_config }}" ]; then + echo "::error::APP_CONFIG is empty" + exit 1 + fi + + # Write the config and validate it's valid JSON + echo '${{ inputs.app_config }}' > "${{ inputs.app_config_path }}" + + # Validate JSON format + if ! jq empty "${{ inputs.app_config_path }}" 2>/dev/null; then + echo "::error::APP_CONFIG is not valid JSON" + exit 1 + fi + + echo "::notice::Application config written to: ${{ inputs.app_config_path }}" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 02fbce8..c0d1512 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -23,20 +23,16 @@ jobs: runs-on: ubuntu-latest environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: - workspace: ${{ steps.set-workspace.outputs.workspace }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} steps: - name: Checkout code uses: actions/checkout@v4 - - name: Determine workspace and environment - id: set-workspace + - name: Set workspace run: | - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "workspace=prod" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then - echo "workspace=dev" >> $GITHUB_OUTPUT - fi + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" - name: Set cache key id: set-cache-key @@ -52,7 +48,7 @@ jobs: service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ steps.set-workspace.outputs.workspace }} + workspace: ${{ env.WORKSPACE }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ steps.set-cache-key.outputs.cache_key }} @@ -93,6 +89,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + - name: Install YC CLI run: | curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash @@ -135,7 +137,6 @@ jobs: GOOSE_DBSTRING="${GOOSE_DBSTRING_BASE}&token=${YDB_ACCESS_TOKEN}&go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" export GOOSE_DBSTRING - WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} echo "::notice::Running Goose migrations for $WORKSPACE environment" goose up echo "::notice::Migration completed successfully for $WORKSPACE environment" @@ -150,6 +151,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + - name: Setup Terraform Environment uses: ./.github/actions/setup-terraform with: @@ -157,7 +164,7 @@ jobs: service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ needs.setup.outputs.workspace }} + workspace: ${{ env.WORKSPACE }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ needs.setup.outputs.cache_key }} @@ -180,6 +187,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + - name: Setup Terraform Environment uses: ./.github/actions/setup-terraform with: @@ -187,7 +200,7 @@ jobs: service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ needs.setup.outputs.workspace }} + workspace: ${{ env.WORKSPACE }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ needs.setup.outputs.cache_key }} @@ -220,11 +233,17 @@ jobs: if: always() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} outputs: - dispatcher_fn_id: ${{ steps.outputs.outputs.dispatcher_fn_id }} + dispatcher_function_id: ${{ steps.outputs.outputs.dispatcher_function_id }} steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + echo "::notice::Workspace: $WORKSPACE" + - name: Setup Terraform Environment uses: ./.github/actions/setup-terraform with: @@ -233,7 +252,7 @@ jobs: service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ needs.setup.outputs.workspace }} + workspace: ${{ env.WORKSPACE }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} cache_key: ${{ needs.setup.outputs.cache_key }} @@ -245,12 +264,12 @@ jobs: run: | TIMESTAMP=$(date +%Y%m%d-%H%M%S) mkdir -p state-backups - terraform state pull > state-backups/terraform-${{ needs.setup.outputs.workspace }}-${TIMESTAMP}-${{ github.run_id }}.tfstate + terraform state pull > state-backups/terraform-$WORKSPACE-${TIMESTAMP}-${{ github.run_id }}.tfstate - name: Upload state backup uses: actions/upload-artifact@v4 with: - name: terraform-state-backup-${{ needs.setup.outputs.workspace }}-${{ github.run_id }} + name: terraform-state-backup-${{ env.WORKSPACE }}-${{ github.run_id }} path: state-backups/ retention-days: 90 @@ -263,57 +282,41 @@ jobs: - name: Export Terraform Outputs id: outputs run: | - DISPATCHER_FN_ID=$(terraform output -raw dispatcher_fn_id) - echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT - echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" + DISPATCHER_FUNCTION_ID=$(terraform output -raw dispatcher_function_id) + echo "dispatcher_function_id=$DISPATCHER_FUNCTION_ID" >> $GITHUB_OUTPUT + echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" hook: name: Setup Telegram Webhooks runs-on: ubuntu-latest - needs: [setup, apply] + needs: [apply] if: needs.apply.result == 'success' environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: - name: Checkout code uses: actions/checkout@v4 - - name: Determine workspace + - name: Set workspace run: | WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV echo "::notice::Workspace: $WORKSPACE" - - name: Set cache key - id: set-cache-key - run: | - CACHE_KEY="${{ runner.os }}-${{ github.ref_name }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}" - echo "cache_key=$CACHE_KEY" >> $GITHUB_OUTPUT - - - name: Setup Terraform Environment - uses: ./.github/actions/setup-terraform + - name: Load application config + uses: ./.github/actions/load-config with: - terraform_version: ${{ env.TERRAFORM_VERSION }} - terraform_wrapper: 'false' - service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }} app_config: ${{ secrets.APP_CONFIG }} app_config_path: ${{ env.APP_CONFIG_PATH }} - workspace: ${{ env.WORKSPACE }} - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - cache_key: ${{ steps.set-cache-key.outputs.cache_key }} - name: Get Dispatcher Function ID - id: get-fn-id run: | - DISPATCHER_FN_ID=$(terraform output -raw dispatcher_fn_id) - echo "dispatcher_fn_id=$DISPATCHER_FN_ID" >> $GITHUB_OUTPUT - echo "::notice::Dispatcher Function ID: $DISPATCHER_FN_ID" + DISPATCHER_FUNCTION_ID="${{ needs.apply.outputs.dispatcher_function_id }}" + echo "DISPATCHER_FUNCTION_ID=$DISPATCHER_FUNCTION_ID" >> $GITHUB_ENV + echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" - name: Execute webhook setup script - env: - DISPATCHER_FUNCTION_ID: ${{ steps.get-fn-id.outputs.dispatcher_fn_id }} run: | - echo "::notice::Config file: $APP_CONFIG_PATH" + echo "::notice::Config file: ${{ env.APP_CONFIG_PATH }}" echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" chmod +x _scripts/hook.sh @@ -330,9 +333,14 @@ jobs: needs: [setup, validate, lint, migrate, plan, apply, hook] if: always() steps: + - name: Set workspace + run: | + WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + - name: Generate summary run: | - echo "## Deployment Summary - ${{ needs.setup.outputs.workspace }} environment" >> $GITHUB_STEP_SUMMARY + echo "## Deployment Summary - $WORKSPACE environment" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY @@ -344,14 +352,14 @@ jobs: echo "| Webhook Setup | ${{ needs.hook.result }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Workspace:** ${{ needs.setup.outputs.workspace }}" >> $GITHUB_STEP_SUMMARY + echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "**Cache Key:** ${{ needs.setup.outputs.cache_key }}" >> $GITHUB_STEP_SUMMARY - if [ "${{ needs.apply.outputs.dispatcher_fn_id }}" != "" ]; then - echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_fn_id }}" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.apply.outputs.dispatcher_function_id }}" != "" ]; then + echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_function_id }}" >> $GITHUB_STEP_SUMMARY fi if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "**Manual Trigger:** Yes" >> $GITHUB_STEP_SUMMARY - fi + fi \ No newline at end of file From 728bf26aa0943d97920c52937ade2d87b089c57f Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 11:44:01 +0300 Subject: [PATCH 18/59] feat: update readme --- README.md | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 73c576b..0797297 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Below is an example of an `APP_CONFIG` value containing all bot information: - To find your owner ID, use [ID bot](https://t.me/myidbot) - Bot ID is the first number before `:` in the bot token - - TOKEN: `123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11` - - Bot ID: `123456789` + - TOKEN: `123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11` + - Bot ID: `123456789` --- @@ -92,21 +92,25 @@ Below is an example of an `APP_CONFIG` value containing all bot information: ### [1] Add a City You do: + 1. Get prayer times for a city in CSV format 2. Make a pull request (or open an issue) with the new file I do: + 1. Create a new Telegram bot 2. Upload the city file to storage bucket ### [2] Add a Language You do: + 1. Create translation text for the following files: - [./serverless/reminder/internal/handler/languages/text.yaml](./serverless/reminder/internal/handler/languages/text.yaml) - [./serverless/dispatcher/internal/handler/languages/en.yaml](./serverless/dispatcher/internal/handler/languages/en.yaml) (replace `en` with the new language code) I do: + 1. Deploy a new version of the code ### [3] Code Contributions @@ -118,16 +122,19 @@ Found a bug? Want to add a new feature? Just open an issue or submit a pull requ ## Development Roadmap 🚀 ### V1 ✅ + - [x] Support date format for `/prayersdate` command with leading zeros and delimiters (. / -) - [x] Implement subscriptions & notifications - [x] Update text messages to be more user-friendly ### V2 ✅ + - [x] Store prayer times in memory to reduce database requests - [x] Add response endpoint for admin to address feedback & bug messages - [x] Add Jumu'ah prayer reminders on Fridays ### V3 ✅ + - [x] Add time keyboard to `/date` command - [x] Remove selection message for `/date` & `/lang` after user interaction or timeout - [x] Terminate other active channels when user sends new commands @@ -137,6 +144,7 @@ Found a bug? Want to add a new feature? Just open an issue or submit a pull requ - [x] Write more robust tests for core features ### V4 ✅ + - [x] Add multi-language support (AR, RU, TT, TR, UZ) - [x] Implement script messages in the bot - [x] Set user script before command if not set @@ -144,11 +152,13 @@ Found a bug? Want to add a new feature? Just open an issue or submit a pull requ - [x] Fix prayer timetables for other languages ### V5 ✅ + - [x] Refactor code for better readability and maintainability - [x] Enhance logging to be more informative - [x] Enable using multiple bots with the same codebase ### V6 ✅ + - [x] Migrate to serverless architecture - [x] Automate deployment using Terraform - [x] Add support for multiple cities @@ -156,29 +166,36 @@ Found a bug? Want to add a new feature? Just open an issue or submit a pull requ - [x] Add `/stats` command for bot usage statistics ### V7 ✅ + - [x] Add jamaat gathering feature for group chats ### V8 🔄 + - [ ] Add support for all major world cities --- -## Development Setup 🖥️ - -Deploy bot on Yandex Cloud: - -```bash -export ENV=dev # or prod -./_scripts/init.sh $ENV prayer-bot -terraform workspace select -or-create=true $ENV -terraform apply # -auto-approve -./_scripts/hook.sh -``` - -Generate Terraform dependencies visualization: +## Visualization 🖥️ ```bash terraform plan -out plan.out terraform show -json plan.out > plan.json docker run --rm -it -p 9000:9000 -v $(pwd)/plan.json:/src/plan.json im2nguyen/rover:latest -planJSONPath=plan.json ``` + +```go +type ReminderConfig struct { + Offest time.Duration + MessageID int + LastAt time.Time +} + +type Reminder struct { + Today ReminderConfig + Soon ReminderConfig + Arrive ReminderConfig +} +``` + +- update code to match new infra +- delpoy to production From 5d217e04e8c6f37418bf908678c08c8f198430d3 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 12:53:56 +0300 Subject: [PATCH 19/59] feat: update chat --- domain/chat.go | 55 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/domain/chat.go b/domain/chat.go index 614a56e..d10de03 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -2,6 +2,7 @@ package domain import ( "errors" + "time" ) var ( @@ -37,6 +38,47 @@ func ReminderOffsets() []int32 { } } +type ReminderConfig struct { + Offset time.Duration `json:"offset"` + MessageID int `json:"message_id"` + LastAt time.Time `json:"last_at"` +} + +type JamaatDelay struct { + Fajr time.Duration `json:"fajr"` // default 10m (set on creation only) + Shuruq time.Duration `json:"shuruq"` // default 10m (set on creation only) + Dhuhr time.Duration `json:"dhuhr"` // default 10m (set on creation only) + Asr time.Duration `json:"asr"` // default 10m (set on creation only) + Maghrib time.Duration `json:"maghrib"` // default 10m (set on creation only) + Isha time.Duration `json:"isha"` // default 20m (set on creation only) +} + +func (j *JamaatDelay) GetDelayByPrayerID(prayerID PrayerID) time.Duration { + switch prayerID { + case PrayerIDFajr: + return j.Fajr + case PrayerIDShuruq: + return j.Shuruq + case PrayerIDDhuhr: + return j.Dhuhr + case PrayerIDAsr: + return j.Asr + case PrayerIDMaghrib: + return j.Maghrib + case PrayerIDIsha: + return j.Isha + default: + return 0 + } +} + +type Reminder struct { + Today ReminderConfig `json:"today"` + Soon ReminderConfig `json:"soon"` + Arrive ReminderConfig `json:"arrive"` + JamaatDelay JamaatDelay `json:"jamaat_delay"` +} + type ( Stats struct { Users uint64 // users using the bot @@ -46,12 +88,11 @@ type ( } Chat struct { - BotID int64 - ChatID int64 - State string - LanguageCode string - ReminderMessageID int32 - Jamaat bool - JamaatMessageID int32 + BotID int64 + ChatID int64 + State string + LanguageCode string + IsGroup bool + Reminder *Reminder } ) From c0dc2a88d7f4ca1ca6cd2fd53ac345d5d42a52ec Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 13:08:54 +0300 Subject: [PATCH 20/59] feat: update chat --- domain/chat.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/chat.go b/domain/chat.go index d10de03..552fd6c 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -84,7 +84,7 @@ type ( Users uint64 // users using the bot Subscribed uint64 // subscribed users Unsubscribed uint64 // unsubscribed users - LanguagesGrouped map[string]uint64 // users using a language + LanguagesGrouped map[string]uint64 // users grouped by language } Chat struct { @@ -93,6 +93,6 @@ type ( State string LanguageCode string IsGroup bool - Reminder *Reminder + Reminder Reminder } ) From 28c30e26192b0771b5680c636ab5cb6c07d9c62d Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 15:19:13 +0300 Subject: [PATCH 21/59] feat: update chat --- domain/chat.go | 44 +--------------------------------- domain/reminder.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 domain/reminder.go diff --git a/domain/chat.go b/domain/chat.go index 552fd6c..95a3d91 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -2,7 +2,6 @@ package domain import ( "errors" - "time" ) var ( @@ -38,47 +37,6 @@ func ReminderOffsets() []int32 { } } -type ReminderConfig struct { - Offset time.Duration `json:"offset"` - MessageID int `json:"message_id"` - LastAt time.Time `json:"last_at"` -} - -type JamaatDelay struct { - Fajr time.Duration `json:"fajr"` // default 10m (set on creation only) - Shuruq time.Duration `json:"shuruq"` // default 10m (set on creation only) - Dhuhr time.Duration `json:"dhuhr"` // default 10m (set on creation only) - Asr time.Duration `json:"asr"` // default 10m (set on creation only) - Maghrib time.Duration `json:"maghrib"` // default 10m (set on creation only) - Isha time.Duration `json:"isha"` // default 20m (set on creation only) -} - -func (j *JamaatDelay) GetDelayByPrayerID(prayerID PrayerID) time.Duration { - switch prayerID { - case PrayerIDFajr: - return j.Fajr - case PrayerIDShuruq: - return j.Shuruq - case PrayerIDDhuhr: - return j.Dhuhr - case PrayerIDAsr: - return j.Asr - case PrayerIDMaghrib: - return j.Maghrib - case PrayerIDIsha: - return j.Isha - default: - return 0 - } -} - -type Reminder struct { - Today ReminderConfig `json:"today"` - Soon ReminderConfig `json:"soon"` - Arrive ReminderConfig `json:"arrive"` - JamaatDelay JamaatDelay `json:"jamaat_delay"` -} - type ( Stats struct { Users uint64 // users using the bot @@ -93,6 +51,6 @@ type ( State string LanguageCode string IsGroup bool - Reminder Reminder + Reminder *Reminder } ) diff --git a/domain/reminder.go b/domain/reminder.go new file mode 100644 index 0000000..68fcc68 --- /dev/null +++ b/domain/reminder.go @@ -0,0 +1,59 @@ +package domain + +import "time" + +type ReminderType string + +const ( + ReminderTypeToday ReminderType = "today" + ReminderTypeSoon ReminderType = "soon" + ReminderTypeArrive ReminderType = "arrive" + ReminderTypeJamaatDelay ReminderType = "jamaat_delay" +) + +func (rt ReminderType) String() string { + return string(rt) +} + +type ( + JamaatDelay struct { + Fajr time.Duration `json:"fajr"` + Shuruq time.Duration `json:"shuruq"` + Dhuhr time.Duration `json:"dhuhr"` + Asr time.Duration `json:"asr"` + Maghrib time.Duration `json:"maghrib"` + Isha time.Duration `json:"isha"` + } + + ReminderConfig struct { + Offset time.Duration `json:"offset"` + MessageID int `json:"message_id"` + LastAt time.Time `json:"last_at"` + } + + Reminder struct { + Today *ReminderConfig `json:"today"` + Soon *ReminderConfig `json:"soon"` + Arrive *ReminderConfig `json:"arrive"` + JamaatDelay *JamaatDelay `json:"jamaat_delay"` + } +) + +func (j *JamaatDelay) GetDelayByPrayerID(prayerID PrayerID) time.Duration { + switch prayerID { + case PrayerIDFajr: + return j.Fajr + case PrayerIDShuruq: + return j.Shuruq + case PrayerIDDhuhr: + return j.Dhuhr + case PrayerIDAsr: + return j.Asr + case PrayerIDMaghrib: + return j.Maghrib + case PrayerIDIsha: + return j.Isha + default: + return 0 + } +} From 65a81fcbc549375e2e47aac1bafc70fc3f350c18 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 15:51:54 +0300 Subject: [PATCH 22/59] feat: update chat --- domain/chat.go | 1 + 1 file changed, 1 insertion(+) diff --git a/domain/chat.go b/domain/chat.go index 95a3d91..c331f3c 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -5,6 +5,7 @@ import ( ) var ( + ErrUnmarshalJSON = errors.New("unmarshal json") ErrInvalidArgument = errors.New("invalid argument") ErrNotFound = errors.New("not found") ErrAlreadyExists = errors.New("already exists") From a6d6306580779c31c521a09180d1ff29b8b624bf Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 16:47:43 +0300 Subject: [PATCH 23/59] feat: update chat --- README.md | 44 ++- _scripts/migrate_reminders/go.mod | 12 + _scripts/migrate_reminders/main.go | 251 ++++++++++++++ _scripts/update_serverless_packages.sh | 27 ++ serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 6 + .../dispatcher/internal/handler/handler.go | 40 ++- .../dispatcher/internal/handler/helper.go | 2 +- .../dispatcher/internal/handler/query.go | 5 +- serverless/dispatcher/internal/service/db.go | 68 ++-- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 6 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 8 + .../reminder/internal/handler/handler.go | 205 +++++------ .../reminder/internal/handler/helper.go | 38 ++ .../reminder/internal/handler/languages.go | 15 +- .../reminder/internal/handler/reminder.go | 327 ++++++++++++++++++ serverless/reminder/internal/service/db.go | 114 ++---- 19 files changed, 922 insertions(+), 252 deletions(-) create mode 100644 _scripts/migrate_reminders/go.mod create mode 100644 _scripts/migrate_reminders/main.go create mode 100755 _scripts/update_serverless_packages.sh create mode 100644 serverless/reminder/internal/handler/helper.go create mode 100644 serverless/reminder/internal/handler/reminder.go diff --git a/README.md b/README.md index 0797297..4242db6 100644 --- a/README.md +++ b/README.md @@ -183,19 +183,51 @@ terraform show -json plan.out > plan.json docker run --rm -it -p 9000:9000 -v $(pwd)/plan.json:/src/plan.json im2nguyen/rover:latest -planJSONPath=plan.json ``` +Hello! + +I want to change how I work with reminders, before I have reminder offest and reminder message id, now I have changed the type in db into json and will use the following Reminder object + ```go type ReminderConfig struct { - Offest time.Duration - MessageID int - LastAt time.Time + Offest time.Duration `json:"offset"` + MessageID int `json:"message_id"` + LastAt time.Time `json:"last_at"` +} + +type JamaatOffset struct { + Fajr time.Duration `json:"fajr"` // default 10m + Shuruq time.Duration `json:"shuruq"` // default 10m + Dhuhr time.Duration `json:"dhuhr"` // default 10m + Asr time.Duration `json:"asr"` // default 10m + Maghrib time.Duration `json:"maghrib"` // default 10m + Isha time.Duration `json:"isha"` // default 20m } type Reminder struct { - Today ReminderConfig - Soon ReminderConfig - Arrive ReminderConfig + // last_at is a date without hours, if last_at + 24h - offset is less than now then trigger + Today ReminderConfig `json:"today"` + // if last_at < prayer_time-offset, then trigger + Soon ReminderConfig `json:"soon"` + // if last_at < prayer_time+offset then trigger + Arrive ReminderConfig `json:"arrive"` // offset here not used + // Use to set the value in Jamaat after X min + GroupJamaatOffset JamaatOffset `json:"group_jamaa_offset"` } ``` +Now, to now that I need to send a message, event time + offset(might be negative) must be after last_at and before or equal now + +Update the dispatcher code when setting reminder, the value will be set on soon.offset + +add a today handler in reminder to send today's prayer message and delete previous one if any (using message_id) + +Refactor the code in reminder so that we have checks for each reminder (today, soon, arrive), if check passed, send message and delete previous message_id and update last_at and message_id + +--- + +I need you to improve this prompt instead of making changes + + + - update code to match new infra - delpoy to production diff --git a/_scripts/migrate_reminders/go.mod b/_scripts/migrate_reminders/go.mod new file mode 100644 index 0000000..11b56c3 --- /dev/null +++ b/_scripts/migrate_reminders/go.mod @@ -0,0 +1,12 @@ +module github.com/escalopa/prayer-bot/scripts/migrate_reminders + +go 1.21 + +require ( + github.com/escalopa/prayer-bot v0.0.0 + github.com/ydb-platform/ydb-go-genproto v0.0.0-20240920120314-0fed943b0136 + github.com/ydb-platform/ydb-go-sdk/v3 v3.82.3 + github.com/ydb-platform/ydb-go-yc v0.12.1 +) + +replace github.com/escalopa/prayer-bot => ../.. diff --git a/_scripts/migrate_reminders/main.go b/_scripts/migrate_reminders/main.go new file mode 100644 index 0000000..c7cd944 --- /dev/null +++ b/_scripts/migrate_reminders/main.go @@ -0,0 +1,251 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "time" + + "github.com/escalopa/prayer-bot/domain" + "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/table" + "github.com/ydb-platform/ydb-go-sdk/v3/table/result" + "github.com/ydb-platform/ydb-go-sdk/v3/table/types" + yc "github.com/ydb-platform/ydb-go-yc" +) + +var ( + writeTx = table.TxControl( + table.BeginTx(table.WithSerializableReadWrite()), + table.CommitTx(), + ) +) + +type oldChatData struct { + BotID int64 + ChatID int64 + ReminderOffset int32 + ReminderMessageID int32 + IsGroup bool + JamaatOffset int32 + JamaatMessageID int32 +} + +func main() { + dryRun := flag.Bool("dry-run", false, "Preview changes without writing to database") + flag.Parse() + + ctx := context.Background() + + // Load Moscow timezone + loc, err := time.LoadLocation("Europe/Moscow") + if err != nil { + log.Fatalf("Failed to load timezone: %v", err) + } + now := time.Now().In(loc) + + // Connect to YDB + ydbEndpoint := os.Getenv("YDB_ENDPOINT") + if ydbEndpoint == "" { + log.Fatal("YDB_ENDPOINT environment variable is not set") + } + + sdk, err := ydb.Open(ctx, ydbEndpoint, + yc.WithMetadataCredentials(), + yc.WithInternalCA(), + ) + if err != nil { + log.Fatalf("Failed to connect to YDB: %v", err) + } + defer func() { _ = sdk.Close(ctx) }() + + client := sdk.Table() + + // Statistics + var ( + totalProcessed int + totalMigrated int + totalSkipped int + ) + + // Query all chats + query := ` + SELECT bot_id, chat_id, reminder_offset, reminder_message_id, is_group, jamaat_offset, jamaat_message_id + FROM chats + ` + + err = client.Do(ctx, func(ctx context.Context, s table.Session) error { + _, res, err := s.Execute(ctx, + table.TxControl( + table.BeginTx(table.WithOnlineReadOnly()), + table.CommitTx(), + ), + query, + table.NewQueryParameters(), + ) + if err != nil { + return fmt.Errorf("execute query: %w", err) + } + defer func() { _ = res.Close() }() + + chats := []oldChatData{} + if res.NextResultSet(ctx) { + for res.NextRow() { + var chat oldChatData + var reminderOffset, reminderMessageID, jamaatOffset, jamaatMessageID *int32 + var isGroup *bool + + err = res.Scan( + &chat.BotID, + &chat.ChatID, + &reminderOffset, + &reminderMessageID, + &isGroup, + &jamaatOffset, + &jamaatMessageID, + ) + if err != nil { + log.Printf("Failed to scan row: %v", err) + continue + } + + // Handle nullable fields + if reminderOffset != nil { + chat.ReminderOffset = *reminderOffset + } + if reminderMessageID != nil { + chat.ReminderMessageID = *reminderMessageID + } + if isGroup != nil { + chat.IsGroup = *isGroup + } + if jamaatOffset != nil { + chat.JamaatOffset = *jamaatOffset + } + if jamaatMessageID != nil { + chat.JamaatMessageID = *jamaatMessageID + } + + chats = append(chats, chat) + } + } + + log.Printf("Found %d chats to process", len(chats)) + + // Process chats in batches + batchSize := 100 + for i := 0; i < len(chats); i += batchSize { + end := i + batchSize + if end > len(chats) { + end = len(chats) + } + batch := chats[i:end] + + for _, chat := range batch { + totalProcessed++ + + // Check if chat has any reminder data + hasReminderData := chat.ReminderOffset > 0 || + chat.ReminderMessageID > 0 || + chat.JamaatOffset > 0 || + chat.JamaatMessageID > 0 + + if !hasReminderData { + totalSkipped++ + continue + } + + // Build Reminder object + reminder := domain.Reminder{ + Today: domain.ReminderConfig{ + Offset: 0, // Disabled by default + MessageID: 0, + LastAt: now.Truncate(24 * time.Hour), // Date only + }, + Soon: domain.ReminderConfig{ + Offset: time.Duration(chat.ReminderOffset) * time.Minute, + MessageID: int(chat.ReminderMessageID), + LastAt: now, + }, + Arrive: domain.ReminderConfig{ + Offset: 0, // Always 0 + MessageID: 0, + LastAt: now, + }, + JamaatDelay: domain.JamaatDelay{ + Fajr: 10 * time.Minute, + Shuruq: 10 * time.Minute, + Dhuhr: 10 * time.Minute, + Asr: 10 * time.Minute, + Maghrib: 10 * time.Minute, + Isha: 20 * time.Minute, + }, + } + + // Serialize to JSON + reminderJSON, err := json.Marshal(reminder) + if err != nil { + log.Printf("Failed to marshal reminder for chat %d/%d: %v", chat.BotID, chat.ChatID, err) + continue + } + + if *dryRun { + log.Printf("[DRY RUN] Would migrate chat %d/%d: %s", chat.BotID, chat.ChatID, string(reminderJSON)) + totalMigrated++ + continue + } + + // Update database + updateQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + DECLARE $reminder AS Json; + + UPDATE chats + SET reminder = $reminder + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + params := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(chat.BotID)), + table.ValueParam("$chat_id", types.Int64Value(chat.ChatID)), + table.ValueParam("$reminder", types.JSONValue(string(reminderJSON))), + ) + + err = client.Do(ctx, func(ctx context.Context, s table.Session) error { + _, _, err := s.Execute(ctx, writeTx, updateQuery, params) + return err + }) + + if err != nil { + log.Printf("Failed to update chat %d/%d: %v", chat.BotID, chat.ChatID, err) + continue + } + + totalMigrated++ + if totalMigrated%10 == 0 { + log.Printf("Migrated %d chats so far...", totalMigrated) + } + } + } + + return nil + }) + + if err != nil { + log.Fatalf("Migration failed: %v", err) + } + + // Print statistics + log.Println("==================================================") + log.Println("Migration completed successfully!") + log.Printf("Total processed: %d", totalProcessed) + log.Printf("Total migrated: %d", totalMigrated) + log.Printf("Total skipped: %d", totalSkipped) + if *dryRun { + log.Println("*** DRY RUN MODE - No changes were made ***") + } +} diff --git a/_scripts/update_serverless_packages.sh b/_scripts/update_serverless_packages.sh new file mode 100755 index 0000000..50b6cd9 --- /dev/null +++ b/_scripts/update_serverless_packages.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Base module path +REPO="github.com/escalopa/prayer-bot" + +# Current commit hash +HASH=$(git rev-parse HEAD) +echo "Using commit hash: $HASH" + +# Ensure serverless directory exists +if [ ! -d "serverless" ]; then + echo "❌ Error: 'serverless' directory not found." + exit 1 +fi + +# Iterate over first-level subdirectories +for dir in serverless/*/; do + [ -d "$dir" ] || continue + echo "➡️ Entering $dir" + + # Run go get inside the subdirectory + (cd "$dir" && go get "$REPO@$HASH") + +done + +echo "✅ All serverless subprojects updated to $HASH" diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 93be88f..b60c62a 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 + github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 6ee1216..cf024cf 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -571,6 +571,12 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 3914db1..7570a59 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -17,7 +17,7 @@ import ( type ( DB interface { - CreateChat(ctx context.Context, botID int64, chatID int64, languageCode string, reminderOffset int32, state string, jamaat bool) error + CreateChat(ctx context.Context, botID int64, chatID int64, languageCode string, state string, isGroup bool, reminder *domain.Reminder) error GetChat(ctx context.Context, botID int64, chatID int64) (chat *domain.Chat, _ error) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, _ error) SetState(ctx context.Context, botID int64, chatID int64, state string) error @@ -27,7 +27,7 @@ type ( SetSubscribed(ctx context.Context, botID int64, chatID int64, subscribed bool) error SetLanguageCode(ctx context.Context, botID int64, chatID int64, languageCode string) error - SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderOffset int32) error + SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error } Handler struct { @@ -200,15 +200,15 @@ func (h *Handler) getBot(botID int64) (*bot.Bot, error) { func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.Chat, error) { botID := getContextBotID(ctx) chatID := int64(0) - jamaat := false + isGroup := false switch { case update.Message != nil: chatID = update.Message.Chat.ID - jamaat = isJamaat(update.Message.Chat) + isGroup = isGroupChat(update.Message.Chat) case update.CallbackQuery != nil && update.CallbackQuery.Message.Message != nil: chatID = update.CallbackQuery.Message.Message.Chat.ID - jamaat = isJamaat(update.CallbackQuery.Message.Message.Chat) + isGroup = isGroupChat(update.CallbackQuery.Message.Message.Chat) default: bytes, _ := json.Marshal(update) log.Info("ignore message", log.BotID(botID), log.String("update", string(bytes))) @@ -232,20 +232,34 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C languageCode = update.Message.From.LanguageCode } - err = h.db.CreateChat(ctx, botID, chatID, languageCode, int32(0), string(defaultState), jamaat) + now := h.now(botID) + reminder := &domain.Reminder{ + Today: &domain.ReminderConfig{LastAt: now}, + Soon: &domain.ReminderConfig{LastAt: now}, + Arrive: &domain.ReminderConfig{LastAt: now}, + JamaatDelay: &domain.JamaatDelay{ + Fajr: 10 * time.Minute, + Shuruq: 10 * time.Minute, + Dhuhr: 10 * time.Minute, + Asr: 10 * time.Minute, + Maghrib: 10 * time.Minute, + Isha: 20 * time.Minute, + }, + } + + err = h.db.CreateChat(ctx, botID, chatID, languageCode, string(defaultState), isGroup, reminder) if err != nil { log.Error("create chat", log.Err(err), log.BotID(botID), log.ChatID(chatID), "update", update) return nil, domain.ErrInternal } chat = &domain.Chat{ - BotID: botID, - ChatID: chatID, - State: string(defaultState), - LanguageCode: languageCode, - ReminderMessageID: 0, - Jamaat: jamaat, - JamaatMessageID: 0, + BotID: botID, + ChatID: chatID, + State: string(defaultState), + LanguageCode: languageCode, + IsGroup: isGroup, + Reminder: reminder, } return chat, nil diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 2b14394..68837ff 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -76,6 +76,6 @@ func layoutRowsInfo(totalItems, itemsPerRow int) (filled int, empty int) { return } -func isJamaat(chat models.Chat) bool { +func isGroupChat(chat models.Chat) bool { return chat.Type == models.ChatTypeGroup || chat.Type == models.ChatTypeSupergroup } diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index 899c48f..ce93ab4 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -100,9 +100,10 @@ func (h *Handler) remindQuery(ctx context.Context, b *bot.Bot, update *models.Up reminderOffset, _ := strconv.Atoi(strings.TrimPrefix(update.CallbackQuery.Data, remindQuery.String())) - err := h.db.SetReminderOffset(ctx, chat.BotID, chat.ChatID, int32(reminderOffset)) + offset := time.Duration(reminderOffset) * time.Minute + err := h.db.SetReminderOffset(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeSoon, offset) if err != nil { - log.Error("remindQuery: set remind offset", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + log.Error("remindQuery: set reminder soon offset", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) return domain.ErrInternal } diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 357b7d8..4cf2e97 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "os" "time" @@ -51,32 +52,38 @@ func (db *DB) CreateChat( botID int64, chatID int64, languageCode string, - reminderOffset int32, state string, - jamaat bool, + isGroup bool, + reminder *domain.Reminder, ) error { + + reminderJSON, err := json.Marshal(reminder) + if err != nil { + return err + } + query := ` DECLARE $bot_id AS Int64; DECLARE $chat_id AS Int64; DECLARE $language_code AS Utf8; - DECLARE $reminder_offset AS Int32; DECLARE $state AS Utf8; - DECLARE $jamaat AS Bool; + DECLARE $is_group AS Bool; + DECLARE $reminder AS Json; - INSERT INTO chats (bot_id, chat_id, language_code, reminder_offset, state, jamaat, created_at) - VALUES ($bot_id, $chat_id, $language_code, $reminder_offset, $state, $jamaat, CurrentUtcDatetime()); + INSERT INTO chats (bot_id, chat_id, language_code, state, is_group, reminder, created_at) + VALUES ($bot_id, $chat_id, $language_code, $state, $is_group, $reminder, CurrentUtcDatetime()); ` params := table.NewQueryParameters( table.ValueParam("$bot_id", types.Int64Value(botID)), table.ValueParam("$chat_id", types.Int64Value(chatID)), table.ValueParam("$language_code", types.UTF8Value(languageCode)), - table.ValueParam("$reminder_offset", types.Int32Value(reminderOffset)), table.ValueParam("$state", types.UTF8Value(state)), - table.ValueParam("$jamaat", types.BoolValue(jamaat)), + table.ValueParam("$is_group", types.BoolValue(isGroup)), + table.ValueParam("$reminder", types.JSONValue(string(reminderJSON))), ) - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { + err = db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, _, err := s.Execute(ctx, writeTx, query, params) return err }) @@ -96,7 +103,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom DECLARE $bot_id AS Int64; DECLARE $chat_id AS int64; - SELECT bot_id, chat_id, state, language_code, reminder_message_id, jamaat, jamaat_message_id + SELECT bot_id, chat_id, state, language_code, is_group, reminder FROM chats WHERE bot_id = $bot_id AND chat_id = $chat_id; ` @@ -115,18 +122,26 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom defer func(res result.Result) { _ = res.Close() }(res) if res.NextResultSet(ctx) && res.NextRow() { chat = &domain.Chat{} + var reminderJSON *string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.ReminderMessageID, - &chat.Jamaat, - &chat.JamaatMessageID, + &chat.IsGroup, + &reminderJSON, ) if err != nil { return err } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + return err + } + + chat.Reminder = &reminder + return nil } @@ -147,7 +162,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, query := ` DECLARE $bot_id AS Int64; - SELECT bot_id, chat_id, state, language_code, reminder_message_id, jamaat, jamaat_message_id + SELECT bot_id, chat_id, state, language_code, is_group, reminder FROM chats WHERE bot_id = $bot_id; ` @@ -163,18 +178,25 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, if res.NextResultSet(ctx) { for res.NextRow() { chat := &domain.Chat{} + var reminderJSON *string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.ReminderMessageID, - &chat.Jamaat, - &chat.JamaatMessageID, + &chat.IsGroup, + &reminderJSON, ) if err != nil { return err } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + return err + } + chat.Reminder = &reminder + chats = append(chats, chat) } } @@ -239,21 +261,25 @@ func (db *DB) SetSubscribed(ctx context.Context, botID int64, chatID int64, subs return err } -func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, offset int32) error { +func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error { + offsetNanos := offset.Nanoseconds() + query := ` DECLARE $bot_id AS Int64; DECLARE $chat_id AS Int64; - DECLARE $reminder_offset AS Int32; + DECLARE $reminder_type AS Utf8; + DECLARE $offset AS Int64; UPDATE chats - SET reminder_offset = $reminder_offset + SET reminder = Json_SetField(reminder, $reminder_type || ".offset", CAST($offset AS Json)) WHERE bot_id = $bot_id AND chat_id = $chat_id; ` params := table.NewQueryParameters( table.ValueParam("$bot_id", types.Int64Value(botID)), table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$reminder_offset", types.Int32Value(offset)), + table.ValueParam("$reminder_type", types.UTF8Value(reminderType.String())), + table.ValueParam("$offset", types.Int64Value(offsetNanos)), ) err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index bacd459..bbf7bdc 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 + github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 16a0a10..28e92f8 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -573,6 +573,12 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 26846ac..726800f 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 + github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index 6ee1216..b0f88c1 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -571,6 +571,14 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= +github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026100854-efcf173e5ca6 h1:jrr872AVeQ81U0l433R9fkoP7+Fys04JhURrREyo7DI= +github.com/escalopa/prayer-bot v0.0.0-20251026100854-efcf173e5ca6/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= +github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= +github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/handler.go b/serverless/reminder/internal/handler/handler.go index acc8a46..d33f182 100644 --- a/serverless/reminder/internal/handler/handler.go +++ b/serverless/reminder/internal/handler/handler.go @@ -3,9 +3,6 @@ package handler import ( "context" "fmt" - "os" - "slices" - "strconv" "strings" "sync" "time" @@ -17,41 +14,20 @@ import ( "golang.org/x/sync/errgroup" ) -var ( - jamaatDelay time.Duration -) - -func init() { - const jamattDelayDefault = 10 * time.Minute - - var ( - jamaatEnvKey = "JAMAAT_DELAY" - jamaatEnvValue = os.Getenv(jamaatEnvKey) - ) - - if jamaatEnvValue == "" { // not set, use default value - jamaatDelay = jamattDelayDefault - return - } - - jamaatDelayDuration, err := time.ParseDuration(jamaatEnvValue) - if err != nil { - log.Error("parse jamaat delay", log.Err(err)) - jamaatDelayDuration = jamattDelayDefault // set to default value on error - } - - jamaatDelay = jamaatDelayDuration -} - type ( DB interface { GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) (chats []*domain.Chat, _ error) GetSubscribers(ctx context.Context, botID int64) (chatIDs []int64, _ error) - GetSubscribersByOffset(ctx context.Context, botID int64, offset int32) (chatIDs []int64, _ error) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (prayerDay *domain.PrayerDay, _ error) - SetReminderMessageID(ctx context.Context, botID int64, chatID int64, reminderMessageID int32) error - SetJamaatMessageID(ctx context.Context, botID int64, chatID int64, jamaatMessageID int32) error DeleteChat(ctx context.Context, botID int64, chatID int64) error + UpdateReminder( + ctx context.Context, + botID int64, + chatID int64, + reminderType domain.ReminderType, + messageID int, + lastAt time.Time, + ) error } Handler struct { @@ -65,7 +41,7 @@ type ( ) func New(cfg map[int64]*domain.BotConfig, db DB) (*Handler, error) { - lp, err := newLanguageProvider() + lp, err := newLanguagesProvider() if err != nil { return nil, err } @@ -104,117 +80,108 @@ func (h *Handler) getBot(botID int64) (*bot.Bot, error) { } func (h *Handler) Handel(ctx context.Context, botID int64) error { - cfg := h.cfg[botID] - prayerID, reminderOffset, err := h.getPrayer(ctx, botID, cfg.Location.V()) - if err != nil { - log.Error("get prayer", log.Err(err), log.BotID(botID)) - return domain.ErrInternal - } - - var chatIDs []int64 - switch { - case reminderOffset == 0: - chatIDs, err = h.db.GetSubscribers(ctx, botID) - case slices.Contains(domain.ReminderOffsets(), reminderOffset): - chatIDs, err = h.db.GetSubscribersByOffset(ctx, botID, reminderOffset) - } - + chatIDs, err := h.db.GetSubscribers(ctx, botID) if err != nil { log.Error("get subscribers", log.Err(err), log.BotID(botID)) return domain.ErrInternal } + if len(chatIDs) == 0 { return nil } - err = h.remindUsers(ctx, botID, chatIDs, prayerID, reminderOffset) + // Get chat details + chats, err := h.db.GetChatsByIDs(ctx, botID, chatIDs) if err != nil { - log.Error("remindUsers", log.Err(err)) + log.Error("get chats", log.Err(err), log.BotID(botID)) return domain.ErrInternal } - return nil -} - -func (h *Handler) getPrayer(ctx context.Context, botID int64, loc *time.Location) (domain.PrayerID, int32, error) { - date := h.now(loc) - prayerDay, err := h.db.GetPrayerDay(ctx, botID, date) - if err != nil { - log.Error("get prayer day", - log.Err(err), - log.BotID(botID), - log.String("date", date.String()), - log.String("location", loc.String()), - ) - return 0, 0, domain.ErrInternal - } - - switch { - case prayerDay.Fajr.After(date) || prayerDay.Fajr.Equal(date): - return domain.PrayerIDFajr, int32(prayerDay.Fajr.Sub(date).Minutes()), nil - case prayerDay.Shuruq.After(date) || prayerDay.Shuruq.Equal(date): - return domain.PrayerIDShuruq, int32(prayerDay.Shuruq.Sub(date).Minutes()), nil - case prayerDay.Dhuhr.After(date) || prayerDay.Dhuhr.Equal(date): - return domain.PrayerIDDhuhr, int32(prayerDay.Dhuhr.Sub(date).Minutes()), nil - case prayerDay.Asr.After(date) || prayerDay.Asr.Equal(date): - return domain.PrayerIDAsr, int32(prayerDay.Asr.Sub(date).Minutes()), nil - case prayerDay.Maghrib.After(date) || prayerDay.Maghrib.Equal(date): - return domain.PrayerIDMaghrib, int32(prayerDay.Maghrib.Sub(date).Minutes()), nil - case prayerDay.Isha.After(date) || prayerDay.Isha.Equal(date): - return domain.PrayerIDIsha, int32(prayerDay.Isha.Sub(date).Minutes()), nil - } - - // if no prayer time is found, return the first prayer of the next day - prayerDay, err = h.db.GetPrayerDay(ctx, botID, date.AddDate(0, 0, 1)) + // Get bot + b, err := h.getBot(botID) if err != nil { - log.Error("get next prayer day", - log.Err(err), - log.BotID(botID), - log.String("date", date.String()), - log.String("location", loc.String()), - ) - return 0, 0, domain.ErrInternal + log.Error("get bot", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } - return domain.PrayerIDFajr, int32(prayerDay.Fajr.Sub(date).Minutes()), nil -} - -func (h *Handler) remindUsers( - ctx context.Context, - botID int64, - chatIDs []int64, - prayerID domain.PrayerID, - reminderOffset int32, -) error { - chats, err := h.db.GetChatsByIDs(ctx, botID, chatIDs) + // Get current day prayer times + cfg := h.cfg[botID] + now := time.Now().In(cfg.Location.V()) + date := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + prayerDay, err := h.db.GetPrayerDay(ctx, botID, date) if err != nil { - log.Error("get chats", log.Err(err), log.BotID(botID)) + log.Error("get prayer day", log.Err(err), log.BotID(botID)) return domain.ErrInternal } - b, err := h.getBot(botID) - if err != nil { - log.Error("get bot", log.Err(err), log.BotID(botID)) - return domain.ErrInternal + // Initialize reminder types + reminders := []ReminderType{ + &TodayReminder{ + lp: h.lp, + botConfig: h.cfg, + formatPrayerDay: h.formatPrayerDay, + }, + &SoonReminder{lp: h.lp}, + &ArriveReminder{lp: h.lp}, + &JamaatReminder{lp: h.lp}, } + // Process each chat errG := &errgroup.Group{} for _, chat := range chats { chat := chat errG.Go(func() error { - fn := h.remindUser - if chat.Jamaat && prayerID != domain.PrayerIDShuruq /* shuruq isn't prayed in Jamaat */ { - fn = h.remindUserJamaat + // Skip chats without reminder config + if chat.Reminder == nil { + return nil } - err := fn(ctx, b, chat, prayerID, reminderOffset) - if err != nil { - log.Error("remindUsers", - log.Err(err), - log.BotID(chat.BotID), - log.ChatID(chat.ChatID), - log.String("prayer_id", strconv.Itoa(int(prayerID))), - log.String("reminder_offset", strconv.Itoa(int(reminderOffset))), - ) + + // Check each reminder type + for _, reminder := range reminders { + shouldSend, prayerID := reminder.Check(ctx, chat, prayerDay, now) + if !shouldSend { + continue + } + + // Send reminder + err := reminder.Send(ctx, b, chat, prayerID, prayerDay) + if err != nil { + if h.isBlockedErr(err) { + h.deleteChat(ctx, chat) + return nil + } + log.Error("send reminder", + log.Err(err), + log.BotID(chat.BotID), + log.ChatID(chat.ChatID), + log.String("reminder_type", reminder.Name()), + ) + continue + } + + // Update state + var messageID int + switch reminder.Name() { + case "today": + messageID = chat.Reminder.Today.MessageID + case "soon": + messageID = chat.Reminder.Soon.MessageID + case "arrive": + messageID = chat.Reminder.Arrive.MessageID + case "jamaat": + // Jamaat is stateless, no message ID to track + messageID = 0 + } + + err = reminder.UpdateState(ctx, h.db, chat, messageID, now) + if err != nil { + log.Error("update reminder state", + log.Err(err), + log.BotID(chat.BotID), + log.ChatID(chat.ChatID), + log.String("reminder_type", reminder.Name()), + ) + } } return nil }) @@ -360,6 +327,7 @@ func (h *Handler) remindUserJamaat( h.deleteMessages(ctx, b, chat, chat.ReminderMessageID, chat.JamaatMessageID) return nil } +// formatPrayerDay formats the domain.PrayerDay into a string (copied from dispatcher service) func (h *Handler) deleteChat(ctx context.Context, chat *domain.Chat) { err := h.db.DeleteChat(ctx, chat.BotID, chat.ChatID) @@ -378,6 +346,7 @@ func (h *Handler) deleteMessages(ctx context.Context, b *bot.Bot, chat *domain.C } messageIDs = append(messageIDs, int(id)) } + if len(messageIDs) == 0 { return // nothing to do } diff --git a/serverless/reminder/internal/handler/helper.go b/serverless/reminder/internal/handler/helper.go new file mode 100644 index 0000000..b652e93 --- /dev/null +++ b/serverless/reminder/internal/handler/helper.go @@ -0,0 +1,38 @@ +package handler + +import ( + "fmt" + + "github.com/escalopa/prayer-bot/domain" +) + +const ( + prayerDayFormat = "02.01.2006" + prayerTimeFormat = "15:04" + + prayerText = ` +🗓 %s + +🕊 %s — %s +🌤 %s — %s +☀️ %s — %s +🌇 %s — %s +🌅 %s — %s +🌙 %s — %s +` +) + +func (h *Handler) formatPrayerDay(botID int64, prayerDay *domain.PrayerDay, languageCode string) string { + loc := h.cfg[botID].Location.V() + text := h.lp.GetText(languageCode) + + return fmt.Sprintf(prayerText, + prayerDay.Date.Format(prayerDayFormat), + text.Prayer[int(domain.PrayerIDFajr)], prayerDay.Fajr.In(loc).Format(prayerTimeFormat), + text.Prayer[int(domain.PrayerIDShuruq)], prayerDay.Shuruq.In(loc).Format(prayerTimeFormat), + text.Prayer[int(domain.PrayerIDDhuhr)], prayerDay.Dhuhr.In(loc).Format(prayerTimeFormat), + text.Prayer[int(domain.PrayerIDAsr)], prayerDay.Asr.In(loc).Format(prayerTimeFormat), + text.Prayer[int(domain.PrayerIDMaghrib)], prayerDay.Maghrib.In(loc).Format(prayerTimeFormat), + text.Prayer[int(domain.PrayerIDIsha)], prayerDay.Isha.In(loc).Format(prayerTimeFormat), + ) +} diff --git a/serverless/reminder/internal/handler/languages.go b/serverless/reminder/internal/handler/languages.go index 7ef6fc5..cef8bd4 100644 --- a/serverless/reminder/internal/handler/languages.go +++ b/serverless/reminder/internal/handler/languages.go @@ -1,7 +1,7 @@ package handler import ( - "os" + _ "embed" "gopkg.in/yaml.v3" ) @@ -21,20 +21,15 @@ type ( } ) -func newLanguageProvider() (*languagesProvider, error) { - const filename = "internal/handler/languages/text.yaml" // relative to the `main.go` directory (source root) - - content, err := os.ReadFile(filename) - if err != nil { - return nil, err - } +//go:embed languages/text.yaml +var textData string +func newLanguagesProvider() (*languagesProvider, error) { var storage map[string]*Text - err = yaml.Unmarshal(content, &storage) + err := yaml.Unmarshal([]byte(textData), &storage) if err != nil { return nil, err } - return &languagesProvider{storage: storage}, nil } diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go new file mode 100644 index 0000000..a33904b --- /dev/null +++ b/serverless/reminder/internal/handler/reminder.go @@ -0,0 +1,327 @@ +package handler + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/escalopa/prayer-bot/domain" + "github.com/escalopa/prayer-bot/log" + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +type ReminderType interface { + Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID) + Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (messageID int, err error) + Name() string +} + +// TodayReminder - Daily schedule +type TodayReminder struct { + lp *languagesProvider + botConfig map[int64]*domain.BotConfig + formatPrayerDay func(botID int64, prayerDay *domain.PrayerDay, languageCode string) string +} + +func (r *TodayReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { + if chat.Reminder == nil || chat.Reminder.Today.Offset == 0 { + return false, 0 // Disabled + } + + config := chat.Reminder.Today + // Trigger logic: last_at + 24h - offset < now + lastDate := config.LastAt.Truncate(24 * time.Hour) + triggerTime := lastDate.Add(24 * time.Hour).Add(-config.Offset) + return triggerTime.Before(now) || triggerTime.Equal(now), 0 // Not prayer-specific +} + +func (r *TodayReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { + // Format using the same logic as dispatcher service + text := r.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode) + + // Delete old message if exists + if chat.Reminder.Today.MessageID != 0 { + _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ + ChatID: chat.ChatID, + MessageID: chat.Reminder.Today.MessageID, + }) + } + + // Send new message + res, err := b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: text, + }) + if err != nil { + return err + } + + // Store message ID for later cleanup + chat.Reminder.Today.MessageID = res.ID + return nil +} + +func (r *TodayReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { + lastAt := now.Truncate(24 * time.Hour) // Date only + chat.Reminder.Today.MessageID = messageID + chat.Reminder.Today.LastAt = lastAt + return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeToday, messageID, lastAt) +} + +func (r *TodayReminder) Name() string { return "today" } + +// SoonReminder - "Prayer coming soon" +type SoonReminder struct { + lp *languagesProvider +} + +func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { + if chat.Reminder == nil || chat.Reminder.Soon.Offset == 0 { + return false, 0 // Disabled + } + + config := chat.Reminder.Soon + + // Check all prayers + prayers := []struct { + id domain.PrayerID + time time.Time + }{ + {domain.PrayerIDFajr, prayerDay.Fajr}, + {domain.PrayerIDShuruq, prayerDay.Shuruq}, + {domain.PrayerIDDhuhr, prayerDay.Dhuhr}, + {domain.PrayerIDAsr, prayerDay.Asr}, + {domain.PrayerIDMaghrib, prayerDay.Maghrib}, + {domain.PrayerIDIsha, prayerDay.Isha}, + } + + for _, p := range prayers { + // Trigger logic: last_at < prayer_time - offset AND prayer_time - offset <= now + triggerTime := p.time.Add(-config.Offset) + if config.LastAt.Before(triggerTime) && + (triggerTime.Before(now) || triggerTime.Equal(now)) { + return true, p.id + } + } + return false, 0 +} + +func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { + text := r.lp.GetText(chat.LanguageCode) + duration := chat.Reminder.Soon.Offset + prayer := text.Prayer[int(prayerID)] + message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)) + + // Delete old message + if chat.Reminder.Soon.MessageID != 0 { + _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ + ChatID: chat.ChatID, + MessageID: chat.Reminder.Soon.MessageID, + }) + } + + res, err := b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: message, + ParseMode: models.ParseModeMarkdown, + }) + if err != nil { + return err + } + + chat.Reminder.Soon.MessageID = res.ID + return nil +} + +func (r *SoonReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { + chat.Reminder.Soon.MessageID = messageID + chat.Reminder.Soon.LastAt = now + return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeSoon, messageID, now) +} + +func (r *SoonReminder) Name() string { return "soon" } + +// ArriveReminder - "Prayer time arrived" +type ArriveReminder struct { + lp *languagesProvider +} + +func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { + if chat.Reminder == nil { + return false, 0 + } + + config := chat.Reminder.Arrive + // Offset not used (always 0), check if any prayer time has arrived + + prayers := []struct { + id domain.PrayerID + time time.Time + }{ + {domain.PrayerIDFajr, prayerDay.Fajr}, + {domain.PrayerIDShuruq, prayerDay.Shuruq}, + {domain.PrayerIDDhuhr, prayerDay.Dhuhr}, + {domain.PrayerIDAsr, prayerDay.Asr}, + {domain.PrayerIDMaghrib, prayerDay.Maghrib}, + {domain.PrayerIDIsha, prayerDay.Isha}, + } + + for _, p := range prayers { + // Trigger logic: last_at < prayer_time AND prayer_time <= now + if config.LastAt.Before(p.time) && + (p.time.Before(now) || p.time.Equal(now)) { + return true, p.id + } + } + return false, 0 +} + +func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { + text := r.lp.GetText(chat.LanguageCode) + prayer := text.Prayer[int(prayerID)] + message := fmt.Sprintf(text.PrayerArrived, prayer) + + // Delete old message + if chat.Reminder.Arrive.MessageID != 0 { + _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ + ChatID: chat.ChatID, + MessageID: chat.Reminder.Arrive.MessageID, + }) + } + + res, err := b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: message, + ParseMode: models.ParseModeMarkdown, + }) + if err != nil { + return err + } + + chat.Reminder.Arrive.MessageID = res.ID + return nil +} + +func (r *ArriveReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { + chat.Reminder.Arrive.MessageID = messageID + chat.Reminder.Arrive.LastAt = now + return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeArrive, messageID, now) +} + +func (r *ArriveReminder) Name() string { return "arrive" } + +// JamaatReminder - Group Jamaat polls +type JamaatReminder struct { + lp *languagesProvider +} + +func (r *JamaatReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { + if !chat.IsGroup || chat.Reminder == nil { + return false, 0 // Only for group chats + } + + delays := chat.Reminder.JamaatDelay // Read as-is + + prayers := []struct { + id domain.PrayerID + time time.Time + }{ + {domain.PrayerIDFajr, prayerDay.Fajr}, + // Skip Shuruq (no Jamaat for sunrise) + {domain.PrayerIDDhuhr, prayerDay.Dhuhr}, + {domain.PrayerIDAsr, prayerDay.Asr}, + {domain.PrayerIDMaghrib, prayerDay.Maghrib}, + {domain.PrayerIDIsha, prayerDay.Isha}, + } + + // Track state per prayer using a simple in-memory approach or check based on lastAt + // Since we removed the Jamaat ReminderConfig, we'll use a simpler approach: + // Check if the Jamaat time (prayer_time + delay) has arrived + for _, p := range prayers { + delay := delays.GetDelayByPrayerID(p.id) + if delay == 0 { + continue // Disabled for this prayer + } + + // Trigger logic: prayer_time + delay <= now + // We'll check if jamaat time has arrived + jamaatTime := p.time.Add(delay) + if jamaatTime.Before(now) || jamaatTime.Equal(now) { + return true, p.id + } + } + return false, 0 +} + +func (r *JamaatReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { + text := r.lp.GetText(chat.LanguageCode) + prayer := text.Prayer[int(prayerID)] + + delay := chat.Reminder.JamaatDelay.GetDelayByPrayerID(prayerID) + prayerTime := getPrayerTimeByID(prayerDay, prayerID) + jamaatTime := prayerTime.Add(delay) + + // Check if Jamaat time has arrived + now := time.Now() + if now.After(jamaatTime) || now.Equal(jamaatTime) { + // Jamaat has started - send arrival message + message := fmt.Sprintf(text.PrayerArrived, prayer) + _, err := b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: message, + ParseMode: models.ParseModeMarkdown, + }) + if err != nil { + return err + } + } else { + // Send poll for upcoming Jamaat + message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(delay)) + message = strings.ReplaceAll(message, "*", "") // Remove markdown for poll + + isAnonymous := false + _, err := b.SendPoll(ctx, &bot.SendPollParams{ + ChatID: chat.ChatID, + Question: message, + Options: []models.InputPollOption{ + {Text: text.PrayerJoin, TextParseMode: models.ParseModeMarkdown}, + {Text: text.PrayerJoinDelay, TextParseMode: models.ParseModeMarkdown}, + }, + IsAnonymous: &isAnonymous, + }) + if err != nil { + return err + } + } + + return nil +} + +func (r *JamaatReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { + // Jamaat reminders are stateless - no state to update + return nil +} + +func (r *JamaatReminder) Name() string { return "jamaat" } + +// Helper function to get prayer time by ID +func getPrayerTimeByID(prayerDay *domain.PrayerDay, prayerID domain.PrayerID) time.Time { + switch prayerID { + case domain.PrayerIDFajr: + return prayerDay.Fajr + case domain.PrayerIDShuruq: + return prayerDay.Shuruq + case domain.PrayerIDDhuhr: + return prayerDay.Dhuhr + case domain.PrayerIDAsr: + return prayerDay.Asr + case domain.PrayerIDMaghrib: + return prayerDay.Maghrib + case domain.PrayerIDIsha: + return prayerDay.Isha + default: + return time.Time{} + } +} diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index bac98b5..6f23838 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -2,16 +2,17 @@ package service import ( "context" + "encoding/json" "os" "time" "github.com/escalopa/prayer-bot/domain" + "github.com/escalopa/prayer-bot/log" "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" - "github.com/ydb-platform/ydb-go-sdk/v3/table/result" - "github.com/ydb-platform/ydb-go-sdk/v3/table/types" - "github.com/ydb-platform/ydb-go-sdk/v3" "github.com/ydb-platform/ydb-go-sdk/v3/table" + "github.com/ydb-platform/ydb-go-sdk/v3/table/result" + "github.com/ydb-platform/ydb-go-sdk/v3/table/types" yc "github.com/ydb-platform/ydb-go-yc" ) @@ -50,7 +51,7 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( DECLARE $bot_id AS Int64; DECLARE $chat_ids AS List; - SELECT bot_id, chat_id, state, language_code, reminder_message_id, jamaat, jamaat_message_id + SELECT bot_id, chat_id, state, language_code, is_group, reminder FROM chats WHERE bot_id = $bot_id AND chat_id IN $chat_ids; ` @@ -75,18 +76,26 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( if res.NextResultSet(ctx) { for res.NextRow() { chat := &domain.Chat{} + var reminderJSON *string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.ReminderMessageID, - &chat.Jamaat, - &chat.JamaatMessageID, + &chat.IsGroup, + &reminderJSON, ) if err != nil { return err } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + log.Error("unmarshal reminder json", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrUnmarshalJSON + } + chat.Reminder = &reminder + chats = append(chats, chat) } } @@ -139,49 +148,6 @@ func (db *DB) GetSubscribers(ctx context.Context, botID int64) (chatIDs []int64, return chatIDs, nil } -func (db *DB) GetSubscribersByOffset(ctx context.Context, botID int64, offset int32) (chatIDs []int64, _ error) { - query := ` - DECLARE $bot_id AS Int64; - DECLARE $offset AS Int32; - - SELECT chat_id - FROM chats - WHERE bot_id = $bot_id AND subscribed = true AND reminder_offset = $offset; - ` - - params := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$offset", types.Int32Value(offset)), - ) - - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, res, err := s.Execute(ctx, readTx, query, params) - if err != nil { - return err - } - - defer func(res result.Result) { _ = res.Close() }(res) - for res.NextResultSet(ctx) { - for res.NextRow() { - var chatID int64 - err = res.ScanWithDefaults(&chatID) - if err != nil { - return err - } - chatIDs = append(chatIDs, chatID) - } - } - - return nil - }) - - if err != nil { - return nil, err - } - - return chatIDs, nil -} - func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (prayerDay *domain.PrayerDay, _ error) { query := ` DECLARE $bot_id AS Int64; @@ -231,46 +197,38 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr return prayerDay, nil } -func (db *DB) SetReminderMessageID(ctx context.Context, botID int64, chatID int64, reminderMessageID int32) error { - query := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - DECLARE $reminder_message_id AS Int32; - - UPDATE chats - SET reminder_message_id = $reminder_message_id - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` - - params := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$reminder_message_id", types.Int32Value(reminderMessageID)), - ) - - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, _, err := s.Execute(ctx, writeTx, query, params) - return err - }) - - return err -} +func (db *DB) UpdateReminder( + ctx context.Context, + botID int64, + chatID int64, + reminderType domain.ReminderType, + messageID int, + lastAt time.Time, +) error { + lastAtNanos := lastAt.UnixNano() -func (db *DB) SetJamaatMessageID(ctx context.Context, botID int64, chatID int64, jamaatMessageID int32) error { query := ` DECLARE $bot_id AS Int64; DECLARE $chat_id AS Int64; - DECLARE $jamaat_message_id AS Int32; + DECLARE $reminder_type AS Utf8; + DECLARE $message_id AS Int64; + DECLARE $last_at AS Int64; UPDATE chats - SET jamaat_message_id = $jamaat_message_id + SET reminder = Json_SetField( + Json_SetField(reminder, $reminder_type || ".message_id", CAST($message_id AS Json)), + $reminder_type || ".last_at", + CAST($last_at AS Json) + ) WHERE bot_id = $bot_id AND chat_id = $chat_id; ` params := table.NewQueryParameters( table.ValueParam("$bot_id", types.Int64Value(botID)), table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$jamaat_message_id", types.Int32Value(jamaatMessageID)), + table.ValueParam("$reminder_type", types.UTF8Value(reminderType.String())), + table.ValueParam("$message_id", types.Int64Value(int64(messageID))), + table.ValueParam("$last_at", types.Int64Value(lastAtNanos)), ) err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { From 58d58e01a8638a04d4c9fd10e59cdacc3c1539af Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 17:20:44 +0300 Subject: [PATCH 24/59] feat: update chat --- domain/prayer.go | 15 ++++++++------- domain/reminder.go | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/domain/prayer.go b/domain/prayer.go index a0a092a..c3582ad 100644 --- a/domain/prayer.go +++ b/domain/prayer.go @@ -18,13 +18,14 @@ const ( ) type PrayerDay struct { - Date time.Time `json:"date"` - Fajr time.Time `json:"fajr"` - Shuruq time.Time `json:"shuruq"` - Dhuhr time.Time `json:"dhuhr"` - Asr time.Time `json:"asr"` - Maghrib time.Time `json:"maghrib"` - Isha time.Time `json:"isha"` + Date time.Time `json:"date"` + Fajr time.Time `json:"fajr"` + Shuruq time.Time `json:"shuruq"` + Dhuhr time.Time `json:"dhuhr"` + Asr time.Time `json:"asr"` + Maghrib time.Time `json:"maghrib"` + Isha time.Time `json:"isha"` + NextDay *PrayerDay `json:"next_day"` } //revive:disable:argument-limit diff --git a/domain/reminder.go b/domain/reminder.go index 68fcc68..d1cc078 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -17,6 +17,7 @@ func (rt ReminderType) String() string { type ( JamaatDelay struct { + Enabled bool `json:"enabled"` Fajr time.Duration `json:"fajr"` Shuruq time.Duration `json:"shuruq"` Dhuhr time.Duration `json:"dhuhr"` From d4a267e729157485703ef845ed333a7ea3dcd675 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 17:30:23 +0300 Subject: [PATCH 25/59] feat: update chat --- domain/reminder.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/domain/reminder.go b/domain/reminder.go index d1cc078..5ca5704 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -5,10 +5,10 @@ import "time" type ReminderType string const ( - ReminderTypeToday ReminderType = "today" - ReminderTypeSoon ReminderType = "soon" - ReminderTypeArrive ReminderType = "arrive" - ReminderTypeJamaatDelay ReminderType = "jamaat_delay" + ReminderTypeToday ReminderType = "today" + ReminderTypeSoon ReminderType = "soon" + ReminderTypeArrive ReminderType = "arrive" + ReminderTypeJamaat ReminderType = "jamaat" ) func (rt ReminderType) String() string { @@ -16,8 +16,7 @@ func (rt ReminderType) String() string { } type ( - JamaatDelay struct { - Enabled bool `json:"enabled"` + JamaatDelayConfig struct { Fajr time.Duration `json:"fajr"` Shuruq time.Duration `json:"shuruq"` Dhuhr time.Duration `json:"dhuhr"` @@ -26,6 +25,11 @@ type ( Isha time.Duration `json:"isha"` } + JamaatConfig struct { + Enabled bool `json:"enabled"` + Delay *JamaatDelayConfig `json:"delay"` + } + ReminderConfig struct { Offset time.Duration `json:"offset"` MessageID int `json:"message_id"` @@ -33,14 +37,14 @@ type ( } Reminder struct { - Today *ReminderConfig `json:"today"` - Soon *ReminderConfig `json:"soon"` - Arrive *ReminderConfig `json:"arrive"` - JamaatDelay *JamaatDelay `json:"jamaat_delay"` + Today *ReminderConfig `json:"today"` + Soon *ReminderConfig `json:"soon"` + Arrive *ReminderConfig `json:"arrive"` + Jamaat *JamaatConfig `json:"jamaat"` } ) -func (j *JamaatDelay) GetDelayByPrayerID(prayerID PrayerID) time.Duration { +func (j *JamaatDelayConfig) GetDelayByPrayerID(prayerID PrayerID) time.Duration { switch prayerID { case PrayerIDFajr: return j.Fajr From 563e91081a471f8fb27def5af27e30058028d71a Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 18:07:41 +0300 Subject: [PATCH 26/59] feat: update serverless --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 8 + .../dispatcher/internal/handler/command.go | 33 ++- .../dispatcher/internal/handler/handler.go | 16 +- .../dispatcher/internal/handler/helper.go | 6 +- serverless/dispatcher/internal/service/db.go | 48 ++- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 8 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 8 + .../reminder/internal/handler/handler.go | 220 +------------- .../reminder/internal/handler/helper.go | 44 +++ .../reminder/internal/handler/reminder.go | 276 +++++------------- serverless/reminder/internal/service/db.go | 48 ++- 14 files changed, 256 insertions(+), 465 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index b60c62a..0e27c92 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 + github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index cf024cf..9ffe8cc 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -577,6 +577,14 @@ github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpL github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index 38cd4e7..924d45f 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -21,11 +21,11 @@ const ( prayerText = ` 🗓 %s, %s -🕊 %s — %s -🌤 %s — %s -☀️ %s — %s -🌇 %s — %s -🌅 %s — %s +🕊 %s — %s +🌤 %s — %s +☀️ %s — %s +🌇 %s — %s +🌅 %s — %s 🌙 %s — %s ` ) @@ -151,16 +151,19 @@ func (h *Handler) next(ctx context.Context, b *bot.Bot, _ *models.Update) error prayerID, duration = domain.PrayerIDMaghrib, prayerDay.Maghrib.Sub(now) case prayerDay.Isha.After(now): prayerID, duration = domain.PrayerIDIsha, prayerDay.Isha.Sub(now) - } - - // when no prayer time is found, return the first prayer of the next day - if prayerID == domain.PrayerIDUnknown || duration == 0 { - prayerDay, err = h.db.GetPrayerDay(ctx, chat.BotID, date.AddDate(0, 0, 1)) - if err != nil { - log.Error("next: get prayer day", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - prayerID, duration = domain.PrayerIDFajr, prayerDay.Fajr.Sub(now) + case prayerDay.NextDay.Fajr.After(now): + prayerID, duration = domain.PrayerIDFajr, prayerDay.NextDay.Fajr.Sub(now) + case prayerDay.NextDay.Shuruq.After(now): + prayerID, duration = domain.PrayerIDShuruq, prayerDay.NextDay.Shuruq.Sub(now) + default: + log.Error("next: no prayer time found", + log.BotID(chat.BotID), + log.ChatID(chat.ChatID), + log.String("date", date.String()), + log.String("prayer_day", fmt.Sprintf("%+v", prayerDay)), + log.String("now", now.String()), + ) + return nil } text := h.lp.GetText(chat.LanguageCode) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 7570a59..caa517f 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -237,13 +237,15 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C Today: &domain.ReminderConfig{LastAt: now}, Soon: &domain.ReminderConfig{LastAt: now}, Arrive: &domain.ReminderConfig{LastAt: now}, - JamaatDelay: &domain.JamaatDelay{ - Fajr: 10 * time.Minute, - Shuruq: 10 * time.Minute, - Dhuhr: 10 * time.Minute, - Asr: 10 * time.Minute, - Maghrib: 10 * time.Minute, - Isha: 20 * time.Minute, + Jamaat: &domain.JamaatConfig{ + Delay: &domain.JamaatDelayConfig{ + Fajr: 10 * time.Minute, + Shuruq: 10 * time.Minute, + Dhuhr: 10 * time.Minute, + Asr: 10 * time.Minute, + Maghrib: 10 * time.Minute, + Isha: 20 * time.Minute, + }, }, } diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 68837ff..3a8813c 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -49,15 +49,13 @@ func (h *Handler) formatPrayerDay(botID int64, date *domain.PrayerDay, languageC // now returns the current time with seconds and nanoseconds set to 0 func (h *Handler) now(botID int64) time.Time { - t := time.Now().In(h.cfg[botID].Location.V()) - return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()) + return time.Now().In(h.cfg[botID].Location.V()).Truncate(time.Minute) } // nowUTC returns the current time in UTC with seconds and nanoseconds set to 0 // Use this function to get prayerDay or current year for a specific botID timezone. func (h *Handler) nowUTC(botID int64) time.Time { - t := time.Now().In(h.cfg[botID].Location.V()) - return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, time.UTC) + return time.Now().In(h.cfg[botID].Location.V()).Truncate(time.Minute).UTC() } // daysInMonth returns the number of days in a month. diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 4cf2e97..42e910f 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -316,18 +316,23 @@ func (db *DB) SetState(ctx context.Context, botID int64, chatID int64, state str } func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (prayerDay *domain.PrayerDay, _ error) { + nextDate := date.Add(24 * time.Hour) + query := ` DECLARE $bot_id AS Int64; DECLARE $date AS Date; + DECLARE $next_date AS Date; SELECT prayer_date, fajr, shuruq, dhuhr, asr, maghrib, isha FROM prayers - WHERE bot_id = $bot_id AND prayer_date = $date; + WHERE bot_id = $bot_id AND prayer_date IN ($date, $next_date) + ORDER BY prayer_date; ` params := table.NewQueryParameters( table.ValueParam("$bot_id", types.Int64Value(botID)), table.ValueParam("$date", types.DateValueFromTime(date)), + table.ValueParam("$next_date", types.DateValueFromTime(nextDate)), ) err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { @@ -337,17 +342,38 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr } defer func(res result.Result) { _ = res.Close() }(res) - if res.NextResultSet(ctx) && res.NextRow() { - prayerDay = &domain.PrayerDay{} - err = res.ScanWithDefaults( - &prayerDay.Date, - &prayerDay.Fajr, &prayerDay.Shuruq, - &prayerDay.Dhuhr, &prayerDay.Asr, - &prayerDay.Maghrib, &prayerDay.Isha, - ) - if err != nil { - return err + if res.NextResultSet(ctx) { + if res.NextRow() { + prayerDay = &domain.PrayerDay{} + err = res.ScanWithDefaults( + &prayerDay.Date, + &prayerDay.Fajr, &prayerDay.Shuruq, + &prayerDay.Dhuhr, &prayerDay.Asr, + &prayerDay.Maghrib, &prayerDay.Isha, + ) + if err != nil { + return err + } + } else { + return domain.ErrNotFound + } + + if res.NextRow() { + nextDay := &domain.PrayerDay{} + err = res.ScanWithDefaults( + &nextDay.Date, + &nextDay.Fajr, &nextDay.Shuruq, + &nextDay.Dhuhr, &nextDay.Asr, + &nextDay.Maghrib, &nextDay.Isha, + ) + if err != nil { + return err + } + prayerDay.NextDay = nextDay + } else { + return domain.ErrNotFound // no next day found (cannot happen) } + return nil } diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index bbf7bdc..a5d9dd3 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 + github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 28e92f8..ea9382b 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -579,6 +579,14 @@ github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpL github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 726800f..73ca6be 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 + github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index b0f88c1..c9552c2 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -579,6 +579,14 @@ github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpL github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= +github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= +github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= +github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= +github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/handler.go b/serverless/reminder/internal/handler/handler.go index d33f182..8ef0b67 100644 --- a/serverless/reminder/internal/handler/handler.go +++ b/serverless/reminder/internal/handler/handler.go @@ -3,14 +3,12 @@ package handler import ( "context" "fmt" - "strings" "sync" "time" "github.com/escalopa/prayer-bot/domain" "github.com/escalopa/prayer-bot/log" "github.com/go-telegram/bot" - "github.com/go-telegram/bot/models" "golang.org/x/sync/errgroup" ) @@ -90,31 +88,27 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { return nil } - // Get chat details chats, err := h.db.GetChatsByIDs(ctx, botID, chatIDs) if err != nil { log.Error("get chats", log.Err(err), log.BotID(botID)) return domain.ErrInternal } - // Get bot b, err := h.getBot(botID) if err != nil { log.Error("get bot", log.Err(err), log.BotID(botID)) return domain.ErrInternal } - // Get current day prayer times cfg := h.cfg[botID] - now := time.Now().In(cfg.Location.V()) - date := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) + now := h.now(cfg.Location.V()) + date := now.Truncate(24 * time.Hour) prayerDay, err := h.db.GetPrayerDay(ctx, botID, date) if err != nil { log.Error("get prayer day", log.Err(err), log.BotID(botID)) return domain.ErrInternal } - // Initialize reminder types reminders := []ReminderType{ &TodayReminder{ lp: h.lp, @@ -123,30 +117,25 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { }, &SoonReminder{lp: h.lp}, &ArriveReminder{lp: h.lp}, - &JamaatReminder{lp: h.lp}, } - // Process each chat errG := &errgroup.Group{} for _, chat := range chats { chat := chat errG.Go(func() error { - // Skip chats without reminder config - if chat.Reminder == nil { + if chat.Reminder == nil { // cannot happen but just in case return nil } - // Check each reminder type for _, reminder := range reminders { shouldSend, prayerID := reminder.Check(ctx, chat, prayerDay, now) if !shouldSend { continue } - // Send reminder - err := reminder.Send(ctx, b, chat, prayerID, prayerDay) + messageID, err := reminder.Send(ctx, b, chat, prayerID, prayerDay) if err != nil { - if h.isBlockedErr(err) { + if isBlockedErr(err) { h.deleteChat(ctx, chat) return nil } @@ -154,32 +143,19 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID), - log.String("reminder_type", reminder.Name()), + log.String("reminder_type", reminder.Name().String()), ) continue } - // Update state - var messageID int - switch reminder.Name() { - case "today": - messageID = chat.Reminder.Today.MessageID - case "soon": - messageID = chat.Reminder.Soon.MessageID - case "arrive": - messageID = chat.Reminder.Arrive.MessageID - case "jamaat": - // Jamaat is stateless, no message ID to track - messageID = 0 - } - - err = reminder.UpdateState(ctx, h.db, chat, messageID, now) + now = now.Add(1 * time.Minute) // increment by 1 minute to avoid sending the same reminder multiple times + err = h.db.UpdateReminder(ctx, chat.BotID, chat.ChatID, reminder.Name(), messageID, now) if err != nil { log.Error("update reminder state", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID), - log.String("reminder_type", reminder.Name()), + log.String("reminder_type", reminder.Name().String()), ) } } @@ -190,181 +166,3 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { _ = errG.Wait() return nil } - -func (h *Handler) remindUser( - ctx context.Context, - b *bot.Bot, - chat *domain.Chat, - prayerID domain.PrayerID, - reminderOffset int32, -) error { - var ( - text = h.lp.GetText(chat.LanguageCode) - duration = time.Duration(reminderOffset) * time.Minute - prayer, message = text.Prayer[int(prayerID)], "" - ) - - switch duration { - case 0: - message = fmt.Sprintf(text.PrayerArrived, prayer) - default: - message = fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)) - } - - params := &bot.SendMessageParams{ - ChatID: chat.ChatID, - Text: message, - ParseMode: models.ParseModeMarkdown, - } - - res, err := b.SendMessage(ctx, params) - - if err != nil { - if h.isBlockedErr(err) { - h.deleteChat(ctx, chat) - return nil - } - log.Error("remindUser: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - err = h.db.SetReminderMessageID(ctx, chat.BotID, chat.ChatID, int32(res.ID)) - if err != nil { - log.Error("remindUser: set remind_message_id", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - h.deleteMessages(ctx, b, chat, chat.ReminderMessageID, chat.JamaatMessageID) - return nil -} - -func (h *Handler) remindUserJamaat( - ctx context.Context, - b *bot.Bot, - chat *domain.Chat, - prayerID domain.PrayerID, - reminderOffset int32, -) error { - var ( - text = h.lp.GetText(chat.LanguageCode) - duration = time.Duration(reminderOffset) * time.Minute - prayer, message = text.Prayer[int(prayerID)], "" - hasArrived = false - ) - - switch duration { - case 0: - hasArrived = true - message = fmt.Sprintf(text.PrayerArrived, prayer) - default: - message = fmt.Sprintf("%s\n%s", - fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)), - fmt.Sprintf(text.PrayerJamaat, domain.FormatDuration(duration+jamaatDelay)), - ) - } - - var ( - res *models.Message - err error - ) - - if hasArrived { - res, err = b.SendMessage(ctx, &bot.SendMessageParams{ - ChatID: chat.ChatID, - Text: message, - ParseMode: models.ParseModeMarkdown, - ReplyParameters: &models.ReplyParameters{ - MessageID: int(chat.JamaatMessageID), - ChatID: chat.ChatID, - AllowSendingWithoutReply: true, - }, - }) - } else { - isAnonymous := false - message = strings.ReplaceAll(message, "*", "") // remove markdown syntax cuz doesn't work on poll - res, err = b.SendPoll(ctx, &bot.SendPollParams{ - ChatID: chat.ChatID, - Question: message, - Options: []models.InputPollOption{ - { - Text: text.PrayerJoin, - TextParseMode: models.ParseModeMarkdown, - }, - { - Text: text.PrayerJoinDelay, - TextParseMode: models.ParseModeMarkdown, - }, - }, - IsAnonymous: &isAnonymous, - }) - } - - if err != nil { - if h.isBlockedErr(err) { - h.deleteChat(ctx, chat) - return nil - } - log.Error("remindUserJamaat: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - if hasArrived { - err = h.db.SetReminderMessageID(ctx, chat.BotID, chat.ChatID, int32(res.ID)) - if err != nil { - log.Error("remindUserJamaat: set remind_message_id", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - h.deleteMessages(ctx, b, chat, chat.ReminderMessageID) // prevent duplicates - return nil // no need to continue further - } - - err = h.db.SetJamaatMessageID(ctx, chat.BotID, chat.ChatID, int32(res.ID)) - if err != nil { - log.Error("remindUserJamaat: set jamaat_message_id", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - h.deleteMessages(ctx, b, chat, chat.ReminderMessageID, chat.JamaatMessageID) - return nil -} -// formatPrayerDay formats the domain.PrayerDay into a string (copied from dispatcher service) - -func (h *Handler) deleteChat(ctx context.Context, chat *domain.Chat) { - err := h.db.DeleteChat(ctx, chat.BotID, chat.ChatID) - if err != nil { - log.Error("remindUserJamaat: delete chat", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return - } - log.Warn("remindUserJamaat: deleted chat", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) -} - -func (h *Handler) deleteMessages(ctx context.Context, b *bot.Bot, chat *domain.Chat, ids ...int32) { - messageIDs := make([]int, 0, len(ids)) - for _, id := range ids { - if id == 0 { - continue - } - messageIDs = append(messageIDs, int(id)) - } - - if len(messageIDs) == 0 { - return // nothing to do - } - - _, err := b.DeleteMessages(ctx, &bot.DeleteMessagesParams{ - ChatID: chat.ChatID, - MessageIDs: messageIDs, - }) - if err != nil { - log.Error("delete messages", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID), "ids", ids) - } -} - -func (h *Handler) isBlockedErr(err error) bool { - return strings.HasPrefix(err.Error(), bot.ErrorForbidden.Error()) -} - -func (h *Handler) now(loc *time.Location) time.Time { - now := time.Now().In(loc) - return time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, loc) -} diff --git a/serverless/reminder/internal/handler/helper.go b/serverless/reminder/internal/handler/helper.go index b652e93..a7a6317 100644 --- a/serverless/reminder/internal/handler/helper.go +++ b/serverless/reminder/internal/handler/helper.go @@ -1,9 +1,14 @@ package handler import ( + "context" "fmt" + "strings" + "time" "github.com/escalopa/prayer-bot/domain" + "github.com/escalopa/prayer-bot/log" + "github.com/go-telegram/bot" ) const ( @@ -22,6 +27,10 @@ const ( ` ) +func (h *Handler) now(loc *time.Location) time.Time { + return time.Now().In(loc).Truncate(time.Minute) +} + func (h *Handler) formatPrayerDay(botID int64, prayerDay *domain.PrayerDay, languageCode string) string { loc := h.cfg[botID].Location.V() text := h.lp.GetText(languageCode) @@ -36,3 +45,38 @@ func (h *Handler) formatPrayerDay(botID int64, prayerDay *domain.PrayerDay, lang text.Prayer[int(domain.PrayerIDIsha)], prayerDay.Isha.In(loc).Format(prayerTimeFormat), ) } + +func (h *Handler) deleteChat(ctx context.Context, chat *domain.Chat) { + err := h.db.DeleteChat(ctx, chat.BotID, chat.ChatID) + if err != nil { + log.Error("remindUserJamaat: delete chat", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return + } + log.Warn("remindUserJamaat: deleted chat", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) +} + +func deleteMessages(ctx context.Context, b *bot.Bot, chat *domain.Chat, ids ...int) { + messageIDs := make([]int, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + messageIDs = append(messageIDs, id) + } + + if len(messageIDs) == 0 { + return // nothing to do + } + + _, err := b.DeleteMessages(ctx, &bot.DeleteMessagesParams{ + ChatID: chat.ChatID, + MessageIDs: messageIDs, + }) + if err != nil { + log.Error("delete messages", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID), "ids", ids) + } +} + +func isBlockedErr(err error) bool { + return strings.HasPrefix(err.Error(), bot.ErrorForbidden.Error()) +} diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index a33904b..db5931d 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -3,11 +3,9 @@ package handler import ( "context" "fmt" - "strings" "time" "github.com/escalopa/prayer-bot/domain" - "github.com/escalopa/prayer-bot/log" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" ) @@ -15,76 +13,44 @@ import ( type ReminderType interface { Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (messageID int, err error) - Name() string + Name() domain.ReminderType } // TodayReminder - Daily schedule type TodayReminder struct { - lp *languagesProvider - botConfig map[int64]*domain.BotConfig + lp *languagesProvider + botConfig map[int64]*domain.BotConfig formatPrayerDay func(botID int64, prayerDay *domain.PrayerDay, languageCode string) string } func (r *TodayReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { - if chat.Reminder == nil || chat.Reminder.Today.Offset == 0 { - return false, 0 // Disabled - } - config := chat.Reminder.Today // Trigger logic: last_at + 24h - offset < now lastDate := config.LastAt.Truncate(24 * time.Hour) triggerTime := lastDate.Add(24 * time.Hour).Add(-config.Offset) - return triggerTime.Before(now) || triggerTime.Equal(now), 0 // Not prayer-specific + return triggerTime.Before(now) || triggerTime.Equal(now), domain.PrayerIDUnknown } -func (r *TodayReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { - // Format using the same logic as dispatcher service - text := r.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode) - - // Delete old message if exists - if chat.Reminder.Today.MessageID != 0 { - _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ - ChatID: chat.ChatID, - MessageID: chat.Reminder.Today.MessageID, - }) - } - - // Send new message +func (r *TodayReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { + deleteMessages(ctx, b, chat, chat.Reminder.Today.MessageID) res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, - Text: text, + Text: r.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode), }) if err != nil { - return err + return 0, err } - // Store message ID for later cleanup - chat.Reminder.Today.MessageID = res.ID - return nil + return res.ID, nil } -func (r *TodayReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { - lastAt := now.Truncate(24 * time.Hour) // Date only - chat.Reminder.Today.MessageID = messageID - chat.Reminder.Today.LastAt = lastAt - return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeToday, messageID, lastAt) -} - -func (r *TodayReminder) Name() string { return "today" } +func (r *TodayReminder) Name() domain.ReminderType { return domain.ReminderTypeToday } -// SoonReminder - "Prayer coming soon" type SoonReminder struct { lp *languagesProvider } func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { - if chat.Reminder == nil || chat.Reminder.Soon.Offset == 0 { - return false, 0 // Disabled - } - - config := chat.Reminder.Soon - - // Check all prayers prayers := []struct { id domain.PrayerID time time.Time @@ -97,53 +63,78 @@ func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay * {domain.PrayerIDIsha, prayerDay.Isha}, } + // Add next day's prayers if available + if prayerDay.NextDay != nil { + prayers = append(prayers, + struct { + id domain.PrayerID + time time.Time + }{domain.PrayerIDFajr, prayerDay.NextDay.Fajr}, + struct { + id domain.PrayerID + time time.Time + }{domain.PrayerIDShuruq, prayerDay.NextDay.Shuruq}, + ) + } + + config := chat.Reminder.Soon for _, p := range prayers { - // Trigger logic: last_at < prayer_time - offset AND prayer_time - offset <= now - triggerTime := p.time.Add(-config.Offset) - if config.LastAt.Before(triggerTime) && - (triggerTime.Before(now) || triggerTime.Equal(now)) { + // logic: last_at < (prayer_time - offset) AND (prayer_time - offset) <= now + trigger := p.time.Add(-config.Offset) + if config.LastAt.Before(trigger) && (trigger.Before(now) || trigger.Equal(now)) { return true, p.id } } return false, 0 } -func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { +func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { text := r.lp.GetText(chat.LanguageCode) - duration := chat.Reminder.Soon.Offset prayer := text.Prayer[int(prayerID)] - message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)) - // Delete old message - if chat.Reminder.Soon.MessageID != 0 { - _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ - ChatID: chat.ChatID, - MessageID: chat.Reminder.Soon.MessageID, + deleteMessages(ctx, b, chat, chat.Reminder.Soon.MessageID, chat.Reminder.Arrive.MessageID) + + if chat.IsGroup && chat.Reminder.Jamaat.Enabled { + delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) + message := fmt.Sprintf("%s\n%s", + fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), + fmt.Sprintf(text.PrayerJamaat, domain.FormatDuration(delay)), + ) + isAnonymous := false + + res, err := b.SendPoll(ctx, &bot.SendPollParams{ + ChatID: chat.ChatID, + Question: message, + Options: []models.InputPollOption{ + {Text: text.PrayerJoin, TextParseMode: models.ParseModeMarkdown}, + {Text: text.PrayerJoinDelay, TextParseMode: models.ParseModeMarkdown}, + }, + IsAnonymous: &isAnonymous, }) + if err != nil { + return 0, err + } + + return res.ID, nil } + duration := chat.Reminder.Soon.Offset + message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)) + res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, Text: message, ParseMode: models.ParseModeMarkdown, }) if err != nil { - return err + return 0, err } - chat.Reminder.Soon.MessageID = res.ID - return nil + return res.ID, nil } -func (r *SoonReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { - chat.Reminder.Soon.MessageID = messageID - chat.Reminder.Soon.LastAt = now - return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeSoon, messageID, now) -} - -func (r *SoonReminder) Name() string { return "soon" } +func (r *SoonReminder) Name() domain.ReminderType { return domain.ReminderTypeSoon } -// ArriveReminder - "Prayer time arrived" type ArriveReminder struct { lp *languagesProvider } @@ -154,7 +145,6 @@ func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay } config := chat.Reminder.Arrive - // Offset not used (always 0), check if any prayer time has arrived prayers := []struct { id domain.PrayerID @@ -169,7 +159,7 @@ func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay } for _, p := range prayers { - // Trigger logic: last_at < prayer_time AND prayer_time <= now + // logic: last_at < prayer_time AND prayer_time <= now if config.LastAt.Before(p.time) && (p.time.Before(now) || p.time.Equal(now)) { return true, p.id @@ -178,150 +168,30 @@ func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay return false, 0 } -func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { +func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { + deleteMessages(ctx, b, chat, chat.Reminder.Arrive.MessageID) + text := r.lp.GetText(chat.LanguageCode) prayer := text.Prayer[int(prayerID)] message := fmt.Sprintf(text.PrayerArrived, prayer) - // Delete old message - if chat.Reminder.Arrive.MessageID != 0 { - _, _ = b.DeleteMessage(ctx, &bot.DeleteMessageParams{ - ChatID: chat.ChatID, - MessageID: chat.Reminder.Arrive.MessageID, - }) - } - - res, err := b.SendMessage(ctx, &bot.SendMessageParams{ + params := &bot.SendMessageParams{ ChatID: chat.ChatID, Text: message, ParseMode: models.ParseModeMarkdown, - }) - if err != nil { - return err - } - - chat.Reminder.Arrive.MessageID = res.ID - return nil -} - -func (r *ArriveReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { - chat.Reminder.Arrive.MessageID = messageID - chat.Reminder.Arrive.LastAt = now - return db.UpdateReminder(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeArrive, messageID, now) -} - -func (r *ArriveReminder) Name() string { return "arrive" } - -// JamaatReminder - Group Jamaat polls -type JamaatReminder struct { - lp *languagesProvider -} - -func (r *JamaatReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { - if !chat.IsGroup || chat.Reminder == nil { - return false, 0 // Only for group chats - } - - delays := chat.Reminder.JamaatDelay // Read as-is - - prayers := []struct { - id domain.PrayerID - time time.Time - }{ - {domain.PrayerIDFajr, prayerDay.Fajr}, - // Skip Shuruq (no Jamaat for sunrise) - {domain.PrayerIDDhuhr, prayerDay.Dhuhr}, - {domain.PrayerIDAsr, prayerDay.Asr}, - {domain.PrayerIDMaghrib, prayerDay.Maghrib}, - {domain.PrayerIDIsha, prayerDay.Isha}, - } - - // Track state per prayer using a simple in-memory approach or check based on lastAt - // Since we removed the Jamaat ReminderConfig, we'll use a simpler approach: - // Check if the Jamaat time (prayer_time + delay) has arrived - for _, p := range prayers { - delay := delays.GetDelayByPrayerID(p.id) - if delay == 0 { - continue // Disabled for this prayer - } - - // Trigger logic: prayer_time + delay <= now - // We'll check if jamaat time has arrived - jamaatTime := p.time.Add(delay) - if jamaatTime.Before(now) || jamaatTime.Equal(now) { - return true, p.id - } + ReplyParameters: &models.ReplyParameters{ + ChatID: chat.ChatID, + MessageID: chat.Reminder.Soon.MessageID, // reply to soon's message + AllowSendingWithoutReply: true, // allow sending without reply in case of soon's message is deleted + }, } - return false, 0 -} - -func (r *JamaatReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) error { - text := r.lp.GetText(chat.LanguageCode) - prayer := text.Prayer[int(prayerID)] - - delay := chat.Reminder.JamaatDelay.GetDelayByPrayerID(prayerID) - prayerTime := getPrayerTimeByID(prayerDay, prayerID) - jamaatTime := prayerTime.Add(delay) - - // Check if Jamaat time has arrived - now := time.Now() - if now.After(jamaatTime) || now.Equal(jamaatTime) { - // Jamaat has started - send arrival message - message := fmt.Sprintf(text.PrayerArrived, prayer) - _, err := b.SendMessage(ctx, &bot.SendMessageParams{ - ChatID: chat.ChatID, - Text: message, - ParseMode: models.ParseModeMarkdown, - }) - if err != nil { - return err - } - } else { - // Send poll for upcoming Jamaat - message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(delay)) - message = strings.ReplaceAll(message, "*", "") // Remove markdown for poll - isAnonymous := false - _, err := b.SendPoll(ctx, &bot.SendPollParams{ - ChatID: chat.ChatID, - Question: message, - Options: []models.InputPollOption{ - {Text: text.PrayerJoin, TextParseMode: models.ParseModeMarkdown}, - {Text: text.PrayerJoinDelay, TextParseMode: models.ParseModeMarkdown}, - }, - IsAnonymous: &isAnonymous, - }) - if err != nil { - return err - } + res, err := b.SendMessage(ctx, params) + if err != nil { + return 0, err } - return nil -} - -func (r *JamaatReminder) UpdateState(ctx context.Context, db DB, chat *domain.Chat, messageID int, now time.Time) error { - // Jamaat reminders are stateless - no state to update - return nil + return res.ID, nil } -func (r *JamaatReminder) Name() string { return "jamaat" } - -// Helper function to get prayer time by ID -func getPrayerTimeByID(prayerDay *domain.PrayerDay, prayerID domain.PrayerID) time.Time { - switch prayerID { - case domain.PrayerIDFajr: - return prayerDay.Fajr - case domain.PrayerIDShuruq: - return prayerDay.Shuruq - case domain.PrayerIDDhuhr: - return prayerDay.Dhuhr - case domain.PrayerIDAsr: - return prayerDay.Asr - case domain.PrayerIDMaghrib: - return prayerDay.Maghrib - case domain.PrayerIDIsha: - return prayerDay.Isha - default: - return time.Time{} - } -} +func (r *ArriveReminder) Name() domain.ReminderType { return domain.ReminderTypeArrive } diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index 6f23838..aea28ed 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -149,18 +149,23 @@ func (db *DB) GetSubscribers(ctx context.Context, botID int64) (chatIDs []int64, } func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (prayerDay *domain.PrayerDay, _ error) { + nextDate := date.Add(24 * time.Hour) + query := ` DECLARE $bot_id AS Int64; DECLARE $date AS Date; + DECLARE $next_date AS Date; SELECT prayer_date, fajr, shuruq, dhuhr, asr, maghrib, isha FROM prayers - WHERE bot_id = $bot_id AND prayer_date = $date; + WHERE bot_id = $bot_id AND prayer_date IN ($date, $next_date) + ORDER BY prayer_date; ` params := table.NewQueryParameters( table.ValueParam("$bot_id", types.Int64Value(botID)), table.ValueParam("$date", types.DateValueFromTime(date)), + table.ValueParam("$next_date", types.DateValueFromTime(nextDate)), ) err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { @@ -170,17 +175,38 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr } defer func(res result.Result) { _ = res.Close() }(res) - if res.NextResultSet(ctx) && res.NextRow() { - prayerDay = &domain.PrayerDay{} - err = res.ScanWithDefaults( - &prayerDay.Date, - &prayerDay.Fajr, &prayerDay.Shuruq, - &prayerDay.Dhuhr, &prayerDay.Asr, - &prayerDay.Maghrib, &prayerDay.Isha, - ) - if err != nil { - return err + if res.NextResultSet(ctx) { + if res.NextRow() { + prayerDay = &domain.PrayerDay{} + err = res.ScanWithDefaults( + &prayerDay.Date, + &prayerDay.Fajr, &prayerDay.Shuruq, + &prayerDay.Dhuhr, &prayerDay.Asr, + &prayerDay.Maghrib, &prayerDay.Isha, + ) + if err != nil { + return err + } + } else { + return domain.ErrNotFound } + + if res.NextRow() { + nextDay := &domain.PrayerDay{} + err = res.ScanWithDefaults( + &nextDay.Date, + &nextDay.Fajr, &nextDay.Shuruq, + &nextDay.Dhuhr, &nextDay.Asr, + &nextDay.Maghrib, &nextDay.Isha, + ) + if err != nil { + return err + } + prayerDay.NextDay = nextDay + } else { + return domain.ErrNotFound // no next day found (cannot happen) + } + return nil } From 550e0761105f3d136acaa4e1e36563b54b7d2bbb Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 18:18:34 +0300 Subject: [PATCH 27/59] feat: update main.tf --- .gitignore | 1 + README.md | 50 ++++---------------------------------------------- main.tf | 2 +- 3 files changed, 6 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 79acaa1..4550f78 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ _config # secret iam.json key.json +config.json diff --git a/README.md b/README.md index 4242db6..f5c4385 100644 --- a/README.md +++ b/README.md @@ -183,51 +183,9 @@ terraform show -json plan.out > plan.json docker run --rm -it -p 9000:9000 -v $(pwd)/plan.json:/src/plan.json im2nguyen/rover:latest -planJSONPath=plan.json ``` -Hello! +## Useful -I want to change how I work with reminders, before I have reminder offest and reminder message id, now I have changed the type in db into json and will use the following Reminder object - -```go -type ReminderConfig struct { - Offest time.Duration `json:"offset"` - MessageID int `json:"message_id"` - LastAt time.Time `json:"last_at"` -} - -type JamaatOffset struct { - Fajr time.Duration `json:"fajr"` // default 10m - Shuruq time.Duration `json:"shuruq"` // default 10m - Dhuhr time.Duration `json:"dhuhr"` // default 10m - Asr time.Duration `json:"asr"` // default 10m - Maghrib time.Duration `json:"maghrib"` // default 10m - Isha time.Duration `json:"isha"` // default 20m -} - -type Reminder struct { - // last_at is a date without hours, if last_at + 24h - offset is less than now then trigger - Today ReminderConfig `json:"today"` - // if last_at < prayer_time-offset, then trigger - Soon ReminderConfig `json:"soon"` - // if last_at < prayer_time+offset then trigger - Arrive ReminderConfig `json:"arrive"` // offset here not used - // Use to set the value in Jamaat after X min - GroupJamaatOffset JamaatOffset `json:"group_jamaa_offset"` -} +```bash +export AWS_ACCESS_KEY_ID=$(jq -r '.aws_access_key_id' key.json) +export AWS_SECRET_ACCESS_KEY=$(jq -r '.aws_secret_access_key' key.json) ``` - -Now, to now that I need to send a message, event time + offset(might be negative) must be after last_at and before or equal now - -Update the dispatcher code when setting reminder, the value will be set on soon.offset - -add a today handler in reminder to send today's prayer message and delete previous one if any (using message_id) - -Refactor the code in reminder so that we have checks for each reminder (today, soon, arrive), if check passed, send message and delete previous message_id and update last_at and message_id - ---- - -I need you to improve this prompt instead of making changes - - - -- update code to match new infra -- delpoy to production diff --git a/main.tf b/main.tf index 4e46993..e078c19 100644 --- a/main.tf +++ b/main.tf @@ -235,7 +235,7 @@ resource "yandex_function" "dispatcher_fn" { } } -output "dispatcher_fn_id" { +output "dispatcher_function_id" { value = yandex_function.dispatcher_fn.id // used to set Webhook URL on Telegram } From b694d81247451f80f604e4acfaa6636a2aeccb6b Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 18:45:07 +0300 Subject: [PATCH 28/59] feat: update config --- .github/actions/load-config/action.yaml | 11 +++++++---- .github/actions/setup-terraform/action.yaml | 17 ++++++++++------- .github/workflows/deploy.yaml | 12 ++++++------ config/config.go | 11 +++++++++-- main.tf | 9 +++++---- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/.github/actions/load-config/action.yaml b/.github/actions/load-config/action.yaml index b70e765..dd9b0e8 100644 --- a/.github/actions/load-config/action.yaml +++ b/.github/actions/load-config/action.yaml @@ -21,14 +21,17 @@ runs: echo "::error::APP_CONFIG is empty" exit 1 fi - + + # Mask the config value to prevent it from appearing in logs + echo "::add-mask::${{ inputs.app_config }}" + # Write the config and validate it's valid JSON echo '${{ inputs.app_config }}' > "${{ inputs.app_config_path }}" - + # Validate JSON format if ! jq empty "${{ inputs.app_config_path }}" 2>/dev/null; then echo "::error::APP_CONFIG is not valid JSON" exit 1 fi - - echo "::notice::Application config written to: ${{ inputs.app_config_path }}" + + echo "::notice::Application config file created successfully" diff --git a/.github/actions/setup-terraform/action.yaml b/.github/actions/setup-terraform/action.yaml index c68251f..e973368 100644 --- a/.github/actions/setup-terraform/action.yaml +++ b/.github/actions/setup-terraform/action.yaml @@ -59,16 +59,16 @@ runs: echo "::error::SERVICE_ACCOUNT_KEY is empty" exit 1 fi - + # Write the key and validate it's valid JSON echo '${{ inputs.service_account_key }}' > "${{ inputs.service_account_key_path }}" - + # Validate JSON format if ! jq empty "${{ inputs.service_account_key_path }}" 2>/dev/null; then echo "::error::SERVICE_ACCOUNT_KEY is not valid JSON" exit 1 fi - + echo "::notice::Service account key file created and validated" - name: Write config.json @@ -78,16 +78,19 @@ runs: echo "::error::APP_CONFIG is empty" exit 1 fi - + + # Mask the config value to prevent it from appearing in logs + echo "::add-mask::${{ inputs.app_config }}" + # Write the config and validate it's valid JSON echo '${{ inputs.app_config }}' > "${{ inputs.app_config_path }}" - + # Validate JSON format if ! jq empty "${{ inputs.app_config_path }}" 2>/dev/null; then echo "::error::APP_CONFIG is not valid JSON" exit 1 fi - + echo "::notice::Application config written to: ${{ inputs.app_config_path }}" - name: Cache Terraform plugins @@ -117,4 +120,4 @@ runs: shell: bash run: | terraform workspace select ${{ inputs.workspace }} || terraform workspace new ${{ inputs.workspace }} - echo "::notice::Terraform workspace: ${{ inputs.workspace }}" \ No newline at end of file + echo "::notice::Terraform workspace: ${{ inputs.workspace }}" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c0d1512..4ec9f33 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -213,10 +213,10 @@ jobs: run: | terraform plan -out=tfplan -detailed-exitcode || EXIT_CODE=$? echo "exitcode=$EXIT_CODE" >> $GITHUB_OUTPUT - + # Print the plan output terraform show tfplan - + if [ $EXIT_CODE -eq 2 ]; then echo "::notice::Changes detected in Terraform plan" elif [ $EXIT_CODE -eq 0 ]; then @@ -318,7 +318,7 @@ jobs: run: | echo "::notice::Config file: ${{ env.APP_CONFIG_PATH }}" echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" - + chmod +x _scripts/hook.sh bash _scripts/hook.sh continue-on-error: false @@ -355,11 +355,11 @@ jobs: echo "**Workspace:** $WORKSPACE" >> $GITHUB_STEP_SUMMARY echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "**Cache Key:** ${{ needs.setup.outputs.cache_key }}" >> $GITHUB_STEP_SUMMARY - + if [ "${{ needs.apply.outputs.dispatcher_function_id }}" != "" ]; then echo "**Dispatcher Function ID:** ${{ needs.apply.outputs.dispatcher_function_id }}" >> $GITHUB_STEP_SUMMARY fi - + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "**Manual Trigger:** Yes" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + fi diff --git a/config/config.go b/config/config.go index 55e78dc..f017f13 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -9,9 +10,15 @@ import ( ) func Load() (botConfig map[int64]*domain.BotConfig, _ error) { - data := os.Getenv("APP_CONFIG") + encodedData := os.Getenv("APP_CONFIG") - err := json.Unmarshal([]byte(data), &botConfig) + // Decode base64-encoded config + data, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + return nil, fmt.Errorf("decode base64 config: %v", err) + } + + err = json.Unmarshal(data, &botConfig) if err != nil { return nil, fmt.Errorf("unmarshal bot config: %v", err) } diff --git a/main.tf b/main.tf index e078c19..1850f41 100644 --- a/main.tf +++ b/main.tf @@ -27,7 +27,8 @@ terraform { } locals { - env = terraform.workspace + env = terraform.workspace + app_config_base64 = sensitive(base64encode(file("${path.module}/config.json"))) } variable "cloud_id" { @@ -144,7 +145,7 @@ resource "yandex_function" "loader_fn" { user_hash = filemd5(data.archive_file.loader_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/config.json") + APP_CONFIG = local.app_config_base64 S3_ENDPOINT = "https://storage.yandexcloud.net" YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint @@ -221,7 +222,7 @@ resource "yandex_function" "dispatcher_fn" { user_hash = filemd5(data.archive_file.dispatcher_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/config.json") + APP_CONFIG = local.app_config_base64 YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint @@ -278,7 +279,7 @@ resource "yandex_function" "reminder_fn" { user_hash = filemd5(data.archive_file.reminder_zip.output_path) environment = { - APP_CONFIG = file("${path.module}/config.json") + APP_CONFIG = local.app_config_base64 YDB_ENDPOINT = yandex_ydb_database_serverless.ydb.ydb_full_endpoint From 6034664856a3cb702c60f3ed828c5c88d8a1f2ed Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 18:54:39 +0300 Subject: [PATCH 29/59] feat: update go.mod --- .github/workflows/deploy.yaml | 1 + serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 ++ serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 ++ serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 ++ 7 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 4ec9f33..4698881 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -278,6 +278,7 @@ jobs: TF_VAR_cloud_id: ${{ secrets.CLOUD_ID }} TF_VAR_folder_id: ${{ secrets.FOLDER_ID }} run: terraform apply -auto-approve + continue-on-error: false - name: Export Terraform Outputs id: outputs diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 0e27c92..6dc6dee 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 + github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 9ffe8cc..9ab4285 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -585,6 +585,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtm github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index a5d9dd3..26ca32e 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 + github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index ea9382b..ab2a21d 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -587,6 +587,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtm github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 73ca6be..b00bb3e 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 + github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index c9552c2..f495313 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -587,6 +587,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtm github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= +github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From 10cc499eed08613bf6c6f3e6e8bf82b83baba793 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 19:12:48 +0300 Subject: [PATCH 30/59] feat: update go.mod --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 20 ++------------------ serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 20 ++------------------ serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 22 ++-------------------- 6 files changed, 9 insertions(+), 59 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 6dc6dee..080a216 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f + github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 9ab4285..df4e441 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -569,24 +569,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 26ca32e..32e60e4 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f + github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index ab2a21d..bd64788 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -571,24 +571,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index b00bb3e..13e7475 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f + github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index f495313..df4e441 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -569,26 +569,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24 h1:Kl4X9Jikl6TJLx97TWGTF99aJxALsJZBIJ+E2lVvo0E= -github.com/escalopa/prayer-bot v0.0.0-20250924182821-90acd5bcfc24/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f h1:4hS8F01H8T228mBw3zGcJln3Bjgz3x1uYWYwW6h9yNI= -github.com/escalopa/prayer-bot v0.0.0-20251026095356-afb407f8729f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026100854-efcf173e5ca6 h1:jrr872AVeQ81U0l433R9fkoP7+Fys04JhURrREyo7DI= -github.com/escalopa/prayer-bot v0.0.0-20251026100854-efcf173e5ca6/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b h1:ynu+WqW1hpLtvQ2aR2gPvsDKtH4MRGSzDb4vgXSa1oM= -github.com/escalopa/prayer-bot v0.0.0-20251026121913-2891bf8b7c6b/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1 h1:8git+VqsedZgrQVEvvr7eahwqh7aiBbNjB6lHz9QtwA= -github.com/escalopa/prayer-bot v0.0.0-20251026125154-9a75dcaf13d1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077 h1:QRwPUr8J28oAJjqvOL1WwBt9KTpB/OFDBtZoaH/gRDc= -github.com/escalopa/prayer-bot v0.0.0-20251026134743-a6d630658077/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863 h1:efyckFgTyF6l9S0Y8+NC1xf0WrfB8I4djrjF71QBMns= -github.com/escalopa/prayer-bot v0.0.0-20251026142044-58d58e01a863/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915 h1:9ZEsx5crGtmXGutLahRY89wQrTgu/FuiR9NHWQWD0NM= -github.com/escalopa/prayer-bot v0.0.0-20251026143023-d4a267e72915/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616 h1:grgbusjCNuCfkkd+SK1M04V9zsUehLsH+gB8/PgQ87s= -github.com/escalopa/prayer-bot v0.0.0-20251026143228-e2dc7dc54616/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f h1:0BBakjDC7oE/napryWCpmJOY7zahq1ezwm6O+43+U2Y= -github.com/escalopa/prayer-bot v0.0.0-20251026151834-550e0761105f/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= +github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From dd9beaab66b27c286f3234ab0a8f518a97044fc5 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 19:25:28 +0300 Subject: [PATCH 31/59] feat: update json unmarshal --- serverless/dispatcher/internal/service/db.go | 8 ++++---- serverless/reminder/internal/service/db.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 42e910f..f133526 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -122,7 +122,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom defer func(res result.Result) { _ = res.Close() }(res) if res.NextResultSet(ctx) && res.NextRow() { chat = &domain.Chat{} - var reminderJSON *string + var reminderJSON string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, @@ -136,7 +136,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom } var reminder domain.Reminder - if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { return err } @@ -178,7 +178,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, if res.NextResultSet(ctx) { for res.NextRow() { chat := &domain.Chat{} - var reminderJSON *string + var reminderJSON string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, @@ -192,7 +192,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, } var reminder domain.Reminder - if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { return err } chat.Reminder = &reminder diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index aea28ed..5449ede 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -76,7 +76,7 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( if res.NextResultSet(ctx) { for res.NextRow() { chat := &domain.Chat{} - var reminderJSON *string + var reminderJSON string err = res.ScanWithDefaults( &chat.BotID, &chat.ChatID, @@ -90,7 +90,7 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( } var reminder domain.Reminder - if err := json.Unmarshal([]byte(*reminderJSON), &reminder); err != nil { + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { log.Error("unmarshal reminder json", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) return domain.ErrUnmarshalJSON } From 67bf85393bb4027a704dfa8eeeb36e5ed29507cf Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 19:46:44 +0300 Subject: [PATCH 32/59] feat: update reminder decode values --- serverless/dispatcher/internal/service/db.go | 100 ++++++++++++++---- .../reminder/internal/handler/reminder.go | 17 +-- serverless/reminder/internal/service/db.go | 95 ++++++++++++----- 3 files changed, 150 insertions(+), 62 deletions(-) diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index f133526..cd4e7fd 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -7,12 +7,12 @@ import ( "time" "github.com/escalopa/prayer-bot/domain" + "github.com/escalopa/prayer-bot/log" "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" - "github.com/ydb-platform/ydb-go-sdk/v3/table/result" - "github.com/ydb-platform/ydb-go-sdk/v3/table/types" - "github.com/ydb-platform/ydb-go-sdk/v3" "github.com/ydb-platform/ydb-go-sdk/v3/table" + "github.com/ydb-platform/ydb-go-sdk/v3/table/result" + "github.com/ydb-platform/ydb-go-sdk/v3/table/types" yc "github.com/ydb-platform/ydb-go-yc" ) @@ -137,7 +137,8 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom var reminder domain.Reminder if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { - return err + log.Error("unmarshal reminder json", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrUnmarshalJSON } chat.Reminder = &reminder @@ -193,7 +194,8 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, var reminder domain.Reminder if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { - return err + log.Error("unmarshal reminder json", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrUnmarshalJSON } chat.Reminder = &reminder @@ -262,28 +264,80 @@ func (db *DB) SetSubscribed(ctx context.Context, botID int64, chatID int64, subs } func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error { - offsetNanos := offset.Nanoseconds() + err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { + txBegin := table.TxControl(table.BeginTx(table.WithSerializableReadWrite())) - query := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - DECLARE $reminder_type AS Utf8; - DECLARE $offset AS Int64; + selectQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; - UPDATE chats - SET reminder = Json_SetField(reminder, $reminder_type || ".offset", CAST($offset AS Json)) - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` + SELECT reminder + FROM chats + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` - params := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$reminder_type", types.UTF8Value(reminderType.String())), - table.ValueParam("$offset", types.Int64Value(offsetNanos)), - ) + selectParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), + ) - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, _, err := s.Execute(ctx, writeTx, query, params) + txID, res, err := s.Execute(ctx, txBegin, selectQuery, selectParams) + if err != nil { + log.Error("execute select query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + + defer func(res result.Result) { _ = res.Close() }(res) + + var reminderJSON string + if res.NextResultSet(ctx) && res.NextRow() { + err = res.ScanWithDefaults(&reminderJSON) + if err != nil { + log.Error("scan reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + } else { + return domain.ErrNotFound + } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { + log.Error("unmarshal reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrUnmarshalJSON + } + + switch reminderType { + case domain.ReminderTypeToday: + reminder.Today.Offset = offset + case domain.ReminderTypeSoon: + reminder.Soon.Offset = offset + case domain.ReminderTypeArrive: + reminder.Arrive.Offset = offset + } + + updatedReminderJSON, err := json.Marshal(&reminder) + if err != nil { + return err + } + + updateQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + DECLARE $reminder AS Json; + + UPDATE chats + SET reminder = $reminder + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + updateParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), + table.ValueParam("$reminder", types.JSONValue(string(updatedReminderJSON))), + ) + + txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) + _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) return err }) diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index db5931d..5647a27 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -16,7 +16,6 @@ type ReminderType interface { Name() domain.ReminderType } -// TodayReminder - Daily schedule type TodayReminder struct { lp *languagesProvider botConfig map[int64]*domain.BotConfig @@ -61,20 +60,8 @@ func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay * {domain.PrayerIDAsr, prayerDay.Asr}, {domain.PrayerIDMaghrib, prayerDay.Maghrib}, {domain.PrayerIDIsha, prayerDay.Isha}, - } - - // Add next day's prayers if available - if prayerDay.NextDay != nil { - prayers = append(prayers, - struct { - id domain.PrayerID - time time.Time - }{domain.PrayerIDFajr, prayerDay.NextDay.Fajr}, - struct { - id domain.PrayerID - time time.Time - }{domain.PrayerIDShuruq, prayerDay.NextDay.Shuruq}, - ) + {domain.PrayerIDFajr, prayerDay.NextDay.Fajr}, + {domain.PrayerIDShuruq, prayerDay.NextDay.Shuruq}, } config := chat.Reminder.Soon diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index 5449ede..cf7fb59 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -231,34 +231,81 @@ func (db *DB) UpdateReminder( messageID int, lastAt time.Time, ) error { - lastAtNanos := lastAt.UnixNano() + err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { + txBegin := table.TxControl(table.BeginTx(table.WithSerializableReadWrite())) - query := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - DECLARE $reminder_type AS Utf8; - DECLARE $message_id AS Int64; - DECLARE $last_at AS Int64; - - UPDATE chats - SET reminder = Json_SetField( - Json_SetField(reminder, $reminder_type || ".message_id", CAST($message_id AS Json)), - $reminder_type || ".last_at", - CAST($last_at AS Json) + selectQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + + SELECT reminder + FROM chats + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + selectParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), ) - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` - params := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$reminder_type", types.UTF8Value(reminderType.String())), - table.ValueParam("$message_id", types.Int64Value(int64(messageID))), - table.ValueParam("$last_at", types.Int64Value(lastAtNanos)), - ) + txID, res, err := s.Execute(ctx, txBegin, selectQuery, selectParams) + if err != nil { + return err + } - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, _, err := s.Execute(ctx, writeTx, query, params) + defer func(res result.Result) { _ = res.Close() }(res) + + var reminderJSON string + if res.NextResultSet(ctx) && res.NextRow() { + err = res.ScanWithDefaults(&reminderJSON) + if err != nil { + return err + } + } else { + return domain.ErrNotFound + } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { + log.Error("unmarshal reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrUnmarshalJSON + } + + switch reminderType { + case domain.ReminderTypeToday: + reminder.Today.MessageID = messageID + reminder.Today.LastAt = lastAt + case domain.ReminderTypeSoon: + reminder.Soon.MessageID = messageID + reminder.Soon.LastAt = lastAt + case domain.ReminderTypeArrive: + reminder.Arrive.MessageID = messageID + reminder.Arrive.LastAt = lastAt + } + + updatedReminderJSON, err := json.Marshal(&reminder) + if err != nil { + return err + } + + updateQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + DECLARE $reminder AS Json; + + UPDATE chats + SET reminder = $reminder + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + updateParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), + table.ValueParam("$reminder", types.JSONValue(string(updatedReminderJSON))), + ) + + txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) + _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) return err }) From a2a691d3c343d13d92b5f14fa72936b1d8349756 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 19:50:52 +0300 Subject: [PATCH 33/59] feat: update db returned error --- serverless/dispatcher/internal/service/db.go | 69 +++++++++++++------- serverless/reminder/internal/service/db.go | 49 +++++++++----- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index cd4e7fd..969c318 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -59,7 +59,8 @@ func (db *DB) CreateChat( reminderJSON, err := json.Marshal(reminder) if err != nil { - return err + log.Error("marshal reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } query := ` @@ -92,7 +93,8 @@ func (db *DB) CreateChat( if ydb.IsOperationError(err, Ydb.StatusIds_PRECONDITION_FAILED) { // chat already exists return domain.ErrAlreadyExists } - return err + log.Error("create chat", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } return nil @@ -116,7 +118,8 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get chat query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -132,7 +135,8 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom &reminderJSON, ) if err != nil { - return err + log.Error("scan chat fields", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } var reminder domain.Reminder @@ -150,9 +154,6 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom }) if err != nil { - if ydb.IsOperationError(err, Ydb.StatusIds_NOT_FOUND) { - return nil, domain.ErrNotFound - } return nil, err } @@ -172,7 +173,8 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get chats query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -189,7 +191,8 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, &reminderJSON, ) if err != nil { - return err + log.Error("scan chat fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } var reminder domain.Reminder @@ -232,7 +235,11 @@ func (db *DB) SetLanguageCode(ctx context.Context, botID int64, chatID int64, la err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, _, err := s.Execute(ctx, writeTx, query, params) - return err + if err != nil { + log.Error("execute set language code query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err @@ -257,7 +264,11 @@ func (db *DB) SetSubscribed(ctx context.Context, botID int64, chatID int64, subs err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, _, err := s.Execute(ctx, writeTx, query, params) - return err + if err != nil { + log.Error("execute set subscribed query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err @@ -317,7 +328,8 @@ func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, updatedReminderJSON, err := json.Marshal(&reminder) if err != nil { - return err + log.Error("marshal updated reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } updateQuery := ` @@ -338,7 +350,11 @@ func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) - return err + if err != nil { + log.Error("execute update reminder query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err @@ -363,7 +379,11 @@ func (db *DB) SetState(ctx context.Context, botID int64, chatID int64, state str err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, _, err := s.Execute(ctx, writeTx, query, params) - return err + if err != nil { + log.Error("execute set state query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err @@ -392,7 +412,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get prayer day query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -406,7 +427,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr &prayerDay.Maghrib, &prayerDay.Isha, ) if err != nil { - return err + log.Error("scan prayer day fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } } else { return domain.ErrNotFound @@ -421,7 +443,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr &nextDay.Maghrib, &nextDay.Isha, ) if err != nil { - return err + log.Error("scan next prayer day fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } prayerDay.NextDay = nextDay } else { @@ -435,9 +458,6 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr }) if err != nil { - if ydb.IsOperationError(err, Ydb.StatusIds_NOT_FOUND) { - return nil, domain.ErrNotFound - } return nil, err } @@ -469,14 +489,16 @@ func (db *DB) GetStats(ctx context.Context, botID int64) (*domain.Stats, error) err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get stats query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) if res.NextResultSet(ctx) && res.NextRow() { err = res.ScanWithDefaults(&stats.Users, &stats.Subscribed, &stats.Unsubscribed) if err != nil { - return err + log.Error("scan stats fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } } @@ -489,7 +511,8 @@ func (db *DB) GetStats(ctx context.Context, botID int64) (*domain.Stats, error) err = res.ScanWithDefaults(&languageCode, &count) if err != nil { - return err + log.Error("scan language stats", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } stats.LanguagesGrouped[languageCode] = count } diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index cf7fb59..3e2aaa1 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -8,7 +8,6 @@ import ( "github.com/escalopa/prayer-bot/domain" "github.com/escalopa/prayer-bot/log" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" "github.com/ydb-platform/ydb-go-sdk/v3" "github.com/ydb-platform/ydb-go-sdk/v3/table" "github.com/ydb-platform/ydb-go-sdk/v3/table/result" @@ -40,7 +39,8 @@ func NewDB(ctx context.Context) (*DB, error) { yc.WithInternalCA(), ) if err != nil { - return nil, err + log.Error("failed to open ydb connection", log.Err(err)) + return nil, domain.ErrInternal } return &DB{client: sdk.Table()}, nil @@ -69,7 +69,8 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get chats by ids query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -86,7 +87,8 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( &reminderJSON, ) if err != nil { - return err + log.Error("scan chat fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } var reminder domain.Reminder @@ -123,7 +125,8 @@ func (db *DB) GetSubscribers(ctx context.Context, botID int64) (chatIDs []int64, err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get subscribers query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -132,7 +135,8 @@ func (db *DB) GetSubscribers(ctx context.Context, botID int64) (chatIDs []int64, var chatID int64 err = res.ScanWithDefaults(&chatID) if err != nil { - return err + log.Error("scan subscriber chat id", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } chatIDs = append(chatIDs, chatID) } @@ -171,7 +175,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, res, err := s.Execute(ctx, readTx, query, params) if err != nil { - return err + log.Error("execute get prayer day query", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -185,7 +190,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr &prayerDay.Maghrib, &prayerDay.Isha, ) if err != nil { - return err + log.Error("scan prayer day fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } } else { return domain.ErrNotFound @@ -200,7 +206,8 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr &nextDay.Maghrib, &nextDay.Isha, ) if err != nil { - return err + log.Error("scan next prayer day fields", log.Err(err), log.BotID(botID)) + return domain.ErrInternal } prayerDay.NextDay = nextDay } else { @@ -214,9 +221,6 @@ func (db *DB) GetPrayerDay(ctx context.Context, botID int64, date time.Time) (pr }) if err != nil { - if ydb.IsOperationError(err, Ydb.StatusIds_NOT_FOUND) { - return nil, domain.ErrNotFound - } return nil, err } @@ -250,7 +254,8 @@ func (db *DB) UpdateReminder( txID, res, err := s.Execute(ctx, txBegin, selectQuery, selectParams) if err != nil { - return err + log.Error("execute select query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } defer func(res result.Result) { _ = res.Close() }(res) @@ -259,7 +264,8 @@ func (db *DB) UpdateReminder( if res.NextResultSet(ctx) && res.NextRow() { err = res.ScanWithDefaults(&reminderJSON) if err != nil { - return err + log.Error("scan reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } } else { return domain.ErrNotFound @@ -285,7 +291,8 @@ func (db *DB) UpdateReminder( updatedReminderJSON, err := json.Marshal(&reminder) if err != nil { - return err + log.Error("marshal updated reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal } updateQuery := ` @@ -306,7 +313,11 @@ func (db *DB) UpdateReminder( txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) - return err + if err != nil { + log.Error("execute update reminder query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err @@ -328,7 +339,11 @@ func (db *DB) DeleteChat(ctx context.Context, botID int64, chatID int64) error { err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { _, _, err := s.Execute(ctx, writeTx, query, params) - return err + if err != nil { + log.Error("execute delete chat query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil }) return err From 9eead151801af3097cb5bd055dc47f907aedce96 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 20:28:43 +0300 Subject: [PATCH 34/59] feat: update db returned error --- domain/chat.go | 1 - 1 file changed, 1 deletion(-) diff --git a/domain/chat.go b/domain/chat.go index c331f3c..dd1b132 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -51,7 +51,6 @@ type ( ChatID int64 State string LanguageCode string - IsGroup bool Reminder *Reminder } ) From cc10640401be8979f9c2967b83a2d6465e84f7a2 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 20:34:38 +0300 Subject: [PATCH 35/59] feat: add migration + remove is_group --- _scripts/migrate/README.md | 84 +++++ _scripts/migrate/go.mod | 20 ++ _scripts/migrate/go.sum | 157 ++++++++ _scripts/migrate/main.go | 336 ++++++++++++++++++ _scripts/migrate_reminders/go.mod | 12 - _scripts/migrate_reminders/main.go | 251 ------------- .../20251025195706_create_chats_table.sql | 3 +- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 + .../dispatcher/internal/handler/handler.go | 15 +- .../dispatcher/internal/handler/helper.go | 5 - serverless/dispatcher/internal/service/db.go | 13 +- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 + .../reminder/internal/handler/helper.go | 4 + .../reminder/internal/handler/reminder.go | 2 +- serverless/reminder/internal/service/db.go | 3 +- 19 files changed, 624 insertions(+), 293 deletions(-) create mode 100644 _scripts/migrate/README.md create mode 100644 _scripts/migrate/go.mod create mode 100644 _scripts/migrate/go.sum create mode 100644 _scripts/migrate/main.go delete mode 100644 _scripts/migrate_reminders/go.mod delete mode 100644 _scripts/migrate_reminders/main.go diff --git a/_scripts/migrate/README.md b/_scripts/migrate/README.md new file mode 100644 index 0000000..cafef46 --- /dev/null +++ b/_scripts/migrate/README.md @@ -0,0 +1,84 @@ +# Chat Migration Script + +This script migrates chat data from the old YDB table schema to the new one. + +## Prerequisites + +- Go 1.23 or higher +- Access to both old and new YDB databases + +## Environment Variables + +Set the following environment variables before running the script: + +- `OLD_DB_CONNECTION_STRING`: Connection string for the old YDB database +- `NEW_DB_CONNECTION_STRING`: Connection string for the new YDB database +- `YDB_TOKEN`: Access token for authentication to both databases + +## Usage + +```bash +# Set environment variables +export OLD_DB_CONNECTION_STRING="grpcs://ydb.serverless.yandexcloud.net:2135/?database=/ru-central1/..." +export NEW_DB_CONNECTION_STRING="grpcs://ydb.serverless.yandexcloud.net:2135/?database=/ru-central1/..." +export YDB_TOKEN="your-access-token" + +# Run the migration +cd _scripts/migrate +go run main.go +``` + +## What the script does + +1. Connects to both old and new YDB databases +2. Fetches all chats from the old `chats` table +3. Transforms the data: + - Most fields remain the same (chat_id, bot_id, language_code, state, subscribed, subscribed_at, created_at) + - Creates new `reminder` JSON object with: + - `today`: LastAt set to current time (Moscow timezone) + - `soon`: Offset from old `reminder_offset`, MessageID from old `reminder_message_id` + - `arrive`: MessageID from old `jamaat_message_id` + - `jamaat`: Enabled if both `subscribed` and `jamaat` were true, with default delay configs +4. Uses UPSERT to insert/update records in the new database + +## Idempotency + +The script uses UPSERT statements, making it safe to run multiple times. Subsequent runs will update existing records rather than failing or creating duplicates. + +## Transformation Details + +### Reminder Field + +The reminder JSON structure: +```json +{ + "today": { + "offset": 0, + "message_id": 0, + "last_at": "2025-10-26T12:00:00+03:00" + }, + "soon": { + "offset": 600000000000, + "message_id": 12345, + "last_at": "2025-10-26T12:00:00+03:00" + }, + "arrive": { + "offset": 0, + "message_id": 67890, + "last_at": "2025-10-26T12:00:00+03:00" + }, + "jamaat": { + "enabled": true, + "delay": { + "fajr": 600000000000, + "shuruq": 600000000000, + "dhuhr": 600000000000, + "asr": 600000000000, + "maghrib": 600000000000, + "isha": 1200000000000 + } + } +} +``` + +Note: All durations are in nanoseconds (Go's default). For example, 10 minutes = 600000000000 nanoseconds. diff --git a/_scripts/migrate/go.mod b/_scripts/migrate/go.mod new file mode 100644 index 0000000..2d51b58 --- /dev/null +++ b/_scripts/migrate/go.mod @@ -0,0 +1,20 @@ +module migrate + +go 1.23 + +require github.com/ydb-platform/ydb-go-sdk/v3 v3.86.1 + +require ( + github.com/golang-jwt/jwt/v4 v4.4.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jonboulle/clockwork v0.3.0 // indirect + github.com/ydb-platform/ydb-go-genproto v0.0.0-20241022174402-dd276c7f197b // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/_scripts/migrate/go.sum b/_scripts/migrate/go.sum new file mode 100644 index 0000000..8ca2f5e --- /dev/null +++ b/_scripts/migrate/go.sum @@ -0,0 +1,157 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM= +github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20241022174402-dd276c7f197b h1:8yiv/W+1xTdifJh1Stkck0gFJjys9kg0/r86Buljuss= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20241022174402-dd276c7f197b/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-sdk/v3 v3.86.1 h1:NsDWGBn5albfKM+Dp9iysKRQLWw6k69VezsX+Uw5ok8= +github.com/ydb-platform/ydb-go-sdk/v3 v3.86.1/go.mod h1:ah+n4KaEcANFM+CBKnJ5VrmR0IvWfYFvkofW/56076c= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/_scripts/migrate/main.go b/_scripts/migrate/main.go new file mode 100644 index 0000000..c0fa274 --- /dev/null +++ b/_scripts/migrate/main.go @@ -0,0 +1,336 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/table" + "github.com/ydb-platform/ydb-go-sdk/v3/table/result/named" + "github.com/ydb-platform/ydb-go-sdk/v3/table/types" +) + +// export OLD_DB_CONNECTION_STRING="your-old-db-connection-string" +// export NEW_DB_CONNECTION_STRING="your-new-db-connection-string" +// export YDB_TOKEN="your-access-token" + +// cd _scripts/migrate +// go run main.go + +// Domain types matching the new schema +type JamaatDelayConfig struct { + Fajr int64 `json:"fajr"` // nanoseconds + Shuruq int64 `json:"shuruq"` // nanoseconds + Dhuhr int64 `json:"dhuhr"` // nanoseconds + Asr int64 `json:"asr"` // nanoseconds + Maghrib int64 `json:"maghrib"` // nanoseconds + Isha int64 `json:"isha"` // nanoseconds +} + +type JamaatConfig struct { + Enabled bool `json:"enabled"` + Delay *JamaatDelayConfig `json:"delay"` +} + +type ReminderConfig struct { + Offset int64 `json:"offset"` // nanoseconds + MessageID int `json:"message_id"` + LastAt string `json:"last_at"` // RFC3339 format +} + +type Reminder struct { + Today *ReminderConfig `json:"today"` + Soon *ReminderConfig `json:"soon"` + Arrive *ReminderConfig `json:"arrive"` + Jamaat *JamaatConfig `json:"jamaat"` +} + +// Old table row structure +type OldChat struct { + ChatID int64 + BotID int64 + LanguageCode *string + State *string + ReminderOffset *int32 + ReminderMessageID *int32 + Jamaat *bool + JamaatMessageID *int32 + Subscribed *bool + SubscribedAt *time.Time + CreatedAt *time.Time +} + +// New table row structure +type NewChat struct { + ChatID int64 + BotID int64 + LanguageCode *string + State *string + Reminder string // JSON string + Subscribed *bool + SubscribedAt *time.Time + CreatedAt *time.Time +} + +func main() { + ctx := context.Background() + + // Get environment variables + oldDBConnectionString := os.Getenv("OLD_DB_CONNECTION_STRING") + newDBConnectionString := os.Getenv("NEW_DB_CONNECTION_STRING") + ydbToken := os.Getenv("YDB_TOKEN") + + if oldDBConnectionString == "" || newDBConnectionString == "" || ydbToken == "" { + log.Fatal("Missing required environment variables: OLD_DB_CONNECTION_STRING, NEW_DB_CONNECTION_STRING, YDB_TOKEN") + } + + // Connect to old database + log.Println("Connecting to old database...") + oldDB, err := ydb.Open(ctx, oldDBConnectionString, + ydb.WithAccessTokenCredentials(ydbToken), + ) + if err != nil { + log.Fatalf("Failed to connect to old database: %v", err) + } + defer func() { _ = oldDB.Close(ctx) }() + + // Connect to new database + log.Println("Connecting to new database...") + newDB, err := ydb.Open(ctx, newDBConnectionString, + ydb.WithAccessTokenCredentials(ydbToken), + ) + if err != nil { + log.Fatalf("Failed to connect to new database: %v", err) + } + defer func() { _ = newDB.Close(ctx) }() + + // Fetch all chats from old database + log.Println("Fetching chats from old database...") + oldChats, err := fetchOldChats(ctx, oldDB) + if err != nil { + log.Fatalf("Failed to fetch old chats: %v", err) + } + log.Printf("Fetched %d chats from old database", len(oldChats)) + + // Transform and migrate chats + log.Println("Migrating chats to new database...") + moscowLocation, err := time.LoadLocation("Europe/Moscow") + if err != nil { + log.Fatalf("Failed to load Moscow timezone: %v", err) + } + + newChats := make([]NewChat, 0, len(oldChats)) + for _, oldChat := range oldChats { + newChat, err := transformChat(oldChat, moscowLocation) + if err != nil { + log.Printf("Failed to transform chat (bot_id=%d, chat_id=%d): %v", oldChat.BotID, oldChat.ChatID, err) + continue + } + newChats = append(newChats, newChat) + } + + // Insert chats into new database + if err := upsertNewChats(ctx, newDB, newChats); err != nil { + log.Fatalf("Failed to upsert new chats: %v", err) + } + + log.Printf("Successfully migrated %d chats", len(newChats)) +} + +func fetchOldChats(ctx context.Context, db *ydb.Driver) ([]OldChat, error) { + var chats []OldChat + + err := db.Table().Do(ctx, func(ctx context.Context, s table.Session) error { + _, res, err := s.Execute(ctx, table.DefaultTxControl(), + `SELECT chat_id, bot_id, language_code, state, reminder_offset, + reminder_message_id, jamaat, jamaat_message_id, subscribed, + subscribed_at, created_at + FROM chats`, + nil, + ) + if err != nil { + return err + } + defer func() { _ = res.Close() }() + + for res.NextResultSet(ctx) { + for res.NextRow() { + var chat OldChat + err := res.ScanNamed( + named.Required("chat_id", &chat.ChatID), + named.Required("bot_id", &chat.BotID), + named.Optional("language_code", &chat.LanguageCode), + named.Optional("state", &chat.State), + named.Optional("reminder_offset", &chat.ReminderOffset), + named.Optional("reminder_message_id", &chat.ReminderMessageID), + named.Optional("jamaat", &chat.Jamaat), + named.Optional("jamaat_message_id", &chat.JamaatMessageID), + named.Optional("subscribed", &chat.Subscribed), + named.Optional("subscribed_at", &chat.SubscribedAt), + named.Optional("created_at", &chat.CreatedAt), + ) + if err != nil { + return err + } + chats = append(chats, chat) + } + } + + return res.Err() + }) + + return chats, err +} + +func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, error) { + now := time.Now().In(moscowLocation) + + // Build reminder object + reminderOffset := int32(20) + if oldChat.ReminderOffset != nil { + reminderOffset = *oldChat.ReminderOffset + } + + reminderMessageID := 0 + if oldChat.ReminderMessageID != nil { + reminderMessageID = int(*oldChat.ReminderMessageID) + } + + jamaatMessageID := 0 + if oldChat.JamaatMessageID != nil { + jamaatMessageID = int(*oldChat.JamaatMessageID) + } + + jamaat := false + if oldChat.Jamaat != nil { + jamaat = *oldChat.Jamaat + } + + subscribed := false + if oldChat.Subscribed != nil { + subscribed = *oldChat.Subscribed + } + + reminder := Reminder{ + Today: &ReminderConfig{ + Offset: int64(3 * time.Hour), + LastAt: now.Format(time.RFC3339), + }, + Soon: &ReminderConfig{ + Offset: int64(reminderOffset) * int64(time.Minute), + MessageID: reminderMessageID, + LastAt: now.Format(time.RFC3339), + }, + Arrive: &ReminderConfig{ + Offset: 0, + MessageID: jamaatMessageID, + LastAt: now.Format(time.RFC3339), + }, + Jamaat: &JamaatConfig{ + Enabled: subscribed && jamaat, + Delay: &JamaatDelayConfig{ + Fajr: int64(10 * time.Minute), + Shuruq: int64(10 * time.Minute), + Dhuhr: int64(10 * time.Minute), + Asr: int64(10 * time.Minute), + Maghrib: int64(10 * time.Minute), + Isha: int64(20 * time.Minute), + }, + }, + } + + // Marshal reminder to JSON + reminderJSON, err := json.Marshal(reminder) + if err != nil { + return NewChat{}, fmt.Errorf("failed to marshal reminder: %w", err) + } + + return NewChat{ + ChatID: oldChat.ChatID, + BotID: oldChat.BotID, + LanguageCode: oldChat.LanguageCode, + State: oldChat.State, + Reminder: string(reminderJSON), + Subscribed: oldChat.Subscribed, + SubscribedAt: oldChat.SubscribedAt, + CreatedAt: oldChat.CreatedAt, + }, nil +} + +func upsertNewChats(ctx context.Context, db *ydb.Driver, chats []NewChat) error { + return db.Table().Do(ctx, func(ctx context.Context, s table.Session) error { + for _, chat := range chats { + err := upsertChat(ctx, s, chat) + if err != nil { + log.Printf("Failed to upsert chat (bot_id=%d, chat_id=%d): %v", chat.BotID, chat.ChatID, err) + return err + } + } + return nil + }) +} + +func upsertChat(ctx context.Context, s table.Session, chat NewChat) error { + query := ` + DECLARE $chat_id AS Int64; + DECLARE $bot_id AS Int64; + DECLARE $language_code AS Utf8?; + DECLARE $state AS Utf8?; + DECLARE $reminder AS Json; + DECLARE $subscribed AS Bool?; + DECLARE $subscribed_at AS Datetime?; + DECLARE $created_at AS Datetime?; + + UPSERT INTO chats (chat_id, bot_id, language_code, state, reminder, subscribed, subscribed_at, created_at) + VALUES ($chat_id, $bot_id, $language_code, $state, $reminder, $subscribed, $subscribed_at, $created_at); + ` + + params := table.NewQueryParameters( + table.ValueParam("$chat_id", types.Int64Value(chat.ChatID)), + table.ValueParam("$bot_id", types.Int64Value(chat.BotID)), + table.ValueParam("$reminder", types.JSONValue(chat.Reminder)), + ) + + // Handle optional language_code + if chat.LanguageCode != nil { + params.Add(table.ValueParam("$language_code", types.OptionalValue(types.UTF8Value(*chat.LanguageCode)))) + } else { + params.Add(table.ValueParam("$language_code", types.NullValue(types.TypeUTF8))) + } + + // Handle optional state + if chat.State != nil { + params.Add(table.ValueParam("$state", types.OptionalValue(types.UTF8Value(*chat.State)))) + } else { + params.Add(table.ValueParam("$state", types.NullValue(types.TypeUTF8))) + } + + // Handle optional subscribed + if chat.Subscribed != nil { + params.Add(table.ValueParam("$subscribed", types.OptionalValue(types.BoolValue(*chat.Subscribed)))) + } else { + params.Add(table.ValueParam("$subscribed", types.NullValue(types.TypeBool))) + } + + // Handle optional subscribed_at + if chat.SubscribedAt != nil { + params.Add(table.ValueParam("$subscribed_at", types.OptionalValue(types.DatetimeValueFromTime(*chat.SubscribedAt)))) + } else { + params.Add(table.ValueParam("$subscribed_at", types.NullValue(types.TypeDatetime))) + } + + // Handle optional created_at + if chat.CreatedAt != nil { + params.Add(table.ValueParam("$created_at", types.OptionalValue(types.DatetimeValueFromTime(*chat.CreatedAt)))) + } else { + params.Add(table.ValueParam("$created_at", types.NullValue(types.TypeDatetime))) + } + + _, _, err := s.Execute(ctx, table.DefaultTxControl(), query, params) + + return err +} diff --git a/_scripts/migrate_reminders/go.mod b/_scripts/migrate_reminders/go.mod deleted file mode 100644 index 11b56c3..0000000 --- a/_scripts/migrate_reminders/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module github.com/escalopa/prayer-bot/scripts/migrate_reminders - -go 1.21 - -require ( - github.com/escalopa/prayer-bot v0.0.0 - github.com/ydb-platform/ydb-go-genproto v0.0.0-20240920120314-0fed943b0136 - github.com/ydb-platform/ydb-go-sdk/v3 v3.82.3 - github.com/ydb-platform/ydb-go-yc v0.12.1 -) - -replace github.com/escalopa/prayer-bot => ../.. diff --git a/_scripts/migrate_reminders/main.go b/_scripts/migrate_reminders/main.go deleted file mode 100644 index c7cd944..0000000 --- a/_scripts/migrate_reminders/main.go +++ /dev/null @@ -1,251 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "time" - - "github.com/escalopa/prayer-bot/domain" - "github.com/ydb-platform/ydb-go-sdk/v3" - "github.com/ydb-platform/ydb-go-sdk/v3/table" - "github.com/ydb-platform/ydb-go-sdk/v3/table/result" - "github.com/ydb-platform/ydb-go-sdk/v3/table/types" - yc "github.com/ydb-platform/ydb-go-yc" -) - -var ( - writeTx = table.TxControl( - table.BeginTx(table.WithSerializableReadWrite()), - table.CommitTx(), - ) -) - -type oldChatData struct { - BotID int64 - ChatID int64 - ReminderOffset int32 - ReminderMessageID int32 - IsGroup bool - JamaatOffset int32 - JamaatMessageID int32 -} - -func main() { - dryRun := flag.Bool("dry-run", false, "Preview changes without writing to database") - flag.Parse() - - ctx := context.Background() - - // Load Moscow timezone - loc, err := time.LoadLocation("Europe/Moscow") - if err != nil { - log.Fatalf("Failed to load timezone: %v", err) - } - now := time.Now().In(loc) - - // Connect to YDB - ydbEndpoint := os.Getenv("YDB_ENDPOINT") - if ydbEndpoint == "" { - log.Fatal("YDB_ENDPOINT environment variable is not set") - } - - sdk, err := ydb.Open(ctx, ydbEndpoint, - yc.WithMetadataCredentials(), - yc.WithInternalCA(), - ) - if err != nil { - log.Fatalf("Failed to connect to YDB: %v", err) - } - defer func() { _ = sdk.Close(ctx) }() - - client := sdk.Table() - - // Statistics - var ( - totalProcessed int - totalMigrated int - totalSkipped int - ) - - // Query all chats - query := ` - SELECT bot_id, chat_id, reminder_offset, reminder_message_id, is_group, jamaat_offset, jamaat_message_id - FROM chats - ` - - err = client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, res, err := s.Execute(ctx, - table.TxControl( - table.BeginTx(table.WithOnlineReadOnly()), - table.CommitTx(), - ), - query, - table.NewQueryParameters(), - ) - if err != nil { - return fmt.Errorf("execute query: %w", err) - } - defer func() { _ = res.Close() }() - - chats := []oldChatData{} - if res.NextResultSet(ctx) { - for res.NextRow() { - var chat oldChatData - var reminderOffset, reminderMessageID, jamaatOffset, jamaatMessageID *int32 - var isGroup *bool - - err = res.Scan( - &chat.BotID, - &chat.ChatID, - &reminderOffset, - &reminderMessageID, - &isGroup, - &jamaatOffset, - &jamaatMessageID, - ) - if err != nil { - log.Printf("Failed to scan row: %v", err) - continue - } - - // Handle nullable fields - if reminderOffset != nil { - chat.ReminderOffset = *reminderOffset - } - if reminderMessageID != nil { - chat.ReminderMessageID = *reminderMessageID - } - if isGroup != nil { - chat.IsGroup = *isGroup - } - if jamaatOffset != nil { - chat.JamaatOffset = *jamaatOffset - } - if jamaatMessageID != nil { - chat.JamaatMessageID = *jamaatMessageID - } - - chats = append(chats, chat) - } - } - - log.Printf("Found %d chats to process", len(chats)) - - // Process chats in batches - batchSize := 100 - for i := 0; i < len(chats); i += batchSize { - end := i + batchSize - if end > len(chats) { - end = len(chats) - } - batch := chats[i:end] - - for _, chat := range batch { - totalProcessed++ - - // Check if chat has any reminder data - hasReminderData := chat.ReminderOffset > 0 || - chat.ReminderMessageID > 0 || - chat.JamaatOffset > 0 || - chat.JamaatMessageID > 0 - - if !hasReminderData { - totalSkipped++ - continue - } - - // Build Reminder object - reminder := domain.Reminder{ - Today: domain.ReminderConfig{ - Offset: 0, // Disabled by default - MessageID: 0, - LastAt: now.Truncate(24 * time.Hour), // Date only - }, - Soon: domain.ReminderConfig{ - Offset: time.Duration(chat.ReminderOffset) * time.Minute, - MessageID: int(chat.ReminderMessageID), - LastAt: now, - }, - Arrive: domain.ReminderConfig{ - Offset: 0, // Always 0 - MessageID: 0, - LastAt: now, - }, - JamaatDelay: domain.JamaatDelay{ - Fajr: 10 * time.Minute, - Shuruq: 10 * time.Minute, - Dhuhr: 10 * time.Minute, - Asr: 10 * time.Minute, - Maghrib: 10 * time.Minute, - Isha: 20 * time.Minute, - }, - } - - // Serialize to JSON - reminderJSON, err := json.Marshal(reminder) - if err != nil { - log.Printf("Failed to marshal reminder for chat %d/%d: %v", chat.BotID, chat.ChatID, err) - continue - } - - if *dryRun { - log.Printf("[DRY RUN] Would migrate chat %d/%d: %s", chat.BotID, chat.ChatID, string(reminderJSON)) - totalMigrated++ - continue - } - - // Update database - updateQuery := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - DECLARE $reminder AS Json; - - UPDATE chats - SET reminder = $reminder - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` - - params := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(chat.BotID)), - table.ValueParam("$chat_id", types.Int64Value(chat.ChatID)), - table.ValueParam("$reminder", types.JSONValue(string(reminderJSON))), - ) - - err = client.Do(ctx, func(ctx context.Context, s table.Session) error { - _, _, err := s.Execute(ctx, writeTx, updateQuery, params) - return err - }) - - if err != nil { - log.Printf("Failed to update chat %d/%d: %v", chat.BotID, chat.ChatID, err) - continue - } - - totalMigrated++ - if totalMigrated%10 == 0 { - log.Printf("Migrated %d chats so far...", totalMigrated) - } - } - } - - return nil - }) - - if err != nil { - log.Fatalf("Migration failed: %v", err) - } - - // Print statistics - log.Println("==================================================") - log.Println("Migration completed successfully!") - log.Printf("Total processed: %d", totalProcessed) - log.Printf("Total migrated: %d", totalMigrated) - log.Printf("Total skipped: %d", totalSkipped) - if *dryRun { - log.Println("*** DRY RUN MODE - No changes were made ***") - } -} diff --git a/migrations/20251025195706_create_chats_table.sql b/migrations/20251025195706_create_chats_table.sql index f3054a5..34fd8e1 100644 --- a/migrations/20251025195706_create_chats_table.sql +++ b/migrations/20251025195706_create_chats_table.sql @@ -5,7 +5,6 @@ CREATE TABLE chats ( language_code Utf8, state Utf8, reminder Json, - is_group Bool, subscribed Bool, subscribed_at Datetime, created_at Datetime, @@ -13,4 +12,4 @@ CREATE TABLE chats ( ); -- +goose Down -DROP TABLE IF EXISTS chats; \ No newline at end of file +DROP TABLE IF EXISTS chats; diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 080a216..c866c21 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 + github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index df4e441..081c272 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -571,6 +571,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index caa517f..1e82e96 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -17,7 +17,7 @@ import ( type ( DB interface { - CreateChat(ctx context.Context, botID int64, chatID int64, languageCode string, state string, isGroup bool, reminder *domain.Reminder) error + CreateChat(ctx context.Context, botID int64, chatID int64, languageCode string, state string, reminder *domain.Reminder) error GetChat(ctx context.Context, botID int64, chatID int64) (chat *domain.Chat, _ error) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, _ error) SetState(ctx context.Context, botID int64, chatID int64, state string) error @@ -200,15 +200,12 @@ func (h *Handler) getBot(botID int64) (*bot.Bot, error) { func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.Chat, error) { botID := getContextBotID(ctx) chatID := int64(0) - isGroup := false switch { case update.Message != nil: chatID = update.Message.Chat.ID - isGroup = isGroupChat(update.Message.Chat) case update.CallbackQuery != nil && update.CallbackQuery.Message.Message != nil: chatID = update.CallbackQuery.Message.Message.Chat.ID - isGroup = isGroupChat(update.CallbackQuery.Message.Message.Chat) default: bytes, _ := json.Marshal(update) log.Info("ignore message", log.BotID(botID), log.String("update", string(bytes))) @@ -234,8 +231,11 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C now := h.now(botID) reminder := &domain.Reminder{ - Today: &domain.ReminderConfig{LastAt: now}, - Soon: &domain.ReminderConfig{LastAt: now}, + Today: &domain.ReminderConfig{LastAt: now}, + Soon: &domain.ReminderConfig{ + LastAt: now, + Offset: 20 * time.Minute, + }, Arrive: &domain.ReminderConfig{LastAt: now}, Jamaat: &domain.JamaatConfig{ Delay: &domain.JamaatDelayConfig{ @@ -249,7 +249,7 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C }, } - err = h.db.CreateChat(ctx, botID, chatID, languageCode, string(defaultState), isGroup, reminder) + err = h.db.CreateChat(ctx, botID, chatID, languageCode, string(defaultState), reminder) if err != nil { log.Error("create chat", log.Err(err), log.BotID(botID), log.ChatID(chatID), "update", update) return nil, domain.ErrInternal @@ -260,7 +260,6 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C ChatID: chatID, State: string(defaultState), LanguageCode: languageCode, - IsGroup: isGroup, Reminder: reminder, } diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 3a8813c..e643ef5 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -6,7 +6,6 @@ import ( "time" "github.com/escalopa/prayer-bot/domain" - "github.com/go-telegram/bot/models" ) type ( @@ -73,7 +72,3 @@ func layoutRowsInfo(totalItems, itemsPerRow int) (filled int, empty int) { filled = (totalItems / itemsPerRow) + 1 return } - -func isGroupChat(chat models.Chat) bool { - return chat.Type == models.ChatTypeGroup || chat.Type == models.ChatTypeSupergroup -} diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 969c318..a121eaa 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -53,7 +53,6 @@ func (db *DB) CreateChat( chatID int64, languageCode string, state string, - isGroup bool, reminder *domain.Reminder, ) error { @@ -68,11 +67,10 @@ func (db *DB) CreateChat( DECLARE $chat_id AS Int64; DECLARE $language_code AS Utf8; DECLARE $state AS Utf8; - DECLARE $is_group AS Bool; DECLARE $reminder AS Json; - INSERT INTO chats (bot_id, chat_id, language_code, state, is_group, reminder, created_at) - VALUES ($bot_id, $chat_id, $language_code, $state, $is_group, $reminder, CurrentUtcDatetime()); + INSERT INTO chats (bot_id, chat_id, language_code, state, reminder, created_at) + VALUES ($bot_id, $chat_id, $language_code, $state, $reminder, CurrentUtcDatetime()); ` params := table.NewQueryParameters( @@ -80,7 +78,6 @@ func (db *DB) CreateChat( table.ValueParam("$chat_id", types.Int64Value(chatID)), table.ValueParam("$language_code", types.UTF8Value(languageCode)), table.ValueParam("$state", types.UTF8Value(state)), - table.ValueParam("$is_group", types.BoolValue(isGroup)), table.ValueParam("$reminder", types.JSONValue(string(reminderJSON))), ) @@ -105,7 +102,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom DECLARE $bot_id AS Int64; DECLARE $chat_id AS int64; - SELECT bot_id, chat_id, state, language_code, is_group, reminder + SELECT bot_id, chat_id, state, language_code, reminder FROM chats WHERE bot_id = $bot_id AND chat_id = $chat_id; ` @@ -131,7 +128,6 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.IsGroup, &reminderJSON, ) if err != nil { @@ -164,7 +160,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, query := ` DECLARE $bot_id AS Int64; - SELECT bot_id, chat_id, state, language_code, is_group, reminder + SELECT bot_id, chat_id, state, language_code, reminder FROM chats WHERE bot_id = $bot_id; ` @@ -187,7 +183,6 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.IsGroup, &reminderJSON, ) if err != nil { diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 32e60e4..f002712 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 + github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index bd64788..a8208fa 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -573,6 +573,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 13e7475..875fce7 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 + github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index df4e441..081c272 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -571,6 +571,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= +github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/helper.go b/serverless/reminder/internal/handler/helper.go index a7a6317..9f1c398 100644 --- a/serverless/reminder/internal/handler/helper.go +++ b/serverless/reminder/internal/handler/helper.go @@ -80,3 +80,7 @@ func deleteMessages(ctx context.Context, b *bot.Bot, chat *domain.Chat, ids ...i func isBlockedErr(err error) bool { return strings.HasPrefix(err.Error(), bot.ErrorForbidden.Error()) } + +func isGroupChat(chatID int64) bool { + return chatID < 0 +} diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index 5647a27..9a92bb3 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -81,7 +81,7 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, deleteMessages(ctx, b, chat, chat.Reminder.Soon.MessageID, chat.Reminder.Arrive.MessageID) - if chat.IsGroup && chat.Reminder.Jamaat.Enabled { + if chat.Reminder.Jamaat.Enabled { delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) message := fmt.Sprintf("%s\n%s", fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index 3e2aaa1..4aae84f 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -51,7 +51,7 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( DECLARE $bot_id AS Int64; DECLARE $chat_ids AS List; - SELECT bot_id, chat_id, state, language_code, is_group, reminder + SELECT bot_id, chat_id, state, language_code, reminder FROM chats WHERE bot_id = $bot_id AND chat_id IN $chat_ids; ` @@ -83,7 +83,6 @@ func (db *DB) GetChatsByIDs(ctx context.Context, botID int64, chatIDs []int64) ( &chat.ChatID, &chat.State, &chat.LanguageCode, - &chat.IsGroup, &reminderJSON, ) if err != nil { From 4eb306682562e0f52a97666d540caf6f119a28d0 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 20:43:12 +0300 Subject: [PATCH 36/59] feat: set today offset --- serverless/dispatcher/internal/handler/handler.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 1e82e96..362d6f1 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -231,7 +231,10 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C now := h.now(botID) reminder := &domain.Reminder{ - Today: &domain.ReminderConfig{LastAt: now}, + Today: &domain.ReminderConfig{ + LastAt: now, + Offset: 3 * time.Hour, + }, Soon: &domain.ReminderConfig{ LastAt: now, Offset: 20 * time.Minute, From d6bccce30ecf9715ce9c3da68e0c47692887f31d Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 22:28:18 +0300 Subject: [PATCH 37/59] feat: update domain --- domain/chat.go | 1 + domain/reminder.go | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/domain/chat.go b/domain/chat.go index dd1b132..180c0dc 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -51,6 +51,7 @@ type ( ChatID int64 State string LanguageCode string + Subscribed bool Reminder *Reminder } ) diff --git a/domain/reminder.go b/domain/reminder.go index 5ca5704..551bcc7 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -5,10 +5,8 @@ import "time" type ReminderType string const ( - ReminderTypeToday ReminderType = "today" - ReminderTypeSoon ReminderType = "soon" - ReminderTypeArrive ReminderType = "arrive" - ReminderTypeJamaat ReminderType = "jamaat" + ReminderTypeToday ReminderType = "today" + ReminderTypeSoon ReminderType = "soon" ) func (rt ReminderType) String() string { @@ -62,3 +60,20 @@ func (j *JamaatDelayConfig) GetDelayByPrayerID(prayerID PrayerID) time.Duration return 0 } } + +func (j *JamaatDelayConfig) SetDelayByPrayerID(prayerID PrayerID, delay time.Duration) { + switch prayerID { + case PrayerIDFajr: + j.Fajr = delay + case PrayerIDShuruq: + j.Shuruq = delay + case PrayerIDDhuhr: + j.Dhuhr = delay + case PrayerIDAsr: + j.Asr = delay + case PrayerIDMaghrib: + j.Maghrib = delay + case PrayerIDIsha: + j.Isha = delay + } +} From 725e9cfbd15d0aab973c997012702fcfab74c50b Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 22:40:15 +0300 Subject: [PATCH 38/59] feat: update domain --- domain/prayer.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/domain/prayer.go b/domain/prayer.go index c3582ad..488a8aa 100644 --- a/domain/prayer.go +++ b/domain/prayer.go @@ -17,6 +17,25 @@ const ( PrayerIDIsha PrayerID = 6 ) +func (p PrayerID) String() string { + switch p { + case PrayerIDFajr: + return "fajr" + case PrayerIDShuruq: + return "shuruq" + case PrayerIDDhuhr: + return "dhuhr" + case PrayerIDAsr: + return "asr" + case PrayerIDMaghrib: + return "maghrib" + case PrayerIDIsha: + return "isha" + default: + return "unknown" + } +} + type PrayerDay struct { Date time.Time `json:"date"` Fajr time.Time `json:"fajr"` From 6f6adf300279850767ee54556a3cc94ec2c1bb28 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:02:18 +0300 Subject: [PATCH 39/59] feat: update domain --- domain/prayer.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/domain/prayer.go b/domain/prayer.go index 488a8aa..77cbf5d 100644 --- a/domain/prayer.go +++ b/domain/prayer.go @@ -36,6 +36,25 @@ func (p PrayerID) String() string { } } +func ParsePrayerID(prayerName string) PrayerID { + switch prayerName { + case "fajr": + return PrayerIDFajr + case "shuruq": + return PrayerIDShuruq + case "dhuhr": + return PrayerIDDhuhr + case "asr": + return PrayerIDAsr + case "maghrib": + return PrayerIDMaghrib + case "isha": + return PrayerIDIsha + default: + return PrayerIDUnknown + } +} + type PrayerDay struct { Date time.Time `json:"date"` Fajr time.Time `json:"fajr"` From aa73723fad534b44c34c046e9aaab1a6dd3c8197 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:05:54 +0300 Subject: [PATCH 40/59] feat: update domain --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 6 + .../dispatcher/internal/handler/command.go | 56 +-- .../dispatcher/internal/handler/handler.go | 17 +- .../dispatcher/internal/handler/helper.go | 15 + .../dispatcher/internal/handler/keyboard.go | 132 +++++- .../dispatcher/internal/handler/languages.go | 38 ++ .../internal/handler/languages/ar.yaml | 56 ++- .../internal/handler/languages/en.yaml | 58 ++- .../internal/handler/languages/es.yaml | 56 ++- .../internal/handler/languages/fr.yaml | 52 ++- .../internal/handler/languages/ru.yaml | 52 ++- .../internal/handler/languages/tr.yaml | 52 ++- .../internal/handler/languages/tt.yaml | 52 ++- .../internal/handler/languages/uz.yaml | 53 ++- .../dispatcher/internal/handler/query.go | 435 ++++++++++++++++++ serverless/dispatcher/internal/service/db.go | 170 ++++--- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 6 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 6 + 21 files changed, 1049 insertions(+), 269 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index c866c21..3b7865e 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a + github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 081c272..4214602 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -573,6 +573,12 @@ github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+q github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf h1:OLDTjLdG0mNigMk9sNNh+fJ5LBWkYboNKu2pFX61WSo= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRcfFQVjEteZWTaLOkyS6B5OlrKr8CmroC4= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index 924d45f..fd5e434 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -189,11 +189,19 @@ func (h *Handler) next(ctx context.Context, b *bot.Bot, _ *models.Update) error func (h *Handler) remind(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + var messageText string + if chat.Subscribed { + messageText = text.RemindMenu.TitleEnabled + } else { + messageText = text.RemindMenu.TitleDisabled + } _, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, - Text: h.lp.GetText(chat.LanguageCode).Remind.Start, - ReplyMarkup: h.remindKeyboard(), + Text: messageText, + ReplyMarkup: h.remindMenuKeyboard(chat), }) if err != nil { log.Error("remind: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) @@ -263,50 +271,6 @@ func (h *Handler) language(ctx context.Context, b *bot.Bot, _ *models.Update) er return nil } -func (h *Handler) subscribe(ctx context.Context, b *bot.Bot, _ *models.Update) error { - chat := getContextChat(ctx) - - err := h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, true) - if err != nil { - log.Error("subscribe: set subscribed", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - _, err = b.SendMessage(ctx, &bot.SendMessageParams{ - ChatID: chat.ChatID, - Text: h.lp.GetText(chat.LanguageCode).SubscriptionSuccess, - }) - if err != nil { - log.Error("subscribe: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - h.resetState(ctx, chat) - return nil -} - -func (h *Handler) unsubscribe(ctx context.Context, b *bot.Bot, _ *models.Update) error { - chat := getContextChat(ctx) - - err := h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, false) - if err != nil { - log.Error("unsubscribe: set subscribed", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - _, err = b.SendMessage(ctx, &bot.SendMessageParams{ - ChatID: chat.ChatID, - Text: h.lp.GetText(chat.LanguageCode).UnsubscriptionSuccess, - }) - if err != nil { - log.Error("unsubscribe: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - h.resetState(ctx, chat) - return nil -} - func (h *Handler) admin(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 362d6f1..ed30020 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -28,6 +28,8 @@ type ( SetSubscribed(ctx context.Context, botID int64, chatID int64, subscribed bool) error SetLanguageCode(ctx context.Context, botID int64, chatID int64, languageCode string) error SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error + SetJamaatEnabled(ctx context.Context, botID int64, chatID int64, enabled bool) error + SetJamaatDelay(ctx context.Context, botID int64, chatID int64, prayerID domain.PrayerID, delay time.Duration) error } Handler struct { @@ -67,8 +69,6 @@ func (h *Handler) opts() []bot.Option { bot.WithMessageTextHandler(bugCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.bug))), bot.WithMessageTextHandler(feedbackCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.feedback))), bot.WithMessageTextHandler(languageCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.language))), - bot.WithMessageTextHandler(subscribeCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.subscribe))), - bot.WithMessageTextHandler(unsubscribeCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.unsubscribe))), bot.WithMessageTextHandler(cancelCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.cancel))), bot.WithMessageTextHandler(adminCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.admin)))), @@ -78,9 +78,20 @@ func (h *Handler) opts() []bot.Option { bot.WithCallbackQueryDataHandler(monthQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.monthQuery))), bot.WithCallbackQueryDataHandler(dayQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.dayQuery))), - bot.WithCallbackQueryDataHandler(remindQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindQuery))), bot.WithCallbackQueryDataHandler(languageQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.languageQuery))), bot.WithCallbackQueryDataHandler(emptyQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.emptyQuery))), + bot.WithCallbackQueryDataHandler(remindMenuQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindMenuQuery))), + bot.WithCallbackQueryDataHandler(remindToggleQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindToggleQuery))), + bot.WithCallbackQueryDataHandler(remindEditQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindEditQuery))), + bot.WithCallbackQueryDataHandler(remindAdjustQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindAdjustQuery))), + bot.WithCallbackQueryDataHandler(remindSaveQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindSaveQuery))), + bot.WithCallbackQueryDataHandler(remindJamaatMenuQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatMenuQuery))), + bot.WithCallbackQueryDataHandler(remindJamaatToggleQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatToggleQuery))), + bot.WithCallbackQueryDataHandler(remindJamaatEditQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatEditQuery))), + bot.WithCallbackQueryDataHandler(remindJamaatAdjustQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatAdjustQuery))), + bot.WithCallbackQueryDataHandler(remindJamaatSaveQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatSaveQuery))), + bot.WithCallbackQueryDataHandler(remindBackQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindBackQuery))), + bot.WithCallbackQueryDataHandler(remindCloseQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindCloseQuery))), } } diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index e643ef5..0e684a6 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -3,6 +3,8 @@ package handler import ( "context" "fmt" + "strconv" + "strings" "time" "github.com/escalopa/prayer-bot/domain" @@ -72,3 +74,16 @@ func layoutRowsInfo(totalItems, itemsPerRow int) (filled int, empty int) { filled = (totalItems / itemsPerRow) + 1 return } + +// parseAdjustment parses the adjustment string and returns the time duration. +func parseAdjustment(adj string) time.Duration { + if strings.HasSuffix(adj, "m") { + val, _ := strconv.Atoi(strings.TrimSuffix(adj, "m")) + return time.Duration(val) * time.Minute + } + if strings.HasSuffix(adj, "h") { + val, _ := strconv.Atoi(strings.TrimSuffix(adj, "h")) + return time.Duration(val) * time.Hour + } + return 0 +} diff --git a/serverless/dispatcher/internal/handler/keyboard.go b/serverless/dispatcher/internal/handler/keyboard.go index 97ae430..a7e093d 100644 --- a/serverless/dispatcher/internal/handler/keyboard.go +++ b/serverless/dispatcher/internal/handler/keyboard.go @@ -90,25 +90,129 @@ func (h *Handler) daysKeyboard(now time.Time, month int) *models.InlineKeyboardM return kb } -func (h *Handler) remindKeyboard() *models.InlineKeyboardMarkup { - reminderOffsets := domain.ReminderOffsets() - rows, empty := layoutRowsInfo(len(reminderOffsets), remindPerRow) +func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMarkup { + text := h.lp.GetText(chat.LanguageCode) - kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, rows)} + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 4)} - row := 0 - for i, offset := range reminderOffsets { - if i%remindPerRow == 0 && i != 0 { - row++ + if chat.Subscribed { + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: text.RemindMenu.Disable, CallbackData: "remind:toggle|"}, + } + } else { + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: text.RemindMenu.Enable, CallbackData: "remind:toggle|"}, } - kb.InlineKeyboard[row] = append(kb.InlineKeyboard[row], models.InlineKeyboardButton{ - Text: strconv.Itoa(int(offset)), - CallbackData: fmt.Sprintf("%s%d", remindQuery, offset), - }) } - for i := 0; i < empty; i++ { - kb.InlineKeyboard[row] = append(kb.InlineKeyboard[row], emptyButton()) + todayOffset := domain.FormatDuration(chat.Reminder.Today.Offset) + soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset) + kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Today, todayOffset), CallbackData: "remind:edit:today|"}, + {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Soon, soonOffset), CallbackData: "remind:edit:soon|"}, + } + + kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ + {Text: text.RemindMenu.JamaatSettings, CallbackData: "remind:jamaat:menu|"}, + } + + kb.InlineKeyboard[3] = []models.InlineKeyboardButton{ + {Text: text.RemindMenu.Close, CallbackData: "remind:close|"}, + } + + return kb +} + +func (h *Handler) remindEditKeyboard(reminderType domain.ReminderType, languageCode string) *models.InlineKeyboardMarkup { + text := h.lp.GetText(languageCode) + + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 3)} + + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: "+1m", CallbackData: fmt.Sprintf("remind:adjust:%s:+1m|", reminderType)}, + {Text: "+10m", CallbackData: fmt.Sprintf("remind:adjust:%s:+10m|", reminderType)}, + {Text: "+1h", CallbackData: fmt.Sprintf("remind:adjust:%s:+1h|", reminderType)}, + } + + kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ + {Text: "-1m", CallbackData: fmt.Sprintf("remind:adjust:%s:-1m|", reminderType)}, + {Text: "-10m", CallbackData: fmt.Sprintf("remind:adjust:%s:-10m|", reminderType)}, + {Text: "-1h", CallbackData: fmt.Sprintf("remind:adjust:%s:-1h|", reminderType)}, + } + + kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ + {Text: text.Buttons.Save, CallbackData: fmt.Sprintf("remind:save:%s|", reminderType)}, + {Text: text.Buttons.Back, CallbackData: "remind:back:menu|"}, + } + + return kb +} + +func (h *Handler) jammatMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMarkup { + text := h.lp.GetText(chat.LanguageCode) + + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 5)} + + if chat.Reminder.Jamaat.Enabled { + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: text.JamaatMenu.Disable, CallbackData: "remind:jamaat:toggle|"}, + } + } else { + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: text.JamaatMenu.Enable, CallbackData: "remind:jamaat:toggle|"}, + } + } + + fajrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr) + shuruqDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Shuruq) + kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDFajr)], fajrDelay), CallbackData: "remind:jamaat:edit:fajr|"}, + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDShuruq)], shuruqDelay), CallbackData: "remind:jamaat:edit:shuruq|"}, + } + + dhuhrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr) + asrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr) + kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDDhuhr)], dhuhrDelay), CallbackData: "remind:jamaat:edit:dhuhr|"}, + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDAsr)], asrDelay), CallbackData: "remind:jamaat:edit:asr|"}, + } + + maghribDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib) + ishaDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Isha) + kb.InlineKeyboard[3] = []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDMaghrib)], maghribDelay), CallbackData: "remind:jamaat:edit:maghrib|"}, + {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDIsha)], ishaDelay), CallbackData: "remind:jamaat:edit:isha|"}, + } + + kb.InlineKeyboard[4] = []models.InlineKeyboardButton{ + {Text: text.Buttons.Back, CallbackData: "remind:back:menu|"}, + } + + return kb +} + +func (h *Handler) jammatEditKeyboard(prayerID domain.PrayerID, languageCode string) *models.InlineKeyboardMarkup { + text := h.lp.GetText(languageCode) + + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 3)} + + prayerName := prayerID.String() + + kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + {Text: "+1m", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:+1m|", prayerName)}, + {Text: "+10m", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:+10m|", prayerName)}, + {Text: "+1h", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:+1h|", prayerName)}, + } + + kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ + {Text: "-1m", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:-1m|", prayerName)}, + {Text: "-10m", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:-10m|", prayerName)}, + {Text: "-1h", CallbackData: fmt.Sprintf("remind:jamaat:adjust:%s:-1h|", prayerName)}, + } + + kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ + {Text: text.Buttons.Save, CallbackData: fmt.Sprintf("remind:jamaat:save:%s|", prayerName)}, + {Text: text.Buttons.Back, CallbackData: "remind:back:jamaat|"}, } return kb diff --git a/serverless/dispatcher/internal/handler/languages.go b/serverless/dispatcher/internal/handler/languages.go index f09d2cd..38d974e 100644 --- a/serverless/dispatcher/internal/handler/languages.go +++ b/serverless/dispatcher/internal/handler/languages.go @@ -25,6 +25,38 @@ type ( Success string `yaml:"success"` } + RemindMenuText struct { + TitleEnabled string `yaml:"title_enabled"` + TitleDisabled string `yaml:"title_disabled"` + Enable string `yaml:"enable"` + Disable string `yaml:"disable"` + Today string `yaml:"today"` + Soon string `yaml:"soon"` + JamaatSettings string `yaml:"jamaat_settings"` + Close string `yaml:"close"` + } + + RemindEditText struct { + TitleToday string `yaml:"title_today"` + TitleSoon string `yaml:"title_soon"` + } + + JamaatMenuText struct { + TitleEnabled string `yaml:"title_enabled"` + TitleDisabled string `yaml:"title_disabled"` + Enable string `yaml:"enable"` + Disable string `yaml:"disable"` + } + + JamaatEditText struct { + Title string `yaml:"title"` + } + + ButtonsText struct { + Save string `yaml:"save"` + Back string `yaml:"back"` + } + Text struct { Name string `yaml:"name"` @@ -37,6 +69,12 @@ type ( Remind InteractiveMessage `yaml:"remind"` Language InteractiveMessage `yaml:"language"` + RemindMenu RemindMenuText `yaml:"remind_menu"` + RemindEdit RemindEditText `yaml:"remind_edit"` + JamaatMenu JamaatMenuText `yaml:"jamaat_menu"` + JamaatEdit JamaatEditText `yaml:"jamaat_edit"` + Buttons ButtonsText `yaml:"buttons"` + SubscriptionSuccess string `yaml:"subscription_success"` UnsubscriptionSuccess string `yaml:"unsubscription_success"` diff --git a/serverless/dispatcher/internal/handler/languages/ar.yaml b/serverless/dispatcher/internal/handler/languages/ar.yaml index ccd9913..b1fd6c6 100644 --- a/serverless/dispatcher/internal/handler/languages/ar.yaml +++ b/serverless/dispatcher/internal/handler/languages/ar.yaml @@ -42,29 +42,51 @@ language: start: "🌐 اختر لغتك المفضلة" success: "✅ تم تغيير اللغة إلى *%s*" -subscription_success: "✅ تم الاشتراك في تذكيرات الصلاة" -unsubscription_success: "✅ تم إلغاء الاشتراك في تذكيرات الصلاة" +remind_menu: + title_enabled: "⚙️ إعدادات التذكير - مفعّل" + title_disabled: "⚙️ إعدادات التذكير - معطّل" + enable: "🔔 تفعيل" + disable: "🔕 تعطيل" + today: "⏰ اليوم" + soon: "🔜 قريباً" + jamaat_settings: "🕌 إعدادات الجماعة" + close: "❌ إغلاق" + +remind_edit: + title_today: "⏰ اليوم" + title_soon: "🔜 قريباً" + +jamaat_menu: + title_enabled: "🕌 إعدادات الجماعة - مفعّل" + title_disabled: "🕌 إعدادات الجماعة - معطّل" + enable: "🔔 تفعيل" + disable: "🔕 تعطيل" + +jamaat_edit: + title: "تأخير جماعة %s" + +buttons: + save: "💾" + back: "🔙" help: | هل تحتاج مساعدة؟ 😊 إليك ما يمكنني فعله: 👇 - + *الصلاة:* - ⏰ /today – مواقيت اليوم - 📅 /date – مواقيت يوم محدد - ⏳ /next – متى الصلاة القادمة؟ - 📨 /remind – ضبط تذكير قبل الصلاة - 🔔 /subscribe – استقبال تذكيرات يومية - 🔕 /unsubscribe – إيقاف التذكيرات اليومية + ⏰ /today – مواقيت اليوم + 📅 /date – مواقيت يوم محدد + ⏳ /next – متى الصلاة القادمة؟ + 📨 /remind – إعدادات التذكير *الإعدادات:* - 🌐 /language – تغيير اللغة + 🌐 /language – تغيير اللغة *الدعم:* - 📖 /help – عرض هذا الدليل - 🐞 /bug – الإبلاغ عن مشكلة + 📖 /help – عرض هذا الدليل + 🐞 /bug – الإبلاغ عن مشكلة 💬 /feedback – إرسال ملاحظات feedback: @@ -90,13 +112,13 @@ announce: stats: | 📊 إحصائيات البوت: - - • عدد المستخدمين: %d - • المشتركين: %d - • غير المشتركين: %d + + • عدد المستخدمين: %d + • المشتركين: %d + • غير المشتركين: %d 🌍 اللغات المستخدمة: - + %s cancel: "✅ تم إلغاء العملية" diff --git a/serverless/dispatcher/internal/handler/languages/en.yaml b/serverless/dispatcher/internal/handler/languages/en.yaml index 9441938..f92a37a 100644 --- a/serverless/dispatcher/internal/handler/languages/en.yaml +++ b/serverless/dispatcher/internal/handler/languages/en.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Pick your preferred language" success: "✅ Language changed to *%s*" -subscription_success: "✅ You’re now subscribed to prayer reminders" -unsubscription_success: "✅ You’ve unsubscribed from prayer reminders" +remind_menu: + title_enabled: "⚙️ Configure Reminders - Enabled" + title_disabled: "⚙️ Configure Reminders - Disabled" + enable: "🔔 Enable" + disable: "🔕 Disable" + today: "⏰ Today" + soon: "🔜 Soon" + jamaat_settings: "🕌 Jamaat Settings" + close: "❌ Close" + +remind_edit: + title_today: "⏰ Today" + title_soon: "🔜 Soon" + +jamaat_menu: + title_enabled: "🕌 Jamaat Settings - Enabled" + title_disabled: "🕌 Jamaat Settings - Disabled" + enable: "🔔 Enable" + disable: "🔕 Disable" + +jamaat_edit: + title: "%s Jamaat Delay" + +buttons: + save: "💾" + back: "🔙" help: | - Need help? 😊 Here’s what I can do: 👇 - + Need help? 😊 Here's what I can do: 👇 + *Prayers:* - ⏰ /today – Today’s times - 📅 /date – Times for a chosen day - ⏳ /next – When’s the next prayer? - 📨 /remind – Set a reminder before prayer - 🔔 /subscribe – Get daily reminders - 🔕 /unsubscribe – Stop daily reminders + ⏰ /today – Today's times + 📅 /date – Times for a chosen day + ⏳ /next – When's the next prayer? + 📨 /remind – Configure reminders *Settings:* - 🌐 /language – Change language + 🌐 /language – Change language *Support:* - 📖 /help – Show this guide - 🐞 /bug – Report a problem + 📖 /help – Show this guide + 🐞 /bug – Report a problem 💬 /feedback – Send feedback feedback: @@ -90,13 +112,13 @@ announce: stats: | 📊 Bot Stats: - - • Users: %d - • Subscribed: %d - • Unsubscribed: %d + + • Users: %d + • Subscribed: %d + • Unsubscribed: %d 🌍 Languages used: - + %s cancel: "✅ Process cancelled" diff --git a/serverless/dispatcher/internal/handler/languages/es.yaml b/serverless/dispatcher/internal/handler/languages/es.yaml index f1a6b19..e75713d 100644 --- a/serverless/dispatcher/internal/handler/languages/es.yaml +++ b/serverless/dispatcher/internal/handler/languages/es.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Elige tu idioma preferido" success: "✅ Idioma cambiado a *%s*" -subscription_success: "✅ Te has suscrito a los recordatorios de oración" -unsubscription_success: "✅ Te has dado de baja de los recordatorios de oración" +remind_menu: + title_enabled: "⚙️ Configurar Recordatorios - Habilitado" + title_disabled: "⚙️ Configurar Recordatorios - Deshabilitado" + enable: "🔔 Habilitar" + disable: "🔕 Deshabilitar" + today: "⏰ Hoy" + soon: "🔜 Pronto" + jamaat_settings: "🕌 Configuración de Jamaat" + close: "❌ Cerrar" + +remind_edit: + title_today: "⏰ Hoy" + title_soon: "🔜 Pronto" + +jamaat_menu: + title_enabled: "🕌 Configuración de Jamaat - Habilitado" + title_disabled: "🕌 Configuración de Jamaat - Deshabilitado" + enable: "🔔 Habilitar" + disable: "🔕 Deshabilitar" + +jamaat_edit: + title: "Retraso de Jamaat %s" + +buttons: + save: "💾" + back: "🔙" help: | ¿Necesitas ayuda? 😊 Aquí está lo que puedo hacer: 👇 - + *Oraciones:* - ⏰ /today – Horarios de hoy - 📅 /date – Horarios de un día específico - ⏳ /next – ¿Cuándo es la próxima oración? - 📨 /remind – Configura un recordatorio antes de la oración - 🔔 /subscribe – Recibir recordatorios diarios - 🔕 /unsubscribe – Detener recordatorios diarios + ⏰ /today – Horarios de hoy + 📅 /date – Horarios de un día específico + ⏳ /next – ¿Cuándo es la próxima oración? + 📨 /remind – Configurar recordatorios *Configuraciones:* - 🌐 /language – Cambiar idioma + 🌐 /language – Cambiar idioma *Soporte:* - 📖 /help – Mostrar esta guía - 🐞 /bug – Reportar un problema + 📖 /help – Mostrar esta guía + 🐞 /bug – Reportar un problema 💬 /feedback – Enviar retroalimentación feedback: @@ -90,13 +112,13 @@ announce: stats: | 📊 Estadísticas del bot: - - • Usuarios: %d - • Suscritos: %d - • No suscritos: %d + + • Usuarios: %d + • Suscritos: %d + • No suscritos: %d 🌍 Idiomas usados: - + %s cancel: "✅ Proceso cancelado" diff --git a/serverless/dispatcher/internal/handler/languages/fr.yaml b/serverless/dispatcher/internal/handler/languages/fr.yaml index b99e64b..59e3b28 100644 --- a/serverless/dispatcher/internal/handler/languages/fr.yaml +++ b/serverless/dispatcher/internal/handler/languages/fr.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Choisissez votre langue préférée" success: "✅ Langue changée en *%s*" -subscription_success: "✅ Vous êtes maintenant abonné aux rappels de prière" -unsubscription_success: "✅ Vous vous êtes désabonné des rappels de prière" +remind_menu: + title_enabled: "⚙️ Configurer les Rappels - Activé" + title_disabled: "⚙️ Configurer les Rappels - Désactivé" + enable: "🔔 Activer" + disable: "🔕 Désactiver" + today: "⏰ Aujourd'hui" + soon: "🔜 Bientôt" + jamaat_settings: "🕌 Paramètres Jamaat" + close: "❌ Fermer" + +remind_edit: + title_today: "⏰ Aujourd'hui" + title_soon: "🔜 Bientôt" + +jamaat_menu: + title_enabled: "🕌 Paramètres Jamaat - Activé" + title_disabled: "🕌 Paramètres Jamaat - Désactivé" + enable: "🔔 Activer" + disable: "🔕 Désactiver" + +jamaat_edit: + title: "Délai Jamaat %s" + +buttons: + save: "💾" + back: "🔙" help: | Besoin d'aide ? 😊 Voici ce que je peux faire : 👇 - + *Prières:* - ⏰ /today – Horaires d'aujourd'hui - 📅 /date – Horaires pour une date choisie - ⏳ /next – Quand est la prochaine prière ? - 📨 /remind – Définir un rappel avant la prière - 🔔 /subscribe – Recevoir des rappels quotidiens - 🔕 /unsubscribe – Arrêter les rappels quotidiens + ⏰ /today – Horaires d'aujourd'hui + 📅 /date – Horaires pour une date choisie + ⏳ /next – Quand est la prochaine prière ? + 📨 /remind – Configurer les rappels *Paramètres:* - 🌐 /language – Changer de langue + 🌐 /language – Changer de langue *Support:* - 📖 /help – Afficher ce guide - 🐞 /bug – Signaler un problème + 📖 /help – Afficher ce guide + 🐞 /bug – Signaler un problème 💬 /feedback – Envoyer un retour feedback: @@ -91,9 +113,9 @@ announce: stats: | 📊 Statistiques du bot: - • Utilisateurs: %d - • Abonnés: %d - • Désabonnés: %d + • Utilisateurs: %d + • Abonnés: %d + • Désabonnés: %d 🌍 Langues utilisées: diff --git a/serverless/dispatcher/internal/handler/languages/ru.yaml b/serverless/dispatcher/internal/handler/languages/ru.yaml index f69a49b..19f2d3a 100644 --- a/serverless/dispatcher/internal/handler/languages/ru.yaml +++ b/serverless/dispatcher/internal/handler/languages/ru.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Выберите предпочтительный язык" success: "✅ Язык изменён на *%s*" -subscription_success: "✅ Вы подписались на напоминания о молитвах" -unsubscription_success: "✅ Вы отписались от напоминаний о молитвах" +remind_menu: + title_enabled: "⚙️ Настройка Напоминаний - Включено" + title_disabled: "⚙️ Настройка Напоминаний - Выключено" + enable: "🔔 Включить" + disable: "🔕 Выключить" + today: "⏰ Сегодня" + soon: "🔜 Скоро" + jamaat_settings: "🕌 Настройки Джамаата" + close: "❌ Закрыть" + +remind_edit: + title_today: "⏰ Сегодня" + title_soon: "🔜 Скоро" + +jamaat_menu: + title_enabled: "🕌 Настройки Джамаата - Включено" + title_disabled: "🕌 Настройки Джамаата - Выключено" + enable: "🔔 Включить" + disable: "🔕 Выключить" + +jamaat_edit: + title: "Задержка Джамаата %s" + +buttons: + save: "💾" + back: "🔙" help: | Нужна помощь? 😊 Вот что я умею: 👇 - + *Молитвы:* - ⏰ /today – Время молитв на сегодня - 📅 /date – Время молитв на выбранный день - ⏳ /next – Когда следующая молитва? - 📨 /remind – Установить напоминание - 🔔 /subscribe – Подписаться на ежедневные напоминания - 🔕 /unsubscribe – Отключить напоминания + ⏰ /today – Время молитв на сегодня + 📅 /date – Время молитв на выбранный день + ⏳ /next – Когда следующая молитва? + 📨 /remind – Настроить напоминания *Настройки:* - 🌐 /language – Изменить язык + 🌐 /language – Изменить язык *Поддержка:* - 📖 /help – Показать эту инструкцию - 🐞 /bug – Сообщить о проблеме + 📖 /help – Показать эту инструкцию + 🐞 /bug – Сообщить о проблеме 💬 /feedback – Оставить отзыв feedback: @@ -91,9 +113,9 @@ announce: stats: | 📊 Статистика бота: - • Пользователей: %d - • Подписчиков: %d - • Отписавшихся: %d + • Пользователей: %d + • Подписчиков: %d + • Отписавшихся: %d 🌍 Используемые языки: diff --git a/serverless/dispatcher/internal/handler/languages/tr.yaml b/serverless/dispatcher/internal/handler/languages/tr.yaml index 07f94a3..4b928bc 100644 --- a/serverless/dispatcher/internal/handler/languages/tr.yaml +++ b/serverless/dispatcher/internal/handler/languages/tr.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Halaýan diliňizi saýlaň" success: "✅ Dil *%s* diýip üýtgedildi" -subscription_success: "✅ Namaz duýduryşlaryna abuna boldyňyz" -unsubscription_success: "✅ Namaz duýduryşlaryna abuna bolmagyňyz bes edildi" +remind_menu: + title_enabled: "⚙️ Duýduryşlary Sazlamak - Açyk" + title_disabled: "⚙️ Duýduryşlary Sazlamak - Ýapyk" + enable: "🔔 Açmak" + disable: "🔕 Ýapmak" + today: "⏰ Şu gün" + soon: "🔜 Tiz wagtda" + jamaat_settings: "🕌 Jemgyýet Sazlamalary" + close: "❌ Ýapmak" + +remind_edit: + title_today: "⏰ Şu gün" + title_soon: "🔜 Tiz wagtda" + +jamaat_menu: + title_enabled: "🕌 Jemgyýet Sazlamalary - Açyk" + title_disabled: "🕌 Jemgyýet Sazlamalary - Ýapyk" + enable: "🔔 Açmak" + disable: "🔕 Ýapmak" + +jamaat_edit: + title: "%s Jemgyýet Gijikdirmesi" + +buttons: + save: "💾" + back: "🔙" help: | Kömek gerekmi? 😊 Men şulary edip bilýärin: 👇 - + *Namazlar:* - ⏰ /today – Şu günki wagtlar - 📅 /date – Saýlanan gün üçin wagtlar - ⏳ /next – Indiki namaz haçan? - 📨 /remind – Namazdan öň duýduryş goýmak - 🔔 /subscribe – Her gün duýduryş almak - 🔕 /unsubscribe – Duýduryşlary bes etmek + ⏰ /today – Şu günki wagtlar + 📅 /date – Saýlanan gün üçin wagtlar + ⏳ /next – Indiki namaz haçan? + 📨 /remind – Duýduryşlary sazlamak *Sazlamalar:* - 🌐 /language – Dili üýtgetmek + 🌐 /language – Dili üýtgetmek *Goldaw:* - 📖 /help – Bu görkezmäni görkez - 🐞 /bug – Nädogrulygy habar ber + 📖 /help – Bu görkezmäni görkez + 🐞 /bug – Nädogrulygy habar ber 💬 /feedback – Pikiriňizi iberiň feedback: @@ -91,9 +113,9 @@ announce: stats: | 📊 Bot statistikalary: - • Ulanyjylar: %d - • Abuna bolanlar: %d - • Abuna bolmadyklar: %d + • Ulanyjylar: %d + • Abuna bolanlar: %d + • Abuna bolmadyklar: %d 🌍 Ulanylýan diller: diff --git a/serverless/dispatcher/internal/handler/languages/tt.yaml b/serverless/dispatcher/internal/handler/languages/tt.yaml index d58126a..f3f1e68 100644 --- a/serverless/dispatcher/internal/handler/languages/tt.yaml +++ b/serverless/dispatcher/internal/handler/languages/tt.yaml @@ -42,29 +42,51 @@ language: start: "🌐 Тел сайлагыз" success: "✅ Тел *%s* итеп үзгәртелде" -subscription_success: "✅ Намаз хәтерләтүләренә язылдыгыз" -unsubscription_success: "✅ Намаз хәтерләтүләреннән язылдыгыз" +remind_menu: + title_enabled: "⚙️ Хәтерләтүләрне Көйләү - Кабызылган" + title_disabled: "⚙️ Хәтерләтүләрне Көйләү - Сүндерелгән" + enable: "🔔 Кабызу" + disable: "🔕 Сүндерү" + today: "⏰ Бүген" + soon: "🔜 Тиздән" + jamaat_settings: "🕌 Җәмәгать Көйләүләре" + close: "❌ Ябу" + +remind_edit: + title_today: "⏰ Бүген" + title_soon: "🔜 Тиздән" + +jamaat_menu: + title_enabled: "🕌 Җәмәгать Көйләүләре - Кабызылган" + title_disabled: "🕌 Җәмәгать Көйләүләре - Сүндерелгән" + enable: "🔔 Кабызу" + disable: "🔕 Сүндерү" + +jamaat_edit: + title: "%s Җәмәгать Тоткарлыгы" + +buttons: + save: "💾" + back: "🔙" help: | Ярдәм кирәкме? 😊 Менә нәрсәләр эшли алам: 👇 - + *Намазлар:* - ⏰ /today – Бүгенге вакытлар - 📅 /date – Күрсәтелгән көн өчен вакытлар - ⏳ /next – Киләсе намаз кайчан? - 📨 /remind – Намаз алдыннан хәтерләтү куярга - 🔔 /subscribe – Көндәлек хәтерләтүләрне кабул итү - 🔕 /unsubscribe – Хәтерләтүләрне сүндерү + ⏰ /today – Бүгенге вакытлар + 📅 /date – Күрсәтелгән көн өчен вакытлар + ⏳ /next – Киләсе намаз кайчан? + 📨 /remind – Хәтерләтүләрне көйләргә *Көйләүләр:* - 🌐 /language – Телне үзгәртү + 🌐 /language – Телне үзгәртү *Ярдәм:* - 📖 /help – Бу кулланманы күрсәтү - 🐞 /bug – Хата турында хәбәр итү + 📖 /help – Бу кулланманы күрсәтү + 🐞 /bug – Хата турында хәбәр итү 💬 /feedback – Фикер җибәрү feedback: @@ -91,9 +113,9 @@ announce: stats: | 📊 Бот статистикасы: - • Кулланучылар саны: %d - • Язылганнар саны: %d - • Язылудан чыкканнар саны: %d + • Кулланучылар саны: %d + • Язылганнар саны: %d + • Язылудан чыкканнар саны: %d 🌍 Кулланылган телләр: diff --git a/serverless/dispatcher/internal/handler/languages/uz.yaml b/serverless/dispatcher/internal/handler/languages/uz.yaml index a451df7..8852050 100644 --- a/serverless/dispatcher/internal/handler/languages/uz.yaml +++ b/serverless/dispatcher/internal/handler/languages/uz.yaml @@ -42,29 +42,52 @@ language: start: "🌐 O‘zingizga qulay tilni tanlang" success: "✅ Til *%s* ga o‘zgartirildi" -subscription_success: "✅ Namoz eslatmalariga obuna bo‘ldingiz" -unsubscription_success: "✅ Namoz eslatmalaridan obuna bekor qilindi" + +remind_menu: + title_enabled: "⚙️ Eslatmalarni Sozlash - Yoqilgan" + title_disabled: "⚙️ Eslatmalarni Sozlash - O'chirilgan" + enable: "🔔 Yoqish" + disable: "🔕 O'chirish" + today: "⏰ Bugun" + soon: "🔜 Tez orada" + jamaat_settings: "🕌 Jamoat Sozlamalari" + close: "❌ Yopish" + +remind_edit: + title_today: "⏰ Bugun" + title_soon: "🔜 Tez orada" + +jamaat_menu: + title_enabled: "🕌 Jamoat Sozlamalari - Yoqilgan" + title_disabled: "🕌 Jamoat Sozlamalari - O'chirilgan" + enable: "🔔 Yoqish" + disable: "🔕 O'chirish" + +jamaat_edit: + title: "%s Jamoat Kechikishi" + +buttons: + save: "💾" + back: "🔙" help: | Yordam kerakmi? 😊 Men quyidagilarni bajara olaman: 👇 - + *Namozlar:* - ⏰ /today – Bugungi vaqtlar - 📅 /date – Tanlangan kun uchun vaqtlar - ⏳ /next – Keyingi namoz qachon? - 📨 /remind – Namozdan oldin eslatma o‘rnatish - 🔔 /subscribe – Har kungi eslatmalarni olish - 🔕 /unsubscribe – Eslatmalarni to‘xtatish + ⏰ /today – Bugungi vaqtlar + 📅 /date – Tanlangan kun uchun vaqtlar + ⏳ /next – Keyingi namoz qachon? + 📨 /remind – Eslatmalarni sozlash *Sozlamalar:* - 🌐 /language – Tilni o‘zgartirish + 🌐 /language – Tilni o‘zgartirish *Yordam:* - 📖 /help – Ushbu qo‘llanmani ko‘rsatish - 🐞 /bug – Xatolik haqida xabar berish + 📖 /help – Ushbu qo‘llanmani ko‘rsatish + 🐞 /bug – Xatolik haqida xabar berish 💬 /feedback – Fikr-mulohaza yuborish feedback: @@ -91,9 +114,9 @@ announce: stats: | 📊 Bot statistikasi: - • Foydalanuvchilar: %d - • Obuna bo‘lganlar: %d - • Obunani bekor qilganlar: %d + • Foydalanuvchilar: %d + • Obuna bo‘lganlar: %d + • Obunani bekor qilganlar: %d 🌍 Foydalanilayotgan tillar: diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index ce93ab4..5fbd467 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -24,12 +24,46 @@ const ( monthQuery query = "date:month|" remindQuery query = "remind|" languageQuery query = "language|" + + remindMenuQuery query = "remind:menu|" + remindToggleQuery query = "remind:toggle|" + remindEditQuery query = "remind:edit:" + remindAdjustQuery query = "remind:adjust:" + remindSaveQuery query = "remind:save:" + remindJamaatMenuQuery query = "remind:jamaat:menu|" + remindJamaatToggleQuery query = "remind:jamaat:toggle|" + remindJamaatEditQuery query = "remind:jamaat:edit:" + remindJamaatAdjustQuery query = "remind:jamaat:adjust:" + remindJamaatSaveQuery query = "remind:jamaat:save:" + remindBackQuery query = "remind:back:" + remindCloseQuery query = "remind:close|" +) + +const ( + TodayMinOffset = 0 * time.Minute + TodayMaxOffset = 6 * time.Hour + + SoonMinOffset = 5 * time.Minute + SoonMaxOffset = 1 * time.Hour + + JamaatMinDelay = 5 * time.Minute + JamaatMaxDelay = 6 * time.Hour ) func (q query) String() string { return string(q) } +func clampOffset(value, min, max time.Duration) time.Duration { + if value < min { + return min + } + if value > max { + return max + } + return value +} + func (h *Handler) emptyQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{ CallbackQueryID: update.CallbackQuery.ID, @@ -174,3 +208,404 @@ func (h *Handler) languageQuery(ctx context.Context, b *bot.Bot, update *models. h.resetState(ctx, chat) return nil } + +func (h *Handler) remindMenuQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + var messageText string + if chat.Subscribed { + messageText = text.RemindMenu.TitleEnabled + } else { + messageText = text.RemindMenu.TitleDisabled + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.remindMenuKeyboard(chat), + }) + if err != nil { + log.Error("remindMenuQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindMenuQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindToggleQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + + newSubscribed := !chat.Subscribed + err := h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, newSubscribed) + if err != nil { + log.Error("remindToggleQuery: set subscribed", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + chat.Subscribed = newSubscribed + ctx = setContextChat(ctx, chat) + + h.remindMenuQuery(ctx, b, update) + return nil +} + +func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + parts := strings.Split(update.CallbackQuery.Data, ":") + if len(parts) < 3 { + log.Error("remindEditQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + reminderType := domain.ReminderType(parts[2]) + + var messageText string + var offset time.Duration + switch reminderType { + case domain.ReminderTypeToday: + offset = chat.Reminder.Today.Offset + messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleToday, domain.FormatDuration(offset)) + case domain.ReminderTypeSoon: + offset = chat.Reminder.Soon.Offset + messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleSoon, domain.FormatDuration(offset)) + default: + log.Error("remindEditQuery: unknown reminder type", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.remindEditKeyboard(reminderType, chat.LanguageCode), + }) + if err != nil { + log.Error("remindEditQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindEditQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + + parts := strings.Split(update.CallbackQuery.Data, ":") + if len(parts) < 4 { + log.Error("remindAdjustQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + reminderType := domain.ReminderType(parts[2]) + adjustment := parseAdjustment(strings.TrimSuffix(parts[3], "|")) + + var currentOffset time.Duration + var minOffset, maxOffset time.Duration + + switch reminderType { + case domain.ReminderTypeToday: + currentOffset = chat.Reminder.Today.Offset + minOffset = TodayMinOffset + maxOffset = TodayMaxOffset + case domain.ReminderTypeSoon: + currentOffset = chat.Reminder.Soon.Offset + minOffset = SoonMinOffset + maxOffset = SoonMaxOffset + default: + log.Error("remindAdjustQuery: unknown reminder type", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + newOffset := clampOffset(currentOffset+adjustment, minOffset, maxOffset) + + // If value didn't change (hit limit), just answer callback and return + if newOffset == currentOffset { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil + } + + err := h.db.SetReminderOffset(ctx, chat.BotID, chat.ChatID, reminderType, newOffset) + if err != nil { + log.Error("remindAdjustQuery: set reminder offset", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + switch reminderType { + case domain.ReminderTypeToday: + chat.Reminder.Today.Offset = newOffset + case domain.ReminderTypeSoon: + chat.Reminder.Soon.Offset = newOffset + } + + ctx = setContextChat(ctx, chat) + return h.remindEditQuery(ctx, b, update) +} + +func (h *Handler) remindSaveQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + var messageText string + if chat.Subscribed { + messageText = text.RemindMenu.TitleEnabled + } else { + messageText = text.RemindMenu.TitleDisabled + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.remindMenuKeyboard(chat), + }) + if err != nil { + log.Error("remindSaveQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindSaveQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindJamaatMenuQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + var messageText string + if chat.Reminder.Jamaat.Enabled { + messageText = text.JamaatMenu.TitleEnabled + } else { + messageText = text.JamaatMenu.TitleDisabled + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.jammatMenuKeyboard(chat), + }) + if err != nil { + log.Error("remindJamaatMenuQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindJamaatMenuQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindJamaatToggleQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + + newEnabled := !chat.Reminder.Jamaat.Enabled + err := h.db.SetJamaatEnabled(ctx, chat.BotID, chat.ChatID, newEnabled) + if err != nil { + log.Error("remindJamaatToggleQuery: set jamaat enabled", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + chat.Reminder.Jamaat.Enabled = newEnabled + ctx = setContextChat(ctx, chat) + return h.remindJamaatMenuQuery(ctx, b, update) +} + +func (h *Handler) remindJamaatEditQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + parts := strings.Split(update.CallbackQuery.Data, ":") + if len(parts) < 4 { + log.Error("remindJamaatEditQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + prayerName := strings.TrimSuffix(parts[3], "|") + prayerID := domain.ParsePrayerID(prayerName) + + delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) + messageText := fmt.Sprintf(text.JamaatEdit.Title, text.Prayer[int(prayerID)]) + " - " + domain.FormatDuration(delay) + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.jammatEditKeyboard(prayerID, chat.LanguageCode), + }) + if err != nil { + log.Error("remindJamaatEditQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindJamaatEditQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindJamaatAdjustQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + + parts := strings.Split(update.CallbackQuery.Data, ":") + if len(parts) < 5 { + log.Error("remindJamaatAdjustQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + prayerName := parts[3] + prayerID := domain.ParsePrayerID(prayerName) + adjustment := parseAdjustment(strings.TrimSuffix(parts[4], "|")) + + currentDelay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) + newDelay := clampOffset(currentDelay+adjustment, JamaatMinDelay, JamaatMaxDelay) + + // If value didn't change (hit limit), just answer callback and return + if newDelay == currentDelay { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil + } + + err := h.db.SetJamaatDelay(ctx, chat.BotID, chat.ChatID, prayerID, newDelay) + if err != nil { + log.Error("remindJamaatAdjustQuery: set jamaat delay", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + chat.Reminder.Jamaat.Delay.SetDelayByPrayerID(prayerID, newDelay) + ctx = setContextChat(ctx, chat) + return h.remindJamaatEditQuery(ctx, b, update) +} + +func (h *Handler) remindJamaatSaveQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + var messageText string + if chat.Reminder.Jamaat.Enabled { + messageText = text.JamaatMenu.TitleEnabled + } else { + messageText = text.JamaatMenu.TitleDisabled + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: h.jammatMenuKeyboard(chat), + }) + if err != nil { + log.Error("remindJamaatSaveQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindJamaatSaveQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindBackQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + parts := strings.Split(update.CallbackQuery.Data, ":") + if len(parts) < 3 { + log.Error("remindBackQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + destination := strings.TrimSuffix(parts[2], "|") + + var messageText string + var keyboard *models.InlineKeyboardMarkup + + switch destination { + case "menu": + if chat.Subscribed { + messageText = text.RemindMenu.TitleEnabled + } else { + messageText = text.RemindMenu.TitleDisabled + } + keyboard = h.remindMenuKeyboard(chat) + case "jamaat": + if chat.Reminder.Jamaat.Enabled { + messageText = text.JamaatMenu.TitleEnabled + } else { + messageText = text.JamaatMenu.TitleDisabled + } + keyboard = h.jammatMenuKeyboard(chat) + default: + log.Error("remindBackQuery: unknown destination", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + Text: messageText, + ReplyMarkup: keyboard, + }) + if err != nil { + log.Error("remindBackQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindBackQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} + +func (h *Handler) remindCloseQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { + chat := getContextChat(ctx) + + _, err := b.DeleteMessage(ctx, &bot.DeleteMessageParams{ + ChatID: chat.ChatID, + MessageID: update.CallbackQuery.Message.Message.ID, + }) + if err != nil { + log.Error("remindCloseQuery: delete message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + if err != nil { + log.Error("remindCloseQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + return nil +} diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index a121eaa..6f1b89d 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -102,7 +102,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom DECLARE $bot_id AS Int64; DECLARE $chat_id AS int64; - SELECT bot_id, chat_id, state, language_code, reminder + SELECT bot_id, chat_id, state, language_code, subscribed, reminder FROM chats WHERE bot_id = $bot_id AND chat_id = $chat_id; ` @@ -128,6 +128,7 @@ func (db *DB) GetChat(ctx context.Context, botID int64, chatID int64) (chat *dom &chat.ChatID, &chat.State, &chat.LanguageCode, + &chat.Subscribed, &reminderJSON, ) if err != nil { @@ -160,7 +161,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, query := ` DECLARE $bot_id AS Int64; - SELECT bot_id, chat_id, state, language_code, reminder + SELECT bot_id, chat_id, state, language_code, subscribed, reminder FROM chats WHERE bot_id = $bot_id; ` @@ -183,6 +184,7 @@ func (db *DB) GetChats(ctx context.Context, botID int64) (chats []*domain.Chat, &chat.ChatID, &chat.State, &chat.LanguageCode, + &chat.Subscribed, &reminderJSON, ) if err != nil { @@ -270,89 +272,26 @@ func (db *DB) SetSubscribed(ctx context.Context, botID int64, chatID int64, subs } func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error { - err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { - txBegin := table.TxControl(table.BeginTx(table.WithSerializableReadWrite())) - - selectQuery := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - - SELECT reminder - FROM chats - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` - - selectParams := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$chat_id", types.Int64Value(chatID)), - ) - - txID, res, err := s.Execute(ctx, txBegin, selectQuery, selectParams) - if err != nil { - log.Error("execute select query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) - return domain.ErrInternal - } - - defer func(res result.Result) { _ = res.Close() }(res) - - var reminderJSON string - if res.NextResultSet(ctx) && res.NextRow() { - err = res.ScanWithDefaults(&reminderJSON) - if err != nil { - log.Error("scan reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) - return domain.ErrInternal - } - } else { - return domain.ErrNotFound - } - - var reminder domain.Reminder - if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { - log.Error("unmarshal reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) - return domain.ErrUnmarshalJSON - } - + return db.updateReminder(ctx, botID, chatID, func(reminder *domain.Reminder) { switch reminderType { case domain.ReminderTypeToday: reminder.Today.Offset = offset case domain.ReminderTypeSoon: reminder.Soon.Offset = offset - case domain.ReminderTypeArrive: - reminder.Arrive.Offset = offset - } - - updatedReminderJSON, err := json.Marshal(&reminder) - if err != nil { - log.Error("marshal updated reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) - return domain.ErrInternal } + }) +} - updateQuery := ` - DECLARE $bot_id AS Int64; - DECLARE $chat_id AS Int64; - DECLARE $reminder AS Json; - - UPDATE chats - SET reminder = $reminder - WHERE bot_id = $bot_id AND chat_id = $chat_id; - ` - - updateParams := table.NewQueryParameters( - table.ValueParam("$bot_id", types.Int64Value(botID)), - table.ValueParam("$chat_id", types.Int64Value(chatID)), - table.ValueParam("$reminder", types.JSONValue(string(updatedReminderJSON))), - ) - - txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) - _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) - if err != nil { - log.Error("execute update reminder query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) - return domain.ErrInternal - } - return nil +func (db *DB) SetJamaatEnabled(ctx context.Context, botID int64, chatID int64, enabled bool) error { + return db.updateReminder(ctx, botID, chatID, func(reminder *domain.Reminder) { + reminder.Jamaat.Enabled = enabled }) +} - return err +func (db *DB) SetJamaatDelay(ctx context.Context, botID int64, chatID int64, prayerID domain.PrayerID, delay time.Duration) error { + return db.updateReminder(ctx, botID, chatID, func(reminder *domain.Reminder) { + reminder.Jamaat.Delay.SetDelayByPrayerID(prayerID, delay) + }) } func (db *DB) SetState(ctx context.Context, botID int64, chatID int64, state string) error { @@ -521,3 +460,82 @@ func (db *DB) GetStats(ctx context.Context, botID int64) (*domain.Stats, error) return stats, nil } + +func (db *DB) updateReminder(ctx context.Context, botID int64, chatID int64, mutate func(*domain.Reminder)) error { + err := db.client.Do(ctx, func(ctx context.Context, s table.Session) error { + txBegin := table.TxControl(table.BeginTx(table.WithSerializableReadWrite())) + + selectQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + + SELECT reminder + FROM chats + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + selectParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), + ) + + txID, res, err := s.Execute(ctx, txBegin, selectQuery, selectParams) + if err != nil { + log.Error("execute select query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + + defer func(res result.Result) { _ = res.Close() }(res) + + var reminderJSON string + if res.NextResultSet(ctx) && res.NextRow() { + err = res.ScanWithDefaults(&reminderJSON) + if err != nil { + log.Error("scan reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + } else { + return domain.ErrNotFound + } + + var reminder domain.Reminder + if err := json.Unmarshal([]byte(reminderJSON), &reminder); err != nil { + log.Error("unmarshal reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrUnmarshalJSON + } + + mutate(&reminder) + + updatedReminderJSON, err := json.Marshal(&reminder) + if err != nil { + log.Error("marshal updated reminder json", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + + updateQuery := ` + DECLARE $bot_id AS Int64; + DECLARE $chat_id AS Int64; + DECLARE $reminder AS Json; + + UPDATE chats + SET reminder = $reminder + WHERE bot_id = $bot_id AND chat_id = $chat_id; + ` + + updateParams := table.NewQueryParameters( + table.ValueParam("$bot_id", types.Int64Value(botID)), + table.ValueParam("$chat_id", types.Int64Value(chatID)), + table.ValueParam("$reminder", types.JSONValue(string(updatedReminderJSON))), + ) + + txCommit := table.TxControl(table.WithTx(txID), table.CommitTx()) + _, _, err = s.Execute(ctx, txCommit, updateQuery, updateParams) + if err != nil { + log.Error("execute update reminder query", log.Err(err), log.BotID(botID), log.ChatID(chatID)) + return domain.ErrInternal + } + return nil + }) + + return err +} diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index f002712..2638f73 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a + github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index a8208fa..05d2230 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -575,6 +575,12 @@ github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+q github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf h1:OLDTjLdG0mNigMk9sNNh+fJ5LBWkYboNKu2pFX61WSo= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRcfFQVjEteZWTaLOkyS6B5OlrKr8CmroC4= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 875fce7..a8086c4 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a + github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index 081c272..4214602 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -573,6 +573,12 @@ github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+q github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf h1:OLDTjLdG0mNigMk9sNNh+fJ5LBWkYboNKu2pFX61WSo= +github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRcfFQVjEteZWTaLOkyS6B5OlrKr8CmroC4= +github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= +github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From b5254b301c89e5463fa8ac828f7ad16a4ff65429 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:19:02 +0300 Subject: [PATCH 41/59] feat: update domain --- domain/reminder.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/domain/reminder.go b/domain/reminder.go index 551bcc7..2d14072 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -35,10 +35,10 @@ type ( } Reminder struct { - Today *ReminderConfig `json:"today"` - Soon *ReminderConfig `json:"soon"` - Arrive *ReminderConfig `json:"arrive"` - Jamaat *JamaatConfig `json:"jamaat"` + Tomorrow *ReminderConfig `json:"tomorrow"` + Soon *ReminderConfig `json:"soon"` + Arrive *ReminderConfig `json:"arrive"` + Jamaat *JamaatConfig `json:"jamaat"` } ) From f09b49eedd92030e24507cc6c6c7a053ff7ed10e Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:27:53 +0300 Subject: [PATCH 42/59] feat: update domain --- domain/reminder.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/domain/reminder.go b/domain/reminder.go index 2d14072..56c69a0 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -5,8 +5,9 @@ import "time" type ReminderType string const ( - ReminderTypeToday ReminderType = "today" - ReminderTypeSoon ReminderType = "soon" + ReminderTypeTomorrow ReminderType = "tomorrow" + ReminderTypeSoon ReminderType = "soon" + ReminderTypeArrive ReminderType = "arrive" ) func (rt ReminderType) String() string { From 7f8d4794c929438ae83943d35b5751af14348f17 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:32:49 +0300 Subject: [PATCH 43/59] feat: update serverless --- _scripts/migrate/README.md | 5 +- _scripts/migrate/main.go | 10 +- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 4 + .../dispatcher/internal/handler/handler.go | 4 +- .../dispatcher/internal/handler/helper.go | 5 + .../dispatcher/internal/handler/keyboard.go | 45 +++--- .../dispatcher/internal/handler/languages.go | 6 +- .../internal/handler/languages/ar.yaml | 24 ++- .../internal/handler/languages/en.yaml | 24 ++- .../internal/handler/languages/es.yaml | 24 ++- .../internal/handler/languages/fr.yaml | 24 ++- .../internal/handler/languages/ru.yaml | 24 ++- .../internal/handler/languages/tr.yaml | 24 ++- .../internal/handler/languages/tt.yaml | 24 ++- .../internal/handler/languages/uz.yaml | 24 ++- .../dispatcher/internal/handler/query.go | 145 ++++-------------- serverless/dispatcher/internal/service/db.go | 4 +- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 4 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 4 + .../reminder/internal/handler/handler.go | 2 +- .../reminder/internal/handler/reminder.go | 12 +- serverless/reminder/internal/service/db.go | 6 +- 25 files changed, 182 insertions(+), 272 deletions(-) diff --git a/_scripts/migrate/README.md b/_scripts/migrate/README.md index cafef46..3535b6b 100644 --- a/_scripts/migrate/README.md +++ b/_scripts/migrate/README.md @@ -35,7 +35,7 @@ go run main.go 3. Transforms the data: - Most fields remain the same (chat_id, bot_id, language_code, state, subscribed, subscribed_at, created_at) - Creates new `reminder` JSON object with: - - `today`: LastAt set to current time (Moscow timezone) + - `tomorrow`: LastAt set to current time (Moscow timezone) - `soon`: Offset from old `reminder_offset`, MessageID from old `reminder_message_id` - `arrive`: MessageID from old `jamaat_message_id` - `jamaat`: Enabled if both `subscribed` and `jamaat` were true, with default delay configs @@ -50,9 +50,10 @@ The script uses UPSERT statements, making it safe to run multiple times. Subsequ ### Reminder Field The reminder JSON structure: + ```json { - "today": { + "tomorrow": { "offset": 0, "message_id": 0, "last_at": "2025-10-26T12:00:00+03:00" diff --git a/_scripts/migrate/main.go b/_scripts/migrate/main.go index c0fa274..7441071 100644 --- a/_scripts/migrate/main.go +++ b/_scripts/migrate/main.go @@ -43,10 +43,10 @@ type ReminderConfig struct { } type Reminder struct { - Today *ReminderConfig `json:"today"` - Soon *ReminderConfig `json:"soon"` - Arrive *ReminderConfig `json:"arrive"` - Jamaat *JamaatConfig `json:"jamaat"` + Tomorrow *ReminderConfig `json:"tomorrow"` + Soon *ReminderConfig `json:"soon"` + Arrive *ReminderConfig `json:"arrive"` + Jamaat *JamaatConfig `json:"jamaat"` } // Old table row structure @@ -216,7 +216,7 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err } reminder := Reminder{ - Today: &ReminderConfig{ + Tomorrow: &ReminderConfig{ Offset: int64(3 * time.Hour), LastAt: now.Format(time.RFC3339), }, diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 3b7865e..b60351f 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 + github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 4214602..ec33df1 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -579,6 +579,10 @@ github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRc github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaKUZs0gG+1QbpVENyN9NVbdZLQJOO+d/q0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index ed30020..c24b5d6 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -84,12 +84,10 @@ func (h *Handler) opts() []bot.Option { bot.WithCallbackQueryDataHandler(remindToggleQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindToggleQuery))), bot.WithCallbackQueryDataHandler(remindEditQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindEditQuery))), bot.WithCallbackQueryDataHandler(remindAdjustQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindAdjustQuery))), - bot.WithCallbackQueryDataHandler(remindSaveQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindSaveQuery))), bot.WithCallbackQueryDataHandler(remindJamaatMenuQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatMenuQuery))), bot.WithCallbackQueryDataHandler(remindJamaatToggleQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatToggleQuery))), bot.WithCallbackQueryDataHandler(remindJamaatEditQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatEditQuery))), bot.WithCallbackQueryDataHandler(remindJamaatAdjustQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatAdjustQuery))), - bot.WithCallbackQueryDataHandler(remindJamaatSaveQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindJamaatSaveQuery))), bot.WithCallbackQueryDataHandler(remindBackQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindBackQuery))), bot.WithCallbackQueryDataHandler(remindCloseQuery.String(), bot.MatchTypePrefix, h.errorH(h.chatH(h.remindCloseQuery))), } @@ -242,7 +240,7 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C now := h.now(botID) reminder := &domain.Reminder{ - Today: &domain.ReminderConfig{ + Tomorrow: &domain.ReminderConfig{ LastAt: now, Offset: 3 * time.Hour, }, diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 0e684a6..3bfc6df 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -87,3 +87,8 @@ func parseAdjustment(adj string) time.Duration { } return 0 } + +// isChatGroup checks if the chat is a group or supergroup. +func isChatGroup(chatID int64) bool { + return chatID < 0 +} diff --git a/serverless/dispatcher/internal/handler/keyboard.go b/serverless/dispatcher/internal/handler/keyboard.go index a7e093d..25fa917 100644 --- a/serverless/dispatcher/internal/handler/keyboard.go +++ b/serverless/dispatcher/internal/handler/keyboard.go @@ -14,6 +14,8 @@ const ( monthsPerRow = 3 remindPerRow = 4 daysPerRow = 5 + + buttonBack = "🔙" ) func (h *Handler) languagesKeyboard() *models.InlineKeyboardMarkup { @@ -93,30 +95,43 @@ func (h *Handler) daysKeyboard(now time.Time, month int) *models.InlineKeyboardM func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMarkup { text := h.lp.GetText(chat.LanguageCode) - kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 4)} + // Calculate number of rows based on whether it's a group + numRows := 3 + if isChatGroup(chat.ChatID) { + numRows = 4 + } + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, numRows)} + + rowIndex := 0 if chat.Subscribed { - kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: text.RemindMenu.Disable, CallbackData: "remind:toggle|"}, } } else { - kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: text.RemindMenu.Enable, CallbackData: "remind:toggle|"}, } } + rowIndex++ - todayOffset := domain.FormatDuration(chat.Reminder.Today.Offset) + tomorrowOffset := domain.FormatDuration(chat.Reminder.Tomorrow.Offset) soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset) - kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ - {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Today, todayOffset), CallbackData: "remind:edit:today|"}, + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ + {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Tomorrow, tomorrowOffset), CallbackData: "remind:edit:tomorrow|"}, {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Soon, soonOffset), CallbackData: "remind:edit:soon|"}, } + rowIndex++ - kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ - {Text: text.RemindMenu.JamaatSettings, CallbackData: "remind:jamaat:menu|"}, + // Only show Jamaat Settings for group chats + if isChatGroup(chat.ChatID) { + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ + {Text: text.RemindMenu.JamaatSettings, CallbackData: "remind:jamaat:menu|"}, + } + rowIndex++ } - kb.InlineKeyboard[3] = []models.InlineKeyboardButton{ + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: text.RemindMenu.Close, CallbackData: "remind:close|"}, } @@ -124,8 +139,6 @@ func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa } func (h *Handler) remindEditKeyboard(reminderType domain.ReminderType, languageCode string) *models.InlineKeyboardMarkup { - text := h.lp.GetText(languageCode) - kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 3)} kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ @@ -141,8 +154,7 @@ func (h *Handler) remindEditKeyboard(reminderType domain.ReminderType, languageC } kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ - {Text: text.Buttons.Save, CallbackData: fmt.Sprintf("remind:save:%s|", reminderType)}, - {Text: text.Buttons.Back, CallbackData: "remind:back:menu|"}, + {Text: buttonBack, CallbackData: "remind:back:menu|"}, } return kb @@ -185,15 +197,13 @@ func (h *Handler) jammatMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa } kb.InlineKeyboard[4] = []models.InlineKeyboardButton{ - {Text: text.Buttons.Back, CallbackData: "remind:back:menu|"}, + {Text: buttonBack, CallbackData: "remind:back:menu|"}, } return kb } func (h *Handler) jammatEditKeyboard(prayerID domain.PrayerID, languageCode string) *models.InlineKeyboardMarkup { - text := h.lp.GetText(languageCode) - kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 3)} prayerName := prayerID.String() @@ -211,8 +221,7 @@ func (h *Handler) jammatEditKeyboard(prayerID domain.PrayerID, languageCode stri } kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ - {Text: text.Buttons.Save, CallbackData: fmt.Sprintf("remind:jamaat:save:%s|", prayerName)}, - {Text: text.Buttons.Back, CallbackData: "remind:back:jamaat|"}, + {Text: buttonBack, CallbackData: "remind:back:jamaat|"}, } return kb diff --git a/serverless/dispatcher/internal/handler/languages.go b/serverless/dispatcher/internal/handler/languages.go index 38d974e..2af3d3a 100644 --- a/serverless/dispatcher/internal/handler/languages.go +++ b/serverless/dispatcher/internal/handler/languages.go @@ -30,15 +30,15 @@ type ( TitleDisabled string `yaml:"title_disabled"` Enable string `yaml:"enable"` Disable string `yaml:"disable"` - Today string `yaml:"today"` + Tomorrow string `yaml:"tomorrow"` Soon string `yaml:"soon"` JamaatSettings string `yaml:"jamaat_settings"` Close string `yaml:"close"` } RemindEditText struct { - TitleToday string `yaml:"title_today"` - TitleSoon string `yaml:"title_soon"` + TitleTomorrow string `yaml:"title_tomorrow"` + TitleSoon string `yaml:"title_soon"` } JamaatMenuText struct { diff --git a/serverless/dispatcher/internal/handler/languages/ar.yaml b/serverless/dispatcher/internal/handler/languages/ar.yaml index b1fd6c6..f750cfb 100644 --- a/serverless/dispatcher/internal/handler/languages/ar.yaml +++ b/serverless/dispatcher/internal/handler/languages/ar.yaml @@ -43,31 +43,27 @@ language: success: "✅ تم تغيير اللغة إلى *%s*" remind_menu: - title_enabled: "⚙️ إعدادات التذكير - مفعّل" - title_disabled: "⚙️ إعدادات التذكير - معطّل" + title_enabled: "⚙️ التذكيرات - مفعّل ✅" + title_disabled: "⚙️ التذكيرات - معطّل 🔕" enable: "🔔 تفعيل" disable: "🔕 تعطيل" - today: "⏰ اليوم" - soon: "🔜 قريباً" - jamaat_settings: "🕌 إعدادات الجماعة" + tomorrow: "🌅 معاينة الغد" + soon: "⏰ تنبيه الصلاة" + jamaat_settings: "🕌 مواعيد الجماعة" close: "❌ إغلاق" remind_edit: - title_today: "⏰ اليوم" - title_soon: "🔜 قريباً" + title_tomorrow: "🌅 معاينة الغد\n⏰ إرسال مواقيت صلاة الغد قبل منتصف الليل" + title_soon: "⏰ تنبيه الصلاة\n⏱ التذكير قبل كل وقت صلاة" jamaat_menu: - title_enabled: "🕌 إعدادات الجماعة - مفعّل" - title_disabled: "🕌 إعدادات الجماعة - معطّل" + title_enabled: "🕌 مواعيد الجماعة - مفعّل ✅\n(للمجموعات فقط)" + title_disabled: "🕌 مواعيد الجماعة - معطّل 🔕\n(للمجموعات فقط)" enable: "🔔 تفعيل" disable: "🔕 تعطيل" jamaat_edit: - title: "تأخير جماعة %s" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 موعد جماعة %s\n⏱ الوقت بعد الأذان" help: | هل تحتاج مساعدة؟ 😊 إليك ما يمكنني فعله: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/en.yaml b/serverless/dispatcher/internal/handler/languages/en.yaml index f92a37a..637f859 100644 --- a/serverless/dispatcher/internal/handler/languages/en.yaml +++ b/serverless/dispatcher/internal/handler/languages/en.yaml @@ -43,31 +43,27 @@ language: success: "✅ Language changed to *%s*" remind_menu: - title_enabled: "⚙️ Configure Reminders - Enabled" - title_disabled: "⚙️ Configure Reminders - Disabled" + title_enabled: "⚙️ Reminders - Enabled ✅" + title_disabled: "⚙️ Reminders - Disabled 🔕" enable: "🔔 Enable" disable: "🔕 Disable" - today: "⏰ Today" - soon: "🔜 Soon" - jamaat_settings: "🕌 Jamaat Settings" + tomorrow: "🌅 Tomorrow Preview" + soon: "⏰ Prayer Alert" + jamaat_settings: "🕌 Jamaat Times" close: "❌ Close" remind_edit: - title_today: "⏰ Today" - title_soon: "🔜 Soon" + title_tomorrow: "🌅 Tomorrow Preview\n⏰ Send tomorrow's prayer times before midnight" + title_soon: "⏰ Prayer Alert\n⏱ Remind before each prayer time" jamaat_menu: - title_enabled: "🕌 Jamaat Settings - Enabled" - title_disabled: "🕌 Jamaat Settings - Disabled" + title_enabled: "🕌 Jamaat Times - Enabled ✅\n(Group Only)" + title_disabled: "🕌 Jamaat Times - Disabled 🔕\n(Group Only)" enable: "🔔 Enable" disable: "🔕 Disable" jamaat_edit: - title: "%s Jamaat Delay" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 %s Jamaat Time\n⏱ Time after Adhan" help: | Need help? 😊 Here's what I can do: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/es.yaml b/serverless/dispatcher/internal/handler/languages/es.yaml index e75713d..2ab064a 100644 --- a/serverless/dispatcher/internal/handler/languages/es.yaml +++ b/serverless/dispatcher/internal/handler/languages/es.yaml @@ -43,31 +43,27 @@ language: success: "✅ Idioma cambiado a *%s*" remind_menu: - title_enabled: "⚙️ Configurar Recordatorios - Habilitado" - title_disabled: "⚙️ Configurar Recordatorios - Deshabilitado" + title_enabled: "⚙️ Recordatorios - Habilitado ✅" + title_disabled: "⚙️ Recordatorios - Deshabilitado 🔕" enable: "🔔 Habilitar" disable: "🔕 Deshabilitar" - today: "⏰ Hoy" - soon: "🔜 Pronto" - jamaat_settings: "🕌 Configuración de Jamaat" + tomorrow: "🌅 Vista Previa de Mañana" + soon: "⏰ Alerta de Oración" + jamaat_settings: "🕌 Horarios de Jamaat" close: "❌ Cerrar" remind_edit: - title_today: "⏰ Hoy" - title_soon: "🔜 Pronto" + title_tomorrow: "🌅 Vista Previa de Mañana\n⏰ Enviar horarios de mañana antes de medianoche" + title_soon: "⏰ Alerta de Oración\n⏱ Recordar antes de cada oración" jamaat_menu: - title_enabled: "🕌 Configuración de Jamaat - Habilitado" - title_disabled: "🕌 Configuración de Jamaat - Deshabilitado" + title_enabled: "🕌 Horarios de Jamaat - Habilitado ✅\n(Solo Grupos)" + title_disabled: "🕌 Horarios de Jamaat - Deshabilitado 🔕\n(Solo Grupos)" enable: "🔔 Habilitar" disable: "🔕 Deshabilitar" jamaat_edit: - title: "Retraso de Jamaat %s" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 Horario de Jamaat %s\n⏱ Tiempo después del Adhan" help: | ¿Necesitas ayuda? 😊 Aquí está lo que puedo hacer: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/fr.yaml b/serverless/dispatcher/internal/handler/languages/fr.yaml index 59e3b28..0121c06 100644 --- a/serverless/dispatcher/internal/handler/languages/fr.yaml +++ b/serverless/dispatcher/internal/handler/languages/fr.yaml @@ -43,31 +43,27 @@ language: success: "✅ Langue changée en *%s*" remind_menu: - title_enabled: "⚙️ Configurer les Rappels - Activé" - title_disabled: "⚙️ Configurer les Rappels - Désactivé" + title_enabled: "⚙️ Rappels - Activé ✅" + title_disabled: "⚙️ Rappels - Désactivé 🔕" enable: "🔔 Activer" disable: "🔕 Désactiver" - today: "⏰ Aujourd'hui" - soon: "🔜 Bientôt" - jamaat_settings: "🕌 Paramètres Jamaat" + tomorrow: "🌅 Aperçu de Demain" + soon: "⏰ Alerte de Prière" + jamaat_settings: "🕌 Horaires Jamaat" close: "❌ Fermer" remind_edit: - title_today: "⏰ Aujourd'hui" - title_soon: "🔜 Bientôt" + title_tomorrow: "🌅 Aperçu de Demain\n⏰ Envoyer les horaires de demain avant minuit" + title_soon: "⏰ Alerte de Prière\n⏱ Rappel avant chaque prière" jamaat_menu: - title_enabled: "🕌 Paramètres Jamaat - Activé" - title_disabled: "🕌 Paramètres Jamaat - Désactivé" + title_enabled: "🕌 Horaires Jamaat - Activé ✅\n(Groupes Seulement)" + title_disabled: "🕌 Horaires Jamaat - Désactivé 🔕\n(Groupes Seulement)" enable: "🔔 Activer" disable: "🔕 Désactiver" jamaat_edit: - title: "Délai Jamaat %s" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 Horaire Jamaat %s\n⏱ Temps après l'Adhan" help: | Besoin d'aide ? 😊 Voici ce que je peux faire : 👇 diff --git a/serverless/dispatcher/internal/handler/languages/ru.yaml b/serverless/dispatcher/internal/handler/languages/ru.yaml index 19f2d3a..53b5963 100644 --- a/serverless/dispatcher/internal/handler/languages/ru.yaml +++ b/serverless/dispatcher/internal/handler/languages/ru.yaml @@ -43,31 +43,27 @@ language: success: "✅ Язык изменён на *%s*" remind_menu: - title_enabled: "⚙️ Настройка Напоминаний - Включено" - title_disabled: "⚙️ Настройка Напоминаний - Выключено" + title_enabled: "⚙️ Напоминания - Включено ✅" + title_disabled: "⚙️ Напоминания - Выключено 🔕" enable: "🔔 Включить" disable: "🔕 Выключить" - today: "⏰ Сегодня" - soon: "🔜 Скоро" - jamaat_settings: "🕌 Настройки Джамаата" + tomorrow: "🌅 Предпросмотр Завтра" + soon: "⏰ Напоминание о Молитве" + jamaat_settings: "🕌 Время Джамаата" close: "❌ Закрыть" remind_edit: - title_today: "⏰ Сегодня" - title_soon: "🔜 Скоро" + title_tomorrow: "🌅 Предпросмотр Завтра\n⏰ Отправить время завтра до полуночи" + title_soon: "⏰ Напоминание о Молитве\n⏱ Напомнить перед каждой молитвой" jamaat_menu: - title_enabled: "🕌 Настройки Джамаата - Включено" - title_disabled: "🕌 Настройки Джамаата - Выключено" + title_enabled: "🕌 Время Джамаата - Включено ✅\n(Только Группы)" + title_disabled: "🕌 Время Джамаата - Выключено 🔕\n(Только Группы)" enable: "🔔 Включить" disable: "🔕 Выключить" jamaat_edit: - title: "Задержка Джамаата %s" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 Время Джамаата %s\n⏱ Время после Азана" help: | Нужна помощь? 😊 Вот что я умею: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/tr.yaml b/serverless/dispatcher/internal/handler/languages/tr.yaml index 4b928bc..4118355 100644 --- a/serverless/dispatcher/internal/handler/languages/tr.yaml +++ b/serverless/dispatcher/internal/handler/languages/tr.yaml @@ -43,31 +43,27 @@ language: success: "✅ Dil *%s* diýip üýtgedildi" remind_menu: - title_enabled: "⚙️ Duýduryşlary Sazlamak - Açyk" - title_disabled: "⚙️ Duýduryşlary Sazlamak - Ýapyk" + title_enabled: "⚙️ Duýduryşlar - Açyk ✅" + title_disabled: "⚙️ Duýduryşlar - Ýapyk 🔕" enable: "🔔 Açmak" disable: "🔕 Ýapmak" - today: "⏰ Şu gün" - soon: "🔜 Tiz wagtda" - jamaat_settings: "🕌 Jemgyýet Sazlamalary" + tomorrow: "🌅 Ertir Syn" + soon: "⏰ Namaz Duýduryşy" + jamaat_settings: "🕌 Jemgyýet Wagty" close: "❌ Ýapmak" remind_edit: - title_today: "⏰ Şu gün" - title_soon: "🔜 Tiz wagtda" + title_tomorrow: "🌅 Ertir Syn\n⏰ Ertirki namaz wagtlaryny gijäniň ýary gijeden öň iberiň" + title_soon: "⏰ Namaz Duýduryşy\n⏱ Her namaz wagtyndan öň ýatlatma" jamaat_menu: - title_enabled: "🕌 Jemgyýet Sazlamalary - Açyk" - title_disabled: "🕌 Jemgyýet Sazlamalary - Ýapyk" + title_enabled: "🕌 Jemgyýet Wagty - Açyk ✅\n(Diňe Toparlar)" + title_disabled: "🕌 Jemgyýet Wagty - Ýapyk 🔕\n(Diňe Toparlar)" enable: "🔔 Açmak" disable: "🔕 Ýapmak" jamaat_edit: - title: "%s Jemgyýet Gijikdirmesi" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 %s Jemgyýet Wagty\n⏱ Ezandan soňky wagt" help: | Kömek gerekmi? 😊 Men şulary edip bilýärin: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/tt.yaml b/serverless/dispatcher/internal/handler/languages/tt.yaml index f3f1e68..3fc6f80 100644 --- a/serverless/dispatcher/internal/handler/languages/tt.yaml +++ b/serverless/dispatcher/internal/handler/languages/tt.yaml @@ -43,31 +43,27 @@ language: success: "✅ Тел *%s* итеп үзгәртелде" remind_menu: - title_enabled: "⚙️ Хәтерләтүләрне Көйләү - Кабызылган" - title_disabled: "⚙️ Хәтерләтүләрне Көйләү - Сүндерелгән" + title_enabled: "⚙️ Хәтерләтүләр - Кабызылган ✅" + title_disabled: "⚙️ Хәтерләтүләр - Сүндерелгән 🔕" enable: "🔔 Кабызу" disable: "🔕 Сүндерү" - today: "⏰ Бүген" - soon: "🔜 Тиздән" - jamaat_settings: "🕌 Җәмәгать Көйләүләре" + tomorrow: "🌅 Иртәгә Карау" + soon: "⏰ Намаз Искәрмәсе" + jamaat_settings: "🕌 Җәмәгать Вакыты" close: "❌ Ябу" remind_edit: - title_today: "⏰ Бүген" - title_soon: "🔜 Тиздән" + title_tomorrow: "🌅 Иртәгә Карау\n⏰ Иртәгә намаз вакытларын төн уртасында җибәрергә" + title_soon: "⏰ Намаз Искәрмәсе\n⏱ Һәр намазга кадәр хәтерләтергә" jamaat_menu: - title_enabled: "🕌 Җәмәгать Көйләүләре - Кабызылган" - title_disabled: "🕌 Җәмәгать Көйләүләре - Сүндерелгән" + title_enabled: "🕌 Җәмәгать Вакыты - Кабызылган ✅\n(Төркемнәр Өчен)" + title_disabled: "🕌 Җәмәгать Вакыты - Сүндерелгән 🔕\n(Төркемнәр Өчен)" enable: "🔔 Кабызу" disable: "🔕 Сүндерү" jamaat_edit: - title: "%s Җәмәгать Тоткарлыгы" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 %s Җәмәгать Вакыты\n⏱ Азаннан соң вакыт" help: | Ярдәм кирәкме? 😊 Менә нәрсәләр эшли алам: 👇 diff --git a/serverless/dispatcher/internal/handler/languages/uz.yaml b/serverless/dispatcher/internal/handler/languages/uz.yaml index 8852050..3d9b7c4 100644 --- a/serverless/dispatcher/internal/handler/languages/uz.yaml +++ b/serverless/dispatcher/internal/handler/languages/uz.yaml @@ -44,31 +44,27 @@ language: remind_menu: - title_enabled: "⚙️ Eslatmalarni Sozlash - Yoqilgan" - title_disabled: "⚙️ Eslatmalarni Sozlash - O'chirilgan" + title_enabled: "⚙️ Eslatmalar - Yoqilgan ✅" + title_disabled: "⚙️ Eslatmalar - O'chirilgan 🔕" enable: "🔔 Yoqish" disable: "🔕 O'chirish" - today: "⏰ Bugun" - soon: "🔜 Tez orada" - jamaat_settings: "🕌 Jamoat Sozlamalari" + tomorrow: "🌅 Ertangi Ko'rinish" + soon: "⏰ Namoz Eslatmasi" + jamaat_settings: "🕌 Jamoat Vaqtlari" close: "❌ Yopish" remind_edit: - title_today: "⏰ Bugun" - title_soon: "🔜 Tez orada" + title_tomorrow: "🌅 Ertangi Ko'rinish\n⏰ Ertangi namoz vaqtlarini yarim tundan oldin yuborish" + title_soon: "⏰ Namoz Eslatmasi\n⏱ Har bir namoz vaqtidan oldin eslatma" jamaat_menu: - title_enabled: "🕌 Jamoat Sozlamalari - Yoqilgan" - title_disabled: "🕌 Jamoat Sozlamalari - O'chirilgan" + title_enabled: "🕌 Jamoat Vaqtlari - Yoqilgan ✅\n(Faqat Guruhlar)" + title_disabled: "🕌 Jamoat Vaqtlari - O'chirilgan 🔕\n(Faqat Guruhlar)" enable: "🔔 Yoqish" disable: "🔕 O'chirish" jamaat_edit: - title: "%s Jamoat Kechikishi" - -buttons: - save: "💾" - back: "🔙" + title: "🕌 %s Jamoat Vaqti\n⏱ Azon vaqtidan keyingi vaqt" help: | Yordam kerakmi? 😊 Men quyidagilarni bajara olaman: 👇 diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index 5fbd467..c935ae3 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -22,26 +22,23 @@ const ( dayQuery query = "date:day|" monthQuery query = "date:month|" - remindQuery query = "remind|" languageQuery query = "language|" remindMenuQuery query = "remind:menu|" remindToggleQuery query = "remind:toggle|" remindEditQuery query = "remind:edit:" remindAdjustQuery query = "remind:adjust:" - remindSaveQuery query = "remind:save:" remindJamaatMenuQuery query = "remind:jamaat:menu|" remindJamaatToggleQuery query = "remind:jamaat:toggle|" remindJamaatEditQuery query = "remind:jamaat:edit:" remindJamaatAdjustQuery query = "remind:jamaat:adjust:" - remindJamaatSaveQuery query = "remind:jamaat:save:" remindBackQuery query = "remind:back:" remindCloseQuery query = "remind:close|" ) const ( - TodayMinOffset = 0 * time.Minute - TodayMaxOffset = 6 * time.Hour + TomorrowMinOffset = 0 * time.Minute + TomorrowMaxOffset = 6 * time.Hour SoonMinOffset = 5 * time.Minute SoonMaxOffset = 1 * time.Hour @@ -129,46 +126,6 @@ func (h *Handler) dayQuery(ctx context.Context, b *bot.Bot, update *models.Updat return nil } -func (h *Handler) remindQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { - chat := getContextChat(ctx) - - reminderOffset, _ := strconv.Atoi(strings.TrimPrefix(update.CallbackQuery.Data, remindQuery.String())) - - offset := time.Duration(reminderOffset) * time.Minute - err := h.db.SetReminderOffset(ctx, chat.BotID, chat.ChatID, domain.ReminderTypeSoon, offset) - if err != nil { - log.Error("remindQuery: set reminder soon offset", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - err = h.db.SetSubscribed(ctx, chat.BotID, chat.ChatID, true) - if err != nil { - log.Error("remindQuery: set subscribed", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - message := fmt.Sprintf(h.lp.GetText(chat.LanguageCode).Remind.Success, reminderOffset) - _, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chat.ChatID, - MessageID: update.CallbackQuery.Message.Message.ID, - Text: message, - ParseMode: models.ParseModeMarkdown, - }) - if err != nil { - log.Error("remindQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) - if err != nil { - log.Error("remindQuery: answer query query", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - h.resetState(ctx, chat) - return nil -} - func (h *Handler) languageQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) @@ -272,9 +229,9 @@ func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *model var messageText string var offset time.Duration switch reminderType { - case domain.ReminderTypeToday: - offset = chat.Reminder.Today.Offset - messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleToday, domain.FormatDuration(offset)) + case domain.ReminderTypeTomorrow: + offset = chat.Reminder.Tomorrow.Offset + messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleTomorrow, domain.FormatDuration(offset)) case domain.ReminderTypeSoon: offset = chat.Reminder.Soon.Offset messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleSoon, domain.FormatDuration(offset)) @@ -319,10 +276,10 @@ func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *mod var minOffset, maxOffset time.Duration switch reminderType { - case domain.ReminderTypeToday: - currentOffset = chat.Reminder.Today.Offset - minOffset = TodayMinOffset - maxOffset = TodayMaxOffset + case domain.ReminderTypeTomorrow: + currentOffset = chat.Reminder.Tomorrow.Offset + minOffset = TomorrowMinOffset + maxOffset = TomorrowMaxOffset case domain.ReminderTypeSoon: currentOffset = chat.Reminder.Soon.Offset minOffset = SoonMinOffset @@ -347,8 +304,8 @@ func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *mod } switch reminderType { - case domain.ReminderTypeToday: - chat.Reminder.Today.Offset = newOffset + case domain.ReminderTypeTomorrow: + chat.Reminder.Tomorrow.Offset = newOffset case domain.ReminderTypeSoon: chat.Reminder.Soon.Offset = newOffset } @@ -357,39 +314,14 @@ func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *mod return h.remindEditQuery(ctx, b, update) } -func (h *Handler) remindSaveQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { +func (h *Handler) remindJamaatMenuQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) - text := h.lp.GetText(chat.LanguageCode) - - var messageText string - if chat.Subscribed { - messageText = text.RemindMenu.TitleEnabled - } else { - messageText = text.RemindMenu.TitleDisabled - } - _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chat.ChatID, - MessageID: update.CallbackQuery.Message.Message.ID, - Text: messageText, - ReplyMarkup: h.remindMenuKeyboard(chat), - }) - if err != nil { - log.Error("remindSaveQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) - if err != nil { - log.Error("remindSaveQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal + if !isChatGroup(chat.ChatID) { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil } - return nil -} - -func (h *Handler) remindJamaatMenuQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { - chat := getContextChat(ctx) text := h.lp.GetText(chat.LanguageCode) var messageText string @@ -422,6 +354,11 @@ func (h *Handler) remindJamaatMenuQuery(ctx context.Context, b *bot.Bot, update func (h *Handler) remindJamaatToggleQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) + if !isChatGroup(chat.ChatID) { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil + } + newEnabled := !chat.Reminder.Jamaat.Enabled err := h.db.SetJamaatEnabled(ctx, chat.BotID, chat.ChatID, newEnabled) if err != nil { @@ -436,6 +373,12 @@ func (h *Handler) remindJamaatToggleQuery(ctx context.Context, b *bot.Bot, updat func (h *Handler) remindJamaatEditQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) + + if !isChatGroup(chat.ChatID) { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil + } + text := h.lp.GetText(chat.LanguageCode) parts := strings.Split(update.CallbackQuery.Data, ":") @@ -473,6 +416,11 @@ func (h *Handler) remindJamaatEditQuery(ctx context.Context, b *bot.Bot, update func (h *Handler) remindJamaatAdjustQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) + if !isChatGroup(chat.ChatID) { + _, _ = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) + return nil + } + parts := strings.Split(update.CallbackQuery.Data, ":") if len(parts) < 5 { log.Error("remindJamaatAdjustQuery: invalid callback data", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) @@ -503,37 +451,6 @@ func (h *Handler) remindJamaatAdjustQuery(ctx context.Context, b *bot.Bot, updat return h.remindJamaatEditQuery(ctx, b, update) } -func (h *Handler) remindJamaatSaveQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { - chat := getContextChat(ctx) - text := h.lp.GetText(chat.LanguageCode) - - var messageText string - if chat.Reminder.Jamaat.Enabled { - messageText = text.JamaatMenu.TitleEnabled - } else { - messageText = text.JamaatMenu.TitleDisabled - } - - _, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chat.ChatID, - MessageID: update.CallbackQuery.Message.Message.ID, - Text: messageText, - ReplyMarkup: h.jammatMenuKeyboard(chat), - }) - if err != nil { - log.Error("remindJamaatSaveQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - _, err = b.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: update.CallbackQuery.ID}) - if err != nil { - log.Error("remindJamaatSaveQuery: answer callback", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) - return domain.ErrInternal - } - - return nil -} - func (h *Handler) remindBackQuery(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) text := h.lp.GetText(chat.LanguageCode) diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 6f1b89d..6d3027c 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -274,8 +274,8 @@ func (db *DB) SetSubscribed(ctx context.Context, botID int64, chatID int64, subs func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, reminderType domain.ReminderType, offset time.Duration) error { return db.updateReminder(ctx, botID, chatID, func(reminder *domain.Reminder) { switch reminderType { - case domain.ReminderTypeToday: - reminder.Today.Offset = offset + case domain.ReminderTypeTomorrow: + reminder.Tomorrow.Offset = offset case domain.ReminderTypeSoon: reminder.Soon.Offset = offset } diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 2638f73..5671a7e 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 + github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 05d2230..35783ca 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -581,6 +581,10 @@ github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRc github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaKUZs0gG+1QbpVENyN9NVbdZLQJOO+d/q0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index a8086c4..04de6f5 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 + github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index 4214602..ec33df1 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -579,6 +579,10 @@ github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRc github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaKUZs0gG+1QbpVENyN9NVbdZLQJOO+d/q0= +github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= +github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/handler.go b/serverless/reminder/internal/handler/handler.go index 8ef0b67..1109d5e 100644 --- a/serverless/reminder/internal/handler/handler.go +++ b/serverless/reminder/internal/handler/handler.go @@ -110,7 +110,7 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { } reminders := []ReminderType{ - &TodayReminder{ + &TomorrowReminder{ lp: h.lp, botConfig: h.cfg, formatPrayerDay: h.formatPrayerDay, diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index 9a92bb3..c863e82 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -16,22 +16,22 @@ type ReminderType interface { Name() domain.ReminderType } -type TodayReminder struct { +type TomorrowReminder struct { lp *languagesProvider botConfig map[int64]*domain.BotConfig formatPrayerDay func(botID int64, prayerDay *domain.PrayerDay, languageCode string) string } -func (r *TodayReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { - config := chat.Reminder.Today +func (r *TomorrowReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { + config := chat.Reminder.Tomorrow // Trigger logic: last_at + 24h - offset < now lastDate := config.LastAt.Truncate(24 * time.Hour) triggerTime := lastDate.Add(24 * time.Hour).Add(-config.Offset) return triggerTime.Before(now) || triggerTime.Equal(now), domain.PrayerIDUnknown } -func (r *TodayReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { - deleteMessages(ctx, b, chat, chat.Reminder.Today.MessageID) +func (r *TomorrowReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { + deleteMessages(ctx, b, chat, chat.Reminder.Tomorrow.MessageID) res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, Text: r.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode), @@ -43,7 +43,7 @@ func (r *TodayReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, return res.ID, nil } -func (r *TodayReminder) Name() domain.ReminderType { return domain.ReminderTypeToday } +func (r *TomorrowReminder) Name() domain.ReminderType { return domain.ReminderTypeTomorrow } type SoonReminder struct { lp *languagesProvider diff --git a/serverless/reminder/internal/service/db.go b/serverless/reminder/internal/service/db.go index 4aae84f..666606e 100644 --- a/serverless/reminder/internal/service/db.go +++ b/serverless/reminder/internal/service/db.go @@ -277,9 +277,9 @@ func (db *DB) UpdateReminder( } switch reminderType { - case domain.ReminderTypeToday: - reminder.Today.MessageID = messageID - reminder.Today.LastAt = lastAt + case domain.ReminderTypeTomorrow: + reminder.Tomorrow.MessageID = messageID + reminder.Tomorrow.LastAt = lastAt case domain.ReminderTypeSoon: reminder.Soon.MessageID = messageID reminder.Soon.LastAt = lastAt From 11cb2dcd014eadaf0b11ec0d1848b5a61d39dc61 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:53:20 +0300 Subject: [PATCH 44/59] feat: update domain --- _scripts/migrate/main.go | 3 +-- domain/chat.go | 26 -------------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/_scripts/migrate/main.go b/_scripts/migrate/main.go index 7441071..43fe785 100644 --- a/_scripts/migrate/main.go +++ b/_scripts/migrate/main.go @@ -191,7 +191,7 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err // Build reminder object reminderOffset := int32(20) - if oldChat.ReminderOffset != nil { + if oldChat.ReminderOffset != nil && *oldChat.ReminderOffset != 0 { reminderOffset = *oldChat.ReminderOffset } @@ -226,7 +226,6 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err LastAt: now.Format(time.RFC3339), }, Arrive: &ReminderConfig{ - Offset: 0, MessageID: jamaatMessageID, LastAt: now.Format(time.RFC3339), }, diff --git a/domain/chat.go b/domain/chat.go index 180c0dc..c78f186 100644 --- a/domain/chat.go +++ b/domain/chat.go @@ -12,32 +12,6 @@ var ( ErrInternal = errors.New("internal") ) -type reminderOffset int32 - -const ( - reminderOffset5m reminderOffset = 5 - reminderOffset10m reminderOffset = 10 - reminderOffset15m reminderOffset = 15 - reminderOffset20m reminderOffset = 20 - reminderOffset30m reminderOffset = 30 - reminderOffset45m reminderOffset = 45 - reminderOffset60m reminderOffset = 60 - reminderOffset90m reminderOffset = 90 -) - -func ReminderOffsets() []int32 { - return []int32{ - int32(reminderOffset5m), - int32(reminderOffset10m), - int32(reminderOffset15m), - int32(reminderOffset20m), - int32(reminderOffset30m), - int32(reminderOffset45m), - int32(reminderOffset60m), - int32(reminderOffset90m), - } -} - type ( Stats struct { Users uint64 // users using the bot From d1e3fe35d6abe2c556ec6fe48997757fbe2a2341 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 26 Oct 2025 23:54:00 +0300 Subject: [PATCH 45/59] feat: update serverless --- main.tf | 9 --------- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 ++ serverless/dispatcher/internal/handler/keyboard.go | 10 +++++++--- serverless/dispatcher/internal/handler/query.go | 6 +++++- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 ++ serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 ++ 9 files changed, 21 insertions(+), 16 deletions(-) diff --git a/main.tf b/main.tf index 1850f41..0836952 100644 --- a/main.tf +++ b/main.tf @@ -61,10 +61,6 @@ resource "yandex_storage_bucket" "bucket" { max_size = 1073741824 # 1 GB in bytes } -output "bucket_name" { - value = yandex_storage_bucket.bucket.bucket -} - ########################### ### ydb ########################### @@ -81,11 +77,6 @@ resource "yandex_ydb_database_serverless" "ydb" { } } -output "ydb_connection_string" { - value = yandex_ydb_database_serverless.ydb.ydb_full_endpoint - sensitive = true -} - ########################### ### trigger ########################### diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index b60351f..04e883d 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 + github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index ec33df1..d311cda 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -583,6 +583,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaK github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/keyboard.go b/serverless/dispatcher/internal/handler/keyboard.go index 25fa917..1ee1f03 100644 --- a/serverless/dispatcher/internal/handler/keyboard.go +++ b/serverless/dispatcher/internal/handler/keyboard.go @@ -96,9 +96,9 @@ func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa text := h.lp.GetText(chat.LanguageCode) // Calculate number of rows based on whether it's a group - numRows := 3 + numRows := 4 if isChatGroup(chat.ChatID) { - numRows = 4 + numRows = 5 } kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, numRows)} @@ -116,9 +116,13 @@ func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa rowIndex++ tomorrowOffset := domain.FormatDuration(chat.Reminder.Tomorrow.Offset) - soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset) kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Tomorrow, tomorrowOffset), CallbackData: "remind:edit:tomorrow|"}, + } + rowIndex++ + + soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset) + kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Soon, soonOffset), CallbackData: "remind:edit:soon|"}, } rowIndex++ diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index c935ae3..b371fae 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -236,7 +236,11 @@ func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *model offset = chat.Reminder.Soon.Offset messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleSoon, domain.FormatDuration(offset)) default: - log.Error("remindEditQuery: unknown reminder type", log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + log.Error("remindEditQuery: unknown reminder type", + log.BotID(chat.BotID), + log.ChatID(chat.ChatID), + log.String("type", string(reminderType)), + ) return domain.ErrInternal } diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 5671a7e..9e9c428 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 + github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 35783ca..b852ec1 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -585,6 +585,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaK github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 04de6f5..fd734de 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 + github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index ec33df1..d311cda 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -583,6 +583,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaK github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= +github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From 73f9e46a72f72e617541f316cc804498dfbe4dcd Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 00:14:35 +0300 Subject: [PATCH 46/59] feat: add info command --- .../dispatcher/internal/handler/command.go | 57 +++++++++++++++++++ .../dispatcher/internal/handler/handler.go | 1 + .../dispatcher/internal/handler/languages.go | 17 +++++- .../internal/handler/languages/ar.yaml | 32 ++++++++++- .../internal/handler/languages/en.yaml | 32 ++++++++++- .../internal/handler/languages/es.yaml | 32 ++++++++++- .../internal/handler/languages/fr.yaml | 32 ++++++++++- .../internal/handler/languages/ru.yaml | 32 ++++++++++- .../internal/handler/languages/tr.yaml | 32 ++++++++++- .../internal/handler/languages/tt.yaml | 32 ++++++++++- .../internal/handler/languages/uz.yaml | 32 ++++++++++- .../dispatcher/internal/handler/query.go | 4 +- 12 files changed, 322 insertions(+), 13 deletions(-) diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index fd5e434..3010206 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -53,6 +53,7 @@ const ( // admin commands adminCommand command = "admin" + infoCommand command = "info" replyCommand command = "reply" // 1 stage statsCommand command = "stats" announceCommand command = "announce" // 1 stage @@ -288,6 +289,62 @@ func (h *Handler) admin(ctx context.Context, b *bot.Bot, _ *models.Update) error return nil } +func (h *Handler) info(ctx context.Context, b *bot.Bot, _ *models.Update) error { + chat := getContextChat(ctx) + text := h.lp.GetText(chat.LanguageCode) + + chatType := text.Info.Type.Private + if isChatGroup(chat.ChatID) { + chatType = text.Info.Type.Group + } + + subscriptionStatus := text.Info.Disabled + if chat.Subscribed { + subscriptionStatus = text.Info.Enabled + } + + jamaatInfo := "" + if isChatGroup(chat.ChatID) { + jamaatStatus := text.Info.Disabled + if chat.Reminder.Jamaat.Enabled { + jamaatStatus = text.Info.Enabled + } + jamaatInfo = fmt.Sprintf(text.Info.Jamaat, + jamaatStatus, + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Shuruq), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Isha), + ) + } + + message := fmt.Sprintf(text.Info.Default, + fmt.Sprintf("%d", chat.ChatID), + chatType, + chat.LanguageCode, + chat.State, + subscriptionStatus, + domain.FormatDuration(chat.Reminder.Tomorrow.Offset), + domain.FormatDuration(chat.Reminder.Soon.Offset), + jamaatInfo, + ) + + _, err := b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: message, + ParseMode: models.ParseModeMarkdown, + }) + if err != nil { + log.Error("info: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + h.resetState(ctx, chat) + return nil +} + func (h *Handler) reply(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index c24b5d6..1b9b9d0 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -72,6 +72,7 @@ func (h *Handler) opts() []bot.Option { bot.WithMessageTextHandler(cancelCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.cancel))), bot.WithMessageTextHandler(adminCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.admin)))), + bot.WithMessageTextHandler(infoCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.info)))), bot.WithMessageTextHandler(replyCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.reply)))), bot.WithMessageTextHandler(statsCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.stats)))), bot.WithMessageTextHandler(announceCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.authorizeH(h.announce)))), diff --git a/serverless/dispatcher/internal/handler/languages.go b/serverless/dispatcher/internal/handler/languages.go index 2af3d3a..1868cea 100644 --- a/serverless/dispatcher/internal/handler/languages.go +++ b/serverless/dispatcher/internal/handler/languages.go @@ -57,6 +57,19 @@ type ( Back string `yaml:"back"` } + InfoTypeText struct { + Private string `yaml:"private"` + Group string `yaml:"group"` + } + + InfoText struct { + Default string `yaml:"default"` + Jamaat string `yaml:"jamaat"` + Type InfoTypeText `yaml:"type"` + Enabled string `yaml:"enabled"` + Disabled string `yaml:"disabled"` + } + Text struct { Name string `yaml:"name"` @@ -74,9 +87,7 @@ type ( JamaatMenu JamaatMenuText `yaml:"jamaat_menu"` JamaatEdit JamaatEditText `yaml:"jamaat_edit"` Buttons ButtonsText `yaml:"buttons"` - - SubscriptionSuccess string `yaml:"subscription_success"` - UnsubscriptionSuccess string `yaml:"unsubscription_success"` + Info InfoText `yaml:"info"` PrayerSoon string `yaml:"prayer_soon"` diff --git a/serverless/dispatcher/internal/handler/languages/ar.yaml b/serverless/dispatcher/internal/handler/languages/ar.yaml index f750cfb..549324e 100644 --- a/serverless/dispatcher/internal/handler/languages/ar.yaml +++ b/serverless/dispatcher/internal/handler/languages/ar.yaml @@ -94,10 +94,40 @@ bug: success: "✅ شكراً! تم استلام تقريرك" help_admin: | + 📊 /info – عرض معلومات وإعدادات المحادثة 📩 /reply – الرد على المستخدمين - 📊 /stats – إحصائيات استخدام البوت + 📈 /stats – إحصائيات استخدام البوت 📢 /announce – إرسال رسالة للجميع +info: + default: | + 📊 *معلومات المحادثة* + + 🆔 *معرف المحادثة:* `%s` + 💬 *النوع:* %s + 🌐 *اللغة:* %s + 📍 *الحالة:* %s + + ⏰ *إعدادات التذكير:* + الحالة: %s + • 🌅 معاينة الغد: %s + • ⏰ تنبيه الصلاة: %s%s + jamaat: | + + 🕌 *إعدادات الجماعة:* + الحالة: %s + • الفجر: %s + • الشروق: %s + • الظهر: %s + • العصر: %s + • المغرب: %s + • العشاء: %s + type: + private: "خاص" + group: "مجموعة" + enabled: "✅ مفعّل" + disabled: "❌ معطّل" + reply: start: "✍️ اكتب ردك أدناه" success: "✅ تم إرسال الرد" diff --git a/serverless/dispatcher/internal/handler/languages/en.yaml b/serverless/dispatcher/internal/handler/languages/en.yaml index 637f859..d5cbbf9 100644 --- a/serverless/dispatcher/internal/handler/languages/en.yaml +++ b/serverless/dispatcher/internal/handler/languages/en.yaml @@ -94,10 +94,40 @@ bug: success: "✅ Thanks! Your report has been received" help_admin: | + 📊 /info – View chat info & settings 📩 /reply – Reply to users - 📊 /stats – Bot usage stats + 📈 /stats – Bot usage stats 📢 /announce – Send message to everyone +info: + default: | + 📊 *Chat Information* + + 🆔 *Chat ID:* `%s` + 💬 *Type:* %s + 🌐 *Language:* %s + 📍 *State:* %s + + ⏰ *Reminder Settings:* + Status: %s + • 🌅 Tomorrow Preview: %s + • ⏰ Prayer Alert: %s%s + jamaat: | + + 🕌 *Jamaat Settings:* + Status: %s + • Fajr: %s + • Shuruq: %s + • Dhuhr: %s + • Asr: %s + • Maghrib: %s + • Isha: %s + type: + private: "Private" + group: "Group" + enabled: "✅ Enabled" + disabled: "❌ Disabled" + reply: start: "✍️ Type your reply below (or /cancel to stop)" success: "✅ Reply sent" diff --git a/serverless/dispatcher/internal/handler/languages/es.yaml b/serverless/dispatcher/internal/handler/languages/es.yaml index 2ab064a..57ebd36 100644 --- a/serverless/dispatcher/internal/handler/languages/es.yaml +++ b/serverless/dispatcher/internal/handler/languages/es.yaml @@ -94,10 +94,40 @@ bug: success: "✅ ¡Gracias! Tu reporte ha sido recibido" help_admin: | + 📊 /info – Ver información y configuración del chat 📩 /reply – Responder a los usuarios - 📊 /stats – Estadísticas del bot + 📈 /stats – Estadísticas del bot 📢 /announce – Enviar un mensaje a todos +info: + default: | + 📊 *Información del Chat* + + 🆔 *ID del Chat:* `%s` + 💬 *Tipo:* %s + 🌐 *Idioma:* %s + 📍 *Estado:* %s + + ⏰ *Configuración de Recordatorios:* + Estado: %s + • 🌅 Vista Previa de Mañana: %s + • ⏰ Alerta de Oración: %s%s + jamaat: | + + 🕌 *Configuración de Jamaat:* + Estado: %s + • Fajr: %s + • Shuruq: %s + • Dhuhr: %s + • Asr: %s + • Maghrib: %s + • Isha: %s + type: + private: "Privado" + group: "Grupo" + enabled: "✅ Habilitado" + disabled: "❌ Deshabilitado" + reply: start: "✍️ Escribe tu respuesta abajo (o escribe /cancel para cancelar)" success: "✅ Respuesta enviada" diff --git a/serverless/dispatcher/internal/handler/languages/fr.yaml b/serverless/dispatcher/internal/handler/languages/fr.yaml index 0121c06..fb8bc7e 100644 --- a/serverless/dispatcher/internal/handler/languages/fr.yaml +++ b/serverless/dispatcher/internal/handler/languages/fr.yaml @@ -94,10 +94,40 @@ bug: success: "✅ Merci ! Votre rapport a été reçu" help_admin: | + 📊 /info – Voir les infos et paramètres du chat 📩 /reply – Répondre aux utilisateurs - 📊 /stats – Statistiques du bot + 📈 /stats – Statistiques du bot 📢 /announce – Envoyer un message à tout le monde +info: + default: | + 📊 *Informations du Chat* + + 🆔 *ID du Chat:* `%s` + 💬 *Type:* %s + 🌐 *Langue:* %s + 📍 *État:* %s + + ⏰ *Paramètres de Rappel:* + Statut: %s + • 🌅 Aperçu de Demain: %s + • ⏰ Alerte de Prière: %s%s + jamaat: | + + 🕌 *Paramètres Jamaat:* + Statut: %s + • Fajr: %s + • Chourouq: %s + • Dhuhr: %s + • Asr: %s + • Maghrib: %s + • Icha: %s + type: + private: "Privé" + group: "Groupe" + enabled: "✅ Activé" + disabled: "❌ Désactivé" + reply: start: "✍️ Tapez votre réponse ci-dessous (/cancel pour annuler)" success: "✅ Réponse envoyée" diff --git a/serverless/dispatcher/internal/handler/languages/ru.yaml b/serverless/dispatcher/internal/handler/languages/ru.yaml index 53b5963..7093b28 100644 --- a/serverless/dispatcher/internal/handler/languages/ru.yaml +++ b/serverless/dispatcher/internal/handler/languages/ru.yaml @@ -94,10 +94,40 @@ bug: success: "✅ Спасибо! Ваш отчет получен" help_admin: | + 📊 /info – Просмотр информации и настроек чата 📩 /reply – Ответить пользователю - 📊 /stats – Статистика использования бота + 📈 /stats – Статистика использования бота 📢 /announce – Отправить сообщение всем +info: + default: | + 📊 *Информация о Чате* + + 🆔 *ID Чата:* `%s` + 💬 *Тип:* %s + 🌐 *Язык:* %s + 📍 *Состояние:* %s + + ⏰ *Настройки Напоминаний:* + Статус: %s + • 🌅 Предпросмотр Завтра: %s + • ⏰ Напоминание о Молитве: %s%s + jamaat: | + + 🕌 *Настройки Джамаата:* + Статус: %s + • Фаджр: %s + • Шурук: %s + • Зухр: %s + • Аср: %s + • Магриб: %s + • Иша: %s + type: + private: "Личный" + group: "Группа" + enabled: "✅ Включено" + disabled: "❌ Выключено" + reply: start: "✍️ Напишите ваш ответ ниже (или /cancel для отмены)" success: "✅ Ответ отправлен" diff --git a/serverless/dispatcher/internal/handler/languages/tr.yaml b/serverless/dispatcher/internal/handler/languages/tr.yaml index 4118355..ca514c0 100644 --- a/serverless/dispatcher/internal/handler/languages/tr.yaml +++ b/serverless/dispatcher/internal/handler/languages/tr.yaml @@ -94,10 +94,40 @@ bug: success: "✅ Sag boluň! Nädogrulyk hasaba alyndy" help_admin: | + 📊 /info – Söhbetdeşlik maglumatlary we sazlamalary görmek 📩 /reply – Ulanyjylara jogap bermek - 📊 /stats – Baryň statistikasyny görmek + 📈 /stats – Baryň statistikasyny görmek 📢 /announce – Ähli ulanyjylara habar ugratmak +info: + default: | + 📊 *Söhbetdeşlik Maglumatlary* + + 🆔 *Söhbetdeşlik ID:* `%s` + 💬 *Görnüşi:* %s + 🌐 *Dil:* %s + 📍 *Ýagdaý:* %s + + ⏰ *Duýduryş Sazlamalary:* + Ýagdaý: %s + • 🌅 Ertir Syn: %s + • ⏰ Namaz Duýduryşy: %s%s + jamaat: | + + 🕌 *Jemgyýet Sazlamalary:* + Ýagdaý: %s + • Fäjir: %s + • Şuruk: %s + • Gündiz: %s + • Ikindi: %s + • Agşam: %s + • Gije: %s + type: + private: "Şahsy" + group: "Topar" + enabled: "✅ Açyk" + disabled: "❌ Ýapyk" + reply: start: "✍️ Jogabyňyzy aşakda ýazyň (ýa-da /cancel ýazyp duruzuň)" success: "✅ Jogap ugradyldy" diff --git a/serverless/dispatcher/internal/handler/languages/tt.yaml b/serverless/dispatcher/internal/handler/languages/tt.yaml index 3fc6f80..a47ba7d 100644 --- a/serverless/dispatcher/internal/handler/languages/tt.yaml +++ b/serverless/dispatcher/internal/handler/languages/tt.yaml @@ -94,10 +94,40 @@ bug: success: "✅ Рәхмәт! Хатагыз кабул ителде" help_admin: | + 📊 /info – Чат мәгълүматы һәм көйләүләр карау 📩 /reply – Кулланучыларга җавап бирү - 📊 /stats – Бот куллану статистикасы + 📈 /stats – Бот куллану статистикасы 📢 /announce – Бөтен кулланучыларга хәбәр җибәрү +info: + default: | + 📊 *Чат Мәгълүматы* + + 🆔 *Чат ID:* `%s` + 💬 *Төр:* %s + 🌐 *Тел:* %s + 📍 *Халәт:* %s + + ⏰ *Хәтерләтү Көйләүләре:* + Халәт: %s + • 🌅 Иртәгә Карау: %s + • ⏰ Намаз Искәрмәсе: %s%s + jamaat: | + + 🕌 *Җәмәгать Көйләүләре:* + Халәт: %s + • Фәҗр: %s + • Шөрак: %s + • Зөһр: %s + • Әср: %s + • Мәгъриб: %s + • Иша: %s + type: + private: "Шәхси" + group: "Төркем" + enabled: "✅ Кабызылган" + disabled: "❌ Сүндерелгән" + reply: start: "✍️ Җавапны түбәндә языгыз (яки /cancel язарга)" success: "✅ Җавап җибәрелде" diff --git a/serverless/dispatcher/internal/handler/languages/uz.yaml b/serverless/dispatcher/internal/handler/languages/uz.yaml index 3d9b7c4..3fb1def 100644 --- a/serverless/dispatcher/internal/handler/languages/uz.yaml +++ b/serverless/dispatcher/internal/handler/languages/uz.yaml @@ -95,10 +95,40 @@ bug: success: "✅ Rahmat! Xatolik qabul qilindi" help_admin: | + 📊 /info – Chat ma'lumotlari va sozlamalarini ko'rish 📩 /reply – Foydalanuvchilarga javob berish - 📊 /stats – Bot statistikasi + 📈 /stats – Bot statistikasi 📢 /announce – Hamma foydalanuvchilarga xabar yuborish +info: + default: | + 📊 *Chat Ma'lumotlari* + + 🆔 *Chat ID:* `%s` + 💬 *Turi:* %s + 🌐 *Til:* %s + 📍 *Holat:* %s + + ⏰ *Eslatma Sozlamalari:* + Holat: %s + • 🌅 Ertangi Ko'rinish: %s + • ⏰ Namoz Eslatmasi: %s%s + jamaat: | + + 🕌 *Jamoat Sozlamalari:* + Holat: %s + • Bomdod: %s + • Quyosh: %s + • Peshin: %s + • Asr: %s + • Shom: %s + • Xufton: %s + type: + private: "Shaxsiy" + group: "Guruh" + enabled: "✅ Yoqilgan" + disabled: "❌ O'chirilgan" + reply: start: "✍️ Javobni quyida yozing (/cancel yozib to‘xtatish mumkin)" success: "✅ Javob yuborildi" diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index b371fae..809eafd 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -224,7 +224,7 @@ func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *model return domain.ErrInternal } - reminderType := domain.ReminderType(parts[2]) + reminderType := domain.ReminderType(strings.TrimSuffix(parts[2], "|")) var messageText string var offset time.Duration @@ -239,7 +239,7 @@ func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *model log.Error("remindEditQuery: unknown reminder type", log.BotID(chat.BotID), log.ChatID(chat.ChatID), - log.String("type", string(reminderType)), + log.String("type", reminderType.String()), ) return domain.ErrInternal } From ab97c092269676d70c53cc9a107f37768d58e9d0 Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 00:15:12 +0300 Subject: [PATCH 47/59] feat: update go.mod --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 ++ serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 ++ serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 04e883d..c549d84 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e + github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index d311cda..e27b663 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -585,6 +585,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 9e9c428..6c3287b 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e + github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index b852ec1..ec7d4c5 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -587,6 +587,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index fd734de..b463570 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e + github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index d311cda..e27b663 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -585,6 +585,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= +github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= From 5af98b73d4adc9fdf05ad417c5eb2d6a9906fe9b Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 00:35:12 +0300 Subject: [PATCH 48/59] feat: update domain --- domain/reminder.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/domain/reminder.go b/domain/reminder.go index 56c69a0..7a9d773 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -17,7 +17,6 @@ func (rt ReminderType) String() string { type ( JamaatDelayConfig struct { Fajr time.Duration `json:"fajr"` - Shuruq time.Duration `json:"shuruq"` Dhuhr time.Duration `json:"dhuhr"` Asr time.Duration `json:"asr"` Maghrib time.Duration `json:"maghrib"` @@ -47,8 +46,6 @@ func (j *JamaatDelayConfig) GetDelayByPrayerID(prayerID PrayerID) time.Duration switch prayerID { case PrayerIDFajr: return j.Fajr - case PrayerIDShuruq: - return j.Shuruq case PrayerIDDhuhr: return j.Dhuhr case PrayerIDAsr: @@ -66,8 +63,6 @@ func (j *JamaatDelayConfig) SetDelayByPrayerID(prayerID PrayerID, delay time.Dur switch prayerID { case PrayerIDFajr: j.Fajr = delay - case PrayerIDShuruq: - j.Shuruq = delay case PrayerIDDhuhr: j.Dhuhr = delay case PrayerIDAsr: From a36e47f87df8a95743f2e491b32c37bd905a24b9 Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 00:42:11 +0300 Subject: [PATCH 49/59] feat: remove shuruq from jamaat --- _scripts/migrate/main.go | 2 - serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 + .../dispatcher/internal/handler/command.go | 1 - .../dispatcher/internal/handler/handler.go | 1 - .../dispatcher/internal/handler/keyboard.go | 39 +++++++++---------- .../internal/handler/languages/ar.yaml | 1 - .../internal/handler/languages/en.yaml | 1 - .../internal/handler/languages/es.yaml | 1 - .../internal/handler/languages/fr.yaml | 1 - .../internal/handler/languages/ru.yaml | 1 - .../internal/handler/languages/tr.yaml | 1 - .../internal/handler/languages/tt.yaml | 1 - .../internal/handler/languages/uz.yaml | 1 - serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 + serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 + .../reminder/internal/handler/handler.go | 5 +-- .../reminder/internal/handler/reminder.go | 38 ++++++++++-------- 20 files changed, 51 insertions(+), 55 deletions(-) diff --git a/_scripts/migrate/main.go b/_scripts/migrate/main.go index 43fe785..125b6c3 100644 --- a/_scripts/migrate/main.go +++ b/_scripts/migrate/main.go @@ -24,7 +24,6 @@ import ( // Domain types matching the new schema type JamaatDelayConfig struct { Fajr int64 `json:"fajr"` // nanoseconds - Shuruq int64 `json:"shuruq"` // nanoseconds Dhuhr int64 `json:"dhuhr"` // nanoseconds Asr int64 `json:"asr"` // nanoseconds Maghrib int64 `json:"maghrib"` // nanoseconds @@ -233,7 +232,6 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err Enabled: subscribed && jamaat, Delay: &JamaatDelayConfig{ Fajr: int64(10 * time.Minute), - Shuruq: int64(10 * time.Minute), Dhuhr: int64(10 * time.Minute), Asr: int64(10 * time.Minute), Maghrib: int64(10 * time.Minute), diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index c549d84..d25b379 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 + github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index e27b663..760b93d 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -587,6 +587,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdv github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index 3010206..c044002 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -312,7 +312,6 @@ func (h *Handler) info(ctx context.Context, b *bot.Bot, _ *models.Update) error jamaatInfo = fmt.Sprintf(text.Info.Jamaat, jamaatStatus, domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr), - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Shuruq), domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr), domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr), domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib), diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 1b9b9d0..8946dd9 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -253,7 +253,6 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C Jamaat: &domain.JamaatConfig{ Delay: &domain.JamaatDelayConfig{ Fajr: 10 * time.Minute, - Shuruq: 10 * time.Minute, Dhuhr: 10 * time.Minute, Asr: 10 * time.Minute, Maghrib: 10 * time.Minute, diff --git a/serverless/dispatcher/internal/handler/keyboard.go b/serverless/dispatcher/internal/handler/keyboard.go index 1ee1f03..5889da8 100644 --- a/serverless/dispatcher/internal/handler/keyboard.go +++ b/serverless/dispatcher/internal/handler/keyboard.go @@ -167,7 +167,15 @@ func (h *Handler) remindEditKeyboard(reminderType domain.ReminderType, languageC func (h *Handler) jammatMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMarkup { text := h.lp.GetText(chat.LanguageCode) - kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, 5)} + prayerIDs := []domain.PrayerID{ + domain.PrayerIDFajr, + domain.PrayerIDDhuhr, + domain.PrayerIDAsr, + domain.PrayerIDMaghrib, + domain.PrayerIDIsha, + } + + kb := &models.InlineKeyboardMarkup{InlineKeyboard: make([][]models.InlineKeyboardButton, len(prayerIDs)+2)} if chat.Reminder.Jamaat.Enabled { kb.InlineKeyboard[0] = []models.InlineKeyboardButton{ @@ -179,28 +187,17 @@ func (h *Handler) jammatMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa } } - fajrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr) - shuruqDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Shuruq) - kb.InlineKeyboard[1] = []models.InlineKeyboardButton{ - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDFajr)], fajrDelay), CallbackData: "remind:jamaat:edit:fajr|"}, - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDShuruq)], shuruqDelay), CallbackData: "remind:jamaat:edit:shuruq|"}, - } - - dhuhrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr) - asrDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr) - kb.InlineKeyboard[2] = []models.InlineKeyboardButton{ - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDDhuhr)], dhuhrDelay), CallbackData: "remind:jamaat:edit:dhuhr|"}, - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDAsr)], asrDelay), CallbackData: "remind:jamaat:edit:asr|"}, - } - - maghribDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib) - ishaDelay := domain.FormatDuration(chat.Reminder.Jamaat.Delay.Isha) - kb.InlineKeyboard[3] = []models.InlineKeyboardButton{ - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDMaghrib)], maghribDelay), CallbackData: "remind:jamaat:edit:maghrib|"}, - {Text: fmt.Sprintf("%s (%s)", text.Prayer[int(domain.PrayerIDIsha)], ishaDelay), CallbackData: "remind:jamaat:edit:isha|"}, + for i, prayerID := range prayerIDs { + delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) + kb.InlineKeyboard[i+1] = []models.InlineKeyboardButton{ + { + Text: fmt.Sprintf("%s (%s)", text.Prayer[int(prayerID)], domain.FormatDuration(delay)), + CallbackData: fmt.Sprintf("remind:jamaat:edit:%s|", prayerID.String()), + }, + } } - kb.InlineKeyboard[4] = []models.InlineKeyboardButton{ + kb.InlineKeyboard[len(prayerIDs)+1] = []models.InlineKeyboardButton{ {Text: buttonBack, CallbackData: "remind:back:menu|"}, } diff --git a/serverless/dispatcher/internal/handler/languages/ar.yaml b/serverless/dispatcher/internal/handler/languages/ar.yaml index 549324e..bc7ab5e 100644 --- a/serverless/dispatcher/internal/handler/languages/ar.yaml +++ b/serverless/dispatcher/internal/handler/languages/ar.yaml @@ -117,7 +117,6 @@ info: 🕌 *إعدادات الجماعة:* الحالة: %s • الفجر: %s - • الشروق: %s • الظهر: %s • العصر: %s • المغرب: %s diff --git a/serverless/dispatcher/internal/handler/languages/en.yaml b/serverless/dispatcher/internal/handler/languages/en.yaml index d5cbbf9..d06d324 100644 --- a/serverless/dispatcher/internal/handler/languages/en.yaml +++ b/serverless/dispatcher/internal/handler/languages/en.yaml @@ -117,7 +117,6 @@ info: 🕌 *Jamaat Settings:* Status: %s • Fajr: %s - • Shuruq: %s • Dhuhr: %s • Asr: %s • Maghrib: %s diff --git a/serverless/dispatcher/internal/handler/languages/es.yaml b/serverless/dispatcher/internal/handler/languages/es.yaml index 57ebd36..fa0ffb7 100644 --- a/serverless/dispatcher/internal/handler/languages/es.yaml +++ b/serverless/dispatcher/internal/handler/languages/es.yaml @@ -117,7 +117,6 @@ info: 🕌 *Configuración de Jamaat:* Estado: %s • Fajr: %s - • Shuruq: %s • Dhuhr: %s • Asr: %s • Maghrib: %s diff --git a/serverless/dispatcher/internal/handler/languages/fr.yaml b/serverless/dispatcher/internal/handler/languages/fr.yaml index fb8bc7e..42a3372 100644 --- a/serverless/dispatcher/internal/handler/languages/fr.yaml +++ b/serverless/dispatcher/internal/handler/languages/fr.yaml @@ -117,7 +117,6 @@ info: 🕌 *Paramètres Jamaat:* Statut: %s • Fajr: %s - • Chourouq: %s • Dhuhr: %s • Asr: %s • Maghrib: %s diff --git a/serverless/dispatcher/internal/handler/languages/ru.yaml b/serverless/dispatcher/internal/handler/languages/ru.yaml index 7093b28..5c08322 100644 --- a/serverless/dispatcher/internal/handler/languages/ru.yaml +++ b/serverless/dispatcher/internal/handler/languages/ru.yaml @@ -117,7 +117,6 @@ info: 🕌 *Настройки Джамаата:* Статус: %s • Фаджр: %s - • Шурук: %s • Зухр: %s • Аср: %s • Магриб: %s diff --git a/serverless/dispatcher/internal/handler/languages/tr.yaml b/serverless/dispatcher/internal/handler/languages/tr.yaml index ca514c0..a0b3dea 100644 --- a/serverless/dispatcher/internal/handler/languages/tr.yaml +++ b/serverless/dispatcher/internal/handler/languages/tr.yaml @@ -117,7 +117,6 @@ info: 🕌 *Jemgyýet Sazlamalary:* Ýagdaý: %s • Fäjir: %s - • Şuruk: %s • Gündiz: %s • Ikindi: %s • Agşam: %s diff --git a/serverless/dispatcher/internal/handler/languages/tt.yaml b/serverless/dispatcher/internal/handler/languages/tt.yaml index a47ba7d..507716a 100644 --- a/serverless/dispatcher/internal/handler/languages/tt.yaml +++ b/serverless/dispatcher/internal/handler/languages/tt.yaml @@ -117,7 +117,6 @@ info: 🕌 *Җәмәгать Көйләүләре:* Халәт: %s • Фәҗр: %s - • Шөрак: %s • Зөһр: %s • Әср: %s • Мәгъриб: %s diff --git a/serverless/dispatcher/internal/handler/languages/uz.yaml b/serverless/dispatcher/internal/handler/languages/uz.yaml index 3fb1def..4e1c44e 100644 --- a/serverless/dispatcher/internal/handler/languages/uz.yaml +++ b/serverless/dispatcher/internal/handler/languages/uz.yaml @@ -118,7 +118,6 @@ info: 🕌 *Jamoat Sozlamalari:* Holat: %s • Bomdod: %s - • Quyosh: %s • Peshin: %s • Asr: %s • Shom: %s diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 6c3287b..6083c5a 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 + github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index ec7d4c5..709ed71 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -589,6 +589,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdv github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index b463570..940fca5 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 + github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index e27b663..760b93d 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -587,6 +587,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdv github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= +github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/handler.go b/serverless/reminder/internal/handler/handler.go index 1109d5e..34c7258 100644 --- a/serverless/reminder/internal/handler/handler.go +++ b/serverless/reminder/internal/handler/handler.go @@ -128,7 +128,7 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { } for _, reminder := range reminders { - shouldSend, prayerID := reminder.Check(ctx, chat, prayerDay, now) + shouldSend, prayerID, nextLastAt := reminder.ShouldTrigger(ctx, chat, prayerDay, now) if !shouldSend { continue } @@ -148,8 +148,7 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { continue } - now = now.Add(1 * time.Minute) // increment by 1 minute to avoid sending the same reminder multiple times - err = h.db.UpdateReminder(ctx, chat.BotID, chat.ChatID, reminder.Name(), messageID, now) + err = h.db.UpdateReminder(ctx, chat.BotID, chat.ChatID, reminder.Name(), messageID, nextLastAt) if err != nil { log.Error("update reminder state", log.Err(err), diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index c863e82..9b3c1c0 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -11,7 +11,7 @@ import ( ) type ReminderType interface { - Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID) + ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID, nextLastAt time.Time) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (messageID int, err error) Name() domain.ReminderType } @@ -22,19 +22,25 @@ type TomorrowReminder struct { formatPrayerDay func(botID int64, prayerDay *domain.PrayerDay, languageCode string) string } -func (r *TomorrowReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { +func (r *TomorrowReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { config := chat.Reminder.Tomorrow + // Trigger logic: last_at + 24h - offset < now lastDate := config.LastAt.Truncate(24 * time.Hour) triggerTime := lastDate.Add(24 * time.Hour).Add(-config.Offset) - return triggerTime.Before(now) || triggerTime.Equal(now), domain.PrayerIDUnknown + shouldSend := triggerTime.Before(now) || triggerTime.Equal(now) + + // Next LastAt should be tomorrow's date (midnight) to prevent re-triggering on the same day + nextLastAt := now.Truncate(24 * time.Hour).Add(24 * time.Hour) + + return shouldSend, domain.PrayerIDUnknown, nextLastAt } -func (r *TomorrowReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { +func (r *TomorrowReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, _ domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { deleteMessages(ctx, b, chat, chat.Reminder.Tomorrow.MessageID) res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, - Text: r.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode), + Text: r.formatPrayerDay(chat.BotID, prayerDay.NextDay, chat.LanguageCode), }) if err != nil { return 0, err @@ -49,7 +55,7 @@ type SoonReminder struct { lp *languagesProvider } -func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { +func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { prayers := []struct { id domain.PrayerID time time.Time @@ -69,10 +75,12 @@ func (r *SoonReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay * // logic: last_at < (prayer_time - offset) AND (prayer_time - offset) <= now trigger := p.time.Add(-config.Offset) if config.LastAt.Before(trigger) && (trigger.Before(now) || trigger.Equal(now)) { - return true, p.id + // // Next LastAt should be incremented by 1 minute to avoid re-triggering + // nextLastAt := now.Add(1 * time.Minute) + return true, p.id, now } } - return false, 0 + return false, 0, time.Time{} } func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { @@ -81,7 +89,7 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, deleteMessages(ctx, b, chat, chat.Reminder.Soon.MessageID, chat.Reminder.Arrive.MessageID) - if chat.Reminder.Jamaat.Enabled { + if chat.Reminder.Jamaat.Enabled && prayerID != domain.PrayerIDShuruq { delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) message := fmt.Sprintf("%s\n%s", fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), @@ -126,11 +134,7 @@ type ArriveReminder struct { lp *languagesProvider } -func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { - if chat.Reminder == nil { - return false, 0 - } - +func (r *ArriveReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { config := chat.Reminder.Arrive prayers := []struct { @@ -149,10 +153,12 @@ func (r *ArriveReminder) Check(ctx context.Context, chat *domain.Chat, prayerDay // logic: last_at < prayer_time AND prayer_time <= now if config.LastAt.Before(p.time) && (p.time.Before(now) || p.time.Equal(now)) { - return true, p.id + // // Next LastAt should be incremented by 1 minute to avoid re-triggering + // nextLastAt := now.Add(1 * time.Minute) + return true, p.id, now } } - return false, 0 + return false, 0, time.Time{} } func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { From d54841f65d07020cd85d4c32d19d362536b1a989 Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 00:58:28 +0300 Subject: [PATCH 50/59] feat: update dispatcher --- serverless/dispatcher/internal/handler/command.go | 4 ++-- serverless/dispatcher/internal/handler/helper.go | 6 ------ serverless/dispatcher/internal/handler/query.go | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index c044002..12632cd 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -88,7 +88,7 @@ func (h *Handler) help(ctx context.Context, b *bot.Bot, _ *models.Update) error func (h *Handler) today(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) - prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, h.nowUTC(chat.BotID)) + prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, h.now(chat.BotID)) if err != nil { log.Error("today: get prayer day", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) return domain.ErrInternal @@ -129,7 +129,7 @@ func (h *Handler) next(ctx context.Context, b *bot.Bot, _ *models.Update) error var ( now = h.now(chat.BotID) - date = h.nowUTC(chat.BotID) + date = now.Truncate(24 * time.Hour) ) prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, date) diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 3bfc6df..8f4af23 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -53,12 +53,6 @@ func (h *Handler) now(botID int64) time.Time { return time.Now().In(h.cfg[botID].Location.V()).Truncate(time.Minute) } -// nowUTC returns the current time in UTC with seconds and nanoseconds set to 0 -// Use this function to get prayerDay or current year for a specific botID timezone. -func (h *Handler) nowUTC(botID int64) time.Time { - return time.Now().In(h.cfg[botID].Location.V()).Truncate(time.Minute).UTC() -} - // daysInMonth returns the number of days in a month. func daysInMonth(month time.Month, year int) int { // month is incremented by 1 and day is 0 because we want the last day of the month. diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index 809eafd..f1ccc73 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -76,7 +76,7 @@ func (h *Handler) monthQuery(ctx context.Context, b *bot.Bot, update *models.Upd ChatID: chat.ChatID, MessageID: update.CallbackQuery.Message.Message.ID, Text: h.lp.GetText(chat.LanguageCode).PrayerDate, - ReplyMarkup: h.daysKeyboard(h.nowUTC(chat.BotID), month), + ReplyMarkup: h.daysKeyboard(h.now(chat.BotID), month), }) if err != nil { log.Error("monthQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) @@ -98,7 +98,7 @@ func (h *Handler) dayQuery(ctx context.Context, b *bot.Bot, update *models.Updat parts := strings.Split(update.CallbackQuery.Data, dataSplitterQuery) month, _ := strconv.Atoi(parts[1]) day, _ := strconv.Atoi(parts[2]) - date := domain.DateUTC(day, time.Month(month), h.nowUTC(chat.BotID).Year()) + date := domain.DateUTC(day, time.Month(month), h.now(chat.BotID).Year()) prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, date) if err != nil { From 06c8426f7e90e7c062539c276978afb7207e10e6 Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 01:10:28 +0300 Subject: [PATCH 51/59] feat: add tomorrow comand --- .../dispatcher/internal/handler/command.go | 28 +++++++++++++++++-- .../dispatcher/internal/handler/handler.go | 1 + .../dispatcher/internal/handler/helper.go | 5 ++++ .../internal/handler/languages/ar.yaml | 1 + .../internal/handler/languages/en.yaml | 1 + .../internal/handler/languages/es.yaml | 1 + .../internal/handler/languages/fr.yaml | 11 ++++---- .../internal/handler/languages/ru.yaml | 1 + .../internal/handler/languages/tr.yaml | 1 + .../internal/handler/languages/tt.yaml | 1 + .../internal/handler/languages/uz.yaml | 5 ++-- .../dispatcher/internal/handler/query.go | 4 +-- 12 files changed, 49 insertions(+), 11 deletions(-) diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index 12632cd..ebe9db3 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -40,6 +40,7 @@ const ( startCommand command = "start" helpCommand command = "help" todayCommand command = "today" + tomorrowCommand command = "tomorrow" dateCommand command = "date" // 2 stages nextCommand command = "next" remindCommand command = "remind" // 1 stage @@ -88,7 +89,7 @@ func (h *Handler) help(ctx context.Context, b *bot.Bot, _ *models.Update) error func (h *Handler) today(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) - prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, h.now(chat.BotID)) + prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, h.nowDateUTC(chat.BotID)) if err != nil { log.Error("today: get prayer day", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) return domain.ErrInternal @@ -107,6 +108,29 @@ func (h *Handler) today(ctx context.Context, b *bot.Bot, _ *models.Update) error return nil } +func (h *Handler) tomorrow(ctx context.Context, b *bot.Bot, _ *models.Update) error { + chat := getContextChat(ctx) + + tomorrow := h.nowDateUTC(chat.BotID).Add(24 * time.Hour) + prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, tomorrow) + if err != nil { + log.Error("tomorrow: get prayer day", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + _, err = b.SendMessage(ctx, &bot.SendMessageParams{ + ChatID: chat.ChatID, + Text: h.formatPrayerDay(chat.BotID, prayerDay, chat.LanguageCode), + }) + if err != nil { + log.Error("tomorrow: send message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) + return domain.ErrInternal + } + + h.resetState(ctx, chat) + return nil +} + func (h *Handler) date(ctx context.Context, b *bot.Bot, _ *models.Update) error { chat := getContextChat(ctx) @@ -129,7 +153,7 @@ func (h *Handler) next(ctx context.Context, b *bot.Bot, _ *models.Update) error var ( now = h.now(chat.BotID) - date = now.Truncate(24 * time.Hour) + date = h.nowDateUTC(chat.BotID) ) prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, date) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 8946dd9..bf0aea0 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -63,6 +63,7 @@ func (h *Handler) opts() []bot.Option { bot.WithMessageTextHandler(startCommand.String(), bot.MatchTypeCommandStartOnly, h.errorH(h.chatH(h.start))), bot.WithMessageTextHandler(helpCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.help))), bot.WithMessageTextHandler(todayCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.today))), + bot.WithMessageTextHandler(tomorrowCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.tomorrow))), bot.WithMessageTextHandler(dateCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.date))), bot.WithMessageTextHandler(nextCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.next))), bot.WithMessageTextHandler(remindCommand.String(), bot.MatchTypeCommand, h.errorH(h.chatH(h.remind))), diff --git a/serverless/dispatcher/internal/handler/helper.go b/serverless/dispatcher/internal/handler/helper.go index 8f4af23..cc308c0 100644 --- a/serverless/dispatcher/internal/handler/helper.go +++ b/serverless/dispatcher/internal/handler/helper.go @@ -53,6 +53,11 @@ func (h *Handler) now(botID int64) time.Time { return time.Now().In(h.cfg[botID].Location.V()).Truncate(time.Minute) } +func (h *Handler) nowDateUTC(botID int64) time.Time { + now := h.now(botID) + return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) +} + // daysInMonth returns the number of days in a month. func daysInMonth(month time.Month, year int) int { // month is incremented by 1 and day is 0 because we want the last day of the month. diff --git a/serverless/dispatcher/internal/handler/languages/ar.yaml b/serverless/dispatcher/internal/handler/languages/ar.yaml index bc7ab5e..1802fb8 100644 --- a/serverless/dispatcher/internal/handler/languages/ar.yaml +++ b/serverless/dispatcher/internal/handler/languages/ar.yaml @@ -71,6 +71,7 @@ help: | *الصلاة:* ⏰ /today – مواقيت اليوم + 🌅 /tomorrow – مواقيت الغد 📅 /date – مواقيت يوم محدد ⏳ /next – متى الصلاة القادمة؟ 📨 /remind – إعدادات التذكير diff --git a/serverless/dispatcher/internal/handler/languages/en.yaml b/serverless/dispatcher/internal/handler/languages/en.yaml index d06d324..2e8e4d2 100644 --- a/serverless/dispatcher/internal/handler/languages/en.yaml +++ b/serverless/dispatcher/internal/handler/languages/en.yaml @@ -71,6 +71,7 @@ help: | *Prayers:* ⏰ /today – Today's times + 🌅 /tomorrow – Tomorrow's times 📅 /date – Times for a chosen day ⏳ /next – When's the next prayer? 📨 /remind – Configure reminders diff --git a/serverless/dispatcher/internal/handler/languages/es.yaml b/serverless/dispatcher/internal/handler/languages/es.yaml index fa0ffb7..d9865b7 100644 --- a/serverless/dispatcher/internal/handler/languages/es.yaml +++ b/serverless/dispatcher/internal/handler/languages/es.yaml @@ -71,6 +71,7 @@ help: | *Oraciones:* ⏰ /today – Horarios de hoy + 🌅 /tomorrow – Horarios de mañana 📅 /date – Horarios de un día específico ⏳ /next – ¿Cuándo es la próxima oración? 📨 /remind – Configurar recordatorios diff --git a/serverless/dispatcher/internal/handler/languages/fr.yaml b/serverless/dispatcher/internal/handler/languages/fr.yaml index 42a3372..f0c4e51 100644 --- a/serverless/dispatcher/internal/handler/languages/fr.yaml +++ b/serverless/dispatcher/internal/handler/languages/fr.yaml @@ -66,13 +66,14 @@ jamaat_edit: title: "🕌 Horaire Jamaat %s\n⏱ Temps après l'Adhan" help: | - Besoin d'aide ? 😊 Voici ce que je peux faire : 👇 + Besoin d'aide ? 😊 Voici ce que je peux faire : 👇 *Prières:* ⏰ /today – Horaires d'aujourd'hui + 🌅 /tomorrow – Horaires de demain 📅 /date – Horaires pour une date choisie - ⏳ /next – Quand est la prochaine prière ? + ⏳ /next – Quand est la prochaine prière ? 📨 /remind – Configurer les rappels *Paramètres:* @@ -86,12 +87,12 @@ help: | 💬 /feedback – Envoyer un retour feedback: - start: "💬 Partagez vos commentaires ! (ou tapez /cancel pour arrêter)" - success: "✅ Merci ! Votre commentaire a été envoyé" + start: "💬 Partagez vos commentaires ! (ou tapez /cancel pour arrêter)" + success: "✅ Merci ! Votre commentaire a été envoyé" bug: start: "🐞 Décrivez le bug rencontré (ou tapez /cancel pour arrêter)" - success: "✅ Merci ! Votre rapport a été reçu" + success: "✅ Merci ! Votre rapport a été reçu" help_admin: | 📊 /info – Voir les infos et paramètres du chat diff --git a/serverless/dispatcher/internal/handler/languages/ru.yaml b/serverless/dispatcher/internal/handler/languages/ru.yaml index 5c08322..24563ea 100644 --- a/serverless/dispatcher/internal/handler/languages/ru.yaml +++ b/serverless/dispatcher/internal/handler/languages/ru.yaml @@ -71,6 +71,7 @@ help: | *Молитвы:* ⏰ /today – Время молитв на сегодня + 🌅 /tomorrow – Время молитв на завтра 📅 /date – Время молитв на выбранный день ⏳ /next – Когда следующая молитва? 📨 /remind – Настроить напоминания diff --git a/serverless/dispatcher/internal/handler/languages/tr.yaml b/serverless/dispatcher/internal/handler/languages/tr.yaml index a0b3dea..783f40b 100644 --- a/serverless/dispatcher/internal/handler/languages/tr.yaml +++ b/serverless/dispatcher/internal/handler/languages/tr.yaml @@ -71,6 +71,7 @@ help: | *Namazlar:* ⏰ /today – Şu günki wagtlar + 🌅 /tomorrow – Ertirki wagtlar 📅 /date – Saýlanan gün üçin wagtlar ⏳ /next – Indiki namaz haçan? 📨 /remind – Duýduryşlary sazlamak diff --git a/serverless/dispatcher/internal/handler/languages/tt.yaml b/serverless/dispatcher/internal/handler/languages/tt.yaml index 507716a..c99947a 100644 --- a/serverless/dispatcher/internal/handler/languages/tt.yaml +++ b/serverless/dispatcher/internal/handler/languages/tt.yaml @@ -71,6 +71,7 @@ help: | *Намазлар:* ⏰ /today – Бүгенге вакытлар + 🌅 /tomorrow – Иртәгәге вакытлар 📅 /date – Күрсәтелгән көн өчен вакытлар ⏳ /next – Киләсе намаз кайчан? 📨 /remind – Хәтерләтүләрне көйләргә diff --git a/serverless/dispatcher/internal/handler/languages/uz.yaml b/serverless/dispatcher/internal/handler/languages/uz.yaml index 4e1c44e..9e8ed7b 100644 --- a/serverless/dispatcher/internal/handler/languages/uz.yaml +++ b/serverless/dispatcher/internal/handler/languages/uz.yaml @@ -72,17 +72,18 @@ help: | *Namozlar:* ⏰ /today – Bugungi vaqtlar + 🌅 /tomorrow – Ertangi vaqtlar 📅 /date – Tanlangan kun uchun vaqtlar ⏳ /next – Keyingi namoz qachon? 📨 /remind – Eslatmalarni sozlash *Sozlamalar:* - 🌐 /language – Tilni o‘zgartirish + 🌐 /language – Tilni o'zgartirish *Yordam:* - 📖 /help – Ushbu qo‘llanmani ko‘rsatish + 📖 /help – Ushbu qo'llanmani ko'rsatish 🐞 /bug – Xatolik haqida xabar berish 💬 /feedback – Fikr-mulohaza yuborish diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index f1ccc73..8271dd1 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -76,7 +76,7 @@ func (h *Handler) monthQuery(ctx context.Context, b *bot.Bot, update *models.Upd ChatID: chat.ChatID, MessageID: update.CallbackQuery.Message.Message.ID, Text: h.lp.GetText(chat.LanguageCode).PrayerDate, - ReplyMarkup: h.daysKeyboard(h.now(chat.BotID), month), + ReplyMarkup: h.daysKeyboard(h.nowDateUTC(chat.BotID), month), }) if err != nil { log.Error("monthQuery: edit message", log.Err(err), log.BotID(chat.BotID), log.ChatID(chat.ChatID)) @@ -98,7 +98,7 @@ func (h *Handler) dayQuery(ctx context.Context, b *bot.Bot, update *models.Updat parts := strings.Split(update.CallbackQuery.Data, dataSplitterQuery) month, _ := strconv.Atoi(parts[1]) day, _ := strconv.Atoi(parts[2]) - date := domain.DateUTC(day, time.Month(month), h.now(chat.BotID).Year()) + date := domain.DateUTC(day, time.Month(month), h.nowDateUTC(chat.BotID).Year()) prayerDay, err := h.db.GetPrayerDay(ctx, chat.BotID, date) if err != nil { From 68d05da3b1594c785cc0074c23cb4c66a3bc6155 Mon Sep 17 00:00:00 2001 From: escalopa Date: Mon, 27 Oct 2025 01:21:07 +0300 Subject: [PATCH 52/59] fix: auth --- serverless/dispatcher/internal/handler/handler.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index bf0aea0..41077b0 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -134,12 +134,8 @@ func (h *Handler) authorizeH(fn func(ctx context.Context, b *bot.Bot, update *mo return func(ctx context.Context, b *bot.Bot, update *models.Update) error { chat := getContextChat(ctx) - var ( - isAdminUser = h.cfg[chat.BotID].OwnerID == update.Message.From.ID - isAdminChat = h.cfg[chat.BotID].OwnerID == update.Message.Chat.ID - ) - - if isAdminUser && isAdminChat { + isAdminUser := h.cfg[chat.BotID].OwnerID == update.Message.From.ID + if isAdminUser { return fn(ctx, b, update) } From a192c6fa32744a6a984f8227c821b17ea294d079 Mon Sep 17 00:00:00 2001 From: escalopa Date: Thu, 30 Oct 2025 03:12:59 +0300 Subject: [PATCH 53/59] feat: fix prayer time text --- .../reminder/internal/handler/handler.go | 4 +- .../reminder/internal/handler/reminder.go | 37 ++++++++----------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/serverless/reminder/internal/handler/handler.go b/serverless/reminder/internal/handler/handler.go index 34c7258..d3d3eb7 100644 --- a/serverless/reminder/internal/handler/handler.go +++ b/serverless/reminder/internal/handler/handler.go @@ -128,7 +128,7 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { } for _, reminder := range reminders { - shouldSend, prayerID, nextLastAt := reminder.ShouldTrigger(ctx, chat, prayerDay, now) + shouldSend, prayerID := reminder.ShouldTrigger(ctx, chat, prayerDay, now) if !shouldSend { continue } @@ -148,7 +148,7 @@ func (h *Handler) Handel(ctx context.Context, botID int64) error { continue } - err = h.db.UpdateReminder(ctx, chat.BotID, chat.ChatID, reminder.Name(), messageID, nextLastAt) + err = h.db.UpdateReminder(ctx, chat.BotID, chat.ChatID, reminder.Name(), messageID, now) if err != nil { log.Error("update reminder state", log.Err(err), diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index 9b3c1c0..197c28e 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -3,6 +3,7 @@ package handler import ( "context" "fmt" + "strings" "time" "github.com/escalopa/prayer-bot/domain" @@ -11,7 +12,7 @@ import ( ) type ReminderType interface { - ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID, nextLastAt time.Time) + ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (shouldSend bool, prayerID domain.PrayerID) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (messageID int, err error) Name() domain.ReminderType } @@ -22,18 +23,13 @@ type TomorrowReminder struct { formatPrayerDay func(botID int64, prayerDay *domain.PrayerDay, languageCode string) string } -func (r *TomorrowReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { +func (r *TomorrowReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, _ *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { config := chat.Reminder.Tomorrow // Trigger logic: last_at + 24h - offset < now - lastDate := config.LastAt.Truncate(24 * time.Hour) - triggerTime := lastDate.Add(24 * time.Hour).Add(-config.Offset) - shouldSend := triggerTime.Before(now) || triggerTime.Equal(now) - - // Next LastAt should be tomorrow's date (midnight) to prevent re-triggering on the same day - nextLastAt := now.Truncate(24 * time.Hour).Add(24 * time.Hour) - - return shouldSend, domain.PrayerIDUnknown, nextLastAt + triggerTime := config.LastAt.Truncate(24 * time.Hour).Add(24 * time.Hour).Add(-config.Offset) + shouldSend := config.LastAt.Before(triggerTime) && (triggerTime.Before(now) || triggerTime.Equal(now)) + return shouldSend, domain.PrayerIDUnknown } func (r *TomorrowReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, _ domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { @@ -55,7 +51,7 @@ type SoonReminder struct { lp *languagesProvider } -func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { +func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { prayers := []struct { id domain.PrayerID time time.Time @@ -77,10 +73,10 @@ func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, pra if config.LastAt.Before(trigger) && (trigger.Before(now) || trigger.Equal(now)) { // // Next LastAt should be incremented by 1 minute to avoid re-triggering // nextLastAt := now.Add(1 * time.Minute) - return true, p.id, now + return true, p.id } } - return false, 0, time.Time{} + return false, 0 } func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { @@ -92,8 +88,8 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, if chat.Reminder.Jamaat.Enabled && prayerID != domain.PrayerIDShuruq { delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) message := fmt.Sprintf("%s\n%s", - fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), - fmt.Sprintf(text.PrayerJamaat, domain.FormatDuration(delay)), + fmt.Sprintf(strings.ReplaceAll(text.PrayerSoon, "*", ""), prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), + fmt.Sprintf(strings.ReplaceAll(text.PrayerJamaat, "*", ""), domain.FormatDuration(delay+chat.Reminder.Soon.Offset)), ) isAnonymous := false @@ -134,7 +130,7 @@ type ArriveReminder struct { lp *languagesProvider } -func (r *ArriveReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID, time.Time) { +func (r *ArriveReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, prayerDay *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { config := chat.Reminder.Arrive prayers := []struct { @@ -151,14 +147,11 @@ func (r *ArriveReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, p for _, p := range prayers { // logic: last_at < prayer_time AND prayer_time <= now - if config.LastAt.Before(p.time) && - (p.time.Before(now) || p.time.Equal(now)) { - // // Next LastAt should be incremented by 1 minute to avoid re-triggering - // nextLastAt := now.Add(1 * time.Minute) - return true, p.id, now + if config.LastAt.Before(p.time) && (p.time.Before(now) || p.time.Equal(now)) { + return true, p.id } } - return false, 0, time.Time{} + return false, 0 } func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { From ebc4c681afa29ddc1aef09189d7ad17eacf13ca7 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 13:55:49 +0300 Subject: [PATCH 54/59] feat: add domain.Duration --- domain/duration.go | 26 ++++++++++++++++++++++++++ domain/reminder.go | 36 ++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 domain/duration.go diff --git a/domain/duration.go b/domain/duration.go new file mode 100644 index 0000000..5cccca5 --- /dev/null +++ b/domain/duration.go @@ -0,0 +1,26 @@ +package domain + +import ( + "encoding/json" + "fmt" + "time" +) + +type Duration time.Duration + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).String()) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err == nil { + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("invalid duration string %q: %w", s, err) + } + *d = Duration(dur) + return nil + } + return fmt.Errorf("unexpected duration format: %s", string(b)) +} diff --git a/domain/reminder.go b/domain/reminder.go index 7a9d773..2b6ad77 100644 --- a/domain/reminder.go +++ b/domain/reminder.go @@ -16,11 +16,11 @@ func (rt ReminderType) String() string { type ( JamaatDelayConfig struct { - Fajr time.Duration `json:"fajr"` - Dhuhr time.Duration `json:"dhuhr"` - Asr time.Duration `json:"asr"` - Maghrib time.Duration `json:"maghrib"` - Isha time.Duration `json:"isha"` + Fajr Duration `json:"fajr"` + Dhuhr Duration `json:"dhuhr"` + Asr Duration `json:"asr"` + Maghrib Duration `json:"maghrib"` + Isha Duration `json:"isha"` } JamaatConfig struct { @@ -29,9 +29,9 @@ type ( } ReminderConfig struct { - Offset time.Duration `json:"offset"` - MessageID int `json:"message_id"` - LastAt time.Time `json:"last_at"` + Offset Duration `json:"offset"` + MessageID int `json:"message_id"` + LastAt time.Time `json:"last_at"` } Reminder struct { @@ -45,15 +45,15 @@ type ( func (j *JamaatDelayConfig) GetDelayByPrayerID(prayerID PrayerID) time.Duration { switch prayerID { case PrayerIDFajr: - return j.Fajr + return time.Duration(j.Fajr) case PrayerIDDhuhr: - return j.Dhuhr + return time.Duration(j.Dhuhr) case PrayerIDAsr: - return j.Asr + return time.Duration(j.Asr) case PrayerIDMaghrib: - return j.Maghrib + return time.Duration(j.Maghrib) case PrayerIDIsha: - return j.Isha + return time.Duration(j.Isha) default: return 0 } @@ -62,14 +62,14 @@ func (j *JamaatDelayConfig) GetDelayByPrayerID(prayerID PrayerID) time.Duration func (j *JamaatDelayConfig) SetDelayByPrayerID(prayerID PrayerID, delay time.Duration) { switch prayerID { case PrayerIDFajr: - j.Fajr = delay + j.Fajr = Duration(delay) case PrayerIDDhuhr: - j.Dhuhr = delay + j.Dhuhr = Duration(delay) case PrayerIDAsr: - j.Asr = delay + j.Asr = Duration(delay) case PrayerIDMaghrib: - j.Maghrib = delay + j.Maghrib = Duration(delay) case PrayerIDIsha: - j.Isha = delay + j.Isha = Duration(delay) } } From 8db404412280b255b2fa9e67676b88ed69644bb7 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 14:02:31 +0300 Subject: [PATCH 55/59] feat: use domain.Duration --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 ++ serverless/dispatcher/internal/service/db.go | 2 ++ serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 ++ serverless/reminder/go.mod | 2 +- serverless/reminder/go.sum | 2 ++ serverless/reminder/internal/handler/reminder.go | 10 ++++------ 8 files changed, 15 insertions(+), 9 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index d25b379..0af1def 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad + github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 760b93d..64205bd 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -589,6 +589,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index 6d3027c..ca7d9dc 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -276,8 +276,10 @@ func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, switch reminderType { case domain.ReminderTypeTomorrow: reminder.Tomorrow.Offset = offset + reminder.Tomorrow.LastAt = time.Now() case domain.ReminderTypeSoon: reminder.Soon.Offset = offset + reminder.Soon.LastAt = time.Now() } }) } diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 6083c5a..4c31296 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad + github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 709ed71..63c1595 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -591,6 +591,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index 940fca5..f9223eb 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad + github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index 760b93d..64205bd 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -589,6 +589,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJ github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= +github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index 197c28e..f274448 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -26,8 +26,7 @@ type TomorrowReminder struct { func (r *TomorrowReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, _ *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { config := chat.Reminder.Tomorrow - // Trigger logic: last_at + 24h - offset < now - triggerTime := config.LastAt.Truncate(24 * time.Hour).Add(24 * time.Hour).Add(-config.Offset) + triggerTime := time.Date(now.Year(), now.Month(), now.Day()+1, int(-config.Offset.Hours()), 0, 0, 0, now.Location()) shouldSend := config.LastAt.Before(triggerTime) && (triggerTime.Before(now) || triggerTime.Equal(now)) return shouldSend, domain.PrayerIDUnknown } @@ -79,7 +78,7 @@ func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, pra return false, 0 } -func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { +func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, _ *domain.PrayerDay) (int, error) { text := r.lp.GetText(chat.LanguageCode) prayer := text.Prayer[int(prayerID)] @@ -109,8 +108,7 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, return res.ID, nil } - duration := chat.Reminder.Soon.Offset - message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(duration)) + message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)) res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, @@ -154,7 +152,7 @@ func (r *ArriveReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, p return false, 0 } -func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, prayerDay *domain.PrayerDay) (int, error) { +func (r *ArriveReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, prayerID domain.PrayerID, _ *domain.PrayerDay) (int, error) { deleteMessages(ctx, b, chat, chat.Reminder.Arrive.MessageID) text := r.lp.GetText(chat.LanguageCode) From 60a899da56022ec61fa52a1b9823fb2821b71949 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 14:03:02 +0300 Subject: [PATCH 56/59] feat: use domain.Duration in migration script --- _scripts/migrate/main.go | 49 ++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/_scripts/migrate/main.go b/_scripts/migrate/main.go index 125b6c3..a6a0914 100644 --- a/_scripts/migrate/main.go +++ b/_scripts/migrate/main.go @@ -21,13 +21,32 @@ import ( // cd _scripts/migrate // go run main.go +type Duration time.Duration + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).String()) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err == nil { + dur, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("invalid duration string %q: %w", s, err) + } + *d = Duration(dur) + return nil + } + return fmt.Errorf("unexpected duration format: %s", string(b)) +} + // Domain types matching the new schema type JamaatDelayConfig struct { - Fajr int64 `json:"fajr"` // nanoseconds - Dhuhr int64 `json:"dhuhr"` // nanoseconds - Asr int64 `json:"asr"` // nanoseconds - Maghrib int64 `json:"maghrib"` // nanoseconds - Isha int64 `json:"isha"` // nanoseconds + Fajr Duration `json:"fajr"` + Dhuhr Duration `json:"dhuhr"` + Asr Duration `json:"asr"` + Maghrib Duration `json:"maghrib"` + Isha Duration `json:"isha"` } type JamaatConfig struct { @@ -36,9 +55,9 @@ type JamaatConfig struct { } type ReminderConfig struct { - Offset int64 `json:"offset"` // nanoseconds - MessageID int `json:"message_id"` - LastAt string `json:"last_at"` // RFC3339 format + Offset Duration `json:"offset"` + MessageID int `json:"message_id"` + LastAt string `json:"last_at"` // RFC3339 format } type Reminder struct { @@ -216,11 +235,11 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err reminder := Reminder{ Tomorrow: &ReminderConfig{ - Offset: int64(3 * time.Hour), + Offset: Duration(3 * time.Hour), LastAt: now.Format(time.RFC3339), }, Soon: &ReminderConfig{ - Offset: int64(reminderOffset) * int64(time.Minute), + Offset: Duration(reminderOffset) * Duration(time.Minute), MessageID: reminderMessageID, LastAt: now.Format(time.RFC3339), }, @@ -231,11 +250,11 @@ func transformChat(oldChat OldChat, moscowLocation *time.Location) (NewChat, err Jamaat: &JamaatConfig{ Enabled: subscribed && jamaat, Delay: &JamaatDelayConfig{ - Fajr: int64(10 * time.Minute), - Dhuhr: int64(10 * time.Minute), - Asr: int64(10 * time.Minute), - Maghrib: int64(10 * time.Minute), - Isha: int64(20 * time.Minute), + Fajr: Duration(10 * time.Minute), + Dhuhr: Duration(10 * time.Minute), + Asr: Duration(10 * time.Minute), + Maghrib: Duration(10 * time.Minute), + Isha: Duration(20 * time.Minute), }, }, } From e7cacf22daf194f07c06d905647e4255eca3fb96 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 14:42:26 +0300 Subject: [PATCH 57/59] fix: update domain --- domain/duration.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/domain/duration.go b/domain/duration.go index 5cccca5..e723d85 100644 --- a/domain/duration.go +++ b/domain/duration.go @@ -8,6 +8,14 @@ import ( type Duration time.Duration +func (d Duration) String() string { + return d.Duration().String() +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(time.Duration(d).String()) } From 0557d95cc07092ac881a5587e445276b43e68e41 Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 14:49:00 +0300 Subject: [PATCH 58/59] fix: use domain.Duration properly --- serverless/dispatcher/go.mod | 2 +- serverless/dispatcher/go.sum | 2 ++ .../dispatcher/internal/handler/command.go | 14 +++++------ .../dispatcher/internal/handler/handler.go | 14 +++++------ .../dispatcher/internal/handler/keyboard.go | 4 ++-- .../dispatcher/internal/handler/query.go | 12 +++++----- serverless/dispatcher/internal/service/db.go | 4 ++-- serverless/loader/go.mod | 2 +- serverless/loader/go.sum | 2 ++ serverless/reminder/go.mod | 4 ++-- serverless/reminder/go.sum | 24 ++----------------- .../reminder/internal/handler/reminder.go | 19 +++++++++++---- 12 files changed, 48 insertions(+), 55 deletions(-) diff --git a/serverless/dispatcher/go.mod b/serverless/dispatcher/go.mod index 0af1def..a100a98 100644 --- a/serverless/dispatcher/go.mod +++ b/serverless/dispatcher/go.mod @@ -3,7 +3,7 @@ module github.com/escalopa/prayer-bot/dispatcher go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 + github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 github.com/go-telegram/bot v1.15.0 github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 diff --git a/serverless/dispatcher/go.sum b/serverless/dispatcher/go.sum index 64205bd..abe6175 100644 --- a/serverless/dispatcher/go.sum +++ b/serverless/dispatcher/go.sum @@ -591,6 +591,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 h1:s9sqt4CtdExFGlZz1k2SDB2rA2elyMHR7I59MJ9c1lg= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/dispatcher/internal/handler/command.go b/serverless/dispatcher/internal/handler/command.go index ebe9db3..ed78eaa 100644 --- a/serverless/dispatcher/internal/handler/command.go +++ b/serverless/dispatcher/internal/handler/command.go @@ -335,11 +335,11 @@ func (h *Handler) info(ctx context.Context, b *bot.Bot, _ *models.Update) error } jamaatInfo = fmt.Sprintf(text.Info.Jamaat, jamaatStatus, - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr), - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr), - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr), - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib), - domain.FormatDuration(chat.Reminder.Jamaat.Delay.Isha), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Fajr.Duration()), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Dhuhr.Duration()), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Asr.Duration()), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Maghrib.Duration()), + domain.FormatDuration(chat.Reminder.Jamaat.Delay.Isha.Duration()), ) } @@ -349,8 +349,8 @@ func (h *Handler) info(ctx context.Context, b *bot.Bot, _ *models.Update) error chat.LanguageCode, chat.State, subscriptionStatus, - domain.FormatDuration(chat.Reminder.Tomorrow.Offset), - domain.FormatDuration(chat.Reminder.Soon.Offset), + domain.FormatDuration(chat.Reminder.Tomorrow.Offset.Duration()), + domain.FormatDuration(chat.Reminder.Soon.Offset.Duration()), jamaatInfo, ) diff --git a/serverless/dispatcher/internal/handler/handler.go b/serverless/dispatcher/internal/handler/handler.go index 41077b0..7579a8b 100644 --- a/serverless/dispatcher/internal/handler/handler.go +++ b/serverless/dispatcher/internal/handler/handler.go @@ -240,20 +240,20 @@ func (h *Handler) getChat(ctx context.Context, update *models.Update) (*domain.C reminder := &domain.Reminder{ Tomorrow: &domain.ReminderConfig{ LastAt: now, - Offset: 3 * time.Hour, + Offset: domain.Duration(3 * time.Hour), }, Soon: &domain.ReminderConfig{ LastAt: now, - Offset: 20 * time.Minute, + Offset: domain.Duration(20 * time.Minute), }, Arrive: &domain.ReminderConfig{LastAt: now}, Jamaat: &domain.JamaatConfig{ Delay: &domain.JamaatDelayConfig{ - Fajr: 10 * time.Minute, - Dhuhr: 10 * time.Minute, - Asr: 10 * time.Minute, - Maghrib: 10 * time.Minute, - Isha: 20 * time.Minute, + Fajr: domain.Duration(10 * time.Minute), + Dhuhr: domain.Duration(10 * time.Minute), + Asr: domain.Duration(10 * time.Minute), + Maghrib: domain.Duration(10 * time.Minute), + Isha: domain.Duration(20 * time.Minute), }, }, } diff --git a/serverless/dispatcher/internal/handler/keyboard.go b/serverless/dispatcher/internal/handler/keyboard.go index 5889da8..921fa86 100644 --- a/serverless/dispatcher/internal/handler/keyboard.go +++ b/serverless/dispatcher/internal/handler/keyboard.go @@ -115,13 +115,13 @@ func (h *Handler) remindMenuKeyboard(chat *domain.Chat) *models.InlineKeyboardMa } rowIndex++ - tomorrowOffset := domain.FormatDuration(chat.Reminder.Tomorrow.Offset) + tomorrowOffset := domain.FormatDuration(chat.Reminder.Tomorrow.Offset.Duration()) kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Tomorrow, tomorrowOffset), CallbackData: "remind:edit:tomorrow|"}, } rowIndex++ - soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset) + soonOffset := domain.FormatDuration(chat.Reminder.Soon.Offset.Duration()) kb.InlineKeyboard[rowIndex] = []models.InlineKeyboardButton{ {Text: fmt.Sprintf("%s (%s)", text.RemindMenu.Soon, soonOffset), CallbackData: "remind:edit:soon|"}, } diff --git a/serverless/dispatcher/internal/handler/query.go b/serverless/dispatcher/internal/handler/query.go index 8271dd1..bb9940e 100644 --- a/serverless/dispatcher/internal/handler/query.go +++ b/serverless/dispatcher/internal/handler/query.go @@ -230,10 +230,10 @@ func (h *Handler) remindEditQuery(ctx context.Context, b *bot.Bot, update *model var offset time.Duration switch reminderType { case domain.ReminderTypeTomorrow: - offset = chat.Reminder.Tomorrow.Offset + offset = chat.Reminder.Tomorrow.Offset.Duration() messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleTomorrow, domain.FormatDuration(offset)) case domain.ReminderTypeSoon: - offset = chat.Reminder.Soon.Offset + offset = chat.Reminder.Soon.Offset.Duration() messageText = fmt.Sprintf("%s - %s", text.RemindEdit.TitleSoon, domain.FormatDuration(offset)) default: log.Error("remindEditQuery: unknown reminder type", @@ -281,11 +281,11 @@ func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *mod switch reminderType { case domain.ReminderTypeTomorrow: - currentOffset = chat.Reminder.Tomorrow.Offset + currentOffset = chat.Reminder.Tomorrow.Offset.Duration() minOffset = TomorrowMinOffset maxOffset = TomorrowMaxOffset case domain.ReminderTypeSoon: - currentOffset = chat.Reminder.Soon.Offset + currentOffset = chat.Reminder.Soon.Offset.Duration() minOffset = SoonMinOffset maxOffset = SoonMaxOffset default: @@ -309,9 +309,9 @@ func (h *Handler) remindAdjustQuery(ctx context.Context, b *bot.Bot, update *mod switch reminderType { case domain.ReminderTypeTomorrow: - chat.Reminder.Tomorrow.Offset = newOffset + chat.Reminder.Tomorrow.Offset = domain.Duration(newOffset) case domain.ReminderTypeSoon: - chat.Reminder.Soon.Offset = newOffset + chat.Reminder.Soon.Offset = domain.Duration(newOffset) } ctx = setContextChat(ctx, chat) diff --git a/serverless/dispatcher/internal/service/db.go b/serverless/dispatcher/internal/service/db.go index ca7d9dc..94ef785 100644 --- a/serverless/dispatcher/internal/service/db.go +++ b/serverless/dispatcher/internal/service/db.go @@ -275,10 +275,10 @@ func (db *DB) SetReminderOffset(ctx context.Context, botID int64, chatID int64, return db.updateReminder(ctx, botID, chatID, func(reminder *domain.Reminder) { switch reminderType { case domain.ReminderTypeTomorrow: - reminder.Tomorrow.Offset = offset + reminder.Tomorrow.Offset = domain.Duration(offset) reminder.Tomorrow.LastAt = time.Now() case domain.ReminderTypeSoon: - reminder.Soon.Offset = offset + reminder.Soon.Offset = domain.Duration(offset) reminder.Soon.LastAt = time.Now() } }) diff --git a/serverless/loader/go.mod b/serverless/loader/go.mod index 4c31296..46c3b34 100644 --- a/serverless/loader/go.mod +++ b/serverless/loader/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/aws/aws-sdk-go v1.55.7 - github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 + github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 ) diff --git a/serverless/loader/go.sum b/serverless/loader/go.sum index 63c1595..beec113 100644 --- a/serverless/loader/go.sum +++ b/serverless/loader/go.sum @@ -593,6 +593,8 @@ github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 h1:s9sqt4CtdExFGlZz1k2SDB2rA2elyMHR7I59MJ9c1lg= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/go.mod b/serverless/reminder/go.mod index f9223eb..0823bc1 100644 --- a/serverless/reminder/go.mod +++ b/serverless/reminder/go.mod @@ -3,9 +3,8 @@ module github.com/escalopa/prayer-bot/reminder go 1.21 require ( - github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 + github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 github.com/go-telegram/bot v1.15.0 - github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 github.com/ydb-platform/ydb-go-sdk/v3 v3.108.0 github.com/ydb-platform/ydb-go-yc v0.12.3 golang.org/x/sync v0.10.0 @@ -18,6 +17,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/jonboulle/clockwork v0.3.0 // indirect github.com/yandex-cloud/go-genproto v0.0.0-20240819112322-98a264d392f6 // indirect + github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/serverless/reminder/go.sum b/serverless/reminder/go.sum index 64205bd..5b81f7a 100644 --- a/serverless/reminder/go.sum +++ b/serverless/reminder/go.sum @@ -569,28 +569,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3 h1:3ze/gQCQs+qIzkU+sB1zd5opatlkHCdeERuWC72/4Ic= -github.com/escalopa/prayer-bot v0.0.0-20251026155439-6034664856a3/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a h1:ymjQDDn1JBdL95DkBVkUxwphZxSTPAfP4Pf1ysikfmQ= -github.com/escalopa/prayer-bot v0.0.0-20251026172843-9eead151801a/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf h1:OLDTjLdG0mNigMk9sNNh+fJ5LBWkYboNKu2pFX61WSo= -github.com/escalopa/prayer-bot v0.0.0-20251026192818-d6bccce30ecf/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d h1:+Dv3qCKHoRcfFQVjEteZWTaLOkyS6B5OlrKr8CmroC4= -github.com/escalopa/prayer-bot v0.0.0-20251026194015-725e9cfbd15d/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279 h1:qc+7zybygYoH1yz948TjHe/uQ6QliqSwEVEd8O9rxug= -github.com/escalopa/prayer-bot v0.0.0-20251026200218-6f6adf300279/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89 h1:+jbrcX4CpaKUZs0gG+1QbpVENyN9NVbdZLQJOO+d/q0= -github.com/escalopa/prayer-bot v0.0.0-20251026201902-b5254b301c89/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92 h1:CZ80HIK0Duq/Rjo1Q+zzFFRNM3mER2fbxBryYu4SFaE= -github.com/escalopa/prayer-bot v0.0.0-20251026202753-f09b49eedd92/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e h1:rnAVK8yqKdvUJu0jwp8RyZwGXNgq1BWLUWTNGgMIPCs= -github.com/escalopa/prayer-bot v0.0.0-20251026205320-11cb2dcd014e/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7 h1:DO0ejIbuZJJxoxareSryEpp1HGNHRDzCkNUtKwlvhFg= -github.com/escalopa/prayer-bot v0.0.0-20251026211435-73f9e46a72f7/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad h1:bWmc3gk++my6XDOXRQKfER/xmXkl70Yobw2nQQdcroY= -github.com/escalopa/prayer-bot v0.0.0-20251026213512-5af98b73d4ad/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= -github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2 h1:JqgNa7R7hRsEV2uNslRcbioATTwlVBAJh9vqsdwYR9c= -github.com/escalopa/prayer-bot v0.0.0-20251102105549-ebc4c681afa2/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1 h1:s9sqt4CtdExFGlZz1k2SDB2rA2elyMHR7I59MJ9c1lg= +github.com/escalopa/prayer-bot v0.0.0-20251102114226-e7cacf22daf1/go.mod h1:HOpIxziLKMTC/a+Q6z40Rn5tJDEZsFyzhniLA4iB2i0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= diff --git a/serverless/reminder/internal/handler/reminder.go b/serverless/reminder/internal/handler/reminder.go index f274448..bf5f412 100644 --- a/serverless/reminder/internal/handler/reminder.go +++ b/serverless/reminder/internal/handler/reminder.go @@ -26,7 +26,16 @@ type TomorrowReminder struct { func (r *TomorrowReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, _ *domain.PrayerDay, now time.Time) (bool, domain.PrayerID) { config := chat.Reminder.Tomorrow - triggerTime := time.Date(now.Year(), now.Month(), now.Day()+1, int(-config.Offset.Hours()), 0, 0, 0, now.Location()) + triggerTime := time.Date( + now.Year(), + now.Month(), + now.Day()+1, // move to next day + int(-config.Offset.Duration().Hours()), // get triggerTime by moving back in time + 0, + 0, + 0, + now.Location(), + ) shouldSend := config.LastAt.Before(triggerTime) && (triggerTime.Before(now) || triggerTime.Equal(now)) return shouldSend, domain.PrayerIDUnknown } @@ -68,7 +77,7 @@ func (r *SoonReminder) ShouldTrigger(ctx context.Context, chat *domain.Chat, pra config := chat.Reminder.Soon for _, p := range prayers { // logic: last_at < (prayer_time - offset) AND (prayer_time - offset) <= now - trigger := p.time.Add(-config.Offset) + trigger := p.time.Add(-config.Offset.Duration()) if config.LastAt.Before(trigger) && (trigger.Before(now) || trigger.Equal(now)) { // // Next LastAt should be incremented by 1 minute to avoid re-triggering // nextLastAt := now.Add(1 * time.Minute) @@ -87,8 +96,8 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, if chat.Reminder.Jamaat.Enabled && prayerID != domain.PrayerIDShuruq { delay := chat.Reminder.Jamaat.Delay.GetDelayByPrayerID(prayerID) message := fmt.Sprintf("%s\n%s", - fmt.Sprintf(strings.ReplaceAll(text.PrayerSoon, "*", ""), prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)), - fmt.Sprintf(strings.ReplaceAll(text.PrayerJamaat, "*", ""), domain.FormatDuration(delay+chat.Reminder.Soon.Offset)), + fmt.Sprintf(strings.ReplaceAll(text.PrayerSoon, "*", ""), prayer, domain.FormatDuration(chat.Reminder.Soon.Offset.Duration())), + fmt.Sprintf(strings.ReplaceAll(text.PrayerJamaat, "*", ""), domain.FormatDuration(delay+chat.Reminder.Soon.Offset.Duration())), ) isAnonymous := false @@ -108,7 +117,7 @@ func (r *SoonReminder) Send(ctx context.Context, b *bot.Bot, chat *domain.Chat, return res.ID, nil } - message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset)) + message := fmt.Sprintf(text.PrayerSoon, prayer, domain.FormatDuration(chat.Reminder.Soon.Offset.Duration())) res, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: chat.ChatID, From 9c6247501b864594749edc0e7a5b50da4d66df7b Mon Sep 17 00:00:00 2001 From: escalopa Date: Sun, 2 Nov 2025 15:13:16 +0300 Subject: [PATCH 59/59] fix: disable hook job --- .github/workflows/deploy.yaml | 80 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 4698881..f5ba158 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -287,46 +287,46 @@ jobs: echo "dispatcher_function_id=$DISPATCHER_FUNCTION_ID" >> $GITHUB_OUTPUT echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" - hook: - name: Setup Telegram Webhooks - runs-on: ubuntu-latest - needs: [apply] - if: needs.apply.result == 'success' - environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set workspace - run: | - WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} - echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV - echo "::notice::Workspace: $WORKSPACE" - - - name: Load application config - uses: ./.github/actions/load-config - with: - app_config: ${{ secrets.APP_CONFIG }} - app_config_path: ${{ env.APP_CONFIG_PATH }} - - - name: Get Dispatcher Function ID - run: | - DISPATCHER_FUNCTION_ID="${{ needs.apply.outputs.dispatcher_function_id }}" - echo "DISPATCHER_FUNCTION_ID=$DISPATCHER_FUNCTION_ID" >> $GITHUB_ENV - echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" - - - name: Execute webhook setup script - run: | - echo "::notice::Config file: ${{ env.APP_CONFIG_PATH }}" - echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" - - chmod +x _scripts/hook.sh - bash _scripts/hook.sh - continue-on-error: false - - - name: Verify webhook setup - run: | - echo "::notice::Webhook setup completed successfully for $WORKSPACE environment" + # hook: + # name: Setup Telegram Webhooks + # runs-on: ubuntu-latest + # needs: [apply] + # if: needs.apply.result == 'success' + # environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Set workspace + # run: | + # WORKSPACE=${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + # echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + # echo "::notice::Workspace: $WORKSPACE" + + # - name: Load application config + # uses: ./.github/actions/load-config + # with: + # app_config: ${{ secrets.APP_CONFIG }} + # app_config_path: ${{ env.APP_CONFIG_PATH }} + + # - name: Get Dispatcher Function ID + # run: | + # DISPATCHER_FUNCTION_ID="${{ needs.apply.outputs.dispatcher_function_id }}" + # echo "DISPATCHER_FUNCTION_ID=$DISPATCHER_FUNCTION_ID" >> $GITHUB_ENV + # echo "::notice::Dispatcher Function ID: $DISPATCHER_FUNCTION_ID" + + # - name: Execute webhook setup script + # run: | + # echo "::notice::Config file: ${{ env.APP_CONFIG_PATH }}" + # echo "::notice::Dispatcher function ID: $DISPATCHER_FUNCTION_ID" + + # chmod +x _scripts/hook.sh + # bash _scripts/hook.sh + # continue-on-error: false + + # - name: Verify webhook setup + # run: | + # echo "::notice::Webhook setup completed successfully for $WORKSPACE environment" summary: name: Deployment Summary