From e5532df7f9560600f559f0d56273f6840d8b46e7 Mon Sep 17 00:00:00 2001 From: HugeFrog24 <62775760+HugeFrog24@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:52:41 +0200 Subject: [PATCH] Handle business messages --- .gitignore | 0 anthropic.go | 13 ++--- bot.go | 56 +++++++++++++++---- clock.go | 0 config.go | 41 ++++++++++---- config/default.json | 4 +- database.go | 0 go.mod | 6 +-- go.sum | 12 ++--- handlers.go | 125 +++++++++++++++++++++++++++++++++++++------ main.go | 0 models.go | 23 ++++---- rate_limiter.go | 0 rate_limiter_test.go | 0 14 files changed, 219 insertions(+), 61 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 anthropic.go mode change 100644 => 100755 bot.go mode change 100644 => 100755 clock.go mode change 100644 => 100755 config.go mode change 100644 => 100755 config/default.json mode change 100644 => 100755 database.go mode change 100644 => 100755 go.mod mode change 100644 => 100755 go.sum mode change 100644 => 100755 handlers.go mode change 100644 => 100755 main.go mode change 100644 => 100755 models.go mode change 100644 => 100755 rate_limiter.go mode change 100644 => 100755 rate_limiter_test.go diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/anthropic.go b/anthropic.go old mode 100644 new mode 100755 index 84869ba..82e1021 --- a/anthropic.go +++ b/anthropic.go @@ -7,7 +7,7 @@ import ( "github.com/liushuangls/go-anthropic/v2" ) -func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Message, isNewChat, isAdminOrOwner bool) (string, error) { +func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Message, isNewChat, isAdminOrOwner, isEmojiOnly bool) (string, error) { // Use prompts from config var systemMessage string if isNewChat { @@ -23,6 +23,10 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Mes systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"] } + if isEmojiOnly { + systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"] + } + // Ensure the roles are correct for i := range messages { switch messages[i].Role { @@ -36,13 +40,10 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Mes } } - model := anthropic.ModelClaude3Dot5Sonnet20240620 - if !isAdminOrOwner { - model = anthropic.ModelClaudeInstant1Dot2 - } + model := anthropic.Model(b.config.Model) resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{ - Model: model, + Model: model, // Now `model` is of type anthropic.Model Messages: messages, System: systemMessage, MaxTokens: 1000, diff --git a/bot.go b/bot.go old mode 100644 new mode 100755 index 849204a..64bb25a --- a/bot.go +++ b/bot.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "strings" "sync" "time" @@ -162,10 +163,17 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message if !msg.IsUser { role = anthropic.RoleAssistant } + + textContent := strings.TrimSpace(msg.Text) + if textContent == "" { + // Skip empty messages + continue + } + contextMessages = append(contextMessages, anthropic.Message{ Role: role, Content: []anthropic.MessageContent{ - anthropic.NewTextMessageContent(msg.Text), + anthropic.NewTextMessageContent(textContent), }, }) } @@ -196,27 +204,37 @@ func initTelegramBot(token string, handleUpdate func(ctx context.Context, tgBot } // sendResponse sends a message to the specified chat. -func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string) { - _, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{ +// Returns an error if sending the message fails. +func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string, businessConnectionID string) error { + params := &bot.SendMessageParams{ ChatID: chatID, Text: text, - }) - if err != nil { - log.Printf("[%s] [ERROR] Error sending message: %v", b.config.ID, err) } + + if businessConnectionID != "" { + params.BusinessConnectionID = businessConnectionID + } + + _, err := b.tgBot.SendMessage(ctx, params) + if err != nil { + log.Printf("[%s] [ERROR] Error sending message to chat %d with BusinessConnectionID %s: %v", + b.config.ID, chatID, businessConnectionID, err) + return err + } + return nil } // sendStats sends the bot statistics to the specified chat. -func (b *Bot) sendStats(ctx context.Context, chatID int64) { +func (b *Bot) sendStats(ctx context.Context, chatID int64, businessConnectionID string) { totalUsers, totalMessages, err := b.getStats() if err != nil { fmt.Printf("Error fetching stats: %v\n", err) - b.sendResponse(ctx, chatID, "Sorry, I couldn't retrieve the stats at this time.") + b.sendResponse(ctx, chatID, "Sorry, I couldn't retrieve the stats at this time.", businessConnectionID) return } statsMessage := fmt.Sprintf("📊 **Bot Statistics:**\n\n- Total Users: %d\n- Total Messages: %d", totalUsers, totalMessages) - b.sendResponse(ctx, chatID, statsMessage) + b.sendResponse(ctx, chatID, statsMessage, businessConnectionID) } // getStats retrieves the total number of users and messages from the database. @@ -233,3 +251,23 @@ func (b *Bot) getStats() (int64, int64, error) { return totalUsers, totalMessages, nil } + +// isOnlyEmojis checks if the string consists solely of emojis. +func isOnlyEmojis(s string) bool { + for _, r := range s { + if !isEmoji(r) { + return false + } + } + return true +} + +// isEmoji determines if a rune is an emoji. +// This is a simplistic check and can be expanded based on requirements. +func isEmoji(r rune) bool { + return (r >= 0x1F600 && r <= 0x1F64F) || // Emoticons + (r >= 0x1F300 && r <= 0x1F5FF) || // Misc Symbols and Pictographs + (r >= 0x1F680 && r <= 0x1F6FF) || // Transport and Map + (r >= 0x2600 && r <= 0x26FF) || // Misc symbols + (r >= 0x2700 && r <= 0x27BF) // Dingbats +} diff --git a/clock.go b/clock.go old mode 100644 new mode 100755 diff --git a/config.go b/config.go old mode 100644 new mode 100755 index 355cdf2..c02496d --- a/config.go +++ b/config.go @@ -5,16 +5,37 @@ import ( "fmt" "os" "path/filepath" + + "github.com/liushuangls/go-anthropic/v2" ) type BotConfig struct { - ID string `json:"id"` // Unique identifier for the bot + ID string `json:"id"` // Unique identifier for the bot + TelegramToken string `json:"telegram_token"` // Telegram Bot Token MemorySize int `json:"memory_size"` MessagePerHour int `json:"messages_per_hour"` MessagePerDay int `json:"messages_per_day"` TempBanDuration string `json:"temp_ban_duration"` + Model anthropic.Model `json:"model"` // Changed from string to anthropic.Model SystemPrompts map[string]string `json:"system_prompts"` - TelegramToken string `json:"telegram_token"` // Telegram Bot Token +} + +// Custom unmarshalling to handle anthropic.Model +func (c *BotConfig) UnmarshalJSON(data []byte) error { + type Alias BotConfig + aux := &struct { + Model string `json:"model"` + *Alias + }{ + Alias: (*Alias)(c), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + c.Model = anthropic.Model(aux.Model) + return nil } func loadAllConfigs(dir string) ([]BotConfig, error) { @@ -57,6 +78,11 @@ func loadAllConfigs(dir string) ([]BotConfig, error) { } tokens[config.TelegramToken] = true + // Validate Model + if config.Model == "" { + return nil, fmt.Errorf("config %s is missing 'model' field", configPath) + } + configs = append(configs, config) } } @@ -77,14 +103,6 @@ func loadConfig(filename string) (BotConfig, error) { return config, fmt.Errorf("failed to decode JSON from %s: %w", filename, err) } - // Optionally override telegram_token with environment variable if set - // Uncomment the following lines if you choose to use environment variables for tokens - /* - if envToken := os.Getenv(fmt.Sprintf("TELEGRAM_TOKEN_%s", config.ID)); envToken != "" { - config.TelegramToken = envToken - } - */ - return config, nil } @@ -100,5 +118,8 @@ func (c *BotConfig) Reload(filename string) error { return fmt.Errorf("failed to decode JSON from %s: %w", filename, err) } + // Ensure the Model is correctly casted + c.Model = anthropic.Model(c.Model) + return nil } diff --git a/config/default.json b/config/default.json old mode 100644 new mode 100755 index e457ee7..b9d0708 --- a/config/default.json +++ b/config/default.json @@ -5,10 +5,12 @@ "messages_per_hour": 20, "messages_per_day": 100, "temp_ban_duration": "24h", + "model": "claude-3-5-sonnet-20240620", "system_prompts": { "default": "You are a helpful assistant.", "custom_instructions": "Please follow these guidelines:\n- Your name is Atom.\n- If a user asks about buying apples, inform them that we don't sell apples.\n- When asked for a joke, tell a clean, family-friendly joke about programming or technology.\n- If someone inquires about our services, explain that we offer AI-powered chatbot solutions.\n- For any questions about pricing, direct users to contact our sales team at sales@example.com.\n- If asked about your capabilities, be honest about what you can and cannot do.\nAlways maintain a friendly and professional tone.", "continue_conversation": "Continuing our conversation. Remember previous context if relevant.", - "avoid_sensitive": "Avoid discussing sensitive topics or providing harmful information." + "avoid_sensitive": "Avoid discussing sensitive topics or providing harmful information.", + "respond_with_emojis": "Since the user sent only emojis, respond using emojis only." } } \ No newline at end of file diff --git a/database.go b/database.go old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index c485318..8ee2d93 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/HugeFrog24/thatsky-telegram-bot go 1.23.2 require ( - github.com/go-telegram/bot v1.8.4 + github.com/go-telegram/bot v1.9.0 github.com/joho/godotenv v1.5.1 github.com/liushuangls/go-anthropic/v2 v2.8.1 golang.org/x/time v0.7.0 @@ -14,6 +14,6 @@ require ( require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum old mode 100644 new mode 100755 index bb232af..f021fe8 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/go-telegram/bot v1.8.4 h1:7viEUESakK29aiCumq6ui5jTPqJLLDeFubTsQzE07Kg= -github.com/go-telegram/bot v1.8.4/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-telegram/bot v1.9.0 h1:z9g0Fgk9B7G/xoVMqji30hpJPlr3Dz3aVW2nzSGfPuI= +github.com/go-telegram/bot v1.9.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -8,10 +8,10 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/liushuangls/go-anthropic/v2 v2.8.1 h1:pxFl88IgkG7e8Z1XwOYu48LcmEN0+6UdO58HF9altw0= github.com/liushuangls/go-anthropic/v2 v2.8.1/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= diff --git a/handlers.go b/handlers.go old mode 100644 new mode 100755 index cdd5a1c..52b1ad9 --- a/handlers.go +++ b/handlers.go @@ -11,35 +11,63 @@ import ( ) func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.Update) { - if update.Message == nil { + var message *models.Message + + if update.Message != nil { + message = update.Message + } else if update.BusinessMessage != nil { + message = update.BusinessMessage + } else { + // No message to process return } - chatID := update.Message.Chat.ID - userID := update.Message.From.ID + chatID := message.Chat.ID + userID := message.From.ID + + // Extract businessConnectionID if available + var businessConnectionID string + if update.BusinessConnection != nil { + businessConnectionID = update.BusinessConnection.ID + } else if message.BusinessConnectionID != "" { + businessConnectionID = message.BusinessConnectionID + } // Check if the message is a command - if update.Message.Entities != nil { - for _, entity := range update.Message.Entities { + if message.Entities != nil { + for _, entity := range message.Entities { if entity.Type == "bot_command" { - command := strings.TrimSpace(update.Message.Text[entity.Offset : entity.Offset+entity.Length]) + command := strings.TrimSpace(message.Text[entity.Offset : entity.Offset+entity.Length]) switch command { case "/stats": - b.sendStats(ctx, chatID) + b.sendStats(ctx, chatID, businessConnectionID) return } } } } - // Existing rate limit and message handling - if !b.checkRateLimits(userID) { - b.sendRateLimitExceededMessage(ctx, chatID) + // Check if the message contains a sticker + if message.Sticker != nil { + b.handleStickerMessage(ctx, chatID, userID, message, businessConnectionID) return } - username := update.Message.From.Username - text := update.Message.Text + // Existing rate limit and message handling + if !b.checkRateLimits(userID) { + b.sendRateLimitExceededMessage(ctx, chatID, businessConnectionID) + return + } + + username := message.From.Username + text := message.Text + + // Proceed only if the message contains text + if text == "" { + // Optionally, handle other message types or ignore + log.Printf("Received a non-text message from user %d in chat %d", userID, chatID) + return + } user, err := b.getOrCreateUser(userID, username) if err != nil { @@ -56,19 +84,84 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U contextMessages := b.prepareContextMessages(chatMemory) - response, err := b.getAnthropicResponse(ctx, contextMessages, b.isNewChat(chatID), b.isAdminOrOwner(userID)) + isEmojiOnly := isOnlyEmojis(text) // Ensure you have this variable defined + response, err := b.getAnthropicResponse(ctx, contextMessages, b.isNewChat(chatID), b.isAdminOrOwner(userID), isEmojiOnly) if err != nil { log.Printf("Error getting Anthropic response: %v", err) response = "I'm sorry, I'm having trouble processing your request right now." } - b.sendResponse(ctx, chatID, response) + b.sendResponse(ctx, chatID, response, businessConnectionID) assistantMessage := b.createMessage(chatID, 0, "", string(anthropic.RoleAssistant), response, false) b.storeMessage(assistantMessage) b.addMessageToChatMemory(chatMemory, assistantMessage) } -func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64) { - b.sendResponse(ctx, chatID, "Rate limit exceeded. Please try again later.") +func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, businessConnectionID string) { + b.sendResponse(ctx, chatID, "Rate limit exceeded. Please try again later.", businessConnectionID) +} + +func (b *Bot) handleStickerMessage(ctx context.Context, chatID, userID int64, message *models.Message, businessConnectionID string) { + username := message.From.Username + + // Create and store the sticker message + userMessage := b.createMessage(chatID, userID, username, "user", "Sent a sticker.", true) + userMessage.StickerFileID = message.Sticker.FileID + + // Safely store the Thumbnail's FileID if available + if message.Sticker.Thumbnail != nil { + userMessage.StickerPNGFile = message.Sticker.Thumbnail.FileID + } + + b.storeMessage(userMessage) + + // Update chat memory + chatMemory := b.getOrCreateChatMemory(chatID) + b.addMessageToChatMemory(chatMemory, userMessage) + + // Generate AI response about the sticker + response, err := b.generateStickerResponse(ctx, userMessage) + if err != nil { + log.Printf("Error generating sticker response: %v", err) + // Provide a fallback dynamic response based on sticker type + if message.Sticker.IsAnimated { + response = "Wow, that's a cool animated sticker!" + } else if message.Sticker.IsVideo { + response = "Interesting video sticker!" + } else { + response = "That's a cool sticker!" + } + } + + b.sendResponse(ctx, chatID, response, businessConnectionID) + + assistantMessage := b.createMessage(chatID, 0, "", string(anthropic.RoleAssistant), response, false) + b.storeMessage(assistantMessage) + b.addMessageToChatMemory(chatMemory, assistantMessage) +} + +func (b *Bot) generateStickerResponse(ctx context.Context, message Message) (string, error) { + // Example: Use the sticker type to generate a response + if message.StickerFileID != "" { + // Prepare context with information about the sticker + contextMessages := []anthropic.Message{ + { + Role: anthropic.RoleUser, + Content: []anthropic.MessageContent{ + anthropic.NewTextMessageContent("User sent a sticker."), + }, + }, + } + + // Since this is a sticker message, isEmojiOnly is false + response, err := b.getAnthropicResponse(ctx, contextMessages, false, false, false) + if err != nil { + return "", err + } + + return response, nil + } + + return "Hmm, that's interesting!", nil } diff --git a/main.go b/main.go old mode 100644 new mode 100755 diff --git a/models.go b/models.go old mode 100644 new mode 100755 index a00f860..cec57fe --- a/models.go +++ b/models.go @@ -28,19 +28,22 @@ type ConfigModel struct { type Message struct { gorm.Model - BotID uint - ChatID int64 - UserID int64 - Username string - UserRole string - Text string - Timestamp time.Time - IsUser bool + BotID uint + ChatID int64 + UserID int64 + Username string + UserRole string + Text string + StickerFileID string `json:"sticker_file_id,omitempty"` // New field to store Sticker File ID + StickerPNGFile string `json:"sticker_png_file,omitempty"` // Optionally store PNG file ID if needed + Timestamp time.Time + IsUser bool } type ChatMemory struct { - Messages []Message - Size int + Messages []Message + Size int + BusinessConnectionID string // New field to store the business connection ID } type Role struct { diff --git a/rate_limiter.go b/rate_limiter.go old mode 100644 new mode 100755 diff --git a/rate_limiter_test.go b/rate_limiter_test.go old mode 100644 new mode 100755