Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ SPRING_DATASOURCE_PASSWORD=atlas
ATLAS_TELEGRAM_ENABLED=false
ATLAS_TELEGRAM_BOT_TOKEN=
ATLAS_TELEGRAM_BOT_USERNAME=
ATLAS_TELEGRAM_WEBHOOK_PATH=/telegram/webhook
ATLAS_TELEGRAM_WEBHOOK_SECRET=
ATLAS_PUBLIC_BASE_URL=
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=false
ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION=true
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v0.3.3

- Added production Telegram webhook configuration
- Added Telegram webhook secret validation
- Added optional webhook registration on application startup
- Added production-safe Telegram update logging
- Added Telegram production deployment documentation
- Expanded Telegram production test coverage

## v0.3.2

- Removed obsolete frontend-related artifacts from the backend repository
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,34 @@ ATLAS_TELEGRAM_BOT_USERNAME=<username>

Не добавляй реальные секреты в репозиторий.

<h2 align="center">Production Telegram Launch</h2>

Production-запуск Telegram-бота описан в [deployment guide](docs/deployment/telegram-production.md).

Минимальные production-переменные:

```bash
ATLAS_TELEGRAM_ENABLED=true
ATLAS_TELEGRAM_BOT_TOKEN=<telegram-bot-token>
ATLAS_TELEGRAM_BOT_USERNAME=<telegram-bot-username>
ATLAS_TELEGRAM_WEBHOOK_PATH=/telegram/webhook
ATLAS_TELEGRAM_WEBHOOK_SECRET=<random-webhook-secret>
ATLAS_PUBLIC_BASE_URL=https://<public-domain>
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=true
```

Production health endpoint:

```text
GET /actuator/health
```

Telegram webhook endpoint:

```text
POST /telegram/webhook
```

<h2 align="center">Roadmap</h2>

Основная продуктовая дорожная карта:
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ services:
ATLAS_TELEGRAM_ENABLED: ${ATLAS_TELEGRAM_ENABLED:-false}
ATLAS_TELEGRAM_BOT_TOKEN: ${ATLAS_TELEGRAM_BOT_TOKEN:-}
ATLAS_TELEGRAM_BOT_USERNAME: ${ATLAS_TELEGRAM_BOT_USERNAME:-}
ATLAS_TELEGRAM_WEBHOOK_PATH: ${ATLAS_TELEGRAM_WEBHOOK_PATH:-/telegram/webhook}
ATLAS_TELEGRAM_WEBHOOK_SECRET: ${ATLAS_TELEGRAM_WEBHOOK_SECRET:-}
ATLAS_PUBLIC_BASE_URL: ${ATLAS_PUBLIC_BASE_URL:-}
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP: ${ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP:-false}
ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION: ${ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION:-true}
ports:
- "${ATLAS_APP_PORT:-8080}:8080"
healthcheck:
Expand Down
158 changes: 158 additions & 0 deletions docs/deployment/telegram-production.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Telegram Production Deployment

Этот чеклист описывает production-запуск ATLAS backend для Telegram-бота.

## BotFather Checklist

- Создай бота через BotFather.
- Сохрани bot token вне репозитория.
- Задай bot username и description.
- Отключи privacy mode только если это нужно для групповых чатов.
- Не публикуй token в README, логах, скриншотах или issue.

## Required Environment Variables

```bash
ATLAS_TELEGRAM_ENABLED=true
ATLAS_TELEGRAM_BOT_TOKEN=<telegram-bot-token>
ATLAS_TELEGRAM_BOT_USERNAME=<telegram-bot-username>
ATLAS_TELEGRAM_WEBHOOK_PATH=/telegram/webhook
ATLAS_TELEGRAM_WEBHOOK_SECRET=<random-webhook-secret>
ATLAS_PUBLIC_BASE_URL=https://<public-domain>
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=false
ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION=true
```

## Example `.env.production`

```bash
POSTGRES_DB=atlas
POSTGRES_USER=atlas
POSTGRES_PASSWORD=<postgres-password>

SPRING_DATASOURCE_URL=jdbc:postgresql://atlas-postgres:5432/atlas
SPRING_DATASOURCE_USERNAME=atlas
SPRING_DATASOURCE_PASSWORD=<postgres-password>

ATLAS_TELEGRAM_ENABLED=true
ATLAS_TELEGRAM_BOT_TOKEN=<telegram-bot-token>
ATLAS_TELEGRAM_BOT_USERNAME=<telegram-bot-username>
ATLAS_TELEGRAM_WEBHOOK_PATH=/telegram/webhook
ATLAS_TELEGRAM_WEBHOOK_SECRET=<random-webhook-secret>
ATLAS_PUBLIC_BASE_URL=https://<public-domain>
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=true
ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION=true
```

## HTTPS Requirement

Telegram must reach the webhook through a public HTTPS URL. `ATLAS_PUBLIC_BASE_URL` must start with `https://` when automatic webhook registration is enabled.

## Docker Deployment Checklist

- Build and publish the backend image.
- Provision PostgreSQL and set datasource environment variables.
- Set all Telegram variables in the deployment environment.
- Keep `ATLAS_TELEGRAM_ENABLED=false` for local development unless testing a real bot.
- Expose the application on port `8080` behind HTTPS.
- Verify health before registering the webhook.

Healthcheck:

```bash
curl -f https://<public-domain>/actuator/health
```

Local container health endpoint:

```bash
curl -f http://localhost:8080/actuator/health
```

## Webhook Registration

### Option 1: Automatic Registration On Startup

Set:

```bash
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=true
ATLAS_PUBLIC_BASE_URL=https://<public-domain>
ATLAS_TELEGRAM_WEBHOOK_PATH=/telegram/webhook
```

On startup the backend registers:

```text
https://<public-domain>/telegram/webhook
```

Automatic registration fails fast if the public URL is missing, is not HTTPS, or the webhook path does not start with `/`.

### Option 2: Manual `setWebhook`

Keep automatic registration disabled:

```bash
ATLAS_TELEGRAM_REGISTER_WEBHOOK_ON_STARTUP=false
```

Register manually:

```bash
curl -X POST "https://api.telegram.org/bot${ATLAS_TELEGRAM_BOT_TOKEN}/setWebhook" \
-d "url=${ATLAS_PUBLIC_BASE_URL}${ATLAS_TELEGRAM_WEBHOOK_PATH}" \
-d "secret_token=${ATLAS_TELEGRAM_WEBHOOK_SECRET}" \
-d "drop_pending_updates=${ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION}" \
-d 'allowed_updates=["message"]'
```

Check webhook status:

```bash
curl "https://api.telegram.org/bot${ATLAS_TELEGRAM_BOT_TOKEN}/getWebhookInfo"
```

## Smoke Test

- `GET /actuator/health` returns `UP`.
- Application starts with `ATLAS_TELEGRAM_ENABLED=true` and a configured bot token.
- Startup logs show Telegram integration enabled.
- If automatic registration is enabled, startup logs show webhook registration success.
- Send `/start` to the bot in Telegram.
- Bot replies in Telegram.
- Send `/day` and verify a planner response.
- Send an unsupported or blank update through the webhook and verify the backend does not crash.
- Verify logs contain `update_id`, `chat_id`, `handled`, and `request_type` where available.
- Verify logs do not contain bot token, webhook secret, or full user message text.

## Common Issues

Bot does not answer:
Check `ATLAS_TELEGRAM_ENABLED`, bot token, webhook URL, app logs, and `getWebhookInfo`.

Health is down:
Check datasource variables, PostgreSQL availability, migrations, container logs, and `/actuator/health`.

Wrong webhook URL:
Verify `ATLAS_PUBLIC_BASE_URL` and `ATLAS_TELEGRAM_WEBHOOK_PATH`. The final URL must be reachable by Telegram over HTTPS.

Invalid token:
Regenerate the token in BotFather and update only the deployment secret store.

Webhook secret mismatch:
The value in `ATLAS_TELEGRAM_WEBHOOK_SECRET` must match the `secret_token` used in `setWebhook`.

App is running but Telegram cannot reach it:
Check DNS, HTTPS certificate, reverse proxy routing, firewall rules, and public access to the webhook path.

Pending updates after deploy:
Use `ATLAS_TELEGRAM_DROP_PENDING_UPDATES_ON_WEBHOOK_REGISTRATION=true` during registration when old updates should be discarded.

## Security Notes

- Never commit the bot token.
- Use `ATLAS_TELEGRAM_WEBHOOK_SECRET` in production.
- Use HTTPS for the public webhook URL.
- Keep Telegram disabled locally by default.
- Store production secrets in the deployment secret manager or environment, not in git.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

<groupId>com.example</groupId>
<artifactId>atlas</artifactId>
<version>0.3.2</version>
<version>0.3.3</version>
<name>ATLAS</name>
<description>Personal AI system for rhythm, training, recovery, habits, nutrition and progress.</description>
<description>Personal system for rhythm, training, recovery, habits, nutrition and progress.</description>

<properties>
<java.version>21</java.version>
Expand Down
33 changes: 31 additions & 2 deletions src/main/java/com/example/atlas/config/AtlasProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,46 @@ public record AtlasProperties(Telegram telegram) {

public AtlasProperties {
if (telegram == null) {
telegram = new Telegram(false, "", "");
telegram = new Telegram(false, "", "", "/telegram/webhook", "", "", false, true);
}
}

public record Telegram(
boolean enabled,
String botToken,
String botUsername
String botUsername,
String webhookPath,
String webhookSecret,
String publicBaseUrl,
boolean registerWebhookOnStartup,
boolean dropPendingUpdatesOnWebhookRegistration
) {
public Telegram {
botToken = defaultString(botToken);
botUsername = defaultString(botUsername);
webhookPath = defaultString(webhookPath, "/telegram/webhook");
webhookSecret = defaultString(webhookSecret);
publicBaseUrl = defaultString(publicBaseUrl);
}

public boolean hasBotToken() {
return botToken != null && !botToken.isBlank();
}

public boolean hasWebhookSecret() {
return webhookSecret != null && !webhookSecret.isBlank();
}

public boolean hasPublicBaseUrl() {
return publicBaseUrl != null && !publicBaseUrl.isBlank();
}

private static String defaultString(String value) {
return defaultString(value, "");
}

private static String defaultString(String value, String fallback) {
return value == null ? fallback : value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public OrchestratorService(List<Agent> agents) {

public AgentResult route(String message) {
RequestType requestType = resolveRequestType(message);
return route(requestType, message);
}

public AgentResult route(RequestType requestType, String message) {
AgentContext context = AgentContext.anonymous(message, requestType);

List<AgentResult> results = agents.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,23 @@ public TelegramMessageSender(TelegramApiClient telegramApiClient) {
}

public void sendText(long chatId, String text) {
for (String chunk : splitText(text)) {
List<String> chunks = splitText(text);
for (int index = 0; index < chunks.size(); index++) {
String chunk = chunks.get(index);
try {
telegramApiClient.sendMessage(chatId, chunk);
log.info(
"Telegram sendMessage succeeded: chat_id={}, chunk_index={}, chunk_count={}",
chatId,
index + 1,
chunks.size()
);
} catch (RuntimeException exception) {
log.warn(
"Telegram sendMessage failed for chat {} with {}",
"Telegram sendMessage failed: chat_id={}, chunk_index={}, chunk_count={}, error_type={}",
chatId,
index + 1,
chunks.size(),
exception.getClass().getSimpleName()
);
}
Expand Down
Loading
Loading