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]
+}