diff --git a/README.md b/README.md index 09168e3272..5d6abc7cbf 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,16 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - [x] 设置温度[正整数] + +
+ 聊天时长统计 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chatcount"` + + - [x] 查询水群@xxx + + - [x] 查看水群排名 +
睡眠管理 diff --git a/main.go b/main.go index 7bd6a05b7e..d21599c505 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,8 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chat" // 基础词库 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chatcount" // 聊天时长统计 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/sleepmanage" // 统计睡眠时间 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/atri" // ATRI词库 diff --git a/plugin/chatcount/chatcount.go b/plugin/chatcount/chatcount.go new file mode 100644 index 0000000000..eb5bf6d98c --- /dev/null +++ b/plugin/chatcount/chatcount.go @@ -0,0 +1,65 @@ +// Package chatcount 聊天时长统计 +package chatcount + +import ( + "fmt" + "strconv" + "strings" + + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" +) + +const ( + rankSize = 10 +) + +func init() { + engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "聊天时长统计", + Help: "- 查询水群@xxx\n- 查看水群排名", + PrivateDataFolder: "chatcount", + }) + go func() { + ctdb = initialize(engine.DataFolder() + "chatcount.db") + }() + engine.OnMessage(zero.OnlyGroup).SetBlock(false). + Handle(func(ctx *zero.Ctx) { + remindTime, remindFlag := ctdb.updateChatTime(ctx.Event.GroupID, ctx.Event.UserID) + if remindFlag { + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("BOT提醒:你今天已经水群%d分钟了!", remindTime))) + } + }) + + engine.OnPrefix(`查询水群`, zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + name := ctx.NickName() + todayTime, todayMessage, totalTime, totalMessage := ctdb.getChatTime(ctx.Event.GroupID, ctx.Event.UserID) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(fmt.Sprintf("%s今天水了%d分%d秒,发了%d条消息;总计水了%d分%d秒,发了%d条消息。", name, todayTime/60, todayTime%60, todayMessage, totalTime/60, totalTime%60, totalMessage))) + }) + engine.OnFullMatch("查看水群排名", zero.OnlyGroup).Limit(ctxext.LimitByGroup).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + text := strings.Builder{} + text.WriteString("今日水群排行榜:\n") + chatTimeList := ctdb.getChatRank(ctx.Event.GroupID) + for i := 0; i < len(chatTimeList) && i < rankSize; i++ { + text.WriteString("第") + text.WriteString(strconv.Itoa(i + 1)) + text.WriteString("名:") + text.WriteString(ctx.CardOrNickName(chatTimeList[i].UserID)) + text.WriteString(" - ") + text.WriteString(strconv.FormatInt(chatTimeList[i].TodayMessage, 10)) + text.WriteString("条,共") + text.WriteString(strconv.FormatInt(chatTimeList[i].TodayTime/60, 10)) + text.WriteString("分") + text.WriteString(strconv.FormatInt(chatTimeList[i].TodayTime%60, 10)) + text.WriteString("秒\n") + } + ctx.SendChain(message.Text(text.String())) + }) + +} diff --git a/plugin/chatcount/model.go b/plugin/chatcount/model.go new file mode 100644 index 0000000000..705eb02d42 --- /dev/null +++ b/plugin/chatcount/model.go @@ -0,0 +1,225 @@ +package chatcount + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/RomiChan/syncx" + + "github.com/jinzhu/gorm" +) + +const ( + chatInterval = 300 +) + +var ( + // ctdb 聊天时长数据库全局变量 + ctdb *chattimedb + // l 水群提醒时间提醒段,单位分钟 + l = newLeveler(60, 120, 180, 240, 300) +) + +// chattimedb 聊天时长数据库结构体 +type chattimedb struct { + // ctdb.userTimestampMap 每个人发言的时间戳 key=groupID_userID + userTimestampMap syncx.Map[string, int64] + // ctdb.userTodayTimeMap 每个人今日水群时间 key=groupID_userID + userTodayTimeMap syncx.Map[string, int64] + // ctdb.userTodayMessageMap 每个人今日水群次数 key=groupID_userID + userTodayMessageMap syncx.Map[string, int64] + // db 数据库 + db *gorm.DB + // chatmu 读写添加锁 + chatmu sync.Mutex +} + +// initialize 初始化 +func initialize(dbpath string) *chattimedb { + var err error + if _, err = os.Stat(dbpath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbpath) + if err != nil { + return nil + } + defer f.Close() + } + gdb, err := gorm.Open("sqlite3", dbpath) + if err != nil { + panic(err) + } + gdb.AutoMigrate(&chatTime{}) + return &chattimedb{ + db: gdb, + } +} + +// Close 关闭 +func (ctdb *chattimedb) Close() error { + db := ctdb.db + return db.Close() +} + +// chatTime 聊天时长,时间的单位都是秒 +type chatTime struct { + ID uint `gorm:"primary_key"` + GroupID int64 `gorm:"column:group_id"` + UserID int64 `gorm:"column:user_id"` + TodayTime int64 `gorm:"-"` + TodayMessage int64 `gorm:"-"` + TotalTime int64 `gorm:"column:total_time;default:0"` + TotalMessage int64 `gorm:"column:total_message;default:0"` +} + +// TableName 表名 +func (chatTime) TableName() string { + return "chat_time" +} + +// updateChatTime 更新发言时间,todayTime的单位是分钟 +func (ctdb *chattimedb) updateChatTime(gid, uid int64) (remindTime int64, remindFlag bool) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + db := ctdb.db + now := time.Now() + keyword := fmt.Sprintf("%v_%v", gid, uid) + ts, ok := ctdb.userTimestampMap.Load(keyword) + if !ok { + ctdb.userTimestampMap.Store(keyword, now.Unix()) + ctdb.userTodayMessageMap.Store(keyword, 1) + return + } + lastTime := time.Unix(ts, 0) + todayTime, _ := ctdb.userTodayTimeMap.Load(keyword) + totayMessage, _ := ctdb.userTodayMessageMap.Load(keyword) + //这个消息数是必须统计的 + ctdb.userTodayMessageMap.Store(keyword, totayMessage+1) + st := chatTime{ + GroupID: gid, + UserID: uid, + TotalTime: todayTime, + TotalMessage: totayMessage, + } + + // 如果不是同一天,把TotalTime,TotalMessage重置 + if lastTime.YearDay() != now.YearDay() { + if err := db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).First(&st).Error; err != nil { + if gorm.IsRecordNotFoundError(err) { + db.Model(&st).Create(&st) + } + } else { + db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).Update( + map[string]any{ + "total_time": st.TotalTime + todayTime, + "total_message": st.TotalMessage + totayMessage, + }) + } + ctdb.userTimestampMap.Store(keyword, now.Unix()) + ctdb.userTodayTimeMap.Delete(keyword) + ctdb.userTodayMessageMap.Delete(keyword) + return + } + + userChatTime := int64(now.Sub(lastTime).Seconds()) + // 当聊天时间在一定范围内的话,则计入时长 + if userChatTime < chatInterval { + ctdb.userTodayTimeMap.Store(keyword, todayTime+userChatTime) + remindTime = (todayTime + userChatTime) / 60 + remindFlag = l.level(int((todayTime+userChatTime)/60)) > l.level(int(todayTime/60)) + } + ctdb.userTimestampMap.Store(keyword, now.Unix()) + return +} + +// getChatTime 获得用户聊天时长和消息次数,todayTime,totalTime的单位是秒,todayMessage,totalMessage单位是条数 +func (ctdb *chattimedb) getChatTime(gid, uid int64) (todayTime, todayMessage, totalTime, totalMessage int64) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + db := ctdb.db + st := chatTime{} + db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).First(&st) + keyword := fmt.Sprintf("%v_%v", gid, uid) + todayTime, _ = ctdb.userTodayTimeMap.Load(keyword) + todayMessage, _ = ctdb.userTodayMessageMap.Load(keyword) + totalTime = st.TotalTime + totalMessage = st.TotalMessage + return +} + +// getChatRank 获得水群排名,时间单位为秒 +func (ctdb *chattimedb) getChatRank(gid int64) (chatTimeList []chatTime) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + chatTimeList = make([]chatTime, 0, 100) + keyList := make([]string, 0, 100) + ctdb.userTimestampMap.Range(func(key string, value int64) bool { + t := time.Unix(value, 0) + if strings.Contains(key, strconv.FormatInt(gid, 10)) && t.YearDay() == time.Now().YearDay() { + keyList = append(keyList, key) + } + return true + }) + for _, v := range keyList { + _, a, _ := strings.Cut(v, "_") + uid, _ := strconv.ParseInt(a, 10, 64) + todayTime, _ := ctdb.userTodayTimeMap.Load(v) + todayMessage, _ := ctdb.userTodayMessageMap.Load(v) + chatTimeList = append(chatTimeList, chatTime{ + GroupID: gid, + UserID: uid, + TodayTime: todayTime, + TodayMessage: todayMessage, + }) + } + sort.Sort(sortChatTime(chatTimeList)) + return +} + +// leveler 结构体,包含一个 levelArray 字段 +type leveler struct { + levelArray []int +} + +// newLeveler 构造函数,用于创建 Leveler 实例 +func newLeveler(levels ...int) *leveler { + return &leveler{ + levelArray: levels, + } +} + +// level 方法,封装了 getLevel 函数的逻辑 +func (l *leveler) level(t int) int { + for i := len(l.levelArray) - 1; i >= 0; i-- { + if t >= l.levelArray[i] { + return i + 1 + } + } + return 0 +} + +// sortChatTime chatTime排序数组 +type sortChatTime []chatTime + +// Len 实现 sort.Interface +func (a sortChatTime) Len() int { + return len(a) +} + +// Less 实现 sort.Interface,按 TodayTime 降序,TodayMessage 降序 +func (a sortChatTime) Less(i, j int) bool { + if a[i].TodayTime == a[j].TodayTime { + return a[i].TodayMessage > a[j].TodayMessage + } + return a[i].TodayTime > a[j].TodayTime +} + +// Swap 实现 sort.Interface +func (a sortChatTime) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +}