mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-08-28 22:11:38 +00:00
173 lines
4.5 KiB
Go
173 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/anthropics/anthropic-sdk-go"
|
|
)
|
|
|
|
const fileNotFoundPrefix = "File not found: "
|
|
|
|
func formatUploadFilename(botID uint, chatID int64, tgMessageID int, ext string) string {
|
|
return fmt.Sprintf("tg-%d-%d-%d.%s", botID, chatID, tgMessageID, ext)
|
|
}
|
|
|
|
func (b *Bot) uploadImageToAnthropic(ctx context.Context, data []byte, filename, contentType string) (string, error) {
|
|
resp, err := b.anthropicClient.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{
|
|
File: anthropic.File(bytes.NewReader(data), filename, contentType),
|
|
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14},
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("anthropic files upload: %w", err)
|
|
}
|
|
return resp.ID, nil
|
|
}
|
|
|
|
func (b *Bot) deleteFileFromAnthropic(ctx context.Context, fileID string) error {
|
|
_, err := b.anthropicClient.Beta.Files.Delete(ctx, fileID, anthropic.BetaFileDeleteParams{
|
|
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14},
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var apiErr *anthropic.Error
|
|
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("anthropic files delete %s: %w", fileID, err)
|
|
}
|
|
|
|
func (b *Bot) compensatingDelete(ctx context.Context, fileIDs []string) {
|
|
for _, fid := range fileIDs {
|
|
if err := b.deleteFileFromAnthropic(ctx, fid); err != nil {
|
|
ErrorLogger.Printf("[%s] compensating delete for %s: %v", b.config.ID, fid, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func extractMissingFileID(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
var apiErr *anthropic.Error
|
|
if !errors.As(err, &apiErr) {
|
|
return ""
|
|
}
|
|
if apiErr.StatusCode != http.StatusNotFound {
|
|
return ""
|
|
}
|
|
return parseMissingFileIDFromBody(apiErr.RawJSON())
|
|
}
|
|
|
|
func parseMissingFileIDFromBody(raw string) string {
|
|
idx := strings.Index(raw, fileNotFoundPrefix)
|
|
if idx == -1 {
|
|
return ""
|
|
}
|
|
rest := raw[idx+len(fileNotFoundPrefix):]
|
|
end := strings.IndexFunc(rest, func(r rune) bool {
|
|
return (r < 'a' || r > 'z') &&
|
|
(r < 'A' || r > 'Z') &&
|
|
(r < '0' || r > '9') &&
|
|
r != '_'
|
|
})
|
|
if end == -1 {
|
|
return rest
|
|
}
|
|
return rest[:end]
|
|
}
|
|
|
|
func (b *Bot) hardDeleteScope(ctx context.Context, query string, args ...interface{}) error {
|
|
var rows []Message
|
|
if err := b.db.Unscoped().Where(query, args...).Find(&rows).Error; err != nil {
|
|
return fmt.Errorf("scan rows: %w", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
if err := b.db.Where(query, args...).Delete(&Message{}).Error; err != nil {
|
|
return fmt.Errorf("soft delete: %w", err)
|
|
}
|
|
|
|
hardDeletable := make([]uint, 0, len(rows))
|
|
for _, row := range rows {
|
|
if b.deleteRowFiles(ctx, row) {
|
|
hardDeletable = append(hardDeletable, row.ID)
|
|
}
|
|
}
|
|
if len(hardDeletable) == 0 {
|
|
return nil
|
|
}
|
|
if err := b.db.Unscoped().Where("id IN ?", hardDeletable).Delete(&Message{}).Error; err != nil {
|
|
return fmt.Errorf("hard delete: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bot) deleteRowFiles(ctx context.Context, row Message) bool {
|
|
if len(row.ImageFileIDs) == 0 {
|
|
return true
|
|
}
|
|
allOk := true
|
|
for _, fid := range row.ImageFileIDs {
|
|
if err := b.deleteFileFromAnthropic(ctx, fid); err != nil {
|
|
ErrorLogger.Printf("[%s] anthropic delete %s (row %d): %v", b.config.ID, fid, row.ID, err)
|
|
allOk = false
|
|
}
|
|
}
|
|
return allOk
|
|
}
|
|
|
|
func stripDeadFileIDs(src []string, deadSet map[string]struct{}) (survivors []string, dirty bool) {
|
|
survivors = make([]string, 0, len(src))
|
|
for _, fid := range src {
|
|
if _, dead := deadSet[fid]; dead {
|
|
dirty = true
|
|
continue
|
|
}
|
|
survivors = append(survivors, fid)
|
|
}
|
|
return survivors, dirty
|
|
}
|
|
|
|
func (b *Bot) markFilesPendingCleanup(ctx context.Context, chatID int64, deadFileIDs []string) (int, error) {
|
|
if len(deadFileIDs) == 0 {
|
|
return 0, nil
|
|
}
|
|
deadSet := make(map[string]struct{}, len(deadFileIDs))
|
|
for _, id := range deadFileIDs {
|
|
deadSet[id] = struct{}{}
|
|
}
|
|
var rows []Message
|
|
if err := b.db.WithContext(ctx).
|
|
Where("bot_id = ? AND chat_id = ? AND image_file_ids IS NOT NULL", b.botID, chatID).
|
|
Find(&rows).Error; err != nil {
|
|
return 0, fmt.Errorf("scan rows for cleanup: %w", err)
|
|
}
|
|
now := time.Now()
|
|
updated := 0
|
|
for _, row := range rows {
|
|
survivors, dirty := stripDeadFileIDs(row.ImageFileIDs, deadSet)
|
|
if !dirty {
|
|
continue
|
|
}
|
|
if len(survivors) == 0 {
|
|
row.ImageFileIDs = nil
|
|
row.FilesCleanedAt = &now
|
|
} else {
|
|
row.ImageFileIDs = survivors
|
|
}
|
|
if err := b.db.WithContext(ctx).Save(&row).Error; err != nil {
|
|
return updated, fmt.Errorf("update row %d: %w", row.ID, err)
|
|
}
|
|
updated++
|
|
}
|
|
return updated, nil
|
|
}
|