Handle business messages

This commit is contained in:
HugeFrog24
2024-10-20 15:52:41 +02:00
parent 0ab56448c7
commit e5532df7f9
14 changed files with 219 additions and 61 deletions

0
.gitignore vendored Normal file → Executable file
View File

13
anthropic.go Normal file → Executable file
View File

@@ -7,7 +7,7 @@ import (
"github.com/liushuangls/go-anthropic/v2" "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 // Use prompts from config
var systemMessage string var systemMessage string
if isNewChat { if isNewChat {
@@ -23,6 +23,10 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Mes
systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"] systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"]
} }
if isEmojiOnly {
systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"]
}
// Ensure the roles are correct // Ensure the roles are correct
for i := range messages { for i := range messages {
switch messages[i].Role { switch messages[i].Role {
@@ -36,13 +40,10 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Mes
} }
} }
model := anthropic.ModelClaude3Dot5Sonnet20240620 model := anthropic.Model(b.config.Model)
if !isAdminOrOwner {
model = anthropic.ModelClaudeInstant1Dot2
}
resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{ resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{
Model: model, Model: model, // Now `model` is of type anthropic.Model
Messages: messages, Messages: messages,
System: systemMessage, System: systemMessage,
MaxTokens: 1000, MaxTokens: 1000,

56
bot.go Normal file → Executable file
View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
@@ -162,10 +163,17 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message
if !msg.IsUser { if !msg.IsUser {
role = anthropic.RoleAssistant role = anthropic.RoleAssistant
} }
textContent := strings.TrimSpace(msg.Text)
if textContent == "" {
// Skip empty messages
continue
}
contextMessages = append(contextMessages, anthropic.Message{ contextMessages = append(contextMessages, anthropic.Message{
Role: role, Role: role,
Content: []anthropic.MessageContent{ 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. // sendResponse sends a message to the specified chat.
func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string) { // Returns an error if sending the message fails.
_, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{ func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string, businessConnectionID string) error {
params := &bot.SendMessageParams{
ChatID: chatID, ChatID: chatID,
Text: text, 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. // 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() totalUsers, totalMessages, err := b.getStats()
if err != nil { if err != nil {
fmt.Printf("Error fetching stats: %v\n", err) 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 return
} }
statsMessage := fmt.Sprintf("📊 **Bot Statistics:**\n\n- Total Users: %d\n- Total Messages: %d", totalUsers, totalMessages) 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. // 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 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
}

0
clock.go Normal file → Executable file
View File

41
config.go Normal file → Executable file
View File

@@ -5,16 +5,37 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"github.com/liushuangls/go-anthropic/v2"
) )
type BotConfig struct { 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"` MemorySize int `json:"memory_size"`
MessagePerHour int `json:"messages_per_hour"` MessagePerHour int `json:"messages_per_hour"`
MessagePerDay int `json:"messages_per_day"` MessagePerDay int `json:"messages_per_day"`
TempBanDuration string `json:"temp_ban_duration"` TempBanDuration string `json:"temp_ban_duration"`
Model anthropic.Model `json:"model"` // Changed from string to anthropic.Model
SystemPrompts map[string]string `json:"system_prompts"` 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) { func loadAllConfigs(dir string) ([]BotConfig, error) {
@@ -57,6 +78,11 @@ func loadAllConfigs(dir string) ([]BotConfig, error) {
} }
tokens[config.TelegramToken] = true tokens[config.TelegramToken] = true
// Validate Model
if config.Model == "" {
return nil, fmt.Errorf("config %s is missing 'model' field", configPath)
}
configs = append(configs, config) 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) 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 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) 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 return nil
} }

4
config/default.json Normal file → Executable file
View File

@@ -5,10 +5,12 @@
"messages_per_hour": 20, "messages_per_hour": 20,
"messages_per_day": 100, "messages_per_day": 100,
"temp_ban_duration": "24h", "temp_ban_duration": "24h",
"model": "claude-3-5-sonnet-20240620",
"system_prompts": { "system_prompts": {
"default": "You are a helpful assistant.", "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.", "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.", "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."
} }
} }

0
database.go Normal file → Executable file
View File

6
go.mod Normal file → Executable file
View File

@@ -3,7 +3,7 @@ module github.com/HugeFrog24/thatsky-telegram-bot
go 1.23.2 go 1.23.2
require ( 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/joho/godotenv v1.5.1
github.com/liushuangls/go-anthropic/v2 v2.8.1 github.com/liushuangls/go-anthropic/v2 v2.8.1
golang.org/x/time v0.7.0 golang.org/x/time v0.7.0
@@ -14,6 +14,6 @@ require (
require ( require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.19.0 // indirect
) )

12
go.sum Normal file → Executable file
View File

@@ -1,5 +1,5 @@
github.com/go-telegram/bot v1.8.4 h1:7viEUESakK29aiCumq6ui5jTPqJLLDeFubTsQzE07Kg= github.com/go-telegram/bot v1.9.0 h1:z9g0Fgk9B7G/xoVMqji30hpJPlr3Dz3aVW2nzSGfPuI=
github.com/go-telegram/bot v1.8.4/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 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/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 h1:pxFl88IgkG7e8Z1XwOYu48LcmEN0+6UdO58HF9altw0=
github.com/liushuangls/go-anthropic/v2 v2.8.1/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek= 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.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=

125
handlers.go Normal file → Executable file
View File

@@ -11,35 +11,63 @@ import (
) )
func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.Update) { 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 return
} }
chatID := update.Message.Chat.ID chatID := message.Chat.ID
userID := update.Message.From.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 // Check if the message is a command
if update.Message.Entities != nil { if message.Entities != nil {
for _, entity := range update.Message.Entities { for _, entity := range message.Entities {
if entity.Type == "bot_command" { 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 { switch command {
case "/stats": case "/stats":
b.sendStats(ctx, chatID) b.sendStats(ctx, chatID, businessConnectionID)
return return
} }
} }
} }
} }
// Existing rate limit and message handling // Check if the message contains a sticker
if !b.checkRateLimits(userID) { if message.Sticker != nil {
b.sendRateLimitExceededMessage(ctx, chatID) b.handleStickerMessage(ctx, chatID, userID, message, businessConnectionID)
return return
} }
username := update.Message.From.Username // Existing rate limit and message handling
text := update.Message.Text 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) user, err := b.getOrCreateUser(userID, username)
if err != nil { if err != nil {
@@ -56,19 +84,84 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
contextMessages := b.prepareContextMessages(chatMemory) 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 { if err != nil {
log.Printf("Error getting Anthropic response: %v", err) log.Printf("Error getting Anthropic response: %v", err)
response = "I'm sorry, I'm having trouble processing your request right now." 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) assistantMessage := b.createMessage(chatID, 0, "", string(anthropic.RoleAssistant), response, false)
b.storeMessage(assistantMessage) b.storeMessage(assistantMessage)
b.addMessageToChatMemory(chatMemory, assistantMessage) b.addMessageToChatMemory(chatMemory, assistantMessage)
} }
func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64) { func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, businessConnectionID string) {
b.sendResponse(ctx, chatID, "Rate limit exceeded. Please try again later.") 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
} }

0
main.go Normal file → Executable file
View File

23
models.go Normal file → Executable file
View File

@@ -28,19 +28,22 @@ type ConfigModel struct {
type Message struct { type Message struct {
gorm.Model gorm.Model
BotID uint BotID uint
ChatID int64 ChatID int64
UserID int64 UserID int64
Username string Username string
UserRole string UserRole string
Text string Text string
Timestamp time.Time StickerFileID string `json:"sticker_file_id,omitempty"` // New field to store Sticker File ID
IsUser bool StickerPNGFile string `json:"sticker_png_file,omitempty"` // Optionally store PNG file ID if needed
Timestamp time.Time
IsUser bool
} }
type ChatMemory struct { type ChatMemory struct {
Messages []Message Messages []Message
Size int Size int
BusinessConnectionID string // New field to store the business connection ID
} }
type Role struct { type Role struct {

0
rate_limiter.go Normal file → Executable file
View File

0
rate_limiter_test.go Normal file → Executable file
View File