From 6f77f0643c00ba545ff7a73654fee78826e2130c Mon Sep 17 00:00:00 2001 From: poignatov Date: Thu, 1 Jan 2026 18:57:30 +0300 Subject: [PATCH] v2.0.5: Fix transaction errors and webhook parsing - Fixed transaction abort error in insertMessageData (replaced ON CONFLICT with SELECT check) - Fixed double body reading in setupTelegramWebhook (use json.Unmarshal) - Fixed Todoist webhook JSON parsing (use json.Unmarshal from bodyBytes) - Improved error handling in webhook responses - Added user_id to nodes insertion --- VERSION | 2 +- play-life-backend/main.go | 117 ++++++++++++++++++++++++++----------- play-life-web/package.json | 2 +- 3 files changed, 86 insertions(+), 35 deletions(-) diff --git a/VERSION b/VERSION index 2165f8f..e010258 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.4 +2.0.5 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 369aa5d..fe19fa9 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3280,15 +3280,19 @@ func setupTelegramWebhook(botToken, webhookURL string) error { } defer resp.Body.Close() - bodyBytes, _ := io.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } log.Printf("Telegram API response: status=%d, body=%s", resp.StatusCode, string(bodyBytes)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("telegram API returned status %d: %s", resp.StatusCode, string(bodyBytes)) } + // Декодируем из уже прочитанных байтов var result map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + if err := json.Unmarshal(bodyBytes, &result); err != nil { return fmt.Errorf("failed to decode response: %w", err) } @@ -3932,34 +3936,55 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr // Вставляем проекты for projectName := range projectNames { if userID != nil { - _, err := tx.Exec(` - INSERT INTO projects (name, deleted, user_id) - VALUES ($1, FALSE, $2) - ON CONFLICT ON CONSTRAINT unique_project_name DO UPDATE - SET name = EXCLUDED.name, deleted = FALSE - `, projectName, *userID) - if err != nil { - // Try without user constraint for backwards compatibility + // Используем более универсальный подход: проверяем существование и вставляем/обновляем + var existingID int + err := tx.QueryRow(` + SELECT id FROM projects + WHERE name = $1 AND user_id = $2 AND deleted = FALSE + `, projectName, *userID).Scan(&existingID) + + if err == sql.ErrNoRows { + // Проект не существует, создаем новый _, err = tx.Exec(` INSERT INTO projects (name, deleted, user_id) VALUES ($1, FALSE, $2) - ON CONFLICT (name) DO UPDATE - SET name = EXCLUDED.name, deleted = FALSE, user_id = COALESCE(projects.user_id, EXCLUDED.user_id) `, projectName, *userID) if err != nil { - return fmt.Errorf("failed to upsert project %s: %w", projectName, err) + // Если ошибка из-за уникальности, пробуем обновить существующий + _, err = tx.Exec(` + UPDATE projects + SET deleted = FALSE, user_id = COALESCE(user_id, $2) + WHERE name = $1 + `, projectName, *userID) + if err != nil { + return fmt.Errorf("failed to upsert project %s: %w", projectName, err) + } } + } else if err != nil { + return fmt.Errorf("failed to check project %s: %w", projectName, err) } + // Проект уже существует, ничего не делаем } else { - _, err := tx.Exec(` - INSERT INTO projects (name, deleted) - VALUES ($1, FALSE) - ON CONFLICT (name) DO UPDATE - SET name = EXCLUDED.name, deleted = FALSE - `, projectName) - if err != nil { - return fmt.Errorf("failed to upsert project %s: %w", projectName, err) + // Для случая без user_id (legacy) + var existingID int + err := tx.QueryRow(` + SELECT id FROM projects + WHERE name = $1 AND deleted = FALSE + `, projectName).Scan(&existingID) + + if err == sql.ErrNoRows { + // Проект не существует, создаем новый + _, err = tx.Exec(` + INSERT INTO projects (name, deleted) + VALUES ($1, FALSE) + `, projectName) + if err != nil { + return fmt.Errorf("failed to insert project %s: %w", projectName, err) + } + } else if err != nil { + return fmt.Errorf("failed to check project %s: %w", projectName, err) } + // Проект уже существует, ничего не делаем } } @@ -3984,12 +4009,37 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr // 3. Вставляем nodes for _, node := range nodes { - _, err := tx.Exec(` - INSERT INTO nodes (project_id, entry_id, score) - SELECT p.id, $1, $2 - FROM projects p - WHERE p.name = $3 AND p.deleted = FALSE - `, entryID, node.Score, node.Project) + var projectID int + if userID != nil { + err = tx.QueryRow(` + SELECT id FROM projects + WHERE name = $1 AND user_id = $2 AND deleted = FALSE + `, node.Project, *userID).Scan(&projectID) + } else { + err = tx.QueryRow(` + SELECT id FROM projects + WHERE name = $1 AND deleted = FALSE + `, node.Project).Scan(&projectID) + } + + if err == sql.ErrNoRows { + return fmt.Errorf("project %s not found after insert", node.Project) + } else if err != nil { + return fmt.Errorf("failed to find project %s: %w", node.Project, err) + } + + // Вставляем node с user_id + if userID != nil { + _, err = tx.Exec(` + INSERT INTO nodes (project_id, entry_id, score, user_id) + VALUES ($1, $2, $3, $4) + `, projectID, entryID, node.Score, *userID) + } else { + _, err = tx.Exec(` + INSERT INTO nodes (project_id, entry_id, score) + VALUES ($1, $2, $3) + `, projectID, entryID, node.Score) + } if err != nil { return fmt.Errorf("failed to insert node for project %s: %w", node.Project, err) } @@ -4963,9 +5013,6 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Request body (raw): %s", string(bodyBytes)) log.Printf("Request body length: %d bytes", len(bodyBytes)) - // Создаем новый reader из прочитанных байтов для парсинга - r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - // Опциональная проверка секрета webhook (если задан в переменных окружения) todoistWebhookSecret := getEnv("TODOIST_WEBHOOK_SECRET", "") log.Printf("Webhook secret check: configured=%v", todoistWebhookSecret != "") @@ -4986,9 +5033,9 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Webhook secret validated successfully") } - // Парсим webhook от Todoist + // Парсим webhook от Todoist из уже прочитанных байтов var webhook TodoistWebhook - if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil { + if err := json.Unmarshal(bodyBytes, &webhook); err != nil { log.Printf("Error decoding Todoist webhook: %v", err) log.Printf("Failed to parse body as JSON: %s", string(bodyBytes)) w.Header().Set("Content-Type", "application/json") @@ -5015,7 +5062,9 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) { if webhook.EventName != "item:completed" { log.Printf("Received Todoist event '%s', ignoring (only processing 'item:completed')", webhook.EventName) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": true, "message": "Event ignored", "event": webhook.EventName, }) @@ -5097,7 +5146,9 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Todoist webhook: no nodes found in message, ignoring (not saving to database and not sending to Telegram)") log.Printf("=== Todoist Webhook Request Ignored (No Nodes) ===") w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ + "ok": true, "message": "Message ignored (no nodes found)", "ignored": true, }) diff --git a/play-life-web/package.json b/play-life-web/package.json index 3f15c59..29c0501 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "2.0.4", + "version": "2.0.5", "type": "module", "scripts": { "dev": "vite",