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

@@ -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,
})