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

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

121
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
}
}
}
}
// Check if the message contains a sticker
if message.Sticker != nil {
b.handleStickerMessage(ctx, chatID, userID, message, businessConnectionID)
return
}
// Existing rate limit and message handling
if !b.checkRateLimits(userID) {
b.sendRateLimitExceededMessage(ctx, chatID)
b.sendRateLimitExceededMessage(ctx, chatID, businessConnectionID)
return
}
username := update.Message.From.Username
text := update.Message.Text
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

3
models.go Normal file → Executable file
View File

@@ -34,6 +34,8 @@ type Message struct {
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
}
@@ -41,6 +43,7 @@ type Message struct {
type ChatMemory struct {
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