diff --git a/coolq/api.go b/coolq/api.go index 5714708..03b2edd 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/Mrs4s/go-cqhttp/db" "math" "os" "path" @@ -474,20 +475,19 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa ts.Add(time.Second) if e.Get("data.id").Exists() { i := e.Get("data.id").Int() - m := bot.GetMessage(int32(i)) + m, _ := bot.db.GetGroupMessageByGlobalID(int32(i)) if m != nil { - sender := m["sender"].(message.Sender) return &message.ForwardNode{ - SenderId: sender.Uin, - SenderName: (&sender).DisplayName(), + SenderId: m.Attribute.SenderUin, + SenderName: m.Attribute.SenderName, Time: func() int32 { - msgTime := m["time"].(int32) + msgTime := m.Attribute.Timestamp if hasCustom && msgTime == 0 { return int32(ts.Unix()) } - return msgTime + return int32(msgTime) }(), - Message: resolveElement(bot.ConvertStringMessage(m["message"].(string), true)), + Message: resolveElement(bot.ConvertContentMessage(m.Content, true)), } } log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str) @@ -791,29 +791,28 @@ func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bo // // https:// git.io/Jtz1y func (bot *CQBot) CQDeleteMessage(messageID int32) global.MSG { - msg := bot.GetMessage(messageID) - if msg == nil { + msg, err := bot.db.GetMessageByGlobalID(messageID) + if err != nil { + log.Warnf("撤回消息时出现错误: %v", err) return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在") } - if _, ok := msg["group"]; ok { - if msg["internal-id"] == nil { - // TODO 撤回临时对话消息 - log.Warnf("撤回 %v 失败: 无法撤回临时对话消息", messageID) - return Failed(100, "CANNOT_RECALL_TEMP_MSG", "无法撤回临时对话消息") - } - if err := bot.Client.RecallGroupMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil { + switch o := msg.(type) { + case *db.StoredGroupMessage: + if err = bot.Client.RecallGroupMessage(o.GroupCode, o.Attribute.MessageSeq, o.Attribute.InternalID); err != nil { log.Warnf("撤回 %v 失败: %v", messageID, err) return Failed(100, "RECALL_API_ERROR", err.Error()) } - } else { - if msg["sender"].(message.Sender).Uin != bot.Client.Uin { + case *db.StoredPrivateMessage: + if o.Attribute.SenderUin != bot.Client.Uin { log.Warnf("撤回 %v 失败: 好友会话无法撤回对方消息.", messageID) return Failed(100, "CANNOT_RECALL_FRIEND_MSG", "无法撤回对方消息") } - if err := bot.Client.RecallPrivateMessage(msg["target"].(int64), int64(msg["time"].(int32)), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil { + if err = bot.Client.RecallPrivateMessage(o.TargetUin, o.Attribute.Timestamp, o.Attribute.MessageSeq, o.Attribute.InternalID); err != nil { log.Warnf("撤回 %v 失败: %v", messageID, err) return Failed(100, "RECALL_API_ERROR", err.Error()) } + default: + return Failed(100, "UNKNOWN_ERROR") } return OK(nil) } @@ -1115,38 +1114,32 @@ func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG { // // https://git.io/Jtz1b func (bot *CQBot) CQGetMessage(messageID int32) global.MSG { - msg := bot.GetMessage(messageID) - if msg == nil { + msg, err := bot.db.GetMessageByGlobalID(messageID) + if err != nil { + log.Warnf("获取消息时出现错误: %v", err) return Failed(100, "MSG_NOT_FOUND", "消息不存在") } - sender := msg["sender"].(message.Sender) - gid, isGroup := msg["group"] - raw := msg["message"].(string) - return OK(global.MSG{ - "message_id": messageID, - "real_id": msg["message-id"], - "message_seq": msg["message-id"], - "group": isGroup, - "group_id": gid, - "message_type": func() string { - if isGroup { - return "group" - } - return "private" - }(), + m := global.MSG{ + "message_id": msg.GetGlobalID(), + "message_id_v2": msg.GetID(), + "message_type": msg.GetType(), + "real_id": msg.GetAttribute().MessageSeq, + "message_seq": msg.GetAttribute().MessageSeq, + "group": msg.GetType() == "group", "sender": global.MSG{ - "user_id": sender.Uin, - "nickname": sender.Nickname, + "user_id": msg.GetAttribute().SenderUin, + "nickname": msg.GetAttribute().SenderName, }, - "time": msg["time"], - "raw_message": raw, - "message": ToFormattedMessage(bot.ConvertStringMessage(raw, isGroup), func() int64 { - if isGroup { - return gid.(int64) - } - return 0 - }(), false), - }) + "time": msg.GetAttribute().Timestamp, + } + switch o := msg.(type) { + case *db.StoredGroupMessage: + m["group_id"] = o.GroupCode + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, true), o.GroupCode, false) + case *db.StoredPrivateMessage: + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, false), 0, false) + } + return OK(m) } // CQGetGroupSystemMessages 扩展API-获取群文件系统消息 @@ -1308,18 +1301,13 @@ func (bot *CQBot) CQGetStatus() global.MSG { // // https://docs.go-cqhttp.org/api/#%E8%AE%BE%E7%BD%AE%E7%B2%BE%E5%8D%8E%E6%B6%88%E6%81%AF func (bot *CQBot) CQSetEssenceMessage(messageID int32) global.MSG { - msg := bot.GetMessage(messageID) - if msg == nil { + msg, err := bot.db.GetGroupMessageByGlobalID(messageID) + if err != nil { return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在") } - if _, ok := msg["group"]; ok { - if err := bot.Client.SetEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil { - log.Warnf("设置精华消息 %v 失败: %v", messageID, err) - return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error()) - } - } else { - log.Warnf("设置精华消息 %v 失败: 非群聊", messageID) - return Failed(100, "SET_ESSENCE_MSG_ERROR", "非群聊") + if err := bot.Client.SetEssenceMessage(msg.GroupCode, msg.Attribute.MessageSeq, msg.Attribute.InternalID); err != nil { + log.Warnf("设置精华消息 %v 失败: %v", messageID, err) + return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error()) } return OK(nil) } @@ -1328,18 +1316,13 @@ func (bot *CQBot) CQSetEssenceMessage(messageID int32) global.MSG { // // https://docs.go-cqhttp.org/api/#%E7%A7%BB%E5%87%BA%E7%B2%BE%E5%8D%8E%E6%B6%88%E6%81%AF func (bot *CQBot) CQDeleteEssenceMessage(messageID int32) global.MSG { - msg := bot.GetMessage(messageID) - if msg == nil { + msg, err := bot.db.GetGroupMessageByGlobalID(messageID) + if err != nil { return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在") } - if _, ok := msg["group"]; ok { - if err := bot.Client.DeleteEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil { - log.Warnf("移出精华消息 %v 失败: %v", messageID, err) - return Failed(100, "DEL_ESSENCE_MSG_ERROR", err.Error()) - } - } else { - log.Warnf("移出精华消息 %v 失败: 非群聊", messageID) - return Failed(100, "DEL_ESSENCE_MSG_ERROR", "非群聊") + if err := bot.Client.DeleteEssenceMessage(msg.GroupCode, msg.Attribute.MessageSeq, msg.Attribute.InternalID); err != nil { + log.Warnf("删除精华消息 %v 失败: %v", messageID, err) + return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error()) } return OK(nil) } @@ -1366,7 +1349,7 @@ func (bot *CQBot) CQGetEssenceMessageList(groupCode int64) global.MSG { "sender_id": m.SenderUin, "operator_id": m.AddDigestUin, } - msg["message_id"] = toGlobalID(groupCode, int32(m.MessageID)) + msg["message_id"] = db.ToGlobalID(groupCode, int32(m.MessageID)) list = append(list, msg) } return OK(list) @@ -1452,18 +1435,17 @@ func (bot *CQBot) CQSetModelShow(modelName string, modelShow string) global.MSG // CQMarkMessageAsRead 标记消息已读 func (bot *CQBot) CQMarkMessageAsRead(msgID int32) global.MSG { - m := bot.GetMessage(msgID) - if m == nil { + m, err := bot.db.GetMessageByGlobalID(msgID) + if err != nil { return Failed(100, "MSG_NOT_FOUND", "消息不存在") } - if _, ok := m["group"]; ok { - bot.Client.MarkGroupMessageReaded(m["group"].(int64), int64(m["message-id"].(int32))) + switch o := m.(type) { + case *db.StoredGroupMessage: + bot.Client.MarkGroupMessageReaded(o.GroupCode, int64(o.Attribute.MessageSeq)) return OK(nil) + case *db.StoredPrivateMessage: + bot.Client.MarkPrivateMessageReaded(o.SessionUin, o.Attribute.Timestamp) } - if _, ok := m["from-group"]; ok { - return Failed(100, "MSG_TYPE_ERROR", "不支持标记临时会话") - } - bot.Client.MarkPrivateMessageReaded(m["sender"].(message.Sender).Uin, int64(m["time"].(int32))) return OK(nil) } diff --git a/coolq/bot.go b/coolq/bot.go index f92744c..2ba3785 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -2,11 +2,10 @@ package coolq import ( "bytes" - "encoding/gob" "encoding/hex" "encoding/json" "fmt" - "hash/crc32" + "github.com/Mrs4s/go-cqhttp/db" "io" "os" "path" @@ -22,8 +21,6 @@ import ( "github.com/Mrs4s/MiraiGo/utils" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/opt" "github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global/config" @@ -36,7 +33,7 @@ type CQBot struct { lock sync.RWMutex events []func(*Event) - db *leveldb.DB + db db.IDatabase friendReqCache sync.Map tempSessionCache sync.Map } @@ -111,15 +108,11 @@ func NewQQBot(cli *client.QQClient, conf *config.Config) *CQBot { enableLevelDB = lconf.Enable } if enableLevelDB { - p := path.Join("data", "leveldb") - db, err := leveldb.OpenFile(p, &opt.Options{ - WriteBuffer: 128 * opt.KiB, - }) - if err != nil { - log.Fatalf("打开数据库失败, 如果频繁遇到此问题请清理 data/leveldb 文件夹或关闭数据库功能。") + level := db.UseLevelDB() + if err := level.Open(); err != nil { + log.Fatalf("打开数据库失败: %v", err) } - bot.db = db - gob.Register(message.Sender{}) + bot.db = level log.Info("信息数据库初始化完成.") } else { log.Warn("警告: 信息数据库已关闭,将无法使用 [回复/撤回] 等功能。") @@ -182,22 +175,6 @@ func (bot *CQBot) OnEventPush(f func(e *Event)) { bot.lock.Unlock() } -// GetMessage 获取给定消息id对应的消息 -func (bot *CQBot) GetMessage(mid int32) global.MSG { - if bot.db != nil { - m := global.MSG{} - data, err := bot.db.Get(binary.ToBytes(mid), nil) - if err == nil { - err = gob.NewDecoder(bytes.NewReader(data)).Decode(&m) - if err == nil { - return m - } - } - log.Warnf("获取信息时出现错误: %v id: %v", err, mid) - } - return nil -} - // UploadLocalImageAsGroup 上传本地图片至群聊 func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElement) (i *message.GroupImageElement, err error) { if img.File != "" { @@ -367,7 +344,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen if session == nil && groupID != 0 { msg := bot.Client.SendGroupTempMessage(groupID, target, m) if msg != nil { - id = bot.InsertTempMessage(target, msg) + // id = bot.InsertTempMessage(target, msg) } break } @@ -377,7 +354,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen break } if msg != nil { - id = bot.InsertTempMessage(target, msg) + // id = bot.InsertTempMessage(target, msg) } } case unidirectionalFriendExists(): // 单向好友 @@ -397,57 +374,91 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen // InsertGroupMessage 群聊消息入数据库 func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { - val := global.MSG{ - "message-id": m.Id, - "internal-id": m.InternalId, - "group": m.GroupCode, - "group-name": m.GroupName, - "sender": m.Sender, - "time": m.Time, - "message": ToStringMessage(m.Elements, m.GroupCode, true), + t := &message.SendingMessage{Elements: m.Elements} + replyElem := t.FirstOrNil(func(e message.IMessageElement) bool { + _, ok := e.(*message.ReplyElement) + return ok + }) + msg := &db.StoredGroupMessage{ + ID: encodeMessageId(m.GroupCode, m.Id), + GlobalID: db.ToGlobalID(m.GroupCode, m.Id), + SubType: "normal", + Attribute: &db.StoredMessageAttribute{ + MessageSeq: m.Id, + InternalID: m.InternalId, + SenderUin: m.Sender.Uin, + SenderName: m.Sender.DisplayName(), + Timestamp: int64(m.Time), + }, + GroupCode: m.GroupCode, + AnonymousID: func() string { + if m.Sender.IsAnonymous() { + return m.Sender.AnonymousInfo.AnonymousId + } + return "" + }(), + Content: ToMessageContent(m.Elements), } - id := toGlobalID(m.GroupCode, m.Id) - if bot.db != nil { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - if err := gob.NewEncoder(buf).Encode(val); err != nil { - log.Warnf("记录聊天数据时出现错误: %v", err) - return -1 - } - if err := bot.db.Put(binary.ToBytes(id), buf.Bytes(), nil); err != nil { - log.Warnf("记录聊天数据时出现错误: %v", err) - return -1 + if replyElem != nil { + reply := replyElem.(*message.ReplyElement) + msg.SubType = "quote" + msg.QuotedInfo = &db.QuotedInfo{ + PrevID: encodeMessageId(m.GroupCode, reply.ReplySeq), + PrevGlobalID: db.ToGlobalID(m.GroupCode, reply.ReplySeq), + QuotedContent: ToMessageContent(reply.Elements), } } - return id + if err := bot.db.InsertGroupMessage(msg); err != nil { + log.Warnf("记录聊天数据时出现错误: %v", err) + return -1 + } + return msg.GlobalID } // InsertPrivateMessage 私聊消息入数据库 func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage) int32 { - val := global.MSG{ - "message-id": m.Id, - "internal-id": m.InternalId, - "target": m.Target, - "sender": m.Sender, - "time": m.Time, - "message": ToStringMessage(m.Elements, 0, true), + t := &message.SendingMessage{Elements: m.Elements} + replyElem := t.FirstOrNil(func(e message.IMessageElement) bool { + _, ok := e.(*message.ReplyElement) + return ok + }) + msg := &db.StoredPrivateMessage{ + ID: encodeMessageId(m.Sender.Uin, m.Id), + GlobalID: db.ToGlobalID(m.Sender.Uin, m.Id), + SubType: "normal", + Attribute: &db.StoredMessageAttribute{ + MessageSeq: m.Id, + InternalID: m.InternalId, + SenderUin: m.Sender.Uin, + SenderName: m.Sender.DisplayName(), + Timestamp: int64(m.Time), + }, + SessionUin: func() int64 { + if m.Sender.Uin == m.Self { + return m.Target + } + return m.Sender.Uin + }(), + TargetUin: m.Target, + Content: ToMessageContent(m.Elements), } - id := toGlobalID(m.Sender.Uin, m.Id) - if bot.db != nil { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - if err := gob.NewEncoder(buf).Encode(val); err != nil { - log.Warnf("记录聊天数据时出现错误: %v", err) - return -1 - } - if err := bot.db.Put(binary.ToBytes(id), buf.Bytes(), nil); err != nil { - log.Warnf("记录聊天数据时出现错误: %v", err) - return -1 + if replyElem != nil { + reply := replyElem.(*message.ReplyElement) + msg.SubType = "quote" + msg.QuotedInfo = &db.QuotedInfo{ + PrevID: encodeMessageId(reply.Sender, reply.ReplySeq), + PrevGlobalID: db.ToGlobalID(reply.Sender, reply.ReplySeq), + QuotedContent: ToMessageContent(m.Elements), } } - return id + if err := bot.db.InsertPrivateMessage(msg); err != nil { + log.Warnf("记录聊天数据时出现错误: %v", err) + return -1 + } + return msg.GlobalID } +/* // InsertTempMessage 临时消息入数据库 func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 { val := global.MSG{ @@ -460,7 +471,7 @@ func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 "time": int32(time.Now().Unix()), "message": ToStringMessage(m.Elements, 0, true), } - id := toGlobalID(m.Sender.Uin, m.Id) + id := db.ToGlobalID(m.Sender.Uin, m.Id) if bot.db != nil { buf := global.NewBuffer() defer global.PutBuffer(buf) @@ -475,17 +486,11 @@ func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 } return id } - -// toGlobalID 构建`code`-`msgID`的字符串并返回其CRC32 Checksum的值 -func toGlobalID(code int64, msgID int32) int32 { - return int32(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d-%d", code, msgID)))) -} +*/ // Release 释放Bot实例 func (bot *CQBot) Release() { - if bot.db != nil { - _ = bot.db.Close() - } + } func (bot *CQBot) dispatchEventMessage(m global.MSG) { @@ -636,3 +641,11 @@ func IsLawfulImage(r io.ReadSeeker) (bool, string) { } return false, t.String() } + +// encodeMessageId 临时先这样, 暂时用不上 +func encodeMessageId(target int64, seq int32) string { + return hex.EncodeToString(binary.NewWriterF(func(w *binary.Writer) { + w.WriteUInt64(uint64(target)) + w.WriteUInt32(uint32(seq)) + })) +} diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 28c81e1..eafc692 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -7,6 +7,7 @@ import ( xml2 "encoding/xml" "errors" "fmt" + "github.com/Mrs4s/go-cqhttp/db" "io" "math/rand" "net/url" @@ -141,7 +142,7 @@ func ToArrayMessage(e []message.IMessageElement, groupID int64) (r []global.MSG) r = append(r, global.MSG{ "type": "reply", "data": map[string]string{ - "id": strconv.FormatInt(int64(toGlobalID(rid, replyElem.ReplySeq)), 10), + "id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10), "seq": strconv.FormatInt(int64(replyElem.ReplySeq), 10), "qq": strconv.FormatInt(replyElem.Sender, 10), "time": strconv.FormatInt(int64(replyElem.Time), 10), @@ -151,7 +152,7 @@ func ToArrayMessage(e []message.IMessageElement, groupID int64) (r []global.MSG) } else { r = append(r, global.MSG{ "type": "reply", - "data": map[string]string{"id": strconv.FormatInt(int64(toGlobalID(rid, replyElem.ReplySeq)), 10)}, + "data": map[string]string{"id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10)}, }) } } @@ -281,11 +282,11 @@ func ToStringMessage(e []message.IMessageElement, groupID int64, isRaw ...bool) } if ExtraReplyData { write("[CQ:reply,id=%d,seq=%d,qq=%d,time=%d,text=%s]", - toGlobalID(rid, replyElem.ReplySeq), + db.ToGlobalID(rid, replyElem.ReplySeq), replyElem.ReplySeq, replyElem.Sender, replyElem.Time, CQCodeEscapeValue(ToStringMessage(replyElem.Elements, groupID))) } else { - write("[CQ:reply,id=%d]", toGlobalID(rid, replyElem.ReplySeq)) + write("[CQ:reply,id=%d]", db.ToGlobalID(rid, replyElem.ReplySeq)) } } for i, elem := range e { @@ -361,6 +362,110 @@ func ToStringMessage(e []message.IMessageElement, groupID int64, isRaw ...bool) return } +// ToMessageContent 将消息转换成 Content. 忽略 Reply +// 不同于 onebot 的 Array Message, 此函数转换出来的 Content 的 data 段为实际类型 +// 方便数据库查询 +func ToMessageContent(e []message.IMessageElement) (r []global.MSG) { + for _, elem := range e { + var m global.MSG + switch o := elem.(type) { + case *message.TextElement: + m = global.MSG{ + "type": "text", + "data": global.MSG{"text": o.Content}, + } + case *message.LightAppElement: + m = global.MSG{ + "type": "json", + "data": global.MSG{"data": o.Content}, + } + case *message.AtElement: + if o.Target == 0 { + m = global.MSG{ + "type": "at", + "data": global.MSG{ + "subType": "all", + }, + } + } else { + m = global.MSG{ + "type": "at", + "data": global.MSG{ + "subType": "user", + "target": o.Target, + "display": o.Display, + }, + } + } + case *message.RedBagElement: + m = global.MSG{ + "type": "redbag", + "data": global.MSG{"title": o.Title, "type": o.MsgType}, + } + case *message.ForwardElement: + m = global.MSG{ + "type": "forward", + "data": global.MSG{"id": o.ResId}, + } + case *message.FaceElement: + m = global.MSG{ + "type": "face", + "data": global.MSG{"id": o.Index}, + } + case *message.VoiceElement: + m = global.MSG{ + "type": "record", + "data": global.MSG{"file": o.Name, "url": o.Url}, + } + case *message.ShortVideoElement: + m = global.MSG{ + "type": "video", + "data": global.MSG{"file": o.Name, "url": o.Url}, + } + case *message.GroupImageElement: + data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url, "subType": uint32(o.ImageBizType)} + switch { + case o.Flash: + data["type"] = "flash" + case o.EffectID != 0: + data["type"] = "show" + data["id"] = o.EffectID + } + m = global.MSG{ + "type": "image", + "data": data, + } + case *message.FriendImageElement: + data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url} + if o.Flash { + data["type"] = "flash" + } + m = global.MSG{ + "type": "image", + "data": data, + } + case *message.ServiceElement: + if isOk := strings.Contains(o.Content, "= 40006 { + id = 40000 + } + } + } + switch img := e.(type) { + case *LocalImageElement: + img.Flash = flash + img.EffectID = id + case *message.GroupImageElement: + img.Flash = flash + img.EffectID = id + img.ImageBizType = message.ImageBizType(data["subType"].(uint32)) + case *message.FriendImageElement: + img.Flash = flash + } + r = append(r, e) + case "at": + switch data["subType"].(string) { + case "all": + r = append(r, message.NewAt(0)) + case "user": + r = append(r, message.NewAt(data["target"].(int64), data["display"].(string))) + default: + continue + } + case "redbag": + r = append(r, &message.RedBagElement{ + MsgType: message.RedBagMessageType(data["type"].(int)), + Title: data["title"].(string), + }) + case "forward": + r = append(r, &message.ForwardElement{ + ResId: data["id"].(string), + }) + case "face": + r = append(r, message.NewFace(data["id"].(int32))) + case "video": + e, err := bot.makeImageOrVideoElem(map[string]string{"file": data["file"].(string)}, true, group) + if err != nil { + log.Warnf("make image elem error: %v", err) + continue + } + r = append(r, e) + } + } + return +} + // ToElement 将解码后的CQCode转换为Element. // // 返回 interface{} 存在三种类型 diff --git a/coolq/event.go b/coolq/event.go index 03f35d6..e230e4e 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -2,6 +2,7 @@ package coolq import ( "encoding/hex" + "github.com/Mrs4s/go-cqhttp/db" "os" "path" "strconv" @@ -112,7 +113,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEven bot.tempSessionCache.Store(m.Sender.Uin, e.Session) id := m.Id if bot.db != nil { - id = bot.InsertTempMessage(m.Sender.Uin, m) + // id = bot.InsertTempMessage(m.Sender.Uin, m) } log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm) tm := global.MSG{ @@ -178,7 +179,7 @@ func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent) func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMessageRecalledEvent) { g := c.FindGroup(e.GroupCode) - gid := toGlobalID(e.GroupCode, e.MessageId) + gid := db.ToGlobalID(e.GroupCode, e.MessageId) log.Infof("群 %v 内 %v 撤回了 %v 的消息: %v.", formatGroupName(g), formatMemberName(g.FindMember(e.OperatorUin)), formatMemberName(g.FindMember(e.AuthorUin)), gid) bot.dispatchEventMessage(global.MSG{ @@ -295,7 +296,7 @@ func (bot *CQBot) memberTitleUpdatedEvent(c *client.QQClient, e *client.MemberSp func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageRecalledEvent) { f := c.FindFriend(e.FriendUin) - gid := toGlobalID(e.FriendUin, e.MessageId) + gid := db.ToGlobalID(e.FriendUin, e.MessageId) if f != nil { log.Infof("好友 %v(%v) 撤回了消息: %v", f.Nickname, f.Uin, gid) } else { @@ -472,7 +473,7 @@ func (bot *CQBot) otherClientStatusChangedEvent(c *client.QQClient, e *client.Ot func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent) { g := c.FindGroup(e.GroupCode) - gid := toGlobalID(e.GroupCode, e.MessageID) + gid := db.ToGlobalID(e.GroupCode, e.MessageID) if e.OperationType == 1 { log.Infof( "群 %v 内 %v 将 %v 的消息(%v)设为了精华消息.", diff --git a/db/database.go b/db/database.go new file mode 100644 index 0000000..0801609 --- /dev/null +++ b/db/database.go @@ -0,0 +1,104 @@ +package db + +import ( + "fmt" + "github.com/Mrs4s/go-cqhttp/global" + "hash/crc32" +) + +type ( + // IDatabase 数据库操作接口定义 + IDatabase interface { + // Open 初始化数据库 + Open() error + + // GetMessageByGlobalID 通过 GlobalID 来获取消息 + GetMessageByGlobalID(int32) (IStoredMessage, error) + // GetGroupMessageByGlobalID 通过 GlobalID 来获取群消息 + GetGroupMessageByGlobalID(int32) (*StoredGroupMessage, error) + // GetPrivateMessageByGlobalID 通过 GlobalID 来获取私聊消息 + GetPrivateMessageByGlobalID(int32) (*StoredPrivateMessage, error) + + // InsertGroupMessage 向数据库写入新的群消息 + InsertGroupMessage(*StoredGroupMessage) error + // InsertPrivateMessage 向数据库写入新的私聊消息 + InsertPrivateMessage(*StoredPrivateMessage) error + } + + IStoredMessage interface { + GetID() string + GetType() string + GetGlobalID() int32 + GetAttribute() *StoredMessageAttribute + GetContent() []global.MSG + } + + // StoredGroupMessage 持久化群消息 + StoredGroupMessage struct { + ID string `bson:"_id"` + GlobalID int32 `bson:"globalId"` + Attribute *StoredMessageAttribute `bson:"attribute"` + SubType string `bson:"subType"` + QuotedInfo *QuotedInfo `bson:"quotedInfo"` + GroupCode int64 `bson:"groupCode"` + AnonymousID string `bson:"anonymousId"` + Content []global.MSG `bson:"content"` + } + + // StoredPrivateMessage 持久化私聊消息 + StoredPrivateMessage struct { + ID string `bson:"_id"` + GlobalID int32 `bson:"globalId"` + Attribute *StoredMessageAttribute `bson:"attribute"` + SubType string `bson:"subType"` + QuotedInfo *QuotedInfo `bson:"quotedInfo"` + SessionUin int64 `bson:"sessionUin"` + TargetUin int64 `bson:"targetUin"` + Content []global.MSG `bson:"content"` + } + + // StoredMessageAttribute 持久化消息属性 + StoredMessageAttribute struct { + MessageSeq int32 `bson:"messageSeq"` + InternalID int32 `bson:"internalId"` + SenderUin int64 `bson:"senderUin"` + SenderName string `bson:"senderName"` + Timestamp int64 `bson:"timestamp"` + } + + // QuotedInfo 引用回复 + QuotedInfo struct { + PrevID string `bson:"prevId"` + PrevGlobalID int32 `bson:"prevGlobalId"` + QuotedContent []global.MSG `bson:"quotedContent"` + } + + // MultiDatabase todo + MultiDatabase struct { + } +) + +// ToGlobalID 构建`code`-`msgID`的字符串并返回其CRC32 Checksum的值 +func ToGlobalID(code int64, msgID int32) int32 { + return int32(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d-%d", code, msgID)))) +} + +func (m *StoredGroupMessage) GetID() string { return m.ID } + +func (m *StoredGroupMessage) GetType() string { return "group" } + +func (m *StoredGroupMessage) GetGlobalID() int32 { return m.GlobalID } + +func (m *StoredGroupMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute } + +func (m *StoredGroupMessage) GetContent() []global.MSG { return m.Content } + +func (m *StoredPrivateMessage) GetID() string { return m.ID } + +func (m *StoredPrivateMessage) GetType() string { return "private" } + +func (m *StoredPrivateMessage) GetGlobalID() int32 { return m.GlobalID } + +func (m *StoredPrivateMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute } + +func (m *StoredPrivateMessage) GetContent() []global.MSG { return m.Content } diff --git a/db/leveldb.go b/db/leveldb.go new file mode 100644 index 0000000..d277cbc --- /dev/null +++ b/db/leveldb.go @@ -0,0 +1,116 @@ +package db + +import ( + "bytes" + "encoding/gob" + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/go-cqhttp/global" + "github.com/pkg/errors" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + "path" +) + +type LevelDBImpl struct { + db *leveldb.DB +} + +const ( + group byte = 0x0 + private byte = 0x1 +) + +func UseLevelDB() IDatabase { + gob.Register(StoredMessageAttribute{}) + gob.Register(QuotedInfo{}) + gob.Register(global.MSG{}) + gob.Register(StoredGroupMessage{}) + gob.Register(StoredPrivateMessage{}) + return &LevelDBImpl{} +} + +func (db *LevelDBImpl) Open() error { + p := path.Join("data", "leveldb-v2") + d, err := leveldb.OpenFile(p, &opt.Options{ + WriteBuffer: 128 * opt.KiB, + }) + if err != nil { + return errors.Wrap(err, "open level db error") + } + db.db = d + return nil +} + +func (db *LevelDBImpl) GetMessageByGlobalID(id int32) (IStoredMessage, error) { + v, err := db.db.Get(binary.ToBytes(id), nil) + if err != nil { + return nil, errors.Wrap(err, "get value error") + } + r := binary.NewReader(v) + switch r.ReadByte() { + case group: + g := &StoredGroupMessage{} + if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(g); err != nil { + return nil, errors.Wrap(err, "decode message error") + } + return g, nil + case private: + p := &StoredPrivateMessage{} + if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(p); err != nil { + return nil, errors.Wrap(err, "decode message error") + } + return p, nil + default: + return nil, errors.New("unknown message flag") + } +} + +func (db *LevelDBImpl) GetGroupMessageByGlobalID(id int32) (*StoredGroupMessage, error) { + i, err := db.GetMessageByGlobalID(id) + if err != nil { + return nil, err + } + g, ok := i.(*StoredGroupMessage) + if !ok { + return nil, errors.New("message type error") + } + return g, nil +} + +func (db *LevelDBImpl) GetPrivateMessageByGlobalID(id int32) (*StoredPrivateMessage, error) { + i, err := db.GetMessageByGlobalID(id) + if err != nil { + return nil, err + } + p, ok := i.(*StoredPrivateMessage) + if !ok { + return nil, errors.New("message type error") + } + return p, nil +} + +func (db *LevelDBImpl) InsertGroupMessage(msg *StoredGroupMessage) error { + buf := global.NewBuffer() + defer global.PutBuffer(buf) + if err := gob.NewEncoder(buf).Encode(msg); err != nil { + return errors.Wrap(err, "encode message error") + } + err := db.db.Put(binary.ToBytes(msg.GlobalID), binary.NewWriterF(func(w *binary.Writer) { + w.WriteByte(group) + w.Write(buf.Bytes()) + }), nil) + return errors.Wrap(err, "put data error") +} + +func (db *LevelDBImpl) InsertPrivateMessage(msg *StoredPrivateMessage) error { + buf := global.NewBuffer() + defer global.PutBuffer(buf) + if err := gob.NewEncoder(buf).Encode(msg); err != nil { + return errors.Wrap(err, "encode message error") + } + err := db.db.Put(binary.ToBytes(msg.GlobalID), binary.NewWriterF(func(w *binary.Writer) { + w.WriteByte(private) + w.Write(buf.Bytes()) + }), nil) + return errors.Wrap(err, "put data error") +} diff --git a/server/http.go b/server/http.go index 24e780f..3329376 100644 --- a/server/http.go +++ b/server/http.go @@ -75,7 +75,7 @@ func (h *httpCtx) Get(s string) gjson.Result { func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ctx httpCtx - contentType := request.Header.Get("Content-Type") + contentType := request.Header.Get("Content-SubType") switch request.Method { case http.MethodPost: if strings.Contains(contentType, "application/json") { @@ -119,7 +119,7 @@ func (s *httpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request log.Debugf("HTTPServer接收到API调用: %v", action) ret := s.api.callAPI(action, &ctx) - writer.Header().Set("Content-Type", "application/json; charset=utf-8") + writer.Header().Set("Content-SubType", "application/json; charset=utf-8") writer.WriteHeader(http.StatusOK) _ = json.NewEncoder(writer).Encode(ret) }