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