v2.0.5: Fix transaction errors and webhook parsing
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 40s

- 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
This commit is contained in:
poignatov
2026-01-01 18:57:30 +03:00
parent edc29fbd97
commit 6f77f0643c
3 changed files with 86 additions and 35 deletions

View File

@@ -1 +1 @@
2.0.4 2.0.5

View File

@@ -3280,15 +3280,19 @@ func setupTelegramWebhook(botToken, webhookURL string) error {
} }
defer resp.Body.Close() 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)) log.Printf("Telegram API response: status=%d, body=%s", resp.StatusCode, string(bodyBytes))
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telegram API returned status %d: %s", resp.StatusCode, string(bodyBytes)) return fmt.Errorf("telegram API returned status %d: %s", resp.StatusCode, string(bodyBytes))
} }
// Декодируем из уже прочитанных байтов
var result map[string]interface{} 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) 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 { for projectName := range projectNames {
if userID != nil { if userID != nil {
_, err := tx.Exec(` // Используем более универсальный подход: проверяем существование и вставляем/обновляем
INSERT INTO projects (name, deleted, user_id) var existingID int
VALUES ($1, FALSE, $2) err := tx.QueryRow(`
ON CONFLICT ON CONSTRAINT unique_project_name DO UPDATE SELECT id FROM projects
SET name = EXCLUDED.name, deleted = FALSE WHERE name = $1 AND user_id = $2 AND deleted = FALSE
`, projectName, *userID) `, projectName, *userID).Scan(&existingID)
if err != nil {
// Try without user constraint for backwards compatibility if err == sql.ErrNoRows {
// Проект не существует, создаем новый
_, err = tx.Exec(` _, err = tx.Exec(`
INSERT INTO projects (name, deleted, user_id) INSERT INTO projects (name, deleted, user_id)
VALUES ($1, FALSE, $2) 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) `, projectName, *userID)
if err != nil { 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 { } else {
_, err := tx.Exec(` // Для случая без user_id (legacy)
INSERT INTO projects (name, deleted) var existingID int
VALUES ($1, FALSE) err := tx.QueryRow(`
ON CONFLICT (name) DO UPDATE SELECT id FROM projects
SET name = EXCLUDED.name, deleted = FALSE WHERE name = $1 AND deleted = FALSE
`, projectName) `, projectName).Scan(&existingID)
if err != nil {
return fmt.Errorf("failed to upsert project %s: %w", projectName, err) 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 // 3. Вставляем nodes
for _, node := range nodes { for _, node := range nodes {
_, err := tx.Exec(` var projectID int
INSERT INTO nodes (project_id, entry_id, score) if userID != nil {
SELECT p.id, $1, $2 err = tx.QueryRow(`
FROM projects p SELECT id FROM projects
WHERE p.name = $3 AND p.deleted = FALSE WHERE name = $1 AND user_id = $2 AND deleted = FALSE
`, entryID, node.Score, node.Project) `, 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 { if err != nil {
return fmt.Errorf("failed to insert node for project %s: %w", node.Project, err) 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 (raw): %s", string(bodyBytes))
log.Printf("Request body length: %d bytes", len(bodyBytes)) log.Printf("Request body length: %d bytes", len(bodyBytes))
// Создаем новый reader из прочитанных байтов для парсинга
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// Опциональная проверка секрета webhook (если задан в переменных окружения) // Опциональная проверка секрета webhook (если задан в переменных окружения)
todoistWebhookSecret := getEnv("TODOIST_WEBHOOK_SECRET", "") todoistWebhookSecret := getEnv("TODOIST_WEBHOOK_SECRET", "")
log.Printf("Webhook secret check: configured=%v", todoistWebhookSecret != "") 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") log.Printf("Webhook secret validated successfully")
} }
// Парсим webhook от Todoist // Парсим webhook от Todoist из уже прочитанных байтов
var webhook TodoistWebhook 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("Error decoding Todoist webhook: %v", err)
log.Printf("Failed to parse body as JSON: %s", string(bodyBytes)) log.Printf("Failed to parse body as JSON: %s", string(bodyBytes))
w.Header().Set("Content-Type", "application/json") 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" { if webhook.EventName != "item:completed" {
log.Printf("Received Todoist event '%s', ignoring (only processing 'item:completed')", webhook.EventName) log.Printf("Received Todoist event '%s', ignoring (only processing 'item:completed')", webhook.EventName)
w.Header().Set("Content-Type", "application/json") 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", "message": "Event ignored",
"event": webhook.EventName, "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: no nodes found in message, ignoring (not saving to database and not sending to Telegram)")
log.Printf("=== Todoist Webhook Request Ignored (No Nodes) ===") log.Printf("=== Todoist Webhook Request Ignored (No Nodes) ===")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true,
"message": "Message ignored (no nodes found)", "message": "Message ignored (no nodes found)",
"ignored": true, "ignored": true,
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "2.0.4", "version": "2.0.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",