refactor logger #2
8
.example.env
Normal file
8
.example.env
Normal 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
6
.gitignore
vendored
@@ -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
2
.vscode/launch.json
vendored
@@ -12,7 +12,7 @@
|
||||
"env": {},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"output": "${workspaceFolder}/output/debug_app"
|
||||
"output": "${workspaceFolder}/__debug_bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
15
Makefile
Normal file
15
Makefile
Normal 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)"
|
||||
@@ -49,7 +49,6 @@ const (
|
||||
LatestInlineResult ChatInfoField_LatestData = "LatestInlineResult"
|
||||
|
||||
LatestCallbackQueryData ChatInfoField_LatestData = "LatestCallbackQueryData"
|
||||
|
||||
)
|
||||
|
||||
type ChatInfoField_UsageCount string
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
152
database/utils.go
Normal 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, ¶ms.Update.Message.Chat)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Dict(utils.GetChatDict(¶ms.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(¶ms.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(¶ms.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(¶ms.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, ¶ms.Update.ChosenInlineResult.From)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Dict(utils.GetUserDict(¶ms.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(¶ms.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(¶ms.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(¶ms.Update.ChosenInlineResult.From)).
|
||||
Msg("Failed to get user info")
|
||||
}
|
||||
case updateType.CallbackQuery:
|
||||
err := InitUser(params.Ctx, ¶ms.Update.CallbackQuery.From)
|
||||
if err != nil {
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Dict(utils.GetUserDict(¶ms.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(¶ms.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(¶ms.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(¶ms.Update.ChosenInlineResult.From)).
|
||||
Msg("Failed get user info")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
BOT_TOKEN="114514:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
WEBHOOK_URL="https://api.example.com/telegram-webhook"
|
||||
DEBUG="true"
|
||||
4
go.mod
4
go.mod
@@ -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
17
go.sum
@@ -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=
|
||||
|
||||
1203
handlers.go
1203
handlers.go
File diff suppressed because it is too large
Load Diff
6
logstruct.txt
Normal file
6
logstruct.txt
Normal 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
118
main.go
@@ -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
@@ -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
@@ -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
@@ -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"`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
98
utils/configs/config.go
Normal 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
368
utils/configs/init.go
Normal 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
99
utils/configs/webhook.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
14
utils/errt/log_template.go
Normal file
14
utils/errt/log_template.go
Normal 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"
|
||||
)
|
||||
@@ -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(¶ms.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(¶ms.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(¶ms.Update.CallbackQuery.From)).
|
||||
Dict(utils.GetChatDict(¶ms.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(¶ms.Update.CallbackQuery.Message.Message.Chat)).
|
||||
Dict(utils.GetUserDict(¶ms.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(¶ms.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(¶ms.Update.CallbackQuery.Message.Message.Chat)).
|
||||
Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)).
|
||||
Msg("Edit messag to `bot help keyboard` failed")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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: "保存数据库",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
37
utils/multe/mult_errors.go
Normal file
37
utils/multe/mult_errors.go
Normal 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...)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,6 +19,9 @@ func BuildHandlerHelpKeyboard() models.ReplyMarkup {
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(button) == 0 {
|
||||
return nil
|
||||
}
|
||||
return models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: button,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
56
utils/plugin_utils/plugin_initializer.go
Normal file
56
utils/plugin_utils/plugin_initializer.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
148
utils/type/update_utils/update_type.go
Normal file
148
utils/type/update_utils/update_type.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
198
utils/utils.go
198
utils/utils.go
@@ -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
31
utils/yaml/yaml.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user