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) {