refactor logger #2

Merged
trle5 merged 27 commits from log into alpha 2025-07-04 01:26:44 +08:00
55 changed files with 7652 additions and 3896 deletions

8
.example.env Normal file
View File

@@ -0,0 +1,8 @@
BOT_TOKEN="114514:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
WEBHOOK_URL="https://api.example.com/telegram-webhook"
DEBUG="true"
LOG_LEVEL="info"
LOG_FILE_LEVEL="warn"
CONFIG_PATH_TO_FILE="./path/to/your_config_file.yaml"
CONFIG_DIRECTORY="." # if use this, must have a config file name `config.yaml` in directory
FFMPEG_PATH="./ffmpeg/bin/ffmpeg.exe"

6
.gitignore vendored
View File

@@ -25,10 +25,12 @@ go.work
log.txt
trbot
__debug_bin*
db_yaml/metadata.yaml
/db_yaml/udonese
/cache
/db_yaml/metadata.yaml
/db_yaml/udonese
/db_yaml/savedmessage
/db_yaml/limitmessage
/db_yaml/detectkeyword
/db_yaml/teamspeak
config.yaml
/ffmpeg

2
.vscode/launch.json vendored
View File

@@ -12,7 +12,7 @@
"env": {},
"args": [],
"cwd": "${workspaceFolder}",
"output": "${workspaceFolder}/output/debug_app"
"output": "${workspaceFolder}/__debug_bin"
}
]
}

15
Makefile Normal file
View File

@@ -0,0 +1,15 @@
COMMIT := $(shell git rev-parse HEAD)
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
VERSION := $(shell git describe --tags --always)
CHANGES := $(shell git status -s | wc -l)
TIME := $(shell date --rfc-3339=seconds)
HOSTNAME := $(shell hostname)
LDFLAGS := -X 'trbot/utils/consts.Commit=$(COMMIT)' \
-X 'trbot/utils/consts.Branch=$(BRANCH)' \
-X 'trbot/utils/consts.Version=$(VERSION)' \
-X 'trbot/utils/consts.Changes=$(CHANGES)' \
-X 'trbot/utils/consts.BuildAt=$(TIME)' \
-X 'trbot/utils/consts.BuildOn=$(HOSTNAME)'
build:
go build -ldflags "$(LDFLAGS)"

View File

@@ -49,7 +49,6 @@ const (
LatestInlineResult ChatInfoField_LatestData = "LatestInlineResult"
LatestCallbackQueryData ChatInfoField_LatestData = "LatestCallbackQueryData"
)
type ChatInfoField_UsageCount string

View File

@@ -2,12 +2,13 @@ package database
import (
"context"
"log"
"trbot/database/db_struct"
"trbot/database/redis_db"
"trbot/database/yaml_db"
"trbot/utils"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
)
type DatabaseBackend struct {
@@ -17,10 +18,11 @@ type DatabaseBackend struct {
// 数据库等级,低优先级的数据库不会实时同步更改,程序仅会在高优先级数据库不可用才会尝试使用其中的数据
IsLowLevel bool
// 是否已被成功初始化
Initializer func() (bool, error)
IsInitialized bool
InitializedErr error
Initializer func(ctx context.Context) error // 数据库初始化函数
// 数据库保存和读取函数
SaveDatabase func(ctx context.Context) error
ReadDatabase func(ctx context.Context) error
// 操作数据库的函数
InitUser func(ctx context.Context, user *models.User) error
@@ -35,31 +37,40 @@ type DatabaseBackend struct {
var DBBackends []DatabaseBackend
var DBBackends_LowLevel []DatabaseBackend
func AddDatabaseBackend(backends ...DatabaseBackend) int {
func AddDatabaseBackends(ctx context.Context, backends ...DatabaseBackend) int {
logger := zerolog.Ctx(ctx)
if DBBackends == nil { DBBackends = []DatabaseBackend{} }
if DBBackends_LowLevel == nil { DBBackends_LowLevel = []DatabaseBackend{} }
var count int
for _, db := range backends {
db.IsInitialized, db.InitializedErr = db.Initializer()
if db.IsInitialized {
err := db.Initializer(ctx)
if err != nil {
logger.Error().
Err(err).
Str("database", db.Name).
Msg("Failed to initialize database")
} else {
if db.IsLowLevel {
DBBackends_LowLevel = append(DBBackends_LowLevel, db)
} else {
DBBackends = append(DBBackends, db)
}
log.Printf("Initialized database backend [%s]", db.Name)
logger.Info().
Str("database", db.Name).
Str("databaseLevel", utils.TextForTrueOrFalse(db.IsLowLevel, "low", "high")).
Msg("Database initialized")
count++
} else {
log.Printf("Failed to initialize database backend [%s]: %s", db.Name, db.InitializedErr)
}
}
return count
}
func InitAndListDatabases() {
AddDatabaseBackend(DatabaseBackend{
func InitAndListDatabases(ctx context.Context) {
logger := zerolog.Ctx(ctx)
AddDatabaseBackends(ctx, DatabaseBackend{
Name: "redis",
Initializer: redis_db.InitializeDB,
@@ -72,11 +83,14 @@ func InitAndListDatabases() {
SetCustomFlag: redis_db.SetCustomFlag,
})
AddDatabaseBackend(DatabaseBackend{
AddDatabaseBackends(ctx, DatabaseBackend{
Name: "yaml",
IsLowLevel: true,
Initializer: yaml_db.InitializeDB,
SaveDatabase: yaml_db.SaveDatabase,
ReadDatabase: yaml_db.ReadDatabase,
InitUser: yaml_db.InitUser,
InitChat: yaml_db.InitChat,
GetChatInfo: yaml_db.GetChatInfo,
@@ -86,16 +100,13 @@ func InitAndListDatabases() {
SetCustomFlag: yaml_db.SetCustomFlag,
})
for _, backend := range DBBackends {
log.Printf("Database backend [%s] is available (High-level)", backend.Name)
}
for _, backend := range DBBackends_LowLevel {
log.Printf("Database backend [%s] is available (Low-level)", backend.Name)
if len(DBBackends) + len(DBBackends_LowLevel) == 0 {
logger.Fatal().
Msg("No database available")
}
if len(DBBackends) + len(DBBackends_LowLevel) == 0 {
log.Fatalln("No database available")
} else {
log.Printf("Available databases: [H: %d, L: %d]", len(DBBackends), len(DBBackends_LowLevel))
}
logger.Info().
Int("highLevel", len(DBBackends)).
Int("lowLevel", len(DBBackends_LowLevel)).
Msg("Available databases")
}

View File

@@ -3,11 +3,14 @@ package database
import (
"context"
"fmt"
"trbot/database/db_struct"
"github.com/go-telegram/bot/models"
)
// 需要给一些函数加上一个 success 返回值,有时部分数据库不可用,但数据成功保存到了其他数据库
func InitChat(ctx context.Context, chat *models.Chat) error {
var allErr error
for _, db := range DBBackends {
@@ -42,13 +45,22 @@ func InitUser(ctx context.Context, user *models.User) error {
return allErr
}
func GetChatInfo(ctx context.Context, chatID int64) (*db_struct.ChatInfo, error) {
func GetChatInfo(ctx context.Context, chatID int64) (data *db_struct.ChatInfo, err error) {
// 优先从高优先级数据库获取数据
for _, db := range DBBackends {
return db.GetChatInfo(ctx, chatID)
data, err = db.GetChatInfo(ctx, chatID)
if err == nil {
return
}
}
for _, db := range DBBackends_LowLevel {
return db.GetChatInfo(ctx, chatID)
data, err = db.GetChatInfo(ctx, chatID)
if err == nil {
return
}
}
if err != nil {
return
}
return nil, fmt.Errorf("no database available")
}
@@ -120,3 +132,49 @@ func SetCustomFlag(ctx context.Context, chatID int64, fieldName db_struct.ChatIn
}
return allErr
}
func SaveDatabase(ctx context.Context) error {
var allErr error
for _, db := range DBBackends {
if db.SaveDatabase == nil {
continue
}
err := db.SaveDatabase(ctx)
if err != nil {
allErr = err
}
}
for _, db := range DBBackends_LowLevel {
if db.SaveDatabase == nil {
continue
}
err := db.SaveDatabase(ctx)
if err != nil {
allErr = fmt.Errorf("%s, %s", allErr, err)
}
}
return allErr
}
func ReadDatabase(ctx context.Context) error {
var allErr error
for _, db := range DBBackends {
if db.ReadDatabase == nil {
continue
}
err := db.ReadDatabase(ctx)
if err != nil {
allErr = err
}
}
for _, db := range DBBackends_LowLevel {
if db.ReadDatabase == nil {
continue
}
err := db.ReadDatabase(ctx)
if err != nil {
allErr = fmt.Errorf("%s, %s", allErr, err)
}
}
return allErr
}

View File

@@ -3,68 +3,48 @@ package redis_db
import (
"context"
"fmt"
"log"
"reflect"
"strconv"
"time"
"trbot/database/db_struct"
"trbot/utils"
"trbot/utils/consts"
"trbot/utils/configs"
"github.com/go-telegram/bot/models"
"github.com/redis/go-redis/v9"
)
var MainDB *redis.Client // 配置文件
var UserDB *redis.Client // 用户数据
var ctxbg = context.Background()
func InitializeDB() (bool, error) {
if consts.RedisURL != "" {
if consts.RedisMainDB != -1 {
MainDB = redis.NewClient(&redis.Options{
Addr: consts.RedisURL,
Password: consts.RedisPassword,
DB: consts.RedisMainDB,
})
err := PingRedis(ctxbg, MainDB)
if err != nil {
return false, fmt.Errorf("error ping Redis MainDB: %s", err)
}
}
if consts.RedisUserInfoDB != -1 {
func InitializeDB(ctx context.Context) error {
if configs.BotConfig.RedisURL != "" {
if configs.BotConfig.RedisDatabaseID != -1 {
UserDB = redis.NewClient(&redis.Options{
Addr: consts.RedisURL,
Password: consts.RedisPassword,
DB: consts.RedisUserInfoDB,
Addr: configs.BotConfig.RedisURL,
Password: configs.BotConfig.RedisPassword,
DB: configs.BotConfig.RedisDatabaseID,
})
err := PingRedis(ctxbg, UserDB)
err := UserDB.Ping(ctx).Err()
if err != nil {
return false, fmt.Errorf("error ping Redis UserDB: %s", err)
return fmt.Errorf("failed to ping Redis [%d] database: %w", configs.BotConfig.RedisDatabaseID, err)
}
}
return true, nil
return nil
} else {
return false, fmt.Errorf("RedisURL is empty")
return fmt.Errorf("RedisURL is empty")
}
}
func PingRedis(ctx context.Context, db *redis.Client) error {
_, err := db.Ping(ctx).Result()
return err
}
// 保存用户信息
func SaveChatInfo(ctx context.Context, chatInfo *db_struct.ChatInfo) error {
if chatInfo == nil {
return fmt.Errorf("chatInfo 不能为空")
return fmt.Errorf("failed to save chat info: chatInfo is nil")
}
key := strconv.FormatInt(chatInfo.ID, 10)
v := reflect.ValueOf(*chatInfo) // 解除指针获取值
v := reflect.ValueOf(*chatInfo)
t := reflect.TypeOf(*chatInfo)
data := make(map[string]interface{})
@@ -82,7 +62,7 @@ func GetChatInfo(ctx context.Context, chatID int64) (*db_struct.ChatInfo, error)
key := strconv.FormatInt(chatID, 10)
data, err := UserDB.HGetAll(ctx, key).Result()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get chat info: %w", err)
}
if len(data) == 0 {
return nil, nil
@@ -116,7 +96,7 @@ func GetChatInfo(ctx context.Context, chatID int64) (*db_struct.ChatInfo, error)
func InitUser(ctx context.Context, user *models.User) error {
chatData, err := GetChatInfo(ctx, user.ID)
if err != nil {
return fmt.Errorf("[UserDB] Error getting chat info from Redis: %v", err)
return fmt.Errorf("failed to get chat info: %w", err)
}
if chatData == nil {
var newUser = db_struct.ChatInfo{
@@ -128,82 +108,77 @@ func InitUser(ctx context.Context, user *models.User) error {
err = SaveChatInfo(ctx, &newUser)
if err != nil {
return fmt.Errorf("[UserDB] Error saving user info to Redis: %v", err)
return fmt.Errorf("failed to init new user: %w", err)
}
log.Printf("newUser: \"%s\"(%d)\n", newUser.ChatName, user.ID)
return nil
} else {
log.Printf("oldUser: \"%s\"(%d)\n", chatData.ChatName, chatData.ID)
return nil
}
return nil
}
func InitChat(ctx context.Context, chat *models.Chat) error {
chatData, err := GetChatInfo(ctx, chat.ID)
if err != nil {
return fmt.Errorf("[UserDB] Error getting chat info from Redis: %v", err)
return fmt.Errorf("failed to get chat info: %w", err)
}
if chatData == nil {
var newChat = db_struct.ChatInfo{
ID: chat.ID,
ChatName: utils.ShowChatName(chat),
ChatType: models.ChatTypePrivate,
ChatType: chat.Type,
AddTime: time.Now().Format(time.RFC3339),
}
err = SaveChatInfo(ctx, &newChat)
if err != nil {
return fmt.Errorf("[UserDB] Error saving chat info to Redis: %v", err)
return fmt.Errorf("failed to init new chat: %w", err)
}
log.Printf("newChat: \"%s\"(%d)\n", newChat.ChatName, newChat.ID)
return nil
} else {
log.Printf("oldChat: \"%s\"(%d)\n", chatData.ChatName, chatData.ID)
return nil
}
return nil
}
func IncrementalUsageCount(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_UsageCount) error {
count, err := UserDB.HGet(ctx, strconv.FormatInt(chatID, 10), string(fieldName)).Int()
if err == nil {
err = UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), fieldName, count + 1).Err()
if err == nil {
return nil
}
} else if err == redis.Nil {
err = UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), fieldName, 1).Err()
if err == nil {
log.Printf("[UserDB] Key %s not found, creating in Redis\n", fieldName)
return nil
if err != nil {
if err == redis.Nil {
err = UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), string(fieldName), 0).Err()
if err != nil {
return fmt.Errorf("failed to create empty [%s] key: %w", string(fieldName), err)
}
} else {
return fmt.Errorf("failed to get [%s] usage count: %w", string(fieldName), err)
}
}
return fmt.Errorf("[UserDB] Error incrementing usage count to Redis: %v", err)
err = UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), string(fieldName), count + 1).Err()
if err != nil {
return fmt.Errorf("failed to incrementing [%s] usage count: %w", string(fieldName), err)
}
return nil
}
func RecordLatestData(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_LatestData, value string) error {
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), fieldName, value).Err()
if err == nil {
return nil
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), string(fieldName), value).Err()
if err != nil {
return fmt.Errorf("failed to record latest [%s] data: %w", string(fieldName), err)
}
return fmt.Errorf("[UserDB] Error saving chat info to Redis: %v", err)
return nil
}
func UpdateOperationStatus(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_Status, value bool) error {
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), fieldName, value).Err()
if err == nil {
return nil
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), string(fieldName), value).Err()
if err != nil {
return fmt.Errorf("failed to update operation [%s] status: %w", string(fieldName), err)
}
return fmt.Errorf("[UserDB] Error update operation status to Redis: %v", err)
return nil
}
func SetCustomFlag(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_CustomFlag, value string) error {
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), fieldName, value).Err()
if err == nil {
return nil
err := UserDB.HSet(ctx, strconv.FormatInt(chatID, 10), string(fieldName), value).Err()
if err != nil {
return fmt.Errorf("failed to set custom [%s] flag: %w", string(fieldName), err)
}
return fmt.Errorf("[UserDB] Error setting custom flag to Redis: %v", err)
return nil
}

152
database/utils.go Normal file
View File

@@ -0,0 +1,152 @@
package database
import (
"strings"
"trbot/database/db_struct"
"trbot/utils"
"trbot/utils/handler_structs"
"trbot/utils/type/update_utils"
"github.com/rs/zerolog"
)
func RecordData(params *handler_structs.SubHandlerParams) {
logger := zerolog.Ctx(params.Ctx).
With().
Str("funcName", "RecordData").
Logger()
updateType := update_utils.GetUpdateType(params.Update)
switch {
case updateType.Message:
if params.Update.Message.Text != "" {
params.Fields = strings.Fields(params.Update.Message.Text)
}
err := InitChat(params.Ctx, &params.Update.Message.Chat)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Msg("Failed to init chat")
}
err = IncrementalUsageCount(params.Ctx, params.Update.Message.Chat.ID, db_struct.MessageNormal)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Msg("Failed to incremental `message` usage count")
}
err = RecordLatestData(params.Ctx, params.Update.Message.Chat.ID, db_struct.LatestMessage, params.Update.Message.Text)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Msg("Failed to record latest `message text` data")
}
params.ChatInfo, err = GetChatInfo(params.Ctx, params.Update.Message.Chat.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Msg("Failed to get chat info")
}
case updateType.EditedMessage:
// no ?
case updateType.InlineQuery:
if params.Update.InlineQuery.Query != "" {
params.Fields = strings.Fields(params.Update.InlineQuery.Query)
}
err := InitUser(params.Ctx, params.Update.InlineQuery.From)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.InlineQuery.From)).
Msg("Failed to init user")
}
err = IncrementalUsageCount(params.Ctx, params.Update.InlineQuery.From.ID, db_struct.InlineRequest)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.InlineQuery.From)).
Msg("Failed to incremental `inline request` usage count")
}
err = RecordLatestData(params.Ctx, params.Update.InlineQuery.From.ID, db_struct.LatestInlineQuery, params.Update.InlineQuery.Query)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.InlineQuery.From)).
Msg("Failed to record latest `inline query` data")
}
params.ChatInfo, err = GetChatInfo(params.Ctx, params.Update.InlineQuery.From.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.InlineQuery.From)).
Msg("Failed to get user info")
}
case updateType.ChosenInlineResult:
if params.Update.ChosenInlineResult.Query != "" {
params.Fields = strings.Fields(params.Update.ChosenInlineResult.Query)
}
err := InitUser(params.Ctx, &params.Update.ChosenInlineResult.From)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.ChosenInlineResult.From)).
Msg("Failed to init user")
}
err = IncrementalUsageCount(params.Ctx, params.Update.ChosenInlineResult.From.ID, db_struct.InlineResult)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.ChosenInlineResult.From)).
Msg("Failed to incremental `inline result` usage count")
}
err = RecordLatestData(params.Ctx, params.Update.ChosenInlineResult.From.ID, db_struct.LatestInlineResult, params.Update.ChosenInlineResult.ResultID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.ChosenInlineResult.From)).
Msg("failed to record latest `inline result` data")
}
params.ChatInfo, err = GetChatInfo(params.Ctx, params.Update.ChosenInlineResult.From.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.ChosenInlineResult.From)).
Msg("Failed to get user info")
}
case updateType.CallbackQuery:
err := InitUser(params.Ctx, &params.Update.CallbackQuery.From)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Msg("Failed to init user")
}
err = IncrementalUsageCount(params.Ctx, params.Update.CallbackQuery.From.ID, db_struct.CallbackQuery)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Msg("Failed to incremental `callback query` usage count")
}
err = RecordLatestData(params.Ctx, params.Update.CallbackQuery.From.ID, db_struct.LatestCallbackQueryData, params.Update.CallbackQuery.Data)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Msg("Failed to record latest `callback query` data")
}
params.ChatInfo, err = GetChatInfo(params.Ctx, params.Update.CallbackQuery.From.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.ChosenInlineResult.From)).
Msg("Failed get user info")
}
}
}

View File

@@ -3,22 +3,25 @@ package yaml_db
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"time"
"trbot/database/db_struct"
"trbot/utils"
"trbot/utils/consts"
"trbot/utils/mess"
"trbot/utils/yaml"
"github.com/go-telegram/bot/models"
"gopkg.in/yaml.v3"
"github.com/rs/zerolog"
)
var Database DataBaseYaml
var YAMLDatabasePath = filepath.Join(consts.YAMLDataBaseDir, consts.YAMLFileName)
// 需要重构,错误信息不足
type DataBaseYaml struct {
// 如果运行中希望程序强制读取新数据,在 YAML 数据库文件的开头添加 FORCEOVERWRITE: true 即可
ForceOverwrite bool `yaml:"FORCEOVERWRITE,omitempty"`
@@ -29,156 +32,267 @@ type DataBaseYaml struct {
} `yaml:"Data"`
}
func InitializeDB() (bool, error) {
if consts.DB_path != "" {
var err error
Database, err = ReadYamlDB(consts.DB_path + consts.MetadataFileName)
func InitializeDB(ctx context.Context) error {
if consts.YAMLDataBaseDir != "" {
err := ReadDatabase(ctx)
if err != nil {
return false, fmt.Errorf("read yaml db error: %s", err)
return fmt.Errorf("failed to read yaml database: %s", err)
}
return true, nil
return nil
} else {
return false, fmt.Errorf("DB path is empty")
return fmt.Errorf("yaml database path is empty")
}
}
func ReadYamlDB(pathToFile string) (DataBaseYaml, error) {
file, err := os.Open(pathToFile)
func SaveDatabase(ctx context.Context) error {
logger := zerolog.Ctx(ctx).
With().
Str("database", "yaml").
Str("funcName", "SaveDatabase").
Logger()
Database.UpdateTimestamp = time.Now().Unix()
err := yaml.SaveYAML(YAMLDatabasePath, &Database)
if err != nil {
log.Println("[Database_yaml]: Not found Database file. Created new one")
err = SaveYamlDB(consts.DB_path, consts.MetadataFileName, DataBaseYaml{})
if err != nil {
return DataBaseYaml{}, err
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to save database")
return fmt.Errorf("failed to save database: %w", err)
}
return nil
}
func ReadDatabase(ctx context.Context) error {
logger := zerolog.Ctx(ctx).
With().
Str("database", "yaml").
Str("funcName", "ReadDatabase").
Logger()
err := yaml.LoadYAML(YAMLDatabasePath, &Database)
if err != nil {
if os.IsNotExist(err) {
logger.Warn().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Not found database file. Created new one")
// 如果是找不到文件,新建一个
err = yaml.SaveYAML(YAMLDatabasePath, &Database)
if err != nil {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to create empty database file")
return fmt.Errorf("failed to create empty database file: %w", err)
}
} else {
return DataBaseYaml{}, nil
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to read database file")
return fmt.Errorf("failed to read database file: %w", err)
}
}
defer file.Close()
var Database DataBaseYaml
decoder := yaml.NewDecoder(file)
err = decoder.Decode(&Database)
return nil
}
func ReadYamlDB(ctx context.Context, pathToFile string) (*DataBaseYaml, error) {
logger := zerolog.Ctx(ctx).
With().
Str("database", "yaml").
Str("funcName", "ReadYamlDB").
Logger()
var tempDatabase *DataBaseYaml
err := yaml.LoadYAML(pathToFile, &tempDatabase)
if err != nil {
if err == io.EOF {
log.Println("[Database]: Database looks empty. now format it")
SaveYamlDB(consts.DB_path, consts.MetadataFileName, DataBaseYaml{})
return DataBaseYaml{}, nil
if os.IsNotExist(err) {
logger.Warn().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Not found database file. Created new one")
// 如果是找不到文件,新建一个
err = yaml.SaveYAML(YAMLDatabasePath, &tempDatabase)
if err != nil {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to create empty database file")
return nil, fmt.Errorf("failed to create empty database file: %w", err)
}
} else {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to read database file")
return nil, fmt.Errorf("failed to read database file: %w", err)
}
return DataBaseYaml{}, err
}
return Database, nil
return tempDatabase, nil
}
// 路径 文件名 YAML 数据结构体
func SaveYamlDB(path string, name string, Database interface{}) error {
data, err := yaml.Marshal(Database)
if err != nil { return err }
func SaveYamlDB(ctx context.Context, path, name string, tempDatabase interface{}) error {
logger := zerolog.Ctx(ctx).
With().
Str("database", "yaml").
Str("funcName", "SaveDatabase").
Logger()
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
}
if _, err := os.Stat(path + name); os.IsNotExist(err) {
_, err := os.Create(path + name)
if err != nil {
return err
}
}
return os.WriteFile(path + name, data, 0644)
}
// 添加数据
func addToYamlDB(params *db_struct.ChatInfo) {
Database.Data.ChatInfo = append(Database.Data.ChatInfo, *params)
}
func AutoSaveDatabaseHandler() {
// 先读取一下数据库文件
savedDatabase, err := ReadYamlDB(consts.DB_path + consts.MetadataFileName)
err := yaml.SaveYAML(filepath.Join(path, name), &tempDatabase)
if err != nil {
log.Println("some issues when read Database file", err)
// 如果读取数据库文件时发现数据库为空,使用当前的数据重建数据库文件
if reflect.DeepEqual(savedDatabase, DataBaseYaml{}){
mess.PrintLogAndSave("The Database file is empty, recovering Database file using current data")
err = SaveYamlDB(consts.DB_path, consts.MetadataFileName, Database)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when recovering empty Database:", err))
} else {
mess.PrintLogAndSave(fmt.Sprintf("The Database is recovered to %s", consts.DB_path + consts.MetadataFileName))
}
return
}
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to save database")
return fmt.Errorf("failed to save database: %w", err)
}
// 没有修改就跳过保存
if reflect.DeepEqual(savedDatabase, Database) && consts.IsDebugMode {
log.Println("looks Database no any change, skip autosave this time")
} else {
// 如果数据库文件中有设定专用的 `FORCEOVERWRITE: true` 覆写标记,无论任何修改,先保存程序中的数据,再读取新的数据替换掉当前的数据并保存
if savedDatabase.ForceOverwrite {
mess.PrintLogAndSave(fmt.Sprintf("The `FORCEOVERWRITE: true` in %s is detected", consts.DB_path + consts.MetadataFileName))
oldFileName := fmt.Sprintf("beforeOverwritten_%d_%s", time.Now().Unix(), consts.MetadataFileName)
err := SaveYamlDB(consts.DB_path, oldFileName, savedDatabase)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when saving the Database before overwritten:", err))
} else {
mess.PrintLogAndSave(fmt.Sprintf("The Database before overwritten is saved to %s", consts.DB_path + oldFileName))
}
Database = savedDatabase
Database.ForceOverwrite = false // 移除强制覆盖标记
err = SaveYamlDB(consts.DB_path, consts.MetadataFileName, Database)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when recreat Database using new Database:", err))
} else {
mess.PrintLogAndSave(fmt.Sprintf("Success read data from the new file and saved to %s", consts.DB_path + consts.MetadataFileName))
}
} else if savedDatabase.UpdateTimestamp > Database.UpdateTimestamp { // 没有设定覆写标记,检测到本地的数据库更新时间比程序中的更新时间更晚
log.Println("The saved Database is newer than current data in the program")
// 如果只是更新时间有差别,更新一下时间,再保存就行
if reflect.DeepEqual(savedDatabase.Data, Database.Data) {
log.Println("But current data and Database is the same, updating UpdateTimestamp in the Database only")
Database.UpdateTimestamp = time.Now().Unix()
err := SaveYamlDB(consts.DB_path, consts.MetadataFileName, Database)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when update Timestamp in Database:", err))
} else {
mess.PrintLogAndSave("Update Timestamp in Database at " + time.Now().Format(time.RFC3339))
}
} else {
// 数据库文件与程序中的数据不同,将新的数据文件改名另存为 `edited_时间戳_文件名`,再使用程序中的数据还原数据文件
log.Println("Saved Database is different from the current Database")
editedFileName := fmt.Sprintf("edited_%d_%s", time.Now().Unix(), consts.MetadataFileName)
// 提示不要在程序运行的时候乱动数据库文件
log.Println("Do not modify the Database file while the program is running, saving modified file and recovering Database file using current data")
err := SaveYamlDB(consts.DB_path, editedFileName, savedDatabase)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when saving modified Database:", err))
} else {
mess.PrintLogAndSave(fmt.Sprintf("The modified Database is saved to %s", consts.DB_path + editedFileName))
}
err = SaveYamlDB(consts.DB_path, consts.MetadataFileName, Database)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when recovering Database:", err))
} else {
mess.PrintLogAndSave(fmt.Sprintf("The Database is recovered to %s", consts.DB_path + consts.MetadataFileName))
}
}
} else { // 数据有更改,程序内的更新时间也比本地数据库晚,正常保存
// 正常情况下更新时间就是会比程序内的时间晚,手动修改数据库途中如果有数据变动,而手动修改的时候没有修改时间戳,不会触发上面的保护机制,会直接覆盖手动修改的内容
// 所以无论如何都尽量不要手动修改数据库文件,如果必要也请在开头添加专用的 `FORCEOVERWRITE: true` 覆写标记,或停止程序后再修改
Database.UpdateTimestamp = time.Now().Unix()
err := SaveYamlDB(consts.DB_path, consts.MetadataFileName, Database)
return nil
}
func AutoSaveDatabaseHandler(ctx context.Context) {
logger := zerolog.Ctx(ctx).
With().
Str("database", "yaml").
Str("funcName", "AutoSaveDatabaseHandler").
Logger()
// 先读取一下数据库文件
savedDatabase, err := ReadYamlDB(ctx, YAMLDatabasePath)
if err != nil {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to read database file")
} else {
// 如果读取数据库文件时发现数据库为空,使用当前的数据重建数据库文件
if savedDatabase == nil {
logger.Warn().
Str("path", YAMLDatabasePath).
Msg("The database file is empty, recover database file using current data")
err = SaveYamlDB(ctx, consts.YAMLDataBaseDir, consts.YAMLFileName, Database)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when auto saving Database:", err))
} else if consts.IsDebugMode {
mess.PrintLogAndSave("auto save at " + time.Now().Format(time.RFC3339))
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to recover database file using current data")
} else {
logger.Warn().
Str("path", YAMLDatabasePath).
Msg("The database file is recovered using current data")
}
} else if reflect.DeepEqual(*savedDatabase, Database) {
// 没有修改就跳过保存
logger.Debug().Msg("looks Database no any change, skip autosave this time")
} else {
// 如果数据库文件中有设定专用的 `FORCEOVERWRITE: true` 覆写标记
// 无论任何修改,先保存程序中的数据,再读取新的数据替换掉当前的数据并保存
if savedDatabase.ForceOverwrite {
logger.Warn().
Str("path", YAMLDatabasePath).
Msg("Detected `FORCEOVERWRITE: true` in database file, save current database to another file first")
oldFileName := fmt.Sprintf("beforeOverwritten_%d_%s", time.Now().Unix(), consts.YAMLFileName)
oldFilePath := filepath.Join(consts.YAMLDataBaseDir, oldFileName)
err := SaveYamlDB(ctx, consts.YAMLDataBaseDir, oldFileName, savedDatabase)
if err != nil {
logger.Warn().
Err(err).
Str("oldPath", oldFilePath).
Msg("Failed to save the database before overwrite")
} else {
logger.Warn().
Err(err).
Str("oldPath", oldFilePath).
Msg("The Database before overwrite is saved to another file")
}
Database = *savedDatabase
Database.ForceOverwrite = false // 移除强制覆盖标记
err = SaveYamlDB(ctx, consts.YAMLDataBaseDir, consts.YAMLFileName, Database)
if err != nil {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to save the database after overwrite")
} else {
logger.Warn().
Str("path", YAMLDatabasePath).
Msg("Read new database file and save to the old file")
}
} else {
// 没有设定覆写标记,检测到本地的数据库更新时间比程序中的更新时间更晚
if savedDatabase.UpdateTimestamp >= Database.UpdateTimestamp {
logger.Warn().
Msg("The database file is newer than current data in the program")
// 如果只是更新时间有差别,更新一下时间,再保存就行
if reflect.DeepEqual(savedDatabase.Data, Database.Data) {
logger.Warn().
Msg("But current data and Database is the same, updating UpdateTimestamp in the Database only")
Database.UpdateTimestamp = time.Now().Unix()
err := SaveYamlDB(ctx, consts.YAMLDataBaseDir, consts.YAMLFileName, Database)
if err != nil {
logger.Error().
Err(err).
Msg("Failed to save database after updating UpdateTimestamp")
}
} else {
// 数据库文件与程序中的数据不同,提示不要在程序运行的时候乱动数据库文件
logger.Warn().
Str("notice", "Do not modify the database file while the program is running, If you want to overwrite the current database, please add the field `FORCEOVERWRITE: true` at the beginning of the file.").
Msg("The database file is different from the current database, saving modified file and recovering database file using current data in the program")
// 将新的数据文件改名另存为 `edited_时间戳_文件名`,再使用程序中的数据还原数据文件
editedFileName := fmt.Sprintf("edited_%d_%s", time.Now().Unix(), consts.YAMLFileName)
editedFilePath := filepath.Join(consts.YAMLDataBaseDir, editedFileName)
err := SaveYamlDB(ctx, consts.YAMLDataBaseDir, editedFileName, savedDatabase)
if err != nil {
logger.Error().
Err(err).
Str("editedPath", editedFilePath).
Msg("Failed to save modified database")
} else {
logger.Warn().
Str("editedPath", editedFilePath).
Msg("The modified database is saved to another file")
}
err = SaveYamlDB(ctx, consts.YAMLDataBaseDir, consts.YAMLFileName, Database)
if err != nil {
logger.Error().
Err(err).
Str("path", YAMLDatabasePath).
Msg("Failed to recover database file")
} else {
logger.Warn().
Str("path", YAMLDatabasePath).
Msg("The database file is recovered using current data in the program")
}
}
} else {
// 数据有更改,程序内的更新时间也比本地数据库晚,正常保存
// 无论如何都尽量不要手动修改数据库文件,如果必要也请在开头添加专用的 `FORCEOVERWRITE: true` 覆写标记,或停止程序后再修改
Database.UpdateTimestamp = time.Now().Unix()
err := SaveYamlDB(ctx, consts.YAMLDataBaseDir, consts.YAMLFileName, Database)
if err != nil {
logger.Error().
Err(err).
Msg("Failed to auto save database")
} else {
logger.Debug().
Str("path", YAMLDatabasePath).
Msg("The database is auto saved")
}
}
}
}
}
}
// 初次添加群组时,获取必要信息
@@ -188,14 +302,13 @@ func InitChat(ctx context.Context, chat *models.Chat) error {
return nil // 群组已存在,不重复添加
}
}
addToYamlDB(&db_struct.ChatInfo{
Database.Data.ChatInfo = append(Database.Data.ChatInfo, db_struct.ChatInfo{
ID: chat.ID,
ChatType: chat.Type,
ChatName: utils.ShowChatName(chat),
AddTime: time.Now().Format(time.RFC3339),
})
consts.SignalsChannel.Database_save <- true
return nil
return SaveDatabase(ctx)
}
func InitUser(ctx context.Context, user *models.User) error {
@@ -204,14 +317,13 @@ func InitUser(ctx context.Context, user *models.User) error {
return nil // 用户已存在,不重复添加
}
}
addToYamlDB(&db_struct.ChatInfo{
Database.Data.ChatInfo = append(Database.Data.ChatInfo, db_struct.ChatInfo{
ID: user.ID,
ChatType: models.ChatTypePrivate,
ChatName: utils.ShowUserName(user),
AddTime: time.Now().Format(time.RFC3339),
})
consts.SignalsChannel.Database_save <- true
return nil
return SaveDatabase(ctx)
}
// 获取 ID 信息
@@ -227,6 +339,7 @@ func GetChatInfo(ctx context.Context, id int64) (*db_struct.ChatInfo, error) {
func IncrementalUsageCount(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_UsageCount) error {
for Index, Data := range Database.Data.ChatInfo {
if Data.ID == chatID {
Database.UpdateTimestamp = time.Now().Unix()
v := reflect.ValueOf(&Database.Data.ChatInfo[Index]).Elem()
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Name == string(fieldName) {
@@ -242,6 +355,7 @@ func IncrementalUsageCount(ctx context.Context, chatID int64, fieldName db_struc
func RecordLatestData(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_LatestData, value string) error {
for Index, Data := range Database.Data.ChatInfo {
if Data.ID == chatID {
Database.UpdateTimestamp = time.Now().Unix()
v := reflect.ValueOf(&Database.Data.ChatInfo[Index]).Elem()
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Name == string(fieldName) {
@@ -257,6 +371,7 @@ func RecordLatestData(ctx context.Context, chatID int64, fieldName db_struct.Cha
func UpdateOperationStatus(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_Status, value bool) error {
for Index, Data := range Database.Data.ChatInfo {
if Data.ID == chatID {
Database.UpdateTimestamp = time.Now().Unix()
v := reflect.ValueOf(&Database.Data.ChatInfo[Index]).Elem()
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Name == string(fieldName) {
@@ -272,6 +387,7 @@ func UpdateOperationStatus(ctx context.Context, chatID int64, fieldName db_struc
func SetCustomFlag(ctx context.Context, chatID int64, fieldName db_struct.ChatInfoField_CustomFlag, value string) error {
for Index, Data := range Database.Data.ChatInfo {
if Data.ID == chatID {
Database.UpdateTimestamp = time.Now().Unix()
v := reflect.ValueOf(&Database.Data.ChatInfo[Index]).Elem()
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Name == string(fieldName) {

View File

@@ -1,2 +0,0 @@
scp root@server:~/trbot/db_yaml/udonese/metadata.yaml db_yaml/udonese/metadata.yaml
scp root@server:~/trbot/db_yaml/metadata.yaml db_yaml/metadata.yaml

View File

@@ -1,3 +0,0 @@
BOT_TOKEN="114514:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
WEBHOOK_URL="https://api.example.com/telegram-webhook"
DEBUG="true"

4
go.mod
View File

@@ -8,7 +8,9 @@ require (
github.com/go-telegram/bot v1.15.0
github.com/joho/godotenv v1.5.1
github.com/multiplay/go-ts3 v1.2.0
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.7.1
github.com/rs/zerolog v1.34.0
golang.org/x/image v0.23.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -16,6 +18,8 @@ require (
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/sys v0.32.0 // indirect

17
go.sum
View File

@@ -4,25 +4,35 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-telegram/bot v1.14.0 h1:qknBErnf5O1CTWZDdDK/qqV8f7wWTf98gFIVW42m6dk=
github.com/go-telegram/bot v1.14.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/multiplay/go-ts3 v1.2.0 h1:LaN6iz9TZjHXxhLwfU0gjUgDxX0Hq7BCbuyuRhYMl3U=
github.com/multiplay/go-ts3 v1.2.0/go.mod h1:OdNmiO3uV++4SldaJDQTIGg8gNAu5MOiccZiAqVqUZA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -50,7 +60,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

File diff suppressed because it is too large Load Diff

6
logstruct.txt Normal file
View File

@@ -0,0 +1,6 @@
bot.SendMessage: Failed to send [%s] message
bot.EditMessage: Failed to edit message to [%s]
bot.EditMessageReplyMarkup: Failed to edit message reply markup to [%s]
bot.DeleteMessages: Failed to delete [%s] message
bot.AnswerInlineQuery: Failed to send [%s] inline answer (sub handler can add a `Str("command", "log")` )
bot.AnswerCallbackQuery: Failed to send [%s] callback answer

118
main.go
View File

@@ -2,98 +2,96 @@ package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
"trbot/database"
"trbot/utils"
"trbot/utils/configs"
"trbot/utils/consts"
"trbot/utils/mess"
"trbot/utils/internal_plugin"
"trbot/utils/signals"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
func main() {
consts.BotToken = mess.WhereIsBotToken()
consts.IsDebugMode = os.Getenv("DEBUG") == "true"
if consts.IsDebugMode {
log.Println("running in debug mode, all log will be printed to stdout")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
allowedUpdates := bot.AllowedUpdates{
models.AllowedUpdateMessage,
models.AllowedUpdateEditedMessage,
models.AllowedUpdateChannelPost,
models.AllowedUpdateEditedChannelPost,
models.AllowedUpdateInlineQuery,
models.AllowedUpdateChosenInlineResult,
models.AllowedUpdateCallbackQuery,
}
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack // set stack trace func
logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
ctx = logger.WithContext(ctx) // attach logger into ctx
opts := []bot.Option{
// read bot configs
err := configs.InitBot(ctx)
if err != nil { logger.Fatal().Err(err).Msg("Failed to read bot configs") }
// writer log to a file or only display on console
if configs.IsUseMultiLogWriter(&logger) { ctx = logger.WithContext(ctx) } // re-attach logger into ctx
configs.CheckConfig(ctx) // check and auto fill some config
configs.ShowConst(ctx) // show build info
thebot, err := bot.New(configs.BotConfig.BotToken, []bot.Option{
bot.WithDefaultHandler(defaultHandler),
bot.WithAllowedUpdates(allowedUpdates),
}
bot.WithAllowedUpdates(configs.BotConfig.AllowedUpdates),
}...)
if err != nil { logger.Fatal().Err(err).Msg("Failed to initialize bot") }
thebot, err := bot.New(consts.BotToken, opts...)
if err != nil { panic(err) }
consts.BotMe, err = thebot.GetMe(ctx)
if err != nil { logger.Fatal().Err(err).Msg("Failed to get bot info") }
consts.BotMe, _ = thebot.GetMe(ctx)
log.Printf("name[%s] [@%s] id[%d]", consts.BotMe.FirstName, consts.BotMe.Username, consts.BotMe.ID)
logger.Info().
Dict(utils.GetUserDict(consts.BotMe)).
Msg("Bot initialized")
log.Printf("starting %d\n", consts.BotMe.ID)
log.Printf("logChat_ID: %v", consts.LogChat_ID)
database.InitAndListDatabases(ctx)
database.InitAndListDatabases()
// set log level after bot initialized
zerolog.SetGlobalLevel(configs.BotConfig.LevelForZeroLog(false))
go signals.SignalsHandler(ctx, consts.SignalsChannel)
// start handler custom signals
go signals.SignalsHandler(ctx)
// 初始化插件
internal_plugin.Register()
// register plugin (plugin use `init()` first, then plugin use `InitPlugins` second, and internal is the last)
internal_plugin.Register(ctx)
// 检查是否设定了 webhookURL 环境变量
if mess.UsingWebhook() { // Webhook
mess.SetUpWebhook(ctx, thebot, &bot.SetWebhookParams{
URL: consts.WebhookURL,
AllowedUpdates: allowedUpdates,
// Select mode by Webhook config
if configs.IsUsingWebhook(ctx) /* Webhook */ {
configs.SetUpWebhook(ctx, thebot, &bot.SetWebhookParams{
URL: configs.BotConfig.WebhookURL,
AllowedUpdates: configs.BotConfig.AllowedUpdates,
})
log.Println("Working at Webhook Mode")
logger.Info().
Str("listenAddress", consts.WebhookListenPort).
Msg("Working at Webhook Mode")
go thebot.StartWebhook(ctx)
go func() {
err := http.ListenAndServe(consts.WebhookPort, thebot.WebhookHandler())
if err != nil { log.Panicln(err) }
}()
} else { // getUpdate, aka Long Polling
// 保存并清理云端 Webhook URL否则该模式会不生效 https://core.telegram.org/bots/api#getupdates
mess.SaveAndCleanRemoteWebhookURL(ctx, thebot)
log.Println("Working at Long Polling Mode")
if consts.IsDebugMode {
fmt.Printf("If in debug, visit https://api.telegram.org/bot%s/getWebhookInfo to check infos \n", consts.BotToken)
fmt.Printf("If in debug, visit https://api.telegram.org/bot%s/setWebhook?url=https://api.trle5.xyz/webhook-trbot to reset webhook\n", consts.BotToken)
err := http.ListenAndServe(consts.WebhookListenPort, thebot.WebhookHandler())
if err != nil {
logger.Fatal().
Err(err).
Msg("Webhook server failed")
}
} else /* getUpdate, aka Long Polling */ {
// save and clean remove Webhook URL befor using getUpdate https://core.telegram.org/bots/api#getupdates
configs.SaveAndCleanRemoteWebhookURL(ctx, thebot)
logger.Info().
Msg("Working at Long Polling Mode")
logger.Debug().
Msgf("visit https://api.telegram.org/bot%s/getWebhookInfo to check infos", configs.BotConfig.BotToken)
thebot.Start(ctx)
}
// A loop wait for getUpdate mode, this program will exit in `utils\signals\signals.go`.
// This loop will only run when the exit signal is received in getUpdate mode.
// Webhook won't reach here, http.ListenAndServe() will keep program running till exit.
// They use the same code to exit, this loop is to give some time to save the database when receive exit signal.
for {
select {
case <- consts.SignalsChannel.WorkDone:
log.Println("manually stopped")
return
default:
// log.Println("still waiting...") // 不在调式模式下,这个日志会非常频繁
time.Sleep(1 * time.Second)
}
time.Sleep(5 * time.Second)
logger.Info().Msg("still waiting...")
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,22 @@
package plugins
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
"trbot/utils"
"trbot/utils/consts"
"trbot/utils/errt"
"trbot/utils/handler_structs"
"trbot/utils/multe"
"trbot/utils/plugin_utils"
"trbot/utils/yaml"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/multiplay/go-ts3"
"github.com/rs/zerolog"
)
// loginname serveradmin
@@ -23,7 +27,8 @@ import (
var tsClient *ts3.Client
var tsErr error
var tsData_path string = consts.DB_path + "teamspeak/"
var tsDataDir string = filepath.Join(consts.YAMLDataBaseDir, "teamspeak/")
var tsDataPath string = filepath.Join(tsDataDir, consts.YAMLFileName)
var botNickName string = "trbot_teamspeak_plugin"
var isCanReInit bool = true
@@ -37,8 +42,8 @@ var hasHandlerByChatID bool
var resetListenTicker chan bool = make(chan bool)
var pollingInterval time.Duration = time.Second * 5
var tsServerQuery TSServerQuery
var privateOpts *handler_structs.SubHandlerParams
var tsData TSServerQuery
var privateOpts *handler_structs.SubHandlerParams
type TSServerQuery struct {
// get Name And Password in TeamSpeak 3 Client -> `Tools`` -> `ServerQuery Login`
@@ -49,21 +54,22 @@ type TSServerQuery struct {
}
func init() {
// 初始化不成功时依然注册 `/ts3` 命令,使用命令式输出初始化时的错误
if initTeamSpeak() {
isSuccessInit = true
log.Println("TeamSpeak plugin loaded")
// 需要以群组 ID 来触发 handler 来获取 opts
plugin_utils.AddHandlerByChatIDPlugins(plugin_utils.HandlerByChatID{
ChatID: tsServerQuery.GroupID,
PluginName: "teamspeak_get_opts",
Handler: getOptsHandler,
})
hasHandlerByChatID = true
} else {
log.Println("TeamSpeak plugin loaded failed:", tsErr)
}
plugin_utils.AddInitializer(plugin_utils.Initializer{
Name: "teamspeak",
Func: func(ctx context.Context) error{
if initTeamSpeak(ctx) {
isSuccessInit = true
// 需要以群组 ID 来触发 handler 来获取 opts
plugin_utils.AddHandlerByChatIDPlugins(plugin_utils.HandlerByChatID{
ChatID: tsData.GroupID,
PluginName: "teamspeak_get_opts",
Handler: getOptsHandler,
})
hasHandlerByChatID = true
}
return tsErr
},
})
plugin_utils.AddHandlerHelpInfo(plugin_utils.HandlerHelp{
Name: "TeamSpeak 检测用户变动",
@@ -77,60 +83,80 @@ func init() {
})
}
func initTeamSpeak() bool {
// 判断配置文件是否存在
_, err := os.Stat(tsData_path)
func initTeamSpeak(ctx context.Context) bool {
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "initTeamSpeak").
Logger()
var handlerErr multe.MultiError
err := yaml.LoadYAML(tsDataPath, &tsData)
if err != nil {
if os.IsNotExist(err) {
// 不存在,创建一份空文件
err = utils.SaveYAML(tsData_path + consts.MetadataFileName, &TSServerQuery{})
logger.Warn().
Err(err).
Str("path", tsDataPath).
Msg("Not found teamspeak config file. Created new one")
err = yaml.SaveYAML(tsDataPath, &TSServerQuery{})
if err != nil {
log.Println("[teamspeak] empty config create faild:", err)
} else {
log.Printf("[teamspeak] empty config created at [ %s ]", tsData_path)
logger.Error().
Err(err).
Str("path", tsDataPath).
Msg("Failed to create empty config")
handlerErr.Addf("failed to create empty config: %w", err)
}
} else {
// 文件存在,但是遇到了其他错误
tsErr = fmt.Errorf("[teamspeak] some error when read config file: %w", err)
logger.Error().
Err(err).
Str("path", tsDataPath).
Msg("Failed to read config file")
// 读取配置文件内容失败也不允许重新启动
tsErr = handlerErr.Addf("failed to read config file: %w", err).Flat()
isCanReInit = false
return false
}
// 无法获取到服务器地址和账号,无法初始化并设定不可重新启动
isCanReInit = false
return false
}
err = utils.LoadYAML(tsData_path + consts.MetadataFileName, &tsServerQuery)
if err != nil {
// if err != nil || tsServerQuery == nil {
// 读取配置文件内容失败也不允许重新启动
tsErr = fmt.Errorf("[teamspeak] read config error: %w", err)
isCanReInit = false
return false
}
// 如果服务器地址为空不允许重新启动
if tsServerQuery.URL == "" {
tsErr = fmt.Errorf("[teamspeak] no URL in config")
if tsData.URL == "" {
logger.Error().
Str("path", tsDataPath).
Msg("No URL in config")
tsErr = handlerErr.Addf("no URL in config").Flat()
isCanReInit = false
return false
} else {
if tsClient != nil { tsClient.Close() }
tsClient, tsErr = ts3.NewClient(tsServerQuery.URL)
if tsErr != nil {
tsErr = fmt.Errorf("[teamspeak] connect error: %w", tsErr)
tsClient, err = ts3.NewClient(tsData.URL)
if err != nil {
logger.Error().
Err(err).
Str("path", tsDataPath).
Msg("Failed to connect to server")
tsErr = handlerErr.Addf("failed to connnect to server: %w", err).Flat()
return false
}
}
// ServerQuery 账号名或密码为空也不允许重新启动
if tsServerQuery.Name == "" || tsServerQuery.Password == "" {
tsErr = fmt.Errorf("[teamspeak] no Name/Password in config")
if tsData.Name == "" || tsData.Password == "" {
logger.Error().
Str("path", tsDataPath).
Msg("No Name/Password in config")
tsErr = handlerErr.Addf("no Name/Password in config").Flat()
isCanReInit = false
return false
} else {
err = tsClient.Login(tsServerQuery.Name, tsServerQuery.Password)
err = tsClient.Login(tsData.Name, tsData.Password)
if err != nil {
tsErr = fmt.Errorf("[teamspeak] login error: %w", err)
logger.Error().
Err(err).
Str("path", tsDataPath).
Msg("Failed to login to server")
tsErr = handlerErr.Addf("failed to login to server: %w", err).Flat()
isLoginFailed = true
return false
} else {
@@ -139,8 +165,11 @@ func initTeamSpeak() bool {
}
// 检查要设定通知的群组 ID 是否存在
if tsServerQuery.GroupID == 0 {
tsErr = fmt.Errorf("[teamspeak] no GroupID in config")
if tsData.GroupID == 0 {
logger.Error().
Str("path", tsDataPath).
Msg("No GroupID in config")
tsErr = handlerErr.Addf("no GroupID in config").Flat()
isCanReInit = false
return false
}
@@ -148,28 +177,45 @@ func initTeamSpeak() bool {
// 显示服务端版本测试一下连接
v, err := tsClient.Version()
if err != nil {
tsErr = fmt.Errorf("[teamspeak] show version error: %w", err)
logger.Error().
Err(err).
Str("path", tsDataPath).
Msg("Failed to get server version")
tsErr = handlerErr.Addf("failed to get server version: %w", err).Flat()
return false
} else {
log.Printf("[teamspeak] running: %v", v)
logger.Info().
Str("version", v.Version).
Str("platform", v.Platform).
Int("build", v.Build).
Msg("TeamSpeak server connected")
}
// 切换默认虚拟服务器
err = tsClient.Use(1)
if err != nil {
tsErr = fmt.Errorf("[teamspeak] switch server error: %w", err)
logger.Error().
Err(err).
Msg("Failed to switch server")
tsErr = handlerErr.Addf("failed to switch server: %w", err).Flat()
return false
}
// 改一下 bot 自己的 nickname使得在检测用户列表时默认不显示自己
m, err := tsClient.Whoami()
if err != nil {
tsErr = fmt.Errorf("[teamspeak] get my info error: %w", err)
logger.Error().
Err(err).
Msg("Failed to get bot info")
tsErr = handlerErr.Addf("failed to get bot info: %w", err).Flat()
} else if m != nil && m.ClientName != botNickName {
// 当 bot 自己的 nickname 不等于配置文件中的 nickname 时,才进行修改
err = tsClient.SetNick(botNickName)
if err != nil {
tsErr = fmt.Errorf("[teamspeak] set nickname error: %w", err)
logger.Error().
Err(err).
Msg("Failed to set bot nickname")
tsErr = handlerErr.Addf("failed to set nickname: %w", err).Flat()
}
}
@@ -178,30 +224,49 @@ func initTeamSpeak() bool {
}
// 用于首次初始化成功时只要对应群组有任何消息,都能自动获取 privateOpts 用来定时发送消息,并开启监听协程
func getOptsHandler(opts *handler_structs.SubHandlerParams) {
if !isListening && isCanReInit && opts.Update.Message.Chat.ID == tsServerQuery.GroupID {
func getOptsHandler(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "getOptsHandler").
Logger()
if !isListening && isCanReInit && opts.Update.Message.Chat.ID == tsData.GroupID {
privateOpts = opts
isCanListening = true
if consts.IsDebugMode { log.Println("[teamspeak] success get opts by handler") }
logger.Debug().
Msg("success get opts by handler")
if !isLoginFailed {
go listenUserStatus()
if consts.IsDebugMode { log.Println("[teamspeak] success start listening") }
go listenUserStatus(opts.Ctx)
logger.Debug().
Msg("success start listen user status")
}
}
return nil
}
func showStatus(opts *handler_structs.SubHandlerParams) {
func showStatus(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "showStatus").
Logger()
var handlerErr multe.MultiError
var pendingMessage string
// 如果首次初始化没成功,没有添加根据群组 ID 来触发的 handler用户发送 /ts3 后可以通过这个来自动获取 opts 并启动监听
// if isSuccessInit && !isCanListening && opts.Update != nil && opts.Update.Message != nil && opts.Update.Message.Chat.ID == tsServerQuery.GroupID {
if !isListening && isCanReInit && opts.Update.Message.Chat.ID == tsServerQuery.GroupID {
if !isListening && isCanReInit && opts.Update.Message.Chat.ID == tsData.GroupID {
privateOpts = opts
isCanListening = true
if consts.IsDebugMode { log.Println("[teamspeak] success get opts") }
logger.Debug().
Msg("success get opts by showStatus")
if !isLoginFailed {
go listenUserStatus()
if consts.IsDebugMode { log.Println("[teamspeak] success start listening") }
go listenUserStatus(opts.Ctx)
logger.Debug().
Msg("success start listen user status")
}
// pendingMessage += fmt.Sprintln("已准备好发送用户状态")
}
@@ -209,7 +274,10 @@ func showStatus(opts *handler_structs.SubHandlerParams) {
if isSuccessInit && isCanListening {
olClient, err := tsClient.Server.ClientList()
if err != nil {
log.Println("[teamspeak] get online client error:", err)
logger.Error().
Err(err).
Msg("Failed to get online client")
handlerErr.Addf("failed to get online client: %w", err)
pendingMessage = fmt.Sprintf("连接到 teamspeak 服务器发生错误:\n<blockquote expandable>%s</blockquote>", err)
} else {
pendingMessage += fmt.Sprintln("在线客户端:")
@@ -227,32 +295,34 @@ func showStatus(opts *handler_structs.SubHandlerParams) {
pendingMessage += "\n"
}
if userCount == 0 {
pendingMessage += "当前无用户在线"
pendingMessage = "当前无用户在线"
}
}
} else {
pendingMessage = fmt.Sprintf("初始化 teamspeak 插件时发生了一些错误:\n<blockquote expandable>%s</blockquote>\n\n", tsErr)
if isCanReInit {
if initTeamSpeak() {
if initTeamSpeak(opts.Ctx) {
isSuccessInit = true
tsErr = fmt.Errorf("")
if !isListening && !isLoginFailed {
go listenUserStatus()
if consts.IsDebugMode { log.Println("[teamspeak] success start listening") }
go listenUserStatus(opts.Ctx)
logger.Debug().
Msg("Start listening user status")
}
resetListenTicker <- true
pendingMessage = "尝试重新初始化成功,现可正常运行"
} else if isListening {
pendingMessage += "尝试重新初始化失败,您可以使用 /ts3 命令来尝试手动初始化,或等待自动重连"
} else {
pendingMessage += "尝试重新初始化失败,您需要在服务器在线时手动使用 /ts3 命令来尝试初始化"
handlerErr.Addf("failed to reinit teamspeak plugin: %w", tsErr)
if isListening {
pendingMessage += "尝试重新初始化失败,您可以使用 /ts3 命令来尝试手动初始化,或等待自动重连"
} else {
pendingMessage += "尝试重新初始化失败,您需要在服务器在线时手动使用 /ts3 命令来尝试初始化"
}
}
} else {
pendingMessage += "这是一个无法恢复的错误,您可能需要联系机器人管理员"
}
}
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: pendingMessage,
@@ -260,11 +330,24 @@ func showStatus(opts *handler_structs.SubHandlerParams) {
ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID },
})
if err != nil {
log.Println("[teamspeak] can't answer `/ts3` command:",err)
logger.Error().
Err(err).
Int64("chatID", opts.Update.Message.Chat.ID).
Str("content", "teamspeak online client status").
Msg(errt.SendMessage)
handlerErr.Addf("failed to send `teamspeak online client status: %w`", err)
}
return handlerErr.Flat()
}
func listenUserStatus() {
func listenUserStatus(ctx context.Context) {
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "listenUserStatus").
Logger()
isListening = true
listenTicker := time.NewTicker(pollingInterval)
defer listenTicker.Stop()
@@ -272,10 +355,11 @@ func listenUserStatus() {
if hasHandlerByChatID {
hasHandlerByChatID = false
// 获取到 privateOpts 后删掉 handler by chatID
plugin_utils.RemoveHandlerByChatIDPlugin(tsServerQuery.GroupID, "teamspeak_get_opts")
plugin_utils.RemoveHandlerByChatIDPlugin(tsData.GroupID, "teamspeak_get_opts")
}
var retryCount int = 1
var checkFailedCount int = 0
var beforeOnlineClient []string
for {
@@ -286,55 +370,88 @@ func listenUserStatus() {
retryCount = 1
case <-listenTicker.C:
if isSuccessInit && isCanListening {
beforeOnlineClient = checkOnlineClientChange(beforeOnlineClient)
beforeOnlineClient = checkOnlineClientChange(ctx, &checkFailedCount, beforeOnlineClient)
} else {
if consts.IsDebugMode { log.Println("[teamspeak] try reconnect...") }
logger.Info().
Msg("try reconnect...")
// 出现错误时,先降低 ticker 速度,然后尝试重新初始化
listenTicker.Reset(time.Duration(retryCount) * 20 * time.Second)
if retryCount < 15 { retryCount++ }
if initTeamSpeak() {
if initTeamSpeak(ctx) {
isSuccessInit = true
isCanListening = true
// 重新初始化成功则恢复 ticker 速度
retryCount = 1
listenTicker.Reset(pollingInterval)
if consts.IsDebugMode { log.Println("[teamspeak] reconnect success") }
privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
ChatID: privateOpts.Update.Message.Chat.ID,
logger.Info().
Msg("reconnect success")
_, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
ChatID: tsData.GroupID,
Text: "已成功与服务器重新建立连接",
ParseMode: models.ParseModeHTML,
})
if err != nil {
logger.Error().
Err(err).
Int64("chatID", tsData.GroupID).
Str("content", "success reconnect to server").
Msg(errt.SendMessage)
}
} else {
// 无法成功则等待下一个周期继续尝试
if consts.IsDebugMode { log.Printf("[teamspeak] connect failed [%s], retry in %ds", tsErr, (retryCount - 1) * 20) }
logger.Warn().
Err(tsErr).
Int("retryCount", retryCount).
Int("nextRetry", (retryCount - 1) * 20).
Msg("connect failed")
}
}
}
}
}
func checkOnlineClientChange(before []string) []string {
func checkOnlineClientChange(ctx context.Context, count *int, before []string) []string {
var nowOnlineClient []string
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "checkOnlineClientChange").
Logger()
olClient, err := tsClient.Server.ClientList()
if err != nil {
log.Println("[teamspeak] get online client error:", err)
isCanListening = false
privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
ChatID: privateOpts.Update.Message.Chat.ID,
Text: "已断开与服务器的连接,开始尝试自动重连",
ParseMode: models.ParseModeHTML,
})
*count++
logger.Error().
Err(err).
Int("failedCount", *count).
Msg("Failed to get online client")
if *count == 5 {
*count = 0
isCanListening = false
_, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
ChatID: tsData.GroupID,
Text: "已连续五次检查在线客户端失败,开始尝试自动重连",
ParseMode: models.ParseModeHTML,
})
if err != nil {
logger.Error().
Err(err).
Int64("chatID", tsData.GroupID).
Str("content", "failed to check online client 5 times, start auto reconnect").
Msg(errt.SendMessage)
}
}
} else {
for _, n := range olClient {
nowOnlineClient = append(nowOnlineClient, n.Nickname)
}
added, removed := DiffSlices(before, nowOnlineClient)
if len(added) + len(removed) > 0 {
if consts.IsDebugMode {
log.Printf("[teamspeak] online client change: added %v, removed %v", added, removed)
}
notifyClientChange(privateOpts, tsServerQuery.GroupID, added, removed)
logger.Debug().
Strs("added", added).
Strs("removed", removed).
Msg("online client change detected")
notifyClientChange(privateOpts, added, removed)
}
}
@@ -362,8 +479,13 @@ func DiffSlices(before, now []string) (added, removed []string) {
return
}
func notifyClientChange(opts *handler_structs.SubHandlerParams, chatID int64, add, remove []string) {
func notifyClientChange(opts *handler_structs.SubHandlerParams, add, remove []string) {
var pendingMessage string
logger := zerolog.Ctx(opts.Ctx).
With().
Str("pluginName", "teamspeak3").
Str("funcName", "notifyClientChange").
Logger()
if len(add) > 0 {
pendingMessage += fmt.Sprintln("以下用户进入了服务器:")
@@ -378,9 +500,16 @@ func notifyClientChange(opts *handler_structs.SubHandlerParams, chatID int64, ad
}
}
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: chatID,
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: tsData.GroupID,
Text: pendingMessage,
ParseMode: models.ParseModeHTML,
})
if err != nil {
logger.Error().
Err(err).
Int64("chatID", tsData.GroupID).
Str("content", "teamspeak user change notify").
Msg(errt.SendMessage)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,31 @@
package plugins
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"trbot/utils"
"trbot/utils/consts"
"trbot/utils/handler_structs"
"trbot/utils/plugin_utils"
"trbot/utils/yaml"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"gopkg.in/yaml.v3"
"github.com/rs/zerolog"
)
var VoiceLists []VoicePack
var VoiceListErr error
var VoiceList_path string = consts.DB_path + "voices/"
var VoiceListDir string = filepath.Join(consts.YAMLDataBaseDir, "voices/")
func init() {
ReadVoicePackFromPath()
plugin_utils.AddInitializer(plugin_utils.Initializer{
Name: "VoiceList",
Func: ReadVoicePackFromPath,
})
plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{
Name: "Voice List",
Loader: ReadVoicePackFromPath,
@@ -46,38 +48,73 @@ type VoicePack struct {
}
// 读取指定目录下所有结尾为 .yaml 或 .yml 的语音文件
func ReadVoicePackFromPath() {
func ReadVoicePackFromPath(ctx context.Context) error {
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "Voice List").
Str("funcName", "ReadVoicePackFromPath").
Logger()
var packs []VoicePack
if _, err := os.Stat(VoiceList_path); os.IsNotExist(err) {
log.Printf("No voices dir, create a new one: %s", VoiceList_path)
if err := os.MkdirAll(VoiceList_path, 0755); err != nil {
VoiceLists, VoiceListErr = nil, err
return
_, err := os.Stat(VoiceListDir)
if err != nil {
if os.IsNotExist(err) {
logger.Warn().
Str("directory", VoiceListDir).
Msg("VoiceList directory not exist, now create it")
err = os.MkdirAll(VoiceListDir, 0755)
if err != nil {
logger.Error().
Err(err).
Str("directory", VoiceListDir).
Msg("Failed to create VoiceList data directory")
VoiceListErr = err
return err
}
} else {
logger.Error().
Err(err).
Str("directory", VoiceListDir).
Msg("Open VoiceList data directory failed")
VoiceListErr = err
return err
}
}
err := filepath.Walk(VoiceList_path, func(path string, info os.FileInfo, err error) error {
if err != nil { return err }
if strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml") {
file, err := os.Open(path)
if err != nil { log.Println("(func)readVoicesFromDir:", err) }
defer file.Close()
err = filepath.Walk(VoiceListDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
logger.Error().
Err(err).
Str("path", path).
Msg("Failed to read file use `filepath.Walk()`")
}
if strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml") {
var singlePack VoicePack
decoder := yaml.NewDecoder(file)
err = decoder.Decode(&singlePack)
if err != nil { log.Println("(func)readVoicesFromDir:", err) }
err = yaml.LoadYAML(path, &singlePack)
if err != nil {
logger.Error().
Err(err).
Str("path", path).
Msg("Failed to decode file use `yaml.NewDecoder()`")
}
packs = append(packs, singlePack)
}
return nil
})
if err != nil {
VoiceLists, VoiceListErr = nil, err
return
logger.Error().
Err(err).
Str("directory", VoiceListDir).
Msg("Failed to read voice packs in VoiceList directory")
VoiceListErr = err
return err
}
VoiceLists, VoiceListErr = packs, nil
VoiceLists = packs
return nil
}
func VoiceListHandler(opts *handler_structs.SubHandlerParams) []models.InlineQueryResult {
@@ -85,11 +122,13 @@ func VoiceListHandler(opts *handler_structs.SubHandlerParams) []models.InlineQue
var results []models.InlineQueryResult
if VoiceLists == nil {
log.Printf("No voices file in voices_path: %s", VoiceList_path)
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: consts.LogChat_ID,
Text: fmt.Sprintf("%s\nInline Mode: some user can't load voices", time.Now().Format(time.RFC3339)),
})
zerolog.Ctx(opts.Ctx).
Warn().
Str("pluginName", "Voice List").
Str("funcName", "VoiceListHandler").
Str("VoiceListDir", VoiceListDir).
Msg("No voices file in VoiceListDir")
return []models.InlineQueryResult{&models.InlineQueryResultVoice{
ID: "none",
Title: "无法读取到语音文件,请联系机器人管理员",

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ type SavedMessageSharedData struct {
Description string
}
// models.InlineQueryResultArticle
type SavedMessageTypeCachedOnlyText struct {
ID string `yaml:"ID"`
TitleAndMessageText string `yaml:"TitleAndMessageText"`
@@ -21,12 +22,14 @@ type SavedMessageTypeCachedOnlyText struct {
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedAudio
type SavedMessageTypeCachedAudio struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
Caption string `yaml:"Caption,omitempty"`
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
// SharedData
Title string `yaml:"Title,omitempty"`
FileName string `yaml:"FileName,omitempty"`
Description string `yaml:"Description,omitempty"`
@@ -35,6 +38,7 @@ type SavedMessageTypeCachedAudio struct {
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedDocument
type SavedMessageTypeCachedDocument struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
@@ -47,6 +51,7 @@ type SavedMessageTypeCachedDocument struct {
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedGif
type SavedMessageTypeCachedGif struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
@@ -54,12 +59,29 @@ type SavedMessageTypeCachedGif struct {
Caption string `yaml:"Caption,omitempty"`
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
// SharedData
Description string `yaml:"Description,omitempty"`
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedMpeg4Gif
type SavedMessageTypeCachedMpeg4Gif struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
Title string `yaml:"Title,omitempty"`
Caption string `yaml:"Caption,omitempty"`
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
// SharedData
Description string `yaml:"Description,omitempty"`
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedPhoto
type SavedMessageTypeCachedPhoto struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
@@ -73,18 +95,22 @@ type SavedMessageTypeCachedPhoto struct {
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedSticker
type SavedMessageTypeCachedSticker struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
// SharedData
SetName string `yaml:"SetName,omitempty"`
SetTitle string `yaml:"SetTitle,omitempty"`
Description string `yaml:"Description,omitempty"`
Emoji string `yaml:"Emoji,omitempty"` // store in sharedata.FileName
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedVideo
type SavedMessageTypeCachedVideo struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
@@ -97,18 +123,20 @@ type SavedMessageTypeCachedVideo struct {
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedDocument
type SavedMessageTypeCachedVideoNote struct {
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
Title string `yaml:"Title"`
Description string `yaml:"Description,omitempty"`
Caption string `yaml:"Caption,omitempty"` // 利用 bot 修改信息可以发出带文字的圆形视频,但是发送后不带文字
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
// models.InlineQueryResultCachedVoice
type SavedMessageTypeCachedVoice struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
@@ -116,19 +144,7 @@ type SavedMessageTypeCachedVoice struct {
Caption string `yaml:"Caption,omitempty"`
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
Description string `yaml:"Description,omitempty"`
IsDeleted bool `yaml:"IsDeleted,omitempty"`
OriginInfo *OriginInfo `yaml:"OriginInfo,omitempty"`
}
type SavedMessageTypeCachedMpeg4Gif struct {
ID string `yaml:"ID"`
FileID string `yaml:"FileID"`
Title string `yaml:"Title,omitempty"`
Caption string `yaml:"Caption,omitempty"`
CaptionEntities []models.MessageEntity `yaml:"CaptionEntities,omitempty"`
// SharedData
Description string `yaml:"Description,omitempty"`
IsDeleted bool `yaml:"IsDeleted,omitempty"`

View File

@@ -1,23 +1,24 @@
package saved_message
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"trbot/utils"
"trbot/utils/consts"
"trbot/utils/type_utils"
"trbot/utils/type/message_utils"
"trbot/utils/yaml"
"github.com/go-telegram/bot/models"
"gopkg.in/yaml.v3"
"github.com/rs/zerolog"
)
var SavedMessageSet map[int64]SavedMessage
var SavedMessageErr error
var SavedMessage_path string = consts.DB_path + "savedmessage/"
var SavedMessagePath string = filepath.Join(consts.YAMLDataBaseDir, "savedmessage/", consts.YAMLFileName)
var textExpandableLength int = 150
@@ -33,53 +34,66 @@ type SavedMessage struct {
Item SavedMessageItems `yaml:"Item,omitempty"`
}
func SaveSavedMessageList() error {
data, err := yaml.Marshal(SavedMessageSet)
if err != nil { return err }
func SaveSavedMessageList(ctx context.Context) error {
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "Saved Message").
Str("funcName", "SaveSavedMessageList").
Logger()
if _, err := os.Stat(SavedMessage_path); os.IsNotExist(err) {
if err := os.MkdirAll(SavedMessage_path, 0755); err != nil {
return err
}
err := yaml.SaveYAML(SavedMessagePath, &SavedMessageSet)
if err != nil {
logger.Error().
Err(err).
Str("path", SavedMessagePath).
Msg("Failed to save savedmessage list")
SavedMessageErr = fmt.Errorf("failed to save savedmessage list: %w", err)
} else {
SavedMessageErr = nil
}
if _, err := os.Stat(SavedMessage_path + consts.MetadataFileName); os.IsNotExist(err) {
_, err := os.Create(SavedMessage_path + consts.MetadataFileName)
if err != nil {
return err
}
}
return os.WriteFile(SavedMessage_path + consts.MetadataFileName, data, 0644)
return SavedMessageErr
}
func ReadSavedMessageList() {
var SavedMessages map[int64]SavedMessage
func ReadSavedMessageList(ctx context.Context) error {
logger := zerolog.Ctx(ctx).
With().
Str("pluginName", "Saved Message").
Str("funcName", "ReadSavedMessageList").
Logger()
file, err := os.Open(SavedMessage_path + consts.MetadataFileName)
err := yaml.LoadYAML(SavedMessagePath, &SavedMessageSet)
if err != nil {
// 如果是找不到目录,新建一个
log.Println("[SavedMessage]: Not found database file. Created new one")
SaveSavedMessageList()
SavedMessageSet, SavedMessageErr = map[int64]SavedMessage{}, err
return
}
defer file.Close()
decoder := yaml.NewDecoder(file)
err = decoder.Decode(&SavedMessages)
if err != nil {
if err == io.EOF {
log.Println("[SavedMessage]: Saved Message list looks empty. now format it")
SaveSavedMessageList()
SavedMessageSet, SavedMessageErr = map[int64]SavedMessage{}, nil
return
if os.IsNotExist(err) {
logger.Warn().
Err(err).
Str("path", SavedMessagePath).
Msg("Not found savedmessage list file. Created new one")
// 如果是找不到文件,新建一个
err = yaml.SaveYAML(SavedMessagePath, &SavedMessageSet)
if err != nil {
logger.Error().
Err(err).
Str("path", SavedMessagePath).
Msg("Failed to create empty savedmessage list file")
SavedMessageErr = fmt.Errorf("failed to create empty savedmessage list file: %w", err)
}
} else {
logger.Error().
Err(err).
Str("path", SavedMessagePath).
Msg("Failed to load savedmessage list file")
SavedMessageErr = fmt.Errorf("failed to load savedmessage list file: %w", err)
}
log.Println("(func)ReadSavedMessageList:", err)
SavedMessageSet, SavedMessageErr = map[int64]SavedMessage{}, err
return
} else {
SavedMessageErr = nil
}
SavedMessageSet, SavedMessageErr = SavedMessages, nil
if SavedMessageSet == nil {
SavedMessageSet = map[int64]SavedMessage{}
}
return SavedMessageErr
}
type sortstruct struct {
@@ -102,12 +116,12 @@ type SavedMessageItems struct {
Audio []SavedMessageTypeCachedAudio `yaml:"Audio,omitempty"`
Document []SavedMessageTypeCachedDocument `yaml:"Document,omitempty"`
Gif []SavedMessageTypeCachedGif `yaml:"Gif,omitempty"`
Mpeg4gif []SavedMessageTypeCachedMpeg4Gif `yaml:"Mpeg4Gif,omitempty"`
Photo []SavedMessageTypeCachedPhoto `yaml:"Photo,omitempty"`
Sticker []SavedMessageTypeCachedSticker `yaml:"Sticker,omitempty"`
Video []SavedMessageTypeCachedVideo `yaml:"Video,omitempty"`
VideoNote []SavedMessageTypeCachedVideoNote `yaml:"VideoNote,omitempty"`
Voice []SavedMessageTypeCachedVoice `yaml:"Voice,omitempty"`
Mpeg4gif []SavedMessageTypeCachedMpeg4Gif `yaml:"Mpeg4Gif,omitempty"`
}
func (s *SavedMessageItems) All() []sortstruct {
@@ -117,14 +131,14 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.OnlyText {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].onlyText != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
// var pendingTitle string
@@ -146,14 +160,14 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.Audio {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].audio != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].audio = &models.InlineQueryResultCachedAudio{
@@ -165,20 +179,22 @@ func (s *SavedMessageItems) All() []sortstruct {
}
list[index].sharedData = &SavedMessageSharedData{
Title: v.Title,
FileName: v.FileName,
Description: v.Description,
}
}
for _, v := range s.Document {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].document != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].document = &models.InlineQueryResultCachedDocument{
@@ -194,14 +210,14 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.Gif {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].gif != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].gif = &models.InlineQueryResultCachedGif{
@@ -217,17 +233,42 @@ func (s *SavedMessageItems) All() []sortstruct {
Description: v.Description,
}
}
for _, v := range s.Mpeg4gif {
index, err := strconv.Atoi(v.ID)
if err != nil {
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].mpeg4gif != nil {
fmt.Println("duplicate id", v.ID)
continue
}
list[index].mpeg4gif = &models.InlineQueryResultCachedMpeg4Gif{
ID: v.ID,
Mpeg4FileID: v.FileID,
Title: v.Title,
Caption: v.Caption,
CaptionEntities: v.CaptionEntities,
ReplyMarkup: buildFromInfoButton(v.OriginInfo),
}
list[index].sharedData = &SavedMessageSharedData{
Description: v.Description,
}
}
for _, v := range s.Photo {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].photo != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].photo = &models.InlineQueryResultCachedPhoto{
@@ -244,14 +285,14 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.Sticker {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].sticker != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].sticker = &models.InlineQueryResultCachedSticker{
@@ -264,21 +305,25 @@ func (s *SavedMessageItems) All() []sortstruct {
Name: v.SetName,
Title: v.SetTitle,
Description: v.Description,
FileName: v.Emoji,
}
}
for _, v := range s.Video {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].video != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
if v.Title == "" {
v.Title = "video.mp4"
}
list[index].video = &models.InlineQueryResultCachedVideo{
ID: v.ID,
VideoFileID: v.FileID,
@@ -292,14 +337,14 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.VideoNote {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].document != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
list[index].document = &models.InlineQueryResultCachedDocument{
@@ -315,16 +360,19 @@ func (s *SavedMessageItems) All() []sortstruct {
for _, v := range s.Voice {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
fmt.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].voice != nil {
log.Println("duplicate id", v.ID)
fmt.Println("duplicate id", v.ID)
continue
}
if v.Title == "" {
v.Title = "audio"
}
list[index].voice = &models.InlineQueryResultCachedVoice{
ID: v.ID,
VoiceFileID: v.FileID,
@@ -337,31 +385,7 @@ func (s *SavedMessageItems) All() []sortstruct {
Description: v.Description,
}
}
for _, v := range s.Mpeg4gif {
index, err := strconv.Atoi(v.ID)
if err != nil {
log.Println("no an valid id", err)
continue
}
if len(list) <= index {
list = append(list, make([]sortstruct, index-len(list)+1)...)
}
if list[index].mpeg4gif != nil {
log.Println("duplicate id", v.ID)
continue
}
list[index].mpeg4gif = &models.InlineQueryResultCachedMpeg4Gif{
ID: v.ID,
Mpeg4FileID: v.FileID,
Title: v.Title,
Caption: v.Caption,
CaptionEntities: v.CaptionEntities,
ReplyMarkup: buildFromInfoButton(v.OriginInfo),
}
list[index].sharedData = &SavedMessageSharedData{
Description: v.Description,
}
}
// for _, n := range list {
// if n.audio != nil {
@@ -419,7 +443,7 @@ func getMessageOriginData(msgOrigin *models.MessageOrigin) *OriginInfo {
func getMessageLink(msg *models.Message) *OriginInfo {
// if msg.From.ID == msg.Chat.ID {
// }
attr := type_utils.GetMessageAttribute(msg)
attr := message_utils.GetMessageAttribute(msg)
if attr.IsFromLinkedChannel || attr.IsFromAnonymous || attr.IsUserAsChannel {
return &OriginInfo{
FromName: utils.ShowChatName(msg.SenderChat),

View File

@@ -1,6 +1,8 @@
package plugins
import "trbot/plugins/saved_message"
import (
"trbot/plugins/saved_message"
)
/*
This `sub_package_plugin.go` file allow you to import other packages.

98
utils/configs/config.go Normal file
View File

@@ -0,0 +1,98 @@
package configs
import (
"fmt"
"strings"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
)
// default "./config.yaml", can be changed by env
var ConfigPath string = "./config.yaml"
var BotConfig config
type config struct {
// bot config
BotToken string `yaml:"BotToken"`
WebhookURL string `yaml:"WebhookURL"`
// log
LogLevel string `yaml:"LogLevel"` // `trace` `debug` `info` `warn` `error` `fatal` `panic`, default "info"
LogFileLevel string `yaml:"LogFileLevel"`
LogChatID int64 `yaml:"LogChatID"`
// admin
AdminIDs []int64 `yaml:"AdminIDs"`
// redis database
RedisURL string `yaml:"RedisURL"`
RedisPassword string `yaml:"RedisPassword"`
RedisDatabaseID int `yaml:"RedisDatabaseID"`
// inline mode config
InlineDefaultHandler string `yaml:"InlineDefaultHandler"` // Leave empty to show inline menu
InlineSubCommandSymbol string `yaml:"InlineSubCommandSymbol"` // default is "+"
InlinePaginationSymbol string `yaml:"InlinePaginationSymbol"` // default is "-"
InlineResultsPerPage int `yaml:"InlineResultsPerPage"` // default 50, maxinum 50, see https://core.telegram.org/bots/api#answerinlinequery
AllowedUpdates bot.AllowedUpdates `yaml:"AllowedUpdates"`
FFmpegPath string `yaml:"FFmpegPath"`
}
func (c config)LevelForZeroLog(forLogFile bool) zerolog.Level {
var levelText string
if forLogFile {
levelText = c.LogFileLevel
} else {
levelText = c.LogLevel
}
switch strings.ToLower(levelText) {
case "trace":
return zerolog.TraceLevel
case "debug":
return zerolog.DebugLevel
case "info":
return zerolog.InfoLevel
case "warn":
return zerolog.WarnLevel
case "error":
return zerolog.ErrorLevel
case "fatal":
return zerolog.FatalLevel
case "panic":
return zerolog.PanicLevel
default:
if forLogFile {
fmt.Printf("Unknown log level [ %s ], using error level for log file", c.LogLevel)
return zerolog.ErrorLevel
} else {
fmt.Printf("Unknown log level [ %s ], using info level for console", c.LogLevel)
return zerolog.InfoLevel
}
}
}
func CreateDefaultConfig() config {
return config{
BotToken: "REPLACE_THIS_USE_YOUR_BOT_TOKEN",
LogLevel: "info",
LogFileLevel: "warn",
InlineSubCommandSymbol: "+",
InlinePaginationSymbol: "-",
InlineResultsPerPage: 50,
AllowedUpdates: bot.AllowedUpdates{
models.AllowedUpdateMessage,
models.AllowedUpdateEditedMessage,
models.AllowedUpdateChannelPost,
models.AllowedUpdateEditedChannelPost,
models.AllowedUpdateInlineQuery,
models.AllowedUpdateChosenInlineResult,
models.AllowedUpdateCallbackQuery,
},
}
}

368
utils/configs/init.go Normal file
View File

@@ -0,0 +1,368 @@
package configs
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"trbot/utils/consts"
"trbot/utils/yaml"
"unicode"
"github.com/joho/godotenv"
"github.com/rs/zerolog"
)
func InitBot(ctx context.Context) error {
var initFuncs = []func(ctx context.Context)error{
readConfig,
readBotToken,
readEnvironment,
}
godotenv.Load()
for _, initfunc := range initFuncs {
err := initfunc(ctx)
if err != nil { return err }
}
return nil
}
// 从 yaml 文件读取配置文件
func readConfig(ctx context.Context) error {
logger := zerolog.Ctx(ctx)
// 先检查一下环境变量里有没有指定配置目录
configPathToFile := os.Getenv("CONFIG_PATH_TO_FILE")
configDirectory := os.Getenv("CONFIG_DIRECTORY")
if configPathToFile != "" {
// 检查配置文件是否存在
if _, err := os.Stat(configPathToFile); err != nil {
if os.IsNotExist(err) {
// 如果配置文件不存在,就以默认配置的方式创建一份
logger.Warn().
Str("configPathToFile", configPathToFile).
Msg("The config file does not exist. creating...")
err = yaml.SaveYAML(configPathToFile, CreateDefaultConfig())
if err != nil {
logger.Error().
Err(err).
Msg("Create default config failed")
return err
} else {
logger.Warn().
Str("configPathToFile", configPathToFile).
Msg("The config file is created, please fill the bot token and restart")
// 创建完成目录就跳到下方读取配置文件
// 默认配置文件没 bot token 的错误就留后面处理
}
} else {
// 读取配置文件时的其他错误
logger.Error().
Err(err).
Str("configPathToFile", configPathToFile).
Msg("Read config file failed")
return err
}
}
// 读取配置文件
ConfigPath = configPathToFile
logger.Info().
Msg("Read config success from `CONFIG_PATH_TO_FILE` environment")
return yaml.LoadYAML(configPathToFile, &BotConfig)
} else if configDirectory != "" {
// 检查目录是否存在
if _, err := os.Stat(configDirectory); err != nil {
if os.IsNotExist(err) {
// 目录不存在则创建
logger.Warn().
Str("configDirectory", configDirectory).
Msg("Config directory does not exist, creating...")
err = os.MkdirAll(configDirectory, 0755)
if err != nil {
logger.Error().
Err(err).
Str("configDirectory", configDirectory).
Msg("Create config directory failed")
return err
}
// 如果不出错,到这里会跳到下方的读取配置文件部分
} else {
// 读取目录时的其他错误
logger.Error().
Err(err).
Str("configDirectory", configDirectory).
Msg("Read config directory failed")
return err
}
}
// 使用默认的配置文件名,把目标配置文件路径补全
targetConfigPath := filepath.Join(configDirectory, "config.yaml")
// 检查目录配置文件是否存在
if _, err := os.Stat(targetConfigPath); err != nil {
if os.IsNotExist(err) {
// 用户指定目录的话,还是不创建配置文件了,提示用户想要自定义配置文件名的话,需要设定另外一个环境变量
logger.Warn().
Str("configDirectory", configDirectory).
Msg("No configuration file named `config.yaml` was found in this directory, If you want to set a specific config file name, set the `CONFIG_PATH_TO_FILE` environment variable")
return err
} else {
// 读取目标配置文件路径时的其他错误
logger.Error().
Err(err).
Str("targetConfigPath", targetConfigPath).
Msg("Read target config file path failed")
return err
}
}
// 读取配置文件
ConfigPath = configPathToFile
logger.Info().
Msg("Read config path success from `CONFIG_DIRECTORY` environment")
return yaml.LoadYAML(targetConfigPath, &BotConfig)
} else {
// 没有指定任何环境变量,就读取默认的路径
if _, err := os.Stat(ConfigPath); err != nil {
if os.IsNotExist(err) {
// 如果配置文件不存在,就以默认配置的方式创建一份
logger.Warn().
Str("defaultConfigPath", ConfigPath).
Msg("The default config file does not exist. creating...")
err = yaml.SaveYAML(ConfigPath, CreateDefaultConfig())
if err != nil {
logger.Error().
Err(err).
Str("defaultConfigPath", ConfigPath).
Msg("Create default config file failed")
return err
} else {
logger.Warn().
Str("defaultConfigPath", ConfigPath).
Msg("Default config file is created, please fill the bot token and restart.")
// 创建完成目录就跳到下方读取配置文件
// 默认配置文件没 bot token 的错误就留后面处理
}
} else {
// 读取配置文件时的其他错误
logger.Error().
Err(err).
Str("defaultConfigPath", ConfigPath).
Msg("Read default config file failed")
return err
}
}
logger.Info().
Str("defaultConfigPath", ConfigPath).
Msg("Read config file from default path")
return yaml.LoadYAML(ConfigPath, &BotConfig)
}
}
// 查找 bot token
func readBotToken(ctx context.Context) error {
logger := zerolog.Ctx(ctx)
botToken := os.Getenv("BOT_TOKEN")
if botToken != "" {
BotConfig.BotToken = botToken
logger.Info().
Str("botTokenID", showBotID()).
Msg("Get token from environment")
return nil
}
// 从 yaml 配置文件中读取
if BotConfig.BotToken != "" {
logger.Info().
Str("botTokenID", showBotID()).
Msg("Get token from config file")
return nil
}
// 都不存在,提示错误
logger.Warn().
Msg("No bot token in environment, .env file and yaml config file, try create a bot from https://t.me/@botfather https://core.telegram.org/bots/tutorial#obtain-your-bot-token and fill it")
return fmt.Errorf("no bot token")
}
func readEnvironment(ctx context.Context) error {
logger := zerolog.Ctx(ctx)
if os.Getenv("DEBUG") != "" {
BotConfig.LogLevel = "debug"
logger.Warn().
Msg("The DEBUG environment variable is set")
}
logLevel := os.Getenv("LOG_LEVEL")
if logLevel != "" {
BotConfig.LogLevel = logLevel
logger.Warn().
Str("logLevel", logLevel).
Msg("Get log level from environment")
}
logFileLevel := os.Getenv("LOG_FILE_LEVEL")
if logFileLevel != "" {
BotConfig.LogFileLevel = logFileLevel
logger.Warn().
Str("logFileLevel", logFileLevel).
Msg("Get log file level from environment")
}
FFmpegPath := os.Getenv("FFMPEG_PATH")
if FFmpegPath != "" {
BotConfig.FFmpegPath = FFmpegPath
logger.Warn().
Str("FFmpegPath", FFmpegPath).
Msg("Get FFmpegPath from environment")
}
return nil
}
func showBotID() string {
var botID string
for _, char := range BotConfig.BotToken {
if unicode.IsDigit(char) {
botID += string(char)
} else {
break // 遇到非数字字符停止
}
}
return botID
}
func IsUseMultiLogWriter(logger *zerolog.Logger) bool {
file, err := os.OpenFile(consts.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
multLogger := zerolog.New(zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: os.Stdout},
&zerolog.FilteredLevelWriter{
Writer: zerolog.MultiLevelWriter(file),
Level: BotConfig.LevelForZeroLog(true),
},
)).With().Timestamp().Logger()
*logger = multLogger
logger.Info().
Str("logFilePath", consts.LogFilePath).
Str("logFileLevel", BotConfig.LogFileLevel).
Msg("Use mult log writer")
return true
} else {
logger.Error().
Err(err).
Str("logFilePath", consts.LogFilePath).
Msg("Failed to open log file, use console log writer only")
return false
}
}
func CheckConfig(ctx context.Context) {
logger := zerolog.Ctx(ctx)
// 部分必要但可以留空的配置
if BotConfig.LogLevel == "" {
BotConfig.LogLevel = "info"
logger.Warn().
Msg("LogLevel is not set, use default value: info")
}
if BotConfig.LogFileLevel == "" {
BotConfig.LogFileLevel = "warn"
logger.Warn().
Msg("LogFileLevel is not set, use default value: warn")
}
if BotConfig.InlineDefaultHandler == "" {
logger.Info().
Msg("Inline default handler is not set, default show all commands")
}
if BotConfig.InlineSubCommandSymbol == "" {
BotConfig.InlineSubCommandSymbol = "+"
logger.Info().
Msg("Inline sub command symbol is not set, use default value: `+` (plus sign)")
}
if BotConfig.InlinePaginationSymbol == "" {
BotConfig.InlinePaginationSymbol = "-"
logger.Info().
Msg("Inline pagination symbol is not set, use default value: `-` (minus sign)")
}
if BotConfig.InlineResultsPerPage == 0 {
BotConfig.InlineResultsPerPage = 50
logger.Info().
Msg("Inline results per page number is not set, set it to 50")
} else if BotConfig.InlineResultsPerPage < 1 || BotConfig.InlineResultsPerPage > 50 {
logger.Warn().
Int("invalidNumber", BotConfig.InlineResultsPerPage).
Msg("Inline results per page number is invalid, set it to 50")
BotConfig.InlineResultsPerPage = 50
}
// 以下为可有可无的配置,主要是提醒下用户
if len(BotConfig.AdminIDs) != 0 {
logger.Info().
Ints64("AdminIDs", BotConfig.AdminIDs).
Msg("Admin list is set")
}
if len(BotConfig.AllowedUpdates) != 0 {
logger.Info().
Strs("allowedUpdates", BotConfig.AllowedUpdates).
Msg("Allowed updates list is set")
}
if BotConfig.LogChatID != 0 {
logger.Info().
Int64("LogChatID", BotConfig.LogChatID).
Msg("Enabled log to chat")
}
if BotConfig.FFmpegPath != "" {
logger.Info().
Str("FFmpegPath", BotConfig.FFmpegPath).
Msg("FFmpeg path is set")
}
logger.Info().
Str("DefaultHandler", BotConfig.InlineDefaultHandler).
Str("SubCommandSymbol", BotConfig.InlineSubCommandSymbol).
Str("PaginationSymbol", BotConfig.InlinePaginationSymbol).
Int("ResultsPerPage", BotConfig.InlineResultsPerPage).
Msg("Inline mode config has been read")
}
func ShowConst(ctx context.Context) {
logger := zerolog.Ctx(ctx)
if consts.BuildAt == "" {
logger.Warn().
Str("runtime", runtime.Version()).
Str("logLevel", BotConfig.LogLevel).
Str("error", "Remind: You are using a version without build info").
Msg("trbot")
} else {
logger.Info().
Str("commit", consts.Commit).
Str("branch", consts.Branch).
Str("version", consts.Version).
Str("buildAt", consts.BuildAt).
Str("buildOn", consts.BuildOn).
Str("changes", consts.Changes).
Str("runtime", runtime.Version()).
Str("logLevel", BotConfig.LogLevel).
Msg("trbot")
}
}

99
utils/configs/webhook.go Normal file
View File

@@ -0,0 +1,99 @@
package configs
import (
"context"
"os"
"github.com/go-telegram/bot"
"github.com/rs/zerolog"
)
// 通过是否设定环境变量和配置文件中的 Webhook URL 来决定是否使用 Webhook 模式
func IsUsingWebhook(ctx context.Context) bool {
logger := zerolog.Ctx(ctx)
webhookURL := os.Getenv("WEBHOOK_URL")
if webhookURL != "" {
BotConfig.WebhookURL = webhookURL
logger.Info().
Str("WebhookURL", BotConfig.WebhookURL).
Msg("Get Webhook URL from environment")
return true
}
// 从 yaml 配置文件中读取
if BotConfig.WebhookURL != "" {
logger.Info().
Str("WebhookURL", BotConfig.WebhookURL).
Msg("Get Webhook URL from config file")
return true
}
logger.Info().
Msg("No Webhook URL in environment and .env file, using getUpdate mode")
return false
}
func SetUpWebhook(ctx context.Context, thebot *bot.Bot, params *bot.SetWebhookParams) bool {
logger := zerolog.Ctx(ctx)
webHookInfo, err := thebot.GetWebhookInfo(ctx)
if err != nil {
logger.Error().
Err(err).
Msg("Get Webhook info error")
}
if webHookInfo != nil && webHookInfo.URL != params.URL {
if webHookInfo.URL == "" {
logger.Info().
Msg("Webhook not set, setting it now...")
} else {
logger.Warn().
Str("remoteURL", webHookInfo.URL).
Str("localURL", params.URL).
Msg("The remote Webhook URL conflicts with the local one, saving and overwriting the remote URL")
}
success, err := thebot.SetWebhook(ctx, params)
if err != nil {
logger.Error().
Err(err).
Msg("Set Webhook URL failed")
return false
}
if success {
logger.Info().
Str("WebhookURL", params.URL).
Msg("Set Webhook URL success")
return true
}
} else {
logger.Info().
Str("WebhookURL", params.URL).
Msg("Webhook URL is already set")
return true
}
return false
}
func SaveAndCleanRemoteWebhookURL(ctx context.Context, thebot *bot.Bot) {
logger := zerolog.Ctx(ctx)
webHookInfo, err := thebot.GetWebhookInfo(ctx)
if err != nil {
logger.Error().
Err(err).
Msg("Failed to get Webhook info")
return
}
if webHookInfo != nil && webHookInfo.URL != "" {
logger.Warn().
Str("remoteURL", webHookInfo.URL).
Msg("There is a Webhook URL remotely, saving and clearing it to use the getUpdate mode")
ok, err := thebot.DeleteWebhook(ctx, &bot.DeleteWebhookParams{
DropPendingUpdates: false,
})
if !ok {
logger.Error().
Err(err).
Msg("Failed to delete Webhook URL")
}
}
}

View File

@@ -4,50 +4,19 @@ import (
"github.com/go-telegram/bot/models"
)
var BotToken string // 全局 bot token
var WebhookListenPort string = "localhost:2847"
var WebhookURL string // Webhook 运行模式下接受请求的 URL 地址
var WebhookPort string = "localhost:2847" // Webhook 运行模式下监听的端口
var YAMLDataBaseDir string = "./db_yaml/"
var YAMLFileName string = "metadata.yaml"
var LogChat_ID int64 = -1002499888124 // 用于接收日志的聊天 ID可以是 用户 群聊 频道
var LogMan_IDs []int64 = []int64{ // 拥有查看日志权限的用户,可设定多个
1086395364,
2074319561,
}
var MetadataFileName string = "metadata.yaml"
var RedisURL string = "localhost:6379"
var RedisPassword string = ""
var RedisMainDB int = 0
var RedisUserInfoDB int = 1
var RedisSubDB int = 2
var DB_path string = "./db_yaml/"
var LogFile_path string = DB_path + "log.txt"
var IsDebugMode bool
var Private_log bool = false
var CacheDirectory string = "./cache/"
var LogFilePath string = YAMLDataBaseDir + "log.txt"
var BotMe *models.User // 用于存储 bot 信息
var InlineDefaultHandler string = "voice" // 默认的 inline 命令,设为 "" 会显示进入 inline 命令菜单的提示
var InlineSubCommandSymbol string = "+"
var InlinePaginationSymbol string = "-"
var InlineResultsPerPage int = 50 // maxinum is 50, see https://core.telegram.org/bots/api#answerinlinequery
var Cache_path string = "./cache/"
type SignalChannel struct {
Database_save chan bool
PluginDB_save chan bool
PluginDB_reload chan bool
WorkDone chan bool
}
var SignalsChannel = SignalChannel{
Database_save: make(chan bool),
PluginDB_save: make(chan bool),
PluginDB_reload: make(chan bool),
WorkDone: make(chan bool),
}
var Commit string
var Branch string
var Version string
var BuildAt string
var BuildOn string
var Changes string // uncommit files when build

View File

@@ -0,0 +1,14 @@
package errt
const (
// LogTemplate is the template for log messages.
SendMessage string = "Failed to send message"
SendDocument string = "Failed to send document"
EditMessageText string = "Failed to edit message text"
EditMessageCaption string = "Failed to edit message caption"
EditMessageReplyMarkup string = "Failed to edit message reply markup"
DeleteMessage string = "Failed to delete message"
DeleteMessages string = "Failed to delete messages"
AnswerCallbackQuery string = "Failed to answer callback query"
AnswerInlineQuery string = "Failed to answer inline query"
)

View File

@@ -2,7 +2,6 @@ package internal_plugin
import (
"fmt"
"log"
"strings"
"trbot/utils"
"trbot/utils/handler_structs"
@@ -10,117 +9,195 @@ import (
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
)
func startHandler(opts *handler_structs.SubHandlerParams) {
defer utils.PanicCatcher("startHandler")
func startHandler(params *handler_structs.SubHandlerParams) error {
defer utils.PanicCatcher(params.Ctx, "startHandler")
logger := zerolog.Ctx(params.Ctx).
With().
Str("funcName", "startHandler").
Logger()
if len(opts.Fields) > 1 {
if len(params.Fields) > 1 {
for _, n := range plugin_utils.AllPlugins.SlashStart.WithPrefixHandler {
if strings.HasPrefix(opts.Fields[1], n.Prefix) {
inlineArgument := strings.Split(opts.Fields[1], "_")
if strings.HasPrefix(params.Fields[1], n.Prefix) {
inlineArgument := strings.Split(params.Fields[1], "_")
if inlineArgument[1] == n.Argument {
if n.Handler == nil {
logger.Debug().
Dict(utils.GetUserDict(params.Update.Message.From)).
Str("handlerPrefix", n.Prefix).
Str("handlerArgument", n.Argument).
Str("handlerName", n.Name).
Str("fullCommand", params.Update.Message.Text).
Msg("tigger /start command handler by prefix, but this handler function is nil, skip")
continue
}
n.Handler(opts)
return
err := n.Handler(params)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.Message.From)).
Str("handlerPrefix", n.Prefix).
Str("handlerArgument", n.Argument).
Str("handlerName", n.Name).
Str("fullCommand", params.Update.Message.Text).
Msg("Error in /start command handler by prefix tigger")
}
return err
}
}
}
for _, n := range plugin_utils.AllPlugins.SlashStart.Handler {
if opts.Fields[1] == n.Argument {
n.Handler(opts)
return
if params.Fields[1] == n.Argument {
err := n.Handler(params)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(params.Update.Message.From)).
Str("handlerArgument", n.Argument).
Str("handlerName", n.Name).
Str("fullCommand", params.Update.Message.Text).
Msg("Error in /start command handler by argument")
}
return err
}
}
}
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: fmt.Sprintf("Hello, *%s %s*\n\n您可以向此处发送一个贴纸您会得到一张转换后的 png 图片\n\n您也可以使用 [inline](https://telegram.org/blog/inline-bots?setln=en) 模式进行交互,点击下方的按钮来使用它", opts.Update.Message.From.FirstName, opts.Update.Message.From.LastName),
_, err := params.Thebot.SendMessage(params.Ctx, &bot.SendMessageParams{
ChatID: params.Update.Message.Chat.ID,
Text: fmt.Sprintf("Hello, *%s %s*\n\n您可以向此处发送一个贴纸您会得到一张转换后的 png 图片\n\n您也可以使用 [inline](https://telegram.org/blog/inline-bots?setln=en) 模式进行交互,点击下方的按钮来使用它", params.Update.Message.From.FirstName, params.Update.Message.From.LastName),
ParseMode: models.ParseModeMarkdownV1,
ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID},
LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: bot.True()},
ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{
ReplyParameters: &models.ReplyParameters{ MessageID: params.Update.Message.ID },
LinkPreviewOptions: &models.LinkPreviewOptions{ IsDisabled: bot.True() },
ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{
Text: "尝试 Inline 模式",
SwitchInlineQueryCurrentChat: " ",
}}}},
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Dict(utils.GetUserDict(params.Update.Message.From)).
Msg("Failed to send `bot welcome` message")
}
return err
}
func helpHandler(opts *handler_structs.SubHandlerParams) {
defer utils.PanicCatcher("helpHandler")
func helpHandler(params *handler_structs.SubHandlerParams) error {
defer utils.PanicCatcher(params.Ctx, "helpHandler")
logger := zerolog.Ctx(params.Ctx).
With().
Str("funcName", "helpHandler").
Logger()
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
_, err := params.Thebot.SendMessage(params.Ctx, &bot.SendMessageParams{
ChatID: params.Update.Message.Chat.ID,
Text: fmt.Sprintf("当前 bot 中有 %d 个帮助文档", len(plugin_utils.AllPlugins.HandlerHelp)),
ParseMode: models.ParseModeMarkdownV1,
ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID},
ReplyParameters: &models.ReplyParameters{MessageID: params.Update.Message.ID},
LinkPreviewOptions: &models.LinkPreviewOptions{IsDisabled: bot.True()},
ReplyMarkup: plugin_utils.BuildHandlerHelpKeyboard(),
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.Message.Chat)).
Dict(utils.GetUserDict(params.Update.Message.From)).
Msg("Failed to send `bot help keyboard` message")
}
return err
}
func helpCallbackHandler(opts *handler_structs.SubHandlerParams) {
if opts.Update.CallbackQuery.Data == "help-close" {
opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
func helpCallbackHandler(params *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(params.Ctx).
With().
Str("funcName", "helpCallbackHandler").
Logger()
if params.Update.CallbackQuery.Data == "help-close" {
_, err := params.Thebot.DeleteMessage(params.Ctx, &bot.DeleteMessageParams{
ChatID: params.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: params.Update.CallbackQuery.Message.Message.ID,
})
return
} else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "help-handler_") {
handlerName := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "help-handler_")
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Dict(utils.GetChatDict(&params.Update.CallbackQuery.Message.Message.Chat)).
Msg("Failed to delete `bot help keyboard` message")
}
return err
} else if strings.HasPrefix(params.Update.CallbackQuery.Data, "help-handler_") {
handlerName := strings.TrimPrefix(params.Update.CallbackQuery.Data, "help-handler_")
for _, handler := range plugin_utils.AllPlugins.HandlerHelp {
if handler.Name == handlerName {
var replyMarkup models.ReplyMarkup
// 如果帮助函数有自定的 ReplyMarkup则使用它否则显示默认的按钮
if handler.ReplyMarkup != nil {
replyMarkup = handler.ReplyMarkup
} else {
replyMarkup = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{
{
Text: "返回",
CallbackData: "help",
},
{
Text: "关闭",
CallbackData: "help-close",
},
}}}
{
Text: "返回",
CallbackData: "help",
},
{
Text: "关闭",
CallbackData: "help-close",
},
}}}
}
_, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
_, err := params.Thebot.EditMessageText(params.Ctx, &bot.EditMessageTextParams{
ChatID: params.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: params.Update.CallbackQuery.Message.Message.ID,
Text: handler.Description,
ParseMode: handler.ParseMode,
ReplyMarkup: replyMarkup,
})
if err != nil {
log.Println("[helpCallbackHandler] error when build handler help message:",err)
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.CallbackQuery.Message.Message.Chat)).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Str("pluginName", handler.Name).
Msg("Edit messag to `plugin help message` failed")
}
return
return err
}
}
_, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
CallbackQueryID: opts.Update.CallbackQuery.ID,
_, err := params.Thebot.AnswerCallbackQuery(params.Ctx, &bot.AnswerCallbackQueryParams{
CallbackQueryID: params.Update.CallbackQuery.ID,
Text: "您请求查看的帮助页面不存在,可能是机器人管理员已经移除了这个插件",
ShowAlert: true,
})
if err != nil {
log.Println("[helpCallbackHandler] error when send no this plugin message:", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Msg("Failed to send `help page is not exist` callback answer")
}
}
_, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
_, err := params.Thebot.EditMessageText(params.Ctx, &bot.EditMessageTextParams{
ChatID: params.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: params.Update.CallbackQuery.Message.Message.ID,
Text: fmt.Sprintf("当前 bot 中有 %d 个帮助文档", len(plugin_utils.AllPlugins.HandlerHelp)),
ReplyMarkup: plugin_utils.BuildHandlerHelpKeyboard(),
})
if err != nil {
log.Println("[helpCallbackHandler] error when rebuild help keyboard:",err)
logger.Error().
Err(err).
Dict(utils.GetChatDict(&params.Update.CallbackQuery.Message.Message.Chat)).
Dict(utils.GetUserDict(&params.Update.CallbackQuery.From)).
Msg("Edit messag to `bot help keyboard` failed")
}
return err
}

View File

@@ -1,28 +1,35 @@
package internal_plugin
import (
"context"
"fmt"
"log"
"strings"
"time"
"trbot/database"
"trbot/database/db_struct"
"trbot/plugins"
"trbot/utils"
"trbot/utils/configs"
"trbot/utils/consts"
"trbot/utils/errt"
"trbot/utils/handler_structs"
"trbot/utils/mess"
"trbot/utils/multe"
"trbot/utils/plugin_utils"
"trbot/utils/signals"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
)
// this function run only once in main
func Register() {
// 初始化 /plugins/ 中的插件
func Register(ctx context.Context) {
// 初始化 plugins/ 中的插件
plugins.InitPlugins()
plugin_utils.RunPluginInitializers(ctx)
// 以 `/` 符号开头的命令
plugin_utils.AddSlashSymbolCommandPlugins([]plugin_utils.SlashSymbolCommand{
{
@@ -35,32 +42,49 @@ func Register() {
},
{
SlashCommand: "chatinfo",
Handler: func(opts *handler_structs.SubHandlerParams) {
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID},
Text: fmt.Sprintf("类型: [<code>%v</code>]\nID: [<code>%v</code>]\n用户名:[<code>%v</code>]", opts.Update.Message.Chat.Type, opts.Update.Message.Chat.ID, opts.Update.Message.Chat.Username),
ParseMode: models.ParseModeHTML,
})
if err != nil {
logger.Error().
Err(err).
Str("command", "/chatinfo").
Msg("send `chat info` message failed")
}
return err
},
},
{
SlashCommand: "test",
Handler: func(opts *handler_structs.SubHandlerParams) {
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: "如果您愿意帮忙,请加入测试群组帮助我们完善机器人",
ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID},
ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{
ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{
Text: "点击加入测试群组",
URL: "https://t.me/+BomkHuFsjqc3ZGE1",
}}}},
})
if err != nil {
logger.Error().
Err(err).
Str("command", "/test").
Msg("send `test group invite link` message failed")
}
return err
},
},
{
SlashCommand: "fileid",
Handler: func(opts *handler_structs.SubHandlerParams) {
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var pendingMessage string
if opts.Update.Message.ReplyToMessage != nil {
if opts.Update.Message.ReplyToMessage.Sticker != nil {
@@ -69,7 +93,7 @@ func Register() {
pendingMessage = fmt.Sprintf("Type: [Document] \nFileID: [<code>%v</code>]", opts.Update.Message.ReplyToMessage.Document.FileID)
} else if opts.Update.Message.ReplyToMessage.Photo != nil {
pendingMessage = "Type: [Photo]\n"
if len(opts.Fields) > 1 && opts.Fields[1] == "all" { // 如果有 all 指示,显示图片所有分辨率的 File ID
if len(opts.Fields) > 1 && opts.Fields[1] == "all" { // 如果有 all 参数则显示图片所有分辨率的 File ID
for i, n := range opts.Update.Message.ReplyToMessage.Photo {
pendingMessage += fmt.Sprintf("\nPhotoID_%d: W:%d H:%d Size:%d \n[<code>%s</code>]\n", i, n.Width, n.Height, n.FileSize, n.FileID)
}
@@ -95,38 +119,35 @@ func Register() {
ParseMode: models.ParseModeHTML,
})
if err != nil {
log.Printf("Error response /fileid command: %v", err)
logger.Error().
Err(err).
Str("command", "/fileid").
Msg("send `file ID` message failed")
}
return err
},
},
{
SlashCommand: "version",
Handler: func(opts *handler_structs.SubHandlerParams) {
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
// info, err := opts.Thebot.GetWebhookInfo(ctx)
// fmt.Println(info)
// return
botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: mess.OutputVersionInfo(),
ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID},
ParseMode: models.ParseModeMarkdownV1,
})
time.Sleep(time.Second * 20)
success, _ := opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{
ChatID: opts.Update.Message.Chat.ID,
MessageIDs: []int{
opts.Update.Message.ID,
botMessage.ID,
},
})
if !success {
// 如果不能把用户的消息也删了,就单独删 bot 的消息
opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{
ChatID: opts.Update.Message.Chat.ID,
MessageID: botMessage.ID,
})
if err != nil {
logger.Error().
Err(err).
Str("command", "/version").
Msg("Failed to send `bot version info` message")
return err
}
return nil
},
},
}...)
@@ -141,41 +162,93 @@ func Register() {
{
Prefix: "via-inline",
Argument: "change-inline-command",
Handler: func(opts *handler_structs.SubHandlerParams) {
opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: fmt.Sprintf("选择一个 Inline 模式下的默认命令<blockquote>由于缓存原因,您可能需要等一会才能看到更新后的结果</blockquote>无论您是否设定了默认命令,您始终都可以在 inline 模式下输入 <code>%s</code> 号来查看全部可用的命令", consts.InlineSubCommandSymbol),
ParseMode: models.ParseModeHTML,
ReplyMarkup: utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
ChatID: opts.Update.Message.Chat.ID,
Text: fmt.Sprintf("选择一个 Inline 模式下的默认命令<blockquote>由于缓存原因,您可能需要等一会才能看到更新后的结果</blockquote>无论您是否设定了默认命令,您始终都可以在 inline 模式下输入 <code>%s</code> 号来查看全部可用的命令", configs.BotConfig.InlineSubCommandSymbol),
ParseMode: models.ParseModeHTML,
ReplyMarkup: plugin_utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID },
})
if err != nil {
logger.Error().
Err(err).
Msg("Failed to send `select inline default command keyboard` message")
}
return err
},
},
}...)
// 触发:'/start <Argument>',如果是通过消息按钮发送的,用户只会看到自己发送了一个 `/start`
plugin_utils.AddSlashStartCommandPlugins([]plugin_utils.SlashStartHandler{
{
Argument: "noreply",
Handler: nil, // 不回复
},
}...)
// 通过消息按钮触发的请求
plugin_utils.AddCallbackQueryCommandPlugins([]plugin_utils.CallbackQuery{
{
CommandChar: "inline_default_",
Handler: func(opts *handler_structs.SubHandlerParams) {
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
if opts.Update.CallbackQuery.Data == "inline_default_none" {
database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, "")
opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
ReplyMarkup: utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
err := database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, "")
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Remove inline default command flag failed")
return err
}
// if chatinfo get from redis database, it won't be the newst data, need reload it from database
opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, opts.Update.CallbackQuery.From.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Get chat info failed")
}
_, err = opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
ReplyMarkup: plugin_utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Edit message to `inline command select keyboard` failed")
return err
}
}
if strings.HasPrefix(opts.Update.CallbackQuery.Data, "inline_default_noedit_") {
callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "inline_default_noedit_")
for _, inlinePlugin := range plugin_utils.AllPlugins.InlineCommandList {
if inlinePlugin.Command == callbackField {
database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, callbackField)
opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
err := database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, callbackField)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Change inline default command flag failed")
return err
}
_, err = opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
CallbackQueryID: opts.Update.CallbackQuery.ID,
Text: fmt.Sprintf("已成功将您的 inline 模式默认命令设为 \"%s\"", callbackField),
ShowAlert: true,
Text: fmt.Sprintf("已成功将您的 inline 模式默认命令设为 \"%s\"", callbackField),
ShowAlert: true,
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Failed to send `inline command changed` callback answer")
return err
}
break
}
}
@@ -183,18 +256,41 @@ func Register() {
callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "inline_default_")
for _, inlinePlugin := range plugin_utils.AllPlugins.InlineCommandList {
if inlinePlugin.Command == callbackField {
database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, callbackField)
opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
ReplyMarkup: utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
err := database.SetCustomFlag(opts.Ctx, opts.Update.CallbackQuery.From.ID, db_struct.DefaultInlinePlugin, callbackField)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Change inline default command flag failed")
return err
}
// if chatinfo get from redis database, it won't be the newst data, need reload it from database
opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, opts.Update.CallbackQuery.From.ID)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Get chat info failed")
}
_, err = opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{
ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
ReplyMarkup: plugin_utils.BuildDefaultInlineCommandSelectKeyboard(opts.ChatInfo),
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Msg("Edit message to `inline command select keyboard` failed")
return err
}
break
}
}
}
consts.SignalsChannel.Database_save <- true
signals.SIGNALS.Database_save <- true
return nil
},
},
{
@@ -214,14 +310,16 @@ func Register() {
IsHideInCommandList: true,
IsCantBeDefault: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) {
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var handlerErr multe.MultiError
keywords := utils.InlineExtractKeywords(opts.Fields)
if len(keywords) == 0 {
_, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{
InlineQueryID: opts.Update.InlineQuery.ID,
Results: []models.InlineQueryResult{
&models.InlineQueryResultArticle{
ID: "custom voices",
ID: "custom_voices",
Title: "URL as a voice",
Description: "接着输入一个音频 URL 来其作为语音样式发送(不会转换格式)",
InputMessageContent: &models.InputTextMessageContent{
@@ -232,7 +330,12 @@ func Register() {
},
})
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some error when answer custom voice tips,", err))
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("content", "uaav command usage tips").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `uaav command usage tips` inline answer: %w", err)
}
} else if len(keywords) == 1 {
if strings.HasPrefix(keywords[0], "https://") {
@@ -248,7 +351,13 @@ func Register() {
IsPersonal: true,
})
if err != nil {
log.Println("Error when answering inline query: ", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "uaav valid voice url").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `uaav valid voice url` inline answer: %w", err)
}
} else {
_, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{
@@ -266,7 +375,13 @@ func Register() {
},
})
if err != nil {
log.Println("Error when answering inline query", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "uaav invalid URL").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `uaav invalid URL` inline answer: %w", err)
}
}
} else {
@@ -276,7 +391,7 @@ func Register() {
&models.InlineQueryResultArticle{
ID: "error",
Title: "参数过多,请注意空格",
Description: fmt.Sprintf("使用方法:@%s %suaav <单个音频链接>", consts.BotMe.Username, consts.InlineSubCommandSymbol),
Description: fmt.Sprintf("使用方法:@%s %suaav <单个音频链接>", consts.BotMe.Username, configs.BotConfig.InlineSubCommandSymbol),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "由于在使用 inline 模式时没有正确填写参数,无法完成消息",
ParseMode: models.ParseModeMarkdownV1,
@@ -285,15 +400,35 @@ func Register() {
},
})
if err != nil {
log.Println("Error when answering inline query", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("command", "uaav").
Msg("Failed to send `too much argumunt` inline result")
return err
}
}
return handlerErr.Flat()
},
Description: "将一个音频链接作为语音格式发送",
})
// inline 模式中以前缀触发的命令,需要自行处理输出。
plugin_utils.AddInlinePrefixHandlerPlugins([]plugin_utils.InlinePrefixHandler{
{
PrefixCommand: "panic",
Attr: plugin_utils.InlineHandlerAttr{
IsHideInCommandList: true,
IsCantBeDefault: true,
IsOnlyAllowAdmin: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) error {
// zerolog.Ctx(ctx).Error().Stack().Err(errors.WithStack(errors.New("test panic"))).Msg("")
panic("test panic")
},
Description: "测试 panic",
},
{
PrefixCommand: "log",
Attr: plugin_utils.InlineHandlerAttr{
@@ -301,8 +436,19 @@ func Register() {
IsCantBeDefault: true,
IsOnlyAllowAdmin: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) {
logs := mess.ReadLog()
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var handlerErr multe.MultiError
logs, err := mess.ReadLog()
if err != nil {
logger.Error().
Err(err).
Str("query", opts.Update.InlineQuery.Query).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("command", "log").
Msg("Failed to read log by inline command")
handlerErr.Addf("failed to read log: %w", err)
}
if logs != nil {
log_count := len(logs)
var log_all string
@@ -325,11 +471,16 @@ func Register() {
CacheTime: 0,
})
if err != nil {
log.Println("Error when answering inline query :log", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "log infos").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `log infos` inline answer: %w", err)
}
} else {
log.Println("Error when reading log file")
}
return handlerErr.Flat()
},
Description: "显示日志",
},
@@ -340,14 +491,16 @@ func Register() {
IsCantBeDefault: true,
IsOnlyAllowAdmin: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) {
consts.SignalsChannel.PluginDB_reload <- true
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var handlerErr multe.MultiError
signals.SIGNALS.PluginDB_reload <- true
_, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{
InlineQueryID: opts.Update.InlineQuery.ID,
Results: []models.InlineQueryResult{
&models.InlineQueryResultArticle{
ID: "reload",
Title: "已请求更新",
ID: "reloadpdb-back",
Title: "已请求重新加载插件数据库",
Description: fmt.Sprintf("last update at %s", time.Now().Format(time.RFC3339)),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "???",
@@ -359,8 +512,15 @@ func Register() {
CacheTime: 0,
})
if err != nil {
log.Println("Error when answering inline query :reload", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "plugin database reloaded").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `plugin database reloaded` inline answer: %w", err)
}
return handlerErr.Flat()
},
Description: "重新读取插件数据库",
},
@@ -371,14 +531,16 @@ func Register() {
IsCantBeDefault: true,
IsOnlyAllowAdmin: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) {
consts.SignalsChannel.PluginDB_save <- true
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var handlerErr multe.MultiError
signals.SIGNALS.PluginDB_save <- true
_, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{
InlineQueryID: opts.Update.InlineQuery.ID,
Results: []models.InlineQueryResult{
&models.InlineQueryResultArticle{
ID: "reload",
Title: "已请求保存",
ID: "savepdb-back",
Title: "已请求保存插件数据库",
Description: fmt.Sprintf("last save at %s", time.Now().Format(time.RFC3339)),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "???",
@@ -390,8 +552,15 @@ func Register() {
CacheTime: 0,
})
if err != nil {
log.Println("Error when answering inline query :reload", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "plugin database saved").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `plugin database saved` inline answer: %w", err)
}
return handlerErr.Flat()
},
Description: "保存插件数据库",
},
@@ -402,14 +571,16 @@ func Register() {
IsCantBeDefault: true,
IsOnlyAllowAdmin: true,
},
Handler: func(opts *handler_structs.SubHandlerParams) {
consts.SignalsChannel.Database_save <- true
Handler: func(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx)
var handlerErr multe.MultiError
signals.SIGNALS.Database_save <- true
_, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{
InlineQueryID: opts.Update.InlineQuery.ID,
Results: []models.InlineQueryResult{
&models.InlineQueryResultArticle{
ID: "savedb",
Title: "已请求保存",
ID: "savedb-back",
Title: "已请求保存数据库",
Description: fmt.Sprintf("last update at %s", time.Now().Format(time.RFC3339)),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "???",
@@ -421,8 +592,15 @@ func Register() {
CacheTime: 0,
})
if err != nil {
log.Println("Error when answering inline query :savedb", err)
logger.Error().
Err(err).
Dict(utils.GetUserDict(opts.Update.InlineQuery.From)).
Str("query", opts.Update.InlineQuery.Query).
Str("content", "database saved").
Msg(errt.AnswerInlineQuery)
handlerErr.Addf("failed to send `database saved` inline answer: %w", err)
}
return handlerErr.Flat()
},
Description: "保存数据库",
},

View File

@@ -4,129 +4,22 @@ import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"runtime"
"time"
"unicode"
"trbot/utils/configs"
"trbot/utils/consts"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/joho/godotenv"
)
// 查找 bot token优先级为 环境变量 > .env 文件
func WhereIsBotToken() string {
consts.BotToken = os.Getenv("BOT_TOKEN")
if consts.BotToken == "" {
// log.Printf("No bot token in environment, trying to read it from the .env file")
godotenv.Load()
consts.BotToken = os.Getenv("BOT_TOKEN")
if consts.BotToken == "" {
log.Fatalln("No bot token in environment and .env file, try create a bot from https://t.me/@botfather https://core.telegram.org/bots/tutorial#obtain-your-bot-token")
}
log.Printf("Get token from .env file: %s", ShowBotID())
} else {
log.Printf("Get token from environment: %s", ShowBotID())
}
return consts.BotToken
}
// 输出 bot 的 ID
func ShowBotID() string {
var botID string
for _, char := range consts.BotToken {
if unicode.IsDigit(char) {
botID += string(char)
} else {
break // 遇到非数字字符停止
}
}
return botID
}
func UsingWebhook() bool {
consts.WebhookURL = os.Getenv("WEBHOOK_URL")
if consts.WebhookURL == "" {
// 到这里可能变量没在环境里,试着读一下 .env 文件
godotenv.Load()
consts.WebhookURL = os.Getenv("WEBHOOK_URL")
if consts.WebhookURL == "" {
// 到这里就是 .env 文件里也没有,不启用
log.Printf("No Webhook URL in environment and .env file, using getUpdate")
return false
}
// 从 .env 文件中获取到了 URL启用 Webhook
log.Printf("Get Webhook URL from .env file: %s", consts.WebhookURL)
return true
} else {
// 从环境变量中获取到了 URL启用 Webhook
log.Printf("Get Webhook URL from environment: %s", consts.WebhookURL)
return true
}
}
func SetUpWebhook(ctx context.Context, thebot *bot.Bot, params *bot.SetWebhookParams) {
webHookInfo, err := thebot.GetWebhookInfo(ctx)
if err != nil { log.Println(err) }
if webHookInfo.URL != params.URL {
if webHookInfo.URL == "" {
log.Println("Webhook not set, setting it now...")
} else {
log.Printf("unsame Webhook URL [%s], save it and setting up new URL...", webHookInfo.URL)
PrintLogAndSave(time.Now().Format(time.RFC3339) + " (unsame) old Webhook URL: " + webHookInfo.URL)
}
success, err := thebot.SetWebhook(ctx, params)
if err != nil { log.Panicln("Set Webhook URL err:", err) }
if success { log.Println("Webhook setup successfully:", params.URL) }
} else {
log.Println("Webhook is already set:", webHookInfo.URL)
}
}
func SaveAndCleanRemoteWebhookURL(ctx context.Context, thebot *bot.Bot) *models.WebhookInfo {
webHookInfo, err := thebot.GetWebhookInfo(ctx)
if err != nil { log.Println(err) }
if webHookInfo.URL != "" {
log.Printf("found Webhook URL [%s] set at api server, save and clean it...", webHookInfo.URL)
PrintLogAndSave(time.Now().Format(time.RFC3339) + " (remote) old Webhook URL: " + webHookInfo.URL)
thebot.DeleteWebhook(ctx, &bot.DeleteWebhookParams{
DropPendingUpdates: false,
})
return webHookInfo
}
return nil
}
func PrintLogAndSave(message string) {
log.Println(message)
// 打开日志文件,如果不存在则创建
file, err := os.OpenFile(consts.LogFile_path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
return
}
defer file.Close()
// 将文本写入日志文件
_, err = file.WriteString(message + "\n")
if err != nil {
log.Println(err)
return
}
}
// 从 log.txt 读取文件
func ReadLog() []string {
func ReadLog() ([]string, error) {
// 打开日志文件
file, err := os.Open(consts.LogFile_path)
file, err := os.Open(consts.LogFilePath)
if err != nil {
log.Println(err)
return nil
return nil, err
}
defer file.Close()
@@ -138,30 +31,38 @@ func ReadLog() []string {
}
if err := scanner.Err(); err != nil {
log.Println(err)
return nil
return nil, err
}
return lines
return lines, nil
}
func PrivateLogToChat(ctx context.Context, thebot *bot.Bot, update *models.Update) {
thebot.SendMessage(ctx, &bot.SendMessageParams{
ChatID: consts.LogChat_ID,
ChatID: configs.BotConfig.LogChatID,
Text: fmt.Sprintf("[%s %s](t.me/@id%d) say: \n%s", update.Message.From.FirstName, update.Message.From.LastName, update.Message.Chat.ID, update.Message.Text),
ParseMode: models.ParseModeMarkdownV1,
})
}
func OutputVersionInfo() string {
// 获取 git sha 和 commit 时间
c, _ := exec.Command("git", "rev-parse", "HEAD").Output()
// 获取 git 分支
b, _ := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
// 获取 commit 说明
m, _ := exec.Command("git", "log", "-1", "--pretty=%s").Output()
r := runtime.Version()
grs := runtime.NumGoroutine()
h, _ := os.Hostname()
info := fmt.Sprintf("Branch: %sCommit: [%s - %s](https://gitea.trle5.xyz/trle5/trbot/commit/%s)\nRuntime: %s\nGoroutine: %d\nHostname: %s", b, m, c[:10], c, r, grs, h)
return info
hostname, _ := os.Hostname()
var gitURL string = "https://gitea.trle5.xyz/trle5/trbot/commit/"
var info string
if consts.BuildAt != "" {
info += fmt.Sprintf("`Version: `%s\n", consts.Version)
info += fmt.Sprintf("`Branch: `%s\n", consts.Branch)
info += fmt.Sprintf("`Commit: `[%s](%s%s) (%s)\n", consts.Commit[:10], gitURL, consts.Commit, consts.Changes)
info += fmt.Sprintf("`BuildAt: `%s\n", consts.BuildAt)
info += fmt.Sprintf("`BuildOn: `%s\n", consts.BuildOn)
info += fmt.Sprintf("`Runtime: `%s\n", runtime.Version())
info += fmt.Sprintf("`Goroutine: `%d\n", runtime.NumGoroutine())
info += fmt.Sprintf("`Hostname: `%s\n", hostname)
return info
}
return fmt.Sprintln(
"Warning: No build info\n",
"\n`Runtime: `", runtime.Version(),
"\n`Goroutine: `", runtime.NumGoroutine(),
"\n`Hostname: `", hostname,
)
}

View File

@@ -0,0 +1,37 @@
package multe
import (
"errors"
"fmt"
)
type MultiError struct {
Errors []error
}
func (e *MultiError) Add(errs ...error) *MultiError {
for _, err := range errs {
if err != nil {
e.Errors = append(e.Errors, err)
}
}
return e
}
// add formatted error by use fmt.Errorf()
func (e *MultiError) Addf(format string, a ...any) *MultiError {
e.Errors = append(e.Errors, fmt.Errorf(format, a...))
return e
}
// func (e *MultiError) AddWith(key string, dict *zerolog.Event) string {
// dict.
// }
// a string error by use errors.Join()
func (e *MultiError) Flat() error {
if len(e.Errors) == 0 {
return nil
}
return errors.Join(e.Errors...)
}

View File

@@ -14,7 +14,7 @@ import (
type HandlerByChatID struct {
ChatID int64
PluginName string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddHandlerByChatIDPlugins(handlers ...HandlerByChatID) int {

View File

@@ -2,14 +2,14 @@ package plugin_utils
import (
"fmt"
"log"
"strings"
"trbot/utils/consts"
"trbot/utils"
"trbot/utils/handler_structs"
"trbot/utils/type_utils"
"trbot/utils/type/message_utils"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/rs/zerolog"
)
type HandlerByMessageTypeFunctions map[string]HandlerByMessageType
@@ -32,15 +32,15 @@ func (funcs HandlerByMessageTypeFunctions) BuildSelectKeyboard() models.ReplyMar
type HandlerByMessageType struct {
PluginName string
ChatType models.ChatType
MessageType type_utils.MessageTypeList
MessageType message_utils.MessageTypeList
AllowAutoTrigger bool // Allow auto trigger when there is only one handler of the same type
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
/*
If more than one such plugin is registered
or the `AllowAutoTrigger`` flag is not `true`
The bot will reply to the message that triggered
this plugin and send a keyboard to let the
user choose which plugin they want to use.
@@ -48,7 +48,7 @@ type HandlerByMessageType struct {
In this case, the data that the plugin needs
to process will change from `update.Message` to
in `opts.Update.CallbackQuery.Message.Message.ReplyToMessage`.
But I'm not sure whether this field will be empty,
so need to manually judge it in the plugin.
@@ -62,11 +62,11 @@ type HandlerByMessageType struct {
```
*/
func AddHandlerByMessageTypePlugins(plugins ...HandlerByMessageType) int {
if AllPlugins.HandlerByMessageType == nil { AllPlugins.HandlerByMessageType = map[models.ChatType]map[type_utils.MessageTypeList]HandlerByMessageTypeFunctions{} }
if AllPlugins.HandlerByMessageType == nil { AllPlugins.HandlerByMessageType = map[models.ChatType]map[message_utils.MessageTypeList]HandlerByMessageTypeFunctions{} }
var pluginCount int
for _, plugin := range plugins {
if AllPlugins.HandlerByMessageType[plugin.ChatType] == nil { AllPlugins.HandlerByMessageType[plugin.ChatType] = map[type_utils.MessageTypeList]HandlerByMessageTypeFunctions{} }
if AllPlugins.HandlerByMessageType[plugin.ChatType] == nil { AllPlugins.HandlerByMessageType[plugin.ChatType] = map[message_utils.MessageTypeList]HandlerByMessageTypeFunctions{} }
if AllPlugins.HandlerByMessageType[plugin.ChatType][plugin.MessageType] == nil { AllPlugins.HandlerByMessageType[plugin.ChatType][plugin.MessageType] = HandlerByMessageTypeFunctions{} }
_, isExist := AllPlugins.HandlerByMessageType[plugin.ChatType][plugin.MessageType][plugin.PluginName]
@@ -79,7 +79,7 @@ func AddHandlerByMessageTypePlugins(plugins ...HandlerByMessageType) int {
return pluginCount
}
func RemoveHandlerByMessageTypePlugin(chatType models.ChatType, messageType type_utils.MessageTypeList, pluginName string) {
func RemoveHandlerByMessageTypePlugin(chatType models.ChatType, messageType message_utils.MessageTypeList, pluginName string) {
if AllPlugins.HandlerByMessageType == nil { return }
_, isExist := AllPlugins.HandlerByMessageType[chatType][messageType][pluginName]
@@ -88,33 +88,81 @@ func RemoveHandlerByMessageTypePlugin(chatType models.ChatType, messageType type
}
}
func SelectHandlerByMessageTypeHandlerCallback(opts *handler_structs.SubHandlerParams) {
func SelectHandlerByMessageTypeHandlerCallback(opts *handler_structs.SubHandlerParams) error {
logger := zerolog.Ctx(opts.Ctx).
With().
Str("funcName", "SelectHandlerByMessageTypeHandlerCallback").
Logger()
var chatType, messageType, pluginName string
var chatTypeMessageTypeAndPluginName string
var chatTypeMessageTypeAndPluginName string
if strings.HasPrefix(opts.Update.CallbackQuery.Data, "HBMT_") {
chatTypeMessageTypeAndPluginName = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "HBMT_")
chatTypeAndPluginNameList := strings.Split(chatTypeMessageTypeAndPluginName, "_")
if len(chatTypeAndPluginNameList) < 3 { return }
chatTypeMessageTypeAndPluginName = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "HBMT_")
chatTypeAndPluginNameList := strings.Split(chatTypeMessageTypeAndPluginName, "_")
if len(chatTypeAndPluginNameList) < 3 {
err := fmt.Errorf("no enough fields")
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Str("CallbackQuery", opts.Update.CallbackQuery.Data).
Msg("User selected callback query doesn't have enough fields")
return err
}
chatType, messageType, pluginName = chatTypeAndPluginNameList[0], chatTypeAndPluginNameList[1], chatTypeAndPluginNameList[2]
handler, isExist := AllPlugins.HandlerByMessageType[models.ChatType(chatType)][type_utils.MessageTypeList(messageType)][pluginName]
handler, isExist := AllPlugins.HandlerByMessageType[models.ChatType(chatType)][message_utils.MessageTypeList(messageType)][pluginName]
if isExist {
if consts.IsDebugMode {
log.Printf("select handler by message type [%s] plugin [%s] for chat type [%s]", messageType, pluginName, chatType)
}
logger.Debug().
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Str("messageType", messageType).
Str("pluginName", pluginName).
Str("chatType", chatType).
Msg("User selected a handler by message")
// if opts.Update.CallbackQuery.Message.Message.ReplyToMessage != nil {
// opts.Update.Message = opts.Update.CallbackQuery.Message.Message.ReplyToMessage
// }
handler.Handler(opts)
opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{
err := handler.Handler(opts)
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Str("handerChatType", string(handler.ChatType)).
Str("handlerMessageType", string(handler.MessageType)).
Bool("allowAutoTrigger", handler.AllowAutoTrigger).
Str("handlerName", handler.PluginName).
Msg("Error in handler by message type")
return err
}
_, err = opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{
ChatID: opts.Update.CallbackQuery.From.ID,
MessageID: opts.Update.CallbackQuery.Message.Message.ID,
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)).
Msg("Failed to delete `select handler by message type keyboard` message")
return err
}
} else {
opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
_, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
CallbackQueryID: opts.Update.CallbackQuery.ID,
Text: fmt.Sprintf("此功能 [ %s ] 不可用,可能是管理员已经移除了这个功能", pluginName),
ShowAlert: true,
})
if err != nil {
logger.Error().
Err(err).
Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)).
Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)).
Msg("Failed to send `handler by message type is not exist` callback answer")
return err
}
}
}
logger.Warn().
Str("callbackQuery", opts.Update.CallbackQuery.Data).
Msg("Receive an invalid callback query, it should start with `HBMT_`")
return nil
}

View File

@@ -7,7 +7,7 @@ import "trbot/utils/handler_structs"
// 你也可以忽略这个提醒,但在发送消息时使用 ReplyMarkup 参数添加按钮的时候,需要评断并控制一下 CallbackData 的长度是否超过了 64 个字符,否则消息会无法发出。
type CallbackQuery struct {
CommandChar string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddCallbackQueryCommandPlugins(Plugins ...CallbackQuery) int {

View File

@@ -4,7 +4,7 @@ import "trbot/utils/handler_structs"
type CustomSymbolCommand struct {
FullCommand string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddCustomSymbolCommandPlugins(Plugins ...CustomSymbolCommand) int {

View File

@@ -19,6 +19,9 @@ func BuildHandlerHelpKeyboard() models.ReplyMarkup {
},
})
}
if len(button) == 0 {
return nil
}
return models.InlineKeyboardMarkup{
InlineKeyboard: button,
}

View File

@@ -1,6 +1,9 @@
package plugin_utils
import (
"fmt"
"trbot/database/db_struct"
"trbot/utils/configs"
"trbot/utils/handler_structs"
"github.com/go-telegram/bot/models"
@@ -13,16 +16,16 @@ type InlineHandlerAttr struct {
}
type InlineCommandList struct {
Command string
Attr InlineHandlerAttr
Command string
Attr InlineHandlerAttr
Description string
}
// 需要返回一个列表,将由程序的分页函数来控制分页和输出
type InlineHandler struct {
Command string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams) []models.InlineQueryResult
Command string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams) []models.InlineQueryResult
Description string
}
@@ -40,9 +43,9 @@ func AddInlineHandlerPlugins(InlineHandlerPlugins ...InlineHandler) int {
// 完全由插件自行控制输出
type InlineManualHandler struct {
Command string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams)
Command string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams) error
Description string
}
@@ -61,9 +64,9 @@ func AddInlineManualHandlerPlugins(InlineManualHandlerPlugins ...InlineManualHan
// 符合命令前缀,完全由插件自行控制输出
type InlinePrefixHandler struct {
PrefixCommand string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams)
Description string
Attr InlineHandlerAttr
Handler func(*handler_structs.SubHandlerParams) error
Description string
}
func AddInlinePrefixHandlerPlugins(InlineManualHandlerPlugins ...InlinePrefixHandler) int {
@@ -77,3 +80,41 @@ func AddInlinePrefixHandlerPlugins(InlineManualHandlerPlugins ...InlinePrefixHan
}
return pluginCount
}
// 构建一个用于选择 Inline 模式下默认命令的按钮键盘
func BuildDefaultInlineCommandSelectKeyboard(chatInfo *db_struct.ChatInfo) models.ReplyMarkup {
var inlinePlugins [][]models.InlineKeyboardButton
for _, v := range AllPlugins.InlineCommandList {
if v.Attr.IsCantBeDefault {
continue
}
if chatInfo.DefaultInlinePlugin == v.Command {
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{{
Text: fmt.Sprintf("✅ [ %s%s ] - %s", configs.BotConfig.InlineSubCommandSymbol, v.Command, v.Description),
CallbackData: "inline_default_" + v.Command,
}})
} else {
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{{
Text: fmt.Sprintf("[ %s%s ] - %s", configs.BotConfig.InlineSubCommandSymbol, v.Command, v.Description),
CallbackData: "inline_default_" + v.Command,
}})
}
}
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{
{
Text: "取消默认命令",
CallbackData: "inline_default_none",
},
{
Text: "浏览 inline 命令菜单",
SwitchInlineQueryCurrentChat: "+",
},
})
kb := &models.InlineKeyboardMarkup{
InlineKeyboard: inlinePlugins,
}
return kb
}

View File

@@ -8,8 +8,9 @@ type SlashStartCommand struct {
}
type SlashStartHandler struct {
Name string
Argument string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddSlashStartCommandPlugins(SlashStartCommandPlugins ...SlashStartHandler) int {
@@ -25,9 +26,10 @@ func AddSlashStartCommandPlugins(SlashStartCommandPlugins ...SlashStartHandler)
}
type SlashStartWithPrefixHandler struct {
Name string
Prefix string
Argument string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddSlashStartWithPrefixCommandPlugins(SlashStartWithPrefixCommandPlugins ...SlashStartWithPrefixHandler) int {

View File

@@ -4,7 +4,7 @@ import "trbot/utils/handler_structs"
type SlashSymbolCommand struct {
SlashCommand string // 'command' in '/command'
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddSlashSymbolCommandPlugins(Plugins ...SlashSymbolCommand) int {

View File

@@ -4,7 +4,7 @@ import "trbot/utils/handler_structs"
type SuffixCommand struct {
SuffixCommand string
Handler func(*handler_structs.SubHandlerParams)
Handler func(*handler_structs.SubHandlerParams) error
}
func AddSuffixCommandPlugins(Plugins ...SuffixCommand) int {

View File

@@ -1,14 +1,15 @@
package plugin_utils
import (
"fmt"
"log"
"context"
"github.com/rs/zerolog"
)
type DatabaseHandler struct {
Name string
Loader func()
Saver func() error
Loader func(ctx context.Context) error
Saver func(ctx context.Context) error
}
func AddDataBaseHandler(InlineHandlerPlugins ...DatabaseHandler) int {
@@ -23,31 +24,61 @@ func AddDataBaseHandler(InlineHandlerPlugins ...DatabaseHandler) int {
return pluginCount
}
func ReloadPluginsDatabase() {
for _, plugin := range AllPlugins.Databases {
if plugin.Loader == nil {
log.Printf("Plugin [%s] has no loader function, skipping", plugin.Name)
continue
}
plugin.Loader()
}
}
func ReloadPluginsDatabase(ctx context.Context) {
logger := zerolog.Ctx(ctx).
With().
Str("funcName", "ReloadPluginsDatabase").
Logger()
func SavePluginsDatabase() string {
dbCount := len(AllPlugins.Databases)
successCount := 0
for _, plugin := range AllPlugins.Databases {
if plugin.Saver == nil {
log.Printf("Plugin [%s] has no saver function, skipping", plugin.Name)
successCount++
if plugin.Loader == nil {
logger.Warn().
Str("pluginName", plugin.Name).
Msg("Plugin has no loader function, skipping")
continue
}
err := plugin.Saver()
err := plugin.Loader(ctx)
if err != nil {
log.Printf("Plugin [%s] failed to save: %s", plugin.Name, err)
logger.Error().
Err(err).
Str("pluginName", plugin.Name).
Msg("Plugin failed to reload database")
} else {
successCount++
}
}
return fmt.Sprintf("[plugin_utils] Saved (%d/%d) plugins database", successCount, dbCount)
logger.Info().Msgf("Reloaded (%d/%d) plugins database", successCount, dbCount)
}
func SavePluginsDatabase(ctx context.Context) {
logger := zerolog.Ctx(ctx).
With().
Str("funcName", "SavePluginsDatabase").
Logger()
dbCount := len(AllPlugins.Databases)
successCount := 0
for _, plugin := range AllPlugins.Databases {
if plugin.Saver == nil {
logger.Warn().
Str("pluginName", plugin.Name).
Msg("Plugin has no saver function, skipping")
successCount++
continue
}
err := plugin.Saver(ctx)
if err != nil {
logger.Error().
Err(err).
Str("pluginName", plugin.Name).
Msg("Plugin failed to reload database")
} else {
successCount++
}
}
logger.Info().Msgf("Saved (%d/%d) plugins database", successCount, dbCount)
}

View File

@@ -0,0 +1,56 @@
package plugin_utils
import (
"context"
"github.com/rs/zerolog"
)
type Initializer struct {
Name string
Func func(ctx context.Context) error
}
func AddInitializer(initializers ...Initializer) int {
if AllPlugins.Initializer == nil {
AllPlugins.Initializer = []Initializer{}
}
var pluginCount int
for _, initializer := range initializers {
AllPlugins.Initializer = append(AllPlugins.Initializer, initializer)
pluginCount++
}
return pluginCount
}
func RunPluginInitializers(ctx context.Context) {
logger := zerolog.Ctx(ctx)
count := len(AllPlugins.Initializer)
successCount := 0
for _, initializer := range AllPlugins.Initializer {
if initializer.Func == nil {
logger.Warn().
Str("pluginName", initializer.Name).
Msg("Plugin has no initialize function, skipping")
continue
}
err := initializer.Func(ctx)
if err != nil {
logger.Error().
Err(err).
Str("pluginName", initializer.Name).
Msg("Failed to initialize plugin")
continue
} else {
logger.Info().
Str("pluginName", initializer.Name).
Msg("Plugin initialize success")
successCount++
}
}
logger.Info().Msgf("Run (%d/%d) initializer success", successCount, count)
}

View File

@@ -1,12 +1,13 @@
package plugin_utils
import (
"trbot/utils/type_utils"
"trbot/utils/type/message_utils"
"github.com/go-telegram/bot/models"
)
type Plugin_All struct {
Initializer []Initializer
Databases []DatabaseHandler
HandlerHelp []HandlerHelp
@@ -25,7 +26,7 @@ type Plugin_All struct {
CallbackQuery []CallbackQuery // 处理 InlineKeyboardMarkup 的 callback 函数
// 根据聊天类型设定的默认处理函数
HandlerByMessageType map[models.ChatType]map[type_utils.MessageTypeList]HandlerByMessageTypeFunctions
HandlerByMessageType map[models.ChatType]map[message_utils.MessageTypeList]HandlerByMessageTypeFunctions
// 以聊天 ID 设定的默认处理函数,第一个 map 为 ID第二个为 handler 名称
HandlerByChatID map[int64]map[string]HandlerByChatID

View File

@@ -2,46 +2,66 @@ package signals
import (
"context"
"fmt"
"log"
"os"
"time"
"trbot/database"
"trbot/database/yaml_db"
"trbot/utils/consts"
"trbot/utils/mess"
"trbot/utils/plugin_utils"
"github.com/rs/zerolog"
)
func SignalsHandler(ctx context.Context, SIGNAL consts.SignalChannel) {
type SignalChannel struct {
Database_save chan bool
PluginDB_save chan bool
PluginDB_reload chan bool
}
var SIGNALS = SignalChannel{
Database_save: make(chan bool),
PluginDB_save: make(chan bool),
PluginDB_reload: make(chan bool),
}
func SignalsHandler(ctx context.Context) {
logger := zerolog.Ctx(ctx)
every10Min := time.NewTicker(10 * time.Minute)
defer every10Min.Stop()
// additional.AdditionalDatas = additional.ReadAdditionalDatas(consts.AdditionalDatas_paths)
var saveDatabaseRetryCount int = 0
var saveDatabaseRetryMax int = 10
for {
select {
case <-every10Min.C: // 每次 Ticker 触发时执行任务
yaml_db.AutoSaveDatabaseHandler()
yaml_db.AutoSaveDatabaseHandler(ctx)
case <-ctx.Done():
log.Println("Cancle signal received")
yaml_db.AutoSaveDatabaseHandler()
log.Println("Database saved")
SIGNAL.WorkDone <- true
return
case <-SIGNAL.Database_save:
yaml_db.Database.UpdateTimestamp = time.Now().Unix()
err := yaml_db.SaveYamlDB(consts.DB_path, consts.MetadataFileName, yaml_db.Database)
if saveDatabaseRetryCount == 0 { logger.Warn().Msg("Cancle signal received") }
err := database.SaveDatabase(ctx)
if err != nil {
mess.PrintLogAndSave(fmt.Sprintln("some issues happend when some function call save database now:", err))
} else {
mess.PrintLogAndSave("save at " + time.Now().Format(time.RFC3339))
saveDatabaseRetryCount++
logger.Error().
Err(err).
Int("retryCount", saveDatabaseRetryCount).
Int("maxRetry", saveDatabaseRetryMax).
Msg("Failed to save database, retrying...")
time.Sleep(2 * time.Second)
if saveDatabaseRetryCount >= saveDatabaseRetryMax {
logger.Fatal().
Err(err).
Msg("Failed to save database too many times, exiting")
}
continue
}
case <-SIGNAL.PluginDB_reload:
plugin_utils.ReloadPluginsDatabase()
log.Println("Plugin Database reloaded")
case <-SIGNAL.PluginDB_save:
mess.PrintLogAndSave(plugin_utils.SavePluginsDatabase())
// log.Println("Plugin Database saved")
logger.Info().Msg("Database saved")
time.Sleep(1 * time.Second)
logger.Warn().Msg("manually stopped")
os.Exit(0)
case <-SIGNALS.Database_save:
database.SaveDatabase(ctx)
case <-SIGNALS.PluginDB_reload:
plugin_utils.ReloadPluginsDatabase(ctx)
case <-SIGNALS.PluginDB_save:
plugin_utils.SavePluginsDatabase(ctx)
}
}
}

View File

@@ -1,4 +1,4 @@
package type_utils
package message_utils
import "github.com/go-telegram/bot/models"
@@ -8,6 +8,9 @@ type MessageAttribute struct {
IsFromLinkedChannel bool `yaml:"IsFromLinkedChannel,omitempty"` // is automatic forward post from linked channel
IsUserAsChannel bool `yaml:"IsUserAsChannel,omitempty"` // user selected to send message as a channel
IsHasSenderChat bool `yaml:"IsHasSenderChat,omitempty"` // sender of the message when sent on behalf of a chat, eg current group/supergroup or linked channel
IsFromBot bool `yaml:"IsFromBot,omitempty"` // message send by bot
IsFromPremium bool `yaml:"IsFromPremium,omitempty"` // message from a premium account
IsFromBusinessBot bool `yaml:"IsFromBusinessBot,omitempty"` // the bot that actually sent the message on behalf of the business account
IsChatEnableForum bool `yaml:"IsChatEnableForum,omitempty"` // group or supergroup is enable topic
IsForwardMessage bool `yaml:"IsForwardMessage,omitempty"` // not a origin message, forward from somewhere
IsTopicMessage bool `yaml:"IsTopicMessage,omitempty"` // the message is sent to a forum topic
@@ -18,7 +21,7 @@ type MessageAttribute struct {
IsQuoteHasEntities bool `yaml:"IsQuoteHasEntities,omitempty"` // is quote message has entities
IsManualQuote bool `yaml:"IsManualQuote,omitempty"` // user manually select text to quote a message. if false, just use 'reply to other chat'
IsReplyToStory bool `yaml:"IsReplyToStory,omitempty"` // TODO
IsViaBot bool `yaml:"IsViaBot,omitempty"` // message by inline mode
IsViaBot bool `yaml:"IsViaBot,omitempty"` // message by using bot inline mode
IsEdited bool `yaml:"IsEdited,omitempty"` // message aready edited
IsFromOffline bool `yaml:"IsFromOffline,omitempty"` // eg scheduled message
IsGroupedMedia bool `yaml:"IsGroupedMedia,omitempty"` // media group, like select more than one file or photo to send
@@ -30,7 +33,7 @@ type MessageAttribute struct {
IsHasInlineKeyboard bool `yaml:"IsHasInlineKeyboard,omitempty"` // message has inline keyboard
}
// 判断消息属性
// 判断消息属性
func GetMessageAttribute(msg *models.Message) MessageAttribute {
var attribute MessageAttribute
if msg.SenderChat != nil {
@@ -49,6 +52,17 @@ func GetMessageAttribute(msg *models.Message) MessageAttribute {
}
}
}
if msg.From != nil {
if msg.From.IsBot {
attribute.IsFromBot = true
}
if msg.From.IsPremium {
attribute.IsFromPremium = true
}
}
if msg.SenderBusinessBot != nil {
attribute.IsFromBusinessBot = true
}
if msg.Chat.IsForum {
attribute.IsChatEnableForum = true
}

View File

@@ -1,4 +1,4 @@
package type_utils
package message_utils
import (
"reflect"
@@ -31,6 +31,7 @@ type MessageType struct {
Giveaway bool `yaml:"Giveaway,omitempty"`
}
// 将消息类型结构体转换为 MessageTypeList(string) 类型
func (mt MessageType)InString() MessageTypeList {
val := reflect.ValueOf(mt)
typ := reflect.TypeOf(mt)

View File

@@ -0,0 +1,148 @@
package update_utils
import (
"reflect"
"github.com/go-telegram/bot/models"
)
// 更新类型
type UpdateType struct {
Message bool `yaml:"Message,omitempty"` // *models.Message
EditedMessage bool `yaml:"EditedMessage,omitempty"` // *models.Message
ChannelPost bool `yaml:"ChannelPost,omitempty"` // *models.Message
EditedChannelPost bool `yaml:"EditedChannelPost,omitempty"` // *models.Message
BusinessConnection bool `yaml:"BusinessConnection,omitempty"` // *models.BusinessConnection
BusinessMessage bool `yaml:"BusinessMessage,omitempty"` // *models.Message
EditedBusinessMessage bool `yaml:"EditedBusinessMessage,omitempty"` // *models.Message
DeletedBusinessMessages bool `yaml:"DeletedBusinessMessages,omitempty"` // *models.BusinessMessagesDeleted
MessageReaction bool `yaml:"MessageReaction,omitempty"` // *models.MessageReactionUpdated
MessageReactionCount bool `yaml:"MessageReactionCount,omitempty"` // *models.MessageReactionCountUpdated
InlineQuery bool `yaml:"InlineQuery,omitempty"` // *models.InlineQuery
ChosenInlineResult bool `yaml:"ChosenInlineResult,omitempty"` // *models.ChosenInlineResult
CallbackQuery bool `yaml:"CallbackQuery,omitempty"` // *models.CallbackQuery
ShippingQuery bool `yaml:"ShippingQuery,omitempty"` // *models.ShippingQuery
PreCheckoutQuery bool `yaml:"PreCheckoutQuery,omitempty"` // *models.PreCheckoutQuery
PurchasedPaidMedia bool `yaml:"PurchasedPaidMedia,omitempty"` // *models.PaidMediaPurchased
Poll bool `yaml:"Poll,omitempty"` // *models.Poll
PollAnswer bool `yaml:"PollAnswer,omitempty"` // *models.PollAnswer
MyChatMember bool `yaml:"MyChatMember,omitempty"` // *models.ChatMemberUpdated
ChatMember bool `yaml:"ChatMember,omitempty"` // *models.ChatMemberUpdated
ChatJoinRequest bool `yaml:"ChatJoinRequest,omitempty"` // *models.ChatJoinRequest
ChatBoost bool `yaml:"ChatBoost,omitempty"` // *models.ChatBoostUpdated
RemovedChatBoost bool `yaml:"RemovedChatBoost,omitempty"` // *models.ChatBoostRemoved
}
// 将消息类型结构体转换为 UpdateTypeList(string) 类型
func (ut UpdateType)InString() UpdateTypeList {
val := reflect.ValueOf(ut)
typ := reflect.TypeOf(ut)
for i := 0; i < val.NumField(); i++ {
if val.Field(i).Bool() {
return UpdateTypeList(typ.Field(i).Name)
}
}
return ""
}
type UpdateTypeList string
const (
Message UpdateTypeList = "Message"
EditedMessage UpdateTypeList = "EditedMessage"
ChannelPost UpdateTypeList = "ChannelPost"
EditedChannelPost UpdateTypeList = "EditedChannelPost"
BusinessConnection UpdateTypeList = "BusinessConnection"
BusinessMessage UpdateTypeList = "BusinessMessage"
EditedBusinessMessage UpdateTypeList = "EditedBusinessMessage"
DeletedBusinessMessages UpdateTypeList = "DeletedBusinessMessages"
MessageReaction UpdateTypeList = "MessageReaction"
MessageReactionCount UpdateTypeList = "MessageReactionCount"
InlineQuery UpdateTypeList = "InlineQuery"
ChosenInlineResult UpdateTypeList = "ChosenInlineResult"
CallbackQuery UpdateTypeList = "CallbackQuery"
ShippingQuery UpdateTypeList = "ShippingQuery"
PreCheckoutQuery UpdateTypeList = "PreCheckoutQuery"
PurchasedPaidMedia UpdateTypeList = "PurchasedPaidMedia"
Poll UpdateTypeList = "Poll"
PollAnswer UpdateTypeList = "PollAnswer"
MyChatMember UpdateTypeList = "MyChatMember"
ChatMember UpdateTypeList = "ChatMember"
ChatJoinRequest UpdateTypeList = "ChatJoinRequest"
ChatBoost UpdateTypeList = "ChatBoost"
RemovedChatBoost UpdateTypeList = "RemovedChatBoost"
)
// 判断更新的类型
func GetUpdateType(update *models.Update) UpdateType {
var updateType UpdateType
if update.Message != nil {
updateType.Message = true
}
if update.EditedMessage != nil {
updateType.EditedMessage = true
}
if update.ChannelPost != nil {
updateType.ChannelPost = true
}
if update.EditedChannelPost != nil {
updateType.EditedChannelPost = true
}
if update.BusinessConnection != nil {
updateType.BusinessConnection = true
}
if update.BusinessMessage != nil {
updateType.BusinessMessage = true
}
if update.EditedBusinessMessage != nil {
updateType.EditedBusinessMessage = true
}
if update.MessageReaction != nil {
updateType.MessageReaction = true
}
if update.MessageReactionCount != nil {
updateType.MessageReactionCount = true
}
if update.InlineQuery != nil {
updateType.InlineQuery = true
}
if update.ChosenInlineResult != nil {
updateType.ChosenInlineResult = true
}
if update.CallbackQuery != nil {
updateType.CallbackQuery = true
}
if update.ShippingQuery != nil {
updateType.ShippingQuery = true
}
if update.PreCheckoutQuery != nil {
updateType.PreCheckoutQuery = true
}
if update.PurchasedPaidMedia != nil {
updateType.PurchasedPaidMedia = true
}
if update.Poll != nil {
updateType.Poll = true
}
if update.PollAnswer != nil {
updateType.PollAnswer = true
}
if update.MyChatMember != nil {
updateType.MyChatMember = true
}
if update.ChatMember != nil {
updateType.ChatMember = true
}
if update.ChatJoinRequest != nil {
updateType.ChatJoinRequest = true
}
if update.ChatBoost != nil {
updateType.ChatBoost = true
}
if update.RemovedChatBoost != nil {
updateType.RemovedChatBoost = true
}
return updateType
}

View File

@@ -1,102 +0,0 @@
package type_utils
import "github.com/go-telegram/bot/models"
// 更新类型
type UpdateType struct {
Message bool // *models.Message
EditedMessage bool // *models.Message
ChannelPost bool // *models.Message
EditedChannelPost bool // *models.Message
BusinessConnection bool // *models.BusinessConnection
BusinessMessage bool // *models.Message
EditedBusinessMessage bool // *models.Message
DeletedBusinessMessages bool // *models.BusinessMessagesDeleted
MessageReaction bool // *models.MessageReactionUpdated
MessageReactionCount bool // *models.MessageReactionCountUpdated
InlineQuery bool // *models.InlineQuery
ChosenInlineResult bool // *models.ChosenInlineResult
CallbackQuery bool // *models.CallbackQuery
ShippingQuery bool // *models.ShippingQuery
PreCheckoutQuery bool // *models.PreCheckoutQuery
PurchasedPaidMedia bool // *models.PaidMediaPurchased
Poll bool // *models.Poll
PollAnswer bool // *models.PollAnswer
MyChatMember bool // *models.ChatMemberUpdated
ChatMember bool // *models.ChatMemberUpdated
ChatJoinRequest bool // *models.ChatJoinRequest
ChatBoost bool // *models.ChatBoostUpdated
RemovedChatBoost bool // *models.ChatBoostRemoved
}
// 判断更新属性
func GetUpdateType(update *models.Update) UpdateType {
var updateType UpdateType
if update.Message != nil {
updateType.Message = true
}
if update.EditedMessage != nil {
updateType.EditedMessage = true
}
if update.ChannelPost != nil {
updateType.ChannelPost = true
}
if update.EditedChannelPost != nil {
updateType.EditedChannelPost = true
}
if update.BusinessConnection != nil {
updateType.BusinessConnection = true
}
if update.BusinessMessage != nil {
updateType.BusinessMessage = true
}
if update.EditedBusinessMessage != nil {
updateType.EditedBusinessMessage = true
}
if update.MessageReaction != nil {
updateType.MessageReaction = true
}
if update.MessageReactionCount != nil {
updateType.MessageReactionCount = true
}
if update.InlineQuery != nil {
updateType.InlineQuery = true
}
if update.ChosenInlineResult != nil {
updateType.ChosenInlineResult = true
}
if update.CallbackQuery != nil {
updateType.CallbackQuery = true
}
if update.ShippingQuery != nil {
updateType.ShippingQuery = true
}
if update.PreCheckoutQuery != nil {
updateType.PreCheckoutQuery = true
}
if update.PurchasedPaidMedia != nil {
updateType.PurchasedPaidMedia = true
}
if update.Poll != nil {
updateType.Poll = true
}
if update.PollAnswer != nil {
updateType.PollAnswer = true
}
if update.MyChatMember != nil {
updateType.MyChatMember = true
}
if update.ChatMember != nil {
updateType.ChatMember = true
}
if update.ChatJoinRequest != nil {
updateType.ChatJoinRequest = true
}
if update.ChatBoost != nil {
updateType.ChatBoost = true
}
if update.RemovedChatBoost != nil {
updateType.RemovedChatBoost = true
}
return updateType
}

View File

@@ -4,27 +4,31 @@ import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"trbot/database/db_struct"
"trbot/utils/configs"
"trbot/utils/consts"
"trbot/utils/mess"
"trbot/utils/plugin_utils"
"trbot/utils/type_utils"
"trbot/utils/type/message_utils"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"gopkg.in/yaml.v3"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// 如果 target 是 candidates 的一部分, 返回 true
// 常规类型会判定值是否相等,字符串如果包含也符合条件,例如 "bc" 在 "abcd" 中
// this is a bad function
func AnyContains(target any, candidates ...any) bool {
for _, candidate := range candidates {
switch c := candidate.(type) {
case reflect.Kind:
if len(c.String()) == 0 { continue }
case []int64:
if len(c) == 0 { continue }
}
if candidate == nil { continue }
// fmt.Println(reflect.ValueOf(target).Kind(), reflect.ValueOf(candidate).Kind(), reflect.Array, reflect.Slice)
targetKind := reflect.ValueOf(target).Kind()
@@ -174,10 +178,10 @@ func UserHavePermissionDeleteMessage(ctx context.Context, thebot *bot.Bot, chatI
func InlineResultPagination(queryFields []string, results []models.InlineQueryResult) []models.InlineQueryResult {
// 当 result 的数量超过 InlineResultsPerPage 时,进行分页
// fmt.Println(len(results), InlineResultsPerPage)
if len(results) > consts.InlineResultsPerPage {
if len(results) > configs.BotConfig.InlineResultsPerPage {
// 获取 update.InlineQuery.Query 末尾的 `<分页符号><数字>` 来选择输出第几页
var pageNow int = 1
var pageSize = (consts.InlineResultsPerPage - 1)
var pageSize = (configs.BotConfig.InlineResultsPerPage - 1)
pageNow, err := InlineExtractPageNumber(queryFields)
// 读取页码发生错误
@@ -198,7 +202,7 @@ func InlineResultPagination(queryFields []string, results []models.InlineQueryRe
return []models.InlineQueryResult{&models.InlineQueryResultArticle{
ID: "noThisOperation",
Title: "无效的操作",
Description: fmt.Sprintf("若您想翻页查看,请尝试输入 `%s2` 来查看第二页", consts.InlinePaginationSymbol),
Description: fmt.Sprintf("若您想翻页查看,请尝试输入 `%s2` 来查看第二页", configs.BotConfig.InlinePaginationSymbol),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "用户在尝试进行分页时输入了错误的页码并点击了分页提示...",
ParseMode: models.ParseModeMarkdownV1,
@@ -233,7 +237,7 @@ func InlineResultPagination(queryFields []string, results []models.InlineQueryRe
pageResults = append(pageResults, &models.InlineQueryResultArticle{
ID: "paginationPage",
Title: fmt.Sprintf("当前您在第 %d 页", pageNow),
Description: fmt.Sprintf("后面还有 %d 页内容,输入 %s%d 查看下一页", totalPages-pageNow, consts.InlinePaginationSymbol, pageNow+1),
Description: fmt.Sprintf("后面还有 %d 页内容,输入 %s%d 查看下一页", totalPages-pageNow, configs.BotConfig.InlinePaginationSymbol, pageNow+1),
InputMessageContent: &models.InputTextMessageContent{
MessageText: "用户在挑选内容时点击了分页提示...",
ParseMode: models.ParseModeMarkdownV1,
@@ -252,7 +256,7 @@ func InlineResultPagination(queryFields []string, results []models.InlineQueryRe
}
return pageResults
} else if len(queryFields) > 0 && strings.HasPrefix(queryFields[len(queryFields)-1], consts.InlinePaginationSymbol) {
} else if len(queryFields) > 0 && strings.HasPrefix(queryFields[len(queryFields)-1], configs.BotConfig.InlinePaginationSymbol) {
return []models.InlineQueryResult{&models.InlineQueryResultArticle{
ID: "noNeedPagination",
Title: "没有多余的内容",
@@ -274,8 +278,8 @@ func InlineExtractSubCommand(fields []string) string {
}
// 判断是不是子命令
if strings.HasPrefix(fields[0], consts.InlineSubCommandSymbol) {
return strings.TrimPrefix(fields[0], consts.InlineSubCommandSymbol)
if strings.HasPrefix(fields[0], configs.BotConfig.InlineSubCommandSymbol) {
return strings.TrimPrefix(fields[0], configs.BotConfig.InlineSubCommandSymbol)
}
return ""
}
@@ -287,11 +291,11 @@ func InlineExtractKeywords(fields []string) []string {
}
// 判断是不是子命令
if strings.HasPrefix(fields[0], consts.InlineSubCommandSymbol) {
if strings.HasPrefix(fields[0], configs.BotConfig.InlineSubCommandSymbol) {
fields = fields[1:]
}
// 判断有没有分页符号
if len(fields) > 0 && strings.HasPrefix(fields[len(fields)-1], consts.InlinePaginationSymbol) {
if len(fields) > 0 && strings.HasPrefix(fields[len(fields)-1], configs.BotConfig.InlinePaginationSymbol) {
fields = fields[:len(fields)-1]
}
@@ -305,7 +309,7 @@ func InlineExtractPageNumber(fields []string) (int, error) {
}
// 判断有没有分页符号
if strings.HasPrefix(fields[len(fields)-1], consts.InlinePaginationSymbol) {
if strings.HasPrefix(fields[len(fields)-1], configs.BotConfig.InlinePaginationSymbol) {
return strconv.Atoi(fields[len(fields)-1][1:])
}
return 1, nil
@@ -348,6 +352,7 @@ func InlineQueryMatchMultKeyword(fields []string, keywords []string) bool {
// 允许响应带有机器人用户名后缀的命令,例如 /help@examplebot
func CommandMaybeWithSuffixUsername(commandFields []string, command string) bool {
if len(commandFields) == 0 { return false }
atBotUsername := "@" + consts.BotMe.Username
if commandFields[0] == command || commandFields[0] == command + atBotUsername {
return true
@@ -355,6 +360,7 @@ func CommandMaybeWithSuffixUsername(commandFields []string, command string) bool
return false
}
// return user fullname
func ShowUserName(user *models.User) string {
if user.LastName != "" {
return user.FirstName + " " + user.LastName
@@ -363,6 +369,7 @@ func ShowUserName(user *models.User) string {
}
}
// return chat fullname
func ShowChatName(chat *models.Chat) string {
if chat.Title != "" { // 群组
return chat.Title
@@ -373,44 +380,6 @@ func ShowChatName(chat *models.Chat) string {
}
}
// 构建一个用于选择 Inline 模式下默认命令的按钮键盘
func BuildDefaultInlineCommandSelectKeyboard(chatInfo *db_struct.ChatInfo) models.ReplyMarkup {
var inlinePlugins [][]models.InlineKeyboardButton
for _, v := range plugin_utils.AllPlugins.InlineCommandList {
if v.Attr.IsCantBeDefault {
continue
}
if chatInfo.DefaultInlinePlugin == v.Command {
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{{
Text: fmt.Sprintf("✅ [ %s%s ] - %s", consts.InlineSubCommandSymbol, v.Command, v.Description),
CallbackData: "inline_default_" + v.Command,
}})
} else {
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{{
Text: fmt.Sprintf("[ %s%s ] - %s", consts.InlineSubCommandSymbol, v.Command, v.Description),
CallbackData: "inline_default_" + v.Command,
}})
}
}
inlinePlugins = append(inlinePlugins, []models.InlineKeyboardButton{
{
Text: "取消默认命令",
CallbackData: "inline_default_none",
},
{
Text: "浏览 inline 命令菜单",
SwitchInlineQueryCurrentChat: "+",
},
})
kb := &models.InlineKeyboardMarkup{
InlineKeyboard: inlinePlugins,
}
return kb
}
// 如果一个 int64 类型的 ID 为 `-100`` 开头的负数,则去掉 `-100``
func RemoveIDPrefix(id int64) string {
mayWithPrefix := fmt.Sprintf("%d", id)
@@ -432,7 +401,7 @@ func TextForTrueOrFalse(condition bool, tureText, falseText string) string {
// 获取消息来源的链接
func GetMessageFromHyperLink(msg *models.Message, ParseMode models.ParseMode) string {
var senderLink string
attr := type_utils.GetMessageAttribute(msg)
attr := message_utils.GetMessageAttribute(msg)
switch ParseMode {
case models.ParseModeHTML:
@@ -455,39 +424,6 @@ func GetMessageFromHyperLink(msg *models.Message, ParseMode models.ParseMode) st
return senderLink
}
// 一个通用的 yaml 结构体读取函数
func LoadYAML(pathToFile string, out interface{}) error {
file, err := os.ReadFile(pathToFile)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
if err := yaml.Unmarshal(file, out); err != nil {
return fmt.Errorf("解析 YAML 失败: %w", err)
}
return nil
}
// 一个通用的 yaml 结构体保存函数,目录和文件不存在则创建,并以结构体类型保存
func SaveYAML(pathToFile string, data interface{}) error {
out, err := yaml.Marshal(data)
if err != nil {
return fmt.Errorf("编码 YAML 失败: %w", err)
}
dir := filepath.Dir(pathToFile)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
if err := os.WriteFile(pathToFile, out, 0644); err != nil {
return fmt.Errorf("写入文件失败: %w", err)
}
return nil
}
// https://jasonkayzk.github.io/2021/09/26/在Golang发生Panic后打印出堆栈信息/
func getCurrentGoroutineStack() string {
var buf [4096]byte
@@ -495,9 +431,89 @@ func getCurrentGoroutineStack() string {
return string(buf[:n])
}
func PanicCatcher(pluginName string) {
func PanicCatcher(ctx context.Context, pluginName string) {
logger := zerolog.Ctx(ctx)
panic := recover()
if panic != nil {
mess.PrintLogAndSave(fmt.Sprintf("recovered panic in [%s]: \"%v\"\nStack: %s", pluginName, panic, getCurrentGoroutineStack()))
logger.Error().
Stack().
Str("commit", consts.Commit).
Err(errors.WithStack(fmt.Errorf("%v", panic))).
Str("catchFunc", pluginName).
Msg("Panic recovered")
// mess.PrintLogAndSave(fmt.Sprintf("recovered panic in [%s]: \"%v\"\nStack: %s", pluginName, panic, getCurrentGoroutineStack()))
}
}
// return a "user" string and a `zerolog.Dict()` with `name`(string), `username`(string), `ID`(int64) *zerolog.Event
func GetUserDict(user *models.User) (string, *zerolog.Event) {
if user == nil {
return "user", zerolog.Dict()
}
return "user", zerolog.Dict().
Str("name", ShowUserName(user)).
Str("username", user.Username).
Int64("ID", user.ID)
}
// return a "chat" string and a `zerolog.Dict()` with `name`(string), `username`(string), `ID`(int64), `type`(string) *zerolog.Event
func GetChatDict(chat *models.Chat) (string, *zerolog.Event) {
if chat == nil {
return "chat", zerolog.Dict()
}
return "chat", zerolog.Dict().
Str("name", ShowChatName(chat)).
Str("username", chat.Username).
Str("type", string(chat.Type)).
Int64("ID", chat.ID)
}
// Can replace GetUserDict(), not for GetChatDict(), and not available for some update type.
// return a sender type string and a `zerolog.Dict()` to show sender info
func GetUserOrSenderChatDict(userOrSenderChat *models.Message) (string, *zerolog.Event) {
if userOrSenderChat == nil {
return "noMessage", zerolog.Dict().Str("error", "no message to check")
}
if userOrSenderChat.From != nil {
return "user", zerolog.Dict().
Str("name", ShowUserName(userOrSenderChat.From)).
Str("username", userOrSenderChat.From.Username).
Int64("ID", userOrSenderChat.From.ID)
}
attr := message_utils.GetMessageAttribute(userOrSenderChat)
if userOrSenderChat.SenderChat != nil {
if attr.IsFromAnonymous {
return "groupAnonymous", zerolog.Dict().
Str("chat", ShowChatName(userOrSenderChat.SenderChat)).
Str("username", userOrSenderChat.SenderChat.Username).
Int64("ID", userOrSenderChat.SenderChat.ID)
} else if attr.IsUserAsChannel {
return "userAsChannel", zerolog.Dict().
Str("chat", ShowChatName(userOrSenderChat.SenderChat)).
Str("username", userOrSenderChat.SenderChat.Username).
Int64("ID", userOrSenderChat.SenderChat.ID)
} else if attr.IsFromLinkedChannel {
return "linkedChannel", zerolog.Dict().
Str("chat", ShowChatName(userOrSenderChat.SenderChat)).
Str("username", userOrSenderChat.SenderChat.Username).
Int64("ID", userOrSenderChat.SenderChat.ID)
} else if attr.IsFromBusinessBot {
return "businessBot", zerolog.Dict().
Str("name", ShowUserName(userOrSenderChat.SenderBusinessBot)).
Str("username", userOrSenderChat.SenderBusinessBot.Username).
Int64("ID", userOrSenderChat.SenderBusinessBot.ID)
} else if attr.IsHasSenderChat && userOrSenderChat.SenderChat.ID != userOrSenderChat.Chat.ID {
// use other channel send message in this channel
return "senderChat", zerolog.Dict().
Str("chat", ShowChatName(userOrSenderChat.SenderChat)).
Str("username", userOrSenderChat.SenderChat.Username).
Int64("ID", userOrSenderChat.SenderChat.ID)
}
}
return "noUserOrSender", zerolog.Dict().Str("warn", "no user or sender chat")
}

31
utils/yaml/yaml.go Normal file
View File

@@ -0,0 +1,31 @@
package yaml
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// 一个通用的 yaml 结构体读取函数
func LoadYAML(pathToFile string, out interface{}) error {
file, err := os.ReadFile(pathToFile)
if err == nil {
err = yaml.Unmarshal(file, out)
}
return err
}
// 一个通用的 yaml 结构体保存函数,目录和文件不存在则创建,并以结构体类型保存
func SaveYAML(pathToFile string, data interface{}) error {
out, err := yaml.Marshal(data)
if err == nil {
err = os.MkdirAll(filepath.Dir(pathToFile), 0755)
if err == nil {
err = os.WriteFile(pathToFile, out, 0644)
}
}
return err
}