vps setup
Some checks failed
Deploy to Railway / deploy (push) Has been cancelled
Network Test / test_network (push) Has been cancelled

This commit is contained in:
Mykhailo Svishchov
2026-03-07 18:49:04 +02:00
parent 7e2985a1a6
commit 8b851e8581
12 changed files with 310 additions and 54 deletions

View File

@@ -75,7 +75,7 @@ Railway is the primary deployment platform for staging. It provides built-in log
1. In your Railway project, click **"+ New"** → **"GitHub Repo"** (or **"Empty Service"**) 1. In your Railway project, click **"+ New"** → **"GitHub Repo"** (or **"Empty Service"**)
2. If using GitHub: 2. If using GitHub:
- Connect your GitHub account - Connect your GitHub account
- Select the `lottery-be` repository - Select the `honey-be` repository
- Railway will automatically detect it's a Java/Maven project - Railway will automatically detect it's a Java/Maven project
3. If using Empty Service: 3. If using Empty Service:
- Click **"Empty Service"** - Click **"Empty Service"**

View File

@@ -0,0 +1,242 @@
# Two frontends on the same backend:
# - Frontend 1: /opt/app/frontend/dist -> https://testforapp.website/ (build with base: '/')
# - Frontend 2: /opt/app/frontend/test-dist -> https://testforapp.website/test/ (build with base: '/test/')
upstream backend {
server 127.0.0.1:8082;
server 127.0.0.1:8080 backup;
keepalive 500;
}
map $http_referer $is_phpmyadmin_request {
~*18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905 1;
default 0;
}
# HTTPS server
server {
server_name testforapp.website;
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/testforapp.website/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/testforapp.website/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()";
root /opt/app/frontend/dist;
index index.html;
# --- Frontend 2 at /test/ (from test-dist) ---
location = /test {
return 301 /test/;
}
location ^~ /test/ {
alias /opt/app/frontend/test-dist/;
index index.html;
try_files $uri $uri/ /test/index.html;
expires 0;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://web.telegram.org https://webk.telegram.org https://webz.telegram.org https://t.me telegram.org;" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()";
}
# Admin Panel - root path (redirects to trailing slash)
location = /dfab0676b6cb6b257370fb5743d8ddac42ab8153c2661072e8ef2717a10fcfaa {
return 301 /dfab0676b6cb6b257370fb5743d8ddac42ab8153c2661072e8ef2717a10fcfaa/;
}
location ~ ^/dfab0676b6cb6b257370fb5743d8ddac42ab8153c2661072e8ef2717a10fcfaa/(assets/.+)$ {
alias /opt/app/admin-panel/$1;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location /dfab0676b6cb6b257370fb5743d8ddac42ab8153c2661072e8ef2717a10fcfaa/ {
alias /opt/app/admin-panel/;
index index.html;
try_files $uri $uri/ /dfab0676b6cb6b257370fb5743d8ddac42ab8153c2661072e8ef2717a10fcfaa/index.html;
expires 0;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
location /18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905 {
proxy_pass http://127.0.0.1:8081/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
gzip off;
proxy_set_header Accept-Encoding "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 16 8k;
proxy_busy_buffers_size 16k;
proxy_cookie_path / /18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/;
sub_filter 'action="index.php?route=/' 'action="/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php?route=/';
sub_filter 'action="index.php' 'action="/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter 'action="/index.php' 'action="/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter 'Location: /index.php' 'Location: /18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter 'Location: index.php' 'Location: /18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter 'href="index.php' 'href="/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter 'href="/index.php' 'href="/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/index.php';
sub_filter_once off;
sub_filter_types text/html;
}
location ~ ^/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/(themes|js|favicon\.ico|libraries|templates|setup|config|tmp|index\.php) {
rewrite ^/18cac693dfdec1c7bd3080d3451d0545746081e2335c241cec5d39ff75822905/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
location ~ ^/(themes|js|favicon\.ico|libraries|templates|setup|config|tmp|index\.php) {
if ($is_phpmyadmin_request = 0) {
return 404;
}
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
location /videos/ {
root /usr/share/nginx/html;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
location ^~ /images/ {
root /usr/share/nginx/html;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
autoindex off;
}
# Frontend 1: static assets at root (from dist)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Frontend 1: SPA at / (from dist)
location / {
try_files $uri $uri/ /index.html;
expires 0;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://web.telegram.org https://webk.telegram.org https://webz.telegram.org https://t.me telegram.org;" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()";
}
location ^~ /avatars/ {
alias /opt/app/data/avatars/;
expires 24h;
add_header Cache-Control "public, must-revalidate";
access_log off;
location ~ \.(php|html)$ {
deny all;
}
}
location /api/telegram/webhook/ {
access_log off;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 30s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
location /api/ {
limit_req zone=api_limit burst=200 nodelay;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location = /phpmyadmin {
return 404;
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
server {
if ($host = testforapp.website) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name testforapp.website;
return 404;
}

View File

@@ -11,8 +11,8 @@
# #
# Prerequisites: # Prerequisites:
# 1. SSH key-based authentication to backup VPS (5.45.77.77) # 1. SSH key-based authentication to backup VPS (5.45.77.77)
# 2. Database password accessible via /run/secrets/lottery-config.properties # 2. Database password accessible via /run/secrets/honey-config.properties
# 3. Docker container 'lottery-mysql' running # 3. Docker container 'honey-mysql' running
# #
# Backup location on backup VPS: /raid/backup/acc_260182/ # Backup location on backup VPS: /raid/backup/acc_260182/
@@ -22,9 +22,9 @@ set -euo pipefail
BACKUP_VPS_HOST="5.45.77.77" BACKUP_VPS_HOST="5.45.77.77"
BACKUP_VPS_USER="acc_260182" # User account on backup VPS BACKUP_VPS_USER="acc_260182" # User account on backup VPS
BACKUP_VPS_PATH="/raid/backup/acc_260182" BACKUP_VPS_PATH="/raid/backup/acc_260182"
MYSQL_CONTAINER="lottery-mysql" MYSQL_CONTAINER="honey-mysql"
MYSQL_DATABASE="lottery_db" MYSQL_DATABASE="lottery_db"
SECRET_FILE="/run/secrets/lottery-config.properties" SECRET_FILE="/run/secrets/honey-config.properties"
BACKUP_DIR="/opt/app/backups" BACKUP_DIR="/opt/app/backups"
KEEP_LOCAL=false KEEP_LOCAL=false
COMPRESS=true COMPRESS=true

View File

@@ -3,8 +3,8 @@
# Script to create secret file from template # Script to create secret file from template
# Usage: ./create-secret-file-from-template.sh /path/to/template /path/to/output # Usage: ./create-secret-file-from-template.sh /path/to/template /path/to/output
TEMPLATE_FILE="${1:-lottery-config.properties.template}" TEMPLATE_FILE="${1:-honey-config.properties.template}"
OUTPUT_FILE="${2:-/run/secrets/lottery-config.properties}" OUTPUT_FILE="${2:-/run/secrets/honey-config.properties}"
OUTPUT_DIR=$(dirname "$OUTPUT_FILE") OUTPUT_DIR=$(dirname "$OUTPUT_FILE")
# Check if template exists # Check if template exists

View File

@@ -3,7 +3,7 @@
# Create secret file from environment variables for testing ConfigLoader # Create secret file from environment variables for testing ConfigLoader
# This simulates the mounted secret file approach used in Inferno # This simulates the mounted secret file approach used in Inferno
SECRET_FILE="/run/secrets/lottery-config.properties" SECRET_FILE="/run/secrets/honey-config.properties"
SECRET_DIR="/run/secrets" SECRET_DIR="/run/secrets"
# Create directory if it doesn't exist # Create directory if it doesn't exist

View File

@@ -2,7 +2,7 @@
# Diagnostic script for backup-database.sh permission issues # Diagnostic script for backup-database.sh permission issues
# Run this on your VPS to identify the root cause # Run this on your VPS to identify the root cause
SCRIPT="/opt/app/backend/lottery-be/scripts/backup-database.sh" SCRIPT="/opt/app/backend/honey-be/scripts/backup-database.sh"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
@@ -167,7 +167,7 @@ echo ""
# 14. Secret file check # 14. Secret file check
echo "14. Checking secret file:" echo "14. Checking secret file:"
SECRET_FILE="/run/secrets/lottery-config.properties" SECRET_FILE="/run/secrets/honey-config.properties"
if [ -f "$SECRET_FILE" ]; then if [ -f "$SECRET_FILE" ]; then
echo -e " ${GREEN}✅ Secret file exists${NC}" echo -e " ${GREEN}✅ Secret file exists${NC}"
if [ -r "$SECRET_FILE" ]; then if [ -r "$SECRET_FILE" ]; then

View File

@@ -13,16 +13,16 @@
# ./scripts/restore-database.sh 5.45.77.77:/raid/backup/acc_260182/lottery_db_backup_20240101_120000.sql.gz # ./scripts/restore-database.sh 5.45.77.77:/raid/backup/acc_260182/lottery_db_backup_20240101_120000.sql.gz
# #
# Prerequisites: # Prerequisites:
# 1. Database password accessible via /run/secrets/lottery-config.properties # 1. Database password accessible via /run/secrets/honey-config.properties
# 2. Docker container 'lottery-mysql' running # 2. Docker container 'honey-mysql' running
# 3. Database will be DROPPED and RECREATED (all data will be lost!) # 3. Database will be DROPPED and RECREATED (all data will be lost!)
set -euo pipefail set -euo pipefail
# Configuration # Configuration
MYSQL_CONTAINER="lottery-mysql" MYSQL_CONTAINER="honey-mysql"
MYSQL_DATABASE="lottery_db" MYSQL_DATABASE="lottery_db"
SECRET_FILE="/run/secrets/lottery-config.properties" SECRET_FILE="/run/secrets/honey-config.properties"
BACKUP_VPS_USER="acc_260182" # User account on backup VPS BACKUP_VPS_USER="acc_260182" # User account on backup VPS
# Colors for output # Colors for output
@@ -179,5 +179,5 @@ fi
log "✅ Database restore completed!" log "✅ Database restore completed!"
warn "⚠️ Remember to restart the backend container if it's running:" warn "⚠️ Remember to restart the backend container if it's running:"
warn " docker restart lottery-backend" warn " docker restart honey-backend"

View File

@@ -9,8 +9,8 @@ set -e
if [ -d "/opt/app/backend" ]; then if [ -d "/opt/app/backend" ]; then
CONFIG_DIR="/opt/app/backend/config" CONFIG_DIR="/opt/app/backend/config"
LOG_DIR="/opt/app/logs" LOG_DIR="/opt/app/logs"
elif [ -d "/opt/app/backend/lottery-be" ]; then elif [ -d "/opt/app/backend/honey-be" ]; then
CONFIG_DIR="/opt/app/backend/lottery-be/config" CONFIG_DIR="/opt/app/backend/honey-be/config"
LOG_DIR="/opt/app/logs" LOG_DIR="/opt/app/logs"
else else
# Try to find from current directory # Try to find from current directory
@@ -38,9 +38,9 @@ if [ ! -f "$CONFIG_DIR/logback-spring.xml" ]; then
# Try multiple locations for JAR file # Try multiple locations for JAR file
JAR_PATH="" JAR_PATH=""
for search_path in "/opt/app/backend" "/opt/app/backend/lottery-be" "$(dirname "$CONFIG_DIR")" "$(dirname "$(dirname "$CONFIG_DIR")")"; do for search_path in "/opt/app/backend" "/opt/app/backend/honey-be" "$(dirname "$CONFIG_DIR")" "$(dirname "$(dirname "$CONFIG_DIR")")"; do
if [ -d "$search_path" ]; then if [ -d "$search_path" ]; then
found_jar=$(find "$search_path" -name "lottery-be-*.jar" -type f 2>/dev/null | head -n 1) found_jar=$(find "$search_path" -name "honey-be-*.jar" -type f 2>/dev/null | head -n 1)
if [ -n "$found_jar" ]; then if [ -n "$found_jar" ]; then
JAR_PATH="$found_jar" JAR_PATH="$found_jar"
break break
@@ -50,7 +50,7 @@ if [ ! -f "$CONFIG_DIR/logback-spring.xml" ]; then
# Try to find in target directory # Try to find in target directory
if [ -z "$JAR_PATH" ]; then if [ -z "$JAR_PATH" ]; then
for search_path in "/opt/app/backend" "/opt/app/backend/lottery-be" "$(dirname "$CONFIG_DIR")"; do for search_path in "/opt/app/backend" "/opt/app/backend/honey-be" "$(dirname "$CONFIG_DIR")"; do
if [ -d "$search_path/target" ]; then if [ -d "$search_path/target" ]; then
found_jar=$(find "$search_path/target" -name "*.jar" -type f | head -n 1) found_jar=$(find "$search_path/target" -name "*.jar" -type f | head -n 1)
if [ -n "$found_jar" ]; then if [ -n "$found_jar" ]; then
@@ -64,7 +64,7 @@ if [ ! -f "$CONFIG_DIR/logback-spring.xml" ]; then
if [ -z "$JAR_PATH" ]; then if [ -z "$JAR_PATH" ]; then
echo "Warning: JAR file not found. Trying to copy from source..." echo "Warning: JAR file not found. Trying to copy from source..."
# If JAR not found, copy from source (if available) # If JAR not found, copy from source (if available)
for search_path in "/opt/app/backend" "/opt/app/backend/lottery-be" "$(dirname "$CONFIG_DIR")"; do for search_path in "/opt/app/backend" "/opt/app/backend/honey-be" "$(dirname "$CONFIG_DIR")"; do
if [ -f "$search_path/src/main/resources/logback-spring.xml" ]; then if [ -f "$search_path/src/main/resources/logback-spring.xml" ]; then
cp "$search_path/src/main/resources/logback-spring.xml" "$CONFIG_DIR/logback-spring.xml" cp "$search_path/src/main/resources/logback-spring.xml" "$CONFIG_DIR/logback-spring.xml"
echo "Copied from source: $search_path/src/main/resources/logback-spring.xml" echo "Copied from source: $search_path/src/main/resources/logback-spring.xml"
@@ -84,7 +84,7 @@ if [ ! -f "$CONFIG_DIR/logback-spring.xml" ]; then
unzip -p "$JAR_PATH" logback-spring.xml > "$CONFIG_DIR/logback-spring.xml" 2>/dev/null || { unzip -p "$JAR_PATH" logback-spring.xml > "$CONFIG_DIR/logback-spring.xml" 2>/dev/null || {
echo "Warning: Could not extract from JAR. Trying to copy from source..." echo "Warning: Could not extract from JAR. Trying to copy from source..."
# Try copying from source # Try copying from source
for search_path in "/opt/app/backend" "/opt/app/backend/lottery-be" "$(dirname "$CONFIG_DIR")"; do for search_path in "/opt/app/backend" "/opt/app/backend/honey-be" "$(dirname "$CONFIG_DIR")"; do
if [ -f "$search_path/src/main/resources/logback-spring.xml" ]; then if [ -f "$search_path/src/main/resources/logback-spring.xml" ]; then
cp "$search_path/src/main/resources/logback-spring.xml" "$CONFIG_DIR/logback-spring.xml" cp "$search_path/src/main/resources/logback-spring.xml" "$CONFIG_DIR/logback-spring.xml"
break break

View File

@@ -4,6 +4,7 @@ import com.honey.honey.config.TelegramProperties;
import com.honey.honey.dto.TelegramApiResponse; import com.honey.honey.dto.TelegramApiResponse;
import com.honey.honey.dto.PaymentWebhookRequest; import com.honey.honey.dto.PaymentWebhookRequest;
import com.honey.honey.model.UserA; import com.honey.honey.model.UserA;
import com.honey.honey.service.FeatureSwitchService;
import com.honey.honey.service.PaymentService; import com.honey.honey.service.PaymentService;
import com.honey.honey.service.TelegramBotApiService; import com.honey.honey.service.TelegramBotApiService;
import com.honey.honey.service.UserService; import com.honey.honey.service.UserService;
@@ -56,6 +57,8 @@ import org.springframework.core.io.ByteArrayResource;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TelegramWebhookController { public class TelegramWebhookController {
private static final String MINI_APP_URL = "https://testforapp.website/test/auth";
@Value("${app.telegram-webhook.token:}") @Value("${app.telegram-webhook.token:}")
private String expectedWebhookToken; private String expectedWebhookToken;
@@ -64,6 +67,7 @@ public class TelegramWebhookController {
private final TelegramProperties telegramProperties; private final TelegramProperties telegramProperties;
private final LocalizationService localizationService; private final LocalizationService localizationService;
private final TelegramBotApiService telegramBotApiService; private final TelegramBotApiService telegramBotApiService;
private final FeatureSwitchService featureSwitchService;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
/** /**
@@ -326,32 +330,27 @@ public class TelegramWebhookController {
} }
Locale locale = LocaleConfig.languageCodeToLocale(languageCode != null ? languageCode : "EN"); Locale locale = LocaleConfig.languageCodeToLocale(languageCode != null ? languageCode : "EN");
ReplyKeyboardMarkup replyKeyboard = buildReplyKeyboard(locale); ReplyKeyboardMarkup replyKeyboard = buildReplyKeyboard(locale);
// Create inline keyboard with only START SPINNING button
InlineKeyboardMarkup inlineKeyboard = new InlineKeyboardMarkup();
List<List<InlineKeyboardButton>> inlineRows = new ArrayList<>();
List<InlineKeyboardButton> inlineRow = new ArrayList<>();
InlineKeyboardButton startInlineButton = new InlineKeyboardButton();
// Add arrows on both sides like in the reference app (right arrow on left, left arrow on right)
String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline");
startInlineButton.setText(startSpinningButtonText);
// Use WebAppInfo to open mini app instead of regular URL
WebAppInfo webAppInfo = new WebAppInfo();
webAppInfo.setUrl("https://win-spin.live/auth");
startInlineButton.setWebApp(webAppInfo);
inlineRow.add(startInlineButton);
inlineRows.add(inlineRow);
inlineKeyboard.setKeyboard(inlineRows);
// Send first message with GIF animation and reply keyboard
// Note: Telegram doesn't allow both inline and reply keyboards in the same message
String firstMessage = localizationService.getMessage(locale, "bot.welcome.firstMessage"); String firstMessage = localizationService.getMessage(locale, "bot.welcome.firstMessage");
sendAnimationWithReplyKeyboard(chatId, firstMessage, replyKeyboard); sendAnimationWithReplyKeyboard(chatId, firstMessage, replyKeyboard);
// Send second message with inline button (START SPINNING)
String welcomeText = localizationService.getMessage(locale, "bot.welcome.message"); String welcomeText = localizationService.getMessage(locale, "bot.welcome.message");
sendMessage(chatId, welcomeText, inlineKeyboard); if (featureSwitchService.isStartGameButtonEnabled()) {
InlineKeyboardMarkup inlineKeyboard = new InlineKeyboardMarkup();
List<List<InlineKeyboardButton>> inlineRows = new ArrayList<>();
List<InlineKeyboardButton> inlineRow = new ArrayList<>();
InlineKeyboardButton startInlineButton = new InlineKeyboardButton();
String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline");
startInlineButton.setText(startSpinningButtonText);
WebAppInfo webAppInfo = new WebAppInfo();
webAppInfo.setUrl(MINI_APP_URL);
startInlineButton.setWebApp(webAppInfo);
inlineRow.add(startInlineButton);
inlineRows.add(inlineRow);
inlineKeyboard.setKeyboard(inlineRows);
sendMessage(chatId, welcomeText, inlineKeyboard);
} else {
sendMessage(chatId, welcomeText, null);
}
} }
/** /**
@@ -359,24 +358,22 @@ public class TelegramWebhookController {
*/ */
private void sendStartSpinningMessage(Long chatId, Locale locale) { private void sendStartSpinningMessage(Long chatId, Locale locale) {
String message = localizationService.getMessage(locale, "bot.message.startSpinning"); String message = localizationService.getMessage(locale, "bot.message.startSpinning");
if (!featureSwitchService.isStartGameButtonEnabled()) {
sendMessage(chatId, message, null);
return;
}
InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup(); InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup();
List<List<InlineKeyboardButton>> rows = new ArrayList<>(); List<List<InlineKeyboardButton>> rows = new ArrayList<>();
List<InlineKeyboardButton> row = new ArrayList<>(); List<InlineKeyboardButton> row = new ArrayList<>();
InlineKeyboardButton button = new InlineKeyboardButton(); InlineKeyboardButton button = new InlineKeyboardButton();
// Add arrows on both sides like in the reference app (right arrow on left, left arrow on right)
String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline"); String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline");
button.setText(startSpinningButtonText); button.setText(startSpinningButtonText);
// Use WebAppInfo to open mini app instead of regular URL
WebAppInfo webAppInfo = new WebAppInfo(); WebAppInfo webAppInfo = new WebAppInfo();
webAppInfo.setUrl("https://win-spin.live/auth"); webAppInfo.setUrl(MINI_APP_URL);
button.setWebApp(webAppInfo); button.setWebApp(webAppInfo);
row.add(button); row.add(button);
rows.add(row); rows.add(row);
keyboard.setKeyboard(rows); keyboard.setKeyboard(rows);
sendMessage(chatId, message, keyboard); sendMessage(chatId, message, keyboard);
} }

View File

@@ -22,6 +22,8 @@ public class FeatureSwitchService {
public static final String PROMOTIONS_ENABLED = "promotions_enabled"; public static final String PROMOTIONS_ENABLED = "promotions_enabled";
/** When enabled, send manual_pay=1 for all crypto payouts. When disabled, only for users who completed 50 or 100 referrals (first withdrawal). Default true. */ /** When enabled, send manual_pay=1 for all crypto payouts. When disabled, only for users who completed 50 or 100 referrals (first withdrawal). Default true. */
public static final String MANUAL_PAY_FOR_ALL_PAYOUTS = "manual_pay_for_all_payouts"; public static final String MANUAL_PAY_FOR_ALL_PAYOUTS = "manual_pay_for_all_payouts";
/** When enabled, "Start Game" inline button is sent (welcome and start-spinning messages). When disabled, the button is not sent. Default true. */
public static final String START_GAME_BUTTON_ENABLED = "start_game_button_enabled";
private final FeatureSwitchRepository featureSwitchRepository; private final FeatureSwitchRepository featureSwitchRepository;
@@ -85,6 +87,16 @@ public class FeatureSwitchService {
.orElse(true); .orElse(true);
} }
/**
* Returns whether the "Start Game" inline button is shown (welcome message and start-spinning reply). Default true when switch is missing.
*/
@Transactional(readOnly = true)
public boolean isStartGameButtonEnabled() {
return featureSwitchRepository.findById(START_GAME_BUTTON_ENABLED)
.map(FeatureSwitch::isEnabled)
.orElse(true);
}
/** /**
* Returns all feature switches for admin (key and enabled). * Returns all feature switches for admin (key and enabled).
*/ */

View File

@@ -41,6 +41,7 @@ public class NotificationBroadcastService {
private final TelegramBotApiService telegramBotApiService; private final TelegramBotApiService telegramBotApiService;
private final UserARepository userARepository; private final UserARepository userARepository;
private final NotificationAuditRepository notificationAuditRepository; private final NotificationAuditRepository notificationAuditRepository;
private final FeatureSwitchService featureSwitchService;
private final AtomicBoolean stopRequested = new AtomicBoolean(false); private final AtomicBoolean stopRequested = new AtomicBoolean(false);
@@ -52,7 +53,7 @@ public class NotificationBroadcastService {
} }
/** Mini app URL for the optional inline button (same as in TelegramWebhookController). */ /** Mini app URL for the optional inline button (same as in TelegramWebhookController). */
private static final String MINI_APP_URL = "https://win-spin.live/auth"; private static final String MINI_APP_URL = "https://testforapp.website/test/auth";
/** /**
* Run broadcast asynchronously. Uses userIdFrom/userIdTo (internal user ids); if null, uses 1 and max id. * Run broadcast asynchronously. Uses userIdFrom/userIdTo (internal user ids); if null, uses 1 and max id.
@@ -166,7 +167,7 @@ public class NotificationBroadcastService {
body.put("text", StringUtils.hasText(normalizedMessage) ? normalizedMessage : "(no text)"); body.put("text", StringUtils.hasText(normalizedMessage) ? normalizedMessage : "(no text)");
} }
if (StringUtils.hasText(buttonText)) { if (StringUtils.hasText(buttonText) && featureSwitchService.isStartGameButtonEnabled()) {
Map<String, Object> webApp = new HashMap<>(); Map<String, Object> webApp = new HashMap<>();
webApp.put("url", MINI_APP_URL); webApp.put("url", MINI_APP_URL);
Map<String, Object> button = new HashMap<>(); Map<String, Object> button = new HashMap<>();

View File

@@ -0,0 +1,4 @@
-- Seed feature switch: "Start Game" inline button in Telegram bot (enabled by default).
INSERT INTO `feature_switches` (`key`, `enabled`, `updated_at`)
VALUES ('start_game_button_enabled', 1, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE `updated_at` = CURRENT_TIMESTAMP;