Files
go-telegram-bot/main.go
2024-10-13 01:36:56 +02:00

286 lines
7.4 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
"time"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/joho/godotenv"
"github.com/liushuangls/go-anthropic/v2"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Message represents the structure for storing messages in the database
type Message struct {
gorm.Model
ChatID int64
UserID int64
Username string
UserRole string // New field
Text string
Timestamp time.Time
}
// Bot wraps the Telegram bot, database connection, and Anthropic client
type Bot struct {
tgBot *bot.Bot
db *gorm.DB
anthropicClient *anthropic.Client
}
type User struct {
gorm.Model
TelegramID int64 `gorm:"uniqueIndex"`
Username string
Role string
}
func main() {
// Initialize logger to write to both console and file
logFile, err := os.OpenFile("bot.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err)
os.Exit(1)
}
defer logFile.Close()
// Create a multi-writer to write to both stdout and the log file
mw := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(mw)
// Load environment variables
if err := godotenv.Load(); err != nil {
log.Printf("Error loading .env file: %v", err)
}
// Check for required environment variables
requiredEnvVars := []string{"TELEGRAM_BOT_TOKEN", "ANTHROPIC_API_KEY"}
for _, envVar := range requiredEnvVars {
if os.Getenv(envVar) == "" {
log.Fatalf("%s environment variable is not set", envVar)
}
}
// Initialize database
db, err := initDB()
if err != nil {
log.Fatalf("Error initializing database: %v", err)
}
// Initialize Anthropic client
anthropicClient := anthropic.NewClient(os.Getenv("ANTHROPIC_API_KEY"))
// Create Bot instance
b := &Bot{
db: db,
anthropicClient: anthropicClient,
}
// Initialize Telegram bot with the handler
tgBot, err := initTelegramBot(b.handleUpdate)
if err != nil {
log.Fatalf("Error initializing Telegram bot: %v", err)
}
b.tgBot = tgBot
// Set up context with cancellation
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// Start the bot
log.Println("Starting bot...")
b.tgBot.Start(ctx)
}
func initDB() (*gorm.DB, error) {
// Use the same logger for GORM
newLogger := logger.New(
log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: false,
},
)
// Initialize GORM with SQLite
db, err := gorm.Open(sqlite.Open("bot.db"), &gorm.Config{
Logger: newLogger,
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Auto-migrate the schema
err = db.AutoMigrate(&Message{}, &User{})
if err != nil {
return nil, fmt.Errorf("failed to migrate database schema: %w", err)
}
return db, nil
}
func initTelegramBot(handler bot.HandlerFunc) (*bot.Bot, error) {
// Load .env file
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
// Get bot token from environment variable
token := os.Getenv("TELEGRAM_BOT_TOKEN")
if token == "" {
return nil, fmt.Errorf("TELEGRAM_BOT_TOKEN environment variable is not set")
}
// Create new bot instance with the handler
b, err := bot.New(token, bot.WithDefaultHandler(handler))
if err != nil {
return nil, fmt.Errorf("failed to create bot: %w", err)
}
log.Println("Telegram bot initialized successfully")
return b, nil
}
func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
if update.Message == nil {
return // Ignore non-message updates
}
chatID := update.Message.Chat.ID
userID := update.Message.From.ID
username := update.Message.From.Username
text := update.Message.Text
// Check if user exists, if not create a new user with default role
var user User
if err := b.db.Where("telegram_id = ?", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
user = User{
TelegramID: userID,
Username: username,
Role: "user", // Default role
}
b.db.Create(&user)
} else {
log.Printf("Error checking user: %v", err)
return
}
}
// Prepare response using Anthropic
var response string
var err error
isNewChat := b.isNewChat(chatID)
if b.isAdminOrOwner(userID) {
response, err = b.getAnthropicResponse(ctx, text, isNewChat)
} else {
response, err = b.getModeratedAnthropicResponse(ctx, text, isNewChat)
}
if err != nil {
log.Printf("Error getting Anthropic response: %v", err)
response = "I'm sorry, I'm having trouble processing your request right now."
}
// Store message in database
message := Message{
ChatID: chatID,
UserID: userID,
Username: username,
UserRole: user.Role,
Text: text,
Timestamp: time.Now(),
}
if err := b.db.Create(&message).Error; err != nil {
log.Printf("Error storing message: %v", err)
}
// Send response
_, err = b.tgBot.SendMessage(ctx, &bot.SendMessageParams{
ChatID: chatID,
Text: response,
})
if err != nil {
log.Printf("Error sending message: %v", err)
}
}
// isNewChat checks if this is a new chat for the user
func (b *Bot) isNewChat(chatID int64) bool {
var count int64
b.db.Model(&Message{}).Where("chat_id = ?", chatID).Count(&count)
return count == 0
}
func (b *Bot) isAdminOrOwner(userID int64) bool {
var user User
if err := b.db.Where("telegram_id = ?", userID).First(&user).Error; err != nil {
return false
}
return user.Role == "admin" || user.Role == "owner"
}
func (b *Bot) getAnthropicResponse(ctx context.Context, userMessage string, isNewChat bool) (string, error) {
var systemMessage string
if isNewChat {
systemMessage = "You are a helpful AI assistant. Greet the user and respond to their message."
} else {
systemMessage = "You are a helpful AI assistant. Respond to the user's message."
}
resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{
Model: anthropic.ModelClaudeInstant1Dot2,
Messages: []anthropic.Message{
anthropic.NewUserTextMessage(systemMessage),
anthropic.NewUserTextMessage(userMessage),
},
MaxTokens: 1000,
})
if err != nil {
return "", fmt.Errorf("error creating Anthropic message: %w", err)
}
if len(resp.Content) == 0 || resp.Content[0].Type != anthropic.MessagesContentTypeText {
return "", fmt.Errorf("unexpected response format from Anthropic")
}
return resp.Content[0].GetText(), nil
}
func (b *Bot) getModeratedAnthropicResponse(ctx context.Context, userMessage string, isNewChat bool) (string, error) {
var systemMessage string
if isNewChat {
systemMessage = "You are a helpful AI assistant. Greet the user and respond to their message. Avoid discussing sensitive topics or providing harmful information."
} else {
systemMessage = "You are a helpful AI assistant. Respond to the user's message while avoiding sensitive topics or harmful information."
}
resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{
Model: anthropic.ModelClaudeInstant1Dot2,
Messages: []anthropic.Message{
anthropic.NewUserTextMessage(systemMessage),
anthropic.NewUserTextMessage(userMessage),
},
MaxTokens: 1000,
})
if err != nil {
return "", fmt.Errorf("error creating Anthropic message: %w", err)
}
if len(resp.Content) == 0 || resp.Content[0].Type != anthropic.MessagesContentTypeText {
return "", fmt.Errorf("unexpected response format from Anthropic")
}
return resp.Content[0].GetText(), nil
}