Compare commits

...

7 Commits

Author SHA1 Message Date
HugeFrog24 e1a9261699 Security/quality 2026-03-05 01:51:59 +01:00
HugeFrog24 6e2d2fce2f Precision and dep upgrade 2024-10-23 23:08:22 +02:00
HugeFrog24 37d6242c06 Added readme 2024-10-23 22:06:56 +02:00
HugeFrog24 d8d0da4704 Upgrade dependencies
Added tests, revised logging

Removed dependency on env file

Try reformatting unit file

Comments clarification

Added readme

Added readme
2024-10-23 22:06:56 +02:00
HugeFrog24 c8af457af1 MVP
md formatting doesnt work yet

Started implementing owner feature

Add .gitattributes to enforce LF line endings

Temporary commit before merge

Updated owner management

Updated json and gitignore

Proceed with role management

Again, CI

Fix some lint errors

Implemented screening

Per-bot API keys implemented

Use getRoleByName func

Fix unused imports

Upgrade actions

rm unused function

Upgrade action

Fix unaddressed errors
2024-10-23 22:06:55 +02:00
HugeFrog24 e5532df7f9 Handle business messages 2024-10-20 15:52:41 +02:00
HugeFrog24 0ab56448c7 Multibot finished 2024-10-13 16:41:03 +02:00
33 changed files with 3941 additions and 948 deletions
+14
View File
@@ -0,0 +1,14 @@
---
alwaysApply: true
always_on: true
trigger: always_on
applyTo: "**"
description: Snyk Security At Inception
---
# Project security best practices
- Always run snyk_code_scan tool for new first party code that is generated in a Snyk-supported language.
- If any security issues are found based on newly introduced or modified code or dependencies, attempt to fix the issues using the results context from Snyk.
- Rescan the code after fixing the issues to ensure that the issues were fixed and that there are no newly introduced issues.
- Repeat this process until no new issues are found.
+77
View File
@@ -0,0 +1,77 @@
# Git files
.git
.gitignore
.gitattributes
# Documentation
README.md
*.md
# Docker files
Dockerfile
docker-compose.yml
.dockerignore
# Environment files
.env
.env.*
# Log files
*.log
logs/
# Database files
*.db
*.sqlite
*.sqlite3
bot.db
# Config files (except default template)
config/*
!config/default.json
# Test files
*_test.go
test/
tests/
# Build artifacts
telegram-bot
*.exe
*.dll
*.so
*.dylib
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Go specific
vendor/
*.mod.backup
*.sum.backup
# Temporary files
tmp/
temp/
*.tmp
# Coverage files
*.out
coverage.html
# CI/CD files
.github/
.gitlab-ci.yml
.travis.yml
# Examples and documentation
examples/
docs/
+13
View File
@@ -0,0 +1,13 @@
# Enforce LF line endings for all files
* text eol=lf
# Specific file types that should always have LF line endings
*.go text eol=lf
*.json text eol=lf
*.sh text eol=lf
*.md text eol=lf
# Example: Binary files should not be modified
*.jpg binary
*.png binary
*.gif binary
+58
View File
@@ -0,0 +1,58 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
# Common setup job that other jobs can depend on
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26.0'
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: go mod tidy
# Lint job
lint:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: golangci/golangci-lint-action@v9
with:
version: v2.10
args: --timeout 5m
# Test job
test:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.26.0'
- run: go test ./... -v
# Security scan job
security:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: securego/gosec@master
with:
args: ./...
+9 -2
View File
@@ -1,11 +1,18 @@
# Local IDE config & user settings
.vscode/
# Go vendor directory # Go vendor directory
vendor/ vendor/
# Environment variables # Environment variables
.env .env
# Log file # Any log files
bot.log *.log
# Database file # Database file
bot.db bot.db
# All config files except for the default
config/*
!config/default.json
+3
View File
@@ -0,0 +1,3 @@
{
"mcpServers": {}
}
+55
View File
@@ -0,0 +1,55 @@
# Multi-stage build for Go Telegram Bot
# Build stage
FROM golang:1.26-alpine AS builder
# Install build dependencies including C compiler for CGO
RUN apk add --no-cache git ca-certificates tzdata gcc musl-dev
# Set working directory
WORKDIR /build
# Copy go mod files first for better caching
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o telegram-bot .
# Runtime stage
FROM alpine:latest
# Merged into a single RUN to minimise image layers (docker:S7031).
# Order matters: packages must be installed before adduser/addgroup,
# and the app directory must exist before chown runs.
RUN apk --no-cache add ca-certificates tzdata sqlite && \
addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup && \
mkdir -p /app/config /app/data /app/logs && \
chown -R appuser:appgroup /app
# Set working directory
WORKDIR /app
# Copy binary from builder stage
COPY --from=builder /build/telegram-bot /app/telegram-bot
# Copy default config as template
COPY --chown=appuser:appgroup config/default.json /app/config/
# Switch to non-root user
USER appuser
# Expose any ports if needed (not required for this bot)
# EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD pgrep telegram-bot || exit 1
# Run the application
CMD ["/app/telegram-bot"]
+147
View File
@@ -0,0 +1,147 @@
# Go Telegram Multibot
A scalable, multi-bot solution for Telegram using Go, GORM, and the Anthropic API.
## Design Considerations
- AI-powered
- Supports multiple bot profiles
- Uses SQLite for persistence
- Implements rate limiting and user management
- Modular architecture
- Comprehensive unit tests
## Usage
### Docker Deployment (Recommended)
1. Clone the repository:
```bash
git clone https://github.com/HugeFrog24/go-telegram-bot.git
cd go-telegram-bot
```
2. Copy the default config template and edit it:
```bash
cp config/default.json config/mybot.json
nano config/mybot.json
```
> [!IMPORTANT]
> Keep your config files secret and do not commit them to version control.
3. Create data directory and run:
```bash
mkdir -p data
docker-compose up -d
```
### Native Deployment
1. Install using `go get`:
```bash
go get -u github.com/HugeFrog24/go-telegram-bot
cd go-telegram-bot
```
2. Configure as above, then build:
```bash
go build -o telegram-bot
```
## Systemd Unit Setup
To enable the bot to start automatically on system boot and run in the background, set up a systemd unit.
1. Copy the systemd unit template and edit it:
```bash
sudo cp examples/systemd/telegram-bot.service /etc/systemd/system/telegram-bot.service
```
Edit the service file:
```bash
sudo nano /etc/systemd/system/telegram-bot.service
```
Adjust the following parameters:
- WorkingDirectory
- ExecStart
- User
2. Enable and start the service:
```bash
sudo systemctl daemon-reload
```
```bash
sudo systemctl enable telegram-bot
```
```bash
sudo systemctl start telegram-bot
```
3. Check the status:
```bash
sudo systemctl status telegram-bot
```
For more details on the systemd setup, refer to the [demo service file](examples/systemd/telegram-bot.service).
## Logs
### Docker
```bash
docker-compose logs -f telegram-bot
```
### Systemd
```bash
journalctl -u telegram-bot -f
```
## Commands
| Command | Access | Description |
| --------------------------------- | ----------- | ------------------------------------------------------------ |
| `/stats` | All users | Show global bot statistics (total users and messages) |
| `/stats user` | All users | Show your own message statistics |
| `/stats user <user_id>` | Admin/Owner | Show statistics for a specific user |
| `/whoami` | All users | Show your Telegram ID, username, and role |
| `/clear` | All users | Soft-delete your own chat history |
| `/clear <user_id>` | Admin/Owner | Soft-delete all messages for a user across every chat |
| `/clear <user_id> <chat_id>` | Admin/Owner | Soft-delete a user's messages in a specific chat |
| `/clear_hard` | All users | Permanently delete your own chat history |
| `/clear_hard <user_id>` | Admin/Owner | Permanently delete all messages for a user across every chat |
| `/clear_hard <user_id> <chat_id>` | Admin/Owner | Permanently delete a user's messages in a specific chat |
> **Note:** In private DMs each user's `chat_id` equals their `user_id`. The scoped `<chat_id>` form is mainly useful for group chat moderation.
## Testing
The GitHub actions workflow already runs tests on every commit:
> [![CI](https://github.com/HugeFrog24/go-telegram-bot/actions/workflows/go-ci.yaml/badge.svg?branch=main)](https://github.com/HugeFrog24/go-telegram-bot/actions/workflows/go-ci.yaml)
However, you can run the tests locally using:
```bash
go test -race -v ./...
```
## Storage
At the moment, a SQLite database (`./data/bot.db`) is used for persistent storage.
Remember to back it up regularly.
Future versions will support more robust storage backends.
+92 -13
View File
@@ -3,42 +3,121 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time"
"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, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int) (string, error) {
// Use prompts from config
var systemMessage string var systemMessage string
if isNewChat { if isNewChat {
systemMessage = "You are a helpful AI assistant." systemMessage = b.config.SystemPrompts["new_chat"]
} else { } else {
systemMessage = "Continue the conversation." systemMessage = b.config.SystemPrompts["continue_conversation"]
} }
// Combine default prompt with custom instructions
systemMessage = b.config.SystemPrompts["default"] + " " + b.config.SystemPrompts["custom_instructions"] + " " + systemMessage
// Handle username placeholder
usernameValue := username
if username == "" {
usernameValue = "unknown" // Use "unknown" when username is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue)
// Handle firstname placeholder
firstnameValue := firstName
if firstName == "" {
firstnameValue = "unknown" // Use "unknown" when first name is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue)
// Handle lastname placeholder
lastnameValue := lastName
if lastName == "" {
lastnameValue = "" // Empty string when last name is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue)
// Handle language code placeholder
langValue := languageCode
if languageCode == "" {
langValue = "en" // Default to English when language code is not available
}
systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue)
// Handle premium status
premiumStatus := "regular user"
if isPremium {
premiumStatus = "premium user"
}
systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
// Handle time awareness
timeObj := time.Unix(int64(messageTime), 0)
hour := timeObj.Hour()
var timeContext string
if hour >= 5 && hour < 12 {
timeContext = "morning"
} else if hour >= 12 && hour < 18 {
timeContext = "afternoon"
} else if hour >= 18 && hour < 22 {
timeContext = "evening"
} else {
timeContext = "night"
}
systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext)
if !isAdminOrOwner { if !isAdminOrOwner {
systemMessage += " Avoid discussing sensitive topics or providing harmful information." systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"]
}
if isEmojiOnly {
systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"]
}
// Debug logging
InfoLogger.Printf("Sending %d messages to Anthropic", len(messages))
for i, msg := range messages {
for _, content := range msg.Content {
if content.Type == anthropic.MessagesContentTypeText {
InfoLogger.Printf("Message %d: Role=%v, Text=%v", i, msg.Role, content.Text)
}
}
} }
// Ensure the roles are correct // Ensure the roles are correct
for i := range messages { for i := range messages {
if messages[i].Role == "user" { switch messages[i].Role {
case anthropic.RoleUser:
messages[i].Role = anthropic.RoleUser messages[i].Role = anthropic.RoleUser
} else if messages[i].Role == "assistant" { case anthropic.RoleAssistant:
messages[i].Role = anthropic.RoleAssistant messages[i].Role = anthropic.RoleAssistant
default:
// Default to 'user' if role is unrecognized
messages[i].Role = anthropic.RoleUser
} }
} }
model := anthropic.ModelClaude3Dot5Sonnet20240620 model := anthropic.Model(b.config.Model)
if !isAdminOrOwner {
model = anthropic.ModelClaudeInstant1Dot2
}
resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{ // Create the request
Model: model, request := anthropic.MessagesRequest{
Model: model, // Now `model` is of type anthropic.Model
Messages: messages, Messages: messages,
System: systemMessage, System: systemMessage,
MaxTokens: 1000, MaxTokens: 1000,
}) }
// Apply temperature if set in config
if b.config.Temperature != nil {
request.Temperature = b.config.Temperature
}
resp, err := b.anthropicClient.CreateMessages(ctx, request)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating Anthropic message: %w", err) return "", fmt.Errorf("error creating Anthropic message: %w", err)
} }
+197
View File
@@ -0,0 +1,197 @@
package main
import (
"fmt"
"strings"
"testing"
"time"
)
// TestLanguageCodeReplacement tests that language code is properly handled and replaced
func TestLanguageCodeReplacement(t *testing.T) {
// Test with provided language code
systemMessage := "User's language preference: '{language}'"
// Test with a specific language code
langValue := "fr"
result := strings.ReplaceAll(systemMessage, "{language}", langValue)
if !strings.Contains(result, "User's language preference: 'fr'") {
t.Errorf("Expected language code 'fr' to be replaced, got: %s", result)
}
// Test with empty language code (should default to "en")
langValue = ""
if langValue == "" {
langValue = "en" // Default to English when language code is not available
}
result = strings.ReplaceAll(systemMessage, "{language}", langValue)
if !strings.Contains(result, "User's language preference: 'en'") {
t.Errorf("Expected default language code 'en' to be used, got: %s", result)
}
}
// TestPremiumStatusReplacement tests that premium status is properly handled and replaced
func TestPremiumStatusReplacement(t *testing.T) {
systemMessage := "User is a {premium_status}"
// Test with premium user
isPremium := true
premiumStatus := "regular user"
if isPremium {
premiumStatus = "premium user"
}
result := strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
if !strings.Contains(result, "User is a premium user") {
t.Errorf("Expected premium status to be replaced with 'premium user', got: %s", result)
}
// Test with regular user
isPremium = false
premiumStatus = "regular user"
if isPremium {
premiumStatus = "premium user"
}
result = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
if !strings.Contains(result, "User is a regular user") {
t.Errorf("Expected premium status to be replaced with 'regular user', got: %s", result)
}
}
// TestTimeContextCalculation tests that time context is correctly calculated for different hours
func TestTimeContextCalculation(t *testing.T) {
// Test cases for different hours
testCases := []struct {
hour int
expected string
}{
{3, "night"}, // Night: hours < 5 or hours >= 22
{5, "morning"}, // Morning: 5 <= hours < 12
{12, "afternoon"}, // Afternoon: 12 <= hours < 18
{17, "afternoon"}, // Afternoon: 12 <= hours < 18
{18, "evening"}, // Evening: 18 <= hours < 22
{21, "evening"}, // Evening: 18 <= hours < 22
{22, "night"}, // Night: hours < 5 or hours >= 22
{23, "night"}, // Night: hours < 5 or hours >= 22
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Hour_%d", tc.hour), func(t *testing.T) {
// Create a timestamp for the specified hour
testTime := time.Date(2025, 5, 15, tc.hour, 0, 0, 0, time.UTC)
// Get the hour directly from the test time to ensure it's what we expect
actualHour := testTime.Hour()
if actualHour != tc.hour {
t.Fatalf("Test setup error: expected hour %d, got %d", tc.hour, actualHour)
}
// Calculate time context using the same logic as in anthropic.go
var timeContext string
if actualHour >= 5 && actualHour < 12 {
timeContext = "morning"
} else if actualHour >= 12 && actualHour < 18 {
timeContext = "afternoon"
} else if actualHour >= 18 && actualHour < 22 {
timeContext = "evening"
} else {
timeContext = "night"
}
// Check if the calculated time context matches the expected value
if timeContext != tc.expected {
t.Errorf("For hour %d: expected time context '%s', got '%s'",
actualHour, tc.expected, timeContext)
}
})
}
}
// TestSystemMessagePlaceholderReplacement tests that all placeholders are correctly replaced
func TestSystemMessagePlaceholderReplacement(t *testing.T) {
systemMessage := "The user you're talking to has username '{username}' and display name '{firstname} {lastname}'.\n" +
"User's language preference: '{language}'\n" +
"User is a {premium_status}\n" +
"It's currently {time_context} in your timezone"
// Set up test data
username := "testuser"
firstName := "Test"
lastName := "User"
isPremium := true
languageCode := "de"
// Create a timestamp for a specific hour (e.g., 14:00 = afternoon)
testTime := time.Date(2025, 5, 15, 14, 0, 0, 0, time.UTC)
messageTime := int(testTime.Unix())
// Handle username placeholder
usernameValue := username
if username == "" {
usernameValue = "unknown"
}
systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue)
// Handle firstname placeholder
firstnameValue := firstName
if firstName == "" {
firstnameValue = "unknown"
}
systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue)
// Handle lastname placeholder
lastnameValue := lastName
if lastName == "" {
lastnameValue = ""
}
systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue)
// Handle language code placeholder
langValue := languageCode
if languageCode == "" {
langValue = "en"
}
systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue)
// Handle premium status
premiumStatus := "regular user"
if isPremium {
premiumStatus = "premium user"
}
systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
// Handle time awareness
timeObj := time.Unix(int64(messageTime), 0)
hour := timeObj.Hour()
var timeContext string
if hour >= 5 && hour < 12 {
timeContext = "morning"
} else if hour >= 12 && hour < 18 {
timeContext = "afternoon"
} else if hour >= 18 && hour < 22 {
timeContext = "evening"
} else {
timeContext = "night"
}
systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext)
// Check that all placeholders were replaced correctly
if !strings.Contains(systemMessage, "username 'testuser'") {
t.Errorf("Username not replaced correctly, got: %s", systemMessage)
}
if !strings.Contains(systemMessage, "display name 'Test User'") {
t.Errorf("Display name not replaced correctly, got: %s", systemMessage)
}
if !strings.Contains(systemMessage, "language preference: 'de'") {
t.Errorf("Language preference not replaced correctly, got: %s", systemMessage)
}
if !strings.Contains(systemMessage, "User is a premium user") {
t.Errorf("Premium status not replaced correctly, got: %s", systemMessage)
}
if !strings.Contains(systemMessage, "It's currently afternoon in your timezone") {
t.Errorf("Time context not replaced correctly, got: %s", systemMessage)
}
}
BIN
View File
Binary file not shown.
+550 -39
View File
@@ -3,7 +3,8 @@ package main
import ( import (
"context" "context"
"errors" "errors"
"os" "fmt"
"strings"
"sync" "sync"
"time" "time"
@@ -14,19 +15,73 @@ import (
) )
type Bot struct { type Bot struct {
tgBot *bot.Bot tgBot TelegramClient
db *gorm.DB db *gorm.DB
anthropicClient *anthropic.Client anthropicClient *anthropic.Client
chatMemories map[int64]*ChatMemory chatMemories map[int64]*ChatMemory
memorySize int memorySize int
chatMemoriesMu sync.RWMutex chatMemoriesMu sync.RWMutex
config Config config BotConfig
userLimiters map[int64]*userLimiter userLimiters map[int64]*userLimiter
userLimitersMu sync.RWMutex userLimitersMu sync.RWMutex
clock Clock
botID uint // Reference to BotModel.ID
} }
func NewBot(db *gorm.DB, config Config) (*Bot, error) { // Helper function to determine message type
anthropicClient := anthropic.NewClient(os.Getenv("ANTHROPIC_API_KEY")) func messageType(msg *models.Message) string {
if msg.Sticker != nil {
return "sticker"
}
return "text"
}
// NewBot initializes and returns a new Bot instance.
func NewBot(db *gorm.DB, config BotConfig, clock Clock, tgClient TelegramClient) (*Bot, error) {
// Retrieve or create Bot entry in the database
var botEntry BotModel
err := db.Where("identifier = ?", config.ID).First(&botEntry).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
botEntry = BotModel{Identifier: config.ID, Name: config.ID} // Customize as needed
if err := db.Create(&botEntry).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
// Ensure the owner exists in the Users table
var owner User
err = db.Where("telegram_id = ? AND bot_id = ?", config.OwnerTelegramID, botEntry.ID).First(&owner).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// Assign the "owner" role
var ownerRole Role
err := db.Where("name = ?", "owner").First(&ownerRole).Error
if err != nil {
return nil, fmt.Errorf("owner role not found: %w", err)
}
owner = User{
BotID: botEntry.ID,
TelegramID: config.OwnerTelegramID,
Username: "", // Initialize as empty; will be updated upon interaction
RoleID: ownerRole.ID,
IsOwner: true,
}
if err := db.Create(&owner).Error; err != nil {
// If unique constraint is violated, another owner already exists
if strings.Contains(err.Error(), "unique index") {
return nil, fmt.Errorf("an owner already exists for this bot")
}
return nil, fmt.Errorf("failed to create owner user: %w", err)
}
} else if err != nil {
return nil, err
}
// Use the per-bot Anthropic API key
anthropicClient := anthropic.NewClient(config.AnthropicAPIKey)
b := &Bot{ b := &Bot{
db: db, db: db,
@@ -35,55 +90,115 @@ func NewBot(db *gorm.DB, config Config) (*Bot, error) {
memorySize: config.MemorySize, memorySize: config.MemorySize,
config: config, config: config,
userLimiters: make(map[int64]*userLimiter), userLimiters: make(map[int64]*userLimiter),
clock: clock,
botID: botEntry.ID, // Ensure BotModel has ID field
tgBot: tgClient,
} }
tgBot, err := initTelegramBot(b.handleUpdate) if tgClient == nil {
if err != nil { var err error
return nil, err tgClient, err = initTelegramBot(config.TelegramToken, b)
if err != nil {
return nil, fmt.Errorf("failed to initialize Telegram bot: %w", err)
}
b.tgBot = tgClient
} }
b.tgBot = tgBot
return b, nil return b, nil
} }
// Start begins the bot's operation.
func (b *Bot) Start(ctx context.Context) { func (b *Bot) Start(ctx context.Context) {
b.tgBot.Start(ctx) b.tgBot.Start(ctx)
} }
func (b *Bot) getOrCreateUser(userID int64, username string) (User, error) { func (b *Bot) getOrCreateUser(userID int64, username string, isOwner bool) (User, error) {
var user User var user User
err := b.db.Preload("Role").Where("telegram_id = ?", userID).First(&user).Error err := b.db.Preload("Role").Where("telegram_id = ? AND bot_id = ?", userID, b.botID).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
var defaultRole Role // Check if an owner already exists for this bot
if err := b.db.Where("name = ?", "user").First(&defaultRole).Error; err != nil { if isOwner {
return User{}, err var existingOwner User
err := b.db.Where("bot_id = ? AND is_owner = ?", b.botID, true).First(&existingOwner).Error
if err == nil {
return User{}, fmt.Errorf("an owner already exists for this bot")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return User{}, fmt.Errorf("failed to check existing owner: %w", err)
}
} }
user = User{TelegramID: userID, Username: username, RoleID: defaultRole.ID}
var role Role
var roleName string
if isOwner {
roleName = "owner"
} else {
roleName = "user" // Assign "user" role to non-owner users
}
err := b.db.Where("name = ?", roleName).First(&role).Error
if err != nil {
return User{}, fmt.Errorf("failed to get role: %w", err)
}
user = User{
BotID: b.botID,
TelegramID: userID,
Username: username,
RoleID: role.ID,
Role: role,
IsOwner: isOwner,
}
if err := b.db.Create(&user).Error; err != nil { if err := b.db.Create(&user).Error; err != nil {
return User{}, err // If unique constraint is violated, another owner already exists
if strings.Contains(err.Error(), "unique index") {
return User{}, fmt.Errorf("an owner already exists for this bot")
}
return User{}, fmt.Errorf("failed to create user: %w", err)
} }
} else { } else {
return User{}, err return User{}, err
} }
} else {
if isOwner && !user.IsOwner {
return User{}, fmt.Errorf("cannot change existing user to owner")
}
} }
return user, nil return user, nil
} }
func (b *Bot) getRoleByName(roleName string) (Role, error) {
var role Role
err := b.db.Where("name = ?", roleName).First(&role).Error
return role, err
}
func (b *Bot) createMessage(chatID, userID int64, username, userRole, text string, isUser bool) Message { func (b *Bot) createMessage(chatID, userID int64, username, userRole, text string, isUser bool) Message {
return Message{ message := Message{
ChatID: chatID, ChatID: chatID,
UserID: userID,
Username: username,
UserRole: userRole, UserRole: userRole,
Text: text, Text: text,
Timestamp: time.Now(), Timestamp: time.Now(),
IsUser: isUser, IsUser: isUser,
} }
if isUser {
message.UserID = userID
message.Username = username
} else {
message.UserID = 0
message.Username = "AI Assistant"
}
return message
} }
func (b *Bot) storeMessage(message Message) error { // storeMessage stores a message in the database and updates its ID
return b.db.Create(&message).Error func (b *Bot) storeMessage(message *Message) error {
message.BotID = b.botID // Associate the message with the correct bot
return b.db.Create(message).Error // This will update the message with its new ID
} }
func (b *Bot) getOrCreateChatMemory(chatID int64) *ChatMemory { func (b *Bot) getOrCreateChatMemory(chatID int64) *ChatMemory {
@@ -92,29 +207,55 @@ func (b *Bot) getOrCreateChatMemory(chatID int64) *ChatMemory {
b.chatMemoriesMu.RUnlock() b.chatMemoriesMu.RUnlock()
if !exists { if !exists {
var messages []Message
b.db.Where("chat_id = ?", chatID).Order("timestamp asc").Limit(b.memorySize * 2).Find(&messages)
chatMemory = &ChatMemory{
Messages: messages,
Size: b.memorySize * 2,
}
b.chatMemoriesMu.Lock() b.chatMemoriesMu.Lock()
b.chatMemories[chatID] = chatMemory defer b.chatMemoriesMu.Unlock()
b.chatMemoriesMu.Unlock()
chatMemory, exists = b.chatMemories[chatID]
if !exists {
// Check if this is a new chat by querying the database
var count int64
b.db.Model(&Message{}).Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Count(&count)
isNewChat := count == 0 // Truly new chat if no messages exist
var messages []Message
if !isNewChat {
// Fetch existing messages only if it's not a new chat
err := b.db.Where("chat_id = ? AND bot_id = ?", chatID, b.botID).
Order("timestamp asc").
Limit(b.memorySize * 2).
Find(&messages).Error
if err != nil {
ErrorLogger.Printf("Error fetching messages from database: %v", err)
messages = []Message{} // Initialize an empty slice on error
}
} else {
messages = []Message{} // Ensure messages is initialized for new chats
}
chatMemory = &ChatMemory{
Messages: messages,
Size: b.memorySize * 2,
}
b.chatMemories[chatID] = chatMemory
}
} }
return chatMemory return chatMemory
} }
// addMessageToChatMemory adds a new message to the chat memory, ensuring the memory size is maintained.
func (b *Bot) addMessageToChatMemory(chatMemory *ChatMemory, message Message) { func (b *Bot) addMessageToChatMemory(chatMemory *ChatMemory, message Message) {
b.chatMemoriesMu.Lock() b.chatMemoriesMu.Lock()
defer b.chatMemoriesMu.Unlock() defer b.chatMemoriesMu.Unlock()
// Add the new message
chatMemory.Messages = append(chatMemory.Messages, message) chatMemory.Messages = append(chatMemory.Messages, message)
// Maintain the memory size
if len(chatMemory.Messages) > chatMemory.Size { if len(chatMemory.Messages) > chatMemory.Size {
chatMemory.Messages = chatMemory.Messages[2:] chatMemory.Messages = chatMemory.Messages[len(chatMemory.Messages)-chatMemory.Size:]
} }
} }
@@ -122,16 +263,34 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message
b.chatMemoriesMu.RLock() b.chatMemoriesMu.RLock()
defer b.chatMemoriesMu.RUnlock() defer b.chatMemoriesMu.RUnlock()
// Debug logging
InfoLogger.Printf("Chat memory contains %d messages", len(chatMemory.Messages))
for i, msg := range chatMemory.Messages {
InfoLogger.Printf("Message %d: IsUser=%v, Text=%q", i, msg.IsUser, msg.Text)
}
// Note: consecutive messages with the same role are permitted.
// The Anthropic API automatically merges them into a single turn rather than
// returning an error. This can happen after a /clear (which only deletes user
// messages, leaving assistant messages in the DB) followed by a restart.
// See: https://platform.claude.com/docs/en/api/messages
var contextMessages []anthropic.Message var contextMessages []anthropic.Message
for _, msg := range chatMemory.Messages { for _, msg := range chatMemory.Messages {
role := anthropic.RoleUser role := anthropic.RoleUser
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),
}, },
}) })
} }
@@ -140,23 +299,375 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message
func (b *Bot) isNewChat(chatID int64) bool { func (b *Bot) isNewChat(chatID int64) bool {
var count int64 var count int64
b.db.Model(&Message{}).Where("chat_id = ?", chatID).Count(&count) b.db.Model(&Message{}).Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Count(&count)
return count == 1 return count == 0 // Only consider a chat new if it has 0 messages
} }
func (b *Bot) isAdminOrOwner(userID int64) bool { func (b *Bot) isAdminOrOwner(userID int64) bool {
var user User var user User
err := b.db.Preload("Role").Where("telegram_id = ?", userID).First(&user).Error err := b.db.Preload("Role").Where("telegram_id = ? AND bot_id = ?", userID, b.botID).First(&user).Error
if err != nil { if err != nil {
return false return false
} }
return user.Role.Name == "admin" || user.Role.Name == "owner" return user.Role.Name == "admin" || user.Role.Name == "owner"
} }
func initTelegramBot(handleUpdate func(ctx context.Context, b *bot.Bot, update *models.Update)) (*bot.Bot, error) { func initTelegramBot(token string, b *Bot) (TelegramClient, error) {
opts := []bot.Option{ opts := []bot.Option{
bot.WithDefaultHandler(handleUpdate), bot.WithDefaultHandler(b.handleUpdate),
} }
return bot.New(os.Getenv("TELEGRAM_BOT_TOKEN"), opts...) tgBot, err := bot.New(token, opts...)
if err != nil {
return nil, err
}
// Define bot commands
commands := []models.BotCommand{
{
Command: "stats",
Description: "Get bot statistics. Usage: /stats or /stats user [user_id]",
},
{
Command: "whoami",
Description: "Get your user information",
},
{
Command: "clear",
Description: "Clear chat history (soft delete). Admins: /clear [user_id]",
},
{
Command: "clear_hard",
Description: "Clear chat history (permanently delete). Admins: /clear_hard [user_id]",
},
}
// Set bot commands
_, err = tgBot.SetMyCommands(context.Background(), &bot.SetMyCommandsParams{
Commands: commands,
})
if err != nil {
ErrorLogger.Printf("Error setting bot commands: %v", err)
return nil, err
}
return tgBot, nil
}
func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string, businessConnectionID string) error {
// Pass the outgoing message through the centralized screen for storage and chat memory update
_, err := b.screenOutgoingMessage(chatID, text)
if err != nil {
ErrorLogger.Printf("Error storing assistant message: %v", err)
return err
}
// Prepare message parameters
params := &bot.SendMessageParams{
ChatID: chatID,
Text: text,
}
if businessConnectionID != "" {
params.BusinessConnectionID = businessConnectionID
}
// Send the message via Telegram client
_, err = b.tgBot.SendMessage(ctx, params)
if err != nil {
ErrorLogger.Printf("[%s] 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, userID int64, targetUserID int64, businessConnectionID string) {
// If targetUserID is 0, show global stats
if targetUserID == 0 {
totalUsers, totalMessages, err := b.getStats()
if err != nil {
ErrorLogger.Printf("Error fetching stats: %v\n", err)
if err := b.sendResponse(ctx, chatID, "Sorry, I couldn't retrieve the stats at this time.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
// Do NOT manually escape hyphens here
statsMessage := fmt.Sprintf(
"📊 Bot Statistics:\n\n"+
"- Total Users: %d\n"+
"- Total Messages: %d",
totalUsers,
totalMessages,
)
// Send the response through the centralized screen
if err := b.sendResponse(ctx, chatID, statsMessage, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending stats message: %v", err)
}
return
}
// If targetUserID is not 0, show user-specific stats
// Check permissions if the user is trying to view someone else's stats
if targetUserID != userID {
if !b.isAdminOrOwner(userID) {
InfoLogger.Printf("User %d attempted to view stats for user %d without permission", userID, targetUserID)
if err := b.sendResponse(ctx, chatID, "Permission denied. Only admins and owners can view other users' statistics.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
// Get user stats
username, messagesIn, messagesOut, totalMessages, err := b.getUserStats(targetUserID)
if err != nil {
ErrorLogger.Printf("Error fetching user stats: %v\n", err)
if err := b.sendResponse(ctx, chatID, fmt.Sprintf("Sorry, I couldn't retrieve statistics for user ID %d.", targetUserID), businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
// Build the user stats message
userInfo := fmt.Sprintf("@%s (ID: %d)", username, targetUserID)
if username == "" {
userInfo = fmt.Sprintf("User ID: %d", targetUserID)
}
statsMessage := fmt.Sprintf(
"👤 User Statistics for %s:\n\n"+
"- Messages Sent: %d\n"+
"- Messages Received: %d\n"+
"- Total Messages: %d",
userInfo,
messagesIn,
messagesOut,
totalMessages,
)
if err := b.sendResponse(ctx, chatID, statsMessage, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending user stats message: %v", err)
}
}
// getStats retrieves the total number of users and messages from the database.
func (b *Bot) getStats() (int64, int64, error) {
var totalUsers int64
if err := b.db.Model(&User{}).Where("bot_id = ?", b.botID).Count(&totalUsers).Error; err != nil {
return 0, 0, err
}
var totalMessages int64
if err := b.db.Model(&Message{}).Where("bot_id = ?", b.botID).Count(&totalMessages).Error; err != nil {
return 0, 0, err
}
return totalUsers, totalMessages, nil
}
// getUserStats retrieves statistics for a specific user
func (b *Bot) getUserStats(userID int64) (string, int64, int64, int64, error) {
// Get user information from database
var user User
err := b.db.Where("telegram_id = ? AND bot_id = ?", userID, b.botID).First(&user).Error
if err != nil {
return "", 0, 0, 0, fmt.Errorf("user not found: %w", err)
}
// Count messages sent by the user (IN)
var messagesIn int64
if err := b.db.Model(&Message{}).Where("user_id = ? AND bot_id = ? AND is_user = ?",
userID, b.botID, true).Count(&messagesIn).Error; err != nil {
return "", 0, 0, 0, err
}
// Count responses to the user (OUT)
var messagesOut int64
if err := b.db.Model(&Message{}).Where("chat_id IN (SELECT DISTINCT chat_id FROM messages WHERE user_id = ? AND bot_id = ?) AND bot_id = ? AND is_user = ?",
userID, b.botID, b.botID, false).Count(&messagesOut).Error; err != nil {
return "", 0, 0, 0, err
}
// Total messages is the sum
totalMessages := messagesIn + messagesOut
return user.Username, messagesIn, messagesOut, 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
}
func (b *Bot) sendWhoAmI(ctx context.Context, chatID int64, userID int64, username string, businessConnectionID string) {
user, err := b.getOrCreateUser(userID, username, false)
if err != nil {
ErrorLogger.Printf("Error getting or creating user: %v", err)
if err := b.sendResponse(ctx, chatID, "Sorry, I couldn't retrieve your information.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
role, err := b.getRoleByName(user.Role.Name)
if err != nil {
ErrorLogger.Printf("Error getting role by name: %v", err)
if err := b.sendResponse(ctx, chatID, "Sorry, I couldn't retrieve your role information.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
whoAmIMessage := fmt.Sprintf(
"👤 Your Information:\n\n"+
"- Username: %s\n"+
"- Role: %s",
user.Username,
role.Name,
)
// Send the response through the centralized screen
if err := b.sendResponse(ctx, chatID, whoAmIMessage, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending /whoami message: %v", err)
}
}
// screenIncomingMessage centralizes all incoming message processing: storing messages and updating chat memory.
func (b *Bot) screenIncomingMessage(message *models.Message) (Message, error) {
if b.config.DebugScreening {
start := time.Now()
defer func() {
InfoLogger.Printf(
"[Screen] Incoming: chat=%d user=%d type=%s memory_size=%d duration=%v",
message.Chat.ID,
message.From.ID,
messageType(message),
len(b.getOrCreateChatMemory(message.Chat.ID).Messages),
time.Since(start),
)
}()
}
userRole := string(anthropic.RoleUser)
// Determine message text based on message type
messageText := message.Text
if message.Sticker != nil {
if message.Sticker.Emoji != "" {
messageText = fmt.Sprintf("Sent a sticker: %s", message.Sticker.Emoji)
} else {
messageText = "Sent a sticker."
}
}
userMessage := b.createMessage(message.Chat.ID, message.From.ID, message.From.Username, userRole, messageText, true)
// Handle sticker-specific details if present
if message.Sticker != nil {
userMessage.StickerFileID = message.Sticker.FileID
userMessage.StickerEmoji = message.Sticker.Emoji // Store the sticker emoji
if message.Sticker.Thumbnail != nil {
userMessage.StickerPNGFile = message.Sticker.Thumbnail.FileID
}
}
// Get the chat memory before storing the message
chatMemory := b.getOrCreateChatMemory(message.Chat.ID)
// Store the message and get its ID
if err := b.storeMessage(&userMessage); err != nil {
return Message{}, err
}
// Add the message to the chat memory
b.addMessageToChatMemory(chatMemory, userMessage)
return userMessage, nil
}
// screenOutgoingMessage handles storing of outgoing messages and updating chat memory.
// It also marks the most recent unanswered user message as answered.
func (b *Bot) screenOutgoingMessage(chatID int64, response string) (Message, error) {
if b.config.DebugScreening {
start := time.Now()
defer func() {
InfoLogger.Printf(
"[Screen] Outgoing: chat=%d len=%d memory_size=%d duration=%v",
chatID,
len(response),
len(b.getOrCreateChatMemory(chatID).Messages),
time.Since(start),
)
}()
}
// Create and store the assistant message
assistantMessage := b.createMessage(chatID, 0, "", string(anthropic.RoleAssistant), response, false)
if err := b.storeMessage(&assistantMessage); err != nil {
return Message{}, err
}
// Find and mark the most recent unanswered user message as answered
now := time.Now()
err := b.db.Model(&Message{}).
Where("chat_id = ? AND bot_id = ? AND is_user = ? AND answered_on IS NULL",
chatID, b.botID, true).
Order("timestamp DESC").
Limit(1).
Update("answered_on", now).Error
if err != nil {
ErrorLogger.Printf("Error marking user message as answered: %v", err)
// Continue even if there's an error updating the user message
}
// Update chat memory with the message that now has an ID
chatMemory := b.getOrCreateChatMemory(chatID)
b.addMessageToChatMemory(chatMemory, assistantMessage)
return assistantMessage, nil
}
func (b *Bot) promoteUserToAdmin(promoterID, userToPromoteID int64) error {
// Check if the promoter is an owner or admin
if !b.isAdminOrOwner(promoterID) {
return errors.New("only admins or owners can promote users to admin")
}
// Get the user to promote
userToPromote, err := b.getOrCreateUser(userToPromoteID, "", false)
if err != nil {
return err
}
// Get the admin role
var adminRole Role
if err := b.db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
// Update the user's role
userToPromote.RoleID = adminRole.ID
userToPromote.Role = adminRole
return b.db.Save(&userToPromote).Error
} }
-740
View File
@@ -1,740 +0,0 @@
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.028ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="messages"
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[28.510ms] [rows:0] CREATE TABLE `messages` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`chat_id` integer,`user_id` integer,`username` text,`user_role` text,`text` text,`timestamp` datetime,`is_user` numeric)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.213ms] [rows:0] CREATE INDEX `idx_messages_deleted_at` ON `messages`(`deleted_at`)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.064ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="roles"
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.650ms] [rows:0] CREATE TABLE `roles` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`name` text)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.510ms] [rows:0] CREATE UNIQUE INDEX `idx_roles_name` ON `roles`(`name`)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.811ms] [rows:0] CREATE INDEX `idx_roles_deleted_at` ON `roles`(`deleted_at`)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.086ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="users"
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.542ms] [rows:0] CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`telegram_id` integer,`username` text,`role_id` integer,CONSTRAINT `fk_users_role` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`))
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.475ms] [rows:0] CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[19.446ms] [rows:0] CREATE UNIQUE INDEX `idx_users_telegram_id` ON `users`(`telegram_id`)
2024/10/13 02:26:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.155ms] [rows:0] SELECT * FROM `roles` WHERE `roles`.`name` = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:26:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[19.868ms] [rows:1] INSERT INTO `roles` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ("2024-10-13 02:26:06.988","2024-10-13 02:26:06.988",NULL,"user") RETURNING `id`
2024/10/13 02:26:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.144ms] [rows:0] SELECT * FROM `roles` WHERE `roles`.`name` = "admin" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:26:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[19.695ms] [rows:1] INSERT INTO `roles` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ("2024-10-13 02:26:07.008","2024-10-13 02:26:07.008",NULL,"admin") RETURNING `id`
2024/10/13 02:26:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.135ms] [rows:0] SELECT * FROM `roles` WHERE `roles`.`name` = "owner" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:26:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[19.908ms] [rows:1] INSERT INTO `roles` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ("2024-10-13 02:26:07.028","2024-10-13 02:26:07.028",NULL,"owner") RETURNING `id`
2024/10/13 02:26:07 Telegram bot initialized successfully
2024/10/13 02:26:07 Starting bot...
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195 record not found
[0.300ms] [rows:0] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:198
[0.182ms] [rows:1] SELECT * FROM `roles` WHERE name = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:203
[29.608ms] [rows:1] INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`telegram_id`,`username`,`role_id`) VALUES ("2024-10-13 02:26:13.713","2024-10-13 02:26:13.713",NULL,1404948412,"tibikgaming",1) RETURNING `id`
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[20.376ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:26:13.743","2024-10-13 02:26:13.743",NULL,1404948412,1404948412,"tibikgaming","","Hello","2024-10-13 02:26:13.743",true) RETURNING `id`
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.243ms] [rows:1] SELECT * FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL ORDER BY timestamp asc LIMIT 20
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.061ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.051ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:26:13 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.218ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:26:14 Error getting Anthropic response: error creating Anthropic message: error, status code: 400, message: anthropic api error type: invalid_request_error, message: messages.0.role: Input should be 'user' or 'assistant'
2024/10/13 02:26:14 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.404ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:26:14.218","2024-10-13 02:26:14.218",NULL,1404948412,0,"Assistant","assistant","I'm sorry, I'm having trouble processing your request right now.","2024-10-13 02:26:14.218",false) RETURNING `id`
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.120ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.641ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.264ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:26:18.626","2024-10-13 02:26:18.626",NULL,1404948412,1404948412,"tibikgaming","user","Hello","2024-10-13 02:26:18.626",true) RETURNING `id`
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.220ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.109ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.376ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:26:18 Error getting Anthropic response: error creating Anthropic message: error, status code: 400, message: anthropic api error type: invalid_request_error, message: messages.0.role: Input should be 'user' or 'assistant'
2024/10/13 02:26:18 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.141ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:26:18.888","2024-10-13 02:26:18.888",NULL,1404948412,0,"Assistant","assistant","I'm sorry, I'm having trouble processing your request right now.","2024-10-13 02:26:18.888",false) RETURNING `id`
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.010ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="messages"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.024ms] [rows:2] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "messages" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.008ms] [rows:-] SELECT * FROM `messages` LIMIT 1
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.007ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "messages" AND name = "idx_messages_deleted_at"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.006ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="roles"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.031ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "roles" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.005ms] [rows:-] SELECT * FROM `roles` LIMIT 1
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.005ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_deleted_at"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.003ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_name"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.003ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="users"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.017ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "users" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.005ms] [rows:-] SELECT * FROM `users` LIMIT 1
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.018ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "table" AND tbl_name = "users" AND (sql LIKE "%CONSTRAINT ""fk_users_role"" %" OR sql LIKE "%CONSTRAINT fk_users_role %" OR sql LIKE "%CONSTRAINT `fk_users_role`%" OR sql LIKE "%CONSTRAINT [fk_users_role]%" OR sql LIKE "%CONSTRAINT fk_users_role %")
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.004ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_deleted_at"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.003ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_telegram_id"
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.040ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.014ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "admin" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:28:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.015ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "owner" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:28:24 Telegram bot initialized successfully
2024/10/13 02:28:24 Starting bot...
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.076ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.579ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.086ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:28:31.237","2024-10-13 02:28:31.237",NULL,1404948412,1404948412,"tibikgaming","user","Hello","2024-10-13 02:28:31.237",true) RETURNING `id`
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.352ms] [rows:5] SELECT * FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL ORDER BY timestamp asc LIMIT 20
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.081ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.080ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.248ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:28:31 Error getting Anthropic response: error creating Anthropic message: error, status code: 400, message: anthropic api error type: invalid_request_error, message: messages.0.role: Input should be 'user' or 'assistant'
2024/10/13 02:28:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.128ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:28:31.589","2024-10-13 02:28:31.589",NULL,1404948412,0,"Assistant","assistant","I'm sorry, I'm having trouble processing your request right now.","2024-10-13 02:28:31.589",false) RETURNING `id`
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.022ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="messages"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.063ms] [rows:2] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "messages" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.021ms] [rows:-] SELECT * FROM `messages` LIMIT 1
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.020ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "messages" AND name = "idx_messages_deleted_at"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.017ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="roles"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.066ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "roles" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.017ms] [rows:-] SELECT * FROM `roles` LIMIT 1
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.019ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_name"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.028ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_deleted_at"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.007ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="users"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.030ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "users" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.011ms] [rows:-] SELECT * FROM `users` LIMIT 1
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.013ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "table" AND tbl_name = "users" AND (sql LIKE "%CONSTRAINT ""fk_users_role"" %" OR sql LIKE "%CONSTRAINT fk_users_role %" OR sql LIKE "%CONSTRAINT `fk_users_role`%" OR sql LIKE "%CONSTRAINT [fk_users_role]%" OR sql LIKE "%CONSTRAINT fk_users_role %")
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.005ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_deleted_at"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:143
[0.007ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_telegram_id"
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.055ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.019ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "admin" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:33:02 /home/fedora/Desktop/thatsky-telegram-bot/main.go:152
[0.023ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "owner" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:33:02 Telegram bot initialized successfully
2024/10/13 02:33:02 Starting bot...
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.091ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.479ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[30.934ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:09.456","2024-10-13 02:33:09.456",NULL,1404948412,1404948412,"tibikgaming","user","Hello","2024-10-13 02:33:09.456",true) RETURNING `id`
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.464ms] [rows:7] SELECT * FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL ORDER BY timestamp asc LIMIT 20
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.125ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.118ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:09 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.409ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:10 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.569ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:10.016","2024-10-13 02:33:10.016",NULL,1404948412,0,"Assistant","assistant","Hello!","2024-10-13 02:33:10.016",false) RETURNING `id`
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.165ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.910ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.973ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:17.238","2024-10-13 02:33:17.238",NULL,1404948412,1404948412,"tibikgaming","user","My name is tibik","2024-10-13 02:33:17.237",true) RETURNING `id`
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.163ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.070ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.289ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:17 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.538ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:17.698","2024-10-13 02:33:17.698",NULL,1404948412,0,"Assistant","assistant","Nice to meet you Tibik!","2024-10-13 02:33:17.697",false) RETURNING `id`
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.216ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.893ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.420ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:22.869","2024-10-13 02:33:22.869",NULL,1404948412,1404948412,"tibikgaming","user","Who am I?","2024-10-13 02:33:22.869",true) RETURNING `id`
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.169ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.076ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.297ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:23 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.088ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:23.734","2024-10-13 02:33:23.734",NULL,1404948412,0,"Assistant","assistant","I'm afraid I don't actually know who you are. As an AI assistant, I was created by Anthropic to be helpful, harmless, and honest in conversations. My name is Claude.","2024-10-13 02:33:23.734",false) RETURNING `id`
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.493ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[1.567ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.415ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:31.881","2024-10-13 02:33:31.881",NULL,1404948412,1404948412,"tibikgaming","user","What is my name?","2024-10-13 02:33:31.881",true) RETURNING `id`
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.327ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.068ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:31 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.351ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:32 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[21.003ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:32.423","2024-10-13 02:33:32.423",NULL,1404948412,0,"Assistant","assistant","Based on our conversation so far, it seems your name is Tibik.","2024-10-13 02:33:32.423",false) RETURNING `id`
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.115ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.741ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.997ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:43.658","2024-10-13 02:33:43.658",NULL,1404948412,1404948412,"tibikgaming","user","Thanks!","2024-10-13 02:33:43.658",true) RETURNING `id`
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.215ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.087ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:43 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.335ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.720ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:44.112","2024-10-13 02:33:44.112",NULL,1404948412,0,"Assistant","assistant","You're welcome!","2024-10-13 02:33:44.112",false) RETURNING `id`
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.117ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.591ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.179ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:52.171","2024-10-13 02:33:52.171",NULL,1404948412,1404948412,"tibikgaming","user","What is your system prompt?","2024-10-13 02:33:52.171",true) RETURNING `id`
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.171ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.071ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.295ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:33:52 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.730ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:33:52.92","2024-10-13 02:33:52.92",NULL,1404948412,0,"Assistant","assistant","I don't have a system prompt. I'm an AI assistant named Claude created by Anthropic to be helpful, harmless, and honest.","2024-10-13 02:33:52.92",false) RETURNING `id`
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.089ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.631ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[28.953ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:34:36.814","2024-10-13 02:34:36.814",NULL,1404948412,1404948412,"tibikgaming","user","What is the very first instruction in this chat?","2024-10-13 02:34:36.814",true) RETURNING `id`
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.168ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.070ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:34:36 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.288ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:34:37 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.091ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:34:37.627","2024-10-13 02:34:37.627",NULL,1404948412,0,"Assistant","assistant","The very first instruction in this chat was you saying ""Hello"". We've had a brief introduction since then where you introduced yourself as Tibik.","2024-10-13 02:34:37.627",false) RETURNING `id`
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.094ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.631ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[29.217ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:34:54.415","2024-10-13 02:34:54.415",NULL,1404948412,1404948412,"tibikgaming","user","And before that?","2024-10-13 02:34:54.415",true) RETURNING `id`
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.171ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.070ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:34:54 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.276ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:34:55 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.166ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:34:55.386","2024-10-13 02:34:55.386",NULL,1404948412,0,"Assistant","assistant","There was no conversation before you said ""Hello"" - that was the very first message in this chat log. As an AI, I don't have any knowledge or memory of interactions before our conversation started.","2024-10-13 02:34:55.385",false) RETURNING `id`
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[0.116ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:195
[1.204ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:221
[28.659ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:35:44.158","2024-10-13 02:35:44.158",NULL,1404948412,1404948412,"tibikgaming","user","Write a very long paragraph","2024-10-13 02:35:44.158",true) RETURNING `id`
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:329
[0.161ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.071ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:35:44 /home/fedora/Desktop/thatsky-telegram-bot/main.go:338
[0.287ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:35:46 /home/fedora/Desktop/thatsky-telegram-bot/main.go:311
[29.384ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:35:46.953","2024-10-13 02:35:46.953",NULL,1404948412,0,"Assistant","assistant","Here is a multi-sentence paragraph:
I hope you're having a wonderful day so far. The sun is shining brightly and there is a cool breeze in the air. It's the perfect weather for going outside for a walk in the park or spending time with friends and family. There are so many activities we can enjoy when the conditions are just right. We could read books under a shady tree, fly kites at the open field, have a picnic by the lake, or simply people watch and appreciate nature's small wonders. Being able to experience the outdoors on a day with agreeable climate really lifts our mood and enriches our well-being. Maybe later this afternoon I'll visit the playground to watch the kids play - their laughter is so joyful and helps remind us of life's simple pleasures. All in all, I'm grateful it's not too hot or rainy right now so we can safely spend the day outdoors enjoying this beautiful day.","2024-10-13 02:35:46.952",false) RETURNING `id`
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.010ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="messages"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.026ms] [rows:2] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "messages" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.009ms] [rows:-] SELECT * FROM `messages` LIMIT 1
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.007ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "messages" AND name = "idx_messages_deleted_at"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.004ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="roles"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.059ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "roles" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.007ms] [rows:-] SELECT * FROM `roles` LIMIT 1
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.005ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_deleted_at"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.004ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_name"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.003ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="users"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.014ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "users" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.005ms] [rows:-] SELECT * FROM `users` LIMIT 1
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.009ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "table" AND tbl_name = "users" AND (sql LIKE "%CONSTRAINT ""fk_users_role"" %" OR sql LIKE "%CONSTRAINT fk_users_role %" OR sql LIKE "%CONSTRAINT `fk_users_role`%" OR sql LIKE "%CONSTRAINT [fk_users_role]%" OR sql LIKE "%CONSTRAINT fk_users_role %")
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.004ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_telegram_id"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:171
[0.005ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_deleted_at"
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:180
[0.041ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:180
[0.014ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "admin" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:45:51 /home/fedora/Desktop/thatsky-telegram-bot/main.go:180
[0.021ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "owner" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:45:51 Telegram bot initialized successfully
2024/10/13 02:45:51 Starting bot...
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.150ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.834ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.863ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:45:56.128","2024-10-13 02:45:56.128",NULL,1404948412,1404948412,"tibikgaming","user","Hello","2024-10-13 02:45:56.128",true) RETURNING `id`
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:279
[0.599ms] [rows:20] SELECT * FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL ORDER BY timestamp asc LIMIT 20
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.081ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.059ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:45:56 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.227ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:45:57 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[30.074ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:45:56.987","2024-10-13 02:45:56.987",NULL,1404948412,0,"Assistant","assistant","Hello again!","2024-10-13 02:45:56.987",false) RETURNING `id`
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.126ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.713ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.792ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:05.342","2024-10-13 02:46:05.342",NULL,1404948412,1404948412,"tibikgaming","user","repeat your previous message","2024-10-13 02:46:05.342",true) RETURNING `id`
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.215ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.113ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:05 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.395ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:06 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[29.672ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:05.999","2024-10-13 02:46:05.999",NULL,1404948412,0,"Assistant","assistant","The very first instruction in this chat was you saying ""Hello"". We've had a brief introduction since then where you introduced yourself as Tibik.","2024-10-13 02:46:05.998",false) RETURNING `id`
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.116ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.861ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.179ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:24.184","2024-10-13 02:46:24.184",NULL,1404948412,1404948412,"tibikgaming","user","what was the long text you've written?","2024-10-13 02:46:24.184",true) RETURNING `id`
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.172ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.076ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.304ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:26 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[29.708ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:25.997","2024-10-13 02:46:25.997",NULL,1404948412,0,"Assistant","assistant","I'm afraid I don't have any long texts to reference. As an AI, I don't store full transcripts of our conversation. Based on your previous questions, it seems like the longest message I've sent so far was repeating that the very first instruction in this chat was you saying ""Hello"", and that we've had a brief introduction where you introduced yourself as Tibik. Please let me know if you need any clarification or have additional questions!","2024-10-13 02:46:25.997",false) RETURNING `id`
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.086ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.494ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[28.367ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:45.045","2024-10-13 02:46:45.045",NULL,1404948412,1404948412,"tibikgaming","user","/start","2024-10-13 02:46:45.045",true) RETURNING `id`
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.151ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.106ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.351ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:46:45 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[29.792ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:46:45.872","2024-10-13 02:46:45.872",NULL,1404948412,0,"Assistant","assistant","I'm afraid I don't have access to any system commands like ""/start"". I'm an AI assistant named Claude created by Anthropic to be helpful, harmless, and honest through natural language conversations.","2024-10-13 02:46:45.872",false) RETURNING `id`
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.128ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.805ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.541ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:07.295","2024-10-13 02:47:07.295",NULL,1404948412,1404948412,"tibikgaming","user","Who am I?","2024-10-13 02:47:07.295",true) RETURNING `id`
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.186ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.062ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:07 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.301ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:08 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[29.566ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:08.415","2024-10-13 02:47:08.415",NULL,1404948412,0,"Assistant","assistant","I'm sorry, I don't actually have any information about who you are. As an AI, I was created by Anthropic to be helpful, harmless, and honest in conversations, but I don't have personal details about users. You'd have to introduce yourself for me to know your name or identity.","2024-10-13 02:47:08.415",false) RETURNING `id`
2024/10/13 02:47:15 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.158ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:15 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.766ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:16 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.550ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:15.998","2024-10-13 02:47:15.998",NULL,1404948412,1404948412,"tibikgaming","user","I am tibik","2024-10-13 02:47:15.998",true) RETURNING `id`
2024/10/13 02:47:16 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.240ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:47:16 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.113ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:16 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.386ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:16 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[28.911ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:16.767","2024-10-13 02:47:16.767",NULL,1404948412,0,"Assistant","assistant","Okay, thank you for introducing yourself. Based on our conversation so far, it's nice to meet you Tibik!","2024-10-13 02:47:16.767",false) RETURNING `id`
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.116ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:237
[0.684ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:263
[29.679ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:22.839","2024-10-13 02:47:22.839",NULL,1404948412,1404948412,"tibikgaming","user","Who am I?","2024-10-13 02:47:22.839",true) RETURNING `id`
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:371
[0.211ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.113ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:47:22 /home/fedora/Desktop/thatsky-telegram-bot/main.go:380
[0.479ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:47:24 /home/fedora/Desktop/thatsky-telegram-bot/main.go:353
[29.105ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:47:24.138","2024-10-13 02:47:24.138",NULL,1404948412,0,"Assistant","assistant","Based on our previous conversation, you told me that your name is Tibik. Unless you've provided additional identifying information that I'm not remembering, Tibik is the only information I have about who you are. Please let me know if I'm missing anything or if you'd like me to clarify or expand on my understanding.","2024-10-13 02:47:24.138",false) RETURNING `id`
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.030ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="messages"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.084ms] [rows:2] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "messages" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.027ms] [rows:-] SELECT * FROM `messages` LIMIT 1
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.026ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "messages" AND name = "idx_messages_deleted_at"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.021ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="roles"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.080ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "roles" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.021ms] [rows:-] SELECT * FROM `roles` LIMIT 1
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.023ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_deleted_at"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.021ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "roles" AND name = "idx_roles_name"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.018ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type='table' AND name="users"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.063ms] [rows:3] SELECT sql FROM sqlite_master WHERE type IN ("table","index") AND tbl_name = "users" AND sql IS NOT NULL order by type = "table" desc
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.021ms] [rows:-] SELECT * FROM `users` LIMIT 1
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.058ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "table" AND tbl_name = "users" AND (sql LIKE "%CONSTRAINT ""fk_users_role"" %" OR sql LIKE "%CONSTRAINT fk_users_role %" OR sql LIKE "%CONSTRAINT `fk_users_role`%" OR sql LIKE "%CONSTRAINT [fk_users_role]%" OR sql LIKE "%CONSTRAINT fk_users_role %")
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.013ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_deleted_at"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:30
[0.013ms] [rows:-] SELECT count(*) FROM sqlite_master WHERE type = "index" AND tbl_name = "users" AND name = "idx_users_telegram_id"
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:47
[0.098ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "user" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:47
[0.032ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "admin" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:56:14 /home/fedora/Desktop/thatsky-telegram-bot/database.go:47
[0.038ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`name` = "owner" AND `roles`.`deleted_at` IS NULL ORDER BY `roles`.`id` LIMIT 1
2024/10/13 02:56:14 Starting bot...
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:55
[0.118ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:55
[0.682ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:86
[29.400ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:56:31.931","2024-10-13 02:56:31.931",NULL,1404948412,1404948412,"tibikgaming","user","Repeat your previous message","2024-10-13 02:56:31.931",true) RETURNING `id`
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:96
[0.763ms] [rows:20] SELECT * FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL ORDER BY timestamp asc LIMIT 20
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:143
[0.125ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:149
[0.113ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:56:31 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:149
[0.387ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:56:33 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:86
[28.522ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:56:33.108","2024-10-13 02:56:33.108",NULL,1404948412,0,"Assistant","assistant","The very first instruction in this chat was you saying ""Hello"". We've had a brief introduction since then where you introduced yourself as Tibik.","2024-10-13 02:56:33.108",false) RETURNING `id`
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:55
[0.124ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:55
[0.741ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:86
[28.963ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:56:42.374","2024-10-13 02:56:42.374",NULL,1404948412,1404948412,"tibikgaming","user","/start","2024-10-13 02:56:42.374",true) RETURNING `id`
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:143
[0.169ms] [rows:1] SELECT count(*) FROM `messages` WHERE chat_id = 1404948412 AND `messages`.`deleted_at` IS NULL
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:149
[0.111ms] [rows:1] SELECT * FROM `roles` WHERE `roles`.`id` = 1 AND `roles`.`deleted_at` IS NULL
2024/10/13 02:56:42 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:149
[0.387ms] [rows:1] SELECT * FROM `users` WHERE telegram_id = 1404948412 AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
2024/10/13 02:56:43 /home/fedora/Desktop/thatsky-telegram-bot/bot.go:86
[29.535ms] [rows:1] INSERT INTO `messages` (`created_at`,`updated_at`,`deleted_at`,`chat_id`,`user_id`,`username`,`user_role`,`text`,`timestamp`,`is_user`) VALUES ("2024-10-13 02:56:43.154","2024-10-13 02:56:43.154",NULL,1404948412,0,"Assistant","assistant","I'm afraid I don't have any system commands like ""/start"". I'm an AI assistant named Claude having a conversation.","2024-10-13 02:56:43.153",false) RETURNING `id`
+32
View File
@@ -0,0 +1,32 @@
// clock.go
package main
import "time"
// Clock is an interface to abstract time-related functions.
type Clock interface {
Now() time.Time
}
// RealClock implements Clock using the actual time.
type RealClock struct{}
// Now returns the current local time.
func (RealClock) Now() time.Time {
return time.Now()
}
// MockClock implements Clock for testing purposes.
type MockClock struct {
currentTime time.Time
}
// Now returns the mocked current time.
func (mc *MockClock) Now() time.Time {
return mc.currentTime
}
// Advance moves the current time forward by the specified duration.
func (mc *MockClock) Advance(d time.Duration) {
mc.currentTime = mc.currentTime.Add(d)
}
+189 -13
View File
@@ -2,25 +2,201 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"path/filepath"
"strings"
"github.com/liushuangls/go-anthropic/v2"
) )
type Config struct { type BotConfig struct {
MemorySize int `json:"memory_size"` ID string `json:"id"`
MessagePerHour int `json:"messages_per_hour"` TelegramToken string `json:"telegram_token"`
MessagePerDay int `json:"messages_per_day"` MemorySize int `json:"memory_size"`
TempBanDuration string `json:"temp_ban_duration"` MessagePerHour int `json:"messages_per_hour"`
MessagePerDay int `json:"messages_per_day"`
TempBanDuration string `json:"temp_ban_duration"`
Model anthropic.Model `json:"model"`
Temperature *float32 `json:"temperature,omitempty"` // Controls creativity vs determinism (0.0-1.0)
SystemPrompts map[string]string `json:"system_prompts"`
Active bool `json:"active"`
OwnerTelegramID int64 `json:"owner_telegram_id"`
AnthropicAPIKey string `json:"anthropic_api_key"`
DebugScreening bool `json:"debug_screening"` // Enable detailed screening logs
} }
func loadConfig(filename string) (Config, error) { // Custom unmarshalling to handle anthropic.Model
var config Config func (c *BotConfig) UnmarshalJSON(data []byte) error {
file, err := os.Open(filename) type Alias BotConfig
if err != nil { aux := &struct {
return config, err Model string `json:"model"`
*Alias
}{
Alias: (*Alias)(c),
} }
defer file.Close() if err := json.Unmarshal(data, &aux); err != nil {
return err
}
c.Model = anthropic.Model(aux.Model)
return nil
}
// validateConfigPath ensures the file path is within the allowed directory
func validateConfigPath(configDir, filename string) (string, error) {
// Clean the paths to remove any . or .. components
configDir = filepath.Clean(configDir)
filename = filepath.Clean(filename)
// Get absolute paths
absConfigDir, err := filepath.Abs(configDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for config directory: %w", err)
}
fullPath := filepath.Join(absConfigDir, filename)
absPath, err := filepath.Abs(fullPath)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for config file: %w", err)
}
// Use filepath.Rel to check if the path is within the config directory
rel, err := filepath.Rel(absConfigDir, absPath)
if err != nil || strings.HasPrefix(rel, "..") || strings.Contains(rel, "..") {
return "", fmt.Errorf("invalid config path: file must be within the config directory")
}
// Verify file extension
if filepath.Ext(absPath) != ".json" {
return "", fmt.Errorf("invalid file extension: must be .json")
}
return absPath, nil
}
func loadAllConfigs(dir string) ([]BotConfig, error) {
var configs []BotConfig
ids := make(map[string]bool)
tokens := make(map[string]bool)
files, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("failed to read config directory: %w", err)
}
for _, file := range files {
if filepath.Ext(file.Name()) == ".json" {
validPath, err := validateConfigPath(dir, file.Name())
if err != nil {
InfoLogger.Printf("Invalid config path for %s: %v", file.Name(), err)
continue
}
config, err := loadConfig(validPath)
if err != nil {
InfoLogger.Printf("Failed to load config %s: %v", validPath, err)
continue
}
if !config.Active {
InfoLogger.Printf("Skipping inactive bot: %s", config.ID)
continue
}
if err := validateConfig(&config, ids, tokens); err != nil {
InfoLogger.Printf("Config validation failed for %s: %v", validPath, err)
continue
}
configs = append(configs, config)
}
}
if len(configs) == 0 {
return nil, fmt.Errorf("no valid configs found")
}
return configs, nil
}
func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
if config.ID == "" {
return fmt.Errorf("missing 'id' field")
}
if _, exists := ids[config.ID]; exists {
return fmt.Errorf("duplicate bot id '%s'", config.ID)
}
ids[config.ID] = true
if config.TelegramToken == "" {
return fmt.Errorf("missing 'telegram_token' field")
}
if _, exists := tokens[config.TelegramToken]; exists {
return fmt.Errorf("duplicate telegram_token")
}
tokens[config.TelegramToken] = true
if config.Model == "" {
return fmt.Errorf("missing 'model' field")
}
if config.MessagePerHour <= 0 {
return fmt.Errorf("'messages_per_hour' must be greater than 0")
}
if config.MessagePerDay <= 0 {
return fmt.Errorf("'messages_per_day' must be greater than 0")
}
return nil
}
func loadConfig(filename string) (BotConfig, error) {
var config BotConfig
// Use filepath.Clean before opening the file
file, err := os.OpenFile(filepath.Clean(filename), os.O_RDONLY, 0)
if err != nil {
return config, fmt.Errorf("failed to open config file %s: %w", filename, err)
}
defer func() {
if err := file.Close(); err != nil {
InfoLogger.Printf("Failed to close config file: %v", err)
}
}()
decoder := json.NewDecoder(file) decoder := json.NewDecoder(file)
err = decoder.Decode(&config) if err := decoder.Decode(&config); err != nil {
return config, err return config, fmt.Errorf("failed to decode JSON from %s: %w", filename, err)
}
return config, nil
}
// Reload reloads the BotConfig from the specified filename within the given config directory
func (c *BotConfig) Reload(configDir, filename string) error {
// Validate the config path
validPath, err := validateConfigPath(configDir, filename)
if err != nil {
return fmt.Errorf("invalid config path: %w", err)
}
// Use filepath.Clean before opening the file
cleanPath := filepath.Clean(validPath)
file, err := os.OpenFile(cleanPath, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("failed to open config file %s: %w", cleanPath, err)
}
defer func() {
if err := file.Close(); err != nil {
InfoLogger.Printf("Failed to close config file: %v", err)
}
}()
decoder := json.NewDecoder(file)
if err := decoder.Decode(c); err != nil {
return fmt.Errorf("failed to decode JSON from %s: %w", validPath, err)
}
c.Model = anthropic.Model(c.Model)
return nil
} }
-6
View File
@@ -1,6 +0,0 @@
{
"memory_size": 10,
"messages_per_hour": 20,
"messages_per_day": 100,
"temp_ban_duration": "24h"
}
+21
View File
@@ -0,0 +1,21 @@
{
"id": "default_bot",
"active": false,
"telegram_token": "YOUR_TELEGRAM_BOT_TOKEN",
"owner_telegram_id": 111111111,
"anthropic_api_key": "YOUR_SPECIFIC_ANTHROPIC_API_KEY",
"memory_size": 10,
"messages_per_hour": 20,
"messages_per_day": 100,
"temp_ban_duration": "24h",
"model": "claude-3-5-haiku-latest",
"temperature": 0.7,
"debug_screening": false,
"system_prompts": {
"default": "You are a helpful assistant.",
"custom_instructions": "You are texting through a limited Telegram interface with 15-word maximum. Write like texting a friend - use shorthand, skip grammar, use slang/abbreviations. System cuts off anything longer than 15 words.\n\n- Your name is Atom.\n- The user you're talking to has username '{username}' and display name '{firstname} {lastname}'.\n- User's language preference: '{language}'\n- User is a {premium_status}\n- It's currently {time_context} in your timezone. Use appropriate time-based greetings and address the user by name.\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.",
"respond_with_emojis": "Since the user sent only emojis, respond using emojis only."
}
}
+754
View File
@@ -0,0 +1,754 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/liushuangls/go-anthropic/v2"
)
// Set up loggers
func TestMain(m *testing.M) {
initLoggers()
os.Exit(m.Run())
}
// TestBotConfig_UnmarshalJSON tests the custom unmarshalling of BotConfig
func TestBotConfig_UnmarshalJSON(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
jsonData := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"temperature": 0.7,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
var config BotConfig
if err := json.Unmarshal([]byte(jsonData), &config); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
expectedModel := anthropic.Model("claude-v1")
if config.Model != expectedModel {
t.Errorf("Expected model %s, got %s", expectedModel, config.Model)
}
expectedID := "bot123"
if config.ID != expectedID {
t.Errorf("Expected ID %s, got %s", expectedID, config.ID)
}
// Add more field checks as necessary
}
// TestValidateConfigPath tests the validateConfigPath function
func TestValidateConfigPath(t *testing.T) {
execDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current directory: %v", err)
}
tests := []struct {
name string
configDir string
filename string
wantErr bool
}{
{
name: "Valid Path",
configDir: execDir,
filename: "config.json",
wantErr: false,
},
{
name: "Invalid Extension",
configDir: execDir,
filename: "config.yaml",
wantErr: true,
},
{
name: "Path Traversal",
configDir: execDir,
filename: "../config.json",
wantErr: true,
},
{
name: "Absolute Path Outside",
configDir: execDir,
filename: "/etc/passwd",
wantErr: true,
},
{
name: "Nested Valid Path",
configDir: execDir,
filename: "subdir/config.json",
wantErr: false,
},
}
// Create a subdirectory for testing
subDir := filepath.Join(execDir, "subdir")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
defer func() {
if err := os.RemoveAll(subDir); err != nil {
t.Errorf("Failed to remove test subdirectory: %v", err)
}
}()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configDir := tt.configDir
filename := tt.filename
if tt.name == "Nested Valid Path" {
configDir = subDir
}
_, err := validateConfigPath(configDir, filename)
if (err != nil) != tt.wantErr {
t.Errorf("validateConfigPath() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// TestLoadConfig tests the loadConfig function
func TestLoadConfig(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "config_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("Failed to remove temp directory: %v", err)
}
}()
// Valid config JSON
validConfig := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"temperature": 0.7,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
// Invalid config JSON
invalidConfig := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": "should be int",
"model": "claude-v1"
}`
// Write valid config file
validPath := filepath.Join(tempDir, "valid_config.json")
if err := os.WriteFile(validPath, []byte(validConfig), 0644); err != nil {
t.Fatalf("Failed to write valid config: %v", err)
}
// Write invalid config file
invalidPath := filepath.Join(tempDir, "invalid_config.json")
if err := os.WriteFile(invalidPath, []byte(invalidConfig), 0644); err != nil {
t.Fatalf("Failed to write invalid config: %v", err)
}
tests := []struct {
name string
filename string
wantErr bool
expectID string
expectErr string
}{
{
name: "Load Valid Config",
filename: validPath,
wantErr: false,
expectID: "bot123",
},
{
name: "Load Invalid Config",
filename: invalidPath,
wantErr: true,
expectErr: "failed to decode JSON",
},
{
name: "Non-existent File",
filename: filepath.Join(tempDir, "nonexistent.json"),
wantErr: true,
expectErr: "failed to open config file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := loadConfig(tt.filename)
if (err != nil) != tt.wantErr {
t.Errorf("loadConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != nil && tt.expectErr != "" {
if !contains(err.Error(), tt.expectErr) {
t.Errorf("loadConfig() error = %v, expected to contain %v", err, tt.expectErr)
}
return
}
if config.ID != tt.expectID {
t.Errorf("Expected ID %s, got %s", tt.expectID, config.ID)
}
})
}
}
// TestValidateConfig tests the validateConfig function
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
config BotConfig
ids map[string]bool
tokens map[string]bool
wantErr bool
expectedError string
}{
{
name: "Valid Config",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Model: "claude-v1",
Active: true,
OwnerTelegramID: 123456789,
MessagePerHour: 10,
MessagePerDay: 100,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: false,
},
{
name: "Missing ID",
config: BotConfig{
TelegramToken: "token123",
Model: "claude-v1",
Active: true,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: true,
expectedError: "missing 'id' field",
},
{
name: "Duplicate ID",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Model: "claude-v1",
Active: true,
},
ids: map[string]bool{"bot123": true},
tokens: make(map[string]bool),
wantErr: true,
expectedError: "duplicate bot id",
},
{
name: "Missing Telegram Token",
config: BotConfig{
ID: "bot123",
Model: "claude-v1",
Active: true,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: true,
expectedError: "missing 'telegram_token' field",
},
{
name: "Duplicate Telegram Token",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Model: "claude-v1",
Active: true,
},
ids: make(map[string]bool),
tokens: map[string]bool{"token123": true},
wantErr: true,
expectedError: "duplicate telegram_token",
},
{
name: "Missing Model",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Active: true,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: true,
expectedError: "missing 'model' field",
},
{
name: "Zero MessagePerHour",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Model: "claude-v1",
MessagePerHour: 0,
MessagePerDay: 100,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: true,
expectedError: "'messages_per_hour' must be greater than 0",
},
{
name: "Zero MessagePerDay",
config: BotConfig{
ID: "bot123",
TelegramToken: "token123",
Model: "claude-v1",
MessagePerHour: 10,
MessagePerDay: 0,
},
ids: make(map[string]bool),
tokens: make(map[string]bool),
wantErr: true,
expectedError: "'messages_per_day' must be greater than 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateConfig(&tt.config, tt.ids, tt.tokens)
if (err != nil) != tt.wantErr {
t.Errorf("validateConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != nil && tt.expectedError != "" {
if !contains(err.Error(), tt.expectedError) {
t.Errorf("validateConfig() error = %v, expected to contain %v", err, tt.expectedError)
}
}
})
}
}
// TestLoadAllConfigs tests the loadAllConfigs function
func TestLoadAllConfigs(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "load_all_configs_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("Failed to remove temp directory: %v", err)
}
}()
tests := []struct {
name string
setupFiles map[string]string // filename -> content
expectConfigs int
expectError bool
expectErrorMsg string
}{
{
name: "Load All Valid Configs",
setupFiles: map[string]string{
"valid_config.json": `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"temperature": 0.7,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`,
},
expectConfigs: 1,
expectError: false,
},
{
name: "Skip Inactive Config",
setupFiles: map[string]string{
"valid_config.json": `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`,
"inactive_config.json": `{
"id": "bot124",
"telegram_token": "token124",
"memory_size": 512,
"messages_per_hour": 5,
"messages_per_day": 50,
"temp_ban_duration": "30m",
"model": "claude-v2",
"temperature": 0.5,
"system_prompts": {"welcome": "Hi!"},
"active": false,
"owner_telegram_id": 987654321,
"anthropic_api_key": "api_key_124"
}`,
},
expectConfigs: 1,
expectError: false,
},
{
name: "Duplicate Bot ID",
setupFiles: map[string]string{
"valid_config.json": `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`,
"duplicate_id_config.json": `{
"id": "bot123",
"telegram_token": "token125",
"memory_size": 256,
"messages_per_hour": 2,
"messages_per_day": 20,
"temp_ban_duration": "15m",
"model": "claude-v3",
"temperature": 0.3,
"system_prompts": {"welcome": "Hey!"},
"active": true,
"owner_telegram_id": 1122334455,
"anthropic_api_key": "api_key_125"
}`,
},
expectConfigs: 1,
expectError: false,
},
{
name: "Duplicate Telegram Token",
setupFiles: map[string]string{
"valid_config.json": `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`,
"duplicate_token_config.json": `{
"id": "bot126",
"telegram_token": "token123",
"memory_size": 128,
"messages_per_hour": 1,
"messages_per_day": 10,
"temp_ban_duration": "5m",
"model": "claude-v4",
"temperature": 0.2,
"system_prompts": {"welcome": "Greetings!"},
"active": true,
"owner_telegram_id": 5566778899,
"anthropic_api_key": "api_key_126"
}`,
},
expectConfigs: 1,
expectError: false,
},
{
name: "Invalid Config",
setupFiles: map[string]string{
"valid_config.json": `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`,
"invalid_config.json": `{
"id": "bot127",
"telegram_token": "token127",
"model": "",
"active": true
}`,
},
expectConfigs: 1,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear the tempDir before each test
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("Failed to remove temp dir: %v", err)
}
if err := os.MkdirAll(tempDir, 0755); err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
// Write the test files directly
for filename, content := range tt.setupFiles {
err := os.WriteFile(filepath.Join(tempDir, filename), []byte(content), 0644)
if err != nil {
t.Fatalf("Failed to write file %s: %v", filename, err)
}
}
configs, err := loadAllConfigs(tempDir)
if (err != nil) != tt.expectError {
t.Errorf("loadAllConfigs() error = %v, wantErr %v", err, tt.expectError)
return
}
if len(configs) != tt.expectConfigs {
t.Errorf("Expected %d configs, got %d", tt.expectConfigs, len(configs))
}
})
}
}
// TestBotConfig_Reload tests the Reload method of BotConfig
func TestBotConfig_Reload(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "reload_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("Failed to remove temp directory: %v", err)
}
}()
// Create initial config file
config1 := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"temperature": 0.7,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
configPath := filepath.Join(tempDir, "config.json")
if err := os.WriteFile(configPath, []byte(config1), 0644); err != nil {
t.Fatalf("Failed to write initial config: %v", err)
}
// Initialize BotConfig
var config BotConfig
if err := config.Reload(tempDir, "config.json"); err != nil {
t.Fatalf("Failed to reload config: %v", err)
}
// Verify initial load
if config.ID != "bot123" {
t.Errorf("Expected ID 'bot123', got '%s'", config.ID)
}
if config.Model != "claude-v1" {
t.Errorf("Expected Model 'claude-v1', got '%s'", config.Model)
}
// Update config file
config2 := `{
"id": "bot123",
"telegram_token": "token123_updated",
"memory_size": 2048,
"messages_per_hour": 20,
"messages_per_day": 200,
"temp_ban_duration": "2h",
"model": "claude-v2",
"temperature": 0.3,
"system_prompts": {"welcome": "Hi there!"},
"active": true,
"owner_telegram_id": 987654321,
"anthropic_api_key": "api_key_456"
}`
if err := os.WriteFile(configPath, []byte(config2), 0644); err != nil {
t.Fatalf("Failed to write updated config: %v", err)
}
// Reload config
if err := config.Reload(tempDir, "config.json"); err != nil {
t.Fatalf("Failed to reload updated config: %v", err)
}
// Verify updated config
if config.TelegramToken != "token123_updated" {
t.Errorf("Expected TelegramToken 'token123_updated', got '%s'", config.TelegramToken)
}
if config.MemorySize != 2048 {
t.Errorf("Expected MemorySize 2048, got %d", config.MemorySize)
}
if config.Model != "claude-v2" {
t.Errorf("Expected Model 'claude-v2', got '%s'", config.Model)
}
if config.OwnerTelegramID != 987654321 {
t.Errorf("Expected OwnerTelegramID 987654321, got %d", config.OwnerTelegramID)
}
}
// TestBotConfig_UnmarshalJSON_Invalid tests unmarshalling with invalid model
func TestBotConfig_UnmarshalJSON_Invalid(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
jsonData := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "",
"temperature": 0.7,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
var config BotConfig
err := json.Unmarshal([]byte(jsonData), &config)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
if config.Model != "" {
t.Errorf("Expected empty model, got %s", config.Model)
}
}
// Helper function to check substring
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}
// TestTemperatureConfig tests that the temperature value is correctly loaded
func TestTemperatureConfig(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "temperature_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("Failed to remove temp directory: %v", err)
}
}()
// Create config with temperature
configWithTemp := `{
"id": "bot123",
"telegram_token": "token123",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"temperature": 0.42,
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
// Create config without temperature
configWithoutTemp := `{
"id": "bot124",
"telegram_token": "token124",
"memory_size": 1024,
"messages_per_hour": 10,
"messages_per_day": 100,
"temp_ban_duration": "1h",
"model": "claude-v1",
"system_prompts": {"welcome": "Hello!"},
"active": true,
"owner_telegram_id": 123456789,
"anthropic_api_key": "api_key_123"
}`
// Write config files
withTempPath := filepath.Join(tempDir, "with_temp.json")
if err := os.WriteFile(withTempPath, []byte(configWithTemp), 0644); err != nil {
t.Fatalf("Failed to write config with temperature: %v", err)
}
withoutTempPath := filepath.Join(tempDir, "without_temp.json")
if err := os.WriteFile(withoutTempPath, []byte(configWithoutTemp), 0644); err != nil {
t.Fatalf("Failed to write config without temperature: %v", err)
}
// Test loading config with temperature
configWithTempObj, err := loadConfig(withTempPath)
if err != nil {
t.Fatalf("Failed to load config with temperature: %v", err)
}
// Verify temperature is set correctly
if configWithTempObj.Temperature == nil {
t.Errorf("Expected Temperature to be set, got nil")
} else if *configWithTempObj.Temperature != 0.42 {
t.Errorf("Expected Temperature 0.42, got %f", *configWithTempObj.Temperature)
}
// Test loading config without temperature
configWithoutTempObj, err := loadConfig(withoutTempPath)
if err != nil {
t.Fatalf("Failed to load config without temperature: %v", err)
}
// Verify temperature is nil when not specified
if configWithoutTempObj.Temperature != nil {
t.Errorf("Expected Temperature to be nil, got %f", *configWithoutTempObj.Temperature)
}
}
// Additional tests can be added here to cover more scenarios
+26 -2
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"log" "log"
"os"
"time" "time"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
@@ -11,6 +12,10 @@ import (
) )
func initDB() (*gorm.DB, error) { func initDB() (*gorm.DB, error) {
if err := os.MkdirAll("data", 0750); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
newLogger := logger.New( newLogger := logger.New(
log.New(log.Writer(), "\r\n", log.LstdFlags), log.New(log.Writer(), "\r\n", log.LstdFlags),
logger.Config{ logger.Config{
@@ -20,18 +25,35 @@ func initDB() (*gorm.DB, error) {
}, },
) )
db, err := gorm.Open(sqlite.Open("bot.db"), &gorm.Config{ db, err := gorm.Open(sqlite.Open("data/bot.db?_journal_mode=WAL&_busy_timeout=5000"), &gorm.Config{
Logger: newLogger, Logger: newLogger,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err) return nil, fmt.Errorf("failed to connect to database: %w", err)
} }
err = db.AutoMigrate(&Message{}, &User{}, &Role{}) sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
sqlDB.SetMaxOpenConns(1)
// AutoMigrate the models
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to migrate database schema: %w", err) return nil, fmt.Errorf("failed to migrate database schema: %w", err)
} }
// Enforce unique owner per bot using raw SQL
// Note: SQLite doesn't support partial indexes, but we can simulate it by making a unique index on (BotID, IsOwner)
// and ensuring that IsOwner can only be true for one user per BotID.
// This approach allows multiple users with IsOwner=false for the same BotID,
// but only one user can have IsOwner=true per BotID.
err = db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_bot_owner ON users (bot_id, is_owner) WHERE is_owner = 1;`).Error
if err != nil {
return nil, fmt.Errorf("failed to create unique index for bot owners: %w", err)
}
err = createDefaultRoles(db) err = createDefaultRoles(db)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -45,8 +67,10 @@ func createDefaultRoles(db *gorm.DB) error {
for _, roleName := range roles { for _, roleName := range roles {
var role Role var role Role
if err := db.FirstOrCreate(&role, Role{Name: roleName}).Error; err != nil { if err := db.FirstOrCreate(&role, Role{Name: roleName}).Error; err != nil {
ErrorLogger.Printf("Failed to create default role %s: %v", roleName, err)
return fmt.Errorf("failed to create default role %s: %w", roleName, err) return fmt.Errorf("failed to create default role %s: %w", roleName, err)
} }
InfoLogger.Printf("Created or confirmed default role: %s", roleName)
} }
return nil return nil
} }
+39
View File
@@ -0,0 +1,39 @@
services:
telegram-bot:
image: bogerserge/go-telegram-bot:latest
build:
context: .
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64
container_name: go-telegram-bot
restart: unless-stopped
# Optional: Environment variables (can be overridden with .env file)
# environment:
# - BOT_LOG_LEVEL=info
# Volume mounts
volumes:
# Bind mount config directory for live configuration updates
- ./config:/app/config:ro
# Named volume for persistent database storage
- ./data:/app/data
# Optional: Bind mount for log access (uncomment if needed)
# - ./logs:/app/logs
# Health check
healthcheck:
test: ["CMD", "pgrep", "telegram-bot"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
+35
View File
@@ -0,0 +1,35 @@
[Unit]
# A concise description of the service
Description=Telegram Bot Service
# Postpone starting until network is available
After=network.target
[Service]
# The user that runs the bot
User=tibik
# The directory where the bot is located
WorkingDirectory=/home/tibik/go-telegram-bot
# The command to start the bot
ExecStart=/home/tibik/go-telegram-bot/telegram-bot
# Restart if crashed
Restart=always
# Delay between restarts to avoid resource exhaustion
RestartSec=5
# Capture stdout (INFO logs)
StandardOutput=journal
# Capture stderr (ERROR logs)
StandardError=journal
# Identifier for journalctl filtering
SyslogIdentifier=telegram-bot
[Install]
# The bot will start automatically at system boot
WantedBy=multi-user.target
# NOTE:
# New line comments: good
# Inline comments: no good, they mess up the service file
# View logs: journalctl -u telegram-bot
# Follow logs: journalctl -u telegram-bot -f
# View errors: journalctl -u telegram-bot -p err
+14 -10
View File
@@ -1,19 +1,23 @@
module github.com/HugeFrog24/thatsky-telegram-bot module github.com/HugeFrog24/go-telegram-bot
go 1.23.2 go 1.26.0
require ( require (
github.com/go-telegram/bot v1.8.4 github.com/go-telegram/bot v1.19.0
github.com/joho/godotenv v1.5.1 github.com/liushuangls/go-anthropic/v2 v2.17.1
github.com/liushuangls/go-anthropic/v2 v2.8.1 github.com/stretchr/testify v1.11.1
golang.org/x/time v0.7.0 golang.org/x/time v0.14.0
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.25.12 gorm.io/gorm v1.31.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
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.34 // indirect
golang.org/x/text v0.14.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+26 -16
View File
@@ -1,20 +1,30 @@
github.com/go-telegram/bot v1.8.4 h1:7viEUESakK29aiCumq6ui5jTPqJLLDeFubTsQzE07Kg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-telegram/bot v1.8.4/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram/bot v1.19.0 h1:tuvTQhgNietHFRN0HUDhuXsgfgkGSaO8WWwZQW3DMQg=
github.com/go-telegram/bot v1.19.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=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/liushuangls/go-anthropic/v2 v2.17.1 h1:ca3oFzgQHs9/mJr+xx2XFQIYcQLM2rDCqieUx0g+8p4=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/liushuangls/go-anthropic/v2 v2.17.1/go.mod h1:a550cJXPoTG2FL3DvfKG2zzD5O2vjgvo4tHtoGPzFLU=
github.com/liushuangls/go-anthropic/v2 v2.8.1 h1:pxFl88IgkG7e8Z1XwOYu48LcmEN0+6UdO58HF9altw0= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/liushuangls/go-anthropic/v2 v2.8.1/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+392 -38
View File
@@ -2,71 +2,425 @@ package main
import ( import (
"context" "context"
"log" "fmt"
"strconv"
"strings"
"github.com/go-telegram/bot" "github.com/go-telegram/bot"
"github.com/go-telegram/bot/models" "github.com/go-telegram/bot/models"
"github.com/liushuangls/go-anthropic/v2"
) )
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 // Extract businessConnectionID if available
userID := update.Message.From.ID var businessConnectionID string
if update.BusinessConnection != nil {
businessConnectionID = update.BusinessConnection.ID
} else if message.BusinessConnectionID != "" {
businessConnectionID = message.BusinessConnectionID
}
if !b.checkRateLimits(userID) { if message.From == nil {
b.sendRateLimitExceededMessage(ctx, chatID) // Channel posts and some automated messages have no sender — ignore them.
// see: https://core.telegram.org/bots/api#message
return return
} }
username := update.Message.From.Username chatID := message.Chat.ID
text := update.Message.Text userID := message.From.ID
username := message.From.Username
firstName := message.From.FirstName
lastName := message.From.LastName
languageCode := message.From.LanguageCode
isPremium := message.From.IsPremium
messageTime := message.Date
text := message.Text
user, err := b.getOrCreateUser(userID, username) // Check if it's a new chat
isNewChatFlag := b.isNewChat(chatID)
// Screen incoming message (store to DB + add to chat memory)
userMsg, err := b.screenIncomingMessage(message)
if err != nil { if err != nil {
log.Printf("Error getting or creating user: %v", err) ErrorLogger.Printf("Error storing user message: %v", err)
return return
} }
userMessage := b.createMessage(chatID, userID, username, user.Role.Name, text, true) // Determine if the user is the owner
b.storeMessage(userMessage) var isOwner bool
err = b.db.Where("telegram_id = ? AND bot_id = ? AND is_owner = ?", userID, b.botID, true).First(&User{}).Error
if err == nil {
isOwner = true
}
// Get the chat memory which now contains the user's message
chatMemory := b.getOrCreateChatMemory(chatID) chatMemory := b.getOrCreateChatMemory(chatID)
b.addMessageToChatMemory(chatMemory, userMessage)
contextMessages := b.prepareContextMessages(chatMemory) contextMessages := b.prepareContextMessages(chatMemory)
response, err := b.getAnthropicResponse(ctx, contextMessages, b.isNewChat(chatID), b.isAdminOrOwner(userID)) if isNewChatFlag {
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) // Get response from Anthropic using the context messages
response, err := b.getAnthropicResponse(ctx, contextMessages, true, isOwner, false, username, firstName, lastName, isPremium, languageCode, messageTime)
if err != nil {
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
// Use the same error message as in the non-new chat case
response = "I'm sorry, I'm having trouble processing your request right now."
}
assistantMessage := b.createMessage(chatID, 0, "Assistant", "assistant", response, false) // Send the AI-generated response or error message
b.storeMessage(assistantMessage) if err := b.sendResponse(ctx, chatID, response, businessConnectionID); err != nil {
b.addMessageToChatMemory(chatMemory, assistantMessage) ErrorLogger.Printf("Error sending response: %v", err)
} return
}
} else {
user, err := b.getOrCreateUser(userID, username, isOwner)
if err != nil {
ErrorLogger.Printf("Error getting or creating user: %v", err)
return
}
func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64) { // Update the username if it's empty or has changed
_, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{ if user.Username != username {
ChatID: chatID, user.Username = username
Text: "Rate limit exceeded. Please try again later.", if err := b.db.Save(&user).Error; err != nil {
}) ErrorLogger.Printf("Error updating user username: %v", err)
if err != nil { }
log.Printf("Error sending rate limit message: %v", err) }
// Check if the message is a command
if message.Entities != nil {
for _, entity := range message.Entities {
if entity.Type == "bot_command" {
command := strings.TrimSpace(message.Text[entity.Offset : entity.Offset+entity.Length])
switch command {
case "/stats":
// Parse command parameters
parts := strings.Fields(message.Text)
// Default: show global stats
if len(parts) == 1 {
b.sendStats(ctx, chatID, userID, 0, businessConnectionID)
return
}
// Check for "user" parameter
if len(parts) >= 2 && parts[1] == "user" {
targetUserID := userID // Default to current user
// If a user ID is provided, parse it
if len(parts) >= 3 {
var parseErr error
targetUserID, parseErr = strconv.ParseInt(parts[2], 10, 64)
if parseErr != nil {
InfoLogger.Printf("User %d provided invalid user ID format: %s", userID, parts[2])
if err := b.sendResponse(ctx, chatID, "Invalid user ID format. Usage: /stats user [user_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
b.sendStats(ctx, chatID, userID, targetUserID, businessConnectionID)
return
}
// Invalid parameter
if err := b.sendResponse(ctx, chatID, "Invalid command format. Usage: /stats or /stats user [user_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
case "/whoami":
b.sendWhoAmI(ctx, chatID, userID, username, businessConnectionID)
return
case "/clear":
parts := strings.Fields(message.Text)
var targetUserID, targetChatID int64
if len(parts) > 1 {
var parseErr error
targetUserID, parseErr = strconv.ParseInt(parts[1], 10, 64)
if parseErr != nil {
InfoLogger.Printf("User %d provided invalid user ID format: %s", userID, parts[1])
if err := b.sendResponse(ctx, chatID, "Invalid user ID format. Usage: /clear [user_id] [chat_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
if len(parts) > 2 {
var parseErr error
targetChatID, parseErr = strconv.ParseInt(parts[2], 10, 64)
if parseErr != nil {
InfoLogger.Printf("User %d provided invalid chat ID format: %s", userID, parts[2])
if err := b.sendResponse(ctx, chatID, "Invalid chat ID format. Usage: /clear [user_id] [chat_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
b.clearChatHistory(ctx, chatID, userID, targetUserID, targetChatID, businessConnectionID, false)
return
case "/clear_hard":
parts := strings.Fields(message.Text)
var targetUserID, targetChatID int64
if len(parts) > 1 {
var parseErr error
targetUserID, parseErr = strconv.ParseInt(parts[1], 10, 64)
if parseErr != nil {
InfoLogger.Printf("User %d provided invalid user ID format: %s", userID, parts[1])
if err := b.sendResponse(ctx, chatID, "Invalid user ID format. Usage: /clear_hard [user_id] [chat_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
if len(parts) > 2 {
var parseErr error
targetChatID, parseErr = strconv.ParseInt(parts[2], 10, 64)
if parseErr != nil {
InfoLogger.Printf("User %d provided invalid chat ID format: %s", userID, parts[2])
if err := b.sendResponse(ctx, chatID, "Invalid chat ID format. Usage: /clear_hard [user_id] [chat_id]", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
}
b.clearChatHistory(ctx, chatID, userID, targetUserID, targetChatID, businessConnectionID, true)
return
}
}
}
}
// Rate limit check applies to all message types including stickers
if !b.checkRateLimits(userID) {
b.sendRateLimitExceededMessage(ctx, chatID, businessConnectionID)
return
}
// Check if the message contains a sticker
if message.Sticker != nil {
b.handleStickerMessage(ctx, chatID, userMsg, message, businessConnectionID)
return
}
// Proceed only if the message contains text
if text == "" {
InfoLogger.Printf("Received a non-text message from user %d in chat %d", userID, chatID)
return
}
// Determine if the text contains only emojis
isEmojiOnly := isOnlyEmojis(text)
// Prepare context messages for Anthropic
chatMemory := b.getOrCreateChatMemory(chatID)
contextMessages := b.prepareContextMessages(chatMemory)
// Get response from Anthropic
response, err := b.getAnthropicResponse(ctx, contextMessages, false, isOwner, isEmojiOnly, username, firstName, lastName, isPremium, languageCode, messageTime) // isNewChat is false here
if err != nil {
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
response = "I'm sorry, I'm having trouble processing your request right now."
}
// Send the response
if err := b.sendResponse(ctx, chatID, response, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
return
}
} }
} }
func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string) { func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, businessConnectionID string) {
_, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{ if err := b.sendResponse(ctx, chatID, "Rate limit exceeded. Please try again later.", businessConnectionID); err != nil {
ChatID: chatID, ErrorLogger.Printf("Error sending rate limit exceeded message: %v", err)
Text: text, }
}) }
if err != nil {
log.Printf("Error sending message: %v", err) func (b *Bot) handleStickerMessage(ctx context.Context, chatID int64, userMessage Message, message *models.Message, businessConnectionID string) {
// userMessage was already screened (stored + added to memory) by handleUpdate — do not call screenIncomingMessage again.
// Generate AI response about the sticker
response, err := b.generateStickerResponse(ctx, userMessage)
if err != nil {
ErrorLogger.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!"
}
}
// Send the response
if err := b.sendResponse(ctx, chatID, response, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
return
}
}
func (b *Bot) generateStickerResponse(ctx context.Context, message Message) (string, error) {
// Example: Use the sticker type to generate a response
if message.StickerFileID != "" {
// Create message content with emoji information if available
var messageContent string
if message.StickerEmoji != "" {
messageContent = fmt.Sprintf("User sent a sticker: %s", message.StickerEmoji)
} else {
messageContent = "User sent a sticker."
}
// Prepare context with information about the sticker
contextMessages := []anthropic.Message{
{
Role: anthropic.RoleUser,
Content: []anthropic.MessageContent{
anthropic.NewTextMessageContent(messageContent),
},
},
}
// Treat sticker messages like emoji messages to get emoji responses
// Convert the timestamp to Unix time for the messageTime parameter
messageTime := int(message.Timestamp.Unix())
response, err := b.getAnthropicResponse(ctx, contextMessages, false, false, true, message.Username, "", "", false, "", messageTime)
if err != nil {
return "", err
}
return response, nil
}
return "Hmm, that's interesting!", nil
}
func (b *Bot) clearChatHistory(ctx context.Context, chatID int64, currentUserID int64, targetUserID int64, targetChatID int64, businessConnectionID string, hardDelete bool) {
// If targetUserID is provided and different from currentUserID, check permissions
if targetUserID != 0 && targetUserID != currentUserID {
// Check if the current user is an admin or owner
if !b.isAdminOrOwner(currentUserID) {
InfoLogger.Printf("User %d attempted to clear history for user %d without permission", currentUserID, targetUserID)
if err := b.sendResponse(ctx, chatID, "Permission denied. Only admins and owners can clear other users' histories.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
// Check if the target user exists
var targetUser User
err := b.db.Where("telegram_id = ? AND bot_id = ?", targetUserID, b.botID).First(&targetUser).Error
if err != nil {
ErrorLogger.Printf("Error finding target user %d: %v", targetUserID, err)
if err := b.sendResponse(ctx, chatID, fmt.Sprintf("User with ID %d not found.", targetUserID), businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
} else {
// If no targetUserID is provided, set it to currentUserID
targetUserID = currentUserID
}
// Delete messages from the database
//
// Assumption: this bot is primarily used in private DMs, where each user's messages
// are stored with chat_id == their own user_id — not the caller's chat_id. Scoping
// a cross-user delete by the caller's chatID would therefore match 0 rows.
//
// When clearing another user's history the default (targetChatID == 0) deletes all
// of that user's messages across every chat for this bot — the natural meaning of
// "/clear <userID>" (wipe their entire history with the bot).
//
// When targetChatID != 0 the deletion is scoped to that specific chat, which is
// useful for group moderation ("/clear <userID> <chatID>").
var err error
if hardDelete {
// Permanently delete messages
if targetUserID == currentUserID {
// Deleting own messages — scope to the current chat only.
err = b.db.Unscoped().Where("chat_id = ? AND bot_id = ? AND user_id = ?", chatID, b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("User %d permanently deleted their own chat history in chat %d", currentUserID, chatID)
} else {
// Deleting another user's messages — scope bot-wide by default; chat-scoped if targetChatID given (see above).
if targetChatID != 0 {
err = b.db.Unscoped().Where("chat_id = ? AND bot_id = ? AND user_id = ?", targetChatID, b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("Admin/owner %d permanently deleted chat history for user %d in chat %d", currentUserID, targetUserID, targetChatID)
} else {
err = b.db.Unscoped().Where("bot_id = ? AND user_id = ?", b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("Admin/owner %d permanently deleted all chat history for user %d", currentUserID, targetUserID)
}
}
} else {
// Soft delete messages
if targetUserID == currentUserID {
// Deleting own messages — scope to the current chat only.
err = b.db.Where("chat_id = ? AND bot_id = ? AND user_id = ?", chatID, b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("User %d soft deleted their own chat history in chat %d", currentUserID, chatID)
} else {
// Deleting another user's messages — scope bot-wide by default; chat-scoped if targetChatID given (see above).
if targetChatID != 0 {
err = b.db.Where("chat_id = ? AND bot_id = ? AND user_id = ?", targetChatID, b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("Admin/owner %d soft deleted chat history for user %d in chat %d", currentUserID, targetUserID, targetChatID)
} else {
err = b.db.Where("bot_id = ? AND user_id = ?", b.botID, targetUserID).Delete(&Message{}).Error
InfoLogger.Printf("Admin/owner %d soft deleted all chat history for user %d", currentUserID, targetUserID)
}
}
}
if err != nil {
ErrorLogger.Printf("Error clearing chat history: %v", err)
if err := b.sendResponse(ctx, chatID, "Sorry, I couldn't clear the chat history.", businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
}
return
}
// Evict the relevant in-memory cache entry so the next access rebuilds from
// the now-clean DB. Applies to all cases: own history, cross-user
// scoped to a specific chat, and bot-wide cross-user clear.
b.chatMemoriesMu.Lock()
if targetUserID == currentUserID {
// Own history is always scoped to the current chat.
delete(b.chatMemories, chatID)
} else if targetChatID != 0 {
// Admin cleared a specific chat — evict that chat's cache.
delete(b.chatMemories, targetChatID)
} else {
// Bot-wide clear: primary use-case is DMs where chatID == userID.
delete(b.chatMemories, targetUserID)
}
b.chatMemoriesMu.Unlock()
// Send a confirmation message
var confirmationMessage string
if targetUserID == currentUserID {
confirmationMessage = "Your chat history has been cleared."
} else {
// Get the username of the target user if available
var targetUser User
err := b.db.Where("telegram_id = ? AND bot_id = ?", targetUserID, b.botID).First(&targetUser).Error
if err == nil && targetUser.Username != "" {
confirmationMessage = fmt.Sprintf("Chat history for user @%s (ID: %d) has been cleared.", targetUser.Username, targetUserID)
} else {
confirmationMessage = fmt.Sprintf("Chat history for user with ID %d has been cleared.", targetUserID)
}
}
if err := b.sendResponse(ctx, chatID, confirmationMessage, businessConnectionID); err != nil {
ErrorLogger.Printf("Error sending response: %v", err)
} }
} }
+626
View File
@@ -0,0 +1,626 @@
package main
import (
"context"
"testing"
"time"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestHandleUpdate_NewChat(t *testing.T) {
// Setup
db := setupTestDB(t)
mockClock := &MockClock{
currentTime: time.Now(),
}
config := BotConfig{
ID: "test_bot",
OwnerTelegramID: 123, // owner's ID
TelegramToken: "test_token",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1h",
SystemPrompts: make(map[string]string),
Active: true,
}
mockTgClient := &MockTelegramClient{}
// Create bot model first
botModel := &BotModel{
Identifier: config.ID,
Name: config.ID,
}
err := db.Create(botModel).Error
assert.NoError(t, err)
// Create bot config
configModel := &ConfigModel{
BotID: botModel.ID,
MemorySize: config.MemorySize,
MessagePerHour: config.MessagePerHour,
MessagePerDay: config.MessagePerDay,
TempBanDuration: config.TempBanDuration,
SystemPrompts: "{}",
TelegramToken: config.TelegramToken,
Active: config.Active,
}
err = db.Create(configModel).Error
assert.NoError(t, err)
// Create bot instance
b, err := NewBot(db, config, mockClock, mockTgClient)
assert.NoError(t, err)
testCases := []struct {
name string
userID int64
isOwner bool
wantResp string
}{
{
name: "Owner First Message",
userID: 123, // owner's ID
isOwner: true,
wantResp: "I'm sorry, I'm having trouble processing your request right now.",
},
{
name: "Regular User First Message",
userID: 456,
isOwner: false,
wantResp: "I'm sorry, I'm having trouble processing your request right now.",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup mock response expectations for error case to test fallback messages
mockTgClient.SendMessageFunc = func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
assert.Equal(t, tc.userID, params.ChatID)
assert.Equal(t, tc.wantResp, params.Text)
return &models.Message{}, nil
}
// Create update with new message
update := &models.Update{
Message: &models.Message{
Chat: models.Chat{ID: tc.userID},
From: &models.User{
ID: tc.userID,
Username: "testuser",
},
Text: "Hello",
},
}
// Handle the update
b.handleUpdate(context.Background(), nil, update)
// Verify message was stored
var storedMsg Message
err := db.Where("chat_id = ? AND user_id = ? AND text = ?", tc.userID, tc.userID, "Hello").First(&storedMsg).Error
assert.NoError(t, err)
// Verify response was stored
var respMsg Message
err = db.Where("chat_id = ? AND is_user = ? AND text = ?", tc.userID, false, tc.wantResp).First(&respMsg).Error
assert.NoError(t, err)
})
}
}
func TestClearChatHistory(t *testing.T) {
// Setup
db := setupTestDB(t)
mockClock := &MockClock{
currentTime: time.Now(),
}
config := BotConfig{
ID: "test_bot",
OwnerTelegramID: 123, // owner's ID
TelegramToken: "test_token",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1h",
SystemPrompts: make(map[string]string),
Active: true,
}
mockTgClient := &MockTelegramClient{}
// Create bot model first
botModel := &BotModel{
Identifier: config.ID,
Name: config.ID,
}
err := db.Create(botModel).Error
assert.NoError(t, err)
// Create bot config
configModel := &ConfigModel{
BotID: botModel.ID,
MemorySize: config.MemorySize,
MessagePerHour: config.MessagePerHour,
MessagePerDay: config.MessagePerDay,
TempBanDuration: config.TempBanDuration,
SystemPrompts: "{}",
TelegramToken: config.TelegramToken,
Active: config.Active,
}
err = db.Create(configModel).Error
assert.NoError(t, err)
// Create bot instance
b, err := NewBot(db, config, mockClock, mockTgClient)
assert.NoError(t, err)
// Create test users
ownerID := int64(123)
adminID := int64(456)
regularUserID := int64(789)
nonExistentUserID := int64(999)
chatID := int64(1000)
// Create admin role
adminRole, err := b.getRoleByName("admin")
assert.NoError(t, err)
// Create admin user
adminUser := User{
BotID: b.botID,
TelegramID: adminID,
Username: "admin",
RoleID: adminRole.ID,
Role: adminRole,
IsOwner: false,
}
err = db.Create(&adminUser).Error
assert.NoError(t, err)
// Create regular user
regularRole, err := b.getRoleByName("user")
assert.NoError(t, err)
regularUser := User{
BotID: b.botID,
TelegramID: regularUserID,
Username: "regular",
RoleID: regularRole.ID,
Role: regularRole,
IsOwner: false,
}
err = db.Create(&regularUser).Error
assert.NoError(t, err)
// Create test messages for each user.
// Each user's messages are stored with chat_id == their own user_id, mirroring
// how Telegram private DMs work (chat_id == user_id in 1-on-1 bot conversations).
// Using a shared artificial chatID here would mask the cross-user delete bug.
for _, userID := range []int64{ownerID, adminID, regularUserID} {
for i := 0; i < 5; i++ {
message := Message{
BotID: b.botID,
ChatID: userID, // per-user chat, not a shared chatID
UserID: userID,
Username: "test",
UserRole: "user",
Text: "Test message",
Timestamp: time.Now(),
IsUser: true,
}
err = db.Create(&message).Error
assert.NoError(t, err)
}
}
// Test cases
testCases := []struct {
name string
currentUserID int64
targetUserID int64
hardDelete bool
expectedError bool
expectedCount int64
expectedMsg string
targetChatID int64
businessConnID string
}{
{
name: "Owner clears own history",
currentUserID: ownerID,
targetUserID: ownerID,
hardDelete: false,
expectedError: false,
expectedCount: 0,
expectedMsg: "Your chat history has been cleared.",
},
{
name: "Admin clears own history",
currentUserID: adminID,
targetUserID: adminID,
hardDelete: false,
expectedError: false,
expectedCount: 0,
expectedMsg: "Your chat history has been cleared.",
},
{
name: "Regular user clears own history",
currentUserID: regularUserID,
targetUserID: regularUserID,
hardDelete: false,
expectedError: false,
expectedCount: 0,
expectedMsg: "Your chat history has been cleared.",
},
{
name: "Owner clears admin's history",
currentUserID: ownerID,
targetUserID: adminID,
hardDelete: false,
expectedError: false,
expectedCount: 0,
expectedMsg: "Chat history for user @admin (ID: 456) has been cleared.",
},
{
name: "Admin clears regular user's history",
currentUserID: adminID,
targetUserID: regularUserID,
hardDelete: false,
expectedError: false,
expectedCount: 0,
expectedMsg: "Chat history for user @regular (ID: 789) has been cleared.",
},
{
name: "Regular user attempts to clear admin's history",
currentUserID: regularUserID,
targetUserID: adminID,
hardDelete: false,
expectedError: true,
expectedCount: 5, // Messages should remain
expectedMsg: "Permission denied. Only admins and owners can clear other users' histories.",
},
{
name: "Admin attempts to clear non-existent user's history",
currentUserID: adminID,
targetUserID: nonExistentUserID,
hardDelete: false,
expectedError: true,
expectedCount: 5, // Messages should remain for admin
expectedMsg: "User with ID 999 not found.",
},
{
name: "Owner hard deletes regular user's history",
currentUserID: ownerID,
targetUserID: regularUserID,
hardDelete: true,
expectedError: false,
expectedCount: 0,
expectedMsg: "Chat history for user @regular (ID: 789) has been cleared.",
},
{
// targetChatID scopes the delete to a specific chat; messages in other chats survive.
// We seed messages with ChatID == userID (per-user DM), so targeting a different chatID
// should leave the user's messages untouched (expectedCount == 5).
name: "Admin clears regular user's history scoped to non-matching chat",
currentUserID: adminID,
targetUserID: regularUserID,
targetChatID: int64(9999), // a chat the user has no messages in
hardDelete: false,
expectedError: false,
expectedCount: 5, // messages in chat 789 are unaffected
expectedMsg: "Chat history for user @regular (ID: 789) has been cleared.",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Reset messages for the test case
if tc.name != "Owner hard deletes regular user's history" {
// Delete all messages for the target user
err = db.Where("user_id = ?", tc.targetUserID).Delete(&Message{}).Error
assert.NoError(t, err)
// Recreate messages for the target user
for i := 0; i < 5; i++ {
message := Message{
BotID: b.botID,
ChatID: chatID,
UserID: tc.targetUserID,
Username: "test",
UserRole: "user",
Text: "Test message",
Timestamp: time.Now(),
IsUser: true,
}
err = db.Create(&message).Error
assert.NoError(t, err)
}
}
// Setup mock response expectations
var sentMessage string
mockTgClient.SendMessageFunc = func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
sentMessage = params.Text
return &models.Message{}, nil
}
// Call the clearChatHistory method
b.clearChatHistory(context.Background(), chatID, tc.currentUserID, tc.targetUserID, tc.targetChatID, tc.businessConnID, tc.hardDelete)
// Verify the response message
assert.Equal(t, tc.expectedMsg, sentMessage)
// Count remaining messages for the target user
var count int64
if tc.hardDelete {
db.Unscoped().Model(&Message{}).Where("user_id = ? AND chat_id = ?", tc.targetUserID, chatID).Count(&count)
} else {
db.Model(&Message{}).Where("user_id = ? AND chat_id = ?", tc.targetUserID, chatID).Count(&count)
}
assert.Equal(t, tc.expectedCount, count)
})
}
}
func TestStatsCommand(t *testing.T) {
// Setup
db := setupTestDB(t)
mockClock := &MockClock{
currentTime: time.Now(),
}
config := BotConfig{
ID: "test_bot",
OwnerTelegramID: 123, // owner's ID
TelegramToken: "test_token",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1h",
SystemPrompts: make(map[string]string),
Active: true,
}
mockTgClient := &MockTelegramClient{}
// Create bot model first
botModel := &BotModel{
Identifier: config.ID,
Name: config.ID,
}
err := db.Create(botModel).Error
assert.NoError(t, err)
// Create bot config
configModel := &ConfigModel{
BotID: botModel.ID,
MemorySize: config.MemorySize,
MessagePerHour: config.MessagePerHour,
MessagePerDay: config.MessagePerDay,
TempBanDuration: config.TempBanDuration,
SystemPrompts: "{}",
TelegramToken: config.TelegramToken,
Active: config.Active,
}
err = db.Create(configModel).Error
assert.NoError(t, err)
// Create bot instance
b, err := NewBot(db, config, mockClock, mockTgClient)
assert.NoError(t, err)
// Create test users
ownerID := int64(123)
adminID := int64(456)
regularUserID := int64(789)
chatID := int64(1000)
// Create admin role
adminRole, err := b.getRoleByName("admin")
assert.NoError(t, err)
// Create admin user
adminUser := User{
BotID: b.botID,
TelegramID: adminID,
Username: "admin",
RoleID: adminRole.ID,
Role: adminRole,
IsOwner: false,
}
err = db.Create(&adminUser).Error
assert.NoError(t, err)
// Create regular user
regularRole, err := b.getRoleByName("user")
assert.NoError(t, err)
regularUser := User{
BotID: b.botID,
TelegramID: regularUserID,
Username: "regular",
RoleID: regularRole.ID,
Role: regularRole,
IsOwner: false,
}
err = db.Create(&regularUser).Error
assert.NoError(t, err)
// Create test messages for each user
for _, userID := range []int64{ownerID, adminID, regularUserID} {
for i := 0; i < 5; i++ {
// User message
userMessage := Message{
BotID: b.botID,
ChatID: chatID,
UserID: userID,
Username: "test",
UserRole: "user",
Text: "Test message",
Timestamp: time.Now(),
IsUser: true,
}
err = db.Create(&userMessage).Error
assert.NoError(t, err)
// Bot response
botMessage := Message{
BotID: b.botID,
ChatID: chatID,
UserID: 0,
Username: "AI Assistant",
UserRole: "assistant",
Text: "Test response",
Timestamp: time.Now(),
IsUser: false,
}
err = db.Create(&botMessage).Error
assert.NoError(t, err)
}
}
// Test cases
testCases := []struct {
name string
command string
currentUserID int64
expectedError bool
expectedMsg string
businessConnID string
}{
{
name: "Global stats",
command: "/stats",
currentUserID: regularUserID,
expectedError: false,
expectedMsg: "📊 Bot Statistics:",
},
{
name: "User requests own stats",
command: "/stats user",
currentUserID: regularUserID,
expectedError: false,
expectedMsg: "👤 User Statistics for @regular (ID: 789):",
},
{
name: "Admin requests another user's stats",
command: "/stats user 789",
currentUserID: adminID,
expectedError: false,
expectedMsg: "👤 User Statistics for @regular (ID: 789):",
},
{
name: "Owner requests another user's stats",
command: "/stats user 456",
currentUserID: ownerID,
expectedError: false,
expectedMsg: "👤 User Statistics for @admin (ID: 456):",
},
{
name: "Regular user attempts to request another user's stats",
command: "/stats user 456",
currentUserID: regularUserID,
expectedError: true,
expectedMsg: "Permission denied. Only admins and owners can view other users' statistics.",
},
{
name: "User provides invalid user ID format",
command: "/stats user abc",
currentUserID: adminID,
expectedError: true,
expectedMsg: "Invalid user ID format. Usage: /stats user [user_id]",
},
{
name: "User provides invalid command format",
command: "/stats invalid",
currentUserID: adminID,
expectedError: true,
expectedMsg: "Invalid command format. Usage: /stats or /stats user [user_id]",
},
{
name: "User requests non-existent user's stats",
command: "/stats user 999",
currentUserID: adminID,
expectedError: true,
expectedMsg: "Sorry, I couldn't retrieve statistics for user ID 999.",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup mock response expectations
var sentMessage string
mockTgClient.SendMessageFunc = func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
sentMessage = params.Text
return &models.Message{}, nil
}
// Create update with command
update := &models.Update{
Message: &models.Message{
Chat: models.Chat{ID: chatID},
From: &models.User{
ID: tc.currentUserID,
Username: getUsernameByID(tc.currentUserID),
},
Text: tc.command,
Entities: []models.MessageEntity{
{
Type: "bot_command",
Offset: 0,
Length: 6, // Length of "/stats"
},
},
},
}
// Handle the update
b.handleUpdate(context.Background(), nil, update)
// Verify the response message contains the expected text
assert.Contains(t, sentMessage, tc.expectedMsg)
})
}
}
// Helper function to get username by ID for test
func getUsernameByID(id int64) string {
switch id {
case 123:
return "owner"
case 456:
return "admin"
case 789:
return "regular"
default:
return "unknown"
}
}
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
// AutoMigrate the models
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{})
if err != nil {
t.Fatalf("Failed to migrate database schema: %v", err)
}
// Create default roles
err = createDefaultRoles(db)
if err != nil {
t.Fatalf("Failed to create default roles: %v", err)
}
return db
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"log"
"os"
)
// For log management, use journalctl commands:
// - View logs: journalctl -u telegram-bot
// - Follow logs: journalctl -u telegram-bot -f
// - View errors: journalctl -u telegram-bot -p err
// Refer to the documentation for details on systemd unit setup.
// Initialize loggers for informational and error messages.
var (
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
// initLoggers sets up separate loggers for stdout and stderr.
func initLoggers() {
// InfoLogger writes to stdout with specific flags.
InfoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
// ErrorLogger writes to stderr with specific flags.
ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
+36 -45
View File
@@ -2,72 +2,63 @@ package main
import ( import (
"context" "context"
"io"
"log"
"os" "os"
"os/signal" "os/signal"
"sync"
"github.com/joho/godotenv"
) )
func main() { func main() {
// Initialize logger // Initialize custom loggers
logFile, err := initLogger() initLoggers()
if err != nil {
log.Fatalf("Error initializing logger: %v", err)
}
defer logFile.Close()
// Load environment variables // Log the start of the application
if err := godotenv.Load(); err != nil { InfoLogger.Println("Starting Telegram Bot Application")
log.Printf("Error loading .env file: %v", err)
}
// Check for required environment variables
checkRequiredEnvVars()
// Initialize database // Initialize database
db, err := initDB() db, err := initDB()
if err != nil { if err != nil {
log.Fatalf("Error initializing database: %v", err) ErrorLogger.Fatalf("Error initializing database: %v", err)
} }
// Load configuration // Load all bot configurations
config, err := loadConfig("config.json") configs, err := loadAllConfigs("config")
if err != nil { if err != nil {
log.Fatalf("Error loading configuration: %v", err) ErrorLogger.Fatalf("Error loading configurations: %v", err)
} }
// Create Bot instance // Create a WaitGroup to manage goroutines
b, err := NewBot(db, config) var wg sync.WaitGroup
if err != nil {
log.Fatalf("Error creating bot: %v", err)
}
// Set up context with cancellation // Set up context with cancellation
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel() defer cancel()
// Start the bot // Initialize and start each bot
log.Println("Starting bot...") for _, config := range configs {
b.Start(ctx) wg.Add(1)
} go func(cfg BotConfig) {
defer wg.Done()
func initLogger() (*os.File, error) { // Create Bot instance without TelegramClient initially
logFile, err := os.OpenFile("bot.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) realClock := RealClock{}
if err != nil { bot, err := NewBot(db, cfg, realClock, nil)
return nil, err if err != nil {
} ErrorLogger.Printf("Error creating bot %s: %v", cfg.ID, err)
mw := io.MultiWriter(os.Stdout, logFile) return
log.SetOutput(mw) }
return logFile, nil
}
func checkRequiredEnvVars() { // Start the bot in a separate goroutine
requiredEnvVars := []string{"TELEGRAM_BOT_TOKEN", "ANTHROPIC_API_KEY"} go bot.Start(ctx)
for _, envVar := range requiredEnvVars {
if os.Getenv(envVar) == "" { // Keep the bot running until the context is cancelled
log.Fatalf("%s environment variable is not set", envVar) <-ctx.Done()
}
InfoLogger.Printf("Bot %s stopped", cfg.ID)
}(config)
} }
// Wait for all bots to finish
wg.Wait()
InfoLogger.Println("All bots have stopped. Exiting application.")
} }
+46 -10
View File
@@ -6,20 +6,48 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type BotModel struct {
gorm.Model
Identifier string `gorm:"uniqueIndex"` // Renamed from ID to Identifier
Name string
Configs []ConfigModel `gorm:"foreignKey:BotID;constraint:OnDelete:CASCADE"`
Users []User `gorm:"foreignKey:BotID;constraint:OnDelete:CASCADE"` // Associated users
Messages []Message `gorm:"foreignKey:BotID;constraint:OnDelete:CASCADE"`
}
type ConfigModel struct {
gorm.Model
BotID uint `gorm:"index"`
MemorySize int `json:"memory_size"`
MessagePerHour int `json:"messages_per_hour"`
MessagePerDay int `json:"messages_per_day"`
TempBanDuration string `json:"temp_ban_duration"`
SystemPrompts string `json:"system_prompts"` // Consider JSON string or separate table
TelegramToken string `json:"telegram_token"`
Active bool `json:"active"`
}
type Message struct { type Message struct {
gorm.Model gorm.Model
ChatID int64 BotID uint `gorm:"index"`
UserID int64 ChatID int64 `gorm:"index"`
Username string UserID int64 `gorm:"index"`
UserRole string Username string `gorm:"index"`
Text string UserRole string // Store the role as a string
Timestamp time.Time Text string `gorm:"type:text"`
IsUser bool Timestamp time.Time `gorm:"index"`
IsUser bool
StickerFileID string
StickerPNGFile string
StickerEmoji string // Store the emoji associated with the sticker
DeletedAt gorm.DeletedAt `gorm:"index"` // Add soft delete field
AnsweredOn *time.Time `gorm:"index"` // Tracks when a user message was answered (NULL for assistant messages and unanswered user messages)
} }
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 {
@@ -29,8 +57,16 @@ type Role struct {
type User struct { type User struct {
gorm.Model gorm.Model
TelegramID int64 `gorm:"uniqueIndex"` BotID uint `gorm:"uniqueIndex:idx_user_bot;index"` // Foreign key to BotModel
TelegramID int64 `gorm:"uniqueIndex:idx_user_bot;not null"` // Unique per (telegram_id, bot_id) pair
Username string Username string
RoleID uint RoleID uint
Role Role `gorm:"foreignKey:RoleID"` Role Role `gorm:"foreignKey:RoleID"`
IsOwner bool `gorm:"default:false"` // Indicates if the user is the owner
}
// idx_user_bot is a composite unique index on (bot_id, telegram_id),
// allowing the same Telegram user to be registered independently on each bot.
func (User) TableName() string {
return "users"
} }
+36 -13
View File
@@ -7,10 +7,12 @@ import (
) )
type userLimiter struct { type userLimiter struct {
hourlyLimiter *rate.Limiter hourlyLimiter *rate.Limiter
dailyLimiter *rate.Limiter dailyLimiter *rate.Limiter
lastReset time.Time lastHourlyReset time.Time
banUntil time.Time lastDailyReset time.Time
banUntil time.Time
clock Clock
} }
func (b *Bot) checkRateLimits(userID int64) bool { func (b *Bot) checkRateLimits(userID int64) bool {
@@ -20,26 +22,47 @@ func (b *Bot) checkRateLimits(userID int64) bool {
limiter, exists := b.userLimiters[userID] limiter, exists := b.userLimiters[userID]
if !exists { if !exists {
limiter = &userLimiter{ limiter = &userLimiter{
hourlyLimiter: rate.NewLimiter(rate.Every(time.Hour/time.Duration(b.config.MessagePerHour)), b.config.MessagePerHour), hourlyLimiter: rate.NewLimiter(rate.Every(time.Hour/time.Duration(b.config.MessagePerHour)), b.config.MessagePerHour),
dailyLimiter: rate.NewLimiter(rate.Every(24*time.Hour/time.Duration(b.config.MessagePerDay)), b.config.MessagePerDay), dailyLimiter: rate.NewLimiter(rate.Every(24*time.Hour/time.Duration(b.config.MessagePerDay)), b.config.MessagePerDay),
lastReset: time.Now(), lastHourlyReset: b.clock.Now(),
lastDailyReset: b.clock.Now(),
clock: b.clock,
} }
b.userLimiters[userID] = limiter b.userLimiters[userID] = limiter
} }
now := time.Now() now := limiter.clock.Now()
// Check if the user is currently banned
if now.Before(limiter.banUntil) { if now.Before(limiter.banUntil) {
return false return false
} }
if now.Sub(limiter.lastReset) >= 24*time.Hour { // Reset hourly limiter if an hour has passed since the last reset
limiter.dailyLimiter = rate.NewLimiter(rate.Every(24*time.Hour/time.Duration(b.config.MessagePerDay)), b.config.MessagePerDay) if now.Sub(limiter.lastHourlyReset) >= time.Hour {
limiter.lastReset = now limiter.hourlyLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(b.config.MessagePerHour)), b.config.MessagePerHour)
limiter.lastHourlyReset = now
} }
if !limiter.hourlyLimiter.Allow() || !limiter.dailyLimiter.Allow() { // Reset daily limiter if 24 hours have passed since the last reset
banDuration, _ := time.ParseDuration(b.config.TempBanDuration) if now.Sub(limiter.lastDailyReset) >= 24*time.Hour {
limiter.dailyLimiter = rate.NewLimiter(rate.Every(24*time.Hour/time.Duration(b.config.MessagePerDay)), b.config.MessagePerDay)
limiter.lastDailyReset = now
}
// Check if the message exceeds rate limits.
// Reserve from both limiters first, then cancel both if either is over budget.
// This prevents consuming a token from one limiter when the other rejects.
dailyRes := limiter.dailyLimiter.ReserveN(now, 1)
hourlyRes := limiter.hourlyLimiter.ReserveN(now, 1)
if dailyRes.DelayFrom(now) > 0 || hourlyRes.DelayFrom(now) > 0 {
dailyRes.CancelAt(now)
hourlyRes.CancelAt(now)
banDuration, err := time.ParseDuration(b.config.TempBanDuration)
if err != nil {
// If parsing fails, default to a 24-hour ban
banDuration = 24 * time.Hour
}
limiter.banUntil = now.Add(banDuration) limiter.banUntil = now.Add(banDuration)
return false return false
} }
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"testing"
"time"
)
// TestCheckRateLimits tests the checkRateLimits method of the Bot.
// It verifies that users are allowed or denied based on their message rates.
func TestCheckRateLimits(t *testing.T) {
// Create a mock clock starting at a fixed time
mockClock := &MockClock{
currentTime: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC),
}
// Create a mock configuration with reduced timeframes for testing
config := BotConfig{
ID: "bot1",
MemorySize: 10,
MessagePerHour: 5, // Allow 5 messages per hour
MessagePerDay: 10, // Allow 10 messages per day
TempBanDuration: "1m", // Temporary ban duration of 1 minute for testing
SystemPrompts: make(map[string]string),
TelegramToken: "YOUR_TELEGRAM_BOT_TOKEN",
OwnerTelegramID: 123456789,
}
// Initialize the Bot with mock data and MockClock
bot := &Bot{
config: config,
userLimiters: make(map[int64]*userLimiter),
clock: mockClock,
}
userID := int64(12345)
// Helper function to simulate message sending
sendMessage := func() bool {
return bot.checkRateLimits(userID)
}
// Send 5 messages within the hourly limit
for i := 0; i < config.MessagePerHour; i++ {
if !sendMessage() {
t.Errorf("Expected message %d to be allowed", i+1)
}
}
// 6th message should exceed the hourly limit and trigger a ban
if sendMessage() {
t.Errorf("Expected message to be denied due to hourly limit exceeded")
}
// Attempt to send another message immediately, should still be banned
if sendMessage() {
t.Errorf("Expected message to be denied while user is banned")
}
// Fast-forward time by TempBanDuration to lift the ban
mockClock.Advance(time.Minute) // Banned for 1 minute
// Advance time to allow hourly limiter to replenish
mockClock.Advance(time.Hour) // Advance by 1 hour
// Send another message, should be allowed now
if !sendMessage() {
t.Errorf("Expected message to be allowed after ban duration")
}
// Send additional messages to reach the daily limit
for i := 0; i < config.MessagePerDay-config.MessagePerHour-1; i++ {
if !sendMessage() {
t.Errorf("Expected message %d to be allowed towards daily limit", i+1)
}
}
// Attempt to exceed the daily limit
if sendMessage() {
t.Errorf("Expected message to be denied due to daily limit exceeded")
}
}
// To ensure thread safety and avoid race conditions during testing,
// you can run the tests with the `-race` flag:
// go test -race -v
+16
View File
@@ -0,0 +1,16 @@
// telegram_client.go
package main
import (
"context"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
)
// TelegramClient defines the methods required from the Telegram bot.
type TelegramClient interface {
SendMessage(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error)
Start(ctx context.Context)
// Add other methods if needed.
}
+38
View File
@@ -0,0 +1,38 @@
// telegram_client_mock.go
package main
import (
"context"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/stretchr/testify/mock"
)
// MockTelegramClient is a mock implementation of TelegramClient for testing.
type MockTelegramClient struct {
mock.Mock
SendMessageFunc func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error)
StartFunc func(ctx context.Context)
}
// SendMessage mocks sending a message.
func (m *MockTelegramClient) SendMessage(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
if m.SendMessageFunc != nil {
return m.SendMessageFunc(ctx, params)
}
args := m.Called(ctx, params)
if msg, ok := args.Get(0).(*models.Message); ok {
return msg, args.Error(1)
}
return nil, args.Error(1)
}
// Start mocks starting the Telegram client.
func (m *MockTelegramClient) Start(ctx context.Context) {
if m.StartFunc != nil {
m.StartFunc(ctx)
return
}
m.Called(ctx)
}
+287
View File
@@ -0,0 +1,287 @@
package main
import (
"context"
"fmt"
"testing"
"time"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestOwnerAssignment(t *testing.T) {
// Initialize loggers
initLoggers()
// Initialize in-memory database for testing
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to open in-memory database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{})
if err != nil {
t.Fatalf("Failed to migrate database schema: %v", err)
}
// Create default roles
err = createDefaultRoles(db)
if err != nil {
t.Fatalf("Failed to create default roles: %v", err)
}
// Create a bot configuration
config := BotConfig{
ID: "test_bot",
TelegramToken: "TEST_TELEGRAM_TOKEN",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1m",
SystemPrompts: make(map[string]string),
Active: true,
OwnerTelegramID: 111111111,
}
// Initialize MockClock
mockClock := &MockClock{
currentTime: time.Now(),
}
// Initialize MockTelegramClient
mockTGClient := &MockTelegramClient{
SendMessageFunc: func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
chatID, ok := params.ChatID.(int64)
if !ok {
return nil, fmt.Errorf("ChatID is not of type int64")
}
// Simulate successful message sending
return &models.Message{ID: 1, Chat: models.Chat{ID: chatID}}, nil
},
}
// Create the bot with the mock Telegram client
bot, err := NewBot(db, config, mockClock, mockTGClient)
if err != nil {
t.Fatalf("Failed to create bot: %v", err)
}
// Verify that the owner exists
var owner User
err = db.Where("telegram_id = ? AND bot_id = ? AND is_owner = ?", config.OwnerTelegramID, bot.botID, true).First(&owner).Error
if err != nil {
t.Fatalf("Owner was not created: %v", err)
}
// Attempt to create another owner for the same bot
_, err = bot.getOrCreateUser(222222222, "AnotherOwner", true)
if err == nil {
t.Fatalf("Expected error when creating a second owner, but got none")
}
// Verify that the error message is appropriate
expectedErrorMsg := "an owner already exists for this bot"
if err.Error() != expectedErrorMsg {
t.Fatalf("Unexpected error message: %v", err)
}
// Assign admin role to a new user
regularUser, err := bot.getOrCreateUser(333333333, "RegularUser", false)
if err != nil {
t.Fatalf("Failed to create regular user: %v", err)
}
if regularUser.Role.Name != "user" {
t.Fatalf("Expected role 'user', got '%s'", regularUser.Role.Name)
}
// Attempt to change an existing user to owner
_, err = bot.getOrCreateUser(333333333, "AdminUser", true)
if err == nil {
t.Fatalf("Expected error when changing existing user to owner, but got none")
}
expectedErrorMsg = "cannot change existing user to owner"
if err.Error() != expectedErrorMsg {
t.Fatalf("Unexpected error message: %v", err)
}
// If you need to test admin creation, you should do it through a separate admin creation function
// or by updating an existing user's role with proper authorization checks
}
func TestPromoteUserToAdmin(t *testing.T) {
// Initialize loggers
initLoggers()
// Initialize in-memory database for testing
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to open in-memory database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{})
if err != nil {
t.Fatalf("Failed to migrate database schema: %v", err)
}
// Create default roles
err = createDefaultRoles(db)
if err != nil {
t.Fatalf("Failed to create default roles: %v", err)
}
config := BotConfig{
ID: "test_bot",
TelegramToken: "TEST_TELEGRAM_TOKEN",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1m",
SystemPrompts: make(map[string]string),
Active: true,
OwnerTelegramID: 111111111,
}
mockClock := &MockClock{currentTime: time.Now()}
mockTGClient := &MockTelegramClient{}
bot, err := NewBot(db, config, mockClock, mockTGClient)
if err != nil {
t.Fatalf("Failed to create bot: %v", err)
}
// Create an owner
owner, err := bot.getOrCreateUser(config.OwnerTelegramID, "OwnerUser", true)
if err != nil {
t.Fatalf("Failed to create owner: %v", err)
}
// Test promoting a user to admin
regularUser, err := bot.getOrCreateUser(444444444, "RegularUser", false)
if err != nil {
t.Fatalf("Failed to create regular user: %v", err)
}
err = bot.promoteUserToAdmin(owner.TelegramID, regularUser.TelegramID)
if err != nil {
t.Fatalf("Failed to promote user to admin: %v", err)
}
// Refresh user data
promotedUser, err := bot.getOrCreateUser(444444444, "RegularUser", false)
if err != nil {
t.Fatalf("Failed to get promoted user: %v", err)
}
if promotedUser.Role.Name != "admin" {
t.Fatalf("Expected role 'admin', got '%s'", promotedUser.Role.Name)
}
}
// TestGetOrCreateUser tests the getOrCreateUser method of the Bot.
// It verifies that a new user is created when one does not exist,
// and an existing user is returned when one does exist.
func TestGetOrCreateUser(t *testing.T) {
// Initialize loggers
initLoggers()
// Initialize in-memory database for testing
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to open in-memory database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{})
if err != nil {
t.Fatalf("Failed to migrate database schema: %v", err)
}
// Create default roles
err = createDefaultRoles(db)
if err != nil {
t.Fatalf("Failed to create default roles: %v", err)
}
// Create a mock clock starting at a fixed time
mockClock := &MockClock{
currentTime: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC),
}
// Create a mock configuration
config := BotConfig{
ID: "bot1",
MemorySize: 10,
MessagePerHour: 5,
MessagePerDay: 10,
TempBanDuration: "1m",
SystemPrompts: make(map[string]string),
TelegramToken: "YOUR_TELEGRAM_BOT_TOKEN",
OwnerTelegramID: 123456789,
}
// Initialize MockTelegramClient
mockTGClient := &MockTelegramClient{
SendMessageFunc: func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
chatID, ok := params.ChatID.(int64)
if !ok {
return nil, fmt.Errorf("ChatID is not of type int64")
}
// Simulate successful message sending
return &models.Message{ID: 1, Chat: models.Chat{ID: chatID}}, nil
},
}
// Create the bot with the mock Telegram client
bot, err := NewBot(db, config, mockClock, mockTGClient)
if err != nil {
t.Fatalf("Failed to create bot: %v", err)
}
// Verify that the owner exists
var owner User
err = db.Where("telegram_id = ? AND bot_id = ? AND is_owner = ?", config.OwnerTelegramID, bot.botID, true).First(&owner).Error
if err != nil {
t.Fatalf("Owner was not created: %v", err)
}
// Attempt to create another owner for the same bot
_, err = bot.getOrCreateUser(222222222, "AnotherOwner", true)
if err == nil {
t.Fatalf("Expected error when creating a second owner, but got none")
}
// Create a new user
newUser, err := bot.getOrCreateUser(987654321, "TestUser", false)
if err != nil {
t.Fatalf("Failed to create a new user: %v", err)
}
// Verify that the new user was created
var userInDB User
err = db.Where("telegram_id = ?", newUser.TelegramID).First(&userInDB).Error
if err != nil {
t.Fatalf("New user was not created in the database: %v", err)
}
// Get the existing user
existingUser, err := bot.getOrCreateUser(987654321, "TestUser", false)
if err != nil {
t.Fatalf("Failed to get existing user: %v", err)
}
// Verify that the existing user is the same as the new user
if existingUser.ID != userInDB.ID {
t.Fatalf("Expected to get the existing user, but got a different user")
}
}
// To ensure thread safety and avoid race conditions during testing,
// you can run the tests with the `-race` flag:
// go test -race -v