diff --git a/coolq/api.go b/coolq/api.go index 75a5ff2..e442798 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -403,7 +403,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) MSG { if m.Type != gjson.JSON { return Failed(100) } - var sendNodes []*message.ForwardNode + fm := message.NewForwardMessage() ts := time.Now().Add(-time.Minute * 5) hasCustom := false m.ForEach(func(_, item gjson.Result) bool { @@ -414,8 +414,23 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) MSG { return true }) - var convert func(e gjson.Result) []*message.ForwardNode - convert = func(e gjson.Result) (nodes []*message.ForwardNode) { + var resolveElement = func(elems []message.IMessageElement) []message.IMessageElement { + for i, elem := range elems { + switch elem.(type) { + case *LocalImageElement, *LocalVideoElement: + gm, err := bot.uploadMedia(elem, groupID, true) + if err != nil { + log.Warnf("警告: 群 %d %s上传失败: %v", groupID, elem.Type().String(), err) + continue + } + elems[i] = gm + } + } + return elems + } + + var convert func(e gjson.Result) *message.ForwardNode + convert = func(e gjson.Result) *message.ForwardNode { if e.Get("type").Str != "node" { return nil } @@ -425,7 +440,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) MSG { m := bot.GetMessage(int32(i)) if m != nil { sender := m["sender"].(message.Sender) - nodes = append(nodes, &message.ForwardNode{ + return &message.ForwardNode{ SenderId: sender.Uin, SenderName: (&sender).DisplayName(), Time: func() int32 { @@ -435,12 +450,11 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) MSG { } return msgTime }(), - Message: bot.ConvertStringMessage(m["message"].(string), true), - }) - return + Message: resolveElement(bot.ConvertStringMessage(m["message"].(string), true)), + } } log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str) - return + return nil } uin := e.Get("data.[user_id,uin].0").Int() msgTime := e.Get("data.time").Int() @@ -450,63 +464,59 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) MSG { name := e.Get("data.name").Str c := e.Get("data.content") if c.IsArray() { - flag := false + nested := false c.ForEach(func(_, value gjson.Result) bool { if value.Get("type").Str == "node" { - flag = true + nested = true return false } return true }) - if flag { - var taowa []*message.ForwardNode + if nested { // 处理嵌套 + nest := message.NewForwardMessage() for _, item := range c.Array() { - taowa = append(taowa, convert(item)...) + node := convert(item) + if node != nil { + nest.AddNode(node) + } } - nodes = append(nodes, &message.ForwardNode{ + elem := bot.Client.UploadGroupForwardMessage(groupID, nest) + return &message.ForwardNode{ SenderId: uin, SenderName: name, Time: int32(msgTime), - Message: []message.IMessageElement{bot.Client.UploadGroupForwardMessage(groupID, &message.ForwardMessage{Nodes: taowa})}, - }) - return + Message: []message.IMessageElement{elem}, + } } } content := bot.ConvertObjectMessage(e.Get("data.content"), true) if uin != 0 && name != "" && len(content) > 0 { - var newElem []message.IMessageElement - for _, elem := range content { - switch elem.(type) { - case *LocalImageElement, *LocalVideoElement: - gm, err := bot.uploadMedia(elem, groupID, true) - if err != nil { - log.Warnf("警告: 群 %d %s上传失败: %v", groupID, elem.Type().String(), err) - continue - } - elem = gm - } - newElem = append(newElem, elem) - } - nodes = append(nodes, &message.ForwardNode{ + return &message.ForwardNode{ SenderId: uin, SenderName: name, Time: int32(msgTime), - Message: newElem, - }) - return + Message: resolveElement(content), + } } log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content)) - return + return nil } if m.IsArray() { for _, item := range m.Array() { - sendNodes = append(sendNodes, convert(item)...) + node := convert(item) + if node != nil { + fm.AddNode(node) + } } } else { - sendNodes = convert(m) + node := convert(m) + if node != nil { + fm.AddNode(node) + } } - if len(sendNodes) > 0 { - ret := bot.Client.SendGroupForwardMessage(groupID, &message.ForwardMessage{Nodes: sendNodes}) + if fm.Length() > 0 { + fe := bot.Client.UploadGroupForwardMessage(groupID, fm) + ret := bot.Client.SendGroupForwardMessage(groupID, fe) if ret == nil || ret.Id == -1 { log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.") return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出") @@ -1408,6 +1418,23 @@ func (bot *CQBot) CQSetModelShow(modelName string, modelShow string) MSG { return OK(nil) } +// CQMarkMessageAsRead 标记消息已读 +func (bot *CQBot) CQMarkMessageAsRead(msgID int32) MSG { + m := bot.GetMessage(msgID) + if m == nil { + return Failed(100, "MSG_NOT_FOUND", "消息不存在") + } + if _, ok := m["group"]; ok { + bot.Client.MarkGroupMessageReaded(m["group"].(int64), int64(m["message-id"].(int32))) + return OK(nil) + } + if _, ok := m["from-group"]; ok { + return Failed(100, "MSG_TYPE_ERROR", "不支持标记临时会话") + } + bot.Client.MarkPrivateMessageReaded(m["sender"].(*message.Sender).Uin, m["time"].(int64)) + return OK(nil) +} + // OK 生成成功返回值 func OK(data interface{}) MSG { return MSG{"data": data, "retcode": 0, "status": "ok"} @@ -1441,11 +1468,12 @@ func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) MSG { // unknown = 0xff return "unknown" }(), - "age": 0, - "area": "", - "join_time": m.JoinTime, - "last_sent_time": m.LastSpeakTime, - "level": strconv.FormatInt(int64(m.Level), 10), + "age": 0, + "area": "", + "join_time": m.JoinTime, + "last_sent_time": m.LastSpeakTime, + "shut_up_timestamp": m.ShutUpTimestamp, + "level": strconv.FormatInt(int64(m.Level), 10), "role": func() string { switch m.Permission { case client.Owner: diff --git a/coolq/bot.go b/coolq/bot.go index 2d8e29b..8ab23fc 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -81,9 +81,9 @@ var ForceFragmented = false // SkipMimeScan 是否跳过Mime扫描 var SkipMimeScan bool -var lawfulImageTypes = []string{"image/png", "image/jpeg", "image/gif", "image/bmp"} +var lawfulImageTypes = [...]string{"image/png", "image/jpeg", "image/gif", "image/bmp", "image/webp"} -var lawfulAudioTypes = []string{ +var lawfulAudioTypes = [...]string{ "audio/mpeg", "audio/flac", "audio/midi", "audio/ogg", "audio/ape", "audio/amr", "audio/wav", "audio/aiff", "audio/mp4", "audio/aac", "audio/x-m4a", @@ -126,6 +126,7 @@ func NewQQBot(cli *client.QQClient, conf *config.Config) *CQBot { bot.Client.OnGroupMessageRecalled(bot.groupRecallEvent) bot.Client.OnGroupNotify(bot.groupNotifyEvent) bot.Client.OnFriendNotify(bot.friendNotifyEvent) + bot.Client.OnMemberSpecialTitleUpdated(bot.memberTitleUpdatedEvent) bot.Client.OnFriendMessageRecalled(bot.friendRecallEvent) bot.Client.OnReceivedOfflineFile(bot.offlineFileEvent) bot.Client.OnJoinGroup(bot.joinGroupEvent) @@ -211,35 +212,32 @@ func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElemen // UploadLocalVideo 上传本地短视频至群聊 func (bot *CQBot) UploadLocalVideo(target int64, v *LocalVideoElement) (*message.ShortVideoElement, error) { - if v.File != "" { - video, err := os.Open(v.File) - if err != nil { - return nil, err - } - defer video.Close() - hash, _ := utils.ComputeMd5AndLength(io.MultiReader(video, v.thumb)) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".cache") - _, _ = video.Seek(0, io.SeekStart) - _, _ = v.thumb.Seek(0, io.SeekStart) - return bot.Client.UploadGroupShortVideo(target, video, v.thumb, cacheFile) + video, err := os.Open(v.File) + if err != nil { + return nil, err } - return &v.ShortVideoElement, nil + defer video.Close() + hash, _ := utils.ComputeMd5AndLength(io.MultiReader(video, v.thumb)) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".cache") + _, _ = video.Seek(0, io.SeekStart) + _, _ = v.thumb.Seek(0, io.SeekStart) + return bot.Client.UploadGroupShortVideo(target, video, v.thumb, cacheFile) } // UploadLocalImageAsPrivate 上传本地图片至私聊 func (bot *CQBot) UploadLocalImageAsPrivate(userID int64, img *LocalImageElement) (i *message.FriendImageElement, err error) { - if img.Stream != nil { - i, err = bot.Client.UploadPrivateImage(userID, img.Stream) - } else { - // need update. - f, e := os.Open(img.File) - if e != nil { - return nil, e + if img.File != "" { + f, err := os.Open(img.File) + if err != nil { + return nil, errors.Wrap(err, "open image error") } - defer f.Close() - i, err = bot.Client.UploadPrivateImage(userID, f) + defer func() { _ = f.Close() }() + img.Stream = f } - + if lawful, mime := IsLawfulImage(img.Stream); !lawful { + return nil, errors.New("image type error: " + mime) + } + i, err = bot.Client.UploadPrivateImage(userID, img.Stream) if i != nil { i.Flash = img.Flash } @@ -428,7 +426,7 @@ func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 val := MSG{ "message-id": m.Id, // FIXME(InsertTempMessage) InternalId missing - "group": m.GroupCode, + "from-group": m.GroupCode, "group-name": m.GroupName, "target": target, "sender": m.Sender, @@ -516,7 +514,7 @@ func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) MSG { "user_id": m.Sender.Uin, }, "sub_type": "normal", - "time": time.Now().Unix(), + "time": m.Time, "user_id": m.Sender.Uin, } if m.Sender.IsAnonymous() { @@ -604,6 +602,10 @@ func IsLawfulImage(r io.ReadSeeker) (bool, string) { log.Debugf("扫描 Mime 时出现问题: %v", err) return false, "" } - mime := t.String() - return mimetype.EqualsAny(mime, lawfulImageTypes...), mime + for _, lt := range lawfulImageTypes { + if t.Is(lt) { + return true, t.String() + } + } + return false, t.String() } diff --git a/coolq/cqcode.go b/coolq/cqcode.go index e7dce37..ff3a863 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -81,7 +81,6 @@ type LocalVoiceElement struct { // LocalVideoElement 本地视频 type LocalVideoElement struct { - message.ShortVideoElement File string thumb io.ReadSeeker } @@ -97,6 +96,11 @@ func (e *GiftElement) Type() message.ElementType { return message.At } +// Type impl message.IMessageElement +func (e *LocalVideoElement) Type() message.ElementType { + return message.Video +} + // GiftID 礼物ID数组 var GiftID = [...]message.GroupGift{ message.SweetWink, @@ -731,9 +735,16 @@ func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m inte return nil, err } if !SkipMimeScan && !global.IsAMRorSILK(data) { - mt := mimetype.Detect(data).String() - if !mimetype.EqualsAny(mt, lawfulAudioTypes...) { - return nil, errors.New("audio type error: " + mt) + mt := mimetype.Detect(data) + lawful := false + for _, lt := range lawfulAudioTypes { + if mt.Is(lt) { + lawful = true + break + } + } + if !lawful { + return nil, errors.New("audio type error: " + mt.String()) } } if !global.IsAMRorSILK(data) { @@ -895,7 +906,10 @@ func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m inte if err != nil { return nil, err } - v := file.(*LocalVideoElement) + v, ok := file.(*LocalVideoElement) + if !ok { + return file, nil + } if v.File == "" { return v, nil } @@ -1117,14 +1131,14 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) ( if path.Ext(rawPath) == ".video" { b, _ := os.ReadFile(rawPath) r := binary.NewReader(b) - return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 + return &message.ShortVideoElement{ // todo 检查缓存是否有效 Md5: r.ReadBytes(16), ThumbMd5: r.ReadBytes(16), Size: r.ReadInt32(), ThumbSize: r.ReadInt32(), Name: r.ReadString(), Uuid: r.ReadAvailable(), - }}, nil + }, nil } return &LocalVideoElement{File: rawPath}, nil } @@ -1133,72 +1147,50 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) ( exist = true rawPath = path.Join(global.ImagePathOld, f) } - if !exist && global.PathExists(rawPath+".cqimg") { - exist = true - rawPath += ".cqimg" + if !exist { + if d["url"] != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) + } + return nil, errors.New("invalid image") } - if !exist && d["url"] != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) + if path.Ext(rawPath) != ".image" { + return &LocalImageElement{File: rawPath}, nil } - if exist { - if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { - return &LocalImageElement{File: rawPath}, nil - } - b, err := os.ReadFile(rawPath) - if err != nil { - return nil, err - } - if len(b) < 20 { - return nil, errors.New("invalid local file") - } - var ( - size int32 - hash []byte - imageURL string - ) - if path.Ext(rawPath) == ".cqimg" { - for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { - kv := strings.SplitN(line, "=", 2) - switch kv[0] { - case "md5": - hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) - case "size": - t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) - size = int32(t) - } - } - } else { - r := binary.NewReader(b) - hash = r.ReadBytes(16) - size = r.ReadInt32() - r.ReadString() - imageURL = r.ReadString() - } - if size == 0 { - if imageURL != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, group) - } - return nil, errors.New("img size is 0") - } - if len(hash) != 16 { - return nil, errors.New("invalid hash") - } - var rsp message.IMessageElement - if group { - rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - goto ok - } - rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) - ok: - if err != nil { - if imageURL != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, group) - } - return nil, err - } - return rsp, nil + b, err := os.ReadFile(rawPath) + if err != nil { + return nil, err } - return nil, errors.New("invalid image") + if len(b) < 20 { + return nil, errors.New("invalid local file") + } + r := binary.NewReader(b) + hash := r.ReadBytes(16) + size := r.ReadInt32() + r.ReadString() + imageURL := r.ReadString() + if size == 0 { + if imageURL != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, group) + } + return nil, errors.New("img size is 0") + } + if len(hash) != 16 { + return nil, errors.New("invalid hash") + } + var rsp message.IMessageElement + if group { + rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) + goto ok + } + rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) +ok: + if err != nil { + if imageURL != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, group) + } + return nil, err + } + return rsp, nil } // makeShowPic 一种xml 方式发送的群消息图片 diff --git a/coolq/event.go b/coolq/event.go index 9efb978..8b88c10 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -69,9 +69,6 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess }, } bot.dispatchEventMessage(fm) - if m.Sender.Uin != c.Uin { - c.MarkPrivateMessageReaded(m.Sender.Uin, int64(m.Time)) - } } func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) { @@ -109,9 +106,6 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) } gm["message_id"] = id bot.dispatchEventMessage(gm) - if m.Sender.Uin != c.Uin { - c.MarkGroupMessageReaded(m.GroupCode, int64(m.Id)) - } } func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEvent) { @@ -268,7 +262,11 @@ func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) { func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotifyEvent) { friend := c.FindFriend(e.From()) if notify, ok := e.(*client.FriendPokeNotifyEvent); ok { - log.Infof("好友 %v 戳了戳你.", friend.Nickname) + if notify.Receiver == notify.Sender { + log.Infof("好友 %v 戳了戳自己.", friend.Nickname) + } else { + log.Infof("好友 %v 戳了戳你.", friend.Nickname) + } bot.dispatchEventMessage(MSG{ "post_type": "notice", "notice_type": "notify", @@ -282,6 +280,22 @@ func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotifyEvent) { } } +func (bot *CQBot) memberTitleUpdatedEvent(c *client.QQClient, e *client.MemberSpecialTitleUpdatedEvent) { + group := c.FindGroup(e.GroupCode) + mem := group.FindMember(e.Uin) + log.Infof("群 %v(%v) 内成员 %v(%v) 获得了新的头衔: %v", group.Name, group.Code, mem.DisplayName(), mem.Uin, e.NewTitle) + bot.dispatchEventMessage(MSG{ + "post_type": "notice", + "notice_type": "notify", + "sub_type": "title", + "group_id": group.Code, + "self_id": c.Uin, + "user_id": e.Uin, + "time": time.Now().Unix(), + "title": e.NewTitle, + }) +} + func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageRecalledEvent) { f := c.FindFriend(e.FriendUin) gid := toGlobalID(e.FriendUin, e.MessageId) diff --git a/docs/config.md b/docs/config.md index f622a4b..ab396c8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -222,7 +222,7 @@ database: # 数据库相关设置 在部署前,请在本地完成登录,并将 `config.yml` , `device.json` ,`bootstrap` 和 `go-cqhttp` 一起打包。 -在触发器中创建一个API网关触发器,并启用继承响应, 创建完成后即可通过api网关访问go-cqhttp(建议配置 AccessToken)。 +在触发器中创建一个API网关触发器,并启用集成响应,创建完成后即可通过api网关访问go-cqhttp(建议配置 AccessToken)。 > scripts/bootstrap 中使用的工作路径为 /tmp, 这个目录最大能容下500M文件, 如需长期使用, -> 请挂载文件存储(CFS). \ No newline at end of file +> 请挂载文件存储(CFS). diff --git a/docs/cqhttp.md b/docs/cqhttp.md index eb02d7e..a70b08a 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -7,6 +7,7 @@

##### CQCode + - [图片](#图片) - [回复](#回复) - [红包](#红包) @@ -21,6 +22,7 @@ - [图片](#图片) ##### API + - [设置群名](#设置群名) - [设置群头像](#设置群头像) - [获取图片信息](#获取图片信息) @@ -47,6 +49,7 @@ - [重载事件过滤器](#重载事件过滤器) ##### 事件 + - [群消息撤回](#群消息撤回) - [好友消息撤回](#好友消息撤回) - [好友戳一戳](#好友戳一戳) @@ -112,9 +115,7 @@ Type : `reply` | `time` | int64 | 可选. 自定义回复时的时间, 格式为Unix时间 | | `seq` | int64 | 起始消息序号, 可通过 `get_msg` 获得 | - - -示例: `[CQ:reply,id=123456]` +示例: `[CQ:reply,id=123456]` \ 自定义回复示例: `[CQ:reply,text=Hello World,qq=10086,time=3376656000,seq=5123]` @@ -122,11 +123,11 @@ Type : `reply` ```json { - "type": "music", - "data": { - "type": "163", - "id": "28949129" - } + "type": "music", + "data": { + "type": "163", + "id": "28949129" + } } ``` @@ -143,13 +144,13 @@ Type : `reply` ```json { - "type": "music", - "data": { - "type": "custom", - "url": "http://baidu.com", - "audio": "http://baidu.com/1.mp3", - "title": "音乐标题" - } + "type": "music", + "data": { + "type": "custom", + "url": "http://baidu.com", + "audio": "http://baidu.com/1.mp3", + "title": "音乐标题" + } } ``` @@ -231,11 +232,9 @@ Type: `gift` | 12 | 我超忙的 | | 13 | 爱心口罩 | - - 示例: `[CQ:gift,qq=123456,id=8]` - ### 合并转发 +### 合并转发 Type: `forward` @@ -259,32 +258,33 @@ Type: `node` | 参数名 | 类型 | 说明 | 特殊说明 | | --------- | ------- | -------------- | -------------------------------------------------------------------------------------- | -| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** | +| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** | | `name` | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) | | `uin` | int64 | 发送者QQ号 | 用于自定义消息 | | `content` | message | 具体消息 | 用于自定义消息 | | `seq` | message | 具体消息 | 用于自定义消息 | -特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送,并且由于消息段较为复杂,仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. 另外按 [CQHTTP](https://git.io/JtxtN) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃** +特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送,并且由于消息段较为复杂,仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. +另外按 [CQHTTP](https://git.io/JtxtN) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃** -示例: +示例: 直接引用消息合并转发: ````json [ - { - "type": "node", - "data": { - "id": "123" - } - }, - { - "type": "node", - "data": { - "id": "456" - } + { + "type": "node", + "data": { + "id": "123" } + }, + { + "type": "node", + "data": { + "id": "456" + } + } ] ```` @@ -292,27 +292,29 @@ Type: `node` ````json [ - { - "type": "node", - "data": { - "name": "消息发送者A", - "uin": "10086", - "content": [ - { - "type": "text", - "data": {"text": "测试消息1"} - } - ] - } - }, - { - "type": "node", - "data": { - "name": "消息发送者B", - "uin": "10087", - "content": "[CQ:image,file=xxxxx]测试消息2" + { + "type": "node", + "data": { + "name": "消息发送者A", + "uin": "10086", + "content": [ + { + "type": "text", + "data": { + "text": "测试消息1" + } } + ] } + }, + { + "type": "node", + "data": { + "name": "消息发送者B", + "uin": "10087", + "content": "[CQ:image,file=xxxxx]测试消息2" + } + } ] ```` @@ -320,24 +322,25 @@ Type: `node` ````json [ - { - "type": "node", - "data": { - "name": "自定义发送者", - "uin": "10086", - "content": "我是自定义消息", - "seq": "5123", - "time": "3376656000" - } - }, - { - "type": "node", - "data": { - "id": "123" - } + { + "type": "node", + "data": { + "name": "自定义发送者", + "uin": "10086", + "content": "我是自定义消息", + "seq": "5123", + "time": "3376656000" } + }, + { + "type": "node", + "data": { + "id": "123" + } + } ] ```` + ### 短视频消息 Type: `video` @@ -351,6 +354,7 @@ Type: `video` | `file` | string | 支持http和file发送 | | `cover` | string | 视频封面,支持http,file和base64发送,格式必须为jpg | | `c` | `2` `3` | 通过网络下载视频时的线程数, 默认单线程. (在资源不支持并发时会自动处理) | + 示例: `[CQ:video,file=file:///C:\\Users\Richard\Videos\1.mp4]` ### XML 消息 @@ -375,30 +379,61 @@ Type: `xml` #### qq音乐 ```xml -

陈奕迅 + + + + + + ``` + #### 网易音乐 + ```xml - + + + + + + ``` #### 卡片消息1 + ```xml -生死8秒!女司机高速急刹,他一个操作救下一车性命 - + + 生死8秒!女司机高速急刹,他一个操作救下一车性命 + + ``` #### 卡片消息2 + ```xml - -test title - - + + test title + + ``` @@ -417,23 +452,24 @@ Type: `json` json中的字符串需要进行转义: ->","=> `,` +> ","=> `,` ->"&"=> `&` +> "&"=> `&` ->"["=> `[` +> "["=> `[` ->"]"=> `]` +> "]"=> `]` 否则无法正确得到解析 示例json 的cq码: + ```test [CQ:json,data={"app":"com.tencent.miniapp","desc":"","view":"notification","ver":"0.0.0.1","prompt":"[应用]","appID":"","sourceName":"","actionData":"","actionData_A":"","sourceUrl":"","meta":{"notification":{"appInfo":{"appName":"全国疫情数据统计","appType":4,"appid":1109659848,"iconUrl":"http:\/\/gchat.qpic.cn\/gchatpic_new\/719328335\/-2010394141-6383A777BEB79B70B31CE250142D740F\/0"},"data":[{"title":"确诊","value":"80932"},{"title":"今日确诊","value":"28"},{"title":"疑似","value":"72"},{"title":"今日疑似","value":"5"},{"title":"治愈","value":"60197"},{"title":"今日治愈","value":"1513"},{"title":"死亡","value":"3140"},{"title":"今**亡","value":"17"}],"title":"中国加油,武汉加油","button":[{"name":"病毒:SARS-CoV-2,其导致疾病命名 COVID-19","action":""},{"name":"传染源:新冠肺炎的患者。无症状感染者也可能成为传染源。","action":""}],"emphasis_keyword":""}},"text":"","sourceAd":""}] ``` - ### cardimage + 一种xml的图片消息(装逼大图) ps: xml 接口的消息都存在风控风险,请自行兼容发送失败后的处理(可以失败后走普通图片模式) @@ -454,8 +490,8 @@ Type: `cardimage` | `source` | string | 分享来源的名称,可以留空 | | `icon` | string | 分享来源的icon图标url,可以留空 | - 示例cardimage 的cq码: + ```test [CQ:cardimage,file=https://i.pixiv.cat/img-master/img/2020/03/25/00/00/08/80334602_p0_master1200.jpg] ``` @@ -480,9 +516,9 @@ Type: `tts` ### 设置群名 -终结点: `/set_group_name` +终结点: `/set_group_name` -**参数** +**参数** | 字段 | 类型 | 说明 | | ------------ | ------ | ---- | @@ -491,9 +527,9 @@ Type: `tts` ### 设置群头像 -终结点: `/set_group_portrait` +终结点: `/set_group_portrait` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ------ | ------------------------ | @@ -505,7 +541,8 @@ Type: `tts` - 绝对路径,例如 `file:///C:\\Users\Richard\Pictures\1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089) - 网络 URL,例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg` -- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==` +- Base64 + 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==` [2]`cache`参数: 通过网络 URL 发送时有效,`1`表示使用缓存,`0`关闭关闭缓存,默认 为`1` @@ -513,7 +550,7 @@ Type: `tts` ### 获取图片信息 -终结点: `/get_image` +终结点: `/get_image` > 该接口为 CQHTTP 接口修改 @@ -533,7 +570,7 @@ Type: `tts` ### 获取消息 -终结点: `/get_msg` +终结点: `/get_msg` 参数 @@ -571,28 +608,29 @@ Type: `tts` ````json5 { - "data": { - "messages": [ - { - "content": "合并转发1", - "sender": { - "nickname": "发送者A", - "user_id": 10086 - }, - "time": 1595694374 - }, - { - "content": "合并转发2[CQ:image,file=xxxx,url=xxxx]", - "sender": { - "nickname": "发送者B", - "user_id": 10087 - }, - "time": 1595694393 // 可选 - } - ] - }, - "retcode": 0, - "status": "ok" + "data": { + "messages": [ + { + "content": "合并转发1", + "sender": { + "nickname": "发送者A", + "user_id": 10086 + }, + "time": 1595694374 + }, + { + "content": "合并转发2[CQ:image,file=xxxx,url=xxxx]", + "sender": { + "nickname": "发送者B", + "user_id": 10087 + }, + "time": 1595694393 + // 可选 + } + ] + }, + "retcode": 0, + "status": "ok" } ```` @@ -600,7 +638,7 @@ Type: `tts` 终结点: `/send_group_forward_msg` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | -------------- | ---------------------------- | @@ -608,16 +646,16 @@ Type: `tts` | `messages` | forward node[] | 自定义转发消息, 具体看 [CQCode](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/cqhttp.md#%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF%E8%8A%82%E7%82%B9) | 响应数据 - + | 字段 | 类型 | 说明 | | ------------ | ------ | ------ | | `message_id` | string | 消息id | ### 获取中文分词 -终结点: `/.get_word_slices` +终结点: `/.get_word_slices` -**参数** +**参数** | 字段 | 类型 | 说明 | | --------- | ------ | ---- | @@ -685,9 +723,9 @@ Type: `tts` > 注意: 目前图片OCR接口仅支持接受的图片 -终结点: `/ocr_image` +终结点: `/ocr_image` -**参数** +**参数** | 字段 | 类型 | 说明 | | ------- | ------ | ------ | @@ -708,7 +746,6 @@ Type: `tts` | `confidence` | int32 | 置信度 | | `coordinates` | vector2 | 坐标 | - ### 获取群系统消息 终结点: `/get_group_system_msg` @@ -720,9 +757,9 @@ Type: `tts` | `invited_requests` | InvitedRequest[] | 邀请消息列表 | | `join_requests` | JoinRequest[] | 进群消息列表 | - > 注意: 如果列表不存在任何消息, 将返回 `null` +> 注意: 如果列表不存在任何消息, 将返回 `null` - **InvitedRequest** +**InvitedRequest** | 字段 | 类型 | 说明 | | -------------- | ------ | ----------------- | @@ -734,7 +771,7 @@ Type: `tts` | `checked` | bool | 是否已被处理 | | `actor` | int64 | 处理者, 未处理为0 | - **JoinRequest** +**JoinRequest** | 字段 | 类型 | 说明 | | ---------------- | ------ | ----------------- | @@ -751,7 +788,7 @@ Type: `tts` 终结点: `/get_group_file_system_info` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ----- | ---- | @@ -772,7 +809,7 @@ Type: `tts` 终结点: `/get_group_root_files` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ----- | ---- | @@ -791,7 +828,7 @@ Type: `tts` 终结点: `/get_group_files_by_folder` -**参数** +**参数** | 字段 | 类型 | 说明 | | ----------- | ------ | --------------------------- | @@ -811,7 +848,7 @@ Type: `tts` 终结点: `/get_group_file_url` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ------ | ------------------------- | @@ -825,7 +862,7 @@ Type: `tts` | ----- | ------ | ------------ | | `url` | string | 文件下载链接 | - **File** +**File** | 字段 | 类型 | 说明 | | ---------------- | ------ | ---------------------- | @@ -840,7 +877,7 @@ Type: `tts` | `uploader` | int64 | 上传者ID | | `uploader_name` | string | 上传者名字 | - **Folder** +**Folder** | 字段 | 类型 | 说明 | | ------------------ | ------ | ---------- | @@ -855,7 +892,7 @@ Type: `tts` 终结点: `/upload_group_file` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ------ | ------------------------- | @@ -885,7 +922,6 @@ Type: `tts` **Statistics** - | 字段 | 类型 | 说明 | | ------------------ | ------ | ---------------- | | `packet_received` | uint64 | 收到的数据包总数 | @@ -902,7 +938,7 @@ Type: `tts` 终结点: `/get_group_at_all_remain` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ----- | ---- | @@ -920,7 +956,7 @@ Type: `tts` 终结点: `/download_file` -**参数** +**参数** | 字段 | 类型 | 说明 | | -------------- | --------------- | ------------ | @@ -960,7 +996,7 @@ JSON数组: 终结点:`/get_group_msg_history` -**参数** +**参数** | 字段 | 类型 | 说明 | | ------------- | ----- | ----------------------------------- | @@ -979,7 +1015,7 @@ JSON数组: 终结点:`/get_online_clients` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ---- | ------------ | @@ -1003,7 +1039,7 @@ JSON数组: 终结点:`/check_url_safely` -**参数** +**参数** | 字段 | 类型 | 说明 | | ---------- | ------ | ------------------------- | @@ -1076,13 +1112,22 @@ JSON数组: | `ext_name` | string | 用户昵称 | | `create_time` | int64 | 账号创建时间 | +### 标记消息已读 + +终结点: `/mark_msg_as_read` + +**参数** + +| 字段名 | 数据类型 | 默认值 | 说明 | +| ---------- | -------- | ------ | -------- | +| `message_id` | int32 | | 消息ID | + ### 重载事件过滤器 终结点:`/reload_event_filter` `该 API 无需参数也没有响应数据` - ## 事件 ### 群消息撤回 @@ -1181,11 +1226,23 @@ JSON数组: | `notice_type` | string | `group_card` | 消息类型 | | `group_id` | int64 | | 群号 | | `user_id` | int64 | | 成员id | -| `card_new` | int64 | | 新名片 | -| `card_old` | int64 | | 旧名片 | +| `card_new` | string | | 新名片 | +| `card_old` | string | | 旧名片 | > PS: 当名片为空时 `card_xx` 字段为空字符串, 并不是昵称 +### 群成员头衔更新事件 + +**上报数据** + +| 字段 | 类型 | 可能的值 | 说明 | +| ------------- | ------ | ------------ | -------- | +| `post_type` | string | `notice` | 上报类型 | +| `notice_type` | string | `notify` | 消息类型 | +| `group_id` | int64 | | 群号 | +| `user_id` | int64 | | 成员id | +| `title` | string | | 新头衔 | + ### 接收到离线文件 **上报数据** diff --git a/docs/file.md b/docs/file.md index 987a49d..61a5d7e 100644 --- a/docs/file.md +++ b/docs/file.md @@ -1,38 +1,40 @@ # 文件 -go-cqhttp 默认生成的文件树如下所示: +go-cqhttp 默认生成的文件树如下所示: -```` +``` . ├── go-cqhttp -├── config.hjson +├── config.yml ├── device.json ├── logs │ └── xx-xx-xx.log └── data ├── images │ └── xxxx.image - └── db -```` + └── levleldb +``` -| 文件 | 用途 | -| ----------- | ------------------- | -| go-cqhttp | go-cqhttp可执行文件 | -| config.hjson | 运行配置文件 | -| device.json | 虚拟设备配置文件 | -| logs | 日志存放目录 | -| data | 数据目录 | -| data/images | 图片缓存目录 | -| data/db | 数据库目录 | +| 文件 | 用途 | +| ------------ | -------------------- | +| go-cqhttp | go-cqhttp 可执行文件 | +| config.yml | 运行配置文件 | +| device.json | 虚拟设备配置文件 | +| logs | 日志存放目录 | +| data | 数据目录 | +| data/leveldb | 数据库目录 | +| data/images | 图片缓存目录 | +| data/voices | 语音缓存目录 | +| data/videos | 视频缓存目录 | +| data/cache | 发送图片缓存目录 | ## 图片缓存文件 -出于性能考虑,go-cqhttp 并不会将图片源文件下载到本地,而是生成一个可以和QQ服务器对应的缓存文件 (.image),该缓存文件结构如下: +出于性能考虑,go-cqhttp 并不会将图片源文件下载到本地,而是生成一个可以和 QQ 服务器对应的缓存文件 (.image),该缓存文件结构如下: -| 偏移 | 类型 | 说明 | -| --------------- | -------- | ------------------ | -| 0x00 | [16]byte | 图片源文件MD5 HASH | -| 0x10 | uint32 | 图片源文件大小 | +| 偏移 | 类型 | 说明 | +| --------------- | -------- | -------------------- | +| 0x00 | [16]byte | 图片源文件 MD5 HASH | +| 0x10 | uint32 | 图片源文件大小 | | 0x14 | string | 图片原名(QQ内部ID) | -| 0x14 + 原名长度 | string | 图片下载链接 | - +| 0x14 + 原名长度 | string | 图片下载链接 | diff --git a/docs/slider.md b/docs/slider.md index c29593c..d668aff 100644 --- a/docs/slider.md +++ b/docs/slider.md @@ -1,5 +1,7 @@ # 滑块验证码 +> 该文档已过期, 最新版本下可直接使用手机扫描二维码通过验证. + 由于TX最新的限制, 所有协议在陌生设备/IP登录时都有可能被要求通过滑块验证码, 否则将会出现 `当前上网环境异常` 的错误. 目前我们准备了两个临时方案应对该验证码. > 如果您有一台运行Windows的PC/Server 并且不会抓包操作, 我们建议直接使用方案B diff --git a/global/codec/codec.go b/global/codec/codec.go index 6e20146..730f001 100644 --- a/global/codec/codec.go +++ b/global/codec/codec.go @@ -29,6 +29,10 @@ func EncodeToSilk(record []byte, tempName string, useCache bool) (silkWav []byte // 2.转换pcm pcmPath := path.Join(silkCachePath, tempName+".pcm") cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath) + if Debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } if err = cmd.Run(); err != nil { return nil, errors.Wrap(err, "convert pcm file error") } diff --git a/global/codec/codec_unsupportedarch.go b/global/codec/codec_unsupported.go similarity index 62% rename from global/codec/codec_unsupportedarch.go rename to global/codec/codec_unsupported.go index 04c4aef..f991f95 100644 --- a/global/codec/codec_unsupportedarch.go +++ b/global/codec/codec_unsupported.go @@ -1,5 +1,5 @@ -//go:build (!arm && !arm64 && !amd64 && !386) || race -// +build !arm,!arm64,!amd64,!386 race +//go:build (!arm && !arm64 && !amd64 && !386) || race || (!windows && !linux && !darwin) || (windows && arm) +// +build !arm,!arm64,!amd64,!386 race !windows,!linux,!darwin windows,arm package codec diff --git a/global/codec/codec_unsupportedos.go b/global/codec/codec_unsupportedos.go deleted file mode 100644 index f7f51a4..0000000 --- a/global/codec/codec_unsupportedos.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !windows && !linux && !darwin -// +build !windows,!linux,!darwin - -package codec - -import "errors" - -// EncodeToSilk 将音频编码为Silk -func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) { - return nil, errors.New("not supported now") -} - -// RecodeTo24K 将silk重新编码为 24000 bit rate -func RecodeTo24K(data []byte) []byte { - return data -} diff --git a/global/codec/codec_windows_arm.go b/global/codec/codec_windows_arm.go deleted file mode 100644 index 5cfcfc7..0000000 --- a/global/codec/codec_windows_arm.go +++ /dev/null @@ -1,13 +0,0 @@ -package codec - -import "errors" - -// EncodeToSilk 将音频编码为Silk -func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) { - return nil, errors.New("not supported now") -} - -// RecodeTo24K 将silk重新编码为 24000 bit rate -func RecodeTo24K(data []byte) []byte { - return data -} diff --git a/global/codec/stubs.go b/global/codec/stubs.go new file mode 100644 index 0000000..9821aa3 --- /dev/null +++ b/global/codec/stubs.go @@ -0,0 +1,4 @@ +package codec + +// Debug mode controls the ffmpeg output. +var Debug bool diff --git a/global/config/config.go b/global/config/config.go index e0f58b2..d85e9ad 100644 --- a/global/config/config.go +++ b/global/config/config.go @@ -174,6 +174,12 @@ func Get() *Config { global.SetAtDefault(&config.Account.ReLogin.Disabled, !global.EnsureBool(os.Getenv("GCQ_RELOGIN"), false), false) global.SetAtDefault(&config.Account.ReLogin.Delay, uint(toInt64(os.Getenv("GCQ_RELOGIN_DELAY"))), uint(0)) global.SetAtDefault(&config.Account.ReLogin.MaxTimes, uint(toInt64(os.Getenv("GCQ_RELOGIN_MAX_TIMES"))), uint(0)) + dbConf := &LevelDBConfig{Enable: global.EnsureBool(os.Getenv("GCQ_LEVELDB"), true)} + config.Database["leveldb"] = func() yaml.Node { + n := &yaml.Node{} + _ = n.Encode(dbConf) + return *n + }() accessTokenEnv := os.Getenv("GCQ_ACCESS_TOKEN") if os.Getenv("GCQ_HTTP_PORT") != "" { node := &yaml.Node{} diff --git a/global/log_hook.go b/global/log_hook.go index 00ab0d3..eb4f273 100644 --- a/global/log_hook.go +++ b/global/log_hook.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "sync" "github.com/sirupsen/logrus" @@ -174,3 +175,21 @@ func GetLogLevel(level string) []logrus.Level { } } } + +// LogFormat specialize for go-cqhttp +type LogFormat struct{} + +// Format implements logrus.Formatter +func (f LogFormat) Format(entry *logrus.Entry) ([]byte, error) { + buf := NewBuffer() + defer PutBuffer(buf) + buf.WriteByte('[') + buf.WriteString(entry.Time.Format("2006-01-02 15:04:05")) + buf.WriteString("] [") + buf.WriteString(strings.ToUpper(entry.Level.String())) + buf.WriteString("]: ") + buf.WriteString(entry.Message) + buf.WriteString(" \n") + ret := append([]byte(nil), buf.Bytes()...) // copy buffer + return ret, nil +} diff --git a/global/param.go b/global/param.go index 8819edb..f4e6b05 100644 --- a/global/param.go +++ b/global/param.go @@ -96,13 +96,14 @@ func SetAtDefault(variable, value, defaultValue interface{}) { if v.Kind() != reflect.Ptr || v.IsNil() { return } - if v.Elem().Interface() != defaultValue { + v = v.Elem() + if v.Interface() != defaultValue { return } - if v.Elem().Kind() != v2.Kind() { + if v.Kind() != v2.Kind() { return } - v.Elem().Set(v2) + v.Set(v2) } // SetExcludeDefault 在目标值 value 不为默认值 defaultValue 时修改 variable 为 value @@ -112,13 +113,14 @@ func SetExcludeDefault(variable, value, defaultValue interface{}) { if v.Kind() != reflect.Ptr || v.IsNil() { return } - if v2.Elem().Interface() != defaultValue { + v = v.Elem() + if reflect.Indirect(v2).Interface() != defaultValue { return } - if v.Elem().Kind() != v2.Kind() { + if v.Kind() != v2.Kind() { return } - v.Elem().Set(v2) + v.Set(v2) } var ( diff --git a/go.mod b/go.mod index a1f6493..fc414fc 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.16 require ( github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Microsoft/go-winio v0.5.0 - github.com/Mrs4s/MiraiGo v0.0.0-20210726103104-1d68826cef0e + github.com/Mrs4s/MiraiGo v0.0.0-20210810070836-6614d2383adb github.com/dustin/go-humanize v1.0.0 - github.com/gabriel-vasile/mimetype v1.3.1 // indirect + github.com/gabriel-vasile/mimetype v1.3.1 github.com/gorilla/websocket v1.4.2 github.com/guonaihong/gout v0.2.1 github.com/jonboulle/clockwork v0.2.2 // indirect @@ -21,7 +21,6 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/stretchr/testify v1.7.0 github.com/syndtr/goleveldb v1.0.0 - github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 github.com/tidwall/gjson v1.8.1 github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2 github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60 diff --git a/go.sum b/go.sum index ce9475e..86a3b75 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/g github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Mrs4s/MiraiGo v0.0.0-20210726103104-1d68826cef0e h1:PgFshw1L5TVdiDRLgr/bSotPGaGXYzbtn5cDBBvpL6U= -github.com/Mrs4s/MiraiGo v0.0.0-20210726103104-1d68826cef0e/go.mod h1:CPaznIPn415uQqxJgjyMHLqGLkvLS6R6+bkW3/fe08Q= +github.com/Mrs4s/MiraiGo v0.0.0-20210810070836-6614d2383adb h1:agCxYd/ZemDwrEYKpnpKqA0cubxfpkQ/b+GpIlIJK0U= +github.com/Mrs4s/MiraiGo v0.0.0-20210810070836-6614d2383adb/go.mod h1:5V3f/+mTYtrI/+hLqbdzZQXuLMl2RyLfx0XYYjCQ90Q= github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -71,7 +71,6 @@ github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMW github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= @@ -104,14 +103,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -119,8 +116,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= -github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU= github.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= @@ -150,7 +145,6 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -158,10 +152,11 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/login.go b/login.go index 10bea15..5e6c064 100644 --- a/login.go +++ b/login.go @@ -117,21 +117,7 @@ func loginResponseProcessor(res *client.LoginResponse) error { var text string switch res.Error { case client.SliderNeededError: - log.Warnf("登录需要滑条验证码. ") - log.Warnf("请参考文档 -> https://docs.go-cqhttp.org/faq/slider.html <- 进行处理") - log.Warnf("1. 自行抓包并获取 Ticket 输入.") - log.Warnf("2. 使用手机QQ扫描二维码登入. (推荐)") - log.Warn("请输入(1 - 2) (将在10秒后自动选择2):") - text = readLineTimeout(time.Second*10, "2") - if strings.Contains(text, "1") { - println() - log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", res.VerifyUrl) - println() - log.Warn("请输入Ticket: (Enter 提交)") - text = readLine() - res, err = cli.SubmitTicket(text) - continue - } + log.Warnf("登录需要滑条验证码, 请使用手机QQ扫描二维码以继续登录.") cli.Disconnect() cli.Release() cli = client.NewClientEmpty() diff --git a/main.go b/main.go index 52f3da2..b29e1fc 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/global" + "github.com/Mrs4s/go-cqhttp/global/codec" "github.com/Mrs4s/go-cqhttp/global/config" "github.com/Mrs4s/go-cqhttp/global/terminal" "github.com/Mrs4s/go-cqhttp/global/update" @@ -29,7 +30,6 @@ import ( "github.com/guonaihong/gout" rotatelogs "github.com/lestrrat-go/file-rotatelogs" log "github.com/sirupsen/logrus" - easy "github.com/t-tomalak/logrus-easy-formatter" "github.com/tidwall/gjson" "golang.org/x/crypto/pbkdf2" "golang.org/x/term" @@ -79,12 +79,9 @@ func main() { } if conf.Output.Debug { log.SetReportCaller(true) + codec.Debug = true } - logFormatter := &easy.Formatter{ - TimestampFormat: "2006-01-02 15:04:05", - LogFormat: "[%time%] [%lvl%]: %msg% \n", - } rotateOptions := []rotatelogs.Option{ rotatelogs.WithRotationTime(time.Hour * 24), } @@ -102,7 +99,7 @@ func main() { panic(err) } - log.AddHook(global.NewLocalHook(w, logFormatter, global.GetLogLevel(conf.Output.LogLevel)...)) + log.AddHook(global.NewLocalHook(w, global.LogFormat{}, global.GetLogLevel(conf.Output.LogLevel)...)) mkCacheDir := func(path string, _type string) { if !global.PathExists(path) { @@ -263,6 +260,7 @@ func main() { text := readLineTimeout(time.Second*5, "1") if text == "2" { _ = os.Remove("session.token") + log.Infof("缓存已删除.") os.Exit(0) } } diff --git a/server/api.go b/server/api.go index ecc62d9..2f35884 100644 --- a/server/api.go +++ b/server/api.go @@ -350,6 +350,10 @@ func setModelShow(bot *coolq.CQBot, p resultGetter) coolq.MSG { return bot.CQSetModelShow(p.Get("model").String(), p.Get("model_show").String()) } +func markMSGAsRead(bot *coolq.CQBot, p resultGetter) coolq.MSG { + return bot.CQMarkMessageAsRead(int32(p.Get("message_id").Int())) +} + // API 是go-cqhttp当前支持的所有api的映射表 var API = map[string]func(*coolq.CQBot, resultGetter) coolq.MSG{ "get_login_info": getLoginInfo, @@ -413,6 +417,7 @@ var API = map[string]func(*coolq.CQBot, resultGetter) coolq.MSG{ "qidian_get_account_info": getQiDianAccountInfo, "_get_model_show": getModelShow, "_set_model_show": setModelShow, + "mark_msg_as_read": markMSGAsRead, } func (api *apiCaller) callAPI(action string, p resultGetter) coolq.MSG { diff --git a/server/scf.go b/server/scf.go index a59d273..2c1f812 100644 --- a/server/scf.go +++ b/server/scf.go @@ -1,6 +1,7 @@ package server import ( + "bytes" "fmt" "io" "net/http" @@ -34,20 +35,22 @@ type lambdaResponse struct { type lambdaResponseWriter struct { statusCode int + buf bytes.Buffer header http.Header } +func (l *lambdaResponseWriter) Write(p []byte) (n int, err error) { + return l.buf.Write(p) +} + func (l *lambdaResponseWriter) Header() http.Header { return l.header } -func (l *lambdaResponseWriter) Write(data []byte) (int, error) { +func (l *lambdaResponseWriter) flush() error { buffer := global.NewBuffer() defer global.PutBuffer(buffer) - body := "" - if data != nil { - body = utils.B2S(data) - } + body := utils.B2S(l.buf.Bytes()) header := make(map[string]string) for k, v := range l.header { header[k] = v[0] @@ -62,10 +65,9 @@ func (l *lambdaResponseWriter) Write(data []byte) (int, error) { r, _ := http.NewRequest("POST", cli.responseURL, buffer) do, err := cli.client.Do(r) if err != nil { - return 0, err + return err } - _ = do.Body.Close() - return len(data), nil + return do.Body.Close() } func (l *lambdaResponseWriter) WriteHeader(statusCode int) { @@ -125,7 +127,11 @@ func RunLambdaClient(bot *coolq.CQBot, conf *config.LambdaServer) { log.Warnf("Lambda 出现不可恢复错误: %v\n%s", e, debug.Stack()) } }() - server.ServeHTTP(&lambdaResponseWriter{header: make(http.Header)}, req) + writer := lambdaResponseWriter{header: make(http.Header)} + server.ServeHTTP(&writer, req) + if err := writer.flush(); err != nil { + log.Warnf("Lambda 发送响应失败: %v", err) + } }() } } diff --git a/server/websocket.go b/server/websocket.go index b49ba29..af3c47f 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -160,7 +160,12 @@ func (c *websocketClient) connectEvent() { } log.Infof("已连接到反向WebSocket Event服务器 %v", c.conf.Event) - c.eventConn = &webSocketConn{Conn: conn, apiCaller: newAPICaller(c.bot)} + if c.eventConn == nil { + wrappedConn := &webSocketConn{Conn: conn, apiCaller: newAPICaller(c.bot)} + c.eventConn = wrappedConn + } else { + c.eventConn.Conn = conn + } } func (c *websocketClient) connectUniversal() { @@ -189,12 +194,16 @@ func (c *websocketClient) connectUniversal() { log.Warnf("反向WebSocket 握手时出现错误: %v", err) } - wrappedConn := &webSocketConn{Conn: conn, apiCaller: newAPICaller(c.bot)} - if c.conf.RateLimit.Enabled { - wrappedConn.apiCaller.use(rateLimit(c.conf.RateLimit.Frequency, c.conf.RateLimit.Bucket)) + if c.universalConn == nil { + wrappedConn := &webSocketConn{Conn: conn, apiCaller: newAPICaller(c.bot)} + if c.conf.RateLimit.Enabled { + wrappedConn.apiCaller.use(rateLimit(c.conf.RateLimit.Frequency, c.conf.RateLimit.Bucket)) + } + c.universalConn = wrappedConn + } else { + c.universalConn.Conn = conn } - go c.listenAPI(wrappedConn, true) - c.universalConn = wrappedConn + go c.listenAPI(c.universalConn, true) } func (c *websocketClient) listenAPI(conn *webSocketConn, u bool) {