diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..75fc164 --- /dev/null +++ b/.example.env @@ -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" diff --git a/.gitignore b/.gitignore index 638e4ea..b058890 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 9370bde..94a13e4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "env": {}, "args": [], "cwd": "${workspaceFolder}", - "output": "${workspaceFolder}/output/debug_app" + "output": "${workspaceFolder}/__debug_bin" } ] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f3849f9 --- /dev/null +++ b/Makefile @@ -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)" diff --git a/database/db_struct/struct.go b/database/db_struct/struct.go index 50cb534..d02ef70 100644 --- a/database/db_struct/struct.go +++ b/database/db_struct/struct.go @@ -49,7 +49,6 @@ const ( LatestInlineResult ChatInfoField_LatestData = "LatestInlineResult" LatestCallbackQueryData ChatInfoField_LatestData = "LatestCallbackQueryData" - ) type ChatInfoField_UsageCount string diff --git a/database/initial.go b/database/initial.go index 8aafe1a..c8b35ad 100644 --- a/database/initial.go +++ b/database/initial.go @@ -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") } diff --git a/database/operates.go b/database/operates.go index 74f88bc..f51c640 100644 --- a/database/operates.go +++ b/database/operates.go @@ -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 +} diff --git a/database/redis_db/redis.go b/database/redis_db/redis.go index c0617d8..3d7e3db 100644 --- a/database/redis_db/redis.go +++ b/database/redis_db/redis.go @@ -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 } diff --git a/database/utils.go b/database/utils.go new file mode 100644 index 0000000..dfc39e7 --- /dev/null +++ b/database/utils.go @@ -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") + } + } +} diff --git a/database/yaml_db/yaml.go b/database/yaml_db/yaml.go index 89cd1d3..4d6641b 100644 --- a/database/yaml_db/yaml.go +++ b/database/yaml_db/yaml.go @@ -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) { diff --git a/download_database.sh b/download_database.sh deleted file mode 100644 index 1f73a7e..0000000 --- a/download_database.sh +++ /dev/null @@ -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 diff --git a/example.env b/example.env deleted file mode 100644 index c8fc326..0000000 --- a/example.env +++ /dev/null @@ -1,3 +0,0 @@ -BOT_TOKEN="114514:ABCDEFGHIJKLMNOPQRSTUVWXYZ" -WEBHOOK_URL="https://api.example.com/telegram-webhook" -DEBUG="true" diff --git a/go.mod b/go.mod index 78bd1cd..64ccd8b 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 88eb9b7..7b6cb20 100644 --- a/go.sum +++ b/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= diff --git a/handlers.go b/handlers.go index 6ab05f5..8038d82 100644 --- a/handlers.go +++ b/handlers.go @@ -3,444 +3,587 @@ package main import ( "context" "fmt" - "log" "strings" "time" "trbot/database" "trbot/database/db_struct" "trbot/utils" + "trbot/utils/configs" "trbot/utils/consts" + "trbot/utils/errt" "trbot/utils/handler_structs" - "trbot/utils/mess" "trbot/utils/plugin_utils" - "trbot/utils/type_utils" + "trbot/utils/type/message_utils" + "trbot/utils/type/update_utils" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" + "github.com/rs/zerolog" ) func defaultHandler(ctx context.Context, thebot *bot.Bot, update *models.Update) { - defer utils.PanicCatcher("defaultHandler") + defer utils.PanicCatcher(ctx, "defaultHandler") + logger := zerolog.Ctx(ctx) var opts = handler_structs.SubHandlerParams{ - Ctx: ctx, - Thebot: thebot, - Update: update, - } - var err error - - if update.Message != nil { - // fmt.Println(getMessageType(update.Message)) - if update.Message.Chat.Type == "private" { - // plugin_utils.AllPlugins.DefaultHandlerByMessageTypeForPrivate - } + Ctx: ctx, + Thebot: thebot, + Update: update, } - // 需要重写来配合 handler by update type - if update.Message != nil { - // 正常消息 - opts.Fields = strings.Fields(update.Message.Text) - database.InitChat(opts.Ctx, &update.Message.Chat) - database.IncrementalUsageCount(opts.Ctx, update.Message.Chat.ID, db_struct.MessageNormal) - database.RecordLatestData(opts.Ctx, update.Message.Chat.ID, db_struct.LatestMessage, update.Message.Text) - opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, update.Message.Chat.ID) - if err != nil { - log.Println(err) - } - - if consts.IsDebugMode { + // Debug or Trace Level + if zerolog.GlobalLevel() <= zerolog.InfoLevel { + if update.Message != nil { + // 正常消息 if update.Message.Photo != nil { - log.Printf("photo message from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], (%d) caption: [%s]", - utils.ShowUserName(update.Message.From), update.Message.From.Username, update.Message.From.ID, - utils.ShowChatName(&update.Message.Chat), update.Message.Chat.Username, update.Message.Chat.ID, - update.Message.ID, update.Message.Caption, - ) + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.Message)). + Dict(utils.GetChatDict(&update.Message.Chat)). + Int("messageID", update.Message.ID). + Str("caption", update.Message.Caption). + Str("photoID", update.Message.Photo[len(update.Message.Photo)-1].FileID). + Msg("photoMessage") } else if update.Message.Sticker != nil { - log.Printf("sticker message from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], (%d) sticker: %s[%s:%s]", - utils.ShowUserName(update.Message.From), update.Message.From.Username, update.Message.From.ID, - utils.ShowChatName(&update.Message.Chat), update.Message.Chat.Username, update.Message.Chat.ID, - update.Message.ID, update.Message.Sticker.Emoji, update.Message.Sticker.SetName, update.Message.Sticker.FileID, - ) + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.Message)). + Dict(utils.GetChatDict(&update.Message.Chat)). + Int("messageID", update.Message.ID). + Str("stickerEmoji", update.Message.Sticker.Emoji). + Str("stickerSetname", update.Message.Sticker.SetName). + Str("stickerFileID", update.Message.Sticker.FileID). + Msg("stickerMessage") } else { - log.Printf("message from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], (%d) message: [%s]", - utils.ShowUserName(update.Message.From), update.Message.From.Username, update.Message.From.ID, - utils.ShowChatName(&update.Message.Chat), update.Message.Chat.Username, update.Message.Chat.ID, - update.Message.ID, update.Message.Text, - ) + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.Message)). + Dict(utils.GetChatDict(&update.Message.Chat)). + Int("messageID", update.Message.ID). + Str("text", update.Message.Text). + Str("type", string(message_utils.GetMessageType(update.Message).InString())). + Msg("message") } - } - - messageHandler(&opts) - } else if update.EditedMessage != nil { - // 私聊或群组消息被编辑 - if consts.IsDebugMode { - if update.EditedMessage.Photo != nil { - log.Printf("edited from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], (%d) edited caption to [%s]", - utils.ShowUserName(update.EditedMessage.From), update.EditedMessage.From.Username, update.EditedMessage.From.ID, - utils.ShowChatName(&update.EditedMessage.Chat), update.EditedMessage.Chat.Username, update.EditedMessage.Chat.ID, - update.EditedMessage.ID, update.EditedMessage.Caption, - ) + } else if update.EditedMessage != nil { + // 私聊或群组消息被编辑 + if update.EditedMessage.Caption != "" { + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.EditedMessage)). + Dict(utils.GetChatDict(&update.EditedMessage.Chat)). + Int("messageID", update.EditedMessage.ID). + Str("editedCaption", update.EditedMessage.Caption). + Msg("editedMessage") } else { - log.Printf("edited from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], (%d) edited message to [%s]", - utils.ShowUserName(update.EditedMessage.From), update.EditedMessage.From.Username, update.EditedMessage.From.ID, - utils.ShowChatName(&update.EditedMessage.Chat), update.EditedMessage.Chat.Username, update.EditedMessage.Chat.ID, - update.EditedMessage.ID, update.EditedMessage.Text, - ) + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.EditedMessage)). + Dict(utils.GetChatDict(&update.EditedMessage.Chat)). + Int("messageID", update.EditedMessage.ID). + Str("editedText", update.EditedMessage.Text). + Msg("editedMessage") } - } - } else if update.InlineQuery != nil { - // inline 查询 - opts.Fields = strings.Fields(update.InlineQuery.Query) - database.InitUser(opts.Ctx, update.InlineQuery.From) - database.IncrementalUsageCount(opts.Ctx, update.InlineQuery.From.ID, db_struct.InlineRequest) - database.RecordLatestData(opts.Ctx, update.InlineQuery.From.ID, db_struct.LatestInlineQuery, update.InlineQuery.Query) - opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, update.InlineQuery.From.ID) - if err != nil { - log.Println(err) - } + } else if update.InlineQuery != nil { + // inline 查询 + logger.Info(). + Dict(utils.GetUserDict(update.InlineQuery.From)). + Str("query", update.InlineQuery.Query). + Msg("inline request") - log.Printf("inline from: \"%s\"(%s)[%d], query: [%s]", - utils.ShowUserName(update.InlineQuery.From), update.InlineQuery.From.Username, update.InlineQuery.From.ID, - update.InlineQuery.Query, - ) + } else if update.ChosenInlineResult != nil { + // inline 查询结果被选择 + logger.Info(). + Dict(utils.GetUserDict(&update.ChosenInlineResult.From)). + Str("query", update.ChosenInlineResult.Query). + Str("resultID", update.ChosenInlineResult.ResultID). + Msg("chosen inline result") - inlineHandler(&opts) - } else if update.ChosenInlineResult != nil { - // inline 查询结果被选择 - opts.Fields = strings.Fields(update.ChosenInlineResult.Query) - database.InitUser(opts.Ctx, &update.ChosenInlineResult.From) - database.IncrementalUsageCount(opts.Ctx, update.ChosenInlineResult.From.ID, db_struct.InlineResult) - database.RecordLatestData(opts.Ctx, update.ChosenInlineResult.From.ID, db_struct.LatestInlineResult, update.ChosenInlineResult.ResultID) - opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, update.ChosenInlineResult.From.ID) - if err != nil { - log.Println(err) - } + } else if update.CallbackQuery != nil { + // replymarkup 回调 + logger.Info(). + Dict(utils.GetUserDict(&update.CallbackQuery.From)). + Dict(utils.GetChatDict(&update.CallbackQuery.Message.Message.Chat)). + Str("query", update.CallbackQuery.Data). + Msg("callback query") - log.Printf("chosen inline from \"%s\"(%s)[%d], ID: [%s] query: [%s]", - utils.ShowUserName(&update.ChosenInlineResult.From), update.ChosenInlineResult.From.Username, update.ChosenInlineResult.From.ID, - update.ChosenInlineResult.ResultID, update.ChosenInlineResult.Query, - ) - } else if update.CallbackQuery != nil { - // replymarkup 回调 - - database.InitUser(opts.Ctx, &update.CallbackQuery.From) - database.IncrementalUsageCount(opts.Ctx, update.CallbackQuery.From.ID, db_struct.CallbackQuery) - database.RecordLatestData(opts.Ctx, update.CallbackQuery.From.ID, db_struct.LatestCallbackQueryData, update.CallbackQuery.Data) - opts.ChatInfo, err = database.GetChatInfo(opts.Ctx, update.CallbackQuery.From.ID) - if err != nil { - log.Println(err) - } - - log.Printf("callback from \"%s\"(%s)[%d] in \"%s\"(%s)[%d] query: [%s]", - utils.ShowUserName(&update.CallbackQuery.From), update.CallbackQuery.From.Username, update.CallbackQuery.From.ID, - utils.ShowChatName(&update.CallbackQuery.Message.Message.Chat), update.CallbackQuery.Message.Message.Chat.Username, update.CallbackQuery.Message.Message.Chat.ID, - update.CallbackQuery.Data, - ) - - // 如果有一个正在处理的请求,且用户再次发送相同的请求,则提示用户等待 - if opts.ChatInfo.HasPendingCallbackQuery && update.CallbackQuery.Data == opts.ChatInfo.LatestCallbackQueryData { - log.Println("same callback query, ignore") - thebot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{ - CallbackQueryID: update.CallbackQuery.ID, - Text: "当前的请求正在处理,请等待处理完成", - ShowAlert: true, - }) - return - } else if opts.ChatInfo.HasPendingCallbackQuery { - // 如果有一个正在处理的请求,用户发送了不同的请求,则提示用户等待 - log.Println("a callback query is pending, ignore") - thebot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{ - CallbackQueryID: update.CallbackQuery.ID, - Text: "请等待上一个请求处理完成再尝试发送新的请求", - ShowAlert: true, - }) - return - } else { - // 如果没有正在处理的请求,则接受新的请求 - log.Println("accept callback query") - opts.ChatInfo.HasPendingCallbackQuery = true - opts.ChatInfo.LatestCallbackQueryData = update.CallbackQuery.Data - // thebot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{ - // CallbackQueryID: update.CallbackQuery.ID, - // Text: "已接受请求", - // ShowAlert: false, - // }) - } - - for _, n := range plugin_utils.AllPlugins.CallbackQuery { - if strings.HasPrefix(update.CallbackQuery.Data, n.CommandChar) { - if n.Handler == nil { continue } - n.Handler(&opts) - break - } - } - - opts.ChatInfo.HasPendingCallbackQuery = false - return - } else if update.MessageReaction != nil { - // 私聊或群组表情回应 - if consts.IsDebugMode { + } else if update.MessageReaction != nil { + // 私聊或群组表情回应 if len(update.MessageReaction.OldReaction) > 0 { for i, oldReaction := range update.MessageReaction.OldReaction { if oldReaction.ReactionTypeEmoji != nil { - log.Printf("%d remove emoji reaction %s from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, oldReaction.ReactionTypeEmoji.Emoji, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("removedEmoji", oldReaction.ReactionTypeEmoji.Emoji). + Str("emojiType", string(oldReaction.ReactionTypeEmoji.Type)). + Int("count", i + 1). + Msg("removed emoji reaction") } else if oldReaction.ReactionTypeCustomEmoji != nil { - log.Printf("%d remove custom emoji reaction %s from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, oldReaction.ReactionTypeCustomEmoji.CustomEmojiID, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("removedEmojiID", oldReaction.ReactionTypeCustomEmoji.CustomEmojiID). + Str("emojiType", string(oldReaction.ReactionTypeCustomEmoji.Type)). + Int("count", i + 1). + Msg("removed custom emoji reaction") } else if oldReaction.ReactionTypePaid != nil { - log.Printf("%d remove paid reaction from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("emojiType", string(oldReaction.ReactionTypePaid.Type)). + Int("count", i + 1). + Msg("removed paid emoji reaction") } } } if len(update.MessageReaction.NewReaction) > 0 { for i, newReaction := range update.MessageReaction.NewReaction { if newReaction.ReactionTypeEmoji != nil { - log.Printf("%d emoji reaction %s from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, newReaction.ReactionTypeEmoji.Emoji, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("addEmoji", newReaction.ReactionTypeEmoji.Emoji). + Str("emojiType", string(newReaction.ReactionTypeEmoji.Type)). + Int("count", i + 1). + Msg("add emoji reaction") } else if newReaction.ReactionTypeCustomEmoji != nil { - log.Printf("%d custom emoji reaction %s from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, newReaction.ReactionTypeCustomEmoji.CustomEmojiID, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("addEmojiID", newReaction.ReactionTypeCustomEmoji.CustomEmojiID). + Str("emojiType", string(newReaction.ReactionTypeCustomEmoji.Type)). + Int("count", i + 1). + Msg("add custom emoji reaction") } else if newReaction.ReactionTypePaid != nil { - log.Printf("%d paid reaction from \"%s\"(%s)[%d] in \"%s\"(%s)[%d], to message [%d]", - i + 1, - utils.ShowUserName(update.MessageReaction.User), update.MessageReaction.User.Username, update.MessageReaction.User.ID, - utils.ShowChatName(&update.MessageReaction.Chat), update.MessageReaction.Chat.Username, update.MessageReaction.Chat.ID, - update.MessageReaction.MessageID, - ) + logger.Info(). + Dict(utils.GetUserDict(update.MessageReaction.User)). + Dict(utils.GetChatDict(&update.MessageReaction.Chat)). + Int("messageID", update.MessageReaction.MessageID). + Str("emojiType", string(newReaction.ReactionTypePaid.Type)). + Int("count", i + 1). + Msg("add paid emoji reaction") } } } - } - } else if update.MessageReactionCount != nil { - // 频道消息表情回应数量 - log.Printf("reaction count from in \"%s\"(%s)[%d], to message [%d], reactions: %v", - utils.ShowChatName(&update.MessageReactionCount.Chat), update.MessageReactionCount.Chat.Username, update.MessageReactionCount.Chat.ID, - update.MessageReactionCount.MessageID, update.MessageReactionCount.Reactions, - ) - } else if update.ChannelPost != nil { - // 频道信息 - if consts.IsDebugMode { - if update.ChannelPost.From != nil { // 在频道中使用户身份发送 - log.Printf("channel post from user \"%s\"(%s)[%d], in \"%s\"(%s)[%d], (%d) message [%s]", - utils.ShowUserName(update.ChannelPost.From), update.ChannelPost.From.Username, update.ChannelPost.From.ID, - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, - ) - } else if update.ChannelPost.SenderBusinessBot != nil { // 在频道中由商业 bot 发送 - log.Printf("channel post from businessbot \"%s\"(%s)[%d], in \"%s\"(%s)[%d], (%d) message [%s]", - utils.ShowUserName(update.ChannelPost.SenderBusinessBot), update.ChannelPost.SenderBusinessBot.Username, update.ChannelPost.SenderBusinessBot.ID, - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, - ) - } else if update.ChannelPost.ViaBot != nil { // 在频道中由 bot 发送 - log.Printf("channel post from bot \"%s\"(%s)[%d], in \"%s\"(%s)[%d], (%d) message [%s]", - utils.ShowUserName(update.ChannelPost.ViaBot), update.ChannelPost.ViaBot.Username, update.ChannelPost.ViaBot.ID, - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, - ) - } else if update.ChannelPost.SenderChat != nil { // 在频道中使用其他频道身份发送 - if update.ChannelPost.SenderChat.ID == update.ChannelPost.Chat.ID { // 在频道中由频道自己发送 - log.Printf("channel post in \"%s\"(%s)[%d], (%d) message [%s]", - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, + } else if update.MessageReactionCount != nil { + // 频道消息表情回应数量 + var emoji = zerolog.Dict() + var customEmoji = zerolog.Dict() + var paid = zerolog.Dict() + for _, n := range update.MessageReactionCount.Reactions { + switch n.Type.Type { + case models.ReactionTypeTypeEmoji: + emoji.Dict(n.Type.ReactionTypeEmoji.Emoji, zerolog.Dict(). + // Str("type", string(n.Type.ReactionTypeEmoji.Type)). + // Str("emoji", n.Type.ReactionTypeEmoji.Emoji). + Int("count", n.TotalCount), ) - } else { - log.Printf("channel post from another channel \"%s\"(%s)[%d], in \"%s\"(%s)[%d], (%d) message [%s]", - utils.ShowChatName(update.ChannelPost.SenderChat), update.ChannelPost.SenderChat.Username, update.ChannelPost.SenderChat.ID, - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, + case models.ReactionTypeTypeCustomEmoji: + customEmoji.Dict(n.Type.ReactionTypeCustomEmoji.CustomEmojiID, zerolog.Dict(). + // Str("type", string(n.Type.ReactionTypeCustomEmoji.Type)). + // Str("customEmojiID", n.Type.ReactionTypeCustomEmoji.CustomEmojiID). + Int("count", n.TotalCount), + ) + case models.ReactionTypeTypePaid: + paid.Dict(n.Type.ReactionTypePaid.Type, zerolog.Dict(). + // Str("type", n.Type.ReactionTypePaid.Type). + Int("count", n.TotalCount), ) } - } else { // 没有身份信息 - log.Printf("channel post from nobody in \"%s\"(%s)[%d], (%d) message [%s]", - // utils.ShowUserName(update.ChannelPost.From), update.ChannelPost.From.Username, update.ChannelPost.From.ID, - utils.ShowChatName(&update.ChannelPost.Chat), update.ChannelPost.Chat.Username, update.ChannelPost.Chat.ID, - update.ChannelPost.ID, update.ChannelPost.Text, - ) + } - return - } - } else if update.EditedChannelPost != nil { - // 频道中编辑过的消息 - if consts.IsDebugMode { - log.Printf("edited channel post in \"%s\"(%s)[%d], message [%s]", - utils.ShowChatName(&update.EditedChannelPost.Chat), update.EditedChannelPost.Chat.Username, update.EditedChannelPost.Chat.ID, - update.EditedChannelPost.Text, - ) - } - } else { - // 其他没有加入的更新类型 - if consts.IsDebugMode { - log.Printf("unknown update type: %v", update) - // thebot.CopyMessage(ctx, &bot.CopyMessageParams{ - // }) + + logger.Info(). + Dict(utils.GetChatDict(&update.MessageReactionCount.Chat)). + Dict("reactions", zerolog.Dict(). + Dict("emoji", emoji). + Dict("customEmoji", customEmoji). + Dict("paid", paid), + ). + Int("messageID", update.MessageReactionCount.MessageID). + Msg("emoji reaction count updated") + } else if update.ChannelPost != nil { + // 频道信息 + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.ChannelPost)). + Dict(utils.GetChatDict(&update.ChannelPost.Chat)). + Str("text", update.ChannelPost.Text). + Int("messageID", update.ChannelPost.ID). + Msg("channel post") + if update.ChannelPost.ViaBot != nil { + // 在频道中由 bot 发送 + _, viaBot := utils.GetUserDict(update.ChannelPost.ViaBot) + logger.Info(). + Dict("viaBot", viaBot). + Dict(utils.GetChatDict(&update.ChannelPost.Chat)). + Str("text", update.ChannelPost.Text). + Int("messageID", update.ChannelPost.ID). + Msg("channel post send via bot") + } + if update.ChannelPost.SenderChat == nil { + // 没有身份信息 + logger.Info(). + Dict(utils.GetChatDict(&update.ChannelPost.Chat)). + Str("text", update.ChannelPost.Text). + Int("messageID", update.ChannelPost.ID). + Msg("channel post from nobody") + } + } else if update.EditedChannelPost != nil { + // 频道中编辑过的消息 + if update.EditedChannelPost.Caption != "" { + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.EditedChannelPost)). + Dict(utils.GetChatDict(&update.EditedChannelPost.Chat)). + Int("messageID", update.EditedChannelPost.ID). + Str("editedCaption", update.EditedChannelPost.Caption). + Msg("edited channel post caption") + } else { + logger.Info(). + Dict(utils.GetUserOrSenderChatDict(update.EditedChannelPost)). + Dict(utils.GetChatDict(&update.EditedChannelPost.Chat)). + Int("messageID", update.EditedChannelPost.ID). + Str("editedText", update.EditedChannelPost.Text). + Msg("edited channel post") + } + } else { + // 其他没有加入的更新类型 + logger.Warn(). + Str("updateType", string(update_utils.GetUpdateType(update).InString())). + Msg("Receive a no tagged update type") } } + + // 记录数据和读取信息 + database.RecordData(&opts) + + updateType := update_utils.GetUpdateType(update) + switch { + case updateType.Message: + messageHandler(&opts) + case updateType.InlineQuery: + inlineHandler(&opts) + case updateType.CallbackQuery: + callbackQueryHandler(&opts) + opts.ChatInfo.HasPendingCallbackQuery = false + } + } // 处理所有信息请求的处理函数,触发条件为任何消息 func messageHandler(opts *handler_structs.SubHandlerParams) { - defer utils.PanicCatcher("messageHandler") + defer utils.PanicCatcher(opts.Ctx, "messageHandler") + logger := zerolog.Ctx(opts.Ctx) // 检测如果消息开头是 / 符号,作为命令来处理 if strings.HasPrefix(opts.Update.Message.Text, "/") { // 匹配默认的 `/xxx` 命令 for _, plugin := range plugin_utils.AllPlugins.SlashSymbolCommand { if utils.CommandMaybeWithSuffixUsername(opts.Fields, "/" + plugin.SlashCommand) { - if consts.IsDebugMode { - log.Printf("hit slashcommand: /%s", plugin.SlashCommand) + logger.Info(). + Str("slashCommand", plugin.SlashCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit slash command handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("slashCommand", plugin.SlashCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit slash symbol command handler, but this handler function is nil, skip") + continue + } + err := database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Warn(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("slashCommand", plugin.SlashCommand). + Str("message", opts.Update.Message.Text). + Msg("Failed to incremental message command count") + } + err = plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("slashCommand", plugin.SlashCommand). + Str("message", opts.Update.Message.Text). + Msg("Error in slash symbol command handler") } - if plugin.Handler == nil { continue } - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) - plugin.Handler(opts) return } } + // 不存在以 `/` 作为前缀的命令 if opts.Update.Message.Chat.Type == models.ChatTypePrivate { // 非冗余条件,在私聊状态下应处理用户发送的所有开头为 / 的命令 // 与群组中不同,群组中命令末尾不指定此 bot 回应的命令无须处理,以防与群组中的其他 bot 冲突 - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, Text: "不存在的命令", }) - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) - if consts.Private_log { mess.PrivateLogToChat(opts.Ctx, opts.Thebot, opts.Update) } + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("message", opts.Update.Message.Text). + Str("content", "no this command"). + Msg(errt.SendMessage) + } + err = database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Warn(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("message", opts.Update.Message.Text). + Msg("Failed to incremental message command count") + } + + // if configs.BotConfig.LogChatID != 0 { mess.PrivateLogToChat(opts.Ctx, opts.Thebot, opts.Update) } } else if strings.HasSuffix(opts.Fields[0], "@" + consts.BotMe.Username) { // 当使用一个不存在的命令,但是命令末尾指定为此 bot 处理 // 为防止与其他 bot 的命令冲突,默认不会处理不在命令列表中的命令 // 如果消息以 /xxx@examplebot 的形式指定此 bot 回应,且 /xxx 不在预设的命令中时,才发送该命令不可用的提示 - botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, Text: "不存在的命令", }) - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("message", opts.Update.Message.Text). + Str("content", "no this command"). + Msg(errt.SendMessage) + } + err = database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Warn(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("message", opts.Update.Message.Text). + Msg("Failed to incremental message command count") + } time.Sleep(time.Second * 10) - opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ + _, err = opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ ChatID: opts.Update.Message.Chat.ID, MessageIDs: []int{ opts.Update.Message.ID, botMessage.ID, }, }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("message", opts.Update.Message.Text). + Str("content", "no this command"). + Msg(errt.DeleteMessages) + } return } } else if len(opts.Update.Message.Text) > 0 { // 没有 `/` 号作为前缀,检查是不是自定义命令 for _, plugin := range plugin_utils.AllPlugins.CustomSymbolCommand { - if utils.CommandMaybeWithSuffixUsername(opts.Fields, plugin.FullCommand) { - if consts.IsDebugMode { - log.Printf("hit fullcommand: %s", plugin.FullCommand) + if strings.HasPrefix(opts.Update.Message.Text, plugin.FullCommand) { + logger.Info(). + Str("fullCommand", plugin.FullCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit full command handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("fullCommand", plugin.FullCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit full command handler, but this handler function is nil, skip") + continue + } + err := database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Warn(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("fullCommand", plugin.FullCommand). + Str("message", opts.Update.Message.Text). + Msg("Failed to incremental message command count") + } + err = plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("fullCommand", plugin.FullCommand). + Str("message", opts.Update.Message.Text). + Msg("Error in full command handler") } - if plugin.Handler == nil { continue } - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) - plugin.Handler(opts) return } } // 以后缀来触发的命令 for _, plugin := range plugin_utils.AllPlugins.SuffixCommand { if strings.HasSuffix(opts.Update.Message.Text, plugin.SuffixCommand) { - if consts.IsDebugMode { - log.Printf("hit suffixcommand: %s", plugin.SuffixCommand) + logger.Info(). + Str("suffixCommand", plugin.SuffixCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit suffix command handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("suffixCommand", plugin.SuffixCommand). + Str("message", opts.Update.Message.Text). + Msg("Hit suffix command handler, but this handler function is nil, skip") + continue + } + err := database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) + if err != nil { + logger.Warn(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("suffixCommand", plugin.SuffixCommand). + Str("message", opts.Update.Message.Text). + Msg("Failed to incremental message command count") + } + err = plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("suffixCommand", plugin.SuffixCommand). + Str("message", opts.Update.Message.Text). + Msg("Error in suffix command handler") } - if plugin.Handler == nil { continue } - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.MessageCommand) - plugin.Handler(opts) return } } } + // 按消息类型来触发的 handler + // handler by message type if plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type] != nil { - msgTypeInString := type_utils.GetMessageType(opts.Update.Message).InString() - var isProcessed bool + msgTypeInString := message_utils.GetMessageType(opts.Update.Message).InString() - // 如果此类型的 handler 数量仅有一个,且允许自动触发 - if len(plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString]) == 1 { - // 虽然是遍历,但实际上只能遍历一次 - for name, handler := range plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString] { - isProcessed = true - if handler.AllowAutoTrigger { - // 允许自动触发的 handler - if consts.IsDebugMode { - log.Printf("trigger handler by message type [%s] plugin [%s] for chat type [%s]", msgTypeInString, name, opts.Update.Message.Chat.Type) + var needBuildSelectKeyboard bool + + if plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString] != nil { + handlersInThisTypeCount := len(plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString]) + if handlersInThisTypeCount == 1 { + // 虽然是遍历,但实际上只能遍历一次 + for name, handler := range plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString] { + if handler.AllowAutoTrigger { + // 允许自动触发的 handler + logger.Info(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("messageType", string(msgTypeInString)). + Str("handlerName", name). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Msg("Hit handler by message type") + err := handler.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("messageType", string(msgTypeInString)). + Str("handlerName", name). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Msg("Error in handler by message type") + } + } else { + needBuildSelectKeyboard = true } - handler.Handler(opts) - } else { - // 此 handler 不允许自动触发,回复一条带按钮的消息让用户手动操作 - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: fmt.Sprintf("请选择一个 [ %s ] 类型消息的功能", msgTypeInString), - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ReplyMarkup: plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString].BuildSelectKeyboard(), - }) } } - } else { - // 多个 handler 自动回复一条带按钮的消息让用户手动操作 - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: fmt.Sprintf("请选择一个 [ %s ] 类型消息的功能", msgTypeInString), - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ReplyMarkup: plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString].BuildSelectKeyboard(), - }) - } - - // 仅在 private 对话中显示无默认处理插件的消息 - if !isProcessed && opts.Update.Message.Chat.Type == models.ChatTypePrivate { - // 非命令消息,提示无操作可用 - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + if needBuildSelectKeyboard { + // 多个 handler 自动回复一条带按钮的消息让用户手动操作 + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: fmt.Sprintf("请选择一个 [ %s ] 类型消息的功能", msgTypeInString), + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ReplyMarkup: plugin_utils.AllPlugins.HandlerByMessageType[opts.Update.Message.Chat.Type][msgTypeInString].BuildSelectKeyboard(), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("messageType", string(msgTypeInString)). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Int("handlerInThisTypeCount", handlersInThisTypeCount). + Str("content", "select a handler by message type keyboard"). + Msg(errt.SendMessage) + } + } + } else if opts.Update.Message.Chat.Type == models.ChatTypePrivate { + // 仅在 private 对话中显示无默认处理插件的消息 + // 如果没有设定任何对于 private 对话按消息来触发的 handler,则代码不会运行到这里 + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, Text: fmt.Sprintf("对于 [ %s ] 类型的消息没有默认处理插件", msgTypeInString), ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("messageType", string(msgTypeInString)). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Str("content", "no handler by message type plugin for this message type"). + Msg(errt.SendMessage) + } } } // 最后才运行针对群组 ID 的 handler - ByChatIDHandlers, isExist := plugin_utils.AllPlugins.HandlerByChatID[opts.Update.Message.Chat.ID] - if isExist { - for name, handler := range ByChatIDHandlers { - if consts.IsDebugMode { - log.Printf("trigger handler by chatID [%s] for group [%d]", name, handler.ChatID) + // handler by chat ID + if plugin_utils.AllPlugins.HandlerByChatID[opts.Update.Message.Chat.ID] != nil { + for name, handler := range plugin_utils.AllPlugins.HandlerByChatID[opts.Update.Message.Chat.ID] { + logger.Info(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("handlerName", name). + Int64("chatID", handler.ChatID). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Msg("Hit handler by chat ID") + err := handler.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("handlerName", name). + Int64("chatID", handler.ChatID). + Str("chatType", string(opts.Update.Message.Chat.Type)). + Msg("Error in handler by chat ID") } - handler.Handler(opts) } } } // 处理 inline 模式下的请求 func inlineHandler(opts *handler_structs.SubHandlerParams) { - defer utils.PanicCatcher("inlineHandler") + defer utils.PanicCatcher(opts.Ctx, "inlineHandler") + logger := zerolog.Ctx(opts.Ctx) - var IsAdmin bool = utils.AnyContains(opts.Update.InlineQuery.From.ID, consts.LogMan_IDs) + var IsAdmin bool = utils.AnyContains(opts.Update.InlineQuery.From.ID, configs.BotConfig.AdminIDs) - if opts.Update.InlineQuery.Query == consts.InlineSubCommandSymbol { + if opts.Update.InlineQuery.Query == configs.BotConfig.InlineSubCommandSymbol { // 仅输入了命令符号,展示命令列表 var inlineButton = &models.InlineQueryResultsButton{ Text: "点击此处修改默认命令", @@ -457,25 +600,22 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { }, }) for _, plugin := range plugin_utils.AllPlugins.InlineCommandList { - if !IsAdmin && plugin.Attr.IsHideInCommandList { - continue - } + if !IsAdmin && plugin.Attr.IsHideInCommandList { continue } var command = &models.InlineQueryResultArticle{ - ID: "inlinemenu" + plugin.Command, + ID: "inlineMenu_" + plugin.Command, Title: plugin.Command, Description: plugin.Description, InputMessageContent: &models.InputTextMessageContent{ MessageText: "请不要点击选单中的命令...", }, } - if plugin.Attr.IsHideInCommandList { - command.Description = "隐藏 | " + command.Description - } - if plugin.Attr.IsOnlyAllowAdmin { - command.Description = "管理员 | " + command.Description - } + + if plugin.Attr.IsHideInCommandList { command.Description = "隐藏 | " + command.Description } + if plugin.Attr.IsOnlyAllowAdmin { command.Description = "管理员 | " + command.Description } + results = append(results, command) } + _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, Results: results, @@ -483,19 +623,34 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { IsPersonal: true, }) if err != nil { - log.Printf("Error sending inline query response: %v", err) - return + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "bot inline handler list"). + Msg(errt.AnswerInlineQuery) } - } else if strings.HasPrefix(opts.Update.InlineQuery.Query, consts.InlineSubCommandSymbol) { + } else if strings.HasPrefix(opts.Update.InlineQuery.Query, configs.BotConfig.InlineSubCommandSymbol) { // 用户输入了分页符号和一些字符,判断接着的命令是否正确,正确则交给对应的插件处理,否则提示错误 // 插件处理完后返回全部列表,由设定好的函数进行分页输出 for _, plugin := range plugin_utils.AllPlugins.InlineHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } if opts.Fields[0][1:] == plugin.Command { - if plugin.Handler == nil { continue } + logger.Info(). + Str("handlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("handlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline handler, but this handler function is nil, skip") + continue + } ResultList := plugin.Handler(opts) _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, @@ -504,7 +659,14 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { CacheTime: 30, }) if err != nil { - log.Printf("Error when answering inline [%s] command: %v", plugin.Command, err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("handlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "sub inline handler"). + Msg(errt.AnswerInlineQuery) // 本来想写一个发生错误后再给用户回答一个错误信息,让用户可以点击发送,结果同一个 ID 的 inlineQuery 只能回答一次 } return @@ -512,27 +674,72 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { } // 完全由插件控制输出,若回答请求时列表数量超过 50 项会出错,无法回应用户请求 for _, plugin := range plugin_utils.AllPlugins.InlineManualHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } if opts.Fields[0][1:] == plugin.Command { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + logger.Info(). + Str("handlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("handlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Error in inline manual answer handler") + } return } } // 符合命令前缀,完全由插件自行控制输出 for _, plugin := range plugin_utils.AllPlugins.InlinePrefixHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } - if strings.HasPrefix(opts.Update.InlineQuery.Query, consts.InlineSubCommandSymbol + plugin.PrefixCommand) { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } + if strings.HasPrefix(opts.Update.InlineQuery.Query, configs.BotConfig.InlineSubCommandSymbol + plugin.PrefixCommand) { + logger.Info(). + Str("handlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline prefix manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("handlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit inline prefix manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("handlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Error in inline prefix manual answer handler") + } return } } + // 没有触发任何 handler + logger.Debug(). + Str("query", opts.Update.InlineQuery.Query). + Msg("No any handler is hit") + // 没有匹配到任何命令 _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, @@ -541,26 +748,43 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { Title: fmt.Sprintf("不存在的命令 [%s]", opts.Fields[0]), Description: "请检查命令是否正确", InputMessageContent: &models.InputTextMessageContent{ - MessageText: "您在使用 inline 模式时没有选择正确的命令...", + MessageText: "您在使用 inline 模式时没有输入正确的命令...", ParseMode: models.ParseModeMarkdownV1, }, }}, }) if err != nil { - log.Println("Error when answering inline no command", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "no this inline command"). + Msg(errt.AnswerInlineQuery) } return } else { + // inline query 不以命令符号开头,检查是否有默认 handler if opts.ChatInfo.DefaultInlinePlugin != "" { // 来自用户设定的默认命令 // 插件处理完后返回全部列表,由设定好的函数进行分页输出 for _, plugin := range plugin_utils.AllPlugins.InlineHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } if opts.ChatInfo.DefaultInlinePlugin == plugin.Command { - if plugin.Handler == nil { continue } + logger.Info(). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user default inline handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user default inline handler, but this handler function is nil, skip") + continue + } ResultList := plugin.Handler(opts) _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, @@ -569,7 +793,14 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { CacheTime: 30, }) if err != nil { - log.Printf("Error when answering inline [%s] command: %v", plugin.Command, err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "user default inline handler result"). + Msg(errt.AnswerInlineQuery) // 本来想写一个发生错误后再给用户回答一个错误信息,让用户可以点击发送,结果同一个 ID 的 inlineQuery 只能回答一次 } return @@ -577,24 +808,65 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { } // 完全由插件控制输出,若回答请求时列表数量超过 50 项会出错,无法回应用户请求 for _, plugin := range plugin_utils.AllPlugins.InlineManualHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } if opts.ChatInfo.DefaultInlinePlugin == plugin.Command { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + logger.Info(). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user default inline manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user default inline manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Error in user default inline manual answer handler") + } return } } // 符合命令前缀,完全由插件自行控制输出 for _, plugin := range plugin_utils.AllPlugins.InlinePrefixHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } if opts.ChatInfo.DefaultInlinePlugin == plugin.PrefixCommand { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + logger.Info(). + Str("userDefaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user default inline prefix manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit user inline prefix manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("userDefaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Error in user inline prefix manual answer handler") + } + return } } @@ -617,19 +889,36 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { }, }) if err != nil { - log.Println("Error when answering inline default command invailid:", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("userDefaultInlineCommand", opts.ChatInfo.DefaultInlinePlugin). + Str("content", "invalid user default inline handler"). + Msg(errt.AnswerInlineQuery) } return - } else if consts.InlineDefaultHandler != "" { + } else if configs.BotConfig.InlineDefaultHandler != "" { // 全局设定里设定的默认命令 // 插件处理完后返回全部列表,由设定好的函数进行分页输出 for _, plugin := range plugin_utils.AllPlugins.InlineHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } - if consts.InlineDefaultHandler == plugin.Command { - if plugin.Handler == nil { continue } + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } + if configs.BotConfig.InlineDefaultHandler == plugin.Command { + logger.Info(). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit bot default inline handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit bot default inline handler, but this handler function is nil, skip") + continue + } ResultList := plugin.Handler(opts) _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, @@ -637,12 +926,19 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { IsPersonal: true, CacheTime: 30, Button: &models.InlineQueryResultsButton{ - Text: "输入 + 号显示菜单,或点击此处修改默认命令", + Text: fmt.Sprintf("输入 %s 号显示菜单,或点击此处修改默认命令", configs.BotConfig.InlineSubCommandSymbol), StartParameter: "via-inline_change-inline-command", }, }) if err != nil { - log.Printf("Error when answering inline [%s] command: %v", plugin.Command, err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "returnResult"). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "bot default inline handler result"). + Msg(errt.AnswerInlineQuery) // 本来想写一个发生错误后再给用户回答一个错误信息,让用户可以点击发送,结果同一个 ID 的 inlineQuery 只能回答一次 } return @@ -650,23 +946,61 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { } // 完全由插件控制输出,若回答请求时列表数量超过 50 项会出错,无法回应用户请求 for _, plugin := range plugin_utils.AllPlugins.InlineManualHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } - if consts.InlineDefaultHandler == plugin.Command { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } + if configs.BotConfig.InlineDefaultHandler == plugin.Command { + logger.Info(). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit bot default inline manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit bot default inline manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerCommand", plugin.Command). + Str("handlerType", "manuallyAnswerResult"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Error in bot default inline manual answer handler") + } return } } // 符合命令前缀,完全由插件自行控制输出 for _, plugin := range plugin_utils.AllPlugins.InlinePrefixHandler { - if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { - continue - } - if opts.ChatInfo.DefaultInlinePlugin == plugin.PrefixCommand { - if plugin.Handler == nil { continue } - plugin.Handler(opts) + if plugin.Attr.IsOnlyAllowAdmin && !IsAdmin { continue } + if configs.BotConfig.InlineDefaultHandler == plugin.PrefixCommand { + logger.Info(). + Str("defaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Str("query", opts.Update.InlineQuery.Query). + Msg("Hit bot default inline prefix manual answer handler") + if plugin.Handler == nil { + logger.Warn(). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Msg("Hit bot default inline prefix manual answer handler, but this handler function is nil, skip") + continue + } + err := plugin.Handler(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("defaultHandlerPrefixCommand", plugin.PrefixCommand). + Str("handlerType", "manuallyAnswerResult_PrefixCommand"). + Msg("Error in bot default inline prefix manual answer handler") + } return } } @@ -676,7 +1010,7 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { if len(plugin_utils.AllPlugins.InlineCommandList) == 0 { pendingMessage = "此 bot 似乎并没有使用任何 inline 模式插件,请联系管理员" } else { - pendingMessage = fmt.Sprintf("您可以继续输入 %s 号来查看其他可用的命令", consts.InlineSubCommandSymbol) + pendingMessage = fmt.Sprintf("您可以继续输入 %s 号来查看其他可用的命令", configs.BotConfig.InlineSubCommandSymbol) } _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ InlineQueryID: opts.Update.InlineQuery.ID, @@ -695,7 +1029,12 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { }, }) if err != nil { - log.Printf("Error sending inline query response: %v", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "invalid bot default inline handler"). + Msg(errt.AnswerInlineQuery) return } return @@ -708,10 +1047,8 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { } var message string = "可用的 Inline 模式命令:\n\n" for _, command := range plugin_utils.AllPlugins.InlineCommandList { - if command.Attr.IsHideInCommandList { - continue - } - message += fmt.Sprintf("命令: %s%s\n", consts.InlineSubCommandSymbol, command.Command) + if command.Attr.IsHideInCommandList { continue } + message += fmt.Sprintf("命令: %s%s\n", configs.BotConfig.InlineSubCommandSymbol, command.Command) if command.Description != "" { message += fmt.Sprintf("描述: %s\n", command.Description) } @@ -722,7 +1059,7 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { InlineQueryID: opts.Update.InlineQuery.ID, Results: []models.InlineQueryResult{&models.InlineQueryResultArticle{ ID: "nodefaulthandler", - Title: fmt.Sprintf("请继续输入 %s 来查看可用的命令", consts.InlineSubCommandSymbol), + Title: fmt.Sprintf("请继续输入 %s 来查看可用的命令", configs.BotConfig.InlineSubCommandSymbol), Description: "由于管理员没有设定默认命令,您需要手动选择一个命令,点击此处查看命令列表", InputMessageContent: &models.InputTextMessageContent{ MessageText: message, @@ -732,8 +1069,100 @@ func inlineHandler(opts *handler_structs.SubHandlerParams) { Button: inlineButton, }) if err != nil { - log.Printf("Error sending inline query no default handler: %v", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "bot no default inline handler"). + Msg(errt.AnswerInlineQuery) return } } } + +func callbackQueryHandler(params *handler_structs.SubHandlerParams) { + defer utils.PanicCatcher(params.Ctx, "callbackQueryHandler") + logger := zerolog.Ctx(params.Ctx) + + // 如果有一个正在处理的请求,且用户再次发送相同的请求,则提示用户等待 + if params.ChatInfo.HasPendingCallbackQuery && params.Update.CallbackQuery.Data == params.ChatInfo.LatestCallbackQueryData { + logger.Info(). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("query", params.Update.CallbackQuery.Data). + Msg("this callback request is processing, ignore") + _, err := params.Thebot.AnswerCallbackQuery(params.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: params.Update.CallbackQuery.ID, + Text: "当前请求正在处理中,请等待处理完成", + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("content", "this callback request is processing"). + Msg(errt.AnswerCallbackQuery) + } + return + } else if params.ChatInfo.HasPendingCallbackQuery { + // 如果有一个正在处理的请求,用户发送了不同的请求,则提示用户等待 + logger.Info(). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("pendingQuery", params.ChatInfo.LatestCallbackQueryData). + Str("query", params.Update.CallbackQuery.Data). + Msg("another callback request is processing, ignore") + _, err := params.Thebot.AnswerCallbackQuery(params.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: params.Update.CallbackQuery.ID, + Text: "请等待上一个请求处理完成后再尝试发送新的请求", + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("content", "a callback request is processing, send new request later"). + Msg(errt.AnswerCallbackQuery) + } + return + } else { + // 如果没有正在处理的请求,则接受新的请求 + logger.Debug(). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("query", params.Update.CallbackQuery.Data). + Msg("accept callback query") + + params.ChatInfo.HasPendingCallbackQuery = true + params.ChatInfo.LatestCallbackQueryData = params.Update.CallbackQuery.Data + // params.Thebot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{ + // CallbackQueryID: params.Update.CallbackQuery.ID, + // Text: "已接受请求", + // ShowAlert: false, + // }) + } + + for _, n := range plugin_utils.AllPlugins.CallbackQuery { + if strings.HasPrefix(params.Update.CallbackQuery.Data, n.CommandChar) { + logger.Info(). + Str("handlerPrefix", n.CommandChar). + Str("callbackData", params.Update.CallbackQuery.Data). + Msg("Hit callback query handler") + if n.Handler == nil { + logger.Debug(). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("handlerPrefix", n.CommandChar). + Str("callbackData", params.Update.CallbackQuery.Data). + Msg("Hit callback query handler, but this handler function is nil, skip") + continue + } + err := n.Handler(params) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(¶ms.Update.CallbackQuery.From)). + Str("handlerPrefix", n.CommandChar). + Str("callbackData", params.Update.CallbackQuery.Data). + Msg("Error in callback query handler") + } + break + } + } +} diff --git a/logstruct.txt b/logstruct.txt new file mode 100644 index 0000000..ca4d18d --- /dev/null +++ b/logstruct.txt @@ -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 diff --git a/main.go b/main.go index f7e515a..7a63f6e 100644 --- a/main.go +++ b/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...") } - } diff --git a/plugins/plugin_detect_keyword.go b/plugins/plugin_detect_keyword.go index c595898..5f673c4 100644 --- a/plugins/plugin_detect_keyword.go +++ b/plugins/plugin_detect_keyword.go @@ -1,32 +1,39 @@ package plugins import ( + "context" "fmt" - "io" - "log" "os" + "path/filepath" "strconv" "strings" "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" - "gopkg.in/yaml.v3" + "github.com/rs/zerolog" ) var KeywordDataList KeywordData = KeywordData{ Chats: map[int64]KeywordChatList{}, Users: map[int64]KeywordUserList{}, } -var KeywordDataErr error -var KeywordData_path string = consts.DB_path + "detectkeyword/" +var KeywordDataErr error +var KeywordDataDir string = filepath.Join(consts.YAMLDataBaseDir, "detectkeyword/") +var KeywordDataPath string = filepath.Join(KeywordDataDir, consts.YAMLFileName) func init() { - ReadKeywordList() + plugin_utils.AddInitializer(plugin_utils.Initializer{ + Name: "Detect Keyword", + Func: ReadKeywordList, + }) plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ Name: "Detect Keyword", Loader: ReadKeywordList, @@ -38,25 +45,24 @@ func init() { }) plugin_utils.AddCallbackQueryCommandPlugins([]plugin_utils.CallbackQuery{ { - CommandChar: "detectkw_groupmng", + CommandChar: "detectkw_g", Handler: groupManageCallbackHandler, }, { - CommandChar: "detectkw_mng", + CommandChar: "detectkw_u", Handler: userManageCallbackHandler, }, }...) plugin_utils.AddSlashStartWithPrefixCommandPlugins(plugin_utils.SlashStartWithPrefixHandler{ - Prefix: "detectkw", + Prefix: "detectkw", Argument: "addgroup", - Handler: startPrefixAddGroup, + Handler: startPrefixAddGroup, }) plugin_utils.AddHandlerHelpInfo(plugin_utils.HandlerHelp{ Name: "群组关键词检测", Description: "此功能可以检测群组中的每一条信息,当包含设定的关键词时,将会向用户发送提醒\n\n使用方法:\n首先将机器人添加至想要监听关键词的群组中,发送 /setkeyword 命令,等待机器人回应后点击下方的 “设定关键词” 按钮即可为自己添加要监听的群组\n\n设定关键词:您可以在对应的群组中直接发送 /setkeyword 要设定的关键词 来为该群组设定关键词\n或前往机器人聊天页面,发送 /setkeyword 命令后点击对应的群组或全局关键词按钮,根据提示来添加关键词", ParseMode: models.ParseModeHTML, }) - buildListenList() } type KeywordData struct { @@ -123,17 +129,17 @@ func (user KeywordUserList)userStatus() string { return pendingMessage } -func (user KeywordUserList)selectChat() models.ReplyMarkup { +func (user KeywordUserList)selectChat(keyword string) models.ReplyMarkup { var buttons [][]models.InlineKeyboardButton buttons = append(buttons, []models.InlineKeyboardButton{{ - Text: "添加为全局关键词", - CallbackData: "detectkw_mng_globaladding", + Text: "🌐 添加为全局关键词", + CallbackData: fmt.Sprintf("detectkw_u_add_%d_%s", user.UserID, keyword), }}) for _, chat := range user.ChatsForUser { targetChat := KeywordDataList.Chats[chat.ChatID] buttons = append(buttons, []models.InlineKeyboardButton{{ - Text: targetChat.ChatName, - CallbackData: fmt.Sprintf("detectkw_mng_adding_%d", targetChat.ChatID), + Text: "👥 " + targetChat.ChatName, + CallbackData: fmt.Sprintf("detectkw_u_add_%d_%s", targetChat.ChatID, keyword), }}) } return &models.InlineKeyboardMarkup{ @@ -144,63 +150,289 @@ func (user KeywordUserList)selectChat() models.ReplyMarkup { type ChatForUser struct { ChatID int64 `yaml:"ChatID"` IsDisable bool `yaml:"IsDisable,omitempty"` - IsConfirmDelete bool `yaml:"IsConfirmDelete,omitempty"` + IsConfirmDelete bool `yaml:"IsConfirmDelete,omitempty"` // todo Keyword []string `yaml:"Keyword"` } -func ReadKeywordList() { - var lists KeywordData +func ReadKeywordList(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "ReadKeywordList"). + Logger() - file, err := os.Open(KeywordData_path + consts.MetadataFileName) + err := yaml.LoadYAML(KeywordDataPath, &KeywordDataList) if err != nil { - // 如果是找不到目录,新建一个 - log.Println("[DetectKeyword]: Not found database file. Created new one") - SaveKeywordList() - KeywordDataErr = err - return - } - defer file.Close() - - decoder := yaml.NewDecoder(file) - err = decoder.Decode(&lists) - if err != nil { - if err == io.EOF { - log.Println("[DetectKeyword]: keyword list looks empty. now format it") - SaveKeywordList() - KeywordDataErr = nil - return + if os.IsNotExist(err) { + logger.Warn(). + Err(err). + Str("path", KeywordDataPath). + Msg("Not found keyword list file. Created new one") + // 如果是找不到文件,新建一个 + err = yaml.SaveYAML(KeywordDataPath, &KeywordDataList) + if err != nil { + logger.Error(). + Err(err). + Str("path", KeywordDataPath). + Msg("Failed to create empty keyword list file") + KeywordDataErr = fmt.Errorf("failed to create empty keyword list file: %w", err) + } + } else { + logger.Error(). + Err(err). + Str("path", KeywordDataPath). + Msg("Failed to load keyword list file") + KeywordDataErr = fmt.Errorf("failed to load keyword list file: %w", err) } - log.Println("(func)ReadKeywordList:", err) - KeywordDataErr = err - return + } else { + KeywordDataErr = nil } - KeywordDataList, KeywordDataErr = lists, nil + + buildListenList() + return KeywordDataErr } -func SaveKeywordList() error { - data, err := yaml.Marshal(KeywordDataList) +func SaveKeywordList(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "SaveKeywordList"). + Logger() + + err := yaml.SaveYAML(KeywordDataPath, &KeywordDataList) if err != nil { - return err + logger.Error(). + Err(err). + Str("path", KeywordDataPath). + Msg("Failed to save udonese list") + KeywordDataErr = fmt.Errorf("failed to save udonese list: %w", err) + } else { + KeywordDataErr = nil } - if _, err := os.Stat(KeywordData_path); os.IsNotExist(err) { - if err := os.MkdirAll(KeywordData_path, 0755); err != nil { - return err - } - } - - if _, err := os.Stat(KeywordData_path + consts.MetadataFileName); os.IsNotExist(err) { - _, err := os.Create(KeywordData_path + consts.MetadataFileName) - if err != nil { - return err - } - } - - return os.WriteFile(KeywordData_path + consts.MetadataFileName, data, 0644) + return KeywordDataErr } -func addKeywordHandler(opts *handler_structs.SubHandlerParams) { - if opts.Update.Message.Chat.Type != models.ChatTypePrivate { +func addKeywordHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "addKeywordHandler"). + Logger() + + var handlerErr multe.MultiError + + if opts.Update.Message.Chat.Type == models.ChatTypePrivate { + // 与机器人的私聊对话 + user := KeywordDataList.Users[opts.Update.Message.From.ID] + if user.AddTime == "" { + // 初始化用户 + user = KeywordUserList{ + UserID: opts.Update.Message.From.ID, + AddTime: time.Now().Format(time.RFC3339), + Limit: 50, + IsDisable: false, + IsSilentNotice: false, + } + KeywordDataList.Users[opts.Update.Message.From.ID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to init user and save keyword list") + return handlerErr.Addf("failed to init user and save keyword list: %w", err).Flat() + } + } + + // 用户没有添加任何群组 + if len(user.ChatsForUser) == 0 { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "您还没有添加任何群组,请在群组中使用 `/setkeyword` 命令来记录群组\n若发送信息后没有回应,请检查机器人是否在对应群组中", + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "no group for user notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `no group for user notice` message: %w", err) + } + } else { + if len(opts.Fields) == 1 { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: user.userStatus(), + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + ReplyMarkup: buildUserChatList(user), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "user group list keyboard"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `user group list keyboard` message: %w", err) + } + } else { + // 限制关键词长度 + if len(opts.Fields[1]) > 30 { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "抱歉,单个关键词长度不能超过 30 个英文字符", + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Int("length", len(opts.Fields[1])). + Str("content", "keyword is too long"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword is too long` message: %w", err) + } + } else { + if user.AddingChatID != 0 { + keyword := strings.ToLower(opts.Fields[1]) + var pendingMessage string + var button models.ReplyMarkup + var isKeywordExist bool + + // 判断是全局关键词还是群组关键词 + if user.AddingChatID == user.UserID { + // 全局关键词 + for _, k := range user.GlobalKeyword { + if k == keyword { + isKeywordExist = true + break + } + } + if !isKeywordExist { + logger.Debug(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("globalKeyword", keyword). + Msg("User add a global keyword") + user.GlobalKeyword = append(user.GlobalKeyword, keyword) + KeywordDataList.Users[user.UserID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("globalKeyword", keyword). + Msg("Failed to add global keyword and save keyword list") + return handlerErr.Addf("failed to add global keyword and save keyword list: %w", err).Flat() + } + pendingMessage = fmt.Sprintf("已添加全局关键词: [ %s ]", opts.Fields[1]) + } else { + pendingMessage = fmt.Sprintf("此全局关键词 [ %s ] 已存在", opts.Fields[1]) + } + + } else { + var chatForUser ChatForUser + var chatForUserIndex int + for i, c := range user.ChatsForUser { + if c.ChatID == user.AddingChatID { + chatForUser = c + chatForUserIndex = i + } + } + targetChat := KeywordDataList.Chats[chatForUser.ChatID] + + // 群组关键词 + for _, k := range chatForUser.Keyword { + if k == keyword { + isKeywordExist = true + break + } + } + if !isKeywordExist { + logger.Debug(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Int64("chatID", chatForUser.ChatID). + Str("keyword", keyword). + Msg("User add a keyword to chat") + chatForUser.Keyword = append(chatForUser.Keyword, keyword) + user.ChatsForUser[chatForUserIndex] = chatForUser + KeywordDataList.Users[user.UserID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Int64("chatID", chatForUser.ChatID). + Str("keyword", keyword). + Msg("Failed to add keyword and save keyword list") + return handlerErr.Addf("failed to add keyword and save keyword list: %w", err).Flat() + } + + pendingMessage = fmt.Sprintf("已为 %s 群组添加关键词 [ %s ],您可以继续向此群组添加更多关键词\n", utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName, strings.ToLower(opts.Fields[1])) + } else { + pendingMessage = fmt.Sprintf("此关键词 [ %s ] 已存在于 %s 群组中,您可以继续向此群组添加其他关键词", opts.Fields[1], utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName) + } + } + if isKeywordExist { + button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "✅ 完成", + CallbackData: "detectkw_u_finish", + }}}} + } else { + button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ + { + Text: "↩️ 撤销操作", + CallbackData: fmt.Sprintf("detectkw_u_undo_%d_%s", user.AddingChatID, opts.Fields[1]), + }, + { + Text: "✅ 完成", + CallbackData: "detectkw_u_finish", + }, + }}} + } + + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: pendingMessage, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + ReplyMarkup: button, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "keyword added notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword added notice` message: %w", err) + } + } else { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "您还没有选定要将关键词添加到哪个群组,请在下方挑选一个您已经添加的群组", + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + ReplyMarkup: user.selectChat(opts.Fields[1]), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "keyword adding select keyboard"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword adding select keyboard` message: %w", err) + } + } + } + + } + } + } else { // 在群组中直接使用 /setkeyword 命令 chat := KeywordDataList.Chats[opts.Update.Message.Chat.ID] if chat.IsDisable { @@ -212,27 +444,39 @@ func addKeywordHandler(opts *handler_structs.SubHandlerParams) { ParseMode: models.ParseModeHTML, ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ Text: "管理此功能", - CallbackData: "detectkw_groupmng", + CallbackData: "detectkw_g", }}}}, }) if err != nil { - log.Printf("Error response /setkeyword command disabled : %v", err) + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "function is disabled by admins"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `function is disabled by admins` message: %w", err) } - return } else { if chat.AddTime == "" { // 初始化群组 chat = KeywordChatList{ - ChatID: opts.Update.Message.Chat.ID, - ChatName: opts.Update.Message.Chat.Title, + ChatID: opts.Update.Message.Chat.ID, + ChatName: opts.Update.Message.Chat.Title, ChatUsername: opts.Update.Message.Chat.Username, - ChatType: opts.Update.Message.Chat.Type, - AddTime: time.Now().Format(time.RFC3339), - InitByID: opts.Update.Message.From.ID, - IsDisable: false, + ChatType: opts.Update.Message.Chat.Type, + AddTime: time.Now().Format(time.RFC3339), + InitByID: opts.Update.Message.From.ID, + IsDisable: false, } KeywordDataList.Chats[opts.Update.Message.Chat.ID] = chat - SaveKeywordList() + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Msg("Failed to init chat and save keyword list") + return handlerErr.Addf("failed to init chat and save keyword list: %w", err).Flat() + } } if len(opts.Fields) == 1 { // 只有一个 /setkeyword 命令 @@ -248,191 +492,96 @@ func addKeywordHandler(opts *handler_structs.SubHandlerParams) { }, { Text: "管理此功能", - CallbackData: "detectkw_groupmng", + CallbackData: "detectkw_g", }, }}}, }) if err != nil { - log.Printf("Error response /setkeyword command: %v", err) + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "group record link button"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `group record link button` message: %w", err) } } else { - user := KeywordDataList.Users[opts.Update.Message.From.ID] - - if user.AddTime == "" { - // 初始化用户 - user = KeywordUserList{ - UserID: opts.Update.Message.From.ID, - AddTime: time.Now().Format(time.RFC3339), - Limit: 50, - IsNotInit: true, - } - KeywordDataList.Users[opts.Update.Message.From.ID] = user - SaveKeywordList() - } - - var isChatAdded bool = false - var chatForUser ChatForUser - var chatForUserIndex int - - for index, keyword := range user.ChatsForUser { - if keyword.ChatID == chat.ChatID { - chatForUser = keyword - chatForUserIndex = index - isChatAdded = true - break - } - } - if !isChatAdded { - log.Println("init group", chat.ChatID, "to user", opts.Update.Message.From.ID) - chatForUser = ChatForUser{ - ChatID: chat.ChatID, - } - user.ChatsForUser = append(user.ChatsForUser, chatForUser) - KeywordDataList.Users[user.UserID] = user - SaveKeywordList() - } - // 限制关键词长度 if len(opts.Fields[1]) > 30 { _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, - Text: "抱歉,单个关键词长度不能超过 30 个字符", + Text: "抱歉,单个关键词长度不能超过 30 个英文字符", ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, ParseMode: models.ParseModeHTML, }) if err != nil { - log.Printf("Error response /setkeyword command keyword too long: %v", err) - } - return - } - - keyword := strings.ToLower(opts.Fields[1]) - var pendingMessage string - var isKeywordExist bool - - for _, k := range chatForUser.Keyword { - if k == keyword { - isKeywordExist = true - break - } - } - if !isKeywordExist { - log.Println("add keyword", keyword, "to user", opts.Update.Message.From.ID, "chat", chatForUser.ChatID) - chatForUser.Keyword = append(chatForUser.Keyword, keyword) - user.ChatsForUser[chatForUserIndex] = chatForUser - KeywordDataList.Users[user.UserID] = user - SaveKeywordList() - if user.IsNotInit { - pendingMessage = fmt.Sprintf("已将 [ %s ] 添加到您的关键词列表\n
若要在检测到关键词时收到提醒,请点击下方的按钮来初始化您的账号
", strings.ToLower(opts.Fields[1])) - } else { - pendingMessage = fmt.Sprintf("已将 [ %s ] 添加到您的关键词列表,您可以继续向此群组添加更多关键词", strings.ToLower(opts.Fields[1])) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Int("keywordLength", len(opts.Fields[1])). + Str("content", "keyword is too long"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword is too long` message: %w", err) } } else { - if user.IsNotInit { - pendingMessage = "您已经添加过这个关键词了。请点击下方的按钮来初始化您的账号以使用此功能" - } else { - pendingMessage = "您已经添加过这个关键词了,您可以继续向此群组添加其他关键词" + user := KeywordDataList.Users[opts.Update.Message.From.ID] + + if user.AddTime == "" { + // 初始化用户 + user = KeywordUserList{ + UserID: opts.Update.Message.From.ID, + AddTime: time.Now().Format(time.RFC3339), + Limit: 50, + IsNotInit: true, + } + KeywordDataList.Users[opts.Update.Message.From.ID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to add a not init user and save keyword list") + return handlerErr.Addf("failed to add a not init user and save keyword list: %w", err).Flat() + } } - } - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: pendingMessage, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "管理关键词", - URL: fmt.Sprintf("https://t.me/%s?start=detectkw_addgroup_%d", consts.BotMe.Username, chat.ChatID), - }}}}, - }) - if err != nil { - log.Printf("Error response /setkeyword command: %v", err) - } - } - - } - } else { - // 与机器人的私聊对话 - user := KeywordDataList.Users[opts.Update.Message.From.ID] - if user.AddTime == "" { - // 初始化用户 - user = KeywordUserList{ - UserID: opts.Update.Message.From.ID, - AddTime: time.Now().Format(time.RFC3339), - Limit: 50, - IsDisable: false, - IsSilentNotice: false, - } - KeywordDataList.Users[opts.Update.Message.From.ID] = user - SaveKeywordList() - } - if len(user.ChatsForUser) == 0 { - // 没有添加群组 - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "您还没有添加任何群组,请在群组中使用 `/setkeyword` 命令来记录群组\n若发送信息后没有回应,请检查机器人是否在对应群组中", - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - }) - if err != nil { - log.Printf("Error response /setkeyword command: %v", err) - } - return - } + var isChatAdded bool = false + var chatForUser ChatForUser + var chatForUserIndex int - if len(opts.Fields) > 1 { - if user.AddingChatID != 0 { - var chatForUser ChatForUser - var chatForUserIndex int - for i, c := range user.ChatsForUser { - if c.ChatID == user.AddingChatID { - chatForUser = c - chatForUserIndex = i - } - } - - // 限制关键词长度 - if len(opts.Fields[1]) > 30 { - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "抱歉,单个关键词长度不能超过 30 个字符", - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - }) - if err != nil { - log.Printf("Error response /setkeyword command keyword too long: %v", err) - } - return - } - - keyword := strings.ToLower(opts.Fields[1]) - var pendingMessage string - var button models.ReplyMarkup - var isKeywordExist bool - - // 判断是全局关键词还是群组关键词 - if user.AddingChatID == user.UserID { - // 全局关键词 - for _, k := range user.GlobalKeyword { - if k == keyword { - isKeywordExist = true + for index, keyword := range user.ChatsForUser { + if keyword.ChatID == chat.ChatID { + chatForUser = keyword + chatForUserIndex = index + isChatAdded = true break } } - if !isKeywordExist { - log.Println("add global keyword", keyword, "to user", opts.Update.Message.From.ID) - user.GlobalKeyword = append(user.GlobalKeyword, keyword) + if !isChatAdded { + logger.Debug(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Msg("User add a chat to listen list by set keyword in group") + chatForUser = ChatForUser{ + ChatID: chat.ChatID, + } + user.ChatsForUser = append(user.ChatsForUser, chatForUser) KeywordDataList.Users[user.UserID] = user - SaveKeywordList() - pendingMessage = fmt.Sprintf("已添加全局关键词: [ %s ]", opts.Fields[1]) - } else { - pendingMessage = fmt.Sprintf("此全局关键词 [ %s ] 已存在", opts.Fields[1]) + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to add chat to user listen list and save keyword list") + return handlerErr.Addf("failed to add chat to user listen list and save keyword list: %w", err).Flat() + } } - } else { - targetChat := KeywordDataList.Chats[chatForUser.ChatID] + keyword := strings.ToLower(opts.Fields[1]) + var pendingMessage string + var isKeywordExist bool - // 群组关键词 for _, k := range chatForUser.Keyword { if k == keyword { isKeywordExist = true @@ -440,69 +589,61 @@ func addKeywordHandler(opts *handler_structs.SubHandlerParams) { } } if !isKeywordExist { - log.Println("add keyword", keyword, "to user", opts.Update.Message.From.ID, "chat", chatForUser.ChatID) + logger.Debug(). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Int64("chatID", chatForUser.ChatID). + Str("keyword", keyword). + Msg("User add a keyword to chat") chatForUser.Keyword = append(chatForUser.Keyword, keyword) user.ChatsForUser[chatForUserIndex] = chatForUser KeywordDataList.Users[user.UserID] = user - SaveKeywordList() - pendingMessage = fmt.Sprintf("已为 %s 群组添加关键词 [ %s ],您可以继续向此群组添加更多关键词\n", utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName, strings.ToLower(opts.Fields[1])) + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("keyword", keyword). + Msg("Failed to add keyword and save keyword list") + return handlerErr.Addf("failed to add keyword and save keyword list: %w", err).Flat() + } + if user.IsNotInit { + pendingMessage = fmt.Sprintf("已将 [ %s ] 添加到您的关键词列表\n
若要在检测到关键词时收到提醒,请点击下方的按钮来初始化您的账号
", strings.ToLower(opts.Fields[1])) + } else { + pendingMessage = fmt.Sprintf("已将 [ %s ] 添加到您的关键词列表,您可以继续向此群组添加更多关键词", strings.ToLower(opts.Fields[1])) + } } else { - pendingMessage = fmt.Sprintf("此关键词 [ %s ] 已存在于 %s 群组中,您可以继续向此群组添加其他关键词", opts.Fields[1], utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName) + if user.IsNotInit { + pendingMessage = "您已经添加过这个关键词了。请点击下方的按钮来初始化您的账号以使用此功能" + } else { + pendingMessage = "您已经添加过这个关键词了,您可以继续向此群组添加其他关键词" + } + } + + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: pendingMessage, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "管理关键词", + URL: fmt.Sprintf("https://t.me/%s?start=detectkw_addgroup_%d", consts.BotMe.Username, chat.ChatID), + }}}}, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "keyword added notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword added notice` message: %w", err) } } - if isKeywordExist { - button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "完成", - CallbackData: "detectkw_mng_finish", - }}}} - } else { - button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ - { - Text: "撤销操作", - CallbackData: fmt.Sprintf("detectkw_mng_undo_%d_%s", user.AddingChatID, opts.Fields[1]), - }, - { - Text: "完成", - CallbackData: "detectkw_mng_finish", - }, - }}} - } - - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: pendingMessage, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - ReplyMarkup: button, - }) - if err != nil { - log.Printf("Error response /setkeyword command success: %v", err) - } - } else { - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "您还没有选定要将关键词添加到哪个群组,请在下方挑选一个您已经添加的群组", - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - ReplyMarkup: user.selectChat(), - }) - if err != nil { - log.Printf("Error response /setkeyword command: %v", err) - } - } - } else { - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: user.userStatus(), - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - ReplyMarkup: buildUserChatList(user), - }) - if err != nil { - log.Printf("Error response /setkeyword command: %v", err) } } } + return handlerErr.Flat() } func buildListenList() { @@ -533,7 +674,8 @@ func buildListenList() { } } -func KeywordDetector(opts *handler_structs.SubHandlerParams) { +func KeywordDetector(opts *handler_structs.SubHandlerParams) error { + var handlerErr multe.MultiError var text string if opts.Update.Message.Caption != "" { text = strings.ToLower(opts.Update.Message.Caption) @@ -541,19 +683,16 @@ func KeywordDetector(opts *handler_structs.SubHandlerParams) { text = strings.ToLower(opts.Update.Message.Text) } - if text == "" { - return - } + // 没有文字直接跳到 + if text == "" { return nil } // 先循环一遍,找出该群组中启用此功能的用户 ID for _, userID := range KeywordDataList.Chats[opts.Update.Message.Chat.ID].UsersID { // 获取用户信息,开始匹配关键词 user := KeywordDataList.Users[userID] if !user.IsDisable && !user.IsNotInit { - if !user.IsIncludeSelf && opts.Update.Message.From.ID == userID { - // 如果用户设定排除了自己发送的消息,则跳过 - continue - } + // 如果用户设定排除了自己发送的消息,则跳过 + if !user.IsIncludeSelf && opts.Update.Message.From.ID == userID { continue } // 用户为单独群组设定的关键词 for _, userKeywordList := range user.ChatsForUser { @@ -561,7 +700,7 @@ func KeywordDetector(opts *handler_structs.SubHandlerParams) { if userKeywordList.ChatID == opts.Update.Message.Chat.ID { for _, keyword := range userKeywordList.Keyword { if strings.Contains(text, keyword) { - notifyUser(opts, user, opts.Update.Message.Chat.Title, keyword, text, false) + handlerErr.Add(notifyUser(opts, user, opts.Update.Message.Chat.Title, keyword, text, false)) break } } @@ -570,20 +709,29 @@ func KeywordDetector(opts *handler_structs.SubHandlerParams) { // 用户全局设定的关键词 for _, userGlobalKeyword := range user.GlobalKeyword { if strings.Contains(text, userGlobalKeyword) { - notifyUser(opts, user, opts.Update.Message.Chat.Title, userGlobalKeyword, text, true) + handlerErr.Add(notifyUser(opts, user, opts.Update.Message.Chat.Title, userGlobalKeyword, text, true)) break } } } } + return handlerErr.Flat() } -func notifyUser(opts *handler_structs.SubHandlerParams, user KeywordUserList, chatname, keyword, text string, isGlobalKeyword bool) { +func notifyUser(opts *handler_structs.SubHandlerParams, user KeywordUserList, chatname, keyword, text string, isGlobalKeyword bool) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "notifyUser"). + Logger() + + var handlerErr multe.MultiError + var messageLink string = fmt.Sprintf("https://t.me/c/%s/%d", utils.RemoveIDPrefix(opts.Update.Message.Chat.ID), opts.Update.Message.ID) _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: user.UserID, - Text: fmt.Sprintf("在 %s 群组中\n来自 %s 的消息\n触发了设定的%s关键词 [ %s ]\n
%s
", + Text: fmt.Sprintf("在 %s 群组中\n来自 %s 的消息\n触发了%s关键词 [ %s ]\n
%s
", utils.RemoveIDPrefix(opts.Update.Message.Chat.ID), chatname, utils.GetMessageFromHyperLink(opts.Update.Message, models.ParseModeHTML), utils.TextForTrueOrFalse(isGlobalKeyword, "全局", "群组"), keyword, text, ), @@ -595,157 +743,426 @@ func notifyUser(opts *handler_structs.SubHandlerParams, user KeywordUserList, ch DisableNotification: user.IsSilentNotice, }) if err != nil { - log.Printf("Error response /setkeyword command: %v", err) + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Int64("userID", user.UserID). + Str("keyword", keyword). + Str("content", "keyword detected notice to user"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `keyword detected notice to user` message to %d: %w", user.UserID, err) } user.MentionCount++ KeywordDataList.Users[user.UserID] = user + + return handlerErr.Flat() } -func groupManageCallbackHandler(opts *handler_structs.SubHandlerParams) { +func groupManageCallbackHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "groupManageCallbackHandler"). + Logger() + + var handlerErr multe.MultiError + if !utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.CallbackQuery.Message.Message.Chat.ID, opts.Update.CallbackQuery.From.ID) { - opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ CallbackQueryID: opts.Update.CallbackQuery.ID, Text: "您没有权限修改此配置", ShowAlert: true, }) - return + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "no permission to change group functions"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `no permission to change group functions` callback answer: %w", err) + } + } else { + chat := KeywordDataList.Chats[opts.Update.CallbackQuery.Message.Message.Chat.ID] + + if opts.Update.CallbackQuery.Data == "detectkw_g_switch" { + // 群组里的全局开关,是否允许群组内用户使用这个功能,优先级最高 + chat.IsDisable = !chat.IsDisable + KeywordDataList.Chats[opts.Update.CallbackQuery.Message.Message.Chat.ID] = chat + buildListenList() + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Failed to change group switch and save keyword list") + handlerErr.Addf("failed to change group switch and save keyword list: %w", err) + } + } + + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: fmt.Sprintf("消息关键词检测\n此功能允许用户设定一些关键词,当机器人检测到群组内的消息包含用户设定的关键词时,向用户发送提醒\n\n当前群组中有 %d 个用户启用了此功能\n\n%s", len(chat.UsersID), utils.TextForTrueOrFalse(chat.IsDisable, "已为当前群组关闭关键词检测功能,已设定了关键词的用户将无法再收到此群组的提醒", "")), + ReplyMarkup: buildGroupManageKB(chat), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "group function manager keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `group function manager keyboard`: %w", err) + } } - chat := KeywordDataList.Chats[opts.Update.CallbackQuery.Message.Message.Chat.ID] - - if opts.Update.CallbackQuery.Data == "detectkw_groupmng_switch" { - // 群组里的全局开关,是否允许群组内用户使用这个功能,优先级最高 - chat.IsDisable = !chat.IsDisable - KeywordDataList.Chats[opts.Update.CallbackQuery.Message.Message.Chat.ID] = chat - buildListenList() - SaveKeywordList() - } - - _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: fmt.Sprintf("消息关键词检测\n此功能允许用户设定一些关键词,当机器人检测到群组内的消息包含用户设定的关键词时,向用户发送提醒\n\n当前群组中有 %d 个用户启用了此功能\n\n%s", len(chat.UsersID), utils.TextForTrueOrFalse(chat.IsDisable, "已为当前群组关闭关键词检测功能,已设定了关键词的用户将无法再收到此群组的提醒", "")), - ReplyMarkup: buildGroupManageKB(chat), - }) - if err != nil { - fmt.Println(err) - } + return handlerErr.Flat() } -func userManageCallbackHandler(opts *handler_structs.SubHandlerParams) { +func userManageCallbackHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "userManageCallbackHandler"). + Logger() + + var handlerErr multe.MultiError user := KeywordDataList.Users[opts.Update.CallbackQuery.From.ID] switch opts.Update.CallbackQuery.Data { - case "detectkw_mng_globalswitch": + case "detectkw_u_globalswitch": // 功能全局开关 user.IsDisable = !user.IsDisable - case "detectkw_mng_noticeswitch": + case "detectkw_u_noticeswitch": // 是否静默通知 user.IsSilentNotice = !user.IsSilentNotice - case "detectkw_mng_selfswitch": + case "detectkw_u_selfswitch": // 是否检测自己发送的消息 user.IsIncludeSelf = !user.IsIncludeSelf - case "detectkw_mng_finish": + case "detectkw_u_finish": // 停止添加群组关键词 user.AddingChatID = 0 - case "detectkw_mng_chatdisablebyadmin": + case "detectkw_u_chatdisablebyadmin": // 目标群组的管理员为群组关闭了此功能 - opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ CallbackQueryID: opts.Update.CallbackQuery.ID, - Text: "此群组的的管理员禁用了此功能,因此,您无法再收到来自该群组的关键词提醒,您可以询问该群组的管理员是否可以重新开启这个功能", + Text: "此群组中的管理员禁用了此功能,因此,您无法再收到来自该群组的关键词提醒,您可以询问该群组的管理员是否可以重新开启这个功能", ShowAlert: true, }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "this group is disable by admins"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `this group is disable by admins` callback answer: %w", err) + } + return handlerErr.Flat() default: - if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_undo_") || strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_delkw_") { - // 撤销添加或删除关键词 + if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_undo_") || strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_delkw_") { + // 撤销添加或删除关键词 var chatIDAndKeyword string - if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_undo_") { - chatIDAndKeyword = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_undo_") + if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_undo_") { + chatIDAndKeyword = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_undo_") } else { - chatIDAndKeyword = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_delkw_") + chatIDAndKeyword = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_delkw_") } chatIDAndKeywordList := strings.Split(chatIDAndKeyword, "_") chatID, err := strconv.ParseInt(chatIDAndKeywordList[0], 10, 64) if err != nil { - fmt.Println(err) - } - - if chatID == user.UserID { - var tempKeyword []string - for _, keyword := range user.GlobalKeyword { - if keyword != chatIDAndKeywordList[1] { - tempKeyword = append(tempKeyword, keyword) - } - } - user.GlobalKeyword = tempKeyword - KeywordDataList.Users[user.UserID] = user + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user undo add or delete a keyword") + handlerErr.Addf("failed to parse chat ID when user undo add or delete a keyword: %w", err) } else { - for index, chatForUser := range KeywordDataList.Users[user.UserID].ChatsForUser { - if chatForUser.ChatID == chatID { - var tempKeyword []string - for _, keyword := range chatForUser.Keyword { - if keyword != chatIDAndKeywordList[1] { - tempKeyword = append(tempKeyword, keyword) - } + // 删除关键词过程 + if chatID == user.UserID { + // 全局关键词 + var tempKeyword []string + for _, keyword := range user.GlobalKeyword { + if keyword != chatIDAndKeywordList[1] { + tempKeyword = append(tempKeyword, keyword) + } + } + user.GlobalKeyword = tempKeyword + KeywordDataList.Users[user.UserID] = user + } else { + // 群组关键词 + for index, chatForUser := range KeywordDataList.Users[user.UserID].ChatsForUser { + if chatForUser.ChatID == chatID { + var tempKeyword []string + for _, keyword := range chatForUser.Keyword { + if keyword != chatIDAndKeywordList[1] { + tempKeyword = append(tempKeyword, keyword) + } + } + chatForUser.Keyword = tempKeyword + } + KeywordDataList.Users[user.UserID].ChatsForUser[index] = chatForUser + } + } + err = SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Int64("chatID", chatID). + Str("keyword", chatIDAndKeywordList[1]). + Msg("Failed to undo add or remove keyword and save keyword list") + handlerErr.Addf("failed to undo add or remove keyword and save keyword list: %w", err) + } else { + if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_undo_") { + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: "已撤销操作,您可以继续使用 /setkeyword 关键词 来添加其他关键词", + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "add keyword has been canceled notice"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `add keyword has been canceled notice`: %w", err) + } + } else { + var buttons [][]models.InlineKeyboardButton + var tempbutton []models.InlineKeyboardButton + var keywordCount int + var pendingMessage string + + if chatID == user.UserID { + for index, keyword := range user.GlobalKeyword { + if index % 2 == 0 && index != 0 { + buttons = append(buttons, tempbutton) + tempbutton = []models.InlineKeyboardButton{} + } + tempbutton = append(tempbutton, models.InlineKeyboardButton{ + Text: keyword, + CallbackData: fmt.Sprintf("detectkw_u_kw_%d_%s", user.UserID, keyword), + }) + keywordCount++ + // buttons = append(buttons, tempbutton) + } + if len(tempbutton) != 0 { + buttons = append(buttons, tempbutton) + } + pendingMessage = fmt.Sprintf("已删除 [ %s ] 关键词\n\n您当前设定了 %d 个全局关键词", chatIDAndKeywordList[1], keywordCount) + + } else { + for _, chat := range user.ChatsForUser { + if chat.ChatID == chatID { + for index, keyword := range chat.Keyword { + if index % 2 == 0 && index != 0 { + buttons = append(buttons, tempbutton) + tempbutton = []models.InlineKeyboardButton{} + } + tempbutton = append(tempbutton, models.InlineKeyboardButton{ + Text: keyword, + CallbackData: fmt.Sprintf("detectkw_u_kw_%d_%s", chat.ChatID, keyword), + }) + keywordCount++ + // buttons = append(buttons, tempbutton) + } + if len(tempbutton) != 0 { + buttons = append(buttons, tempbutton) + } + } + } + pendingMessage = fmt.Sprintf("已删除 [ %s ] 关键词\n\n您当前为 %s 群组设定了 %d 个关键词", chatIDAndKeywordList[1], utils.RemoveIDPrefix(chatID), KeywordDataList.Chats[chatID].ChatName, keywordCount) + } + + buttons = append(buttons, []models.InlineKeyboardButton{ + { + Text: "⬅️ 返回主菜单", + CallbackData: "detectkw_u", + }, + { + Text: "➕ 添加关键词", + CallbackData: fmt.Sprintf("detectkw_u_adding_%d", chatID), + }, + }) + + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: pendingMessage, + ParseMode: models.ParseModeHTML, + ReplyMarkup: &models.InlineKeyboardMarkup{ + InlineKeyboard: buttons, + }, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "keyword list keyboard with deleted keyword notice"). + Msg(errt.EditMessageText) } - chatForUser.Keyword = tempKeyword } - KeywordDataList.Users[user.UserID].ChatsForUser[index] = chatForUser } } - SaveKeywordList() - if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_undo_") { - _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: "已撤销操作,您可以继续使用 /setkeyword 关键词 来添加其他关键词", - ParseMode: models.ParseModeHTML, - }) + return handlerErr.Flat() + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_adding_") { + // 设定要往哪个群组里添加关键词 + chatID := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_adding_") + chatID_int64, err := strconv.ParseInt(chatID, 10, 64) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user selecting chat to add keyword") + handlerErr.Addf("failed to parse chat ID when user selecting chat to add keyword: %w", err) + } else { + user := KeywordDataList.Users[user.UserID] + user.AddingChatID = chatID_int64 + KeywordDataList.Users[user.UserID] = user + buildListenList() + err = SaveKeywordList(opts.Ctx) if err != nil { - fmt.Println(err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Int64("chatID", chatID_int64). + Msg("Failed to set a chat ID for user add keyword and save keyword list") + handlerErr.Addf("failed to set a chat ID for user add keyword and save keyword list: %w", err) + } else { + var pendingMessage string + if chatID_int64 == user.UserID { + pendingMessage = "已将全局关键词设为添加关键词的目标,请继续使用 /setkeyword 关键词 来添加全局关键词" + } else { + pendingMessage = fmt.Sprintf("已将 %s 群组设为添加关键词的目标群组,请继续使用 /setkeyword 关键词 来为该群组添加关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName) + } + + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: pendingMessage, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "already to add keyword notice"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `already to add keyword notice`: %w", err) + } } + } + + return handlerErr.Flat() + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_switch_chat_") { + // 启用或禁用某个群组的关键词检测开关 + id := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_switch_chat_") + id_int64, err := strconv.ParseInt(id, 10, 64) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user change the group switch") + return handlerErr.Addf("failed to parse chat ID when user change the group switch: %w", err).Flat() + } else { + for index, chat := range KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser { + if chat.ChatID == id_int64 { + chat.IsDisable = !chat.IsDisable + } + KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser[index] = chat + } + } + // edit by the end + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_chat_") { + // 显示某个群组的关键词列表 + chatID := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_chat_") + chatID_int64, err := strconv.ParseInt(chatID, 10, 64) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user wanna manage keyword for group") + handlerErr.Addf("failed to parse chat ID when user wanna manage keyword for group: %w", err) } else { var buttons [][]models.InlineKeyboardButton var tempbutton []models.InlineKeyboardButton - for _, chat := range user.ChatsForUser { - if chat.ChatID == chatID { - for index, keyword := range chat.Keyword { - if index % 2 == 0 && index != 0 { - buttons = append(buttons, tempbutton) - tempbutton = []models.InlineKeyboardButton{} - } - tempbutton = append(tempbutton, models.InlineKeyboardButton{ - Text: keyword, - CallbackData: fmt.Sprintf("detectkw_mng_kw_%d_%s", chat.ChatID, keyword), - }) - // buttons = append(buttons, tempbutton) - } - if len(tempbutton) != 0 { + var pendingMessage string + var keywordCount int + + if chatID_int64 == user.UserID { + // 全局关键词 + for index, keyword := range user.GlobalKeyword { + if index % 2 == 0 && index != 0 { buttons = append(buttons, tempbutton) + tempbutton = []models.InlineKeyboardButton{} + } + tempbutton = append(tempbutton, models.InlineKeyboardButton{ + Text: keyword, + CallbackData: fmt.Sprintf("detectkw_u_kw_%d_%s", user.UserID, keyword), + }) + keywordCount++ + } + if len(tempbutton) != 0 { + buttons = append(buttons, tempbutton) + } + if len(buttons) == 0 { + pendingMessage = "您没有设定任何全局关键词\n点击下方按钮来添加全局关键词" + } else { + pendingMessage = fmt.Sprintf("您当前设定了 %d 个全局关键词\n
全局关键词将对您添加的全部群组生效\n但在部分情况下,全局关键词不会生效:\n- 您手动将群组设定为禁用状态\n- 对应群组的管理员为该群组关闭了此功能
", keywordCount) + } + } else { + // 为群组设定的关键词 + for _, chat := range KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser { + if chat.ChatID == chatID_int64 { + for index, keyword := range chat.Keyword { + if index % 2 == 0 && index != 0 { + buttons = append(buttons, tempbutton) + tempbutton = []models.InlineKeyboardButton{} + } + tempbutton = append(tempbutton, models.InlineKeyboardButton{ + Text: keyword, + CallbackData: fmt.Sprintf("detectkw_u_kw_%d_%s", chat.ChatID, keyword), + }) + keywordCount++ + // buttons = append(buttons, tempbutton) + } + if len(tempbutton) != 0 { + buttons = append(buttons, tempbutton) + } + break } } - } - - var pendingMessage string - if chatID == user.UserID { - pendingMessage = fmt.Sprintf("已删除 [ %s ] 关键词\n\n您当前设定了 %d 个全局关键词", chatIDAndKeywordList[1], len(buttons)) - } else { - pendingMessage = fmt.Sprintf("已删除 [ %s ] 关键词\n\n您当前为 %s 群组设定了 %d 个关键词", chatIDAndKeywordList[1], utils.RemoveIDPrefix(chatID), KeywordDataList.Chats[chatID].ChatName, len(buttons)) + if len(buttons) == 0 { + pendingMessage = fmt.Sprintf("当前群组 %s 没有关键词\n点击下方按钮来为此群组添加关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName) + } else { + pendingMessage = fmt.Sprintf("您当前为 %s 群组设定了 %d 个关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName, keywordCount) + } } buttons = append(buttons, []models.InlineKeyboardButton{ { - Text: "返回主菜单", - CallbackData: "detectkw_mng", + Text: "⬅️ 返回主菜单", + CallbackData: "detectkw_u", }, { - Text: "添加关键词", - CallbackData: fmt.Sprintf("detectkw_mng_adding_%d", chatID), + Text: "➕ 添加关键词", + CallbackData: fmt.Sprintf("detectkw_u_adding_%d", chatID_int64), }, }) - _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, MessageID: opts.Update.CallbackQuery.Message.Message.ID, Text: pendingMessage, @@ -755,171 +1172,188 @@ func userManageCallbackHandler(opts *handler_structs.SubHandlerParams) { }, }) if err != nil { - fmt.Println(err) - } - } - - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_adding_") { - // 设定要往哪个群组里添加关键词 - chatID := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_adding_") - chatID_int64, err := strconv.ParseInt(chatID, 10, 64) - if err != nil { - fmt.Println(err) - } - user := KeywordDataList.Users[user.UserID] - user.AddingChatID = chatID_int64 - KeywordDataList.Users[user.UserID] = user - buildListenList() - SaveKeywordList() - - var pendingMessage string - if chatID_int64 == user.UserID { - pendingMessage = "已将全局关键词设为添加关键词的目标,请继续使用 /setkeyword 关键词 来添加全局关键词" - } else { - pendingMessage = fmt.Sprintf("已将 %s 群组设为添加关键词的目标群组,请继续使用 /setkeyword 关键词 来为该群组添加关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName) - } - - _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, - }) - if err != nil { - fmt.Println(err) - } - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_switch_chat_") { - // 启用或禁用某个群组的关键词检测开关 - id := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_switch_chat_") - id_int64, err := strconv.ParseInt(id, 10, 64) - if err != nil { - fmt.Println(err) - } - for index, chat := range KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser { - if chat.ChatID == id_int64 { - chat.IsDisable = !chat.IsDisable - } - KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser[index] = chat - } - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_chat_") { - // 显示某个群组的关键词列表 - chatID := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_chat_") - chatID_int64, err := strconv.ParseInt(chatID, 10, 64) - if err != nil { - fmt.Println(err) - } - var buttons [][]models.InlineKeyboardButton - var tempbutton []models.InlineKeyboardButton - var pendingMessage string - - if chatID_int64 == user.UserID { - // 全局关键词 - for index, keyword := range user.GlobalKeyword { - if index % 2 == 0 && index != 0 { - buttons = append(buttons, tempbutton) - tempbutton = []models.InlineKeyboardButton{} - } - tempbutton = append(tempbutton, models.InlineKeyboardButton{ - Text: keyword, - CallbackData: fmt.Sprintf("detectkw_mng_kw_%d_%s", user.UserID, keyword), - }) - } - if len(tempbutton) != 0 { - buttons = append(buttons, tempbutton) - } - if len(buttons) == 0 { - pendingMessage = "您没有设定任何全局关键词\n点击下方按钮来添加全局关键词" - } else { - pendingMessage = fmt.Sprintf("您当前设定了 %d 个全局关键词\n
全局关键词将对您添加的全部群组生效\n但在部分情况下,全局关键词不会生效:\n- 您手动将群组设定为禁用状态\n- 对应群组的管理员为该群组关闭了此功能
", len(buttons)) - } - } else { - // 为群组设定的关键词 - for _, chat := range KeywordDataList.Users[opts.Update.CallbackQuery.From.ID].ChatsForUser { - if chat.ChatID == chatID_int64 { - for index, keyword := range chat.Keyword { - if index % 2 == 0 && index != 0 { - buttons = append(buttons, tempbutton) - tempbutton = []models.InlineKeyboardButton{} - } - tempbutton = append(tempbutton, models.InlineKeyboardButton{ - Text: keyword, - CallbackData: fmt.Sprintf("detectkw_mng_kw_%d_%s", chat.ChatID, keyword), - }) - // buttons = append(buttons, tempbutton) - } - if len(tempbutton) != 0 { - buttons = append(buttons, tempbutton) - } - break - } - } - if len(buttons) == 0 { - pendingMessage = fmt.Sprintf("当前群组 %s 没有关键词\n点击下方按钮来为此群组添加关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName) - } else { - pendingMessage = fmt.Sprintf("您当前为 %s 群组设定了 %d 个关键词", utils.RemoveIDPrefix(chatID_int64), KeywordDataList.Chats[chatID_int64].ChatName, len(buttons)) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "group keyword list keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `group keyword list keyboard`: %w", err) } } - buttons = append(buttons, []models.InlineKeyboardButton{ - { - Text: "返回主菜单", - CallbackData: "detectkw_mng", - }, - { - Text: "添加关键词", - CallbackData: fmt.Sprintf("detectkw_mng_adding_%d", chatID_int64), - }, - }) - - _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, - ReplyMarkup: &models.InlineKeyboardMarkup{ - InlineKeyboard: buttons, - }, - }) - if err != nil { - fmt.Println(err) - } - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_kw_") { - // 删除某个关键词 - chatIDAndKeyword := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_mng_kw_") + return handlerErr.Flat() + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_kw_") { + // 管理一个关键词 + chatIDAndKeyword := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_kw_") chatIDAndKeywordList := strings.Split(chatIDAndKeyword, "_") - chatID, _ := strconv.ParseInt(chatIDAndKeywordList[0], 10, 64) - - var pendingMessage string - - if chatID == user.UserID { - pendingMessage = fmt.Sprintf("[ %s ] 是您设定的全局关键词", chatIDAndKeywordList[1]) - } else { - pendingMessage = fmt.Sprintf("[ %s ] 是为 %s 群组设定的关键词", chatIDAndKeywordList[1], utils.RemoveIDPrefix(chatID), KeywordDataList.Chats[chatID].ChatName) - } - - _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ - { - Text: "返回", - CallbackData: "detectkw_mng_chat_" + chatIDAndKeywordList[0], - }, - { - Text: "删除此关键词", - CallbackData: "detectkw_mng_delkw_" + chatIDAndKeyword, - }, - }}}, - ParseMode: models.ParseModeHTML, - }) + chatID, err := strconv.ParseInt(chatIDAndKeywordList[0], 10, 64) if err != nil { - fmt.Println(err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user wanna manage a keyword") + handlerErr.Addf("failed to parse chat ID when user wanna manage a keyword: %w", err) + } else { + var pendingMessage string + + if chatID == user.UserID { + pendingMessage = fmt.Sprintf("[ %s ] 是您设定的全局关键词", chatIDAndKeywordList[1]) + } else { + pendingMessage = fmt.Sprintf("[ %s ] 是为 %s 群组设定的关键词", chatIDAndKeywordList[1], utils.RemoveIDPrefix(chatID), KeywordDataList.Chats[chatID].ChatName) + } + + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: pendingMessage, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ + { + Text: "⬅️ 返回", + CallbackData: "detectkw_u_chat_" + chatIDAndKeywordList[0], + }, + { + Text: "❌ 删除此关键词", + CallbackData: "detectkw_u_delkw_" + chatIDAndKeyword, + }, + }}}, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "keyword manager keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `keyword manager keyboard`: %w", err) + } } - return + + return handlerErr.Flat() + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_add_") { + chatIDAndKeyword := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "detectkw_u_add_") + chatIDAndKeywordList := strings.Split(chatIDAndKeyword, "_") + chatID, err := strconv.ParseInt(chatIDAndKeywordList[0], 10, 64) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse chat ID when user wanna add a keyword") + return handlerErr.Addf("failed to parse chat ID when user wanna add a keyword: %w", err).Flat() + } else { + var pendingMessage string + var button models.ReplyMarkup + var isKeywordExist bool + + if chatID == user.UserID { + // 全局关键词 + for _, k := range user.GlobalKeyword { + if k == chatIDAndKeywordList[1] { + isKeywordExist = true + break + } + } + if !isKeywordExist { + logger.Debug(). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("globalKeyword", chatIDAndKeywordList[1]). + Msg("User add a global keyword") + user.GlobalKeyword = append(user.GlobalKeyword, chatIDAndKeywordList[1]) + KeywordDataList.Users[user.UserID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("globalKeyword", chatIDAndKeywordList[1]). + Msg("Failed to add global keyword and save keyword list") + return handlerErr.Addf("failed to add global keyword and save keyword list: %w", err).Flat() + } + pendingMessage = fmt.Sprintf("已添加全局关键词 [ %s ]", chatIDAndKeywordList[1]) + } else { + pendingMessage = fmt.Sprintf("此全局关键词 [ %s ] 已存在", chatIDAndKeywordList[1]) + } + } else { + // 群组关键词 + var chatForUser ChatForUser + var chatForUserIndex int + for i, c := range user.ChatsForUser { + if c.ChatID == chatID { + chatForUser = c + chatForUserIndex = i + } + } + targetChat := KeywordDataList.Chats[chatID] + for _, k := range chatForUser.Keyword { + if k == chatIDAndKeywordList[1] { + isKeywordExist = true + break + } + } + if !isKeywordExist { + logger.Debug(). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("User add a keyword to chat") + chatForUser.Keyword = append(chatForUser.Keyword, chatIDAndKeywordList[1]) + user.ChatsForUser[chatForUserIndex] = chatForUser + KeywordDataList.Users[user.UserID] = user + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to add keyword and save keyword list") + return handlerErr.Addf("failed to add keyword and save keyword list: %w", err).Flat() + } + pendingMessage = fmt.Sprintf("已为 %s 群组添加关键词 [ %s ]", utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName, strings.ToLower(chatIDAndKeywordList[1])) + } else { + pendingMessage = fmt.Sprintf("此关键词 [ %s ] 已存在于 %s 群组中,您可以继续向此群组添加其他关键词", chatIDAndKeywordList[1], utils.RemoveIDPrefix(targetChat.ChatID), targetChat.ChatName) + } + } + + if isKeywordExist { + button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "✅ 完成", + CallbackData: "detectkw_u", + }}}} + } else { + button = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ + { + Text: "↩️ 撤销操作", + CallbackData: fmt.Sprintf("detectkw_u_undo_%d_%s", chatID, chatIDAndKeywordList[1]), + }, + { + Text: "✅ 完成", + CallbackData: "detectkw_u", + }, + }}} + } + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: pendingMessage, + ReplyMarkup: button, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "keyword added notice"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `keyword added notice`: %w", err) + } + } + + return handlerErr.Flat() } } @@ -930,20 +1364,36 @@ func userManageCallbackHandler(opts *handler_structs.SubHandlerParams) { ReplyMarkup: buildUserChatList(user), }) if err != nil { - fmt.Println(err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "main manager keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `this group is disable by admins`: %w", err) } KeywordDataList.Users[opts.Update.CallbackQuery.From.ID] = user buildListenList() - SaveKeywordList() + err = SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to save keyword list") + handlerErr.Addf("failed to save keyword list: %w", err) + } + + return handlerErr.Flat() } func buildGroupManageKB(chat KeywordChatList) models.ReplyMarkup { var buttons [][]models.InlineKeyboardButton buttons = append(buttons, []models.InlineKeyboardButton{{ - Text: "🔄 当前状态: " + utils.TextForTrueOrFalse(chat.IsDisable, "已禁用", "已启用"), - CallbackData: "detectkw_groupmng_switch", + Text: "🔄 当前状态: " + utils.TextForTrueOrFalse(chat.IsDisable, "已禁用 ❌", "已启用 ✅"), + CallbackData: "detectkw_g_switch", }}) return &models.InlineKeyboardMarkup{ @@ -951,7 +1401,15 @@ func buildGroupManageKB(chat KeywordChatList) models.ReplyMarkup { } } -func startPrefixAddGroup(opts *handler_structs.SubHandlerParams) { +func startPrefixAddGroup(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "DetectKeyword"). + Str("funcName", "startPrefixAddGroup"). + Logger() + + var handlerErr multe.MultiError + user := KeywordDataList.Users[opts.Update.Message.From.ID] if user.AddTime == "" { // 初始化用户 @@ -963,21 +1421,39 @@ func startPrefixAddGroup(opts *handler_structs.SubHandlerParams) { IsSilentNotice: false, } KeywordDataList.Users[user.UserID] = user - SaveKeywordList() + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageText", opts.Update.Message.Text). + Msg("Failed to add user and save keyword list") + return handlerErr.Addf("failed to add user and save keyword list: %w", err).Flat() + } } if user.IsNotInit { - // 用户之前仅在群组内发送了命令,但并没有点击机器人来初始化 + // 用户之前仅在群组内发送命令添加了关键词,但并没有点击机器人来初始化 user.IsNotInit = false KeywordDataList.Users[user.UserID] = user buildListenList() - SaveKeywordList() + err := SaveKeywordList(opts.Ctx) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageText", opts.Update.Message.Text). + Msg("Failed to init user and save keyword list") + return handlerErr.Addf("failed to init user and save keyword list: %w", err).Flat() } if strings.HasPrefix(opts.Fields[1], "detectkw_addgroup_") { groupID := strings.TrimPrefix(opts.Fields[1], "detectkw_addgroup_") groupID_int64, err := strconv.ParseInt(groupID, 10, 64) if err != nil { - fmt.Println("format groupID error:", err) - return + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageText", opts.Update.Message.Text). + Msg("Failed to parse chat ID when user add a group by /start command") + return handlerErr.Addf("failed to parse chat ID when user add a group by /start command: %w", err).Flat() } chat := KeywordDataList.Chats[groupID_int64] @@ -991,7 +1467,10 @@ func startPrefixAddGroup(opts *handler_structs.SubHandlerParams) { } } if !IsAdded { - log.Println("add group", groupID_int64, "to user", opts.Update.Message.From.ID) + logger.Debug(). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("User add a chat to listen list by /start command") user.ChatsForUser = append(user.ChatsForUser, ChatForUser{ ChatID: groupID_int64, }) @@ -1008,11 +1487,25 @@ func startPrefixAddGroup(opts *handler_structs.SubHandlerParams) { ReplyMarkup: buildUserChatList(user), }) if err != nil { - fmt.Println(err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "added group in user list"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `added group in user list` message: %w", err) } } - SaveKeywordList() + err := SaveKeywordList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageText", opts.Update.Message.Text). + Msg("Failed to add group for user and save keyword list") + return handlerErr.Addf("failed to add group for user and save keyword list: %w", err).Flat() + } + return handlerErr.Flat() } func buildUserChatList(user KeywordUserList) models.ReplyMarkup { @@ -1021,46 +1514,46 @@ func buildUserChatList(user KeywordUserList) models.ReplyMarkup { if !user.IsDisable { buttons = append(buttons, []models.InlineKeyboardButton{{ Text: fmt.Sprintf("全局关键词 %d 个", len(user.GlobalKeyword)), - CallbackData: fmt.Sprintf("detectkw_mng_chat_%d", user.UserID), + CallbackData: fmt.Sprintf("detectkw_u_chat_%d", user.UserID), }}) for _, chat := range user.ChatsForUser { var subchats []models.InlineKeyboardButton var targetChat = KeywordDataList.Chats[chat.ChatID] - + subchats = append(subchats, models.InlineKeyboardButton{ - Text: targetChat.ChatName, - CallbackData: fmt.Sprintf("detectkw_mng_chat_%d", targetChat.ChatID), + Text: fmt.Sprintf("(%d) %s", len(chat.Keyword), targetChat.ChatName), + CallbackData: fmt.Sprintf("detectkw_u_chat_%d", targetChat.ChatID), }) if targetChat.IsDisable { subchats = append(subchats, models.InlineKeyboardButton{ Text: "🚫 查看帮助", - CallbackData: "detectkw_mng_chatdisablebyadmin", + CallbackData: "detectkw_u_chatdisablebyadmin", }) } else { subchats = append(subchats, models.InlineKeyboardButton{ Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsDisable, "当前已禁用 ❌", "当前已启用 ✅"), - CallbackData: fmt.Sprintf("detectkw_mng_switch_chat_%d", targetChat.ChatID), + CallbackData: fmt.Sprintf("detectkw_u_switch_chat_%d", targetChat.ChatID), }) } - + buttons = append(buttons, subchats) } buttons = append(buttons, []models.InlineKeyboardButton{{ Text: "🔄 通知偏好:" + utils.TextForTrueOrFalse(user.IsSilentNotice, "🔇 无声通知", "🔉 有声通知"), - CallbackData: "detectkw_mng_noticeswitch", + CallbackData: "detectkw_u_noticeswitch", }}) buttons = append(buttons, []models.InlineKeyboardButton{{ Text: "🔄 检测偏好:" + utils.TextForTrueOrFalse(user.IsIncludeSelf, "不排除自己的消息", "排除自己的消息"), - CallbackData: "detectkw_mng_selfswitch", + CallbackData: "detectkw_u_selfswitch", }}) } buttons = append(buttons, []models.InlineKeyboardButton{{ Text: "🔄 全局状态:" + utils.TextForTrueOrFalse(user.IsDisable, "已禁用 ❌", "已启用 ✅"), - CallbackData: "detectkw_mng_globalswitch", + CallbackData: "detectkw_u_globalswitch", }}) return &models.InlineKeyboardMarkup{ diff --git a/plugins/plugin_limit_message.go b/plugins/plugin_limit_message.go index c5ded28..791c6e3 100644 --- a/plugins/plugin_limit_message.go +++ b/plugins/plugin_limit_message.go @@ -1,43 +1,50 @@ package plugins import ( + "context" "fmt" - "io" - "log" "os" + "path/filepath" "reflect" "strings" "time" "trbot/utils" "trbot/utils/consts" + "trbot/utils/errt" "trbot/utils/handler_structs" + "trbot/utils/multe" "trbot/utils/plugin_utils" - "trbot/utils/type_utils" + "trbot/utils/type/message_utils" + "trbot/utils/yaml" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" - "gopkg.in/yaml.v3" + "github.com/rs/zerolog" ) var LimitMessageList map[int64]AllowMessages var LimitMessageErr error -var LimitMessage_path string = consts.DB_path + "limitmessage/" +var LimitMessageDir string = filepath.Join(consts.YAMLDataBaseDir, "limitmessage/") +var LimitMessagePath string = filepath.Join(LimitMessageDir, consts.YAMLFileName) type AllowMessages struct { - IsEnable bool `yaml:"IsEnable"` - IsUnderTest bool `yaml:"IsUnderTest"` - AddTime string `yaml:"AddTime"` - IsLogicAnd bool `yaml:"IsLogicAnd"` // true: `&&``, false: `||` - MessageType type_utils.MessageType `yaml:"MessageType"` - IsWhiteForType bool `yaml:"IsWhiteForType"` - MessageAttribute type_utils.MessageAttribute `yaml:"MessageAttribute"` - IsWhiteForAttribute bool `yaml:"IsWhiteForAttribute"` + IsEnable bool `yaml:"IsEnable"` + IsUnderTest bool `yaml:"IsUnderTest"` + AddTime string `yaml:"AddTime"` + IsLogicAnd bool `yaml:"IsLogicAnd"` // true: `&&``, false: `||` + IsWhiteForType bool `yaml:"IsWhiteForType"` + MessageType message_utils.MessageType `yaml:"MessageType"` + IsWhiteForAttribute bool `yaml:"IsWhiteForAttribute"` + MessageAttribute message_utils.MessageAttribute `yaml:"MessageAttribute"` } func init() { - ReadLimitMessageList() + plugin_utils.AddInitializer(plugin_utils.Initializer{ + Name: "Limit Message", + Func: ReadLimitMessageList, + }) plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ Name: "Limit Message", Saver: SaveLimitMessageList, @@ -57,162 +64,300 @@ func init() { Description: "此功能需要 bot 为群组管理员并拥有删除消息的权限\n可以按照消息类型和消息属性来自动删除不允许的消息,支持自定逻辑和黑白名单,作为管理员在群组中使用 /limitmessage 命令来查看菜单", ParseMode: models.ParseModeHTML, }) +} + +func ReadLimitMessageList(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "LimitMessage"). + Str("funcName", "ReadLimitMessageList"). + Logger() + + err := yaml.LoadYAML(LimitMessagePath, &LimitMessageList) + if err != nil { + if os.IsNotExist(err) { + logger.Warn(). + Err(err). + Str("path", LimitMessagePath). + Msg("Not found limit message list file. Created new one") + // 如果是找不到文件,新建一个 + err = yaml.SaveYAML(LimitMessagePath, &LimitMessageList) + if err != nil { + logger.Error(). + Err(err). + Str("path", LimitMessagePath). + Msg("Failed to create empty limit message list file") + LimitMessageErr = fmt.Errorf("failed to create empty limit message list file: %w", err) + } + } else { + logger.Error(). + Err(err). + Str("path", LimitMessagePath). + Msg("Failed to load limit message list file") + LimitMessageErr = fmt.Errorf("failed to load limit message list file: %w", err) + } + } else { + LimitMessageErr = nil + } + + if LimitMessageList == nil { + LimitMessageList = map[int64]AllowMessages{} + } + buildLimitGroupList() + + return LimitMessageErr } -func SaveLimitMessageList() error { - data, err := yaml.Marshal(LimitMessageList) - if err != nil { return err } - - if _, err := os.Stat(LimitMessage_path); os.IsNotExist(err) { - if err := os.MkdirAll(LimitMessage_path, 0755); err != nil { - return err - } - } - - if _, err := os.Stat(LimitMessage_path + consts.MetadataFileName); os.IsNotExist(err) { - _, err := os.Create(LimitMessage_path + consts.MetadataFileName) - if err != nil { - return err - } - } - - return os.WriteFile(LimitMessage_path + consts.MetadataFileName, data, 0644) -} - -func ReadLimitMessageList() { - var limitMessageList map[int64]AllowMessages - - file, err := os.Open(LimitMessage_path + consts.MetadataFileName) +func SaveLimitMessageList(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "LimitMessage"). + Str("funcName", "SaveLimitMessageList"). + Logger() + err := yaml.SaveYAML(LimitMessagePath, &LimitMessageList) if err != nil { - // 如果是找不到目录,新建一个 - log.Println("[LimitMessage]: Not found database file. Created new one") - SaveLimitMessageList() - LimitMessageList, LimitMessageErr = map[int64]AllowMessages{}, err - return + logger.Error(). + Err(err). + Str("path", LimitMessagePath). + Msg("Failed to save limit message list") + LimitMessageErr = fmt.Errorf("failed to save limit message list: %w", err) + } else { + LimitMessageErr = nil } - defer file.Close() - - decoder := yaml.NewDecoder(file) - err = decoder.Decode(&limitMessageList) - if err != nil { - if err == io.EOF { - log.Println("[LimitMessage]: database looks empty. now format it") - SaveLimitMessageList() - LimitMessageList, LimitMessageErr = map[int64]AllowMessages{}, nil - return - } - log.Println("(func)ReadLimitMessageList:", err) - LimitMessageList, LimitMessageErr = map[int64]AllowMessages{}, err - return - } - LimitMessageList, LimitMessageErr = limitMessageList, nil + return LimitMessageErr } -func SomeMessageOnlyHandler(opts *handler_structs.SubHandlerParams) { - if opts.Update.Message.Chat.Type == "private" { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ +func SomeMessageOnlyHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "LimitMessage"). + Str("funcName", "SomeMessageOnlyHandler"). + Logger() + + var handlerErr multe.MultiError + + if opts.Update.Message.Chat.Type == models.ChatTypePrivate { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, Text: "此功能被设计为仅在群组中可用", ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, }) - } else if utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, opts.Update.Message.From.ID) { - thisChat := LimitMessageList[opts.Update.Message.Chat.ID] - - if thisChat.AddTime == "" { - thisChat.AddTime = time.Now().Format(time.RFC3339) - } - - if utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, consts.BotMe.ID) && utils.UserHavePermissionDeleteMessage(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, consts.BotMe.ID) { - - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "Limit Message 菜单", - ReplyMarkup: buildMessageAllKB(thisChat), - }) - opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageID: opts.Update.Message.ID, - }) - LimitMessageList[opts.Update.Message.Chat.ID] = thisChat - SaveLimitMessageList() - } else { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "启用此功能前,请先将机器人设为管理员\n如果还是提示本消息,请检查机器人是否有删除消息的权限", - }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "limit message only allows in group"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `limit message only allows in group` message: %w", err) } } else { - botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "抱歉,您不是群组的管理员,无法为群组更改此功能", - }) - time.Sleep(time.Second * 5) - opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageIDs: []int{ - opts.Update.Message.ID, - botMessage.ID, - }, - }) + if utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, opts.Update.Message.From.ID) { + thisChat := LimitMessageList[opts.Update.Message.Chat.ID] + + var isNeedInit bool = false + + if thisChat.AddTime == "" { + isNeedInit = true + thisChat.AddTime = time.Now().Format(time.RFC3339) + } + + if utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, consts.BotMe.ID) && utils.UserHavePermissionDeleteMessage(opts.Ctx, opts.Thebot, opts.Update.Message.Chat.ID, consts.BotMe.ID) { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "Limit Message 菜单", + ReplyMarkup: buildMessageAllKB(thisChat), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "limit message main menu"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `limit message main menu` message: %w", err) + } + _, err = opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + MessageID: opts.Update.Message.ID, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "limit message command"). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `limit message command` message: %w", err) + } + if isNeedInit { + LimitMessageList[opts.Update.Message.Chat.ID] = thisChat + err = SaveLimitMessageList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Msg("Failed to save limit message list after adding new chat") + handlerErr.Addf("failed to save limit message list after adding new chat: %w", err) + } + } + } else { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "启用此功能前,请先将机器人设为管理员\n如果还是提示本消息,请检查机器人是否有删除消息的权限", + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "bot need be admin and delete message permission"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `bot need be admin and delete message permission` message: %w", err) + } + } + } else { + botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "抱歉,您不是群组的管理员,无法为群组更改此功能", + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "non-admin can not change limit message config"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `non-admin can not change limit message config` message: %w", err) + } + time.Sleep(time.Second * 5) + _, err = opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ + ChatID: opts.Update.Message.Chat.ID, + MessageIDs: []int{ + opts.Update.Message.ID, + botMessage.ID, + }, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "non-admin can not change limit message config"). + Msg(errt.DeleteMessages) + handlerErr.Addf("failed to delete `non-admin can not change limit message config` messages: %w", err) + } + } } + + return handlerErr.Flat() } -func DeleteNotAllowMessage(opts *handler_structs.SubHandlerParams) { - +func DeleteNotAllowMessage(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "LimitMessage"). + Str("funcName", "SomeMessageOnlyHandler"). + Logger() + + var handlerErr multe.MultiError + var deleteAction bool + var deleteHelp string = "当前模式:" if utils.AnyContains(opts.Update.Message.Chat.Type, models.ChatTypeGroup, models.ChatTypeSupergroup) { // 处理消息删除逻辑,只有当群组启用该功能时才处理 thisChat := LimitMessageList[opts.Update.Message.Chat.ID] - if thisChat.IsEnable { - this := type_utils.GetMessageType(opts.Update.Message) - thisattribute := type_utils.GetMessageAttribute(opts.Update.Message) + if thisChat.IsEnable || thisChat.IsUnderTest { + thisMsgType := message_utils.GetMessageType(opts.Update.Message) + thisMsgAttr := message_utils.GetMessageAttribute(opts.Update.Message) // 根据规则的黑白名单选择判断逻辑 if thisChat.IsLogicAnd { - deleteAction = CheckMessageType(this, thisChat.MessageType, thisChat.IsWhiteForType) && CheckMessageAttribute(thisattribute, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) + deleteHelp += "同时触发两个规则才删除消息\n" + msgType, typeHelp := CheckMessageType(thisMsgType, thisChat.MessageType, thisChat.IsWhiteForType) + deleteHelp += "消息类型:" + typeHelp + if msgType { + msgAttr, attrHelp := CheckMessageAttribute(thisMsgAttr, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) + deleteHelp += "消息属性:" + attrHelp + if msgType && msgAttr { + deleteAction = true + } + } + + // deleteAction = CheckMessageType(thisMsgType, thisChat.MessageType, thisChat.IsWhiteForType) && CheckMessageAttribute(thisMsgAttr, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) } else { - deleteAction = CheckMessageType(this, thisChat.MessageType, thisChat.IsWhiteForType) || CheckMessageAttribute(thisattribute, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) + deleteHelp += "触发任一规则就删除消息\n" + msgType, typeHelp := CheckMessageType(thisMsgType, thisChat.MessageType, thisChat.IsWhiteForType) + deleteHelp += "消息类型:" + typeHelp + if msgType { + deleteAction = true + } else { + msgAttr, attrHelp := CheckMessageAttribute(thisMsgAttr, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) + deleteHelp += "消息属性:" + attrHelp + if msgAttr { + deleteAction = true + } + } + + // deleteAction = CheckMessageType(thisMsgType, thisChat.MessageType, thisChat.IsWhiteForType) || CheckMessageAttribute(thisMsgAttr, thisChat.MessageAttribute, thisChat.IsWhiteForAttribute) } - if deleteAction { - if thisChat.IsUnderTest { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "测试模式:此消息将被设定的规则删除", - DisableNotification: true, - ReplyParameters: &models.ReplyParameters{ - MessageID: opts.Update.Message.ID, + if thisChat.IsUnderTest { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: utils.TextForTrueOrFalse(deleteAction, "此消息会被设定的规则删除\n\n", "") + + deleteHelp + + utils.TextForTrueOrFalse(thisChat.IsEnable, "
当前已启用,关闭测试模式将开始删除触发了规则的消息
", "
您可以继续进行测试,以便达到您想要的效果,之后请手动启用此功能\n
"), + DisableNotification: true, + ParseMode: models.ParseModeHTML, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ + { + Text: "打开配置菜单", + CallbackData: "limitmsg_back", }, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ - { - Text: "删除此提醒", - CallbackData: "limitmsg_done", - }, - { - Text: "关闭测试模式", - CallbackData: "limitmsg_offtest", - }, - }}}, - }) + { + Text: "关闭测试模式", + CallbackData: "limitmsg_offtest", + }, + }}}, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "test mode delete message notification"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `test mode delete message notification` message: %w", err) + } + } else if deleteAction { + _, err := opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + MessageID: opts.Update.Message.ID, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageType", string(thisMsgType.InString())). + Int("messageID", opts.Update.Message.ID). + Str("content", "message trigger limit message rules"). + Bool("IsLogicAnd", thisChat.IsLogicAnd). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `message trigger limit message rules` message: %w", err) } else { - _, err := opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageID: opts.Update.Message.ID, - }) - if err != nil { - log.Printf("Failed to delete message: %v", err) - } else { - log.Printf("Deleted message from %d in %d: %s\n", opts.Update.Message.From.ID, opts.Update.Message.Chat.ID, opts.Update.Message.Text) - } + logger.Info(). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("messageType", string(thisMsgType.InString())). + Int("messageID", opts.Update.Message.ID). + Bool("IsLogicAnd", thisChat.IsLogicAnd). + Msg("Deleted message trigger limit message rules") } } } } + return handlerErr.Flat() } -func CheckMessageType(this, target type_utils.MessageType, IsWhiteList bool) bool { +func CheckMessageType(this, target message_utils.MessageType, IsWhiteList bool) (bool, string) { var delete bool = IsWhiteList + var deleteHelp string v1 := reflect.ValueOf(this) v2 := reflect.ValueOf(target) @@ -224,33 +369,31 @@ func CheckMessageType(this, target type_utils.MessageType, IsWhiteList bool) boo val2 := v2.Field(i).Interface() if val1 == true && val1 == val2 { + deleteHelp += fmt.Sprintf("%s 消息类型 %s %s\n", + utils.TextForTrueOrFalse(IsWhiteList, "白名单", "黑名单"), + field.Name, + utils.TextForTrueOrFalse(IsWhiteList, "不删除", "删除"), + ) if IsWhiteList { - fmt.Printf("白名单 消息类型 %s 不删除\n", field.Name) delete = false } else { - fmt.Printf("黑名单 消息类型 %s 删除\n", field.Name) delete = true } } else if val1 == true && val1 != val2 { - if IsWhiteList { - fmt.Printf("白名单 ") - } else { - fmt.Printf("黑名单 ") - } - fmt.Printf("未命中 消息类型 %s 遵循默认规则 ", field.Name) - if delete { - fmt.Println("删除") - } else { - fmt.Println("不删除") - } + deleteHelp += fmt.Sprintf("%s 未命中 消息类型 %s 遵循默认规则 %s\n", + utils.TextForTrueOrFalse(IsWhiteList, "白名单", "黑名单"), + field.Name, + utils.TextForTrueOrFalse(delete, "删除", "不删除"), + ) } } - return delete + return delete, deleteHelp } -func CheckMessageAttribute(this, target type_utils.MessageAttribute, IsWhiteList bool) bool { +func CheckMessageAttribute(this, target message_utils.MessageAttribute, IsWhiteList bool) (bool, string) { var delete bool = IsWhiteList var noAttribute bool = true // 如果没有命中任何消息属性,提示内容,根据黑白名单判断是否删除 + var deleteHelp string v1 := reflect.ValueOf(this) v2 := reflect.ValueOf(target) @@ -264,96 +407,43 @@ func CheckMessageAttribute(this, target type_utils.MessageAttribute, IsWhiteList if val1 == true && val1 == val2 { noAttribute = false + deleteHelp += fmt.Sprintf("%s 消息属性 %s %s\n", + utils.TextForTrueOrFalse(IsWhiteList, "白名单", "黑名单"), + field.Name, + utils.TextForTrueOrFalse(IsWhiteList, "不删除", "删除"), + ) if IsWhiteList { - fmt.Printf("白名单 消息属性 %s 不删除\n", field.Name) delete = false } else { - fmt.Printf("黑名单 消息属性 %s 删除\n", field.Name) delete = true } } else if val1 == true && val1 != val2 { noAttribute = false - if IsWhiteList { - fmt.Printf("白名单 ") - } else { - fmt.Printf("黑名单 ") - } - fmt.Printf("未命中 消息属性 %s 遵循默认规则 ", field.Name) - if delete { - fmt.Println("删除") - } else { - fmt.Println("不删除") - } + deleteHelp += fmt.Sprintf("%s 未命中 消息属性 %s 遵循默认规则 %s\n", + utils.TextForTrueOrFalse(IsWhiteList, "白名单", "黑名单"), + field.Name, + utils.TextForTrueOrFalse(delete, "删除", "不删除"), + ) } } if noAttribute { - if IsWhiteList { - fmt.Printf("白名单 ") - } else { - fmt.Printf("黑名单 ") - } - fmt.Printf("未命中 消息属性 无 遵循默认规则 ") - if delete { - fmt.Println("删除") - } else { - fmt.Println("不删除") - } + deleteHelp += fmt.Sprintf("%s 未命中 消息属性 无 遵循默认规则 %s\n", + utils.TextForTrueOrFalse(IsWhiteList, "白名单", "黑名单"), + utils.TextForTrueOrFalse(delete, "删除", "不删除"), + ) } - return delete + + return delete, deleteHelp } func buttonText(text string, opt, IsWhiteList bool) string { if opt { - if IsWhiteList { - return "✅ " + text - } else { - return "❌ " + text - } + return utils.TextForTrueOrFalse(IsWhiteList, "✅ ", "❌ ") + text } return text } -func buttonWhiteBlackRule(opt bool) string { - if opt { - return "白名单模式" - } - - return "黑名单模式" -} - -func buttonWhiteBlackDescription(opt bool) string { - if opt { - return "仅允许发送选中的项目,其他消息将被删除" - } - - return "将删除选中的项目" -} - -func buttonIsEnable(opt bool) string { - if opt { - return "当前已启用" - } - - return "当前已关闭" -} - -func buttonIsLogicAnd(opt bool) string { - if opt { - return "满足上方所有条件才删除消息" - } - - return "满足其中一个条件就删除消息" -} - -func buttonIsUnderTest(opt bool) string { - if opt { - return "点击关闭测试模式" - } - - return "点此开启测试模式" -} - func buildMessageTypeKB(chat AllowMessages) models.ReplyMarkup { var msgTypeItems [][]models.InlineKeyboardButton @@ -365,7 +455,7 @@ func buildMessageTypeKB(chat AllowMessages) models.ReplyMarkup { for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) - if i % 2 == 0 && i != 0 { + if i % 3 == 0 && i != 0 { msgTypeItems = append(msgTypeItems, msgTypeItemsTemp) msgTypeItemsTemp = []models.InlineKeyboardButton{} } @@ -380,7 +470,7 @@ func buildMessageTypeKB(chat AllowMessages) models.ReplyMarkup { msgTypeItems = append(msgTypeItems, []models.InlineKeyboardButton{{ - Text: "返回上一级", + Text: "⬅️ 返回上一级", CallbackData: "limitmsg_back", }}) @@ -417,7 +507,7 @@ func buildMessageAttributeKB(chat AllowMessages) models.ReplyMarkup { msgAttributeItems = append(msgAttributeItems, []models.InlineKeyboardButton{{ - Text: "返回上一级", + Text: "⬅️ 返回上一级", CallbackData: "limitmsg_back", }}) @@ -437,7 +527,7 @@ func buildMessageAllKB(chat AllowMessages) models.ReplyMarkup { CallbackData: "limitmsg_typekb", }, { - Text: "<-- " + buttonWhiteBlackRule(chat.IsWhiteForType), + Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsWhiteForType, "白名单模式", "黑名单模式"), CallbackData: "limitmsg_typekb_switchrule", }, }) @@ -448,32 +538,32 @@ func buildMessageAllKB(chat AllowMessages) models.ReplyMarkup { CallbackData: "limitmsg_attrkb", }, { - Text: "<-- " + buttonWhiteBlackRule(chat.IsWhiteForAttribute), + Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsWhiteForAttribute, "白名单模式", "黑名单模式"), CallbackData: "limitmsg_attrkb_switchrule", }, }) chatAllow = append(chatAllow, []models.InlineKeyboardButton{ { - Text: buttonIsLogicAnd(chat.IsLogicAnd), + Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsLogicAnd, "满足上方所有条件才删除消息", "满足其中一个条件就删除消息"), CallbackData: "limitmsg_switchlogic", }, }) chatAllow = append(chatAllow, []models.InlineKeyboardButton{ { - Text: buttonIsUnderTest(chat.IsUnderTest), + Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsUnderTest, "测试模式已开启 ✅", "测试模式已关闭 ❌"), CallbackData: "limitmsg_switchtest", }, }) chatAllow = append(chatAllow, []models.InlineKeyboardButton{ { - Text: "关闭菜单", + Text: "🚫 关闭菜单", CallbackData: "limitmsg_done", }, { - Text: buttonIsEnable(chat.IsEnable), + Text: "🔄 " + utils.TextForTrueOrFalse(chat.IsEnable, "当前已启用 ✅", "当前已关闭 ❌"), CallbackData: "limitmsg_switchenable", }, }) @@ -485,147 +575,251 @@ func buildMessageAllKB(chat AllowMessages) models.ReplyMarkup { return kb } -func LimitMessageCallback(opts *handler_structs.SubHandlerParams) { +func LimitMessageCallback(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "LimitMessage"). + Str("funcName", "LimitMessageCallback"). + Logger() + + var handlerErr multe.MultiError + if !utils.UserIsAdmin(opts.Ctx, opts.Thebot, opts.Update.CallbackQuery.Message.Message.Chat.ID, opts.Update.CallbackQuery.From.ID) { - opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ CallbackQueryID: opts.Update.CallbackQuery.ID, Text: "您没有权限修改此配置", ShowAlert: true, }) - return - } - thisChat := LimitMessageList[opts.Update.CallbackQuery.Message.Message.Chat.ID] + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "no permission to change limit message config"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `no permission to change limit message config` callback answer: %w", err) + } + } else { + thisChat := LimitMessageList[opts.Update.CallbackQuery.Message.Message.Chat.ID] - var needRebuildGroupList bool + var needRebuildGroupList bool + var needSavelimitMessageList bool + var needEditMainMenuMessage bool - switch opts.Update.CallbackQuery.Data { - case "limitmsg_typekb": - // opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ - // CallbackQueryID: opts.Update.CallbackQuery.ID, - // Text: "已选择消息类型", - // }) - opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: buttonWhiteBlackRule(thisChat.IsWhiteForType) + ": " + buttonWhiteBlackDescription(thisChat.IsWhiteForType), - ReplyMarkup: buildMessageTypeKB(thisChat), - }) - case "limitmsg_typekb_switchrule": - thisChat.IsWhiteForType = !thisChat.IsWhiteForType - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: buildMessageAllKB(thisChat), - }) - case "limitmsg_attrkb": - opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: buttonWhiteBlackRule(thisChat.IsWhiteForAttribute) + ": " + buttonWhiteBlackDescription(thisChat.IsWhiteForAttribute) + "\n有一些项目可能无法使用", - ReplyMarkup: buildMessageAttributeKB(thisChat), - }) - case "limitmsg_attrkb_switchrule": - thisChat.IsWhiteForAttribute = !thisChat.IsWhiteForAttribute - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: buildMessageAllKB(thisChat), - }) - case "limitmsg_back": - opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: "Limit Message 菜单", - ReplyMarkup: buildMessageAllKB(thisChat), - }) - case "limitmsg_done": - opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - }) - case "limitmsg_switchenable": - thisChat.IsEnable = !thisChat.IsEnable - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: buildMessageAllKB(thisChat), - }) - needRebuildGroupList = true - case "limitmsg_switchlogic": - thisChat.IsLogicAnd = !thisChat.IsLogicAnd - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: buildMessageAllKB(thisChat), - }) - case "limitmsg_switchtest": - thisChat.IsUnderTest = !thisChat.IsUnderTest - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: buildMessageAllKB(thisChat), - }) - needRebuildGroupList = true - case "limitmsg_offtest": - thisChat.IsUnderTest = false - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "删除此提醒", - CallbackData: "limitmsg_done", - }}}}, - }) - needRebuildGroupList = true - default: - if strings.HasPrefix(opts.Update.CallbackQuery.Data, "limitmsg_type_") { - callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "limitmsg_type_") - - data := thisChat.MessageType - v := reflect.ValueOf(data) // 解除指针获取值 - t := reflect.TypeOf(data) - newStruct := reflect.New(v.Type()).Elem() - newStruct.Set(v) // 复制原始值 - for i := 0; i < newStruct.NumField(); i++ { - if t.Field(i).Name == callbackField { - newStruct.Field(i).SetBool(!newStruct.Field(i).Bool()) - } - } - thisChat.MessageType = newStruct.Interface().(type_utils.MessageType) - - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ + switch opts.Update.CallbackQuery.Data { + case "limitmsg_typekb": + // opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + // CallbackQueryID: opts.Update.CallbackQuery.ID, + // Text: "已选择消息类型", + // }) + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: utils.TextForTrueOrFalse(thisChat.IsWhiteForType, "白名单模式", "黑名单模式") + ": " + utils.TextForTrueOrFalse(thisChat.IsWhiteForType, "仅允许发送选中的项目,其他消息将被删除", "将删除选中的项目"), ReplyMarkup: buildMessageTypeKB(thisChat), }) - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "limitmsg_attr_") { - callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "limitmsg_attr_") - data := thisChat.MessageAttribute - v := reflect.ValueOf(data) // 解除指针获取值 - t := reflect.TypeOf(data) - newStruct := reflect.New(v.Type()).Elem() - newStruct.Set(v) // 复制原始值 - for i := 0; i < newStruct.NumField(); i++ { - if t.Field(i).Name == callbackField { - newStruct.Field(i).SetBool(!newStruct.Field(i).Bool()) - } + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "limit message type keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `limit message type keyboard`: %w", err) } - - thisChat.MessageAttribute = newStruct.Interface().(type_utils.MessageAttribute) - - opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ + case "limitmsg_typekb_switchrule": + thisChat.IsWhiteForType = !thisChat.IsWhiteForType + needSavelimitMessageList = true + needEditMainMenuMessage = true + case "limitmsg_attrkb": + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: utils.TextForTrueOrFalse(thisChat.IsWhiteForAttribute, "白名单模式", "黑名单模式") + ": " + utils.TextForTrueOrFalse(thisChat.IsWhiteForAttribute, "仅允许发送选中的项目,其他消息将被删除", "将删除选中的项目") + "\n有一些项目可能无法使用", ReplyMarkup: buildMessageAttributeKB(thisChat), }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "limit message attribute keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `limit message attribute keyboard`: %w", err) + } + case "limitmsg_attrkb_switchrule": + thisChat.IsWhiteForAttribute = !thisChat.IsWhiteForAttribute + needSavelimitMessageList = true + needEditMainMenuMessage = true + case "limitmsg_back": + needEditMainMenuMessage = true + case "limitmsg_done": + _, err := opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "limit message main menu or test mode delete message notification"). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `limit message main menu or test mode delete message notification` message: %w", err) + } + case "limitmsg_switchenable": + thisChat.IsEnable = !thisChat.IsEnable + if thisChat.IsEnable { thisChat.IsUnderTest = false } + needRebuildGroupList = true + needSavelimitMessageList = true + needEditMainMenuMessage = true + case "limitmsg_switchlogic": + thisChat.IsLogicAnd = !thisChat.IsLogicAnd + needSavelimitMessageList = true + needEditMainMenuMessage = true + case "limitmsg_switchtest": + thisChat.IsUnderTest = !thisChat.IsUnderTest + needEditMainMenuMessage = true + needRebuildGroupList = true + needSavelimitMessageList = true + case "limitmsg_offtest": + thisChat.IsUnderTest = false + needSavelimitMessageList = true + needRebuildGroupList = true + _, err := opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "删除此提醒", + CallbackData: "limitmsg_done", + }}}}, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "test mode turned off notice"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `test mode turned off notice`: %w", err) + } + default: + if strings.HasPrefix(opts.Update.CallbackQuery.Data, "limitmsg_type_") { + needSavelimitMessageList = true + callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "limitmsg_type_") + + data := thisChat.MessageType + v := reflect.ValueOf(data) // 解除指针获取值 + t := reflect.TypeOf(data) + newStruct := reflect.New(v.Type()).Elem() + newStruct.Set(v) // 复制原始值 + for i := 0; i < newStruct.NumField(); i++ { + if t.Field(i).Name == callbackField { + newStruct.Field(i).SetBool(!newStruct.Field(i).Bool()) + } + } + thisChat.MessageType = newStruct.Interface().(message_utils.MessageType) + + _, err := opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + ReplyMarkup: buildMessageTypeKB(thisChat), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "limit message type keyboard"). + Msg(errt.EditMessageReplyMarkup) + handlerErr.Addf("failed to edit message reply markup to `limit message type keyboard`: %w", err) + } + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "limitmsg_attr_") { + needSavelimitMessageList = true + callbackField := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "limitmsg_attr_") + data := thisChat.MessageAttribute + v := reflect.ValueOf(data) // 解除指针获取值 + t := reflect.TypeOf(data) + newStruct := reflect.New(v.Type()).Elem() + newStruct.Set(v) // 复制原始值 + for i := 0; i < newStruct.NumField(); i++ { + if t.Field(i).Name == callbackField { + newStruct.Field(i).SetBool(!newStruct.Field(i).Bool()) + } + } + + thisChat.MessageAttribute = newStruct.Interface().(message_utils.MessageAttribute) + + _, err := opts.Thebot.EditMessageReplyMarkup(opts.Ctx, &bot.EditMessageReplyMarkupParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + ReplyMarkup: buildMessageAttributeKB(thisChat), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "limit message attribute keyboard"). + Msg(errt.EditMessageReplyMarkup) + handlerErr.Addf("failed to edit message reply markup to `limit message attribute keyboard`: %w", err) + } + } + } + + if needSavelimitMessageList { + LimitMessageList[opts.Update.CallbackQuery.Message.Message.Chat.ID] = thisChat + err := SaveLimitMessageList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to save limit message list") + handlerErr.Addf("failed to save limit message list: %w", err) + _, err = opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: opts.Update.CallbackQuery.ID, + Text: "保存修改失败,请重试或联系机器人管理员\n" + err.Error(), + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Str("content", "failed to save limit message list"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `failed to save limit message list` callback answer: %w", err) + } + } + } + + if needRebuildGroupList { + buildLimitGroupList() + } + + if needEditMainMenuMessage { + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: "Limit Message 菜单", + ReplyMarkup: buildMessageAllKB(thisChat), + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.CallbackQuery.Message.Message.Chat)). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Str("content", "limit message main menu"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `limit message main menu`: %w", err) + } } } - LimitMessageList[opts.Update.CallbackQuery.Message.Message.Chat.ID] = thisChat - if needRebuildGroupList { - buildLimitGroupList() - } - SaveLimitMessageList() + return handlerErr.Flat() } func buildLimitGroupList() { diff --git a/plugins/plugin_sticker.go b/plugins/plugin_sticker.go index db868ed..08e8b9b 100644 --- a/plugins/plugin_sticker.go +++ b/plugins/plugin_sticker.go @@ -5,26 +5,35 @@ import ( "fmt" "image/png" "io" - "log" "net/http" "os" + "os/exec" "path/filepath" "strings" + "time" "trbot/database" "trbot/database/db_struct" + "trbot/utils" + "trbot/utils/configs" "trbot/utils/consts" + "trbot/utils/errt" "trbot/utils/handler_structs" + "trbot/utils/multe" "trbot/utils/plugin_utils" - "trbot/utils/type_utils" + "trbot/utils/type/message_utils" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" + "github.com/rs/zerolog" "golang.org/x/image/webp" ) -var StickerCache_path string = consts.Cache_path + "sticker/" -var StickerCachePNG_path string = consts.Cache_path + "sticker_png/" -var StickerCacheZip_path string = consts.Cache_path + "sticker_zip/" +var StickerCollectionChannelID int64 = -1002506914682 + +var StickerCache_path string = filepath.Join(consts.CacheDirectory, "sticker/") +var StickerCachePNG_path string = filepath.Join(consts.CacheDirectory, "sticker_png/") +var StickerCacheGIF_path string = filepath.Join(consts.CacheDirectory, "sticker_gif/") +var StickerCacheZip_path string = filepath.Join(consts.CacheDirectory, "sticker_zip/") func init() { plugin_utils.AddCallbackQueryCommandPlugins([]plugin_utils.CallbackQuery{ @@ -38,6 +47,20 @@ func init() { CommandChar: "S", Handler: DownloadStickerPackCallBackHandler, }, + { + CommandChar: "c", + Handler: collectStickerSet, + }, + }...) + plugin_utils.AddCustomSymbolCommandPlugins([]plugin_utils.CustomSymbolCommand{ + { + FullCommand: "https://t.me/addstickers/", + Handler: getStickerPackInfo, + }, + { + FullCommand: "t.me/addstickers/", + Handler: getStickerPackInfo, + }, }...) plugin_utils.AddHandlerHelpInfo(plugin_utils.HandlerHelp{ Name: "贴纸下载", @@ -45,27 +68,162 @@ func init() { ParseMode: models.ParseModeHTML, }) plugin_utils.AddHandlerByMessageTypePlugins(plugin_utils.HandlerByMessageType{ - PluginName: "StickerDownload", - ChatType: models.ChatTypePrivate, - MessageType: type_utils.Sticker, + PluginName: "StickerDownload", + ChatType: models.ChatTypePrivate, + MessageType: message_utils.Sticker, AllowAutoTrigger: true, - Handler: EchoStickerHandler, + Handler: EchoStickerHandler, + }) + plugin_utils.AddSlashSymbolCommandPlugins(plugin_utils.SlashSymbolCommand{ + SlashCommand: "cachedsticker", + Handler: showCachedStickers, }) } type stickerDatas struct { Data io.Reader + IsConverted bool IsCustomSticker bool StickerCount int StickerIndex int StickerSetName string // 贴纸包的 urlname StickerSetTitle string // 贴纸包名称 + + WebP int + WebM int + tgs int +} + +func EchoStickerHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "EchoStickerHandler"). + Logger() + + var handlerErr multe.MultiError + + if opts.Update.Message == nil && opts.Update.CallbackQuery != nil && strings.HasPrefix(opts.Update.CallbackQuery.Data, "HBMT_") && opts.Update.CallbackQuery.Message.Message != nil && opts.Update.CallbackQuery.Message.Message.ReplyToMessage != nil { + // if this handler tigger by `handler by message type`, copy `update.CallbackQuery.Message.Message.ReplyToMessage` to `update.Message` + opts.Update.Message = opts.Update.CallbackQuery.Message.Message.ReplyToMessage + logger.Info(). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("copy `update.CallbackQuery.Message.Message.ReplyToMessage` to `update.Message`") + } + + logger.Info(). + Str("emoji", opts.Update.Message.Sticker.Emoji). + Str("setName", opts.Update.Message.Sticker.SetName). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Start download sticker") + + err := database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.From.ID, db_struct.StickerDownloaded) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to incremental sticker download count") + handlerErr.Addf("failed to incremental sticker download count: %w", err) + } + + stickerData, err := EchoSticker(opts) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Error when downloading sticker") + handlerErr.Addf("error when downloading sticker: %w", err) + + _, msgerr := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.From.ID, + Text: fmt.Sprintf("下载贴纸时发生了一些错误\n
Failed to download sticker: %s
", err), + ParseMode: models.ParseModeHTML, + }) + if msgerr != nil { + logger.Error(). + Err(msgerr). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "sticker download error"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `sticker download error` message: %w", msgerr) + } + } else { + documentParams := &bot.SendDocumentParams{ + ChatID: opts.Update.Message.From.ID, + ParseMode: models.ParseModeHTML, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + DisableNotification: true, + DisableContentTypeDetection: true, // Prevent the server convert gif to mp4 + } + + var stickerFilePrefix, stickerFileSuffix string + + if opts.Update.Message.Sticker.IsVideo { + if stickerData.IsConverted { + stickerFileSuffix = "gif" + } else { + documentParams.Caption = "
see wikipedia/WebM
" + stickerFileSuffix = "webm" + } + } else if opts.Update.Message.Sticker.IsAnimated { + documentParams.Caption = "
see stickers/animated-stickers
" + stickerFileSuffix = "tgs.file" + } else { + stickerFileSuffix = "png" + } + + if stickerData.IsCustomSticker { + stickerFilePrefix = "sticker" + } else { + var button [][]models.InlineKeyboardButton = [][]models.InlineKeyboardButton{ + { + { Text: "下载贴纸包中的静态贴纸", CallbackData: fmt.Sprintf("S_%s", opts.Update.Message.Sticker.SetName) }, + }, + { + { Text: "下载整个贴纸包(不转换格式)", CallbackData: fmt.Sprintf("s_%s", opts.Update.Message.Sticker.SetName) }, + }, + } + + if StickerCollectionChannelID != 0 && utils.AnyContains(opts.Update.Message.From.ID, configs.BotConfig.AdminIDs) { + button = append(button, []models.InlineKeyboardButton{{ + Text: "⭐️ 收藏至频道", + CallbackData: fmt.Sprintf("c_%s", stickerData.StickerSetName), + }}) + } + + stickerFilePrefix = fmt.Sprintf("%s_%d", stickerData.StickerSetName, stickerData.StickerIndex) + + // 仅在不为自定义贴纸时显示下载整个贴纸包按钮 + documentParams.Caption += fmt.Sprintf("%s 贴纸包中一共有 %d 个贴纸\n", stickerData.StickerSetName, stickerData.StickerSetTitle, stickerData.StickerCount) + documentParams.ReplyMarkup = &models.InlineKeyboardMarkup{ InlineKeyboard: button } + } + + documentParams.Document = &models.InputFileUpload{ Filename: fmt.Sprintf("%s.%s", stickerFilePrefix, stickerFileSuffix), Data: stickerData.Data } + + _, err = opts.Thebot.SendDocument(opts.Ctx, documentParams) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "sticker file"). + Msg(errt.SendDocument) + handlerErr.Addf("failed to send sticker file: %w", err) + } + } + + return handlerErr.Flat() } func EchoSticker(opts *handler_structs.SubHandlerParams) (*stickerDatas, error) { - var data stickerDatas + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "EchoSticker"). + Logger() + var data stickerDatas var fileSuffix string // `webp`, `webm`, `tgs` + // 根据贴纸类型设置文件扩展名 if opts.Update.Message.Sticker.IsVideo { fileSuffix = "webm" @@ -82,7 +240,20 @@ func EchoSticker(opts *handler_structs.SubHandlerParams) (*stickerDatas, error) if opts.Update.Message.Sticker.SetName != "" { // 获取贴纸包信息 stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{ Name: opts.Update.Message.Sticker.SetName }) - if err == nil { + if err != nil { + // this sticker has a setname, but that sticker set has been deleted + // it may also be because of an error in obtaining sticker there are no static stickers in the sticker set information, so just let the user try again. + logger.Warn(). + Err(err). + Str("setName", opts.Update.Message.Sticker.SetName). + Msg("Failed to get sticker set info, download it as a custom sticker") + + // 到这里是因为用户发送的贴纸对应的贴纸包已经被删除了,但贴纸中的信息还有对应的 SetName,会触发查询,但因为贴纸包被删了就查不到,将 index 值设为 -1,缓存后当作自定义贴纸继续 + data.IsCustomSticker = true + stickerSetNamePrivate = opts.Update.Message.Sticker.SetName + stickerFileNameWithDot = fmt.Sprintf("%s %d %s.", opts.Update.Message.Sticker.SetName, -1, opts.Update.Message.Sticker.FileID) + } else { + // sticker is in a sticker set data.StickerCount = len(stickerSet.Stickers) data.StickerSetName = stickerSet.Name data.StickerSetTitle = stickerSet.Title @@ -98,402 +269,253 @@ func EchoSticker(opts *handler_structs.SubHandlerParams) (*stickerDatas, error) // 在这个条件下,贴纸包名和贴纸索引都存在,赋值完整的贴纸文件名 stickerSetNamePrivate = opts.Update.Message.Sticker.SetName stickerFileNameWithDot = fmt.Sprintf("%s %d %s.", opts.Update.Message.Sticker.SetName, data.StickerIndex, opts.Update.Message.Sticker.FileID) - } else { - // 到这里是因为用户发送的贴纸对应的贴纸包已经被删除了,但贴纸中的信息还有对应的 SetName,会触发查询,但因为贴纸包被删了就查不到,将 index 值设为 -1,缓存后当作自定义贴纸继续 - log.Println("error getting sticker set:", err) - data.IsCustomSticker = true - stickerSetNamePrivate = opts.Update.Message.Sticker.SetName - stickerFileNameWithDot = fmt.Sprintf("%s %d %s.", opts.Update.Message.Sticker.SetName, -1, opts.Update.Message.Sticker.FileID) } } else { + // this sticker doesn't have a setname, so it is a custom sticker // 自定义贴纸,防止与普通贴纸包冲突,将贴纸包名设置为 `-custom`,文件名仅有 FileID 用于辨识 data.IsCustomSticker = true stickerSetNamePrivate = "-custom" stickerFileNameWithDot = fmt.Sprintf("%s.", opts.Update.Message.Sticker.FileID) } - var filePath string = StickerCache_path + stickerSetNamePrivate + "/" // 保存贴纸源文件的目录 .cache/sticker/setName/ - var originFullPath string = filePath + stickerFileNameWithDot + fileSuffix // 到贴纸文件的完整目录 .cache/sticker/setName/stickerFileName.webp - - var filePathPNG string = StickerCachePNG_path + stickerSetNamePrivate + "/" // 转码后为 png 格式的目录 .cache/sticker_png/setName/ - var toPNGFullPath string = filePathPNG + stickerFileNameWithDot + "png" // 转码后到 png 格式贴纸的完整目录 .cache/sticker_png/setName/stickerFileName.png + var filePath string = filepath.Join(StickerCache_path, stickerSetNamePrivate) // 保存贴纸源文件的目录 .cache/sticker/setName/ + var originFullPath string = filepath.Join(filePath, stickerFileNameWithDot + fileSuffix) // 到贴纸文件的完整目录 .cache/sticker/setName/stickerFileName.webp + var finalFullPath string // 存放最后读取并发送的文件完整目录 .cache/sticker/setName/stickerFileName.webp _, err := os.Stat(originFullPath) // 检查贴纸源文件是否已缓存 - // 如果文件不存在,进行下载,否则跳过 - if os.IsNotExist(err) { - // 从服务器获取文件信息 - fileinfo, err := opts.Thebot.GetFile(opts.Ctx, &bot.GetFileParams{ FileID: opts.Update.Message.Sticker.FileID }) - if err != nil { return nil, fmt.Errorf("error getting fileinfo %s: %v", opts.Update.Message.Sticker.FileID, err) } - - // 日志提示该文件没被缓存,正在下载 - if consts.IsDebugMode { log.Printf("file [%s] doesn't exist, downloading %s", originFullPath, fileinfo.FilePath) } - - // 组合链接下载贴纸源文件 - resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", consts.BotToken, fileinfo.FilePath)) - if err != nil { return nil, fmt.Errorf("error downloading file %s: %v", fileinfo.FilePath, err) } - defer resp.Body.Close() - - // 创建保存贴纸的目录 - err = os.MkdirAll(filePath, 0755) - if err != nil { return nil, fmt.Errorf("error creating directory %s: %w", filePath, err) } - - // 创建贴纸空文件 - downloadedSticker, err := os.Create(originFullPath) - if err != nil { return nil, fmt.Errorf("error creating file %s: %w", originFullPath, err) } - defer downloadedSticker.Close() - - // 将下载的原贴纸写入空文件 - _, err = io.Copy(downloadedSticker, resp.Body) - if err != nil { return nil, fmt.Errorf("error writing to file %s: %w", originFullPath, err) } - } else if consts.IsDebugMode { - // 文件已存在,跳过下载 - log.Printf("file %s already exists", originFullPath) - } - - var finalFullPath string // 存放最后读取并发送的文件完整目录 .cache/sticker/setName/stickerFileName.webp - - // 如果贴纸类型不是视频和矢量,进行转换 - if !opts.Update.Message.Sticker.IsVideo && !opts.Update.Message.Sticker.IsAnimated { - _, err = os.Stat(toPNGFullPath) // 使用目录提前检查一下是否已经转换过 - // 如果提示不存在,进行转换 + if err != nil { + // 如果文件不存在,进行下载,否则返回错误 if os.IsNotExist(err) { - // 日志提示该文件没转换,正在转换 - if consts.IsDebugMode { log.Printf("file [%s] does not exist, converting", toPNGFullPath) } + // 日志提示该文件没被缓存,正在下载 + logger.Debug(). + Str("originFullPath", originFullPath). + Msg("Sticker file not cached, downloading") + + // 从服务器获取文件信息 + fileinfo, err := opts.Thebot.GetFile(opts.Ctx, &bot.GetFileParams{ FileID: opts.Update.Message.Sticker.FileID }) + if err != nil { + logger.Error(). + Err(err). + Str("fileID", opts.Update.Message.Sticker.FileID). + Msg("Failed to get sticker file info") + return nil, fmt.Errorf("failed to get sticker file [%s] info: %w", opts.Update.Message.Sticker.FileID, err) + } + + // 组合链接下载贴纸源文件 + resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", configs.BotConfig.BotToken, fileinfo.FilePath)) + if err != nil { + logger.Error(). + Err(err). + Str("filePath", fileinfo.FilePath). + Msg("Failed to download sticker file") + return nil, fmt.Errorf("failed to download sticker file [%s]: %w", fileinfo.FilePath, err) + } + defer resp.Body.Close() // 创建保存贴纸的目录 - err = os.MkdirAll(filePathPNG, 0755) - if err != nil { return nil, fmt.Errorf("error creating directory %s: %w", filePathPNG, err) } + err = os.MkdirAll(filePath, 0755) + if err != nil { + logger.Error(). + Err(err). + Str("filePath", filePath). + Msg("Failed to create sticker directory to save sticker") + return nil, fmt.Errorf("failed to create directory [%s] to save sticker: %w", filePath, err) + } - // 读取原贴纸文件,转码后存储到 png 格式贴纸的完整目录 - err = ConvertWebPToPNG(originFullPath, toPNGFullPath) - if err != nil { return nil, fmt.Errorf("error converting webp to png %s: %w", originFullPath, err) } - } else if consts.IsDebugMode { - // 文件存在,跳过转换 - log.Printf("file [%s] already converted", toPNGFullPath) + // 创建贴纸空文件 + downloadedSticker, err := os.Create(originFullPath) + if err != nil { + logger.Error(). + Err(err). + Str("originFullPath", originFullPath). + Msg("Failed to create sticker file") + return nil, fmt.Errorf("failed to create sticker file [%s]: %w", originFullPath, err) + } + defer downloadedSticker.Close() + + // 将下载的原贴纸写入空文件 + _, err = io.Copy(downloadedSticker, resp.Body) + if err != nil { + logger.Error(). + Err(err). + Str("originFullPath", originFullPath). + Msg("Failed to writing sticker data to file") + return nil, fmt.Errorf("failed to writing sticker data to file [%s]: %w", originFullPath, err) + } + } else { + logger.Error(). + Err(err). + Str("originFullPath", originFullPath). + Msg("Failed to read cached sticker file info") + return nil, fmt.Errorf("failed to read cached sticker file [%s] info: %w", originFullPath, err) } - // 处理完成,将最后要读取的目录设为转码后 png 格式贴纸的完整目录 - finalFullPath = toPNGFullPath } else { + // 文件已存在,跳过下载 + logger.Debug(). + Str("originFullPath", originFullPath). + Msg("Sticker file already cached") + } + + if opts.Update.Message.Sticker.IsAnimated { + // tgs // 不需要转码,直接读取原贴纸文件 finalFullPath = originFullPath + } else if opts.Update.Message.Sticker.IsVideo { + if configs.BotConfig.FFmpegPath != "" { + // webm, convert to gif + var GIFFilePath string = filepath.Join(StickerCacheGIF_path, stickerSetNamePrivate) // 转码后为 png 格式的目录 .cache/sticker_png/setName/ + var toGIFFullPath string = filepath.Join(GIFFilePath, stickerFileNameWithDot + "gif") // 转码后到 png 格式贴纸的完整目录 .cache/sticker_png/setName/stickerFileName.png + + _, err = os.Stat(toGIFFullPath) // 使用目录提前检查一下是否已经转换过 + if err != nil { + // 如果提示不存在,进行转换 + if os.IsNotExist(err) { + // 日志提示该文件没转换,正在转换 + logger.Debug(). + Str("toGIFFullPath", toGIFFullPath). + Msg("Sticker file does not convert, converting") + + // 创建保存贴纸的目录 + err = os.MkdirAll(GIFFilePath, 0755) + if err != nil { + logger.Error(). + Err(err). + Str("GIFFilePath", GIFFilePath). + Msg("Failed to create directory to convert file") + return nil, fmt.Errorf("failed to create directory [%s] to convert sticker file: %w", GIFFilePath, err) + } + + // 读取原贴纸文件,转码后存储到 png 格式贴纸的完整目录 + err = convertWebmToGif(originFullPath, toGIFFullPath) + if err != nil { + logger.Error(). + Err(err). + Str("originFullPath", originFullPath). + Msg("Failed to convert webm to gif") + return nil, fmt.Errorf("failed to convert webm [%s] to gif: %w", originFullPath, err) + } + } else { + // 其他错误 + logger.Error(). + Err(err). + Str("toGIFFullPath", toGIFFullPath). + Msg("Failed to read converted file info") + return nil, fmt.Errorf("failed to read converted sticker file [%s] info: %w", toGIFFullPath, err) + } + } else { + // 文件存在,跳过转换 + logger.Debug(). + Str("toGIFFullPath", toGIFFullPath). + Msg("Sticker file already converted to gif") + } + + // 处理完成,将最后要读取的目录设为转码后 gif 格式贴纸的完整目录 + data.IsConverted = true + finalFullPath = toGIFFullPath + } else { + // 没有 ffmpeg 能用来转码,直接读取原贴纸文件 + finalFullPath = originFullPath + } + } else { + // webp, need convert to png + var PNGFilePath string = filepath.Join(StickerCachePNG_path, stickerSetNamePrivate) // 转码后为 png 格式的目录 .cache/sticker_png/setName/ + var toPNGFullPath string = filepath.Join(PNGFilePath, stickerFileNameWithDot + "png") // 转码后到 png 格式贴纸的完整目录 .cache/sticker_png/setName/stickerFileName.png + + _, err = os.Stat(toPNGFullPath) // 使用目录提前检查一下是否已经转换过 + if err != nil { + // 如果提示不存在,进行转换 + if os.IsNotExist(err) { + // 日志提示该文件没转换,正在转换 + logger.Debug(). + Str("toPNGFullPath", toPNGFullPath). + Msg("Sticker file does not convert, converting") + + // 创建保存贴纸的目录 + err = os.MkdirAll(PNGFilePath, 0755) + if err != nil { + logger.Error(). + Err(err). + Str("PNGFilePath", PNGFilePath). + Msg("Failed to create directory to convert sticker") + return nil, fmt.Errorf("failed to create directory [%s] to convert sticker: %w", PNGFilePath, err) + } + + // 读取原贴纸文件,转码后存储到 png 格式贴纸的完整目录 + err = convertWebPToPNG(originFullPath, toPNGFullPath) + if err != nil { + logger.Error(). + Err(err). + Str("originFullPath", originFullPath). + Msg("Failed to convert webp to png") + return nil, fmt.Errorf("failed to convert webp [%s] to png: %w", originFullPath, err) + } + } else { + // 其他错误 + logger.Error(). + Err(err). + Str("toPNGFullPath", toPNGFullPath). + Msg("Failed to read converted sticker file info") + return nil, fmt.Errorf("failed to read converted png sticker file [%s] info : %w", toPNGFullPath, err) + } + } else { + // 文件存在,跳过转换 + logger.Debug(). + Str("toPNGFullPath", toPNGFullPath). + Msg("Sticker file already converted to png") + } + + // 处理完成,将最后要读取的目录设为转码后 png 格式贴纸的完整目录 + data.IsConverted = true + finalFullPath = toPNGFullPath } // 逻辑完成,读取最后的文件,返回给上一级函数 data.Data, err = os.Open(finalFullPath) - if err != nil { return nil, fmt.Errorf("error opening file %s: %w", finalFullPath, err) } - - if data.IsCustomSticker { log.Printf("sticker [%s] is downloaded", finalFullPath) } - - return &data, nil -} - -func getStickerPack(opts *handler_structs.SubHandlerParams, stickerSet *models.StickerSet, isOnlyPNG bool) (*stickerDatas, error) { - var data stickerDatas = stickerDatas{ - IsCustomSticker: false, - StickerSetName: stickerSet.Name, - StickerSetTitle: stickerSet.Title, - } - - filePath := StickerCache_path + stickerSet.Name + "/" - filePathPNG := StickerCachePNG_path + stickerSet.Name + "/" - - var allCached bool = true - var allConverted bool = true - - var stickerCount_webm int - var stickerCount_tgs int - var stickerCount_webp int - - for i, sticker := range stickerSet.Stickers { - stickerfileName := fmt.Sprintf("%s %d %s.", sticker.SetName, i, sticker.FileID) - var fileSuffix string - - // 根据贴纸类型设置文件扩展名和统计贴纸数量 - if sticker.IsVideo { - fileSuffix = "webm" - stickerCount_webm++ - } else if sticker.IsAnimated { - fileSuffix = "tgs" - stickerCount_tgs++ - } else { - fileSuffix = "webp" - stickerCount_webp++ - } - - var originFullPath string = filePath + stickerfileName + fileSuffix - var toPNGFullPath string = filePathPNG + stickerfileName + "png" - - _, err := os.Stat(originFullPath) // 检查单个贴纸是否已缓存 - if os.IsNotExist(err) { - allCached = false - - // 从服务器获取文件内容 - fileinfo, err := opts.Thebot.GetFile(opts.Ctx, &bot.GetFileParams{ FileID: sticker.FileID }) - if err != nil { return nil, fmt.Errorf("error getting file info %s: %v", sticker.FileID, err) } - - if consts.IsDebugMode { - log.Printf("file [%s] does not exist, downloading %s", originFullPath, fileinfo.FilePath) - } - - // 下载贴纸文件 - resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", consts.BotToken, fileinfo.FilePath)) - if err != nil { return nil, fmt.Errorf("error downloading file %s: %v", fileinfo.FilePath, err) } - defer resp.Body.Close() - - err = os.MkdirAll(filePath, 0755) - if err != nil { return nil, fmt.Errorf("error creating directory %s: %w", filePath, err) } - - // 创建文件并保存 - downloadedSticker, err := os.Create(originFullPath) - if err != nil { return nil, fmt.Errorf("error creating file %s: %w", originFullPath, err) } - defer downloadedSticker.Close() - - // 将下载的内容写入文件 - _, err = io.Copy(downloadedSticker, resp.Body) - if err != nil { return nil, fmt.Errorf("error writing to file %s: %w", originFullPath, err) } - } else if consts.IsDebugMode { - // 存在跳过下载过程 - log.Printf("file [%s] already exists", originFullPath) - } - - // 仅需要 PNG 格式时进行转换 - if isOnlyPNG && !sticker.IsVideo && !sticker.IsAnimated { - _, err = os.Stat(toPNGFullPath) - if os.IsNotExist(err) { - allConverted = false - if consts.IsDebugMode { - log.Printf("file [%s] does not exist, converting", toPNGFullPath) - } - // 创建保存贴纸的目录 - err = os.MkdirAll(filePathPNG, 0755) - if err != nil { - return nil, fmt.Errorf("error creating directory %s: %w", filePathPNG, err) - } - // 将 webp 转换为 png - err = ConvertWebPToPNG(originFullPath, toPNGFullPath) - if err != nil { - return nil, fmt.Errorf("error converting webp to png %s: %w", originFullPath, err) - } - } else if consts.IsDebugMode { - log.Printf("file [%s] already converted", toPNGFullPath) - } - } - } - - var zipFileName string - var compressFolderPath string - - var isZiped bool = true - - // 根据要下载的类型设置压缩包的文件名和路径以及压缩包中的贴纸数量 - if isOnlyPNG { - if stickerCount_webp == 0 { - return nil, fmt.Errorf("there are no static stickers in the sticker pack") - } - data.StickerCount = stickerCount_webp - zipFileName = fmt.Sprintf("%s(%d)_png.zip", stickerSet.Name, data.StickerCount) - compressFolderPath = filePathPNG - } else { - data.StickerCount = stickerCount_webp + stickerCount_webm + stickerCount_tgs - zipFileName = fmt.Sprintf("%s(%d).zip", stickerSet.Name, data.StickerCount) - compressFolderPath = filePath - } - - _, err := os.Stat(StickerCacheZip_path + zipFileName) // 检查压缩包文件是否存在 - if os.IsNotExist(err) { - isZiped = false - err = os.MkdirAll(StickerCacheZip_path, 0755) - if err != nil { - return nil, fmt.Errorf("error creating directory %s: %w", StickerCacheZip_path, err) - } - err = zipFolder(compressFolderPath, StickerCacheZip_path + zipFileName) - if err != nil { - return nil, fmt.Errorf("error zipping folder %s: %w", compressFolderPath, err) - } else if consts.IsDebugMode { - log.Println("successfully zipped folder", StickerCacheZip_path + zipFileName) - } - } else if consts.IsDebugMode { - log.Println("zip file already exists", StickerCacheZip_path + zipFileName) - } - - // 读取压缩后的贴纸包 - data.Data, err = os.Open(StickerCacheZip_path + zipFileName) if err != nil { - return nil, fmt.Errorf("error opening zip file %s: %w", StickerCacheZip_path + zipFileName, err) - } - - if isZiped { // 存在已经完成压缩的贴纸包 - log.Printf("sticker pack \"%s\"[%s](%d) is already zipped", stickerSet.Title, stickerSet.Name, data.StickerCount) - } else if isOnlyPNG && allConverted { // 仅需要 PNG 格式,且贴纸包完全转换成 PNG 格式,但尚未压缩 - log.Printf("sticker pack \"%s\"[%s](%d) is already converted", stickerSet.Title, stickerSet.Name, data.StickerCount) - } else if allCached { // 贴纸包中的贴纸已经全部缓存了 - log.Printf("sticker pack \"%s\"[%s](%d) is already cached", stickerSet.Title, stickerSet.Name, data.StickerCount) - } else { // 新下载的贴纸包(如果有部分已经下载了也是这个) - log.Printf("sticker pack \"%s\"[%s](%d) is downloaded", stickerSet.Title, stickerSet.Name, data.StickerCount) + logger.Error(). + Err(err). + Str("finalFullPath", finalFullPath). + Msg("Failed to open downloaded sticker file") + return nil, fmt.Errorf("failed to open downloaded sticker file [%s]: %w", finalFullPath, err) } return &data, nil } -func ConvertWebPToPNG(webpPath, pngPath string) error { - // 打开 WebP 文件 - webpFile, err := os.Open(webpPath) - if err != nil { - return fmt.Errorf("打开 WebP 文件失败: %v", err) - } - defer webpFile.Close() +func DownloadStickerPackCallBackHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "DownloadStickerPackCallBackHandler"). + Logger() - // 解码 WebP 图片 - img, err := webp.Decode(webpFile) - if err != nil { - return fmt.Errorf("解码 WebP 失败: %v", err) - } + var handlerErr multe.MultiError - // 创建 PNG 文件 - pngFile, err := os.Create(pngPath) - if err != nil { - return fmt.Errorf("创建 PNG 文件失败: %v", err) - } - defer pngFile.Close() - - // 编码 PNG - err = png.Encode(pngFile, img) - if err != nil { - return fmt.Errorf("编码 PNG 失败: %v", err) - } - - return nil -} - -func zipFolder(srcDir, zipFile string) error { - // 创建 ZIP 文件 - outFile, err := os.Create(zipFile) - if err != nil { - return err - } - defer outFile.Close() - - // 创建 ZIP 写入器 - zipWriter := zip.NewWriter(outFile) - defer zipWriter.Close() - - // 遍历文件夹并添加文件到 ZIP - err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // 计算文件在 ZIP 中的相对路径 - relPath, err := filepath.Rel(srcDir, path) - if err != nil { - return err - } - - // 如果是目录,则跳过 - if info.IsDir() { - return nil - } - - // 打开文件 - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - - // 创建 ZIP 内的文件 - zipFileWriter, err := zipWriter.Create(relPath) - if err != nil { - return err - } - - // 复制文件内容到 ZIP - _, err = io.Copy(zipFileWriter, file) - return err - }) - - return err -} - -func EchoStickerHandler(opts *handler_structs.SubHandlerParams) { - if opts.Update.Message == nil && opts.Update.CallbackQuery != nil && strings.HasPrefix(opts.Update.CallbackQuery.Data, "HBMT_") && opts.Update.CallbackQuery.Message.Message != nil && opts.Update.CallbackQuery.Message.Message.ReplyToMessage != nil { - opts.Update.Message = opts.Update.CallbackQuery.Message.Message.ReplyToMessage - } - - // 下载 webp 格式的贴纸 - if consts.IsDebugMode { - fmt.Println(opts.Update.Message.Sticker) - } - - database.IncrementalUsageCount(opts.Ctx, opts.Update.Message.Chat.ID, db_struct.StickerDownloaded) - - stickerData, err := EchoSticker(opts) - if err != nil { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: fmt.Sprintf("下载贴纸时发生了一些错误\n
Error downloading sticker: %s
", err), - ParseMode: models.ParseModeHTML, - }) - } - - if stickerData == nil || stickerData.Data == nil { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "未能获取到贴纸", - ParseMode: models.ParseModeMarkdownV1, - }) - return - } - - documentParams := &bot.SendDocumentParams{ - ChatID: opts.Update.Message.Chat.ID, - ParseMode: models.ParseModeHTML, - ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID}, - } - - var stickerFilePrefix, stickerFileSuffix string - - if opts.Update.Message.Sticker.IsVideo { - documentParams.Caption = "
see wikipedia/WebM
" - stickerFileSuffix = "webm" - } else if opts.Update.Message.Sticker.IsAnimated { - documentParams.Caption = "
see stickers/animated-stickers
" - stickerFileSuffix = "tgs.file" - } else { - stickerFileSuffix = "png" - } - - if stickerData.IsCustomSticker { - stickerFilePrefix = "sticker" - } else { - stickerFilePrefix = fmt.Sprintf("%s_%d", stickerData.StickerSetName, stickerData.StickerIndex) - // 仅在不为自定义贴纸时显示下载整个贴纸包按钮 - documentParams.Caption += fmt.Sprintf("%s 贴纸包中一共有 %d 个贴纸\n", stickerData.StickerSetName, stickerData.StickerSetTitle, stickerData.StickerCount) - documentParams.ReplyMarkup = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{ - { - {Text: "下载贴纸包中的静态贴纸", CallbackData: fmt.Sprintf("S_%s", opts.Update.Message.Sticker.SetName)}, - }, - { - {Text: "下载整个贴纸包(不转换格式)", CallbackData: fmt.Sprintf("s_%s", opts.Update.Message.Sticker.SetName)}, - }, - }} - } - - documentParams.Document = &models.InputFileUpload{Filename: fmt.Sprintf("%s.%s", stickerFilePrefix, stickerFileSuffix), Data: stickerData.Data} - - opts.Thebot.SendDocument(opts.Ctx, documentParams) -} - -func DownloadStickerPackCallBackHandler(opts *handler_structs.SubHandlerParams) { - botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, Text: "已请求下载,请稍候", ParseMode: models.ParseModeMarkdownV1, + DisableNotification: true, }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "start download stickerset"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `start download stickerset` message: %w", err) + } - database.IncrementalUsageCount(opts.Ctx, opts.Update.CallbackQuery.Message.Message.Chat.ID, db_struct.StickerSetDownloaded) + err = database.IncrementalUsageCount(opts.Ctx, opts.Update.CallbackQuery.Message.Message.Chat.ID, db_struct.StickerSetDownloaded) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to incremental sticker set download count") + handlerErr.Addf("failed to incremental sticker set download count: %w", err) + } - var packName string + var packName string var isOnlyPNG bool if opts.Update.CallbackQuery.Data[0:2] == "S_" { packName = strings.TrimPrefix(opts.Update.CallbackQuery.Data, "S_") @@ -506,51 +528,718 @@ func DownloadStickerPackCallBackHandler(opts *handler_structs.SubHandlerParams) // 通过贴纸的 packName 获取贴纸集 stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{ Name: packName }) if err != nil { - log.Printf("error getting sticker set: %v", err) - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Failed to get sticker set info") + handlerErr.Addf("Failed to get sticker set info: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.CallbackQuery.From.ID, - Text: fmt.Sprintf("获取贴纸包时发生了一些错误\n
Error getting sticker set: %s
", err), + Text: fmt.Sprintf("获取贴纸包时发生了一些错误\n
Failed to get sticker set info: %s
", err), ParseMode: models.ParseModeHTML, }) - return - } - - stickerData, err := getStickerPack(opts, stickerSet, isOnlyPNG) - if err != nil { - log.Println("Error downloading sticker:", err) - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.CallbackQuery.From.ID, - Text: fmt.Sprintf("下载贴纸包时发生了一些错误\n
Error download sticker set: %s
", err), - ParseMode: models.ParseModeHTML, - }) - } - if stickerData == nil || stickerData.Data == nil { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.CallbackQuery.From.ID, - Text: "未能获取到压缩包", - ParseMode: models.ParseModeMarkdownV1, - }) - return - } - - documentParams := &bot.SendDocumentParams{ - ChatID: opts.Update.CallbackQuery.From.ID, - ParseMode: models.ParseModeMarkdownV1, - } - - if isOnlyPNG { - documentParams.Caption = fmt.Sprintf("[%s](https://t.me/addstickers/%s) 已下载\n包含 %d 个贴纸(仅转换后的 PNG 格式)", stickerData.StickerSetTitle, stickerData.StickerSetName, stickerData.StickerCount) - documentParams.Document = &models.InputFileUpload{Filename: fmt.Sprintf("%s(%d)_png.zip", stickerData.StickerSetName, stickerData.StickerCount), Data: stickerData.Data} + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "get sticker set info error"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `get sticker set info error` message: %w", err) + } } else { - documentParams.Caption = fmt.Sprintf("[%s](https://t.me/addstickers/%s) 已下载\n包含 %d 个贴纸", stickerData.StickerSetTitle, stickerData.StickerSetName, stickerData.StickerCount) - documentParams.Document = &models.InputFileUpload{Filename: fmt.Sprintf("%s(%d).zip", stickerData.StickerSetName, stickerData.StickerCount), Data: stickerData.Data} + logger.Info(). + Dict("stickerSet", zerolog.Dict(). + Str("title", stickerSet.Title). + Str("name", stickerSet.Name). + Int("allCount", len(stickerSet.Stickers)), + ). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Start download sticker set") + + stickerData, err := getStickerPack(opts, stickerSet, isOnlyPNG) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Failed to download sticker set") + handlerErr.Addf("failed to download sticker set: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.CallbackQuery.From.ID, + Text: fmt.Sprintf("下载贴纸包时发生了一些错误\n
Failed to download sticker set: %s
", err), + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "download sticker set error"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `download sticker set error` message: %w", err) + } + } else { + documentParams := &bot.SendDocumentParams{ + ChatID: opts.Update.CallbackQuery.From.ID, + ParseMode: models.ParseModeMarkdownV1, + } + + if isOnlyPNG { + documentParams.Caption = fmt.Sprintf("[%s](https://t.me/addstickers/%s) 已下载\n包含 %d 个贴纸(仅转换后的 PNG 格式)", stickerData.StickerSetTitle, stickerData.StickerSetName, stickerData.StickerCount) + documentParams.Document = &models.InputFileUpload{Filename: fmt.Sprintf("%s(%d)_png.zip", stickerData.StickerSetName, stickerData.StickerCount), Data: stickerData.Data} + } else { + documentParams.Caption = fmt.Sprintf("[%s](https://t.me/addstickers/%s) 已下载\n包含 %d 个贴纸", stickerData.StickerSetTitle, stickerData.StickerSetName, stickerData.StickerCount) + documentParams.Document = &models.InputFileUpload{Filename: fmt.Sprintf("%s(%d).zip", stickerData.StickerSetName, stickerData.StickerCount), Data: stickerData.Data} + } + + _, err = opts.Thebot.SendDocument(opts.Ctx, documentParams) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "sticker set zip file"). + Msg(errt.SendDocument) + handlerErr.Addf("failed to send sticker set zip file: %w", err) + } + + _, err = opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: botMessage.ID, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "start download stickerset notice"). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `start download sticker set notice` message: %w", err) + } + } } - opts.Thebot.SendDocument(opts.Ctx, documentParams) + return handlerErr.Flat() +} - opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: botMessage.ID, +func getStickerPack(opts *handler_structs.SubHandlerParams, stickerSet *models.StickerSet, isOnlyPNG bool) (*stickerDatas, error) { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "getStickerPack"). + Logger() + + var data stickerDatas = stickerDatas{ + IsCustomSticker: false, + StickerSetName: stickerSet.Name, + StickerSetTitle: stickerSet.Title, + } + + logger.Debug(). + Dict("stickerSet", zerolog.Dict(). + Str("title", data.StickerSetTitle). + Str("name", data.StickerSetName). + Int("allCount", len(stickerSet.Stickers)), + ). + Msg("Start download sticker set") + + filePath := filepath.Join(StickerCache_path, stickerSet.Name) + PNGFilePath := filepath.Join(StickerCachePNG_path, stickerSet.Name) + + var allCached bool = true + var allConverted bool = true + + for i, sticker := range stickerSet.Stickers { + stickerfileName := fmt.Sprintf("%s %d %s.", sticker.SetName, i, sticker.FileID) + var fileSuffix string + + // 根据贴纸类型设置文件扩展名和统计贴纸数量 + if sticker.IsVideo { + fileSuffix = "webm" + data.WebM++ + } else if sticker.IsAnimated { + fileSuffix = "tgs" + data.tgs++ + } else { + fileSuffix = "webp" + data.WebP++ + } + + var originFullPath string = filepath.Join(filePath, stickerfileName + fileSuffix) + + _, err := os.Stat(originFullPath) // 检查单个贴纸是否已缓存 + if err != nil { + if os.IsNotExist(err) { + allCached = false + logger.Trace(). + Str("originFullPath", originFullPath). + Str("stickerSetName", data.StickerSetName). + Int("stickerIndex", i). + Msg("Sticker file not cached, downloading") + + // 从服务器获取文件内容 + fileinfo, err := opts.Thebot.GetFile(opts.Ctx, &bot.GetFileParams{ FileID: sticker.FileID }) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("fileID", opts.Update.Message.Sticker.FileID). + Msg("Failed to get sticker file info") + return nil, fmt.Errorf("failed to get file info %s: %w", sticker.FileID, err) + } + + // 下载贴纸文件 + resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", configs.BotConfig.BotToken, fileinfo.FilePath)) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("filePath", fileinfo.FilePath). + Msg("Failed to download sticker file") + return nil, fmt.Errorf("failed to download sticker file %s: %w", fileinfo.FilePath, err) + } + defer resp.Body.Close() + + err = os.MkdirAll(filePath, 0755) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("filePath", filePath). + Msg("Failed to creat directory to save sticker") + return nil, fmt.Errorf("failed to create directory [%s] to save sticker: %w", filePath, err) + } + + // 创建文件并保存 + downloadedSticker, err := os.Create(originFullPath) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("originFullPath", originFullPath). + Msg("Failed to create sticker file") + return nil, fmt.Errorf("failed to create sticker file [%s]: %w", originFullPath, err) + } + defer downloadedSticker.Close() + + // 将下载的内容写入文件 + _, err = io.Copy(downloadedSticker, resp.Body) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("originFullPath", originFullPath). + Msg("Failed to writing sticker data to file") + return nil, fmt.Errorf("failed to writing sticker data to file [%s]: %w", originFullPath, err) + } + } else { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("originFullPath", originFullPath). + Msg("Failed to read cached sticker file info") + return nil, fmt.Errorf("failed to read cached sticker file [%s] info: %w", originFullPath, err) + } + } else { + // 存在跳过下载过程 + logger.Trace(). + Int("stickerIndex", i). + Str("originFullPath", originFullPath). + Msg("Sticker file already exists") + } + + // 仅需要 PNG 格式时进行转换 + if isOnlyPNG && !sticker.IsVideo && !sticker.IsAnimated { + var toPNGFullPath string = filepath.Join(PNGFilePath, stickerfileName + "png") + + _, err = os.Stat(toPNGFullPath) + if err != nil { + if os.IsNotExist(err) { + allConverted = false + logger.Trace(). + Int("stickerIndex", i). + Str("toPNGFullPath", toPNGFullPath). + Msg("File does not convert, converting") + // 创建保存贴纸的目录 + err = os.MkdirAll(PNGFilePath, 0755) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("PNGFilePath", PNGFilePath). + Msg("Failed to create directory to convert file") + return nil, fmt.Errorf("failed to create directory [%s] to convert file: %w", PNGFilePath, err) + } + // 将 webp 转换为 png + err = convertWebPToPNG(originFullPath, toPNGFullPath) + if err != nil { + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("originFullPath", originFullPath). + Msg("Failed to convert webp to png") + return nil, fmt.Errorf("failed to converting webp to png [%s]: %w", originFullPath, err) + } + } else { + // 其他错误 + logger.Error(). + Err(err). + Int("stickerIndex", i). + Str("toPNGFullPath", toPNGFullPath). + Msg("Failed to read converted file info") + return nil, fmt.Errorf("failed to read converted file info: %w", err) + } + } else { + logger.Trace(). + Str("toPNGFullPath", toPNGFullPath). + Msg("File already converted") + } + } + } + + var zipFileName string + var compressFolderPath string + + var isZiped bool = true + + // 根据要下载的类型设置压缩包的文件名和路径以及压缩包中的贴纸数量 + if isOnlyPNG { + if data.WebP == 0 { + logger.Warn(). + Dict("stickerSet", zerolog.Dict(). + Str("stickerSetName", stickerSet.Name). + Int("WebP", data.WebP). + Int("tgs", data.tgs). + Int("WebM", data.WebM), + ). + Msg("There are no static stickers in the sticker set") + return nil, fmt.Errorf("there are no static stickers in the sticker set [%s]", stickerSet.Name) + } + data.StickerCount = data.WebP + zipFileName = fmt.Sprintf("%s(%d)_png.zip", stickerSet.Name, data.StickerCount) + compressFolderPath = PNGFilePath + } else { + data.StickerCount = data.WebP + data.WebM + data.tgs + zipFileName = fmt.Sprintf("%s(%d).zip", stickerSet.Name, data.StickerCount) + compressFolderPath = filePath + } + + var zipFileFullPath string = filepath.Join(StickerCacheZip_path, zipFileName) + + _, err := os.Stat(zipFileFullPath) // 检查压缩包文件是否存在 + if err != nil { + if os.IsNotExist(err) { + isZiped = false + err = os.MkdirAll(StickerCacheZip_path, 0755) + if err != nil { + logger.Error(). + Err(err). + Str("StickerCacheZip_path", StickerCacheZip_path). + Msg("Failed to create zip file directory") + return nil, fmt.Errorf("failed to create zip file directory [%s]: %w", StickerCacheZip_path, err) + } + err = zipFolder(compressFolderPath, zipFileFullPath) + if err != nil { + logger.Error(). + Err(err). + Str("compressFolderPath", compressFolderPath). + Msg("Failed to compress sticker folder") + return nil, fmt.Errorf("failed to compress sticker folder [%s]: %w", compressFolderPath, err) + } + logger.Debug(). + Str("compressFolderPath", compressFolderPath). + Str("zipFileFullPath", zipFileFullPath). + Msg("Compress sticker folder successfully") + } else { + logger.Error(). + Err(err). + Str("zipFileFullPath", zipFileFullPath). + Msg("Failed to read compressed sticker set zip file info") + return nil, fmt.Errorf("failed to read compressed sticker set zip file [%s] info: %w", zipFileFullPath, err) + } + } else { + logger.Debug(). + Str("zipFileFullPath", zipFileFullPath). + Msg("sticker set zip file already compressed") + } + + // 读取压缩后的贴纸包 + data.Data, err = os.Open(zipFileFullPath) + if err != nil { + logger.Error(). + Err(err). + Str("zipFileFullPath", zipFileFullPath). + Msg("Failed to open compressed sticker set zip file") + return nil, fmt.Errorf("failed to open compressed sticker set zip file [%s]: %w", zipFileFullPath, err) + } + + if isZiped { + // 存在已经完成压缩的贴纸包(原始格式或已转换) + logger.Info(). + Str("zipFileFullPath", zipFileFullPath). + Dict("stickerSet", zerolog.Dict(). + Str("title", data.StickerSetTitle). + Str("name", data.StickerSetName). + Int("count", data.StickerCount), + ). + Msg("Sticker set already zipped") + } else if isOnlyPNG && allConverted { + // 仅需要 PNG 格式,且贴纸包完全转换成 PNG 格式,但尚未压缩 + logger.Info(). + Str("zipFileFullPath", zipFileFullPath). + Dict("stickerSet", zerolog.Dict(). + Str("title", data.StickerSetTitle). + Str("name", data.StickerSetName). + Int("count", data.StickerCount), + ). + Msg("Sticker set already converted") + } else if allCached { + // 贴纸包中的贴纸已经全部缓存了 + logger.Info(). + Str("zipFileFullPath", zipFileFullPath). + Dict("stickerSet", zerolog.Dict(). + Str("title", data.StickerSetTitle). + Str("name", data.StickerSetName). + Int("count", data.StickerCount), + ). + Msg("Sticker set already cached") + } else { + // 新下载的贴纸包(如果有部分已经下载了也是这个) + logger.Info(). + Str("zipFileFullPath", zipFileFullPath). + Dict("stickerSet", zerolog.Dict(). + Str("title", data.StickerSetTitle). + Str("name", data.StickerSetName). + Int("count", data.StickerCount), + ). + Msg("Sticker set already downloaded") + } + + return &data, nil +} + +func convertWebPToPNG(webpPath, pngPath string) error { + // 打开 WebP 文件 + webpFile, err := os.Open(webpPath) + if err != nil { + return fmt.Errorf("打开 WebP 文件失败: %w", err) + } + defer webpFile.Close() + + // 解码 WebP 图片 + img, err := webp.Decode(webpFile) + if err != nil { + return fmt.Errorf("解码 WebP 失败: %w", err) + } + + // 创建 PNG 文件 + pngFile, err := os.Create(pngPath) + if err != nil { + return fmt.Errorf("创建 PNG 文件失败: %w", err) + } + defer pngFile.Close() + + // 编码 PNG + err = png.Encode(pngFile, img) + if err != nil { + return fmt.Errorf("编码 PNG 失败: %w", err) + } + + return nil +} + +// use ffmpeg +func convertWebmToGif(webmPath, gifPath string) error { + return exec.Command(configs.BotConfig.FFmpegPath, "-i", webmPath, gifPath).Run() +} + +func zipFolder(srcDir, zipFile string) error { + // 创建 ZIP 文件 + outFile, err := os.Create(zipFile) + if err != nil { return err } + defer outFile.Close() + + // 创建 ZIP 写入器 + zipWriter := zip.NewWriter(outFile) + defer zipWriter.Close() + + // 遍历文件夹并添加文件到 ZIP + err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { return err } + + // 计算文件在 ZIP 中的相对路径 + relPath, err := filepath.Rel(srcDir, path) + if err != nil { return err } + + // 如果是目录,则跳过 + if info.IsDir() { return nil } + + // 打开文件 + file, err := os.Open(path) + if err != nil { return err } + defer file.Close() + + // 创建 ZIP 内的文件 + zipFileWriter, err := zipWriter.Create(relPath) + if err != nil { return err } + + // 复制文件内容到 ZIP + _, err = io.Copy(zipFileWriter, file) + return err }) + return err +} + +func showCachedStickers(opts *handler_structs.SubHandlerParams) error { + var button [][]models.InlineKeyboardButton + var tempButtom []models.InlineKeyboardButton + + entries, err := os.ReadDir(StickerCache_path) + if err != nil { return err } + + for _, entry := range entries { + if entry.IsDir() && entry.Name() != "-custom" { + if len(tempButtom) == 4 { + button = append(button, tempButtom) + tempButtom = []models.InlineKeyboardButton{} + } + tempButtom = append(tempButtom, models.InlineKeyboardButton{ + Text: entry.Name(), + URL: "https://t.me/addstickers/" + entry.Name(), + }) + } + } + + if len(tempButtom) > 0 { button = append(button, tempButtom) } + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "请选择要查看的贴纸包", + ReplyMarkup: models.InlineKeyboardMarkup{ + InlineKeyboard: button, + }, + MessageEffectID: "5104841245755180586", + }) + return err +} + +func collectStickerSet(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "collectStickerSet"). + Logger() + + var handlerErr multe.MultiError + + if StickerCollectionChannelID == 0 { + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: opts.Update.CallbackQuery.ID, + Text: "未设置贴纸包收集频道", + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("callbackQuery", opts.Update.CallbackQuery.Data). + Str("content", "collect channel ID not set"). + Msg(errt.AnswerCallbackQuery) + } + } else { + stickerSetName := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "c_") + + stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{ Name: stickerSetName }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Failed to get sticker set info") + handlerErr.Addf("Failed to get sticker set info: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.CallbackQuery.From.ID, + Text: fmt.Sprintf("获取贴纸包时发生了一些错误\n
Failed to get sticker set info: %s
", err), + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "get sticker set info error"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `get sticker set info error` message: %w", err) + } + } else { + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: opts.Update.CallbackQuery.ID, + Text: "已开始下载贴纸包", + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "start downloading sticker pack notice"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("Failed to send `start downloading sticker pack notice` callback answer: %w", err) + } + stickerData, err := getStickerPack(opts, stickerSet, false) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Msg("Failed to download sticker set") + handlerErr.Addf("failed to download sticker set: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.CallbackQuery.From.ID, + Text: fmt.Sprintf("下载贴纸包时发生了一些错误\n
Failed to download sticker set: %s
", err), + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "download sticker set error"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `download sticker set error` message: %w", err) + } + } else { + var pendingMessage string = fmt.Sprintf("[%s](https://t.me/addstickers/%s)\n", stickerData.StickerSetTitle, stickerData.StickerSetName) + if stickerData.WebP > 0 { + pendingMessage += fmt.Sprintf("%d(静态) ", stickerData.WebP) + } + if stickerData.WebM > 0 { + pendingMessage += fmt.Sprintf("%d(动态) ", stickerData.WebM) + } + if stickerData.tgs > 0 { + pendingMessage += fmt.Sprintf("%d(矢量) ", stickerData.tgs) + } + _, err := opts.Thebot.SendDocument(opts.Ctx, &bot.SendDocumentParams{ + ChatID: StickerCollectionChannelID, + ParseMode: models.ParseModeMarkdownV1, + Caption: fmt.Sprintf("%s 共 %d 个贴纸\n存档时间 %s", pendingMessage, stickerData.StickerCount, time.Now().Format(time.RFC3339)), + Document: &models.InputFileUpload{Filename: fmt.Sprintf("%s(%d).zip", stickerData.StickerSetName, stickerData.StickerCount), Data: stickerData.Data}, + ReplyMarkup: &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "查看贴纸包", URL: "https://t.me/addstickers/" + stickerData.StickerSetName }, + }}}, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("channelID", StickerCollectionChannelID). + Str("stickerSetName", stickerSetName). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "collect sticker set file"). + Msg(errt.SendDocument) + handlerErr.Addf("Failed to send `collect sticker set` file: %w", err) + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.CallbackQuery.From.ID, + Text: fmt.Sprintf("将贴纸包发送到收藏频道失败:
%s
", err.Error()), + ParseMode: models.ParseModeHTML, + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("channelID", StickerCollectionChannelID). + Str("stickerSetName", stickerSetName). + Dict(utils.GetUserDict(&opts.Update.CallbackQuery.From)). + Str("content", "collect sticker set failed notice"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `collect sticker set failed notice` message: %w", err) + } + } + } + } + + } + + return handlerErr.Flat() +} + +func getStickerPackInfo(opts *handler_structs.SubHandlerParams) error { + if opts.Update.Message == nil || opts.Update.Message.Text == "" { + return nil + } + + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "StickerDownload"). + Str("funcName", "getStickerPackInfo"). + Logger() + + var handlerErr multe.MultiError + var stickerSetName string + + if strings.HasPrefix(opts.Update.Message.Text, "https://t.me/addstickers/") { + stickerSetName = strings.TrimPrefix(opts.Update.Message.Text, "https://t.me/addstickers/") + } else if strings.HasPrefix(opts.Update.Message.Text, "t.me/addstickers/") { + stickerSetName = strings.TrimPrefix(opts.Update.Message.Text, "t.me/addstickers/") + } + + if stickerSetName != "" { + stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{ Name: stickerSetName }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to get sticker set info") + handlerErr.Addf("Failed to get sticker set info: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.From.ID, + Text: fmt.Sprintf("获取贴纸包信息时发生了一些错误\n
Failed to get sticker set info: %s
", err), + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "get sticker set info error"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `get sticker set info error` message: %w", err) + } + } else { + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.From.ID, + Text: fmt.Sprintf("%s 贴纸包中一共有 %d 个贴纸\n", stickerSet.Name, stickerSet.Title, len(stickerSet.Stickers)), + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + DisableNotification: true, + ParseMode: models.ParseModeHTML, + ReplyMarkup: &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{ + { + { Text: "下载贴纸包中的静态贴纸", CallbackData: fmt.Sprintf("S_%s", stickerSet.Name) }, + }, + { + { Text: "下载整个贴纸包(不转换格式)", CallbackData: fmt.Sprintf("s_%s", stickerSet.Name) }, + }, + }}, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "sticker set info"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `sticker set info` message: %w", err) + } + } + } else { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.From.ID, + Text: "请发送一个有效的贴纸链接", + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "empty sticker link notice"). + Msg(errt.SendMessage) + handlerErr.Addf("Failed to send `empty sticker link notice` message: %w", err) + } + } + + return handlerErr.Flat() } diff --git a/plugins/plugin_teamspeak3.go b/plugins/plugin_teamspeak3.go index 2af44db..0d8d1eb 100644 --- a/plugins/plugin_teamspeak3.go +++ b/plugins/plugin_teamspeak3.go @@ -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
%s
", 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
%s
\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) + } } diff --git a/plugins/plugin_udonese.go b/plugins/plugin_udonese.go index a010271..3bc2b08 100644 --- a/plugins/plugin_udonese.go +++ b/plugins/plugin_udonese.go @@ -1,31 +1,38 @@ package plugins import ( + "context" "fmt" - "io" - "log" "os" + "path/filepath" "strconv" "strings" "time" "trbot/utils" + "trbot/utils/configs" "trbot/utils/consts" + "trbot/utils/errt" "trbot/utils/handler_structs" + "trbot/utils/multe" "trbot/utils/plugin_utils" + "trbot/utils/type/message_utils" + "trbot/utils/yaml" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" - "gopkg.in/yaml.v3" + "github.com/rs/zerolog" ) -var UdoneseData *Udonese -var UdoneseErr error +var UdoneseData Udonese +var UdoneseErr error -var Udonese_path string = consts.DB_path + "udonese/" -var UdonGroupID int64 = -1002205667779 +var UdoneseDir string = filepath.Join(consts.YAMLDataBaseDir, "udonese/") +var UdonesePath string = filepath.Join(UdoneseDir, consts.YAMLFileName) +var UdonGroupID int64 = -1002205667779 +// var UdonGroupID int64 = -1002499888124 // trbot var UdoneseManagerIDs []int64 = []int64{ - 872082796, // akaudon + 872082796, // akaudon 1086395364, // trle5 } @@ -64,7 +71,7 @@ func (list UdoneseWord) OutputMeanings() string { if len(list.MeaningList) != 0 { pendingMessage += "它的意思有\n" } else { - pendingMessage += "还没有添加任何意思\n" + pendingMessage += "它还没有添加任何意思\n" } for i, s := range list.MeaningList { // 先加意思 @@ -98,6 +105,7 @@ func (list UdoneseWord) OutputMeanings() string { return pendingMessage } +// 管理一个词和其所有意思的键盘 func (list UdoneseWord) buildUdoneseWordKeyboard() models.ReplyMarkup { var buttons [][]models.InlineKeyboardButton for index, singleMeaning := range list.MeaningList { @@ -135,61 +143,77 @@ type UdoneseMeaning struct { ViaName string `yaml:"ViaName,omitempty"` } -func ReadUdonese() { - var udonese *Udonese +func ReadUdonese(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "ReadUdonese"). + Logger() - file, err := os.Open(Udonese_path + consts.MetadataFileName) + err := yaml.LoadYAML(UdonesePath, &UdoneseData) if err != nil { - // 如果是找不到目录,新建一个 - log.Println("[Udonese]: Not found database file. Created new one") - SaveUdonese() - UdoneseData, UdoneseErr = &Udonese{}, err - return - } - defer file.Close() - - decoder := yaml.NewDecoder(file) - err = decoder.Decode(&udonese) - if err != nil { - if err == io.EOF { - log.Println("[Udonese]: Udonese list looks empty. now format it") - SaveUdonese() - UdoneseData, UdoneseErr = &Udonese{}, nil - return + if os.IsNotExist(err) { + logger.Warn(). + Err(err). + Str("path", UdonesePath). + Msg("Not found udonese list file. Created new one") + // 如果是找不到文件,新建一个 + err = yaml.SaveYAML(UdonesePath, &UdoneseData) + if err != nil { + logger.Error(). + Err(err). + Str("path", UdonesePath). + Msg("Failed to create empty udonese list file") + UdoneseErr = fmt.Errorf("failed to create empty udonese list file: %w", err) + } + } else { + logger.Error(). + Err(err). + Str("path", UdonesePath). + Msg("Failed to load udonese list file") + UdoneseErr = fmt.Errorf("failed to load udonese list file: %w", err) } - log.Println("(func)ReadUdonese:", err) - UdoneseData, UdoneseErr = &Udonese{}, err - return + } else { + UdoneseErr = nil } - UdoneseData, UdoneseErr = udonese, nil + + return UdoneseErr } -func SaveUdonese() error { - data, err := yaml.Marshal(UdoneseData) - if err != nil { return err } +func SaveUdonese(ctx context.Context) error { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "SaveUdonese"). + Logger() - if _, err := os.Stat(Udonese_path); os.IsNotExist(err) { - if err := os.MkdirAll(Udonese_path, 0755); err != nil { - return err - } + err := yaml.SaveYAML(UdonesePath, &UdoneseData) + if err != nil { + logger.Error(). + Err(err). + Str("path", UdonesePath). + Msg("Failed to save udonese list") + UdoneseErr = fmt.Errorf("failed to save udonese list: %w", err) + } else { + UdoneseErr = nil } - - if _, err := os.Stat(Udonese_path + consts.MetadataFileName); os.IsNotExist(err) { - _, err := os.Create(Udonese_path + consts.MetadataFileName) - if err != nil { - return err - } - } - - return os.WriteFile(Udonese_path + consts.MetadataFileName, data, 0644) + return UdoneseErr } // 如果要添加的意思重复,返回对应意思的单个词结构体指针,否则返回空指针 // 设计之初可以添加多个意思,但现在不推荐这样 -func addUdonese(udonese *Udonese, params *UdoneseWord) *UdoneseWord { - for wordIndex, savedList := range udonese.List { +func addUdonese(ctx context.Context, params *UdoneseWord) *UdoneseWord { + logger := zerolog.Ctx(ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "SaveUdonese"). + Logger() + + for wordIndex, savedList := range UdoneseData.List { if strings.EqualFold(savedList.Word, params.Word){ - log.Printf("发现已存在的词 [%s],正在检查是否有新增的意思", savedList.Word) + logger.Info(). + Str("word", params.Word). + Msg("Found existing word") for _, newMeaning := range params.MeaningList { var isreallynew bool = true for _, oldmeanlist := range savedList.MeaningList { @@ -198,27 +222,42 @@ func addUdonese(udonese *Udonese, params *UdoneseWord) *UdoneseWord { } } if isreallynew { - udonese.List[wordIndex].MeaningList = append(udonese.List[wordIndex].MeaningList, newMeaning) - log.Printf("正在为 [%s] 添加 [%s] 意思", udonese.List[wordIndex].Word, newMeaning.Meaning) + UdoneseData.List[wordIndex].MeaningList = append(UdoneseData.List[wordIndex].MeaningList, newMeaning) + logger.Info(). + Str("word", params.Word). + Str("meaning", newMeaning.Meaning). + Msg("Add new meaning") } else { - log.Println("存在的意思,跳过", newMeaning) + logger.Info(). + Str("word", params.Word). + Str("meaning", newMeaning.Meaning). + Msg("Skip existing meaning") return &savedList } } return nil } } - log.Printf("发现新的词 [%s],正在添加 %v", params.Word, params.MeaningList) - udonese.List = append(udonese.List, *params) - udonese.Count++ + logger.Info(). + Str("word", params.Word). + Interface("meaningList", params.MeaningList). + Msg("Add new word") + UdoneseData.List = append(UdoneseData.List, *params) + UdoneseData.Count++ return nil } -func addUdoneseHandler(opts *handler_structs.SubHandlerParams) { +func addUdoneseHandler(opts *handler_structs.SubHandlerParams) error { // 不响应来自转发的命令 - if opts.Update.Message.ForwardOrigin != nil { - return - } + if opts.Update.Message.ForwardOrigin != nil { return nil } + + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "addUdoneseHandler"). + Logger() + + var handlerErr multe.MultiError isManager := utils.AnyContains(opts.Update.Message.From.ID, UdoneseManagerIDs) @@ -231,251 +270,289 @@ func addUdoneseHandler(opts *handler_structs.SubHandlerParams) { DisableNotification: true, }) if err != nil { - log.Println("error sending /udonese not allowed group:", err) + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "/udonese not allowed group"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `/udonese not allowed group` message: %w", err) } - return - } + } else { + if len(opts.Fields) < 3 { + // 如果是管理员,则显示可以管理词的帮助 + if isManager { + if len(opts.Fields) < 2 { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + Text: "使用 `/udonese <词> <单个意思>` 来添加记录\n或使用 `/udonese <词>` 来管理记录", + ParseMode: models.ParseModeMarkdownV1, + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "/udonese admin command help"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `/udonese admin command help` message: %w", err) + } + } else /* 词信息 */ { + checkWord := opts.Fields[1] + var targetWord UdoneseWord + for _, wordlist := range UdoneseData.List { + if wordlist.Word == checkWord { + targetWord = wordlist + } + } - - if isManager && len(opts.Fields) < 3 { - if len(opts.Fields) < 2 { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - Text: "使用 `/udonese <词> <单个意思>` 来添加记录\n或使用 `/udonese <词>` 来管理记录", - ParseMode: models.ParseModeMarkdownV1, - DisableNotification: true, - }) - return - } else { - checkWord := opts.Fields[1] - var targetWord UdoneseWord - for _, wordlist := range UdoneseData.List { - if wordlist.Word == checkWord { - targetWord = wordlist + // 如果词存在,则显示词的信息 + if targetWord.Word == "" { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "似乎没有这个词呢...", + ParseMode: models.ParseModeMarkdownV1, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "/udonese admin command no this word"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `/udonese admin command no this word` message: %w", err) + } + } else { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: fmt.Sprintf("词: [ %s ]\n有 %d 个意思,已使用 %d 次\n", targetWord.Word, len(targetWord.MeaningList), targetWord.Used), + ParseMode: models.ParseModeHTML, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "/udonese manage keyboard"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `/udonese manage keyboard` message: %w", err) + } + } } - } - - if targetWord.Word == "" { + } else /* 普通用户 */ { _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, - Text: "似乎没有这个词呢...", - ParseMode: models.ParseModeMarkdownV1, ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + Text: "使用 `/udonese <词> <单个意思>` 来添加记录", + ParseMode: models.ParseModeMarkdownV1, DisableNotification: true, }) if err != nil { - log.Println("error sending /udonese word not found:", err) + logger.Info(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "/udonese command help"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `/udonese command help` message: %w", err) + } + } + } else { + meaning := strings.TrimSpace(opts.Update.Message.Text[len(opts.Fields[0]) + len(opts.Fields[1]) + 2:]) + + var ( + fromID int64 + fromUsername string + fromName string + viaID int64 + viaUsername string + viaName string + ) + + msgAttr := message_utils.GetMessageAttribute(opts.Update.Message) + + if msgAttr.IsReplyToMessage { + replyAttr := message_utils.GetMessageAttribute(opts.Update.Message.ReplyToMessage) + + if replyAttr.IsUserAsChannel || replyAttr.IsFromLinkedChannel || replyAttr.IsFromAnonymous { + // 回复给一条频道身份的信息 + fromID = opts.Update.Message.ReplyToMessage.SenderChat.ID + fromUsername = opts.Update.Message.ReplyToMessage.SenderChat.Username + fromName = utils.ShowChatName(opts.Update.Message.ReplyToMessage.SenderChat) + } else { + // 回复给普通用户 + fromName = utils.ShowUserName(opts.Update.Message.ReplyToMessage.From) + fromID = opts.Update.Message.ReplyToMessage.From.ID + } + if msgAttr.IsUserAsChannel || msgAttr.IsFromLinkedChannel || msgAttr.IsFromAnonymous { + // 频道身份 + viaID = opts.Update.Message.SenderChat.ID + viaUsername = opts.Update.Message.SenderChat.Username + viaName = utils.ShowChatName(opts.Update.Message.SenderChat) + } else { + // 普通用户身份 + viaID = opts.Update.Message.From.ID + viaName = utils.ShowUserName(opts.Update.Message.From) + } + } else { + if msgAttr.IsUserAsChannel || msgAttr.IsFromAnonymous { + // 频道身份 + fromID = opts.Update.Message.SenderChat.ID + fromUsername = opts.Update.Message.SenderChat.Username + fromName = utils.ShowChatName(opts.Update.Message.SenderChat) + } else { + // 普通用户身份 + fromID = opts.Update.Message.From.ID + fromName = utils.ShowUserName(opts.Update.Message.From) } - return } - var pendingMessage string = fmt.Sprintf("词: [ %s ]\n有 %d 个意思\n", targetWord.Word, len(targetWord.MeaningList)) + // 来源和经过都是同一位用户,删除 via 信息 + if fromID == viaID { + viaID = 0 + viaUsername = "" + viaName = "" + } - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, + var pendingMessage string + var err error + + oldMeaning := addUdonese(opts.Ctx, &UdoneseWord{ + Word: opts.Fields[1], + MeaningList: []UdoneseMeaning{{ + Meaning: meaning, + FromID: fromID, + FromUsername: fromUsername, + FromName: fromName, + ViaID: viaID, + ViaUsername: viaUsername, + ViaName: viaName, + }}, + }) + if oldMeaning != nil { + pendingMessage += fmt.Sprintf("[%s] 意思已存在于 [%s] 中:\n", meaning, oldMeaning.Word) + for i, s := range oldMeaning.MeaningList { + if meaning == s.Meaning { + pendingMessage += fmt.Sprintf("%d. [%s] ", i + 1, s.Meaning) + + // 来源的用户或频道 + if s.FromUsername != "" { + pendingMessage += fmt.Sprintf("From %s ", s.FromUsername, s.FromName) + } else if s.FromID != 0 { + if s.FromID < 0 { + pendingMessage += fmt.Sprintf("From %s ", utils.RemoveIDPrefix(s.FromID), s.FromName) + } else { + pendingMessage += fmt.Sprintf("From %s ", s.FromID, s.FromName) + } + } + + // 由其他用户添加时的信息 + if s.ViaUsername != "" { + pendingMessage += fmt.Sprintf("Via %s ", s.ViaUsername, s.ViaName) + } else if s.ViaID != 0 { + if s.ViaID < 0 { + pendingMessage += fmt.Sprintf("Via %s ", utils.RemoveIDPrefix(s.ViaID), s.ViaName) + } else { + pendingMessage += fmt.Sprintf("Via %s ", s.ViaID, s.ViaName) + } + } + + // 末尾换行 + pendingMessage += "\n" + } + } + } else { + err = SaveUdonese(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Str("messageText", opts.Update.Message.Text). + Msg("Failed to save udonese list after add word") + handlerErr.Addf("failed to save udonese list after add word: %w", err) + + pendingMessage += fmt.Sprintf("保存语句时似乎发生了一些错误:
%s
", err.Error()) + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: pendingMessage, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Str("messageText", opts.Update.Message.Text). + Str("content", "failed save udonese list notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `failed to save udonese list notice` message: %w", err) + } + return handlerErr.Flat() + } else { + pendingMessage += fmt.Sprintf("已添加 [%s]\n", opts.Fields[1]) + pendingMessage += fmt.Sprintf("[%s] ", meaning) + + // 来源的用户或频道 + if fromUsername != "" { + pendingMessage += fmt.Sprintf("From %s ", fromUsername, fromName) + } else if fromID != 0 { + if fromID < 0 { + pendingMessage += fmt.Sprintf("From %s ", utils.RemoveIDPrefix(fromID), fromName) + } else { + pendingMessage += fmt.Sprintf("From %s ", fromID, fromName) + } + } + + // 由其他用户添加时的信息 + if viaUsername != "" { + pendingMessage += fmt.Sprintf("Via %s ", viaUsername, viaName) + } else if viaID != 0 { + if viaID < 0 { + pendingMessage += fmt.Sprintf("Via %s ", utils.RemoveIDPrefix(viaID), viaName) + } else { + pendingMessage += fmt.Sprintf("Via %s ", viaID, viaName) + } + } + } + } + + pendingMessage += fmt.Sprintln("
发送的消息与此消息将在十秒后删除
") + botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: pendingMessage, ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), + ParseMode: models.ParseModeHTML, DisableNotification: true, }) - return - } - } else if len(opts.Fields) < 3 { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - Text: "使用 `/udonese <词> <单个意思>` 来添加记录", - ParseMode: models.ParseModeMarkdownV1, - DisableNotification: true, - }) - return - } - - meaning := strings.TrimSpace(opts.Update.Message.Text[len(opts.Fields[0])+len(opts.Fields[1])+2:]) - - var ( - fromID int64 - fromUsername string - fromName string - viaID int64 - viaUsername string - viaName string - - isVia bool - isFromGroup bool - isViaGroup bool - isFromChannel bool - isViaChannel bool - ) - - if opts.Update.Message.ReplyToMessage != nil { - // 有回复一条信息,通过回复消息添加词 - isVia = true - if opts.Update.Message.ReplyToMessage.From.IsBot { - if opts.Update.Message.ReplyToMessage.From.ID == 136817688 { - // 频道身份信息 - isViaChannel = true - } else if opts.Update.Message.ReplyToMessage.From.ID == 1087968824 { - // 群组匿名身份 - isViaGroup = true + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Str("content", "udonese keyword added"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `udonese keyword added` message: %w", err) } else { - // 有 bot 标识,但不是频道身份也不是群组匿名,则是普通 bot - isVia = false - } - } - } - // 发送命令的人信息 - if opts.Update.Message.From.IsBot { - if opts.Update.Message.From.ID == 136817688 { - // 用频道身份发言 - isFromChannel = true - } else if opts.Update.Message.From.ID == 1087968824 { - // 用群组匿名身份发言 - isFromGroup = true - } - } - - if isVia { - if isViaChannel || isViaGroup { - // 回复给一条频道身份的信息 - fromID = opts.Update.Message.ReplyToMessage.SenderChat.ID - fromUsername = opts.Update.Message.ReplyToMessage.SenderChat.Username - fromName = utils.ShowChatName(opts.Update.Message.ReplyToMessage.SenderChat) - } else { - // 回复给普通用户 - fromName = utils.ShowUserName(opts.Update.Message.ReplyToMessage.From) - fromID = opts.Update.Message.ReplyToMessage.From.ID - } - if isFromChannel || isFromGroup { - // 频道身份 - viaID = opts.Update.Message.SenderChat.ID - viaUsername = opts.Update.Message.SenderChat.Username - viaName = utils.ShowChatName(opts.Update.Message.SenderChat) - } else { - // 普通用户身份 - viaID = opts.Update.Message.From.ID - viaName = utils.ShowUserName(opts.Update.Message.From) - } - } else { - if isFromChannel || isFromGroup { - // 频道身份 - fromID = opts.Update.Message.SenderChat.ID - fromUsername = opts.Update.Message.SenderChat.Username - fromName = utils.ShowChatName(opts.Update.Message.SenderChat) - } else { - // 普通用户身份 - fromID = opts.Update.Message.From.ID - fromName = utils.ShowUserName(opts.Update.Message.From) - } - } - - // 来源和经过都是同一位用户,删除 via 信息 - if fromID == viaID { - isVia = false - viaID = 0 - viaUsername = "" - viaName = "" - } - - var pendingMessage string - var botMessage *models.Message - - oldMeaning := addUdonese(UdoneseData, &UdoneseWord{ - Word: opts.Fields[1], - MeaningList: []UdoneseMeaning{{ - Meaning: meaning, - FromID: fromID, - FromUsername: fromUsername, - FromName: fromName, - ViaID: viaID, - ViaUsername: viaUsername, - ViaName: viaName, - }}, - }) - if oldMeaning != nil { - pendingMessage += fmt.Sprintf("[%s] 意思已存在于 [%s] 中:\n", meaning, oldMeaning.Word) - for i, s := range oldMeaning.MeaningList { - if meaning == s.Meaning { - pendingMessage += fmt.Sprintf("%d. [%s] ", i + 1, s.Meaning) - - // 来源的用户或频道 - if s.FromUsername != "" { - pendingMessage += fmt.Sprintf("From %s ", s.FromUsername, s.FromName) - } else if s.FromID != 0 { - if s.FromID < 0 { - pendingMessage += fmt.Sprintf("From %s ", utils.RemoveIDPrefix(s.FromID), s.FromName) - } else { - pendingMessage += fmt.Sprintf("From %s ", s.FromID, s.FromName) - } - } - - // 由其他用户添加时的信息 - if s.ViaUsername != "" { - pendingMessage += fmt.Sprintf("Via %s ", s.ViaUsername, s.ViaName) - } else if s.ViaID != 0 { - if s.ViaID < 0 { - pendingMessage += fmt.Sprintf("Via %s ", utils.RemoveIDPrefix(s.ViaID), s.ViaName) - } else { - pendingMessage += fmt.Sprintf("Via %s ", s.ViaID, s.ViaName) - } - } - - // 末尾换行 - pendingMessage += "\n" - } - } - } else { - err := SaveUdonese() - if err != nil { - pendingMessage += fmt.Sprintln("保存语句时似乎发生了一些错误:\n", err) - } else { - pendingMessage += fmt.Sprintf("已添加 [%s]\n", opts.Fields[1]) - pendingMessage += fmt.Sprintf("[%s] ", meaning) - - // 来源的用户或频道 - if fromUsername != "" { - pendingMessage += fmt.Sprintf("From %s ", fromUsername, fromName) - } else if fromID != 0 { - if fromID < 0 { - pendingMessage += fmt.Sprintf("From %s ", utils.RemoveIDPrefix(fromID), fromName) - } else { - pendingMessage += fmt.Sprintf("From %s ", fromID, fromName) - } - } - - // 由其他用户添加时的信息 - if viaUsername != "" { - pendingMessage += fmt.Sprintf("Via %s ", viaUsername, viaName) - } else if viaID != 0 { - if viaID < 0 { - pendingMessage += fmt.Sprintf("Via %s ", utils.RemoveIDPrefix(viaID), viaName) - } else { - pendingMessage += fmt.Sprintf("Via %s ", viaID, viaName) + time.Sleep(time.Second * 10) + _, err = opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ + ChatID: opts.Update.Message.Chat.ID, + MessageIDs: []int{ + opts.Update.Message.ID, + botMessage.ID, + }, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetChatDict(&opts.Update.Message.Chat)). + Ints("messageIDs", []int{ opts.Update.Message.ID, botMessage.ID }). + Str("content", "udonese keyword added"). + Msg(errt.DeleteMessages) + handlerErr.Addf("failed to delete `udonese keyword added` messages: %w", err) } } } } - - pendingMessage += fmt.Sprintln("
发送的消息与此消息将在十秒后删除
") - botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: pendingMessage, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - DisableNotification: true, - }) - if err == nil { - time.Sleep(time.Second * 10) - opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageIDs: []int{ - opts.Update.Message.ID, - botMessage.ID, - }, - }) - } + return handlerErr.Flat() } func udoneseInlineHandler(opts *handler_structs.SubHandlerParams) []models.InlineQueryResult { @@ -529,10 +606,10 @@ func udoneseInlineHandler(opts *handler_structs.SubHandlerParams) []models.Inlin } // 通过意思查找词 if utils.InlineQueryMatchMultKeyword(keywordFields, data.OnlyMeaning()) { - for _, n := range data.MeaningList { + for i, n := range data.MeaningList { if utils.InlineQueryMatchMultKeyword(keywordFields, []string{strings.ToLower(n.Meaning)}) { udoneseResultList = append(udoneseResultList, &models.InlineQueryResultArticle{ - ID: n.Meaning + "-meaning", + ID: fmt.Sprintf("%s-meaning-%d", data.Word, i), Title: n.Meaning, Description: fmt.Sprintf("%s 对应的词是 %s", n.Meaning, data.Word), InputMessageContent: models.InputTextMessageContent{ @@ -556,40 +633,66 @@ func udoneseInlineHandler(opts *handler_structs.SubHandlerParams) []models.Inlin }) } } + if len(udoneseResultList) == 0 { + udoneseResultList = append(udoneseResultList, &models.InlineQueryResultArticle{ + ID: "none", + Title: "没有记录任何内容", + Description: "什么都没有,使用 `/udonese <词> <意思>` 来添加吧", + InputMessageContent: models.InputTextMessageContent{ + MessageText: "使用 `/udonese <词> <单个意思>` 来添加记录", + ParseMode: models.ParseModeMarkdownV1, + }, + }) + } return udoneseResultList } -func udoneseGroupHandler(opts *handler_structs.SubHandlerParams) { - // 不响应来自转发的命令 - if opts.Update.Message.ForwardOrigin != nil { - return - } - // 空文本 - if len(opts.Fields) < 1 { - return - } +func udoneseGroupHandler(opts *handler_structs.SubHandlerParams) error { + // 不响应来自转发的命令和空文本 + if opts.Update.Message.ForwardOrigin != nil || len(opts.Fields) < 1 { return nil } + + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "udoneseGroupHandler"). + Logger() + + var handlerErr multe.MultiError if UdoneseErr != nil { - log.Println("some error in while read udonese list: ", UdoneseErr) - ReadUdonese() + logger.Warn(). + Err(UdoneseErr). + Msg("Some error in while read udonese list, try to read again") + + err := ReadUdonese(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Msg("Failed to read udonese list") + return handlerErr.Addf("failed to read udonese list: %w", err).Flat() + } } // 统计词使用次数 for i, n := range UdoneseData.OnlyWord() { if n == opts.Update.Message.Text || strings.HasPrefix(opts.Update.Message.Text, n) { UdoneseData.List[i].Used++ - err := SaveUdonese() + err := SaveUdonese(opts.Ctx) if err != nil { - log.Println("get some error when add udonese used count:", err) + logger.Error(). + Err(err). + Msg("Failed to save udonese list after add word usage count") + handlerErr.Addf("failed to save udonese list after add word usage count: %w", err) } - // fmt.Println(udon.List[i].Word, "+1", udon.List[i].Used) } } + var needNotice bool + if opts.Fields[0] == "sms" { // 参数过少,提示用法 if len(opts.Fields) < 2 { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, Text: "使用方法:发送 `sms <词>` 来查看对应的意思", @@ -597,15 +700,44 @@ func udoneseGroupHandler(opts *handler_structs.SubHandlerParams) { DisableNotification: true, ReplyMarkup: &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{{{ Text: "点击浏览全部词与意思", - SwitchInlineQueryCurrentChat: consts.InlineSubCommandSymbol + "sms ", + SwitchInlineQueryCurrentChat: configs.BotConfig.InlineSubCommandSymbol + "sms ", }}}}, }) - return + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "sms command usage"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `sms command usage` message: %w", err) + } + } else { + // 在数据库循环查找这个词 + for _, word := range UdoneseData.List { + if strings.EqualFold(word.Word, opts.Fields[1]) { + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: word.OutputMeanings(), + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + ParseMode: models.ParseModeHTML, + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "sms keyword meaning"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `sms keyword meaning` message: %w", err) + } + } + } + needNotice = true } - + } else if len(opts.Fields) == 2 && strings.HasSuffix(opts.Update.Message.Text, "ssm") { // 在数据库循环查找这个词 for _, word := range UdoneseData.List { - if strings.EqualFold(word.Word, opts.Fields[1]) && len(word.MeaningList) > 0 { + if strings.EqualFold(word.Word, opts.Fields[0]) { _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, Text: word.OutputMeanings(), @@ -614,74 +746,69 @@ func udoneseGroupHandler(opts *handler_structs.SubHandlerParams) { DisableNotification: true, }) if err != nil { - log.Println("get some error when answer udonese meaning:", err) + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Str("content", "sms keyword meaning"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `sms keyword meaning` message: %w", err) } - return } } - - // 到这里就是没找到,提示没有 - botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - Text: "这个词还没有记录,使用 `udonese <词> <意思>` 来添加吧", - ParseMode: models.ParseModeMarkdownV1, - DisableNotification: true, - }) - - time.Sleep(time.Second * 10) - opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageIDs: []int{ - botMessage.ID, - }, - }) - - return - } else if len(opts.Fields) > 1 && strings.HasSuffix(opts.Update.Message.Text, "ssm") { - // 在数据库循环查找这个词 - for _, word := range UdoneseData.List { - if strings.EqualFold(word.Word, opts.Fields[0]) && len(word.MeaningList) > 0 { - opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: word.OutputMeanings(), - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - ParseMode: models.ParseModeHTML, - DisableNotification: true, - }) - return - } - } - - // 到这里就是没找到,提示没有 - botMessage, _ := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, - Text: "这个词还没有记录,使用 `udonese <词> <意思>` 来添加吧", - ParseMode: models.ParseModeMarkdownV1, - DisableNotification: true, - }) - - time.Sleep(time.Second * 10) - opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ - ChatID: opts.Update.Message.Chat.ID, - MessageIDs: []int{ - botMessage.ID, - }, - }) + needNotice = true } + + if needNotice { + // 到这里就是没找到,提示没有 + botMessage, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + ReplyParameters: &models.ReplyParameters{ MessageID: opts.Update.Message.ID }, + Text: "这个词还没有记录,使用 `udonese <词> <意思>` 来添加吧", + ParseMode: models.ParseModeMarkdownV1, + DisableNotification: true, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Int("messageID", botMessage.ID). + Str("content", "sms keyword no meaning"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `sms keyword no meaning` message: %w", err) + } else { + time.Sleep(time.Second * 10) + _, err = opts.Thebot.DeleteMessages(opts.Ctx, &bot.DeleteMessagesParams{ + ChatID: opts.Update.Message.Chat.ID, + MessageIDs: []int{ botMessage.ID }, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.Message.Chat.ID). + Int("messageID", botMessage.ID). + Str("content", "sms keyword no meaning"). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `sms keyword no meaning` message: %w", err) + } + } + } + + return handlerErr.Flat() } func init() { - ReadUdonese() - plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ + plugin_utils.AddInitializer(plugin_utils.Initializer{ Name: "Udonese", - Saver: SaveUdonese, + Func: ReadUdonese, + }) + plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ + Name: "Udonese", + Saver: SaveUdonese, Loader: ReadUdonese, }) plugin_utils.AddInlineHandlerPlugins(plugin_utils.InlineHandler{ - Command: "sms", - Handler: udoneseInlineHandler, + Command: "sms", + Handler: udoneseInlineHandler, Description: "查询 Udonese 词典", }) plugin_utils.AddSlashSymbolCommandPlugins(plugin_utils.SlashSymbolCommand{ @@ -689,186 +816,269 @@ func init() { Handler: addUdoneseHandler, }) plugin_utils.AddHandlerByChatIDPlugins(plugin_utils.HandlerByChatID{ - ChatID: UdonGroupID, + ChatID: UdonGroupID, PluginName: "udoneseGroupHandler", - Handler: udoneseGroupHandler, + Handler: udoneseGroupHandler, }) plugin_utils.AddCallbackQueryCommandPlugins(plugin_utils.CallbackQuery{ CommandChar: "udonese", Handler: udoneseCallbackHandler, }) - // plugin_utils.AddSuffixCommandPlugins(plugin_utils.SuffixCommand{ - // SuffixCommand: "ssm", - // Handler: udoneseHandler, - // }) } -func udoneseCallbackHandler(opts *handler_structs.SubHandlerParams) { +func udoneseCallbackHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Udonese"). + Str("funcName", "udoneseCallbackHandler"). + Logger() + + var handlerErr multe.MultiError + if !utils.AnyContains(opts.Update.CallbackQuery.From.ID, UdoneseManagerIDs) { - opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + _, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ CallbackQueryID: opts.Update.CallbackQuery.ID, Text: "不可以!", ShowAlert: true, }) - return - } - - if opts.Update.CallbackQuery.Data == "udonese_done" { - opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - }) - return - } - - if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_word_") { - word := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_word_") - var targetWord UdoneseWord - for _, wordlist := range UdoneseData.List { - if wordlist.Word == word { - targetWord = wordlist - } - } - - opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: fmt.Sprintf("词: %s\n有 %d 个意思\n", targetWord.Word, len(targetWord.MeaningList)), - ParseMode: models.ParseModeMarkdownV1, - ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), - }) - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_meaning_") { - wordAndIndex := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_meaning_") - wordAndIndexList := strings.Split(wordAndIndex, "_") - meanningIndex, err := strconv.Atoi(wordAndIndexList[1]) if err != nil { - log.Println("covert meanning index error:", err) + logger.Error(). + Err(err). + Str("callbackQueryID", opts.Update.CallbackQuery.ID). + Str("content", "udonese no edit permissions"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `udonese no edit permissions` inline result: %w", err) } - - var targetMeaning UdoneseMeaning - - for _, udonese := range UdoneseData.List { - if udonese.Word == wordAndIndexList[0] { - for i, meaning := range udonese.MeaningList { - if i == meanningIndex { - targetMeaning = meaning - } + } else { + if opts.Update.CallbackQuery.Data == "udonese_done" { + _, err := opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.CallbackQuery.Message.Message.Chat.ID). + Int("messageID", opts.Update.CallbackQuery.Message.Message.ID). + Str("content", "udonese keyword manage keyboard"). + Msg(errt.DeleteMessage) + handlerErr.Addf("failed to delete `udonese keyword manage keyboard` inline result: %w", err) + } + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_word_") { + word := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_word_") + var targetWord UdoneseWord + for _, wordlist := range UdoneseData.List { + if wordlist.Word == word { + targetWord = wordlist } } - } - var pendingMessage string = fmt.Sprintf("意思 [ %s ]\n", targetMeaning.Meaning) - - // 来源的用户或频道 - if targetMeaning.FromUsername != "" { - pendingMessage += fmt.Sprintf("From %s\n", targetMeaning.FromUsername, targetMeaning.FromName) - } else if targetMeaning.FromID != 0 { - if targetMeaning.FromID < 0 { - pendingMessage += fmt.Sprintf("From %s\n", utils.RemoveIDPrefix(targetMeaning.FromID), targetMeaning.FromName) - } else { - pendingMessage += fmt.Sprintf("From %s\n", targetMeaning.FromID, targetMeaning.FromName) + _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: fmt.Sprintf("词: [ %s ]\n有 %d 个意思,已使用 %d 次\n", targetWord.Word, len(targetWord.MeaningList), targetWord.Used), + ParseMode: models.ParseModeHTML, + ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.CallbackQuery.Message.Message.Chat.ID). + Int("messageID", opts.Update.CallbackQuery.Message.Message.ID). + Str("content", "udonese word meaning list"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `udonese keyword manage keyboard`: %w", err) } - } - - // 由其他用户添加时的信息 - if targetMeaning.ViaUsername != "" { - pendingMessage += fmt.Sprintf("Via %s\n", targetMeaning.ViaUsername, targetMeaning.ViaName) - } else if targetMeaning.ViaID != 0 { - if targetMeaning.ViaID < 0 { - pendingMessage += fmt.Sprintf("Via %s\n", utils.RemoveIDPrefix(targetMeaning.ViaID), targetMeaning.ViaName) + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_meaning_") { + wordAndIndex := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_meaning_") + wordAndIndexList := strings.Split(wordAndIndex, "_") + meanningIndex, err := strconv.Atoi(wordAndIndexList[1]) + if err != nil { + logger.Error(). + Err(err). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse meanning index") + handlerErr.Addf("failed to parse meanning index: %w", err) } else { - pendingMessage += fmt.Sprintf("Via %s\n", targetMeaning.ViaID, targetMeaning.ViaName) - } - } + var targetMeaning UdoneseMeaning - _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ - { - Text: "删除此意思", - CallbackData: fmt.Sprintf("udonese_delmeaning_%s_%d", wordAndIndexList[0], meanningIndex), - }, - { - Text: "返回", - CallbackData: "udonese_word_" + wordAndIndexList[0], - }, - }}}, - }) - if err != nil { - log.Println(err) - } + for _, udonese := range UdoneseData.List { + if udonese.Word == wordAndIndexList[0] { + for i, meaning := range udonese.MeaningList { + if i == meanningIndex { + targetMeaning = meaning + } + } + } + } - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_delmeaning_") { - wordAndIndex := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_delmeaning_") - wordAndIndexList := strings.Split(wordAndIndex, "_") - meanningIndex, err := strconv.Atoi(wordAndIndexList[1]) - if err != nil { - log.Println("covert meanning index error:", err) - } - var newMeaningList []UdoneseMeaning - var targetWord UdoneseWord - var deletedMeaning string + var pendingMessage string = fmt.Sprintf("意思: [ %s ]\n", targetMeaning.Meaning) - for index, udonese := range UdoneseData.List { - if udonese.Word == wordAndIndexList[0] { - for i, meaning := range udonese.MeaningList { - if i == meanningIndex { - deletedMeaning = meaning.Meaning + // 来源的用户或频道 + if targetMeaning.FromUsername != "" { + pendingMessage += fmt.Sprintf("From %s\n", targetMeaning.FromUsername, targetMeaning.FromName) + } else if targetMeaning.FromID != 0 { + if targetMeaning.FromID < 0 { + pendingMessage += fmt.Sprintf("From %s\n", utils.RemoveIDPrefix(targetMeaning.FromID), targetMeaning.FromName) } else { - newMeaningList = append(newMeaningList, meaning) + pendingMessage += fmt.Sprintf("From %s\n", targetMeaning.FromID, targetMeaning.FromName) } } - UdoneseData.List[index].MeaningList = newMeaningList - targetWord = UdoneseData.List[index] + + // 由其他用户添加时的信息 + if targetMeaning.ViaUsername != "" { + pendingMessage += fmt.Sprintf("Via %s\n", targetMeaning.ViaUsername, targetMeaning.ViaName) + } else if targetMeaning.ViaID != 0 { + if targetMeaning.ViaID < 0 { + pendingMessage += fmt.Sprintf("Via %s\n", utils.RemoveIDPrefix(targetMeaning.ViaID), targetMeaning.ViaName) + } else { + pendingMessage += fmt.Sprintf("Via %s\n", targetMeaning.ViaID, targetMeaning.ViaName) + } + } + + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: pendingMessage, + ParseMode: models.ParseModeHTML, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{ + { + Text: "删除此意思", + CallbackData: fmt.Sprintf("udonese_delmeaning_%s_%d", wordAndIndexList[0], meanningIndex), + }, + { + Text: "返回", + CallbackData: "udonese_word_" + wordAndIndexList[0], + }, + }}}, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.CallbackQuery.Message.Message.Chat.ID). + Int("messageID", opts.Update.CallbackQuery.Message.Message.ID). + Str("content", "udonese meaning manage keyboard"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `udonese meaning manage keyboard`: %w", err) + } + } + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_delmeaning_") { + wordAndIndex := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_delmeaning_") + wordAndIndexList := strings.Split(wordAndIndex, "_") + meanningIndex, err := strconv.Atoi(wordAndIndexList[1]) + if err != nil { + logger.Error(). + Err(err). + Str("callbackQueryData", opts.Update.CallbackQuery.Data). + Msg("Failed to parse meanning index") + handlerErr.Addf("failed to parse meanning index: %w", err) + } else { + var newMeaningList []UdoneseMeaning + var targetWord UdoneseWord + var deletedMeaning string + + for index, udonese := range UdoneseData.List { + if udonese.Word == wordAndIndexList[0] { + for i, meaning := range udonese.MeaningList { + if i == meanningIndex { + deletedMeaning = meaning.Meaning + } else { + newMeaningList = append(newMeaningList, meaning) + } + } + UdoneseData.List[index].MeaningList = newMeaningList + targetWord = UdoneseData.List[index] + } + } + + err = SaveUdonese(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Msg("Failed to save udonese data after deleting meaning") + handlerErr.Addf("failed to save udonese data after deleting meaning: %w", err) + + _, err = opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: opts.Update.CallbackQuery.ID, + Text: "删除意思时保存数据库失败,请重试或联系机器人管理员\n" + err.Error(), + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Str("content", "failed to save udonese data after delete meaning"). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `failed to save udonese data after delete meaning` callback answer: %w", err) + } + } else { + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: fmt.Sprintf("词: [ %s ]\n有 %d 个意思,已使用 %d 次\n
已删除 [ %s ] 词中的 [ %s ] 意思
", targetWord.Word, len(targetWord.MeaningList), targetWord.Used, targetWord.Word, deletedMeaning), + ParseMode: models.ParseModeHTML, + ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.CallbackQuery.Message.Message.Chat.ID). + Int("messageID", opts.Update.CallbackQuery.Message.Message.ID). + Str("content", "udonese meaning manage keyboard after delete meaning"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `udonese meaning manage keyboard after delete meaning`: %w", err) + } + } + } + } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_delword_") { + word := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_delword_") + var newWordList []UdoneseWord + for _, udonese := range UdoneseData.List { + if udonese.Word != word { + newWordList = append(newWordList, udonese) + } + } + UdoneseData.List = newWordList + + err := SaveUdonese(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Msg("Failed to save udonese data after delete word") + handlerErr.Addf("failed to save udonese data after delete word: %w", err) + + _, err = opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{ + CallbackQueryID: opts.Update.CallbackQuery.ID, + Text: "删除词时保存数据库失败,请重试\n" + err.Error(), + ShowAlert: true, + }) + if err != nil { + logger.Error(). + Err(err). + Msg(errt.AnswerCallbackQuery) + handlerErr.Addf("failed to send `failed to save udonese data after delete word` callback answer: %w", err) + } + } else { + _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ + ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, + MessageID: opts.Update.CallbackQuery.Message.Message.ID, + Text: fmt.Sprintf("
已删除 [ %s ] 词
", word), + ParseMode: models.ParseModeHTML, + ReplyMarkup: &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "关闭菜单", + CallbackData: "udonese_done", + }}}}, + }) + if err != nil { + logger.Error(). + Err(err). + Int64("chatID", opts.Update.CallbackQuery.Message.Message.Chat.ID). + Int("messageID", opts.Update.CallbackQuery.Message.Message.ID). + Str("content", "udonese word deleted notice"). + Msg(errt.EditMessageText) + handlerErr.Addf("failed to edit message to `udonese word deleted notice`: %w", err) + } } } - - var pendingMessage string = fmt.Sprintf("词: [ %s ]\n有 %d 个意思\n\n
已删除 [ %s ] 词中的 %s 意思
", targetWord.Word, len(targetWord.MeaningList), wordAndIndexList[0], deletedMeaning) - - _, err = opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, - ReplyMarkup: targetWord.buildUdoneseWordKeyboard(), - }) - if err != nil { - log.Println("error when edit deleted meaning keyboard:", err) - } - - SaveUdonese() - return - } else if strings.HasPrefix(opts.Update.CallbackQuery.Data, "udonese_delword_") { - word := strings.TrimPrefix(opts.Update.CallbackQuery.Data, "udonese_delword_") - var newWordList []UdoneseWord - for _, udonese := range UdoneseData.List { - if udonese.Word != word { - newWordList = append(newWordList, udonese) - } - } - UdoneseData.List = newWordList - - var pendingMessage string = fmt.Sprintf("
已删除 [ %s ] 词
", word) - - _, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{ - ChatID: opts.Update.CallbackQuery.Message.Message.Chat.ID, - MessageID: opts.Update.CallbackQuery.Message.Message.ID, - Text: pendingMessage, - ParseMode: models.ParseModeHTML, - ReplyMarkup: &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "关闭菜单", - CallbackData: "udonese_done", - }}}}, - }) - if err != nil { - log.Println("error when edit deleted word message:", err) - } - - SaveUdonese() } + return handlerErr.Flat() } diff --git a/plugins/plugin_voicelist.go b/plugins/plugin_voicelist.go index 66f5962..606d176 100644 --- a/plugins/plugin_voicelist.go +++ b/plugins/plugin_voicelist.go @@ -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: "无法读取到语音文件,请联系机器人管理员", diff --git a/plugins/saved_message/functions.go b/plugins/saved_message/functions.go index c196fd4..47396ac 100644 --- a/plugins/saved_message/functions.go +++ b/plugins/saved_message/functions.go @@ -2,19 +2,31 @@ package saved_message import ( "fmt" - "log" "reflect" "trbot/utils" + "trbot/utils/configs" "trbot/utils/consts" + "trbot/utils/errt" "trbot/utils/handler_structs" + "trbot/utils/multe" "trbot/utils/plugin_utils" + "trbot/utils/type/message_utils" "unicode/utf8" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" + "github.com/rs/zerolog" ) -func saveMessageHandler(opts *handler_structs.SubHandlerParams) { +func saveMessageHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Saved Message"). + Str("funcName", "saveMessageHandler"). + Logger() + + var handlerErr multe.MultiError + var needSave bool = true UserSavedMessage := SavedMessageSet[opts.Update.Message.From.ID] messageParams := &bot.SendMessageParams{ @@ -23,7 +35,7 @@ func saveMessageHandler(opts *handler_structs.SubHandlerParams) { ParseMode: models.ParseModeHTML, ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ Text: "点击浏览您的收藏", - SwitchInlineQueryCurrentChat: consts.InlineSubCommandSymbol + "saved ", + SwitchInlineQueryCurrentChat: configs.BotConfig.InlineSubCommandSymbol + "saved ", }}}}, } @@ -35,523 +47,588 @@ func saveMessageHandler(opts *handler_structs.SubHandlerParams) { }}}} _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) if err != nil { - log.Printf("Error response /save command initial info: %v", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "need agree privacy policy"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `need agree privacy policy` message: %w", err) } - return - } - - if UserSavedMessage.Limit == 0 && UserSavedMessage.Count == 0 { - UserSavedMessage.Limit = 100 - } - - // 若不是初次添加,为 0 就是不限制 - if UserSavedMessage.Limit != 0 && UserSavedMessage.Count >= UserSavedMessage.Limit { - messageParams.Text = "已达到限制,无法保存更多内容" - _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) - if err != nil { - log.Printf("Error response /save command reach limit: %v", err) - } - return - } - - // var pendingMessage string - if opts.Update.Message.ReplyToMessage == nil { - messageParams.Text = "在回复一条消息的同时发送 /save 来添加" } else { - var DescriptionText string - // 获取使用命令保存时设定的描述 - if len(opts.Update.Message.Text) > len(opts.Fields[0])+1 { - DescriptionText = opts.Update.Message.Text[len(opts.Fields[0])+1:] + if UserSavedMessage.Limit == 0 && UserSavedMessage.Count == 0 { + // 每个用户初次添加时,默认限制 100 条 + UserSavedMessage.Limit = 100 } - var originInfo *OriginInfo - if opts.Update.Message.ReplyToMessage.ForwardOrigin != nil && opts.Update.Message.ReplyToMessage.ForwardOrigin.MessageOriginHiddenUser == nil { - originInfo = getMessageOriginData(opts.Update.Message.ReplyToMessage.ForwardOrigin) - } else if opts.Update.Message.Chat.Type != models.ChatTypePrivate { - originInfo = getMessageLink(opts.Update.Message) - } - - var isSaved bool - var messageLength int - var pendingEntitites []models.MessageEntity - var needChangeEntitites bool = true - - if opts.Update.Message.ReplyToMessage.Caption != "" { - messageLength = utf8.RuneCountInString(opts.Update.Message.ReplyToMessage.Caption) - pendingEntitites = opts.Update.Message.ReplyToMessage.CaptionEntities - } else if opts.Update.Message.ReplyToMessage.Text != "" { - messageLength = utf8.RuneCountInString(opts.Update.Message.ReplyToMessage.Text) - pendingEntitites = opts.Update.Message.ReplyToMessage.Entities + // 若不是初次添加,为 0 就是不限制 + if UserSavedMessage.Limit != 0 && UserSavedMessage.Count >= UserSavedMessage.Limit { + messageParams.Text = "已达到限制,无法保存更多内容" + _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "reach saved limit"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `reach saved limit` message: %w", err) + } } else { - needChangeEntitites = false - } - - if needChangeEntitites { - // 若字符长度大于设定的阈值,添加折叠样式引用再保存 - if messageLength > textExpandableLength { - if len(pendingEntitites) == 1 && pendingEntitites[0].Type == models.MessageEntityTypeBlockquote && pendingEntitites[0].Offset == 0 && pendingEntitites[0].Length == messageLength { - // 如果消息仅为一个消息格式实体,且是不折叠形式的引用,则将格式实体改为可折叠格式引用后再保存 - pendingEntitites = []models.MessageEntity{{ - Type: models.MessageEntityTypeExpandableBlockquote, - Offset: 0, - Length: messageLength, - }} - } else { - // 其他则仅在末尾加一个可折叠形式的引用 - pendingEntitites = append(pendingEntitites, models.MessageEntity{ - Type: models.MessageEntityTypeExpandableBlockquote, - Offset: 0, - Length: messageLength, - }) - } - } - } - - if opts.Update.Message.ReplyToMessage.Text != "" { - for i, n := range UserSavedMessage.Item.OnlyText { - if n.TitleAndMessageText == opts.Update.Message.ReplyToMessage.Text && reflect.DeepEqual(n.Entities, pendingEntitites) { - isSaved = true - messageParams.Text = "已保存过该文本\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此文本添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此文本的搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此文本的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.OnlyText[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - - if !isSaved { - UserSavedMessage.Item.OnlyText = append(UserSavedMessage.Item.OnlyText, SavedMessageTypeCachedOnlyText{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - TitleAndMessageText: opts.Update.Message.ReplyToMessage.Text, - Description: DescriptionText, - Entities: pendingEntitites, - LinkPreviewOptions: opts.Update.Message.ReplyToMessage.LinkPreviewOptions, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存文本" - } - } else if opts.Update.Message.ReplyToMessage.Audio != nil { - for i, n := range UserSavedMessage.Item.Audio { - if n.FileID == opts.Update.Message.ReplyToMessage.Audio.FileID { - isSaved = true - messageParams.Text = "已保存过该音乐\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此音乐添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此音乐的搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此音乐的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Audio[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.Audio = append(UserSavedMessage.Item.Audio, SavedMessageTypeCachedAudio{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Audio.FileID, - Title: opts.Update.Message.ReplyToMessage.Audio.Title, - FileName: opts.Update.Message.ReplyToMessage.Audio.FileName, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存音乐" - } - } else if opts.Update.Message.ReplyToMessage.Animation != nil { - for i, n := range UserSavedMessage.Item.Mpeg4gif { - if n.FileID == opts.Update.Message.ReplyToMessage.Animation.FileID { - isSaved = true - messageParams.Text = "已保存过该 GIF\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此 GIF 添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此 GIF 搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此 GIF 的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Mpeg4gif[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.Mpeg4gif = append(UserSavedMessage.Item.Mpeg4gif, SavedMessageTypeCachedMpeg4Gif{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Animation.FileID, - Title: opts.Update.Message.ReplyToMessage.Caption, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存 GIF" - } - } else if opts.Update.Message.ReplyToMessage.Document != nil { - if opts.Update.Message.ReplyToMessage.Document.MimeType == "image/gif" { - for i, n := range UserSavedMessage.Item.Gif { - if n.FileID == opts.Update.Message.ReplyToMessage.Document.FileID { - isSaved = true - messageParams.Text = "已保存过该 GIF (文件)\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此 GIF (文件)添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此 GIF (文件)搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此 GIF (文件)的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Gif[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.Gif = append(UserSavedMessage.Item.Gif, SavedMessageTypeCachedGif{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Document.FileID, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存 GIF (文件)" - } + // var pendingMessage string + if opts.Update.Message.ReplyToMessage == nil { + needSave = false + messageParams.Text = "在回复一条消息的同时发送 /save 来添加" } else { - for i, n := range UserSavedMessage.Item.Document { - if n.FileID == opts.Update.Message.ReplyToMessage.Document.FileID { - isSaved = true - messageParams.Text = "已保存过该文件\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此文件添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此文件搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此文件的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Document[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } + var DescriptionText string + // 获取使用命令保存时设定的描述 + if len(opts.Update.Message.Text) > len(opts.Fields[0])+1 { + DescriptionText = opts.Update.Message.Text[len(opts.Fields[0])+1:] } - if !isSaved { - UserSavedMessage.Item.Document = append(UserSavedMessage.Item.Document, SavedMessageTypeCachedDocument{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Document.FileID, - Title: opts.Update.Message.ReplyToMessage.Document.FileName, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存文件" - } - } - } else if opts.Update.Message.ReplyToMessage.Photo != nil { - for i, n := range UserSavedMessage.Item.Photo { - if n.FileID == opts.Update.Message.ReplyToMessage.Photo[len(opts.Update.Message.ReplyToMessage.Photo)-1].FileID { - isSaved = true - messageParams.Text = "已保存过该图片\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此图片添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此图片搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此图片的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Photo[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.Photo = append(UserSavedMessage.Item.Photo, SavedMessageTypeCachedPhoto{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Photo[len(opts.Update.Message.ReplyToMessage.Photo)-1].FileID, - // Title: opts.Update.Message.ReplyToMessage.Caption, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - CaptionAboveMedia: opts.Update.Message.ReplyToMessage.ShowCaptionAboveMedia, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存图片" - } - } else if opts.Update.Message.ReplyToMessage.Sticker != nil { - for i, n := range UserSavedMessage.Item.Sticker { - if n.FileID == opts.Update.Message.ReplyToMessage.Sticker.FileID { - isSaved = true - messageParams.Text = "已保存过该贴纸\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此贴纸添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此贴纸的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Sticker[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{Name: opts.Update.Message.ReplyToMessage.Sticker.SetName}) - if err != nil { - log.Printf("Error response /save command sticker no pack info: %v", err) + var originInfo *OriginInfo + if opts.Update.Message.ReplyToMessage.ForwardOrigin != nil && opts.Update.Message.ReplyToMessage.ForwardOrigin.MessageOriginHiddenUser == nil { + originInfo = getMessageOriginData(opts.Update.Message.ReplyToMessage.ForwardOrigin) + } else if opts.Update.Message.Chat.Type != models.ChatTypePrivate { + originInfo = getMessageLink(opts.Update.Message) } - if stickerSet != nil { - // 属于一个贴纸包中的贴纸 - UserSavedMessage.Item.Sticker = append(UserSavedMessage.Item.Sticker, SavedMessageTypeCachedSticker{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Sticker.FileID, - SetName: stickerSet.Name, - SetTitle: stickerSet.Title, - Description: DescriptionText, - OriginInfo: originInfo, - }) + + var isSaved bool + var messageLength int + var pendingEntitites []models.MessageEntity + var needChangeEntitites bool = true + + if opts.Update.Message.ReplyToMessage.Caption != "" { + messageLength = utf8.RuneCountInString(opts.Update.Message.ReplyToMessage.Caption) + pendingEntitites = opts.Update.Message.ReplyToMessage.CaptionEntities + } else if opts.Update.Message.ReplyToMessage.Text != "" { + messageLength = utf8.RuneCountInString(opts.Update.Message.ReplyToMessage.Text) + pendingEntitites = opts.Update.Message.ReplyToMessage.Entities } else { - UserSavedMessage.Item.Sticker = append(UserSavedMessage.Item.Sticker, SavedMessageTypeCachedSticker{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Sticker.FileID, - Description: DescriptionText, - OriginInfo: originInfo, - }) + needChangeEntitites = false + } + + if needChangeEntitites { + // 若字符长度大于设定的阈值,添加折叠样式引用再保存 + if messageLength > textExpandableLength { + if len(pendingEntitites) == 1 && pendingEntitites[0].Type == models.MessageEntityTypeBlockquote && pendingEntitites[0].Offset == 0 && pendingEntitites[0].Length == messageLength { + // 如果消息仅为一个消息格式实体,且是不折叠形式的引用,则将格式实体改为可折叠格式引用后再保存 + pendingEntitites = []models.MessageEntity{{ + Type: models.MessageEntityTypeExpandableBlockquote, + Offset: 0, + Length: messageLength, + }} + } else { + // 其他则仅在末尾加一个可折叠形式的引用 + pendingEntitites = append(pendingEntitites, models.MessageEntity{ + Type: models.MessageEntityTypeExpandableBlockquote, + Offset: 0, + Length: messageLength, + }) + } + } + } + + replyMsgType := message_utils.GetMessageType(opts.Update.Message.ReplyToMessage) + switch { + case replyMsgType.OnlyText: + for i, n := range UserSavedMessage.Item.OnlyText { + if n.TitleAndMessageText == opts.Update.Message.ReplyToMessage.Text && reflect.DeepEqual(n.Entities, pendingEntitites) { + isSaved = true + messageParams.Text = "已保存过该文本\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此文本添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此文本的搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此文本的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.OnlyText[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + + if !isSaved { + UserSavedMessage.Item.OnlyText = append(UserSavedMessage.Item.OnlyText, SavedMessageTypeCachedOnlyText{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + TitleAndMessageText: opts.Update.Message.ReplyToMessage.Text, + Description: DescriptionText, + Entities: pendingEntitites, + LinkPreviewOptions: opts.Update.Message.ReplyToMessage.LinkPreviewOptions, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存文本" + } + case replyMsgType.Audio: + for i, n := range UserSavedMessage.Item.Audio { + if n.FileID == opts.Update.Message.ReplyToMessage.Audio.FileID { + isSaved = true + messageParams.Text = "已保存过该音乐\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此音乐添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此音乐的搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此音乐的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Audio[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.Audio = append(UserSavedMessage.Item.Audio, SavedMessageTypeCachedAudio{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Audio.FileID, + Title: opts.Update.Message.ReplyToMessage.Audio.Title, + FileName: opts.Update.Message.ReplyToMessage.Audio.FileName, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存音乐" + } + case replyMsgType.Animation: + for i, n := range UserSavedMessage.Item.Mpeg4gif { + if n.FileID == opts.Update.Message.ReplyToMessage.Animation.FileID { + isSaved = true + messageParams.Text = "已保存过该 GIF\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此 GIF 添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此 GIF 搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此 GIF 的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Mpeg4gif[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.Mpeg4gif = append(UserSavedMessage.Item.Mpeg4gif, SavedMessageTypeCachedMpeg4Gif{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Animation.FileID, + Title: opts.Update.Message.ReplyToMessage.Caption, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存 GIF" + } + case replyMsgType.Document: + if opts.Update.Message.ReplyToMessage.Document.MimeType == "image/gif" { + for i, n := range UserSavedMessage.Item.Gif { + if n.FileID == opts.Update.Message.ReplyToMessage.Document.FileID { + isSaved = true + messageParams.Text = "已保存过该 GIF (文件)\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此 GIF (文件) 添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此 GIF (文件) 搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此 GIF (文件) 的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Gif[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.Gif = append(UserSavedMessage.Item.Gif, SavedMessageTypeCachedGif{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Document.FileID, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存 GIF (文件)" + } + } else { + for i, n := range UserSavedMessage.Item.Document { + if n.FileID == opts.Update.Message.ReplyToMessage.Document.FileID { + isSaved = true + messageParams.Text = "已保存过该文件\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此文件添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此文件搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此文件的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Document[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.Document = append(UserSavedMessage.Item.Document, SavedMessageTypeCachedDocument{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Document.FileID, + Title: opts.Update.Message.ReplyToMessage.Document.FileName, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存文件" + } + } + case replyMsgType.Photo: + for i, n := range UserSavedMessage.Item.Photo { + if n.FileID == opts.Update.Message.ReplyToMessage.Photo[len(opts.Update.Message.ReplyToMessage.Photo)-1].FileID { + isSaved = true + messageParams.Text = "已保存过该图片\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此图片添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此图片搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此图片的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Photo[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.Photo = append(UserSavedMessage.Item.Photo, SavedMessageTypeCachedPhoto{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Photo[len(opts.Update.Message.ReplyToMessage.Photo)-1].FileID, + // Title: opts.Update.Message.ReplyToMessage.Caption, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + CaptionAboveMedia: opts.Update.Message.ReplyToMessage.ShowCaptionAboveMedia, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存图片" + } + case replyMsgType.Sticker: + for i, n := range UserSavedMessage.Item.Sticker { + if n.FileID == opts.Update.Message.ReplyToMessage.Sticker.FileID { + isSaved = true + messageParams.Text = "已保存过该贴纸\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此贴纸添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此贴纸搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此贴纸的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Sticker[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + + if !isSaved { + if opts.Update.Message.ReplyToMessage.Sticker.SetName != "" { + stickerSet, err := opts.Thebot.GetStickerSet(opts.Ctx, &bot.GetStickerSetParams{Name: opts.Update.Message.ReplyToMessage.Sticker.SetName}) + if err != nil { + logger.Warn(). + Err(err). + Str("setName", opts.Update.Message.ReplyToMessage.Sticker.SetName). + Msg("Failed to get sticker set info, save it as a custom sticker") + } + if stickerSet != nil { + // 属于一个贴纸包中的贴纸 + UserSavedMessage.Item.Sticker = append(UserSavedMessage.Item.Sticker, SavedMessageTypeCachedSticker{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Sticker.FileID, + SetName: stickerSet.Name, + SetTitle: stickerSet.Title, + Description: DescriptionText, + Emoji: opts.Update.Message.ReplyToMessage.Sticker.Emoji, + OriginInfo: originInfo, + }) + } else { + // 有贴纸信息,但是对应的贴纸包已经删掉了 + UserSavedMessage.Item.Sticker = append(UserSavedMessage.Item.Sticker, SavedMessageTypeCachedSticker{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Sticker.FileID, + Description: DescriptionText, + Emoji: opts.Update.Message.ReplyToMessage.Sticker.Emoji, + OriginInfo: originInfo, + }) + } + } else { + UserSavedMessage.Item.Sticker = append(UserSavedMessage.Item.Sticker, SavedMessageTypeCachedSticker{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Sticker.FileID, + Description: DescriptionText, + Emoji: opts.Update.Message.ReplyToMessage.Sticker.Emoji, + OriginInfo: originInfo, + }) + } + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存贴纸" + } + case replyMsgType.Video: + for i, n := range UserSavedMessage.Item.Video { + if n.FileID == opts.Update.Message.ReplyToMessage.Video.FileID { + isSaved = true + messageParams.Text = "已保存过该视频\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此视频添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此视频搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此视频的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Video[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + videoTitle := opts.Update.Message.ReplyToMessage.Video.FileName + if videoTitle == "" { + videoTitle = "video.mp4" + } + UserSavedMessage.Item.Video = append(UserSavedMessage.Item.Video, SavedMessageTypeCachedVideo{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Video.FileID, + Title: videoTitle, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存视频" + } + case replyMsgType.VideoNote: + for i, n := range UserSavedMessage.Item.VideoNote { + if n.FileID == opts.Update.Message.ReplyToMessage.VideoNote.FileID { + isSaved = true + messageParams.Text = "已保存过该圆形视频\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此圆形视频添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此圆形视频搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此圆形视频的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.VideoNote[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + UserSavedMessage.Item.VideoNote = append(UserSavedMessage.Item.VideoNote, SavedMessageTypeCachedVideoNote{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.VideoNote.FileID, + Title: opts.Update.Message.ReplyToMessage.VideoNote.FileUniqueID, + Description: DescriptionText, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存圆形视频" + } + case replyMsgType.Voice: + for i, n := range UserSavedMessage.Item.Voice { + if n.FileID == opts.Update.Message.ReplyToMessage.Voice.FileID { + isSaved = true + messageParams.Text = "已保存过该语音\n" + if DescriptionText != "" { + if n.Description == "" { + messageParams.Text += fmt.Sprintf("已为此语音添加搜索关键词 [ %s ]", DescriptionText) + } else if DescriptionText == n.Description { + messageParams.Text += fmt.Sprintf("此语音搜索关键词未修改 [ %s ]", DescriptionText) + needSave = false + break + } else { + messageParams.Text += fmt.Sprintf("已将此语音的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) + } + n.Description = DescriptionText + UserSavedMessage.Item.Voice[i] = n + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + } + break + } + } + if !isSaved { + voiceTitle := DescriptionText + if voiceTitle == "" { + voiceTitle = opts.Update.Message.ReplyToMessage.Voice.MimeType + } + UserSavedMessage.Item.Voice = append(UserSavedMessage.Item.Voice, SavedMessageTypeCachedVoice{ + ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), + FileID: opts.Update.Message.ReplyToMessage.Voice.FileID, + Title: voiceTitle, + Description: DescriptionText, + Caption: opts.Update.Message.ReplyToMessage.Caption, + CaptionEntities: pendingEntitites, + OriginInfo: originInfo, + }) + UserSavedMessage.Count++ + UserSavedMessage.SavedTimes++ + SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage + messageParams.Text = "已保存语音" + } + default: + messageParams.Text = "暂不支持的消息类型" + } + + if needSave { + err := SaveSavedMessageList(opts.Ctx) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("saveMessageType", string(replyMsgType.InString())). + Msg("Failed to save savedmessage list after save a item") + handlerErr.Addf("failed to save savedmessage list after save a item: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: fmt.Sprintf("保存内容时保存收藏列表数据库失败,请稍后再试或联系机器人管理员\n
%s", err.Error()), + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("saveMessageType", string(replyMsgType.InString())). + Str("content", "failed to save savedmessage list notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `failed to save savedmessage list notice` message: %w", err) + } + + return handlerErr.Flat() + } } - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存贴纸" } - } else if opts.Update.Message.ReplyToMessage.Video != nil { - for i, n := range UserSavedMessage.Item.Video { - if n.FileID == opts.Update.Message.ReplyToMessage.Video.FileID { - isSaved = true - messageParams.Text = "已保存过该视频\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此视频添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此视频搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此视频的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Video[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } + _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "saved message response"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `saved message response` message: %w", err) } - if !isSaved { - UserSavedMessage.Item.Video = append(UserSavedMessage.Item.Video, SavedMessageTypeCachedVideo{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Video.FileID, - Title: opts.Update.Message.ReplyToMessage.Video.FileName, - Description: DescriptionText, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存视频" - } - - } else if opts.Update.Message.ReplyToMessage.VideoNote != nil { - for i, n := range UserSavedMessage.Item.VideoNote { - if n.FileID == opts.Update.Message.ReplyToMessage.VideoNote.FileID { - isSaved = true - messageParams.Text = "已保存过该圆形视频\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此圆形视频添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此圆形视频搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此圆形视频的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.VideoNote[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.VideoNote = append(UserSavedMessage.Item.VideoNote, SavedMessageTypeCachedVideoNote{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.VideoNote.FileID, - Title: opts.Update.Message.ReplyToMessage.VideoNote.FileUniqueID, - Description: DescriptionText, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存圆形视频" - } - - } else if opts.Update.Message.ReplyToMessage.Voice != nil { - for i, n := range UserSavedMessage.Item.Voice { - if n.FileID == opts.Update.Message.ReplyToMessage.Voice.FileID { - isSaved = true - messageParams.Text = "已保存过该语音\n" - if DescriptionText != "" { - if n.Description == "" { - messageParams.Text += fmt.Sprintf("已为此语音添加搜索关键词 [ %s ]", DescriptionText) - } else if DescriptionText == n.Description { - messageParams.Text += fmt.Sprintf("此语音搜索关键词未修改 [ %s ]", DescriptionText) - break - } else { - messageParams.Text += fmt.Sprintf("已将此语音的搜索关键词从 [ %s ] 改为 [ %s ]", n.Description, DescriptionText) - } - n.Description = DescriptionText - UserSavedMessage.Item.Voice[i] = n - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - } - break - } - } - if !isSaved { - UserSavedMessage.Item.Voice = append(UserSavedMessage.Item.Voice, SavedMessageTypeCachedVoice{ - ID: fmt.Sprintf("%d", UserSavedMessage.SavedTimes), - FileID: opts.Update.Message.ReplyToMessage.Voice.FileID, - Title: DescriptionText, - Description: opts.Update.Message.ReplyToMessage.Caption, - Caption: opts.Update.Message.ReplyToMessage.Caption, - CaptionEntities: pendingEntitites, - OriginInfo: originInfo, - }) - UserSavedMessage.Count++ - UserSavedMessage.SavedTimes++ - SavedMessageSet[opts.Update.Message.From.ID] = UserSavedMessage - SaveSavedMessageList() - messageParams.Text = "已保存语音" - } - } else { - messageParams.Text = "暂不支持的消息类型" } } - // fmt.Println(opts.ChatInfo) - - _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) - if err != nil { - log.Printf("Error response /save command: %v", err) - } + return handlerErr.Flat() } -func channelSaveMessageHandler(opts *handler_structs.SubHandlerParams) { - ChannelSavedMessage := SavedMessageSet[opts.Update.ChannelPost.From.ID] +// func channelSaveMessageHandler(opts *handler_structs.SubHandlerParams) { +// ChannelSavedMessage := SavedMessageSet[opts.Update.ChannelPost.From.ID] - messageParams := &bot.SendMessageParams{ - ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID}, - ParseMode: models.ParseModeHTML, - } +// messageParams := &bot.SendMessageParams{ +// ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID}, +// ParseMode: models.ParseModeHTML, +// } - if !ChannelSavedMessage.AgreePrivacyPolicy { - messageParams.ChatID = opts.Update.ChannelPost.From.ID - messageParams.Text = "此功能需要保存一些信息才能正常工作,在使用这个功能前,请先阅读一下我们会保存哪些信息" - messageParams.ReplyMarkup = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "点击查看", - URL: fmt.Sprintf("https://t.me/%s?start=savedmessage_channel_privacy_policy", consts.BotMe.Username), - }}}} - _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) - if err != nil { - log.Printf("Error response /save command initial info: %v", err) - } - return - } +// if !ChannelSavedMessage.AgreePrivacyPolicy { +// messageParams.ChatID = opts.Update.ChannelPost.From.ID +// messageParams.Text = "此功能需要保存一些信息才能正常工作,在使用这个功能前,请先阅读一下我们会保存哪些信息" +// messageParams.ReplyMarkup = &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ +// Text: "点击查看", +// URL: fmt.Sprintf("https://t.me/%s?start=savedmessage_channel_privacy_policy", consts.BotMe.Username), +// }}}} +// _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) +// if err != nil { +// log.Printf("Error response /save command initial info: %v", err) +// } +// return +// } - if ChannelSavedMessage.DiscussionID == 0 { - messageParams.Text = "您需要为此频道绑定一个讨论群组,用于接收收藏成功的确认信息与关键词更改" - _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) - if err != nil { - log.Printf("Error response /save command initial info: %v", err) - } - } +// if ChannelSavedMessage.DiscussionID == 0 { +// messageParams.Text = "您需要为此频道绑定一个讨论群组,用于接收收藏成功的确认信息与关键词更改" +// _, err := opts.Thebot.SendMessage(opts.Ctx, messageParams) +// if err != nil { +// log.Printf("Error response /save command initial info: %v", err) +// } +// } -} +// } -func InlineShowSavedMessageHandler(opts *handler_structs.SubHandlerParams) []models.InlineQueryResult { +func InlineShowSavedMessageHandler(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Saved Message"). + Str("funcName", "InlineShowSavedMessageHandler"). + Logger() + + var handlerErr multe.MultiError var InlineSavedMessageResultList []models.InlineQueryResult + var button *models.InlineQueryResultsButton SavedMessage := SavedMessageSet[opts.ChatInfo.ID] @@ -588,17 +665,17 @@ func InlineShowSavedMessageHandler(opts *handler_structs.SubHandlerParams) []mod for _, n := range SavedMessage.Item.All() { if n.onlyText != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.onlyText.Description, n.onlyText.Title}) { all = append(all, n.onlyText) - } else if n.audio != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.audio.Caption, n.sharedData.Description}) { + } else if n.audio != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.audio.Caption, n.sharedData.Description, n.sharedData.Title, n.sharedData.FileName}) { all = append(all, n.audio) - } else if n.mpeg4gif != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.mpeg4gif.Title, n.mpeg4gif.Caption, n.sharedData.Description}) { - all = append(all, n.mpeg4gif) } else if n.document != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.document.Title, n.document.Caption, n.document.Description}) { all = append(all, n.document) } else if n.gif != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.gif.Title, n.gif.Caption, n.sharedData.Description}) { all = append(all, n.gif) + } else if n.mpeg4gif != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.mpeg4gif.Title, n.mpeg4gif.Caption, n.sharedData.Description}) { + all = append(all, n.mpeg4gif) } else if n.photo != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.photo.Title, n.photo.Caption, n.photo.Description}) { all = append(all, n.photo) - } else if n.sticker != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.sharedData.Title, n.sharedData.Name, n.sharedData.Description}) { + } else if n.sticker != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.sharedData.Title, n.sharedData.Name, n.sharedData.Description, n.sharedData.FileName}) { all = append(all, n.sticker) } else if n.video != nil && utils.InlineQueryMatchMultKeyword(keywordFields, []string{n.video.Title, n.video.Caption, n.video.Description}) { all = append(all, n.video) @@ -624,24 +701,18 @@ func InlineShowSavedMessageHandler(opts *handler_structs.SubHandlerParams) []mod } if len(InlineSavedMessageResultList) == 0 { - _, err := opts.Thebot.AnswerInlineQuery(opts.Ctx, &bot.AnswerInlineQueryParams{ - InlineQueryID: opts.Update.InlineQuery.ID, - Results: []models.InlineQueryResult{&models.InlineQueryResultArticle{ - ID: "empty", - Title: "没有保存内容(点击查看详细教程)", - Description: "对一条信息回复 /save 来保存它", - InputMessageContent: models.InputTextMessageContent{ - MessageText: fmt.Sprintf("您可以在任何聊天的输入栏中输入 @%s +saved 来查看您的收藏\n若要添加,您需要确保机器人可以读取到您的指令,例如在群组中需要添加机器人,或点击 @%s 进入与机器人的聊天窗口,找到想要收藏的信息,然后对着那条信息回复 /save 即可\n若收藏成功,机器人会回复您并提示收藏成功,您也可以手动发送一条想要收藏的息,再使用 /save 命令回复它", consts.BotMe.Username, consts.BotMe.Username), - ParseMode: models.ParseModeHTML, - }, - }}, - Button: &models.InlineQueryResultsButton{ - Text: "点击此处快速跳转到机器人", - StartParameter: "via-inline_noreply", + InlineSavedMessageResultList = append(InlineSavedMessageResultList, &models.InlineQueryResultArticle{ + ID: "empty", + Title: "没有保存内容(点击查看详细教程)", + Description: "对一条信息回复 /save 来保存它", + InputMessageContent: models.InputTextMessageContent{ + MessageText: fmt.Sprintf("您可以在任何聊天的输入栏中输入 @%s +saved 来查看您的收藏\n若要添加,您需要确保机器人可以读取到您的指令,例如在群组中需要添加机器人,或点击 @%s 进入与机器人的聊天窗口,找到想要收藏的信息,然后对着那条信息回复 /save 即可\n若收藏成功,机器人会回复您并提示收藏成功,您也可以手动发送一条想要收藏的息,再使用 /save 命令回复它", consts.BotMe.Username, consts.BotMe.Username), + ParseMode: models.ParseModeHTML, }, }) - if err != nil { - log.Println("Error when answering inline [saved] command", err) + button = &models.InlineQueryResultsButton{ + Text: "点击此处快速跳转到机器人", + StartParameter: "via-inline_noreply", } } @@ -649,15 +720,30 @@ func InlineShowSavedMessageHandler(opts *handler_structs.SubHandlerParams) []mod InlineQueryID: opts.Update.InlineQuery.ID, Results: utils.InlineResultPagination(opts.Fields, InlineSavedMessageResultList), IsPersonal: true, + Button: button, }) if err != nil { - log.Println("Error when answering inline [saved] command", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.InlineQuery.From)). + Str("query", opts.Update.InlineQuery.Query). + Str("content", "saved message result"). + Msg(errt.AnswerInlineQuery) + handlerErr.Addf("failed to send `saved message result` inline answer: %w", err) } - return InlineSavedMessageResultList + return handlerErr.Flat() } -func SendPrivacyPolicy(opts *handler_structs.SubHandlerParams) { +func SendPrivacyPolicy(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Saved Message"). + Str("funcName", "SendPrivacyPolicy"). + Logger() + + var handlerErr multe.MultiError + _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ ChatID: opts.Update.Message.Chat.ID, Text: "目前此机器人仍在开发阶段中,此信息可能会有更改\n" + @@ -687,37 +773,79 @@ func SendPrivacyPolicy(opts *handler_structs.SubHandlerParams) { ParseMode: models.ParseModeHTML, }) if err != nil { - log.Println("error when send savedmessage_privacy_policy:", err) - return + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "saved message privacy policy"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `saved message privacy policy` message: %w", err) } + + return handlerErr.Flat() } -func AgreePrivacyPolicy(opts *handler_structs.SubHandlerParams) { +func AgreePrivacyPolicy(opts *handler_structs.SubHandlerParams) error { + logger := zerolog.Ctx(opts.Ctx). + With(). + Str("pluginName", "Saved Message"). + Str("funcName", "AgreePrivacyPolicy"). + Logger() + + var handlerErr multe.MultiError + var UserSavedMessage SavedMessage - // , ok := consts.Database.Data.SavedMessage[opts.ChatInfo.ID] - if len(SavedMessageSet) == 0 { - SavedMessageSet = map[int64]SavedMessage{} - // consts.Database.Data.SavedMessage[opts.ChatInfo.ID] = SavedMessages - } UserSavedMessage.AgreePrivacyPolicy = true SavedMessageSet[opts.ChatInfo.ID] = UserSavedMessage - SaveSavedMessageList() - _, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ - ChatID: opts.Update.Message.Chat.ID, - Text: "您已成功开启收藏信息功能,回复一条信息的时候发送 /save 来使用收藏功能吧!\n由于服务器性能原因,每个人的收藏数量上限默认为 100 个,您也可以从机器人的个人信息中寻找管理员来申请更高的上限\n点击下方按钮来浏览您的收藏内容", - ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID}, - ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ - Text: "点击浏览您的收藏", - SwitchInlineQueryCurrentChat: consts.InlineSubCommandSymbol + "saved ", - }}}}, - }) + + err := SaveSavedMessageList(opts.Ctx) if err != nil { - log.Println("error when send savedmessage_privacy_policy_agree:", err) + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("failed to save savemessage list after user agree privacy policy") + handlerErr.Addf("failed to save savemessage list after user agree privacy policy: %w", err) + + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: fmt.Sprintf("保存收藏列表数据库失败,请稍后再试或联系机器人管理员\n
%s", err.Error()), + ParseMode: models.ParseModeHTML, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Str("content", "failed to save savedmessage list notice"). + Msg(errt.SendMessage) + handlerErr.Addf("failed to send `failed to save savedmessage list notice` message: %w", err) + } + } else { + _, err = opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{ + ChatID: opts.Update.Message.Chat.ID, + Text: "您已成功开启收藏信息功能,回复一条信息的时候发送 /save 来使用收藏功能吧!\n由于服务器性能原因,每个人的收藏数量上限默认为 100 个,您也可以从机器人的个人信息中寻找管理员来申请更高的上限\n点击下方按钮来浏览您的收藏内容", + ReplyParameters: &models.ReplyParameters{MessageID: opts.Update.Message.ID}, + ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{{{ + Text: "点击浏览您的收藏", + SwitchInlineQueryCurrentChat: configs.BotConfig.InlineSubCommandSymbol + "saved ", + }}}}, + }) + if err != nil { + logger.Error(). + Err(err). + Dict(utils.GetUserDict(opts.Update.Message.From)). + Msg("Failed to send `saved message function enabled` message") + handlerErr.Addf("failed to send `saved message function enabled` message: %w", err) + } } + + return handlerErr.Flat() } func Init() { - ReadSavedMessageList() + plugin_utils.AddInitializer(plugin_utils.Initializer{ + Name: "Saved Message", + Func: ReadSavedMessageList, + }) + // ReadSavedMessageList() plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{ Name: "Saved Message", Saver: SaveSavedMessageList, @@ -727,7 +855,7 @@ func Init() { SlashCommand: "save", Handler: saveMessageHandler, }) - plugin_utils.AddInlineHandlerPlugins(plugin_utils.InlineHandler{ + plugin_utils.AddInlineManualHandlerPlugins(plugin_utils.InlineManualHandler{ Command: "saved", Handler: InlineShowSavedMessageHandler, Description: "显示自己保存的消息", @@ -762,7 +890,7 @@ func Init() { ReplyMarkup: &models.InlineKeyboardMarkup{InlineKeyboard: [][]models.InlineKeyboardButton{ {{ Text: "点击浏览您的收藏", - SwitchInlineQueryCurrentChat: consts.InlineSubCommandSymbol + "saved ", + SwitchInlineQueryCurrentChat: configs.BotConfig.InlineSubCommandSymbol + "saved ", }}, {{ Text: "将此功能设定为您的默认 inline 命令", diff --git a/plugins/saved_message/item_structs.go b/plugins/saved_message/item_structs.go index 35d08d7..7c96361 100644 --- a/plugins/saved_message/item_structs.go +++ b/plugins/saved_message/item_structs.go @@ -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"` diff --git a/plugins/saved_message/utils.go b/plugins/saved_message/utils.go index ab0932b..68bc604 100644 --- a/plugins/saved_message/utils.go +++ b/plugins/saved_message/utils.go @@ -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), diff --git a/plugins/sub_package_plugin.go b/plugins/sub_package_plugin.go index 73dae21..cc78f83 100644 --- a/plugins/sub_package_plugin.go +++ b/plugins/sub_package_plugin.go @@ -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. diff --git a/utils/configs/config.go b/utils/configs/config.go new file mode 100644 index 0000000..bd32ff2 --- /dev/null +++ b/utils/configs/config.go @@ -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, + }, + } +} diff --git a/utils/configs/init.go b/utils/configs/init.go new file mode 100644 index 0000000..c0dec61 --- /dev/null +++ b/utils/configs/init.go @@ -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") + } +} diff --git a/utils/configs/webhook.go b/utils/configs/webhook.go new file mode 100644 index 0000000..e0d412b --- /dev/null +++ b/utils/configs/webhook.go @@ -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") + } + } +} diff --git a/utils/consts/consts.go b/utils/consts/consts.go index 827439d..af791cf 100644 --- a/utils/consts/consts.go +++ b/utils/consts/consts.go @@ -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 diff --git a/utils/errt/log_template.go b/utils/errt/log_template.go new file mode 100644 index 0000000..f77454c --- /dev/null +++ b/utils/errt/log_template.go @@ -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" +) diff --git a/utils/internal_plugin/handler.go b/utils/internal_plugin/handler.go index bc405c0..b42aa6f 100644 --- a/utils/internal_plugin/handler.go +++ b/utils/internal_plugin/handler.go @@ -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 } diff --git a/utils/internal_plugin/register.go b/utils/internal_plugin/register.go index 2819eb2..240ad6d 100644 --- a/utils/internal_plugin/register.go +++ b/utils/internal_plugin/register.go @@ -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("类型: [%v]\nID: [%v]\n用户名:[%v]", 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: [%v]", 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[%s]\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 模式下的默认命令
由于缓存原因,您可能需要等一会才能看到更新后的结果
无论您是否设定了默认命令,您始终都可以在 inline 模式下输入 %s 号来查看全部可用的命令", 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 模式下的默认命令
由于缓存原因,您可能需要等一会才能看到更新后的结果
无论您是否设定了默认命令,您始终都可以在 inline 模式下输入 %s 号来查看全部可用的命令", 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 ',如果是通过消息按钮发送的,用户只会看到自己发送了一个 `/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: "保存数据库", }, diff --git a/utils/mess/mess.go b/utils/mess/mess.go index 2a5bbb2..0af6ad5 100644 --- a/utils/mess/mess.go +++ b/utils/mess/mess.go @@ -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, + ) } diff --git a/utils/multe/mult_errors.go b/utils/multe/mult_errors.go new file mode 100644 index 0000000..4c05fdb --- /dev/null +++ b/utils/multe/mult_errors.go @@ -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...) +} diff --git a/utils/plugin_utils/handler_by_chatid.go b/utils/plugin_utils/handler_by_chatid.go index 9edb520..87d978a 100644 --- a/utils/plugin_utils/handler_by_chatid.go +++ b/utils/plugin_utils/handler_by_chatid.go @@ -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 { diff --git a/utils/plugin_utils/handler_by_message_type.go b/utils/plugin_utils/handler_by_message_type.go index 63b12a9..da096c0 100644 --- a/utils/plugin_utils/handler_by_message_type.go +++ b/utils/plugin_utils/handler_by_message_type.go @@ -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 } diff --git a/utils/plugin_utils/handler_callback_query.go b/utils/plugin_utils/handler_callback_query.go index 3556e0a..63c9636 100644 --- a/utils/plugin_utils/handler_callback_query.go +++ b/utils/plugin_utils/handler_callback_query.go @@ -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 { diff --git a/utils/plugin_utils/handler_custom_symbol.go b/utils/plugin_utils/handler_custom_symbol.go index f86e389..5cc2e04 100644 --- a/utils/plugin_utils/handler_custom_symbol.go +++ b/utils/plugin_utils/handler_custom_symbol.go @@ -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 { diff --git a/utils/plugin_utils/handler_help.go b/utils/plugin_utils/handler_help.go index ebefa2d..7e95061 100644 --- a/utils/plugin_utils/handler_help.go +++ b/utils/plugin_utils/handler_help.go @@ -19,6 +19,9 @@ func BuildHandlerHelpKeyboard() models.ReplyMarkup { }, }) } + if len(button) == 0 { + return nil + } return models.InlineKeyboardMarkup{ InlineKeyboard: button, } diff --git a/utils/plugin_utils/handler_inline.go b/utils/plugin_utils/handler_inline.go index 0d31fd6..ad7dc28 100644 --- a/utils/plugin_utils/handler_inline.go +++ b/utils/plugin_utils/handler_inline.go @@ -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 +} diff --git a/utils/plugin_utils/handler_slash_start.go b/utils/plugin_utils/handler_slash_start.go index 4468dae..3bd3e30 100644 --- a/utils/plugin_utils/handler_slash_start.go +++ b/utils/plugin_utils/handler_slash_start.go @@ -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 { diff --git a/utils/plugin_utils/handler_slash_symbol.go b/utils/plugin_utils/handler_slash_symbol.go index dcc7e3d..3b56015 100644 --- a/utils/plugin_utils/handler_slash_symbol.go +++ b/utils/plugin_utils/handler_slash_symbol.go @@ -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 { diff --git a/utils/plugin_utils/handler_suffix.go b/utils/plugin_utils/handler_suffix.go index e2422b3..d2a372a 100644 --- a/utils/plugin_utils/handler_suffix.go +++ b/utils/plugin_utils/handler_suffix.go @@ -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 { diff --git a/utils/plugin_utils/plugin_database_handler.go b/utils/plugin_utils/plugin_database_handler.go index 217fc74..a8961e6 100644 --- a/utils/plugin_utils/plugin_database_handler.go +++ b/utils/plugin_utils/plugin_database_handler.go @@ -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) } diff --git a/utils/plugin_utils/plugin_initializer.go b/utils/plugin_utils/plugin_initializer.go new file mode 100644 index 0000000..5f50f7f --- /dev/null +++ b/utils/plugin_utils/plugin_initializer.go @@ -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) +} diff --git a/utils/plugin_utils/plugin_type.go b/utils/plugin_utils/plugin_type.go index 408eafb..6ec21f7 100644 --- a/utils/plugin_utils/plugin_type.go +++ b/utils/plugin_utils/plugin_type.go @@ -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 diff --git a/utils/signals/signals.go b/utils/signals/signals.go index 423d5f8..c4aa5ab 100644 --- a/utils/signals/signals.go +++ b/utils/signals/signals.go @@ -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) } - } } diff --git a/utils/type_utils/message_attribute.go b/utils/type/message_utils/message_attribute.go similarity index 88% rename from utils/type_utils/message_attribute.go rename to utils/type/message_utils/message_attribute.go index ed75e9d..f7839c7 100644 --- a/utils/type_utils/message_attribute.go +++ b/utils/type/message_utils/message_attribute.go @@ -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 } diff --git a/utils/type_utils/message_type.go b/utils/type/message_utils/message_type.go similarity index 97% rename from utils/type_utils/message_type.go rename to utils/type/message_utils/message_type.go index b5f2017..f38abd2 100644 --- a/utils/type_utils/message_type.go +++ b/utils/type/message_utils/message_type.go @@ -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) diff --git a/utils/type/update_utils/update_type.go b/utils/type/update_utils/update_type.go new file mode 100644 index 0000000..797fb3e --- /dev/null +++ b/utils/type/update_utils/update_type.go @@ -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 +} diff --git a/utils/type_utils/update_type.go b/utils/type_utils/update_type.go deleted file mode 100644 index e021182..0000000 --- a/utils/type_utils/update_type.go +++ /dev/null @@ -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 -} diff --git a/utils/utils.go b/utils/utils.go index 044e36b..e56181f 100644 --- a/utils/utils.go +++ b/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") +} diff --git a/utils/yaml/yaml.go b/utils/yaml/yaml.go new file mode 100644 index 0000000..f900e8c --- /dev/null +++ b/utils/yaml/yaml.go @@ -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 +}