mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-04-30 23:32:19 +00:00
Design
This commit is contained in:
@@ -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.
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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: ./...
|
||||
+10
-3
@@ -1,11 +1,18 @@
|
||||
# Local IDE config & user settings
|
||||
.vscode/
|
||||
|
||||
# Go vendor directory
|
||||
vendor/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Log file
|
||||
bot.log
|
||||
# Any log files
|
||||
*.log
|
||||
|
||||
# Database file
|
||||
bot.db
|
||||
bot.db
|
||||
|
||||
# All config files except for the default
|
||||
config/*
|
||||
!config/default.json
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mcpServers": {}
|
||||
}
|
||||
+55
@@ -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"]
|
||||
@@ -0,0 +1,149 @@
|
||||
# Go Telegram Multibot
|
||||
|
||||
A scalable, multi-bot solution for Telegram using Go, GORM, and the Anthropic API.
|
||||
|
||||
## Design Considerations
|
||||
|
||||
- AI-powered (Anthropic Claude)
|
||||
- Voice message support (ElevenLabs STT + TTS) — optional, enabled per bot via config
|
||||
- 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 |
|
||||
| `/set_model <model-id>` | Admin/Owner | Switch the AI model live without restarting |
|
||||
|
||||
> **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:
|
||||
|
||||
> [](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.
|
||||
+103
-14
@@ -2,44 +2,133 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/liushuangls/go-anthropic/v2"
|
||||
)
|
||||
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Message, isNewChat, isAdminOrOwner bool) (string, error) {
|
||||
// ErrModelNotFound is returned when the configured Anthropic model is no longer available
|
||||
// (deprecated or removed). Callers can use errors.Is to detect this and surface an
|
||||
// actionable message to admins/owners while keeping the response vague for regular users.
|
||||
var ErrModelNotFound = errors.New("model not found or deprecated")
|
||||
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Message, isNewChat, isOwner, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int) (string, error) {
|
||||
// Use prompts from config
|
||||
var systemMessage string
|
||||
if isNewChat {
|
||||
systemMessage = "You are a helpful AI assistant."
|
||||
systemMessage = b.config.SystemPrompts["new_chat"]
|
||||
} else {
|
||||
systemMessage = "Continue the conversation."
|
||||
systemMessage = b.config.SystemPrompts["continue_conversation"]
|
||||
}
|
||||
|
||||
if !isAdminOrOwner {
|
||||
systemMessage += " Avoid discussing sensitive topics or providing harmful information."
|
||||
// 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 !isOwner {
|
||||
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
|
||||
for i := range messages {
|
||||
if messages[i].Role == "user" {
|
||||
switch messages[i].Role {
|
||||
case anthropic.RoleUser:
|
||||
messages[i].Role = anthropic.RoleUser
|
||||
} else if messages[i].Role == "assistant" {
|
||||
case anthropic.RoleAssistant:
|
||||
messages[i].Role = anthropic.RoleAssistant
|
||||
default:
|
||||
// Default to 'user' if role is unrecognized
|
||||
messages[i].Role = anthropic.RoleUser
|
||||
}
|
||||
}
|
||||
|
||||
model := anthropic.ModelClaude3Dot5Sonnet20240620
|
||||
if !isAdminOrOwner {
|
||||
model = anthropic.ModelClaudeInstant1Dot2
|
||||
}
|
||||
model := anthropic.Model(b.config.Model)
|
||||
|
||||
resp, err := b.anthropicClient.CreateMessages(ctx, anthropic.MessagesRequest{
|
||||
Model: model,
|
||||
// Create the request
|
||||
request := anthropic.MessagesRequest{
|
||||
Model: model, // Now `model` is of type anthropic.Model
|
||||
Messages: messages,
|
||||
System: systemMessage,
|
||||
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 {
|
||||
var apiErr *anthropic.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.IsNotFoundErr() {
|
||||
return "", fmt.Errorf("%w: %s", ErrModelNotFound, b.config.Model)
|
||||
}
|
||||
return "", fmt.Errorf("error creating Anthropic message: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -14,76 +15,190 @@ import (
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
tgBot *bot.Bot
|
||||
db *gorm.DB
|
||||
anthropicClient *anthropic.Client
|
||||
chatMemories map[int64]*ChatMemory
|
||||
tgBot TelegramClient
|
||||
db *gorm.DB
|
||||
anthropicClient *anthropic.Client
|
||||
chatMemories map[int64]*ChatMemory
|
||||
memorySize int
|
||||
chatMemoriesMu sync.RWMutex
|
||||
config Config
|
||||
config BotConfig
|
||||
userLimiters map[int64]*userLimiter
|
||||
userLimitersMu sync.RWMutex
|
||||
clock Clock
|
||||
botID uint // Reference to BotModel.ID
|
||||
}
|
||||
|
||||
func NewBot(db *gorm.DB, config Config) (*Bot, error) {
|
||||
anthropicClient := anthropic.NewClient(os.Getenv("ANTHROPIC_API_KEY"))
|
||||
// Helper function to determine message type
|
||||
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{
|
||||
db: db,
|
||||
anthropicClient: anthropicClient,
|
||||
db: db,
|
||||
anthropicClient: anthropicClient,
|
||||
chatMemories: make(map[int64]*ChatMemory),
|
||||
memorySize: config.MemorySize,
|
||||
config: config,
|
||||
userLimiters: make(map[int64]*userLimiter),
|
||||
clock: clock,
|
||||
botID: botEntry.ID, // Ensure BotModel has ID field
|
||||
tgBot: tgClient,
|
||||
}
|
||||
|
||||
tgBot, err := initTelegramBot(b.handleUpdate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if tgClient == nil {
|
||||
var err error
|
||||
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
|
||||
}
|
||||
|
||||
// Start begins the bot's operation.
|
||||
func (b *Bot) Start(ctx context.Context) {
|
||||
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
|
||||
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 errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
var defaultRole Role
|
||||
if err := b.db.Where("name = ?", "user").First(&defaultRole).Error; err != nil {
|
||||
return User{}, err
|
||||
// Check if an owner already exists for this bot
|
||||
if isOwner {
|
||||
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 {
|
||||
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 {
|
||||
return User{}, err
|
||||
}
|
||||
} else {
|
||||
if isOwner && !user.IsOwner {
|
||||
return User{}, fmt.Errorf("cannot change existing user to owner")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return Message{
|
||||
message := Message{
|
||||
ChatID: chatID,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserRole: userRole,
|
||||
Text: text,
|
||||
Timestamp: time.Now(),
|
||||
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 {
|
||||
return b.db.Create(&message).Error
|
||||
// storeMessage stores a message in the database and updates its ID
|
||||
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 {
|
||||
@@ -92,29 +207,60 @@ func (b *Bot) getOrCreateChatMemory(chatID int64) *ChatMemory {
|
||||
b.chatMemoriesMu.RUnlock()
|
||||
|
||||
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.chatMemories[chatID] = chatMemory
|
||||
b.chatMemoriesMu.Unlock()
|
||||
defer 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 desc").
|
||||
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 {
|
||||
// Reverse from newest-first to chronological order for conversation context.
|
||||
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
|
||||
messages[i], messages[j] = messages[j], messages[i]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messages = []Message{} // Ensure messages is initialized for new chats
|
||||
}
|
||||
|
||||
chatMemory = &ChatMemory{
|
||||
Messages: messages,
|
||||
Size: b.memorySize * 2,
|
||||
}
|
||||
|
||||
b.chatMemories[chatID] = 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) {
|
||||
b.chatMemoriesMu.Lock()
|
||||
defer b.chatMemoriesMu.Unlock()
|
||||
|
||||
// Add the new message
|
||||
chatMemory.Messages = append(chatMemory.Messages, message)
|
||||
|
||||
// Maintain the memory size
|
||||
if len(chatMemory.Messages) > chatMemory.Size {
|
||||
chatMemory.Messages = chatMemory.Messages[2:]
|
||||
chatMemory.Messages = chatMemory.Messages[len(chatMemory.Messages)-chatMemory.Size:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,16 +268,34 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message
|
||||
b.chatMemoriesMu.RLock()
|
||||
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
|
||||
for _, msg := range chatMemory.Messages {
|
||||
role := anthropic.RoleUser
|
||||
if !msg.IsUser {
|
||||
role = anthropic.RoleAssistant
|
||||
}
|
||||
|
||||
textContent := strings.TrimSpace(msg.Text)
|
||||
if textContent == "" {
|
||||
// Skip empty messages
|
||||
continue
|
||||
}
|
||||
|
||||
contextMessages = append(contextMessages, anthropic.Message{
|
||||
Role: role,
|
||||
Content: []anthropic.MessageContent{
|
||||
anthropic.NewTextMessageContent(msg.Text),
|
||||
anthropic.NewTextMessageContent(textContent),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -140,23 +304,475 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.Message
|
||||
|
||||
func (b *Bot) isNewChat(chatID int64) bool {
|
||||
var count int64
|
||||
b.db.Model(&Message{}).Where("chat_id = ?", chatID).Count(&count)
|
||||
return count == 1
|
||||
b.db.Model(&Message{}).Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Count(&count)
|
||||
return count == 0 // Only consider a chat new if it has 0 messages
|
||||
}
|
||||
|
||||
func (b *Bot) isAdminOrOwner(userID int64) bool {
|
||||
// roleHasScope reports whether role (with pre-loaded Scopes) contains the given scope name.
|
||||
func roleHasScope(role Role, scope string) bool {
|
||||
for _, s := range role.Scopes {
|
||||
if s.Name == scope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasScope reports whether the user identified by userID holds the given scope for this bot.
|
||||
// Owners implicitly hold all scopes regardless of their assigned role.
|
||||
func (b *Bot) hasScope(userID int64, scope string) bool {
|
||||
var user User
|
||||
err := b.db.Preload("Role").Where("telegram_id = ?", userID).First(&user).Error
|
||||
if err != nil {
|
||||
if err := b.db.Preload("Role.Scopes").
|
||||
Where("telegram_id = ? AND bot_id = ?", userID, b.botID).
|
||||
First(&user).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return user.Role.Name == "admin" || user.Role.Name == "owner"
|
||||
if user.IsOwner {
|
||||
return true
|
||||
}
|
||||
return roleHasScope(user.Role, scope)
|
||||
}
|
||||
|
||||
func initTelegramBot(handleUpdate func(ctx context.Context, b *bot.Bot, update *models.Update)) (*bot.Bot, error) {
|
||||
// publicBotCommands are shown to every user in the Telegram command palette.
|
||||
var publicBotCommands = []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]"},
|
||||
}
|
||||
|
||||
// adminBotCommands are shown only in admin/owner chats via BotCommandScopeChatMember.
|
||||
var adminBotCommands = []models.BotCommand{
|
||||
{Command: "clear_hard", Description: "Clear chat history (permanently delete). Admins: /clear_hard [user_id]"},
|
||||
{Command: "set_model", Description: "Switch the AI model (admin/owner only). Usage: /set_model <model-id>"},
|
||||
}
|
||||
|
||||
// registerAdminCommandsForUser scopes the full command palette to a specific user's private chat.
|
||||
// In Telegram private chats, chat_id == user_id, so both fields carry the same value.
|
||||
// Errors are logged but treated as non-fatal: the user retains access via permission checks.
|
||||
func (b *Bot) registerAdminCommandsForUser(ctx context.Context, telegramID int64) {
|
||||
allCommands := make([]models.BotCommand, 0, len(publicBotCommands)+len(adminBotCommands))
|
||||
allCommands = append(allCommands, publicBotCommands...)
|
||||
allCommands = append(allCommands, adminBotCommands...)
|
||||
_, err := b.tgBot.SetMyCommands(ctx, &bot.SetMyCommandsParams{
|
||||
Commands: allCommands,
|
||||
Scope: &models.BotCommandScopeChat{ChatID: telegramID},
|
||||
})
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Failed to register admin commands for user %d: %v", telegramID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// setElevatedCommands registers the full command palette (public + admin) for every user
|
||||
// whose role carries the model:set scope, or who is the bot owner. Called once at startup
|
||||
// and uses the freshly created tgBot directly (b.tgBot is not yet assigned at that point).
|
||||
func setElevatedCommands(tgBot TelegramClient, users []User) {
|
||||
allCommands := make([]models.BotCommand, 0, len(publicBotCommands)+len(adminBotCommands))
|
||||
allCommands = append(allCommands, publicBotCommands...)
|
||||
allCommands = append(allCommands, adminBotCommands...)
|
||||
for _, u := range users {
|
||||
if u.TelegramID == 0 {
|
||||
continue // skip placeholder users not yet seen in a chat
|
||||
}
|
||||
if !u.IsOwner && !roleHasScope(u.Role, ScopeModelSet) {
|
||||
continue
|
||||
}
|
||||
_, err := tgBot.SetMyCommands(context.Background(), &bot.SetMyCommandsParams{
|
||||
Commands: allCommands,
|
||||
Scope: &models.BotCommandScopeChat{ChatID: u.TelegramID},
|
||||
})
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Warning: could not set admin commands for user %d: %v", u.TelegramID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initTelegramBot(token string, b *Bot) (TelegramClient, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// Register public commands for all users.
|
||||
_, err = tgBot.SetMyCommands(context.Background(), &bot.SetMyCommandsParams{
|
||||
Commands: publicBotCommands,
|
||||
Scope: &models.BotCommandScopeDefault{},
|
||||
})
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error setting default bot commands: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register full command palette (public + admin) scoped to each known elevated user.
|
||||
// BotCommandScopeChatMember targets the user's private DM with the bot (chat_id == user_id).
|
||||
// Elevation is determined by scope rather than role name, so renaming roles requires no code change.
|
||||
// This is best-effort: failures are logged but do not prevent the bot from starting.
|
||||
var allUsers []User
|
||||
if err := b.db.Preload("Role.Scopes").Where("bot_id = ?", b.botID).Find(&allUsers).Error; err != nil {
|
||||
ErrorLogger.Printf("Warning: could not query users for command scoping: %v", err)
|
||||
} else {
|
||||
setElevatedCommands(tgBot, allUsers)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if b.hasScope(userID, ScopeStatsViewAny) {
|
||||
type topEntry struct {
|
||||
UserID int64
|
||||
MsgCount int64
|
||||
}
|
||||
var top []topEntry
|
||||
if err := b.db.Model(&Message{}).
|
||||
Select("user_id, COUNT(*) as msg_count").
|
||||
Where("bot_id = ? AND is_user = ? AND deleted_at IS NULL", b.botID, true).
|
||||
Group("user_id").
|
||||
Order("msg_count DESC").
|
||||
Limit(3).
|
||||
Scan(&top).Error; err != nil {
|
||||
ErrorLogger.Printf("Error fetching top users: %v", err)
|
||||
} else if len(top) > 0 {
|
||||
statsMessage += "\n\n🏆 Most Active Users:"
|
||||
for i, entry := range top {
|
||||
var u User
|
||||
if err := b.db.Select("username").Where("telegram_id = ? AND bot_id = ?", entry.UserID, b.botID).First(&u).Error; err != nil {
|
||||
u.Username = fmt.Sprintf("ID:%d", entry.UserID)
|
||||
}
|
||||
name := u.Username
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("ID:%d", entry.UserID)
|
||||
}
|
||||
statsMessage += fmt.Sprintf("\n%d. @%s — %d messages", i+1, name, entry.MsgCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.hasScope(userID, ScopeStatsViewAny) {
|
||||
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 deleted_at IS NULL) 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."
|
||||
}
|
||||
}
|
||||
if message.Voice != nil {
|
||||
messageText = "[Voice message]"
|
||||
}
|
||||
|
||||
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 has the user:promote scope
|
||||
if !b.hasScope(promoterID, ScopeUserPromote) {
|
||||
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
|
||||
if err := b.db.Save(&userToPromote).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Surface admin commands in the newly promoted user's private chat.
|
||||
b.registerAdminCommandsForUser(context.Background(), userToPromoteID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
}
|
||||
@@ -2,25 +2,238 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/liushuangls/go-anthropic/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MemorySize int `json:"memory_size"`
|
||||
MessagePerHour int `json:"messages_per_hour"`
|
||||
MessagePerDay int `json:"messages_per_day"`
|
||||
TempBanDuration string `json:"temp_ban_duration"`
|
||||
type BotConfig struct {
|
||||
ID string `json:"id"`
|
||||
TelegramToken string `json:"telegram_token"`
|
||||
MemorySize int `json:"memory_size"`
|
||||
MessagePerHour int `json:"messages_per_hour"`
|
||||
MessagePerDay int `json:"messages_per_day"`
|
||||
TempBanDuration string `json:"temp_ban_duration"`
|
||||
Model anthropic.Model `json:"model"`
|
||||
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"`
|
||||
ElevenLabsAPIKey string `json:"elevenlabs_api_key"`
|
||||
ElevenLabsVoiceID string `json:"elevenlabs_voice_id"`
|
||||
ElevenLabsModel string `json:"elevenlabs_model"`
|
||||
DebugScreening bool `json:"debug_screening"` // Enable detailed screening logs
|
||||
ConfigFilePath string `json:"-"` // Set at load time; not serialized
|
||||
}
|
||||
|
||||
func loadConfig(filename string) (Config, error) {
|
||||
var config Config
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return config, err
|
||||
// Custom unmarshalling to handle anthropic.Model
|
||||
func (c *BotConfig) UnmarshalJSON(data []byte) error {
|
||||
type Alias BotConfig
|
||||
aux := &struct {
|
||||
Model string `json:"model"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(c),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
config.ConfigFilePath = validPath
|
||||
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)
|
||||
err = decoder.Decode(&config)
|
||||
return config, err
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// PersistModel updates the model field in memory and writes it back to the config file on disk.
|
||||
// Only the "model" key is changed; all other fields are preserved verbatim.
|
||||
func (c *BotConfig) PersistModel(newModel string) error {
|
||||
if c.ConfigFilePath == "" {
|
||||
return fmt.Errorf("config file path not set; cannot persist model")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(c.ConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config for update: %w", err)
|
||||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("failed to parse config for update: %w", err)
|
||||
}
|
||||
|
||||
raw["model"] = newModel
|
||||
|
||||
updated, err := json.MarshalIndent(raw, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to re-encode config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(c.ConfigFilePath, updated, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
c.Model = anthropic.Model(newModel)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"memory_size": 10,
|
||||
"messages_per_hour": 20,
|
||||
"messages_per_day": 100,
|
||||
"temp_ban_duration": "24h"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "default_bot",
|
||||
"active": false,
|
||||
"telegram_token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
"owner_telegram_id": 111111111,
|
||||
"anthropic_api_key": "YOUR_SPECIFIC_ANTHROPIC_API_KEY",
|
||||
"elevenlabs_api_key": "",
|
||||
"elevenlabs_voice_id": "",
|
||||
"elevenlabs_model": "",
|
||||
"memory_size": 10,
|
||||
"messages_per_hour": 20,
|
||||
"messages_per_day": 100,
|
||||
"temp_ban_duration": "24h",
|
||||
"model": "claude-haiku-4-5",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
+818
@@ -0,0 +1,818 @@
|
||||
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
|
||||
|
||||
// TestBotConfig_PersistModel verifies that PersistModel updates the model both in memory
|
||||
// and on disk while leaving all other config fields unchanged.
|
||||
func TestBotConfig_PersistModel(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
||||
tempDir, err := os.MkdirTemp("", "persist_model_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)
|
||||
}
|
||||
}()
|
||||
|
||||
initialJSON := `{
|
||||
"id": "bot1",
|
||||
"telegram_token": "token1",
|
||||
"model": "claude-v1",
|
||||
"messages_per_hour": 10,
|
||||
"messages_per_day": 100
|
||||
}`
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(initialJSON), 0600); err != nil {
|
||||
t.Fatalf("Failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
config := BotConfig{
|
||||
ID: "bot1",
|
||||
Model: "claude-v1",
|
||||
ConfigFilePath: configPath,
|
||||
}
|
||||
|
||||
// Successful model update
|
||||
if err := config.PersistModel("claude-sonnet-4-6"); err != nil {
|
||||
t.Fatalf("PersistModel() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// In-memory model must be updated immediately
|
||||
if string(config.Model) != "claude-sonnet-4-6" {
|
||||
t.Errorf("in-memory model: got %q, want %q", config.Model, "claude-sonnet-4-6")
|
||||
}
|
||||
|
||||
// On-disk model must be updated; other fields must be preserved
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated config file: %v", err)
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatalf("Failed to unmarshal updated config: %v", err)
|
||||
}
|
||||
if raw["model"] != "claude-sonnet-4-6" {
|
||||
t.Errorf("on-disk model: got %v, want %q", raw["model"], "claude-sonnet-4-6")
|
||||
}
|
||||
if raw["id"] != "bot1" {
|
||||
t.Errorf("on-disk id should be preserved: got %v, want %q", raw["id"], "bot1")
|
||||
}
|
||||
|
||||
// Error case: empty ConfigFilePath must return an error
|
||||
noPath := BotConfig{Model: "claude-v1"}
|
||||
if err := noPath.PersistModel("claude-sonnet-4-6"); err == nil {
|
||||
t.Error("PersistModel with empty ConfigFilePath: expected error, got nil")
|
||||
}
|
||||
}
|
||||
+76
-2
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -11,6 +12,10 @@ import (
|
||||
)
|
||||
|
||||
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(
|
||||
log.New(log.Writer(), "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
@@ -20,33 +25,102 @@ 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&_foreign_keys=on"), &gorm.Config{
|
||||
Logger: newLogger,
|
||||
})
|
||||
if err != nil {
|
||||
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{}, &Scope{})
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := createDefaultScopes(db); err != nil {
|
||||
return nil, fmt.Errorf("createDefaultScopes: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func createDefaultScopes(db *gorm.DB) error {
|
||||
all := []string{
|
||||
ScopeStatsViewOwn, ScopeStatsViewAny,
|
||||
ScopeHistoryClearOwn, ScopeHistoryClearAny,
|
||||
ScopeHistoryClearHardOwn, ScopeHistoryClearHardAny,
|
||||
ScopeModelSet, ScopeUserPromote, ScopeTTSUse,
|
||||
}
|
||||
for _, name := range all {
|
||||
if err := db.FirstOrCreate(&Scope{}, Scope{Name: name}).Error; err != nil {
|
||||
return fmt.Errorf("failed to create scope %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
userScopes := []string{
|
||||
ScopeStatsViewOwn,
|
||||
ScopeHistoryClearOwn,
|
||||
ScopeHistoryClearHardOwn,
|
||||
}
|
||||
elevatedScopes := []string{
|
||||
ScopeStatsViewOwn, ScopeStatsViewAny,
|
||||
ScopeHistoryClearOwn, ScopeHistoryClearAny,
|
||||
ScopeHistoryClearHardOwn, ScopeHistoryClearHardAny,
|
||||
ScopeModelSet, ScopeUserPromote, ScopeTTSUse,
|
||||
}
|
||||
assignments := map[string][]string{
|
||||
"user": userScopes,
|
||||
"admin": elevatedScopes,
|
||||
// owner gets the same scopes as admin; owner uniqueness is enforced by the IsOwner flag
|
||||
"owner": elevatedScopes,
|
||||
}
|
||||
for roleName, scopes := range assignments {
|
||||
var role Role
|
||||
if err := db.Where("name = ?", roleName).First(&role).Error; err != nil {
|
||||
return fmt.Errorf("role %s not found: %w", roleName, err)
|
||||
}
|
||||
var scopeModels []Scope
|
||||
if err := db.Where("name IN ?", scopes).Find(&scopeModels).Error; err != nil {
|
||||
return fmt.Errorf("failed to find scopes for %s: %w", roleName, err)
|
||||
}
|
||||
if err := db.Model(&role).Association("Scopes").Replace(scopeModels); err != nil {
|
||||
return fmt.Errorf("failed to assign scopes to %s: %w", roleName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultRoles(db *gorm.DB) error {
|
||||
roles := []string{"user", "admin", "owner"}
|
||||
for _, roleName := range roles {
|
||||
var role Role
|
||||
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)
|
||||
}
|
||||
InfoLogger.Printf("Created or confirmed default role: %s", roleName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
tgbot "github.com/go-telegram/bot"
|
||||
)
|
||||
|
||||
const (
|
||||
elevenLabsTTSURL = "https://api.elevenlabs.io/v1/text-to-speech/"
|
||||
elevenLabsSTTURL = "https://api.elevenlabs.io/v1/speech-to-text"
|
||||
elevenLabsDefaultModel = "eleven_multilingual_v2"
|
||||
)
|
||||
|
||||
// generateSpeech converts text to an mp3 audio stream via ElevenLabs TTS.
|
||||
func (b *Bot) generateSpeech(ctx context.Context, text string) (io.Reader, error) {
|
||||
model := b.config.ElevenLabsModel
|
||||
if model == "" {
|
||||
model = elevenLabsDefaultModel
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"text": text,
|
||||
"model_id": model,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elevenlabs TTS marshal error: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
elevenLabsTTSURL+b.config.ElevenLabsVoiceID, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elevenlabs TTS request error: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("xi-api-key", b.config.ElevenLabsAPIKey)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elevenlabs TTS error: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("elevenlabs TTS error: status %d: %s", resp.StatusCode, errBody)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// transcribeVoice downloads a Telegram voice file and transcribes it via ElevenLabs STT.
|
||||
// Uses a direct multipart HTTP call instead of the SDK wrapper to avoid a bug in the
|
||||
// ogen-generated encoder: AdditionalFormats (nil slice) is always written as an empty
|
||||
// string with Content-Type: application/json, which ElevenLabs rejects with 400.
|
||||
func (b *Bot) transcribeVoice(ctx context.Context, fileID string) (string, error) {
|
||||
// 1. Resolve and download the voice file from Telegram.
|
||||
fileInfo, err := b.tgBot.GetFile(ctx, &tgbot.GetFileParams{FileID: fileID})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("telegram GetFile error: %w", err)
|
||||
}
|
||||
downloadURL := b.tgBot.FileDownloadLink(fileInfo)
|
||||
audioResp, err := http.Get(downloadURL) //nolint:noctx
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("voice download error: %w", err)
|
||||
}
|
||||
defer audioResp.Body.Close()
|
||||
|
||||
// 2. Build multipart body with binary audio — bypasses SDK encoding issues.
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
if err := mw.WriteField("model_id", "scribe_v1"); err != nil {
|
||||
return "", fmt.Errorf("multipart write error: %w", err)
|
||||
}
|
||||
part, err := mw.CreateFormFile("file", "audio.ogg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("multipart create file error: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(part, audioResp.Body); err != nil {
|
||||
return "", fmt.Errorf("multipart copy error: %w", err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
return "", fmt.Errorf("multipart close error: %w", err)
|
||||
}
|
||||
|
||||
// 3. POST to ElevenLabs STT.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
elevenLabsSTTURL, &buf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create STT request error: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("xi-api-key", b.config.ElevenLabsAPIKey)
|
||||
|
||||
sttResp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("elevenlabs STT request error: %w", err)
|
||||
}
|
||||
defer sttResp.Body.Close()
|
||||
|
||||
if sttResp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(sttResp.Body)
|
||||
return "", fmt.Errorf("elevenlabs STT error: status %d: %s", sttResp.StatusCode, body)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.NewDecoder(sttResp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("elevenlabs STT decode error: %w", err)
|
||||
}
|
||||
return result.Text, nil
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -1,19 +1,26 @@
|
||||
module github.com/HugeFrog24/thatsky-telegram-bot
|
||||
module github.com/HugeFrog24/go-telegram-bot
|
||||
|
||||
go 1.23.2
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/go-telegram/bot v1.8.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/liushuangls/go-anthropic/v2 v2.8.1
|
||||
golang.org/x/time v0.7.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
github.com/go-telegram/bot v1.19.0
|
||||
github.com/liushuangls/go-anthropic/v2 v2.17.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/time v0.14.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
github.com/go-telegram/bot v1.8.4 h1:7viEUESakK29aiCumq6ui5jTPqJLLDeFubTsQzE07Kg=
|
||||
github.com/go-telegram/bot v1.8.4/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/liushuangls/go-anthropic/v2 v2.8.1 h1:pxFl88IgkG7e8Z1XwOYu48LcmEN0+6UdO58HF9altw0=
|
||||
github.com/liushuangls/go-anthropic/v2 v2.8.1/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/liushuangls/go-anthropic/v2 v2.17.1 h1:ca3oFzgQHs9/mJr+xx2XFQIYcQLM2rDCqieUx0g+8p4=
|
||||
github.com/liushuangls/go-anthropic/v2 v2.17.1/go.mod h1:a550cJXPoTG2FL3DvfKG2zzD5O2vjgvo4tHtoGPzFLU=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
|
||||
+497
-38
@@ -2,71 +2,530 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"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) {
|
||||
if update.Message == nil {
|
||||
func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, userMsg Message, chatID, userID int64, username, firstName, lastName string, isPremium bool, languageCode string, messageTime int, isNewChat, isOwner bool, businessConnectionID string) {
|
||||
// If ElevenLabs is not configured, respond with text — consistent with all other error paths.
|
||||
if b.config.ElevenLabsAPIKey == "" {
|
||||
if err := b.sendResponse(ctx, chatID, "I don't understand voice messages.", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending voice-unsupported message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
chatID := update.Message.Chat.ID
|
||||
userID := update.Message.From.ID
|
||||
|
||||
if !b.checkRateLimits(userID) {
|
||||
b.sendRateLimitExceededMessage(ctx, chatID)
|
||||
if !b.hasScope(userID, ScopeTTSUse) {
|
||||
if err := b.sendResponse(ctx, chatID, "You don't have permission to use voice features.", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending permission denied message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
username := update.Message.From.Username
|
||||
text := update.Message.Text
|
||||
|
||||
user, err := b.getOrCreateUser(userID, username)
|
||||
transcript, err := b.transcribeVoice(ctx, message.Voice.FileID)
|
||||
if err != nil {
|
||||
log.Printf("Error getting or creating user: %v", err)
|
||||
ErrorLogger.Printf("Error transcribing voice message from user %d: %v", userID, err)
|
||||
if err := b.sendResponse(ctx, chatID, "Sorry, I couldn't understand your voice message.", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending transcription error message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
userMessage := b.createMessage(chatID, userID, username, user.Role.Name, text, true)
|
||||
b.storeMessage(userMessage)
|
||||
// Replace the stored "[Voice message]" placeholder with the actual transcript,
|
||||
// keeping the audit record intact while giving the LLM meaningful context.
|
||||
if err := b.db.Model(&userMsg).Update("text", transcript).Error; err != nil {
|
||||
ErrorLogger.Printf("Error updating voice transcript in DB: %v", err)
|
||||
}
|
||||
b.chatMemoriesMu.Lock()
|
||||
if mem, exists := b.chatMemories[chatID]; exists {
|
||||
for i := len(mem.Messages) - 1; i >= 0; i-- {
|
||||
if mem.Messages[i].ID == userMsg.ID {
|
||||
mem.Messages[i].Text = transcript
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
b.chatMemoriesMu.Unlock()
|
||||
|
||||
chatMemory := b.getOrCreateChatMemory(chatID)
|
||||
b.addMessageToChatMemory(chatMemory, userMessage)
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, isNewChat, isOwner, false, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response for voice: %v", err)
|
||||
if err := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending anthropic error response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
audioReader, err := b.generateSpeech(ctx, response)
|
||||
if err != nil {
|
||||
// TTS failed — fall back to text so the user still gets a reply.
|
||||
ErrorLogger.Printf("Error generating speech, falling back to text: %v", err)
|
||||
if err := b.sendResponse(ctx, chatID, response, businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending text fallback: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Store the assistant response before sending.
|
||||
if _, err := b.screenOutgoingMessage(chatID, response); err != nil {
|
||||
ErrorLogger.Printf("Error storing assistant voice response: %v", err)
|
||||
}
|
||||
|
||||
params := &bot.SendAudioParams{
|
||||
ChatID: chatID,
|
||||
Audio: &models.InputFileUpload{Filename: "response.mp3", Data: audioReader},
|
||||
}
|
||||
if businessConnectionID != "" {
|
||||
params.BusinessConnectionID = businessConnectionID
|
||||
}
|
||||
if _, err := b.tgBot.SendAudio(ctx, params); err != nil {
|
||||
ErrorLogger.Printf("Error sending audio to chat %d: %v", chatID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// anthropicErrorResponse returns the message to send back to the user when getAnthropicResponse
|
||||
// fails. Admins and owners receive an actionable hint when the model is deprecated; regular users
|
||||
// always get the generic fallback to avoid leaking internal details.
|
||||
func (b *Bot) anthropicErrorResponse(err error, userID int64) string {
|
||||
if errors.Is(err, ErrModelNotFound) && b.hasScope(userID, ScopeModelSet) {
|
||||
return fmt.Sprintf(
|
||||
"⚠️ Model `%s` is no longer available (deprecated or removed by Anthropic).\n"+
|
||||
"Use /set_model <model-id> to switch. Current models: https://platform.claude.com/docs/en/about-claude/models/overview",
|
||||
b.config.Model,
|
||||
)
|
||||
}
|
||||
return "I'm sorry, I'm having trouble processing your request right now."
|
||||
}
|
||||
|
||||
func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
|
||||
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
|
||||
}
|
||||
|
||||
// Extract businessConnectionID if available
|
||||
var businessConnectionID string
|
||||
if update.BusinessConnection != nil {
|
||||
businessConnectionID = update.BusinessConnection.ID
|
||||
} else if message.BusinessConnectionID != "" {
|
||||
businessConnectionID = message.BusinessConnectionID
|
||||
}
|
||||
|
||||
if message.From == nil {
|
||||
// Channel posts and some automated messages have no sender — ignore them.
|
||||
// see: https://core.telegram.org/bots/api#message
|
||||
return
|
||||
}
|
||||
|
||||
chatID := message.Chat.ID
|
||||
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
|
||||
|
||||
// Check if it's a new chat (before storing the message so the flag is accurate).
|
||||
isNewChatFlag := b.isNewChat(chatID)
|
||||
|
||||
// Screen incoming message (store to DB + add to chat memory)
|
||||
userMsg, err := b.screenIncomingMessage(message)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error storing user message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if the user is the owner
|
||||
var isOwner bool
|
||||
if b.db.Where("telegram_id = ? AND bot_id = ? AND is_owner = ?", userID, b.botID, true).First(&User{}).Error == nil {
|
||||
isOwner = true
|
||||
}
|
||||
|
||||
// Always create/get the user record — on the very first message and on all subsequent ones.
|
||||
user, err := b.getOrCreateUser(userID, username, isOwner)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting or creating user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the username if it has changed
|
||||
if user.Username != username {
|
||||
user.Username = username
|
||||
if err := b.db.Save(&user).Error; err != nil {
|
||||
ErrorLogger.Printf("Error updating user username: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the message is a command — applies on every message, including the very first.
|
||||
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 "/set_model":
|
||||
if !b.hasScope(userID, ScopeModelSet) {
|
||||
if err := b.sendResponse(ctx, chatID, "Permission denied. Only admins and owners can change the model.", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
parts := strings.Fields(message.Text)
|
||||
if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
|
||||
if err := b.sendResponse(ctx, chatID, "Usage: /set_model <model-id>", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
newModel := strings.TrimSpace(parts[1])
|
||||
// No upfront model validation:
|
||||
// - The go-anthropic library constants are not enumerable at runtime (Go has no const reflection).
|
||||
// - A live /v1/models probe would add a network round-trip and show in the API audit log.
|
||||
// - An invalid model ID will produce a 404 on the next real message, which routes through
|
||||
// anthropicErrorResponse and already delivers an actionable admin-facing hint.
|
||||
if err := b.config.PersistModel(newModel); err != nil {
|
||||
ErrorLogger.Printf("Failed to persist model change: %v", err)
|
||||
if err := b.sendResponse(ctx, chatID, fmt.Sprintf("Model updated in memory to `%s`, but failed to save to config file: %v", newModel, err), businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
InfoLogger.Printf("Model changed to %s by user %d", newModel, userID)
|
||||
if err := b.sendResponse(ctx, chatID, fmt.Sprintf("✅ Model updated to `%s` and saved to config.", newModel), businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
}
|
||||
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 voice note (context is built inside the handler
|
||||
// after the transcript replaces the placeholder, so it must not be built here).
|
||||
if message.Voice != nil {
|
||||
b.handleVoiceMessage(ctx, message, userMsg, chatID, userID, username, firstName, lastName, isPremium, languageCode, messageTime, isNewChatFlag, isOwner, businessConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
// Build context once — shared by the sticker and text response paths.
|
||||
chatMemory := b.getOrCreateChatMemory(chatID)
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, b.isNewChat(chatID), b.isAdminOrOwner(userID))
|
||||
if err != nil {
|
||||
log.Printf("Error getting Anthropic response: %v", err)
|
||||
response = "I'm sorry, I'm having trouble processing your request right now."
|
||||
// Check if the message contains a sticker
|
||||
if message.Sticker != nil {
|
||||
b.handleStickerMessage(ctx, chatID, userMsg, message, contextMessages, businessConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
b.sendResponse(ctx, chatID, response)
|
||||
// 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
|
||||
}
|
||||
|
||||
assistantMessage := b.createMessage(chatID, 0, "Assistant", "assistant", response, false)
|
||||
b.storeMessage(assistantMessage)
|
||||
b.addMessageToChatMemory(chatMemory, assistantMessage)
|
||||
}
|
||||
// Determine if the text contains only emojis
|
||||
isEmojiOnly := isOnlyEmojis(text)
|
||||
|
||||
func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64) {
|
||||
_, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: "Rate limit exceeded. Please try again later.",
|
||||
})
|
||||
// Get response from Anthropic
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, isNewChatFlag, isOwner, isEmojiOnly, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
if err != nil {
|
||||
log.Printf("Error sending rate limit message: %v", err)
|
||||
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
|
||||
response = b.anthropicErrorResponse(err, userID)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
_, err := b.tgBot.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error sending message: %v", err)
|
||||
func (b *Bot) sendRateLimitExceededMessage(ctx context.Context, chatID int64, businessConnectionID string) {
|
||||
if err := b.sendResponse(ctx, chatID, "Rate limit exceeded. Please try again later.", businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending rate limit exceeded message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleStickerMessage(ctx context.Context, chatID int64, userMessage Message, message *models.Message, contextMessages []anthropic.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, contextMessages)
|
||||
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, contextMessages []anthropic.Message) (string, error) {
|
||||
// contextMessages already contains the sticker turn (added by screenIncomingMessage as
|
||||
// "Sent a sticker: <emoji>"), so the full conversation history is preserved.
|
||||
if message.StickerFileID != "" {
|
||||
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 {
|
||||
requiredScope := ScopeHistoryClearAny
|
||||
if hardDelete {
|
||||
requiredScope = ScopeHistoryClearHardAny
|
||||
}
|
||||
if !b.hasScope(currentUserID, requiredScope) {
|
||||
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 {
|
||||
// Own history — delete ALL messages (user + assistant) in the current chat.
|
||||
err = b.db.Unscoped().Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Delete(&Message{}).Error
|
||||
InfoLogger.Printf("User %d permanently deleted their own chat history in chat %d", currentUserID, chatID)
|
||||
} else {
|
||||
if targetChatID != 0 {
|
||||
// Chat-scoped: delete ALL messages (user + assistant) in the specified chat.
|
||||
err = b.db.Unscoped().Where("chat_id = ? AND bot_id = ?", targetChatID, b.botID).Delete(&Message{}).Error
|
||||
InfoLogger.Printf("Admin/owner %d permanently deleted chat history for user %d in chat %d", currentUserID, targetUserID, targetChatID)
|
||||
} else {
|
||||
// Bot-wide: delete all of the user's own messages across every chat, then delete
|
||||
// assistant messages from their DM chat (where chat_id == user_id by Telegram convention).
|
||||
err = b.db.Unscoped().Where("bot_id = ? AND user_id = ?", b.botID, targetUserID).Delete(&Message{}).Error
|
||||
if err == nil {
|
||||
err = b.db.Unscoped().Where("chat_id = ? AND bot_id = ? AND is_user = ?", targetUserID, b.botID, false).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 {
|
||||
// Own history — delete ALL messages (user + assistant) in the current chat.
|
||||
err = b.db.Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Delete(&Message{}).Error
|
||||
InfoLogger.Printf("User %d soft deleted their own chat history in chat %d", currentUserID, chatID)
|
||||
} else {
|
||||
if targetChatID != 0 {
|
||||
// Chat-scoped: delete ALL messages (user + assistant) in the specified chat.
|
||||
err = b.db.Where("chat_id = ? AND bot_id = ?", targetChatID, b.botID).Delete(&Message{}).Error
|
||||
InfoLogger.Printf("Admin/owner %d soft deleted chat history for user %d in chat %d", currentUserID, targetUserID, targetChatID)
|
||||
} else {
|
||||
// Bot-wide: delete all of the user's own messages across every chat, then delete
|
||||
// assistant messages from their DM chat (where chat_id == user_id by Telegram convention).
|
||||
err = b.db.Where("bot_id = ? AND user_id = ?", b.botID, targetUserID).Delete(&Message{}).Error
|
||||
if err == nil {
|
||||
err = b.db.Where("chat_id = ? AND bot_id = ? AND is_user = ?", targetUserID, b.botID, false).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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,891 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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(®ularUser).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(®ularUser).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{}, &Scope{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to migrate database schema: %v", err)
|
||||
}
|
||||
|
||||
// Create default roles and scopes
|
||||
err = createDefaultRoles(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create default roles: %v", err)
|
||||
}
|
||||
if err := createDefaultScopes(db); err != nil {
|
||||
t.Fatalf("Failed to create default scopes: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// setupBotForTest creates a minimal Bot instance backed by an in-memory DB.
|
||||
// It follows the same pattern as the existing handler tests to avoid duplication.
|
||||
func setupBotForTest(t *testing.T, ownerID int64) (*Bot, *MockTelegramClient) {
|
||||
t.Helper()
|
||||
db := setupTestDB(t)
|
||||
mockClock := &MockClock{currentTime: time.Now()}
|
||||
config := BotConfig{
|
||||
ID: "test_bot",
|
||||
OwnerTelegramID: ownerID,
|
||||
TelegramToken: "test_token",
|
||||
MemorySize: 10,
|
||||
MessagePerHour: 5,
|
||||
MessagePerDay: 10,
|
||||
TempBanDuration: "1h",
|
||||
Model: "claude-3-5-haiku-latest",
|
||||
SystemPrompts: make(map[string]string),
|
||||
Active: true,
|
||||
}
|
||||
mockTgClient := &MockTelegramClient{}
|
||||
botModel := &BotModel{Identifier: config.ID, Name: config.ID}
|
||||
assert.NoError(t, db.Create(botModel).Error)
|
||||
assert.NoError(t, db.Create(&ConfigModel{
|
||||
BotID: botModel.ID,
|
||||
MemorySize: config.MemorySize,
|
||||
MessagePerHour: config.MessagePerHour,
|
||||
MessagePerDay: config.MessagePerDay,
|
||||
TempBanDuration: config.TempBanDuration,
|
||||
SystemPrompts: "{}",
|
||||
TelegramToken: config.TelegramToken,
|
||||
Active: config.Active,
|
||||
}).Error)
|
||||
b, err := NewBot(db, config, mockClock, mockTgClient)
|
||||
assert.NoError(t, err)
|
||||
return b, mockTgClient
|
||||
}
|
||||
|
||||
// TestAnthropicErrorResponse verifies that model-deprecation errors surface actionable
|
||||
// details only to admin/owner, and that regular users and non-model errors always get
|
||||
// the generic fallback.
|
||||
func TestAnthropicErrorResponse(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
||||
b, _ := setupBotForTest(t, 123)
|
||||
|
||||
// Create admin user
|
||||
adminRole, err := b.getRoleByName("admin")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 456, Username: "admin",
|
||||
RoleID: adminRole.ID, Role: adminRole,
|
||||
}).Error)
|
||||
|
||||
// Create regular user
|
||||
userRole, err := b.getRoleByName("user")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 789, Username: "regular",
|
||||
RoleID: userRole.ID, Role: userRole,
|
||||
}).Error)
|
||||
|
||||
modelErr := fmt.Errorf("%w: claude-3-5-haiku-latest", ErrModelNotFound)
|
||||
otherErr := errors.New("network error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
userID int64
|
||||
wantSubstr string
|
||||
wantMissing string
|
||||
}{
|
||||
{
|
||||
name: "owner receives actionable model-not-found message",
|
||||
err: modelErr,
|
||||
userID: 123,
|
||||
wantSubstr: "/set_model",
|
||||
},
|
||||
{
|
||||
name: "admin receives actionable model-not-found message",
|
||||
err: modelErr,
|
||||
userID: 456,
|
||||
wantSubstr: "/set_model",
|
||||
},
|
||||
{
|
||||
name: "regular user receives generic message for model-not-found",
|
||||
err: modelErr,
|
||||
userID: 789,
|
||||
wantSubstr: "I'm sorry",
|
||||
wantMissing: "/set_model",
|
||||
},
|
||||
{
|
||||
name: "owner receives generic message for non-model error",
|
||||
err: otherErr,
|
||||
userID: 123,
|
||||
wantSubstr: "I'm sorry",
|
||||
wantMissing: "/set_model",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp := b.anthropicErrorResponse(tc.err, tc.userID)
|
||||
assert.Contains(t, resp, tc.wantSubstr)
|
||||
if tc.wantMissing != "" {
|
||||
assert.NotContains(t, resp, tc.wantMissing)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetModelCommand verifies that /set_model enforces permissions, validates input,
|
||||
// updates the model in memory, and persists the change to the config file on disk.
|
||||
func TestSetModelCommand(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
||||
b, mockTgClient := setupBotForTest(t, 123)
|
||||
|
||||
// Point the config at a temporary file so PersistModel can write to disk.
|
||||
tempDir, err := os.MkdirTemp("", "set_model_cmd_test")
|
||||
assert.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
initialJSON := `{"id":"test_bot","telegram_token":"test_token","model":"claude-3-5-haiku-latest","messages_per_hour":5,"messages_per_day":10}`
|
||||
assert.NoError(t, os.WriteFile(configPath, []byte(initialJSON), 0600))
|
||||
b.config.ConfigFilePath = configPath
|
||||
|
||||
// Create admin and regular users
|
||||
adminRole, err := b.getRoleByName("admin")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 456, Username: "admin",
|
||||
RoleID: adminRole.ID, Role: adminRole,
|
||||
}).Error)
|
||||
userRole, err := b.getRoleByName("user")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 789, Username: "regular",
|
||||
RoleID: userRole.ID, Role: userRole,
|
||||
}).Error)
|
||||
|
||||
chatID := int64(1000)
|
||||
|
||||
// Seed chat 1000 with a prior message so isNewChatFlag is false for all subtests.
|
||||
// Commands are only processed in the non-new-chat branch of handleUpdate.
|
||||
assert.NoError(t, b.db.Create(&Message{
|
||||
BotID: b.botID, ChatID: chatID, UserID: 789, Username: "regular",
|
||||
UserRole: "user", Text: "hello", IsUser: true,
|
||||
}).Error)
|
||||
|
||||
makeUpdate := func(userID int64, text string, cmdLen int) *models.Update {
|
||||
return &models.Update{
|
||||
Message: &models.Message{
|
||||
Chat: models.Chat{ID: chatID},
|
||||
From: &models.User{ID: userID, Username: getUsernameByID(userID)},
|
||||
Text: text,
|
||||
Entities: []models.MessageEntity{
|
||||
{Type: "bot_command", Offset: 0, Length: cmdLen},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userID int64
|
||||
text string
|
||||
wantSubstr string
|
||||
}{
|
||||
{
|
||||
name: "regular user is denied",
|
||||
userID: 789,
|
||||
text: "/set_model claude-sonnet-4-6",
|
||||
wantSubstr: "Permission denied",
|
||||
},
|
||||
{
|
||||
name: "admin missing argument shows usage",
|
||||
userID: 456,
|
||||
text: "/set_model",
|
||||
wantSubstr: "Usage:",
|
||||
},
|
||||
{
|
||||
name: "owner missing argument shows usage",
|
||||
userID: 123,
|
||||
text: "/set_model",
|
||||
wantSubstr: "Usage:",
|
||||
},
|
||||
{
|
||||
name: "admin sets model successfully",
|
||||
userID: 456,
|
||||
text: "/set_model claude-sonnet-4-6",
|
||||
wantSubstr: "✅",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var sentMessage string
|
||||
mockTgClient.SendMessageFunc = func(ctx context.Context, params *bot.SendMessageParams) (*models.Message, error) {
|
||||
sentMessage = params.Text
|
||||
return &models.Message{}, nil
|
||||
}
|
||||
b.handleUpdate(context.Background(), nil, makeUpdate(tc.userID, tc.text, 10))
|
||||
assert.Contains(t, sentMessage, tc.wantSubstr)
|
||||
})
|
||||
}
|
||||
|
||||
// Verify the successful update took effect in memory and on disk.
|
||||
t.Run("model change persisted in memory and on disk", func(t *testing.T) {
|
||||
assert.Equal(t, "claude-sonnet-4-6", string(b.config.Model))
|
||||
data, err := os.ReadFile(configPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"claude-sonnet-4-6"`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestHasScope verifies that scope checks honour role assignments and the owner bypass.
|
||||
func TestHasScope(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
||||
const ownerID int64 = 100
|
||||
b, _ := setupBotForTest(t, ownerID)
|
||||
|
||||
// Admin user
|
||||
adminRole, err := b.getRoleByName("admin")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 200, Username: "admin_user",
|
||||
RoleID: adminRole.ID, Role: adminRole,
|
||||
}).Error)
|
||||
|
||||
// Regular user
|
||||
userRole, err := b.getRoleByName("user")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, b.db.Create(&User{
|
||||
BotID: b.botID, TelegramID: 300, Username: "regular_user",
|
||||
RoleID: userRole.ID, Role: userRole,
|
||||
}).Error)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userID int64
|
||||
scope string
|
||||
want bool
|
||||
}{
|
||||
{"owner bypass: model:set", ownerID, ScopeModelSet, true},
|
||||
{"owner bypass: stats:view:any", ownerID, ScopeStatsViewAny, true},
|
||||
{"admin: model:set", 200, ScopeModelSet, true},
|
||||
{"admin: stats:view:any", 200, ScopeStatsViewAny, true},
|
||||
{"admin: history:clear:any", 200, ScopeHistoryClearAny, true},
|
||||
{"user: model:set denied", 300, ScopeModelSet, false},
|
||||
{"user: stats:view:any denied", 300, ScopeStatsViewAny, false},
|
||||
{"user: history:clear:any denied", 300, ScopeHistoryClearAny, false},
|
||||
{"user: stats:view:own allowed", 300, ScopeStatsViewOwn, true},
|
||||
{"user: history:clear:own allowed", 300, ScopeHistoryClearOwn, true},
|
||||
{"unknown telegram_id", 999, ScopeModelSet, false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, b.hasScope(tc.userID, tc.scope))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -2,72 +2,63 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize logger
|
||||
logFile, err := initLogger()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing logger: %v", err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
// Initialize custom loggers
|
||||
initLoggers()
|
||||
|
||||
// Load environment variables
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Printf("Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
// Check for required environment variables
|
||||
checkRequiredEnvVars()
|
||||
// Log the start of the application
|
||||
InfoLogger.Println("Starting Telegram Bot Application")
|
||||
|
||||
// Initialize database
|
||||
db, err := initDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing database: %v", err)
|
||||
ErrorLogger.Fatalf("Error initializing database: %v", err)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := loadConfig("config.json")
|
||||
// Load all bot configurations
|
||||
configs, err := loadAllConfigs("config")
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading configuration: %v", err)
|
||||
ErrorLogger.Fatalf("Error loading configurations: %v", err)
|
||||
}
|
||||
|
||||
// Create Bot instance
|
||||
b, err := NewBot(db, config)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating bot: %v", err)
|
||||
}
|
||||
// Create a WaitGroup to manage goroutines
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Set up context with cancellation
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
// Start the bot
|
||||
log.Println("Starting bot...")
|
||||
b.Start(ctx)
|
||||
}
|
||||
// Initialize and start each bot
|
||||
for _, config := range configs {
|
||||
wg.Add(1)
|
||||
go func(cfg BotConfig) {
|
||||
defer wg.Done()
|
||||
|
||||
func initLogger() (*os.File, error) {
|
||||
logFile, err := os.OpenFile("bot.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mw := io.MultiWriter(os.Stdout, logFile)
|
||||
log.SetOutput(mw)
|
||||
return logFile, nil
|
||||
}
|
||||
// Create Bot instance without TelegramClient initially
|
||||
realClock := RealClock{}
|
||||
bot, err := NewBot(db, cfg, realClock, nil)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error creating bot %s: %v", cfg.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
func checkRequiredEnvVars() {
|
||||
requiredEnvVars := []string{"TELEGRAM_BOT_TOKEN", "ANTHROPIC_API_KEY"}
|
||||
for _, envVar := range requiredEnvVars {
|
||||
if os.Getenv(envVar) == "" {
|
||||
log.Fatalf("%s environment variable is not set", envVar)
|
||||
}
|
||||
// Start the bot in a separate goroutine
|
||||
go bot.Start(ctx)
|
||||
|
||||
// Keep the bot running until the context is cancelled
|
||||
<-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.")
|
||||
}
|
||||
|
||||
@@ -6,31 +6,86 @@ import (
|
||||
"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 {
|
||||
gorm.Model
|
||||
ChatID int64
|
||||
UserID int64
|
||||
Username string
|
||||
UserRole string
|
||||
Text string
|
||||
Timestamp time.Time
|
||||
IsUser bool
|
||||
BotID uint `gorm:"index"`
|
||||
ChatID int64 `gorm:"index"`
|
||||
UserID int64 `gorm:"index"`
|
||||
Username string `gorm:"index"`
|
||||
UserRole string // Store the role as a string
|
||||
Text string `gorm:"type:text"`
|
||||
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 {
|
||||
Messages []Message
|
||||
Size int
|
||||
Messages []Message
|
||||
Size int
|
||||
BusinessConnectionID string // New field to store the business connection ID
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
// Scope name constants — used in DB seeding, hasScope checks, and tests.
|
||||
const (
|
||||
ScopeStatsViewOwn = "stats:view:own"
|
||||
ScopeStatsViewAny = "stats:view:any"
|
||||
ScopeHistoryClearOwn = "history:clear:own"
|
||||
ScopeHistoryClearAny = "history:clear:any"
|
||||
ScopeHistoryClearHardOwn = "history:clear_hard:own"
|
||||
ScopeHistoryClearHardAny = "history:clear_hard:any"
|
||||
ScopeModelSet = "model:set"
|
||||
ScopeUserPromote = "user:promote"
|
||||
ScopeTTSUse = "tts:use"
|
||||
)
|
||||
|
||||
type Scope struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Scopes []Scope `gorm:"many2many:role_scopes;"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
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
|
||||
RoleID uint
|
||||
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
@@ -7,10 +7,12 @@ import (
|
||||
)
|
||||
|
||||
type userLimiter struct {
|
||||
hourlyLimiter *rate.Limiter
|
||||
dailyLimiter *rate.Limiter
|
||||
lastReset time.Time
|
||||
banUntil time.Time
|
||||
hourlyLimiter *rate.Limiter
|
||||
dailyLimiter *rate.Limiter
|
||||
lastHourlyReset time.Time
|
||||
lastDailyReset time.Time
|
||||
banUntil time.Time
|
||||
clock Clock
|
||||
}
|
||||
|
||||
func (b *Bot) checkRateLimits(userID int64) bool {
|
||||
@@ -20,26 +22,47 @@ func (b *Bot) checkRateLimits(userID int64) bool {
|
||||
limiter, exists := b.userLimiters[userID]
|
||||
if !exists {
|
||||
limiter = &userLimiter{
|
||||
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),
|
||||
lastReset: time.Now(),
|
||||
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),
|
||||
lastHourlyReset: b.clock.Now(),
|
||||
lastDailyReset: b.clock.Now(),
|
||||
clock: b.clock,
|
||||
}
|
||||
b.userLimiters[userID] = limiter
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := limiter.clock.Now()
|
||||
|
||||
// Check if the user is currently banned
|
||||
if now.Before(limiter.banUntil) {
|
||||
return false
|
||||
}
|
||||
|
||||
if now.Sub(limiter.lastReset) >= 24*time.Hour {
|
||||
limiter.dailyLimiter = rate.NewLimiter(rate.Every(24*time.Hour/time.Duration(b.config.MessagePerDay)), b.config.MessagePerDay)
|
||||
limiter.lastReset = now
|
||||
// Reset hourly limiter if an hour has passed since the last reset
|
||||
if now.Sub(limiter.lastHourlyReset) >= time.Hour {
|
||||
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() {
|
||||
banDuration, _ := time.ParseDuration(b.config.TempBanDuration)
|
||||
// Reset daily limiter if 24 hours have passed since the last reset
|
||||
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)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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)
|
||||
SendAudio(ctx context.Context, params *bot.SendAudioParams) (*models.Message, error)
|
||||
SetMyCommands(ctx context.Context, params *bot.SetMyCommandsParams) (bool, error)
|
||||
GetFile(ctx context.Context, params *bot.GetFileParams) (*models.File, error)
|
||||
FileDownloadLink(f *models.File) string
|
||||
Start(ctx context.Context)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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)
|
||||
SendAudioFunc func(ctx context.Context, params *bot.SendAudioParams) (*models.Message, error)
|
||||
SetMyCommandsFunc func(ctx context.Context, params *bot.SetMyCommandsParams) (bool, error)
|
||||
GetFileFunc func(ctx context.Context, params *bot.GetFileParams) (*models.File, error)
|
||||
FileDownloadLinkFunc func(f *models.File) string
|
||||
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)
|
||||
}
|
||||
|
||||
// SetMyCommands mocks registering bot commands.
|
||||
func (m *MockTelegramClient) SetMyCommands(ctx context.Context, params *bot.SetMyCommandsParams) (bool, error) {
|
||||
if m.SetMyCommandsFunc != nil {
|
||||
return m.SetMyCommandsFunc(ctx, params)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendAudio mocks sending an audio message.
|
||||
func (m *MockTelegramClient) SendAudio(ctx context.Context, params *bot.SendAudioParams) (*models.Message, error) {
|
||||
if m.SendAudioFunc != nil {
|
||||
return m.SendAudioFunc(ctx, params)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetFile mocks retrieving file info from Telegram.
|
||||
func (m *MockTelegramClient) GetFile(ctx context.Context, params *bot.GetFileParams) (*models.File, error) {
|
||||
if m.GetFileFunc != nil {
|
||||
return m.GetFileFunc(ctx, params)
|
||||
}
|
||||
return &models.File{}, nil
|
||||
}
|
||||
|
||||
// FileDownloadLink mocks building the file download URL.
|
||||
func (m *MockTelegramClient) FileDownloadLink(f *models.File) string {
|
||||
if m.FileDownloadLinkFunc != nil {
|
||||
return m.FileDownloadLinkFunc(f)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Start mocks starting the Telegram client.
|
||||
func (m *MockTelegramClient) Start(ctx context.Context) {
|
||||
if m.StartFunc != nil {
|
||||
m.StartFunc(ctx)
|
||||
return
|
||||
}
|
||||
m.Called(ctx)
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
errOpenDB = "Failed to open in-memory database: %v"
|
||||
errMigrateSchema = "Failed to migrate database schema: %v"
|
||||
errCreateRoles = "Failed to create default roles: %v"
|
||||
errCreateScopes = "Failed to create default scopes: %v"
|
||||
errCreateBot = "Failed to create bot: %v"
|
||||
memoryDSN = ":memory:"
|
||||
)
|
||||
|
||||
func TestOwnerAssignment(t *testing.T) {
|
||||
// Initialize loggers
|
||||
initLoggers()
|
||||
|
||||
// Initialize in-memory database for testing
|
||||
db, err := gorm.Open(sqlite.Open(memoryDSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf(errOpenDB, err)
|
||||
}
|
||||
|
||||
// Migrate the schema
|
||||
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{}, &Scope{})
|
||||
if err != nil {
|
||||
t.Fatalf(errMigrateSchema, err)
|
||||
}
|
||||
|
||||
// Create default roles and scopes
|
||||
err = createDefaultRoles(db)
|
||||
if err != nil {
|
||||
t.Fatalf(errCreateRoles, err)
|
||||
}
|
||||
if err := createDefaultScopes(db); err != nil {
|
||||
t.Fatalf(errCreateScopes, 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(errCreateBot, 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(memoryDSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf(errOpenDB, err)
|
||||
}
|
||||
|
||||
// Migrate the schema
|
||||
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{}, &Scope{})
|
||||
if err != nil {
|
||||
t.Fatalf(errMigrateSchema, err)
|
||||
}
|
||||
|
||||
// Create default roles and scopes
|
||||
err = createDefaultRoles(db)
|
||||
if err != nil {
|
||||
t.Fatalf(errCreateRoles, err)
|
||||
}
|
||||
if err := createDefaultScopes(db); err != nil {
|
||||
t.Fatalf(errCreateScopes, 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(errCreateBot, 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(memoryDSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf(errOpenDB, err)
|
||||
}
|
||||
|
||||
// Migrate the schema
|
||||
err = db.AutoMigrate(&BotModel{}, &ConfigModel{}, &Message{}, &User{}, &Role{}, &Scope{})
|
||||
if err != nil {
|
||||
t.Fatalf(errMigrateSchema, err)
|
||||
}
|
||||
|
||||
// Create default roles and scopes
|
||||
err = createDefaultRoles(db)
|
||||
if err != nil {
|
||||
t.Fatalf(errCreateRoles, err)
|
||||
}
|
||||
if err := createDefaultScopes(db); err != nil {
|
||||
t.Fatalf(errCreateScopes, 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(errCreateBot, 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
|
||||
Reference in New Issue
Block a user