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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user