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"
)
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,

56
bot.go Normal file → Executable file
View File

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

0
clock.go Normal file → Executable file
View File

41
config.go Normal file → Executable file
View File

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

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

@@ -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."
}
}

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
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
)

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.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=

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) {
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
}

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 {
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 {

0
rate_limiter.go Normal file → Executable file
View File

0
rate_limiter_test.go Normal file → Executable file
View File