Release v1.1.0: Add Telegram and Todoist integrations UI
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 57s
- Add telegram_integrations table to store bot token and chat_id - Add Integrations tab with Todoist and Telegram integration screens - Remove TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID from env variables - All Telegram configuration now done through UI - Telegram webhook registration happens when user saves bot token - Rename TELEGRAM_WEBHOOK_BASE_URL to WEBHOOK_BASE_URL
This commit is contained in:
@@ -56,9 +56,8 @@ docker run -d \
|
||||
- `DB_USER` - пользователь БД
|
||||
- `DB_PASSWORD` - пароль БД
|
||||
- `DB_NAME` - имя БД
|
||||
- `TELEGRAM_BOT_TOKEN` - токен Telegram бота (опционально)
|
||||
- `TELEGRAM_CHAT_ID` - ID чата Telegram (опционально)
|
||||
- `TELEGRAM_WEBHOOK_BASE_URL` - базовый URL для webhook (опционально)
|
||||
- `WEBHOOK_BASE_URL` - базовый URL для webhook (опционально)
|
||||
- Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
||||
- `TODOIST_WEBHOOK_SECRET` - секрет для Todoist webhook (опционально)
|
||||
|
||||
**Важно:** Backend внутри контейнера всегда работает на порту 8080. Nginx проксирует запросы с порта 80 на backend.
|
||||
|
||||
25
ENV_SETUP.md
25
ENV_SETUP.md
@@ -38,10 +38,9 @@
|
||||
|
||||
**Примечание:** API запросы автоматически проксируются к бэкенду. В development режиме Vite проксирует запросы к `http://localhost:8080`. В production nginx проксирует запросы к бэкенд контейнеру. Не требуется настройка `VITE_API_BASE_URL`.
|
||||
|
||||
### Telegram Bot Configuration (опционально)
|
||||
- `TELEGRAM_BOT_TOKEN` - токен бота от @BotFather
|
||||
- `TELEGRAM_CHAT_ID` - ID чата для отправки сообщений
|
||||
- `TELEGRAM_WEBHOOK_BASE_URL` - базовый URL для автоматической настройки webhook. Webhook будет настроен автоматически при старте сервера на `<TELEGRAM_WEBHOOK_BASE_URL>/webhook/telegram`. Если не указан, webhook нужно настраивать вручную.
|
||||
### Telegram Bot Configuration
|
||||
- `WEBHOOK_BASE_URL` - базовый URL для автоматической настройки webhook. Webhook будет настроен автоматически при сохранении bot token через UI на `<WEBHOOK_BASE_URL>/webhook/telegram`.
|
||||
- Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
||||
|
||||
**Примеры значений:**
|
||||
- Production с HTTPS: `https://your-domain.com` (порт не нужен для стандартных 80/443)
|
||||
@@ -213,12 +212,14 @@ WEB_PORT=4001 # для production Docker контейнера
|
||||
#### Автоматическая настройка (рекомендуется)
|
||||
|
||||
1. Создайте бота через [@BotFather](https://t.me/botfather) в Telegram
|
||||
2. Получите токен бота и добавьте его в `.env`:
|
||||
2. Получите токен бота
|
||||
3. Добавьте `WEBHOOK_BASE_URL` в `.env`:
|
||||
```bash
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
TELEGRAM_WEBHOOK_BASE_URL=https://your-domain.com
|
||||
WEBHOOK_BASE_URL=https://your-domain.com
|
||||
```
|
||||
4. Откройте приложение и перейдите в раздел "Интеграции" -> "Telegram"
|
||||
5. Введите Bot Token в поле и нажмите "Сохранить"
|
||||
6. Отправьте первое сообщение боту в Telegram - Chat ID будет сохранён автоматически
|
||||
|
||||
**Важно о портах:**
|
||||
- Если сервер доступен на стандартных портах (HTTP 80 или HTTPS 443), порт можно не указывать
|
||||
@@ -231,8 +232,8 @@ WEB_PORT=4001 # для production Docker контейнера
|
||||
```bash
|
||||
# Установите ngrok: https://ngrok.com/
|
||||
ngrok http 8080
|
||||
# Используйте полученный URL в TELEGRAM_WEBHOOK_BASE_URL (без порта)
|
||||
# Например: TELEGRAM_WEBHOOK_BASE_URL=https://abc123.ngrok.io
|
||||
# Используйте полученный URL в WEBHOOK_BASE_URL (без порта)
|
||||
# Например: WEBHOOK_BASE_URL=https://abc123.ngrok.io
|
||||
```
|
||||
|
||||
4. Проверьте логи сервера - должно появиться сообщение:
|
||||
@@ -240,9 +241,9 @@ WEB_PORT=4001 # для production Docker контейнера
|
||||
Telegram webhook configured successfully: https://abc123.ngrok.io/webhook/telegram
|
||||
```
|
||||
|
||||
#### Ручная настройка (если не указан TELEGRAM_WEBHOOK_BASE_URL)
|
||||
#### Ручная настройка (если не указан WEBHOOK_BASE_URL)
|
||||
|
||||
Если вы не указали `TELEGRAM_WEBHOOK_BASE_URL`, webhook нужно настроить вручную:
|
||||
Если вы не указали `WEBHOOK_BASE_URL`, webhook нужно настроить вручную:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
### Создание дампа
|
||||
|
||||
```bash
|
||||
# Дамп из production БД (по умолчанию .env.prod)
|
||||
# Дамп из БД (по умолчанию .env)
|
||||
./dump-db.sh
|
||||
|
||||
# Дамп с именем
|
||||
./dump-db.sh production-backup
|
||||
|
||||
# Дамп из локальной БД
|
||||
./dump-db.sh --env-file .env.local
|
||||
|
||||
# Дамп из другого окружения
|
||||
./dump-db.sh --env-file .env.prod my-backup
|
||||
```
|
||||
@@ -29,10 +26,10 @@
|
||||
### Восстановление дампа
|
||||
|
||||
```bash
|
||||
# Восстановление в локальную БД (по умолчанию .env.local)
|
||||
# Восстановление в БД (по умолчанию .env)
|
||||
./restore-db.sh dump_20240101_120000.sql.gz
|
||||
|
||||
# Восстановление в production БД
|
||||
# Восстановление в другое окружение
|
||||
./restore-db.sh --env-file .env.prod dump_20240101_120000.sql.gz
|
||||
|
||||
# Можно указать имя без расширения
|
||||
@@ -41,8 +38,8 @@
|
||||
|
||||
## Поведение по умолчанию
|
||||
|
||||
- **Создание дампа**: использует `.env.prod` (production БД)
|
||||
- **Восстановление**: использует `.env.local` (локальная БД)
|
||||
- **Создание дампа**: использует `.env`
|
||||
- **Восстановление**: использует `.env`
|
||||
|
||||
Это можно изменить с помощью параметра `--env-file`.
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ services:
|
||||
DB_PASSWORD: ${DB_PASSWORD:-playeng}
|
||||
DB_NAME: ${DB_NAME:-playeng}
|
||||
PORT: ${PORT:-8080}
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
# Скрипт для создания дампа базы данных
|
||||
# Использование:
|
||||
# ./dump-db.sh [имя_дампа] # Дамп из .env.prod
|
||||
# ./dump-db.sh --env-file .env.local [имя] # Дамп из указанного файла
|
||||
# ./dump-db.sh production-backup # Именованный дамп из .env.prod
|
||||
# ./dump-db.sh [имя_дампа] # Дамп из .env
|
||||
# ./dump-db.sh --env-file .env.prod [имя] # Дамп из указанного файла
|
||||
# ./dump-db.sh production-backup # Именованный дамп из .env
|
||||
|
||||
set -e
|
||||
|
||||
# Значения по умолчанию
|
||||
DEFAULT_ENV_FILE=".env.prod"
|
||||
DEFAULT_ENV_FILE=".env"
|
||||
ENV_FILE="$DEFAULT_ENV_FILE"
|
||||
DUMP_NAME=""
|
||||
|
||||
|
||||
11
env.example
11
env.example
@@ -26,21 +26,18 @@ PORT=8080
|
||||
WEB_PORT=3001
|
||||
|
||||
# ============================================
|
||||
# Telegram Bot Configuration (optional)
|
||||
# Telegram Bot Configuration
|
||||
# ============================================
|
||||
# Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
||||
# Get token from @BotFather in Telegram: https://t.me/botfather
|
||||
# To get chat ID: send a message to your bot, then visit: https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
|
||||
# Look for "chat":{"id":123456789} - that number is your chat ID
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
# Base URL для автоматической настройки webhook
|
||||
# Примеры:
|
||||
# - Для production с HTTPS: https://your-domain.com
|
||||
# - Для локальной разработки с ngrok: https://abc123.ngrok.io
|
||||
# - Для прямого доступа на нестандартном порту: http://your-server:8080
|
||||
# Webhook будет настроен автоматически при старте сервера на: <TELEGRAM_WEBHOOK_BASE_URL>/webhook/telegram
|
||||
# Webhook будет настроен автоматически при старте сервера на: <WEBHOOK_BASE_URL>/webhook/telegram
|
||||
# Если не указан, webhook нужно настраивать вручную
|
||||
TELEGRAM_WEBHOOK_BASE_URL=https://your-domain.com
|
||||
WEBHOOK_BASE_URL=https://your-domain.com
|
||||
|
||||
# ============================================
|
||||
# Todoist Webhook Configuration (optional)
|
||||
|
||||
@@ -23,13 +23,13 @@ if ls "$DUMP_DIR"/*.sql.gz 2>/dev/null | grep -q .; then
|
||||
echo "Всего дампов: $(ls -1 "$DUMP_DIR"/*.sql.gz 2>/dev/null | wc -l | tr -d ' ')"
|
||||
echo ""
|
||||
echo "Для восстановления используйте:"
|
||||
echo " ./restore-db.sh <имя_дампа.sql.gz> # В .env.local"
|
||||
echo " ./restore-db.sh <имя_дампа.sql.gz> # В .env"
|
||||
echo " ./restore-db.sh --env-file .env.prod <имя_дампа> # В указанный файл"
|
||||
else
|
||||
echo " (нет дампов)"
|
||||
echo ""
|
||||
echo "Для создания дампа используйте:"
|
||||
echo " ./dump-db.sh # Из .env.prod"
|
||||
echo " ./dump-db.sh --env-file .env.local [имя] # Из указанного файла"
|
||||
echo " ./dump-db.sh # Из .env"
|
||||
echo " ./dump-db.sh --env-file .env.prod [имя] # Из указанного файла"
|
||||
fi
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
### Опциональные (для Telegram интеграции)
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN` - токен бота от @BotFather
|
||||
- `TELEGRAM_CHAT_ID` - ID чата для отправки сообщений
|
||||
- `WEBHOOK_BASE_URL` - базовый URL для автоматической настройки webhook
|
||||
- Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
||||
|
||||
## Использование в коде
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
### Вариант 1: Установить переменные вручную
|
||||
```bash
|
||||
export DB_PASSWORD=your_password
|
||||
export TELEGRAM_BOT_TOKEN=your_token
|
||||
go run main.go
|
||||
```
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ services:
|
||||
DB_PASSWORD: ${DB_PASSWORD:-playeng}
|
||||
DB_NAME: ${DB_NAME:-playeng}
|
||||
PORT: ${PORT:-8080}
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -8,12 +8,9 @@ DB_NAME=playeng
|
||||
# Server Configuration
|
||||
PORT=8080
|
||||
|
||||
# Telegram Bot Configuration (optional - for direct Telegram integration)
|
||||
# Telegram Bot Configuration
|
||||
# Bot Token и Chat ID настраиваются через UI приложения в разделе "Интеграции" -> "Telegram"
|
||||
# Get token from @BotFather in Telegram: https://t.me/botfather
|
||||
# To get chat ID: send a message to your bot, then visit: https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
|
||||
# Look for "chat":{"id":123456789} - that number is your chat ID
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
|
||||
# Scheduler Configuration
|
||||
# Часовой пояс для планировщика (формат IANA: Europe/Moscow, America/New_York и т.д.)
|
||||
|
||||
@@ -166,9 +166,14 @@ type TelegramEntity struct {
|
||||
Length int `json:"length"`
|
||||
}
|
||||
|
||||
type TelegramChat struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type TelegramMessage struct {
|
||||
Text string `json:"text"`
|
||||
Entities []TelegramEntity `json:"entities"`
|
||||
Chat TelegramChat `json:"chat"`
|
||||
}
|
||||
|
||||
type TelegramWebhook struct {
|
||||
@@ -1947,6 +1952,18 @@ func (a *App) initPlayLifeDB() error {
|
||||
log.Printf("Warning: Failed to create materialized view index: %v", err)
|
||||
}
|
||||
|
||||
// Создаем таблицу telegram_integrations
|
||||
createTelegramIntegrationsTable := `
|
||||
CREATE TABLE IF NOT EXISTS telegram_integrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chat_id VARCHAR(255),
|
||||
bot_token VARCHAR(255)
|
||||
)
|
||||
`
|
||||
if _, err := a.DB.Exec(createTelegramIntegrationsTable); err != nil {
|
||||
return fmt.Errorf("failed to create telegram_integrations table: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2346,49 +2363,31 @@ func main() {
|
||||
log.Printf("Successfully connected to database: %s@%s:%s/%s", dbUser, dbHost, dbPort, dbName)
|
||||
defer db.Close()
|
||||
|
||||
// Инициализируем Telegram бота (если токен указан)
|
||||
var telegramBot *tgbotapi.BotAPI
|
||||
var telegramChatID int64
|
||||
telegramToken := getEnv("TELEGRAM_BOT_TOKEN", "")
|
||||
telegramChatIDStr := getEnv("TELEGRAM_CHAT_ID", "")
|
||||
telegramWebhookBaseURL := getEnv("TELEGRAM_WEBHOOK_BASE_URL", "")
|
||||
// Telegram бот теперь загружается из БД при необходимости
|
||||
// Webhook будет настроен автоматически при сохранении bot token через UI
|
||||
|
||||
if telegramToken != "" && telegramChatIDStr != "" {
|
||||
bot, err := tgbotapi.NewBotAPI(telegramToken)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to initialize Telegram bot: %v. Telegram notifications will be disabled.", err)
|
||||
} else {
|
||||
telegramBot = bot
|
||||
chatID, err := strconv.ParseInt(telegramChatIDStr, 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Invalid TELEGRAM_CHAT_ID format: %v. Telegram notifications will be disabled.", err)
|
||||
telegramBot = nil
|
||||
} else {
|
||||
telegramChatID = chatID
|
||||
log.Printf("Telegram bot initialized successfully. Chat ID: %d", telegramChatID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Println("Telegram bot token or chat ID not provided. Telegram notifications disabled.")
|
||||
}
|
||||
|
||||
// Настраиваем webhook для Telegram (если указан base URL)
|
||||
if telegramToken != "" && telegramWebhookBaseURL != "" {
|
||||
webhookURL := strings.TrimRight(telegramWebhookBaseURL, "/") + "/webhook/telegram"
|
||||
if err := setupTelegramWebhook(telegramToken, webhookURL); err != nil {
|
||||
log.Printf("Warning: Failed to setup Telegram webhook: %v. Webhook will not be configured.", err)
|
||||
} else {
|
||||
log.Printf("Telegram webhook configured successfully: %s", webhookURL)
|
||||
}
|
||||
} else if telegramToken != "" {
|
||||
log.Println("TELEGRAM_WEBHOOK_BASE_URL not provided. Telegram webhook will not be configured automatically.")
|
||||
}
|
||||
|
||||
app := &App{
|
||||
DB: db,
|
||||
lastWebhookTime: make(map[int]time.Time),
|
||||
telegramBot: telegramBot,
|
||||
telegramChatID: telegramChatID,
|
||||
telegramBot: nil, // Больше не используем глобальный bot
|
||||
telegramChatID: 0, // Больше не используем глобальный chat_id
|
||||
}
|
||||
|
||||
// Пытаемся настроить webhook автоматически при старте, если есть base URL и bot token в БД
|
||||
// Это опционально - основная регистрация происходит при сохранении токена через UI
|
||||
webhookBaseURL := getEnv("WEBHOOK_BASE_URL", "")
|
||||
if webhookBaseURL != "" {
|
||||
integration, err := app.getTelegramIntegration()
|
||||
if err == nil && integration.BotToken != nil && *integration.BotToken != "" {
|
||||
webhookURL := strings.TrimRight(webhookBaseURL, "/") + "/webhook/telegram"
|
||||
if err := setupTelegramWebhook(*integration.BotToken, webhookURL); err != nil {
|
||||
log.Printf("Warning: Failed to setup Telegram webhook at startup: %v. Webhook will be configured when user saves bot token.", err)
|
||||
} else {
|
||||
log.Printf("Telegram webhook configured successfully at startup: %s", webhookURL)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Telegram bot token not found in database. Webhook will be configured when user saves bot token.")
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем БД для play-life проекта
|
||||
@@ -2440,6 +2439,9 @@ func main() {
|
||||
r.HandleFunc("/admin", app.adminHandler).Methods("GET")
|
||||
r.HandleFunc("/admin.html", app.adminHandler).Methods("GET")
|
||||
r.HandleFunc("/admin/recreate-mv", app.recreateMaterializedViewHandler).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/integrations/telegram", app.getTelegramIntegrationHandler).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/integrations/telegram", app.updateTelegramIntegrationHandler).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/integrations/todoist/webhook-url", app.getTodoistWebhookURLHandler).Methods("GET", "OPTIONS")
|
||||
|
||||
port := getEnv("PORT", "8080")
|
||||
log.Printf("Server starting on port %s", port)
|
||||
@@ -2529,13 +2531,145 @@ func roundToFourDecimals(val float64) float64 {
|
||||
return float64(int(val*10000+0.5)) / 10000.0
|
||||
}
|
||||
|
||||
// TelegramIntegration представляет запись из таблицы telegram_integrations
|
||||
type TelegramIntegration struct {
|
||||
ID int `json:"id"`
|
||||
ChatID *string `json:"chat_id"`
|
||||
BotToken *string `json:"bot_token"`
|
||||
}
|
||||
|
||||
// getTelegramIntegration получает telegram интеграцию из БД
|
||||
func (a *App) getTelegramIntegration() (*TelegramIntegration, error) {
|
||||
var integration TelegramIntegration
|
||||
var chatID, botToken sql.NullString
|
||||
|
||||
err := a.DB.QueryRow(`
|
||||
SELECT id, chat_id, bot_token
|
||||
FROM telegram_integrations
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`).Scan(&integration.ID, &chatID, &botToken)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
// Если записи нет, создаем новую
|
||||
_, err = a.DB.Exec(`
|
||||
INSERT INTO telegram_integrations (chat_id, bot_token)
|
||||
VALUES (NULL, NULL)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create telegram integration: %w", err)
|
||||
}
|
||||
// Повторно получаем созданную запись
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT id, chat_id, bot_token
|
||||
FROM telegram_integrations
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`).Scan(&integration.ID, &chatID, &botToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get created telegram integration: %w", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to get telegram integration: %w", err)
|
||||
}
|
||||
|
||||
if chatID.Valid {
|
||||
integration.ChatID = &chatID.String
|
||||
}
|
||||
if botToken.Valid {
|
||||
integration.BotToken = &botToken.String
|
||||
}
|
||||
|
||||
return &integration, nil
|
||||
}
|
||||
|
||||
// saveTelegramBotToken сохраняет bot token в БД
|
||||
func (a *App) saveTelegramBotToken(botToken string) error {
|
||||
// Проверяем, есть ли уже запись
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err != nil {
|
||||
// Если записи нет, создаем новую
|
||||
_, err = a.DB.Exec(`
|
||||
INSERT INTO telegram_integrations (bot_token, chat_id)
|
||||
VALUES ($1, NULL)
|
||||
`, botToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create telegram bot token: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Обновляем существующую запись
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE telegram_integrations
|
||||
SET bot_token = $1
|
||||
WHERE id = $2
|
||||
`, botToken, integration.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update telegram bot token: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveTelegramChatID сохраняет chat_id в БД
|
||||
func (a *App) saveTelegramChatID(chatID string) error {
|
||||
// Получаем текущую интеграцию
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get telegram integration: %w", err)
|
||||
}
|
||||
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE telegram_integrations
|
||||
SET chat_id = $1
|
||||
WHERE id = $2
|
||||
`, chatID, integration.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save telegram chat_id: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTelegramBotAndChatID получает bot token и chat_id из БД и создает bot API
|
||||
func (a *App) getTelegramBotAndChatID() (*tgbotapi.BotAPI, int64, error) {
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if integration.BotToken == nil || *integration.BotToken == "" {
|
||||
return nil, 0, nil // Bot token не настроен
|
||||
}
|
||||
|
||||
bot, err := tgbotapi.NewBotAPI(*integration.BotToken)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to initialize Telegram bot: %w", err)
|
||||
}
|
||||
|
||||
var chatID int64 = 0
|
||||
if integration.ChatID != nil && *integration.ChatID != "" {
|
||||
chatID, err = strconv.ParseInt(*integration.ChatID, 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Invalid chat_id format in database: %v", err)
|
||||
chatID = 0
|
||||
}
|
||||
}
|
||||
|
||||
return bot, chatID, nil
|
||||
}
|
||||
|
||||
func (a *App) sendTelegramMessage(text string) {
|
||||
log.Printf("sendTelegramMessage called with text length: %d", len(text))
|
||||
log.Printf("Telegram bot status: bot=%v, chatID=%d", a.telegramBot != nil, a.telegramChatID)
|
||||
|
||||
if a.telegramBot == nil || a.telegramChatID == 0 {
|
||||
// Получаем bot и chat_id из БД
|
||||
bot, chatID, err := a.getTelegramBotAndChatID()
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Failed to get Telegram bot from database: %v, skipping message send", err)
|
||||
return
|
||||
}
|
||||
|
||||
if bot == nil || chatID == 0 {
|
||||
// Telegram не настроен, пропускаем отправку
|
||||
log.Printf("WARNING: Telegram bot not initialized (bot=%v, chatID=%d), skipping message send", a.telegramBot != nil, a.telegramChatID)
|
||||
log.Printf("WARNING: Telegram bot not configured (bot=%v, chatID=%d), skipping message send", bot != nil, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2545,14 +2679,14 @@ func (a *App) sendTelegramMessage(text string) {
|
||||
telegramText := regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(text, "*$1*")
|
||||
log.Printf("Sending Telegram message (converted text length: %d): %s", len(telegramText), telegramText)
|
||||
|
||||
msg := tgbotapi.NewMessage(a.telegramChatID, telegramText)
|
||||
msg := tgbotapi.NewMessage(chatID, telegramText)
|
||||
msg.ParseMode = "Markdown" // Markdown (Legacy) format
|
||||
|
||||
_, err := a.telegramBot.Send(msg)
|
||||
_, err = bot.Send(msg)
|
||||
if err != nil {
|
||||
log.Printf("ERROR sending Telegram message: %v", err)
|
||||
} else {
|
||||
log.Printf("Telegram message sent successfully to chat ID %d", a.telegramChatID)
|
||||
log.Printf("Telegram message sent successfully to chat ID %d", chatID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3966,6 +4100,22 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем chat_id при первом сообщении
|
||||
if update.Message.Chat.ID != 0 {
|
||||
chatIDStr := strconv.FormatInt(update.Message.Chat.ID, 10)
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err == nil {
|
||||
// Сохраняем chat_id, если его еще нет
|
||||
if integration.ChatID == nil || *integration.ChatID == "" {
|
||||
if err := a.saveTelegramChatID(chatIDStr); err != nil {
|
||||
log.Printf("Warning: Failed to save chat_id: %v", err)
|
||||
} else {
|
||||
log.Printf("Saved chat_id from first message: %s", chatIDStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что есть message
|
||||
if update.Message.Text == "" {
|
||||
log.Printf("Telegram webhook: no text in message")
|
||||
@@ -4077,3 +4227,100 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(statistics)
|
||||
}
|
||||
|
||||
// getTelegramIntegrationHandler возвращает текущую telegram интеграцию
|
||||
func (a *App) getTelegramIntegrationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
setCORSHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
setCORSHeaders(w)
|
||||
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Failed to get telegram integration: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(integration)
|
||||
}
|
||||
|
||||
// TelegramIntegrationUpdateRequest представляет запрос на обновление telegram интеграции
|
||||
type TelegramIntegrationUpdateRequest struct {
|
||||
BotToken string `json:"bot_token"`
|
||||
}
|
||||
|
||||
// updateTelegramIntegrationHandler обновляет bot token для telegram интеграции
|
||||
func (a *App) updateTelegramIntegrationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
setCORSHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
setCORSHeaders(w)
|
||||
|
||||
var req TelegramIntegrationUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.BotToken == "" {
|
||||
sendErrorWithCORS(w, "bot_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.saveTelegramBotToken(req.BotToken); err != nil {
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Failed to save bot token: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Настраиваем webhook автоматически при сохранении токена
|
||||
webhookBaseURL := getEnv("WEBHOOK_BASE_URL", "")
|
||||
if webhookBaseURL != "" {
|
||||
webhookURL := strings.TrimRight(webhookBaseURL, "/") + "/webhook/telegram"
|
||||
if err := setupTelegramWebhook(req.BotToken, webhookURL); err != nil {
|
||||
log.Printf("Warning: Failed to setup Telegram webhook: %v", err)
|
||||
// Не возвращаем ошибку, так как токен уже сохранен
|
||||
} else {
|
||||
log.Printf("Telegram webhook configured successfully: %s", webhookURL)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Warning: WEBHOOK_BASE_URL not set. Webhook will not be configured automatically.")
|
||||
}
|
||||
|
||||
integration, err := a.getTelegramIntegration()
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Failed to get updated integration: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(integration)
|
||||
}
|
||||
|
||||
// getTodoistWebhookURLHandler возвращает URL для Todoist webhook
|
||||
func (a *App) getTodoistWebhookURLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
setCORSHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
setCORSHeaders(w)
|
||||
|
||||
// Получаем base URL из env
|
||||
baseURL := getEnv("WEBHOOK_BASE_URL", "")
|
||||
if baseURL == "" {
|
||||
sendErrorWithCORS(w, "WEBHOOK_BASE_URL not configured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
webhookURL := strings.TrimRight(baseURL, "/") + "/webhook/todoist"
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"webhook_url": webhookURL,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration: Add telegram_integrations table
|
||||
-- This script creates a table to store Telegram bot tokens and chat IDs
|
||||
|
||||
-- Create telegram_integrations table
|
||||
CREATE TABLE IF NOT EXISTS telegram_integrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chat_id VARCHAR(255),
|
||||
bot_token VARCHAR(255)
|
||||
);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON TABLE telegram_integrations IS 'Stores Telegram bot tokens and chat IDs for integrations';
|
||||
COMMENT ON COLUMN telegram_integrations.id IS 'Auto-increment primary key';
|
||||
COMMENT ON COLUMN telegram_integrations.chat_id IS 'Telegram chat ID (nullable, set automatically after first message)';
|
||||
COMMENT ON COLUMN telegram_integrations.bot_token IS 'Telegram bot token (nullable, set by user)';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -7,6 +7,7 @@ import AddWords from './components/AddWords'
|
||||
import TestConfigSelection from './components/TestConfigSelection'
|
||||
import AddConfig from './components/AddConfig'
|
||||
import TestWords from './components/TestWords'
|
||||
import Integrations from './components/Integrations'
|
||||
|
||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||
const CURRENT_WEEK_API_URL = '/playlife-feed'
|
||||
@@ -24,6 +25,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
})
|
||||
|
||||
// Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок)
|
||||
@@ -36,6 +38,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
})
|
||||
|
||||
// Параметры для навигации между вкладками
|
||||
@@ -74,7 +77,7 @@ function App() {
|
||||
|
||||
try {
|
||||
const savedTab = window.localStorage?.getItem('activeTab')
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test']
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'integrations']
|
||||
if (savedTab && validTabs.includes(savedTab)) {
|
||||
setActiveTab(savedTab)
|
||||
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
|
||||
@@ -184,6 +187,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
})
|
||||
|
||||
// Используем ref для отслеживания кеша (чтобы не зависеть от состояния в useCallback)
|
||||
@@ -471,6 +475,12 @@ function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs.integrations && (
|
||||
<div className={activeTab === 'integrations' ? 'block' : 'hidden'}>
|
||||
<Integrations onNavigate={handleNavigate} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -519,6 +529,26 @@ function App() {
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('integrations')}
|
||||
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
|
||||
activeTab === 'integrations'
|
||||
? 'text-indigo-700 bg-white/50'
|
||||
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
|
||||
}`}
|
||||
title="Интеграции"
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
</span>
|
||||
{activeTab === 'integrations' && (
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
25
play-life-web/src/components/Integrations.css
Normal file
25
play-life-web/src/components/Integrations.css
Normal file
@@ -0,0 +1,25 @@
|
||||
.close-x-button {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
z-index: 1600;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.close-x-button:hover {
|
||||
background-color: #ffffff;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
57
play-life-web/src/components/Integrations.jsx
Normal file
57
play-life-web/src/components/Integrations.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react'
|
||||
import TodoistIntegration from './TodoistIntegration'
|
||||
import TelegramIntegration from './TelegramIntegration'
|
||||
|
||||
function Integrations({ onNavigate }) {
|
||||
const [selectedIntegration, setSelectedIntegration] = useState(null)
|
||||
|
||||
const integrations = [
|
||||
{ id: 'todoist', name: 'TODOist' },
|
||||
{ id: 'telegram', name: 'Telegram' },
|
||||
]
|
||||
|
||||
if (selectedIntegration) {
|
||||
if (selectedIntegration === 'todoist') {
|
||||
return <TodoistIntegration onBack={() => setSelectedIntegration(null)} />
|
||||
} else if (selectedIntegration === 'telegram') {
|
||||
return <TelegramIntegration onBack={() => setSelectedIntegration(null)} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Интеграции</h1>
|
||||
<div className="space-y-4">
|
||||
{integrations.map((integration) => (
|
||||
<button
|
||||
key={integration.id}
|
||||
onClick={() => setSelectedIntegration(integration.id)}
|
||||
className="w-full p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow text-left border border-gray-200 hover:border-indigo-300"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-semibold text-gray-800">
|
||||
{integration.name}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Integrations
|
||||
|
||||
170
play-life-web/src/components/TelegramIntegration.jsx
Normal file
170
play-life-web/src/components/TelegramIntegration.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import './Integrations.css'
|
||||
|
||||
function TelegramIntegration({ onBack }) {
|
||||
const [botToken, setBotToken] = useState('')
|
||||
const [chatId, setChatId] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchIntegration()
|
||||
}, [])
|
||||
|
||||
const fetchIntegration = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch('/api/integrations/telegram')
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при загрузке интеграции')
|
||||
}
|
||||
const data = await response.json()
|
||||
setBotToken(data.bot_token || '')
|
||||
setChatId(data.chat_id || '')
|
||||
} catch (error) {
|
||||
console.error('Error fetching integration:', error)
|
||||
setError('Не удалось загрузить данные интеграции')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!botToken.trim()) {
|
||||
setError('Bot Token обязателен для заполнения')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
const response = await fetch('/api/integrations/telegram', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ bot_token: botToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Ошибка при сохранении')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setBotToken(data.bot_token || '')
|
||||
setChatId(data.chat_id || '')
|
||||
setSuccess('Bot Token успешно сохранен!')
|
||||
} catch (error) {
|
||||
console.error('Error saving integration:', error)
|
||||
setError(error.message || 'Не удалось сохранить Bot Token')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<button className="close-x-button" onClick={onBack} title="Закрыть">
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-bold mb-6">Telegram интеграция</h1>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-gray-500">Загрузка...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Настройки</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Telegram Bot Token
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={botToken}
|
||||
onChange={(e) => setBotToken(e.target.value)}
|
||||
placeholder="Введите Bot Token"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{chatId && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Chat ID (устанавливается автоматически)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={chatId}
|
||||
readOnly
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !botToken.trim()}
|
||||
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? 'Сохранение...' : 'Сохранить Bot Token'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-blue-900">
|
||||
Откуда взять Bot Token
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
||||
<li>Откройте Telegram и найдите бота @BotFather</li>
|
||||
<li>Отправьте команду /newbot</li>
|
||||
<li>Следуйте инструкциям для создания нового бота</li>
|
||||
<li>
|
||||
После создания бота BotFather предоставит вам Bot Token
|
||||
</li>
|
||||
<li>Скопируйте токен и вставьте его в поле выше</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-yellow-900">
|
||||
Что нужно сделать после сохранения Bot Token
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
||||
<li>После сохранения Bot Token отправьте первое сообщение вашему боту в Telegram</li>
|
||||
<li>
|
||||
Chat ID будет автоматически сохранен после обработки первого
|
||||
сообщения
|
||||
</li>
|
||||
<li>
|
||||
После этого бот сможет отправлять вам ответные сообщения
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TelegramIntegration
|
||||
|
||||
90
play-life-web/src/components/TodoistIntegration.jsx
Normal file
90
play-life-web/src/components/TodoistIntegration.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import './Integrations.css'
|
||||
|
||||
function TodoistIntegration({ onBack }) {
|
||||
const [webhookURL, setWebhookURL] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchWebhookURL()
|
||||
}, [])
|
||||
|
||||
const fetchWebhookURL = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch('/api/integrations/todoist/webhook-url')
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при загрузке URL webhook')
|
||||
}
|
||||
const data = await response.json()
|
||||
setWebhookURL(data.webhook_url)
|
||||
} catch (error) {
|
||||
console.error('Error fetching webhook URL:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(webhookURL)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<button className="close-x-button" onClick={onBack} title="Закрыть">
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-bold mb-6">TODOist интеграция</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Webhook URL</h2>
|
||||
{loading ? (
|
||||
<div className="text-gray-500">Загрузка...</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={webhookURL}
|
||||
readOnly
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors whitespace-nowrap"
|
||||
>
|
||||
{copied ? 'Скопировано!' : 'Копировать'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-blue-900">
|
||||
Как использовать в приложении TODOist
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
||||
<li>Откройте приложение TODOist на вашем устройстве</li>
|
||||
<li>Перейдите в настройки проекта или задачи</li>
|
||||
<li>Найдите раздел "Интеграции" или "Webhooks"</li>
|
||||
<li>Вставьте скопированный URL webhook в соответствующее поле</li>
|
||||
<li>Сохраните настройки</li>
|
||||
<li>
|
||||
Теперь при закрытии задач в TODOist они будут автоматически
|
||||
обрабатываться системой
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoistIntegration
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
# Скрипт для восстановления базы данных из дампа
|
||||
# Использование:
|
||||
# ./restore-db.sh [имя_дампа.sql.gz] # Восстановление в .env.local
|
||||
# ./restore-db.sh [имя_дампа.sql.gz] # Восстановление в .env
|
||||
# ./restore-db.sh --env-file .env.prod [имя_дампа] # Восстановление в указанный файл
|
||||
# ./restore-db.sh production-backup.sql.gz # Восстановление в .env.local
|
||||
# ./restore-db.sh production-backup.sql.gz # Восстановление в .env
|
||||
|
||||
set -e
|
||||
|
||||
# Значения по умолчанию
|
||||
DEFAULT_ENV_FILE=".env.local"
|
||||
DEFAULT_ENV_FILE=".env"
|
||||
ENV_FILE="$DEFAULT_ENV_FILE"
|
||||
DUMP_FILE=""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user