diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e67cffa..888004f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,6 +17,8 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - name: Set RELEASE_VERSION env
+ run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- uses: wangyoucao577/go-release-action@master
env:
CGO_ENABLED: 0
@@ -24,4 +26,5 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
- ldflags: "-w -s"
+ ldflags: -w -s -X "github.com/Mrs4s/go-cqhttp/coolq.version=${{ env.RELEASE_VERSION }}"
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..31e3ac6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+vendor/
+.idea
diff --git a/README.md b/README.md
index e9944d0..4cd982b 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@
- [CQ:image]
- [CQ:record]
+- [CQ:video]
- [CQ:face]
- [CQ:at]
- [CQ:share]
@@ -91,6 +92,16 @@
+# 关于ISSUE
+
+以下ISSUE会被直接关闭
+- 提交BUG不使用Template
+- 询问已知问题
+- 提问找不到重点
+- 重复提问
+
+> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。
+
# 性能
在关闭数据库的情况下, 加载25个好友128个群运行24小时后内存使用为10MB左右. 开启数据库后内存使用将根据消息量增加10-20MB, 如果系统内存小于128M建议关闭数据库使用.
diff --git a/coolq/api.go b/coolq/api.go
index 1350ee5..ea4aefa 100644
--- a/coolq/api.go
+++ b/coolq/api.go
@@ -1,20 +1,23 @@
package coolq
import (
- "github.com/Mrs4s/MiraiGo/binary"
- "github.com/Mrs4s/MiraiGo/client"
- "github.com/Mrs4s/MiraiGo/message"
- "github.com/Mrs4s/go-cqhttp/global"
- log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
"io/ioutil"
"os"
"path"
"runtime"
"strconv"
"time"
+
+ "github.com/Mrs4s/MiraiGo/binary"
+ "github.com/Mrs4s/MiraiGo/client"
+ "github.com/Mrs4s/MiraiGo/message"
+ "github.com/Mrs4s/go-cqhttp/global"
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
)
+var version = "unknown"
+
// https://cqhttp.cc/docs/4.15/#/API?id=get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
func (bot *CQBot) CQGetLoginInfo() MSG {
return OK(MSG{"user_id": bot.Client.Uin, "nickname": bot.Client.Nickname})
@@ -34,8 +37,11 @@ func (bot *CQBot) CQGetFriendList() MSG {
}
// https://cqhttp.cc/docs/4.15/#/API?id=get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
-func (bot *CQBot) CQGetGroupList() MSG {
+func (bot *CQBot) CQGetGroupList(noCache bool) MSG {
var gs []MSG
+ if noCache {
+ _ = bot.Client.ReloadGroupList()
+ }
for _, g := range bot.Client.GroupList {
gs = append(gs, MSG{
"group_id": g.Code,
@@ -96,11 +102,25 @@ func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64, noCache bool) MSG
}
// https://cqhttp.cc/docs/4.15/#/API?id=send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
-func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}) MSG {
+func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bool) MSG {
var str string
+ fixAt := func(elem []message.IMessageElement) {
+ for _, e := range elem {
+ if at, ok := e.(*message.AtElement); ok && at.Target != 0 {
+ at.Display = "@" + func() string {
+ mem := bot.Client.FindGroup(groupId).FindMember(at.Target)
+ if mem != nil {
+ return mem.DisplayName()
+ }
+ return strconv.FormatInt(at.Target, 10)
+ }()
+ }
+ }
+ }
if m, ok := i.(gjson.Result); ok {
if m.Type == gjson.JSON {
elem := bot.ConvertObjectMessage(m, true)
+ fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 {
return Failed(100)
@@ -113,20 +133,20 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}) MSG {
}
return m.Raw
}()
- }
- if s, ok := i.(string); ok {
+ } else if s, ok := i.(string); ok {
str = s
}
if str == "" {
+ log.Warnf("群消息发送失败: 信息为空. MSG: %v", i)
return Failed(100)
}
- elem := bot.ConvertStringMessage(str, true)
- // fix at display
- for _, e := range elem {
- if at, ok := e.(*message.AtElement); ok && at.Target != 0 {
- at.Display = "@" + bot.Client.FindGroup(groupId).FindMember(at.Target).DisplayName()
- }
+ var elem []message.IMessageElement
+ if autoEscape {
+ elem = append(elem, message.NewText(str))
+ } else {
+ elem = bot.ConvertStringMessage(str, true)
}
+ fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 {
return Failed(100)
@@ -205,7 +225,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
}
// https://cqhttp.cc/docs/4.15/#/API?id=send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
-func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}) MSG {
+func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape bool) MSG {
var str string
if m, ok := i.(gjson.Result); ok {
if m.Type == gjson.JSON {
@@ -222,14 +242,18 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}) MSG {
}
return m.Raw
}()
- }
- if s, ok := i.(string); ok {
+ } else if s, ok := i.(string); ok {
str = s
}
if str == "" {
return Failed(100)
}
- elem := bot.ConvertStringMessage(str, false)
+ var elem []message.IMessageElement
+ if autoEscape {
+ elem = append(elem, message.NewText(str))
+ } else {
+ elem = bot.ConvertStringMessage(str, false)
+ }
mid := bot.SendPrivateMessage(userId, &message.SendingMessage{Elements: elem})
if mid == -1 {
return Failed(100)
@@ -322,7 +346,7 @@ func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) MSG {
}
// https://cqhttp.cc/docs/4.15/#/API?id=set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%EF%BC%8F%E9%82%80%E8%AF%B7
-func (bot *CQBot) CQProcessGroupRequest(flag, subType string, approve bool) MSG {
+func (bot *CQBot) CQProcessGroupRequest(flag, subType, reason string, approve bool) MSG {
if subType == "add" {
req, ok := bot.joinReqCache.Load(flag)
if !ok {
@@ -332,7 +356,7 @@ func (bot *CQBot) CQProcessGroupRequest(flag, subType string, approve bool) MSG
if approve {
req.(*client.UserJoinGroupRequest).Accept()
} else {
- req.(*client.UserJoinGroupRequest).Reject()
+ req.(*client.UserJoinGroupRequest).Reject(false, reason)
}
return OK(nil)
}
@@ -342,7 +366,7 @@ func (bot *CQBot) CQProcessGroupRequest(flag, subType string, approve bool) MSG
if approve {
req.(*client.GroupInvitedRequest).Accept()
} else {
- req.(*client.GroupInvitedRequest).Reject()
+ req.(*client.GroupInvitedRequest).Reject(false, reason)
}
return OK(nil)
}
@@ -359,6 +383,61 @@ func (bot *CQBot) CQDeleteMessage(messageId int32) MSG {
return OK(nil)
}
+// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
+func (bot *CQBot) CQGetGroupHonorInfo(groupId int64, t string) MSG {
+ msg := MSG{"group_id": groupId}
+ convertMem := func(memList []client.HonorMemberInfo) (ret []MSG) {
+ for _, mem := range memList {
+ ret = append(ret, MSG{
+ "user_id": mem.Uin,
+ "nickname": mem.Name,
+ "avatar": mem.Avatar,
+ "description": mem.Desc,
+ })
+ }
+ return
+ }
+ if t == "talkative" || t == "all" {
+ if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Talkative); err == nil {
+ if honor.CurrentTalkative.Uin != 0 {
+ msg["current_talkative"] = MSG{
+ "user_id": honor.CurrentTalkative.Uin,
+ "nickname": honor.CurrentTalkative.Name,
+ "avatar": honor.CurrentTalkative.Avatar,
+ "day_count": honor.CurrentTalkative.DayCount,
+ }
+ }
+ msg["talkative_list"] = convertMem(honor.TalkativeList)
+ }
+ }
+
+ if t == "performer" || t == "all" {
+ if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Performer); err == nil {
+ msg["performer_lis"] = convertMem(honor.ActorList)
+ }
+ }
+
+ if t == "legend" || t == "all" {
+ if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Legend); err == nil {
+ msg["legend_list"] = convertMem(honor.LegendList)
+ }
+ }
+
+ if t == "strong_newbie" || t == "all" {
+ if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.StrongNewbie); err == nil {
+ msg["strong_newbie_list"] = convertMem(honor.StrongNewbieList)
+ }
+ }
+
+ if t == "emotion" || t == "all" {
+ if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Emotion); err == nil {
+ msg["emotion_list"] = convertMem(honor.EmotionList)
+ }
+ }
+
+ return OK(msg)
+}
+
// https://cqhttp.cc/docs/4.15/#/API?id=-handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
// https://github.com/richardchien/coolq-http-api/blob/master/src/cqhttp/plugins/web/http.cpp#L376
func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
@@ -368,6 +447,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
msgType := context.Get("message_type").Str
reply := operation.Get("reply")
if reply.Exists() {
+ autoEscape := global.EnsureBool(operation.Get("auto_escape"), false)
/*
at := true
if operation.Get("at_sender").Exists() {
@@ -376,10 +456,10 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
*/
// TODO: 处理at字段
if msgType == "group" {
- bot.CQSendGroupMessage(context.Get("group_id").Int(), reply)
+ bot.CQSendGroupMessage(context.Get("group_id").Int(), reply, autoEscape)
}
if msgType == "private" {
- bot.CQSendPrivateMessage(context.Get("user_id").Int(), reply)
+ bot.CQSendPrivateMessage(context.Get("user_id").Int(), reply, autoEscape)
}
}
if msgType == "group" {
@@ -404,12 +484,12 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
}
case "request":
reqType := context.Get("request_type").Str
- if context.Get("approve").Bool() {
+ if operation.Get("approve").Exists() {
if reqType == "friend" {
- bot.CQProcessFriendRequest(context.Get("flag").Str, true)
+ bot.CQProcessFriendRequest(context.Get("flag").Str, operation.Get("approve").Bool())
}
if reqType == "group" {
- bot.CQProcessGroupRequest(context.Get("flag").Str, context.Get("sub_type").Str, true)
+ bot.CQProcessGroupRequest(context.Get("flag").Str, context.Get("sub_type").Str, operation.Get("reason").Str, operation.Get("approve").Bool())
}
}
}
@@ -423,11 +503,19 @@ func (bot *CQBot) CQGetImage(file string) MSG {
if b, err := ioutil.ReadFile(path.Join(global.IMAGE_PATH, file)); err == nil {
r := binary.NewReader(b)
r.ReadBytes(16)
- return OK(MSG{
+ msg := MSG{
"size": r.ReadInt32(),
"filename": r.ReadString(),
"url": r.ReadString(),
- })
+ }
+ local := path.Join(global.CACHE_PATH, file+"."+path.Ext(msg["filename"].(string)))
+ if !global.PathExists(local) {
+ if data, err := global.GetBytes(msg["url"].(string)); err == nil {
+ _ = ioutil.WriteFile(local, data, 0644)
+ }
+ }
+ msg["file"] = local
+ return OK(msg)
}
return Failed(100)
}
@@ -439,14 +527,14 @@ func (bot *CQBot) CQGetForwardMessage(resId string) MSG {
}
var r []MSG
for _, n := range m.Nodes {
- checkMedia(n.Message)
+ bot.checkMedia(n.Message)
r = append(r, MSG{
"sender": MSG{
"user_id": n.SenderId,
"nickname": n.SenderName,
},
"time": n.Time,
- "content": ToStringMessage(n.Message, 0, false),
+ "content": ToFormattedMessage(n.Message, 0, false),
})
}
return OK(MSG{
@@ -502,6 +590,7 @@ func (bot *CQBot) CQGetVersionInfo() MSG {
"plugin_build_configuration": "release",
"runtime_version": runtime.Version(),
"runtime_os": runtime.GOOS,
+ "version": version,
})
}
diff --git a/coolq/bot.go b/coolq/bot.go
index a63b5a3..6f36aa2 100644
--- a/coolq/bot.go
+++ b/coolq/bot.go
@@ -10,6 +10,7 @@ import (
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/go-cqhttp/global"
log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
"github.com/xujiajun/nutsdb"
"hash/crc32"
"path"
@@ -30,6 +31,8 @@ type CQBot struct {
type MSG map[string]interface{}
+var ForceFragmented = false
+
func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot := &CQBot{
Client: cli,
@@ -63,6 +66,19 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot.Client.OnNewFriendAdded(bot.friendAddedEvent)
bot.Client.OnGroupInvited(bot.groupInvitedEvent)
bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent)
+ go func() {
+ for {
+ time.Sleep(time.Second * 5)
+ bot.dispatchEventMessage(MSG{
+ "time": time.Now().Unix(),
+ "self_id": bot.Client.Uin,
+ "post_type": "meta_event",
+ "meta_event_type": "heartbeat",
+ "status": nil,
+ "interval": 5000,
+ })
+ }
+ }()
return bot
}
@@ -85,7 +101,7 @@ func (bot *CQBot) GetGroupMessage(mid int32) MSG {
if err == nil {
return m
}
- log.Warnf("获取信息时出现错误: %v", err)
+ log.Warnf("获取信息时出现错误: %v id: %v", err, mid)
}
return nil
}
@@ -114,7 +130,11 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
newElem = append(newElem, elem)
}
m.Elements = newElem
- ret := bot.Client.SendGroupMessage(groupId, m)
+ ret := bot.Client.SendGroupMessage(groupId, m, ForceFragmented)
+ if ret == nil || ret.Id == -1 {
+ log.Warnf("群消息发送失败: 账号可能被风控.")
+ return -1
+ }
return bot.InsertGroupMessage(ret)
}
@@ -133,16 +153,23 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
newElem = append(newElem, elem)
}
m.Elements = newElem
- var id int32
+ var id int32 = -1
if bot.Client.FindFriend(target) != nil {
- id = bot.Client.SendPrivateMessage(target, m).Id
+ msg := bot.Client.SendPrivateMessage(target, m)
+ if msg != nil {
+ id = msg.Id
+ }
} else {
if code, ok := bot.tempMsgCache.Load(target); ok {
- id = bot.Client.SendTempMessage(code.(int64), target, m).Id
- } else {
- return -1
+ msg := bot.Client.SendTempMessage(code.(int64), target, m)
+ if msg != nil {
+ id = msg.Id
+ }
}
}
+ if id == -1 {
+ return -1
+ }
return ToGlobalId(target, id)
}
@@ -184,6 +211,12 @@ func (bot *CQBot) Release() {
}
func (bot *CQBot) dispatchEventMessage(m MSG) {
+ payload := gjson.Parse(m.ToJson())
+ filter := global.GetFilter()
+ if filter != nil && (*filter).Eval(payload) == false {
+ log.Debug("Event filtered!")
+ return
+ }
for _, f := range bot.events {
fn := f
go func() {
@@ -191,7 +224,7 @@ func (bot *CQBot) dispatchEventMessage(m MSG) {
fn(m)
end := time.Now()
if end.Sub(start) > time.Second*5 {
- log.Debugf("警告: 事件处理耗时超过 5 秒 (%v秒), 请检查应用是否有堵塞.", end.Sub(start)/time.Second)
+ log.Debugf("警告: 事件处理耗时超过 5 秒 (%v), 请检查应用是否有堵塞.", end.Sub(start))
}
}()
}
@@ -202,6 +235,9 @@ func formatGroupName(group *client.GroupInfo) string {
}
func formatMemberName(mem *client.GroupMemberInfo) string {
+ if mem == nil {
+ return "未知"
+ }
return fmt.Sprintf("%s(%d)", mem.DisplayName(), mem.Uin)
}
diff --git a/coolq/cqcode.go b/coolq/cqcode.go
index fde5dc6..726129d 100644
--- a/coolq/cqcode.go
+++ b/coolq/cqcode.go
@@ -1,15 +1,11 @@
package coolq
import (
+ "crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
- "github.com/Mrs4s/MiraiGo/binary"
- "github.com/Mrs4s/MiraiGo/message"
- "github.com/Mrs4s/go-cqhttp/global"
- log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
"io/ioutil"
"net/url"
"path"
@@ -17,17 +13,116 @@ import (
"runtime"
"strconv"
"strings"
+
+ "github.com/Mrs4s/MiraiGo/binary"
+ "github.com/Mrs4s/MiraiGo/message"
+ "github.com/Mrs4s/go-cqhttp/global"
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
)
var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`)
var typeReg = regexp.MustCompile(`\[CQ:(\w+)`)
var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`)
+var IgnoreInvalidCQCode = false
+
+func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []MSG) {
+ ur := false
+ if len(raw) != 0 {
+ ur = raw[0]
+ }
+ for _, elem := range e {
+ m := MSG{}
+ switch o := elem.(type) {
+ case *message.TextElement:
+ m = MSG{
+ "type": "text",
+ "data": map[string]string{"text": o.Content},
+ }
+ case *message.AtElement:
+ if o.Target == 0 {
+ m = MSG{
+ "type": "at",
+ "data": map[string]string{"qq": "all"},
+ }
+ } else {
+ m = MSG{
+ "type": "at",
+ "data": map[string]string{"qq": fmt.Sprint(o.Target)},
+ }
+ }
+ case *message.ReplyElement:
+ m = MSG{
+ "type": "reply",
+ "data": map[string]string{"id": fmt.Sprint(ToGlobalId(code, o.ReplySeq))},
+ }
+ case *message.ForwardElement:
+ m = MSG{
+ "type": "forward",
+ "data": map[string]string{"id": o.ResId},
+ }
+ case *message.FaceElement:
+ m = MSG{
+ "type": "face",
+ "data": map[string]string{"id": fmt.Sprint(o.Index)},
+ }
+ case *message.VoiceElement:
+ if ur {
+ m = MSG{
+ "type": "record",
+ "data": map[string]string{"file": o.Name},
+ }
+ } else {
+ m = MSG{
+ "type": "record",
+ "data": map[string]string{"file": o.Name, "url": o.Url},
+ }
+ }
+ case *message.ShortVideoElement:
+ if ur {
+ m = MSG{
+ "type": "video",
+ "data": map[string]string{"file": o.Name},
+ }
+ } else {
+ m = MSG{
+ "type": "video",
+ "data": map[string]string{"file": o.Name, "url": o.Url},
+ }
+ }
+ case *message.ImageElement:
+ if ur {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": o.Filename},
+ }
+ } else {
+ m = MSG{
+ "type": "image",
+ "data": map[string]string{"file": o.Filename, "url": o.Url},
+ }
+ }
+ }
+ r = append(r, m)
+ }
+ return
+}
+
func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r string) {
ur := false
if len(raw) != 0 {
ur = raw[0]
}
+ // 方便
+ m := &message.SendingMessage{Elements: e}
+ reply := m.FirstOrNil(func(e message.IMessageElement) bool {
+ _, ok := e.(*message.ReplyElement)
+ return ok
+ })
+ if reply != nil {
+ r += fmt.Sprintf("[CQ:reply,id=%d]", ToGlobalId(code, reply.(*message.ReplyElement).ReplySeq))
+ }
for _, elem := range e {
switch o := elem.(type) {
case *message.TextElement:
@@ -38,8 +133,6 @@ func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r st
continue
}
r += fmt.Sprintf("[CQ:at,qq=%d]", o.Target)
- case *message.ReplyElement:
- r += fmt.Sprintf("[CQ:reply,id=%d]", ToGlobalId(code, o.ReplySeq))
case *message.ForwardElement:
r += fmt.Sprintf("[CQ:forward,id=%s]", o.ResId)
case *message.FaceElement:
@@ -48,13 +141,19 @@ func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r st
if ur {
r += fmt.Sprintf(`[CQ:record,file=%s]`, o.Name)
} else {
- r += fmt.Sprintf(`[CQ:record,file=%s,url=%s]`, o.Name, o.Url)
+ r += fmt.Sprintf(`[CQ:record,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
+ }
+ case *message.ShortVideoElement:
+ if ur {
+ r += fmt.Sprintf(`[CQ:video,file=%s]`, o.Name)
+ } else {
+ r += fmt.Sprintf(`[CQ:video,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
}
case *message.ImageElement:
if ur {
r += fmt.Sprintf(`[CQ:image,file=%s]`, o.Filename)
} else {
- r += fmt.Sprintf(`[CQ:image,file=%s,url=%s]`, o.Filename, o.Url)
+ r += fmt.Sprintf(`[CQ:image,file=%s,url=%s]`, o.Filename, CQCodeEscapeValue(o.Url))
}
}
}
@@ -75,7 +174,7 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
ps := paramReg.FindAllStringSubmatch(code, -1)
d := make(map[string]string)
for _, p := range ps {
- d[p[1]] = p[2]
+ d[p[1]] = CQCodeUnescapeValue(p[2])
}
if t == "reply" && group {
if len(r) > 0 {
@@ -102,8 +201,12 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
}
elem, err := bot.ToElement(t, d, group)
if err != nil {
- log.Warnf("转换CQ码到MiraiGo Element时出现错误: %v 将原样发送.", err)
- r = append(r, message.NewText(code))
+ if !IgnoreInvalidCQCode {
+ log.Warnf("转换CQ码 %v 到MiraiGo Element时出现错误: %v 将原样发送.", code, err)
+ r = append(r, message.NewText(code))
+ } else {
+ log.Warnf("转换CQ码 %v 到MiraiGo Element时出现错误: %v 将忽略.", code, err)
+ }
continue
}
r = append(r, elem)
@@ -173,10 +276,23 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
case "image":
f := d["file"]
if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") {
+ cache := d["cache"]
+ if cache == "" {
+ cache = "1"
+ }
+ hash := md5.Sum([]byte(f))
+ cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".cache")
+ if global.PathExists(cacheFile) && cache == "1" {
+ b, err := ioutil.ReadFile(cacheFile)
+ if err == nil {
+ return message.NewImage(b), nil
+ }
+ }
b, err := global.GetBytes(f)
if err != nil {
return nil, err
}
+ _ = ioutil.WriteFile(cacheFile, b, 0644)
return message.NewImage(b), nil
}
if strings.HasPrefix(f, "base64") {
@@ -220,6 +336,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
}
var size int32
var hash []byte
+ var url string
if path.Ext(rawPath) == ".cqimg" {
for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") {
kv := strings.SplitN(line, "=", 2)
@@ -235,8 +352,13 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
r := binary.NewReader(b)
hash = r.ReadBytes(16)
size = r.ReadInt32()
+ r.ReadString()
+ url = r.ReadString()
}
if size == 0 {
+ if url != "" {
+ return bot.ToElement(t, map[string]string{"file": url}, group)
+ }
return nil, errors.New("img size is 0")
}
if len(hash) != 16 {
@@ -297,7 +419,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
}
data = b
}
- if !global.IsAMR(data) {
+ if !global.IsAMRorSILK(data) {
return nil, errors.New("unsupported voice file format (please use AMR file for now)")
}
return &message.VoiceElement{Data: data}, nil
@@ -316,6 +438,66 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
return message.NewAt(t), nil
case "share":
return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil
+ case "music":
+ if d["type"] == "qq" {
+ info, err := global.QQMusicSongInfo(d["id"])
+ if err != nil {
+ return nil, err
+ }
+ if !info.Get("track_info").Exists() {
+ return nil, errors.New("song not found")
+ }
+ aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10)
+ name := info.Get("track_info.name").Str
+ if len(aid) < 2 {
+ return nil, errors.New("song error")
+ }
+ xml := fmt.Sprintf(`- %s%s
`,
+ name, d["id"], aid[:len(aid)-2], aid, name, "", info.Get("track_info.singer.name").Str)
+ return &message.ServiceElement{
+ Id: 60,
+ Content: xml,
+ SubType: "music",
+ }, nil
+ }
+ if d["type"] == "163" {
+ info, err := global.NeteaseMusicSongInfo(d["id"])
+ if err != nil {
+ return nil, err
+ }
+ if !info.Exists() {
+ return nil, errors.New("song not found")
+ }
+ name := info.Get("name").Str
+ artistName := ""
+ if info.Get("artists.0").Exists() {
+ artistName = info.Get("artists.0.name").Str
+ }
+ xml := fmt.Sprintf(`- %s%s
`,
+ name, d["id"], info.Get("album.picUrl").Str, d["id"], name, artistName)
+ return &message.ServiceElement{
+ Id: 60,
+ Content: xml,
+ SubType: "music",
+ }, nil
+ }
+ if d["type"] == "custom" {
+ xml := fmt.Sprintf(`- %s%s
`,
+ d["title"], d["url"], d["image"], d["audio"], d["title"], d["content"])
+ return &message.ServiceElement{
+ Id: 60,
+ Content: xml,
+ SubType: "music",
+ }, nil
+ }
+ return nil, errors.New("unsupported music type: " + d["type"])
+ case "xml":
+ resId := d["resid"]
+ template := CQCodeEscapeValue(d["data"])
+ //println(template)
+ i, _ := strconv.ParseInt(resId, 10, 64)
+ msg := global.NewXmlMsg(template, i)
+ return msg, nil
default:
return nil, errors.New("unsupported cq code: " + t)
}
@@ -329,6 +511,12 @@ func CQCodeEscapeText(raw string) string {
return ret
}
+func CQCodeEscapeValue(value string) string {
+ ret := CQCodeEscapeText(value)
+ ret = strings.ReplaceAll(ret, ",", ",")
+ return ret
+}
+
func CQCodeUnescapeText(content string) string {
ret := content
ret = strings.ReplaceAll(ret, "[", "[")
@@ -336,3 +524,9 @@ func CQCodeUnescapeText(content string) string {
ret = strings.ReplaceAll(ret, "&", "&")
return ret
}
+
+func CQCodeUnescapeValue(content string) string {
+ ret := strings.ReplaceAll(content, ",", ",")
+ ret = CQCodeUnescapeText(ret)
+ return ret
+}
diff --git a/coolq/event.go b/coolq/event.go
index 7838ea3..142e096 100644
--- a/coolq/event.go
+++ b/coolq/event.go
@@ -14,8 +14,23 @@ import (
"time"
)
+var format = "string"
+
+func SetMessageFormat(f string) {
+ format = f
+}
+
+func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r interface{}) {
+ if format == "string" {
+ r = ToStringMessage(e, code, raw...)
+ } else if format == "array" {
+ r = ToArrayMessage(e, code, raw...)
+ }
+ return
+}
+
func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) {
- checkMedia(m.Elements)
+ bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true)
log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm)
fm := MSG{
@@ -24,7 +39,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
"sub_type": "friend",
"message_id": ToGlobalId(m.Sender.Uin, m.Id),
"user_id": m.Sender.Uin,
- "message": ToStringMessage(m.Elements, 0, false),
+ "message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
@@ -40,7 +55,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
}
func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) {
- checkMedia(m.Elements)
+ bot.checkMedia(m.Elements)
for _, elem := range m.Elements {
if file, ok := elem.(*message.GroupFileElement); ok {
log.Infof("群 %v(%v) 内 %v(%v) 上传了文件: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, file.Name)
@@ -72,7 +87,7 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
"anonymous": nil,
"font": 0,
"group_id": m.GroupCode,
- "message": ToStringMessage(m.Elements, m.GroupCode, false),
+ "message": ToFormattedMessage(m.Elements, m.GroupCode, false),
"message_id": id,
"message_type": "group",
"post_type": "message",
@@ -118,7 +133,7 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
}
func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
- checkMedia(m.Elements)
+ bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true)
bot.tempMsgCache.Store(m.Sender.Uin, m.GroupCode)
log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm)
@@ -128,7 +143,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
"sub_type": "group",
"message_id": m.Id,
"user_id": m.Sender.Uin,
- "message": ToStringMessage(m.Elements, 0, false),
+ "message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
@@ -301,7 +316,7 @@ func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupR
"sub_type": "add",
"group_id": e.GroupCode,
"user_id": e.RequesterUin,
- "comment": "",
+ "comment": e.Message,
"flag": flag,
"time": time.Now().Unix(),
"self_id": c.Uin,
@@ -347,7 +362,7 @@ func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.Group
}
}
-func checkMedia(e []message.IMessageElement) {
+func (bot *CQBot) checkMedia(e []message.IMessageElement) {
for _, elem := range e {
switch i := elem.(type) {
case *message.ImageElement:
@@ -358,7 +373,7 @@ func checkMedia(e []message.IMessageElement) {
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
w.WriteString(i.Url)
- }), 0777)
+ }), 0644)
}
i.Filename = filename
case *message.VoiceElement:
@@ -370,8 +385,20 @@ func checkMedia(e []message.IMessageElement) {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue
}
- _ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0777)
+ _ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0644)
}
+ case *message.ShortVideoElement:
+ filename := hex.EncodeToString(i.Md5) + ".video"
+ if !global.PathExists(path.Join(global.VIDEO_PATH, filename)) {
+ _ = ioutil.WriteFile(path.Join(global.VIDEO_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
+ w.Write(i.Md5)
+ w.WriteUInt32(uint32(i.Size))
+ w.WriteString(i.Name)
+ w.Write(i.Uuid)
+ }), 0644)
+ }
+ i.Name = filename
+ i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5)
}
}
}
diff --git a/docs/config.md b/docs/config.md
index 6428cba..245800f 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -24,10 +24,14 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
"access_token": "",
"relogin": false,
"relogin_delay": 0,
+ "post_message_format": "string",
+ "ignore_invalid_cqcode": false,
+ "force_fragmented": true,
"http_config": {
"enabled": true,
"host": "0.0.0.0",
"port": 5700,
+ "timeout": 5,
"post_urls": {"url:port": "secret"}
},
"ws_config": {
@@ -49,18 +53,23 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
| 字段 | 类型 | 说明 |
| ------------------ | -------- | ------------------------------------------------------------------- |
-| uin | int64 | 登录用QQ号 |
-| password | string | 登录用密码 |
-| encrypt_password | bool | 是否对密码进行加密. |
-| password_encrypted | string | 加密后的密码(请勿修改) |
-| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
-| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
-| relogin | bool | 是否自动重新登录 |
-| relogin_delay | int | 重登录延时(秒) |
-| http_config | object | HTTP API配置 |
-| ws_config | object | Websocket API 配置 |
-| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
+| uin | int64 | 登录用QQ号 |
+| password | string | 登录用密码 |
+| encrypt_password | bool | 是否对密码进行加密. |
+| password_encrypted | string | 加密后的密码(请勿修改) |
+| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
+| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
+| relogin | bool | 是否自动重新登录 |
+| relogin_delay | int | 重登录延时(秒) |
+| post_message_format | string | 上报信息类型 |
+| ignore_invalid_cqcode| bool | 是否忽略错误的CQ码 |
+| force_fragmented | bool | 是否强制分片发送群长消息 |
+| http_config | object | HTTP API配置 |
+| ws_config | object | Websocket API 配置 |
+| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
> 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
> 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥
+
+> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
\ No newline at end of file
diff --git a/docs/cqhttp.md b/docs/cqhttp.md
index 8cddcc7..a0f5c42 100644
--- a/docs/cqhttp.md
+++ b/docs/cqhttp.md
@@ -119,6 +119,51 @@ Type: `node`
]
````
+### xml支持
+
+Type: `xml`
+
+范围: **发送**
+
+参数:
+
+| 参数名 | 类型 | 说明 |
+| ------ | ------ | ------------------------------------------------------------ |
+| data | string | xml内容,xml中的value部分,记得实体化处理|
+| resid | int32 | 可以不填|
+
+示例: `[CQ:xml,data=xxxx]`
+
+####一些xml样例
+####ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中
+#### qq音乐
+```xml
+- 十年陈奕迅
+```
+#### 网易音乐
+```xml
+- 十年黄梦之
+```
+
+#### 卡片消息1
+```xml
+
+
+- 生死8秒!女司机高速急刹,他一个操作救下一车性命
+
+
+```
+
+#### 卡片消息2
+```xml
+
+
+-
+test title
+
+
+
+```
## API
diff --git a/global/config.go b/global/config.go
index 267cd5b..5f13ac4 100644
--- a/global/config.go
+++ b/global/config.go
@@ -6,18 +6,21 @@ import (
)
type JsonConfig struct {
- Uin int64 `json:"uin"`
- Password string `json:"password"`
- EncryptPassword bool `json:"encrypt_password"`
- PasswordEncrypted string `json:"password_encrypted"`
- EnableDB bool `json:"enable_db"`
- AccessToken string `json:"access_token"`
- ReLogin bool `json:"relogin"`
- ReLoginDelay int `json:"relogin_delay"`
- HttpConfig *GoCQHttpConfig `json:"http_config"`
- WSConfig *GoCQWebsocketConfig `json:"ws_config"`
- ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
- Debug bool `json:"debug"`
+ Uin int64 `json:"uin"`
+ Password string `json:"password"`
+ EncryptPassword bool `json:"encrypt_password"`
+ PasswordEncrypted string `json:"password_encrypted"`
+ EnableDB bool `json:"enable_db"`
+ AccessToken string `json:"access_token"`
+ ReLogin bool `json:"relogin"`
+ ReLoginDelay int `json:"relogin_delay"`
+ IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"`
+ ForceFragmented bool `json:"force_fragmented"`
+ HttpConfig *GoCQHttpConfig `json:"http_config"`
+ WSConfig *GoCQWebsocketConfig `json:"ws_config"`
+ ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
+ PostMessageFormat string `json:"post_message_format"`
+ Debug bool `json:"debug"`
}
type CQHttpApiConfig struct {
@@ -43,6 +46,7 @@ type GoCQHttpConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
+ Timeout int32 `json:"timeout"`
PostUrls map[string]string `json:"post_urls"`
}
@@ -62,9 +66,11 @@ type GoCQReverseWebsocketConfig struct {
func DefaultConfig() *JsonConfig {
return &JsonConfig{
- EnableDB: true,
- ReLogin: true,
- ReLoginDelay: 3,
+ EnableDB: true,
+ ReLogin: true,
+ ReLoginDelay: 3,
+ PostMessageFormat: "string",
+ ForceFragmented: true,
HttpConfig: &GoCQHttpConfig{
Enabled: true,
Host: "0.0.0.0",
diff --git a/global/filter.go b/global/filter.go
new file mode 100644
index 0000000..e7fdad6
--- /dev/null
+++ b/global/filter.go
@@ -0,0 +1,267 @@
+package global
+
+import (
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
+ "io/ioutil"
+ "regexp"
+ "strings"
+ "sync"
+)
+
+type Filter interface {
+ Eval(payload gjson.Result) bool
+}
+
+type OperationNode struct {
+ key string
+ filter Filter
+}
+
+type NotOperator struct {
+ operand_ Filter
+}
+
+func notOperatorConstruct(argument gjson.Result) *NotOperator {
+ if !argument.IsObject() {
+ log.Error("the argument of 'not' operator must be an object")
+ }
+ op := new(NotOperator)
+ op.operand_ = GetOperatorFactory().Generate("and", argument)
+ return op
+}
+
+func (notOperator NotOperator) Eval(payload gjson.Result) bool {
+ log.Debug("not " + payload.Str)
+ return !(notOperator.operand_).Eval(payload)
+}
+
+type AndOperator struct {
+ operands []OperationNode
+}
+
+func andOperatorConstruct(argument gjson.Result) *AndOperator {
+ if !argument.IsObject() {
+ log.Error("the argument of 'and' operator must be an object")
+ }
+ op := new(AndOperator)
+ argument.ForEach(func(key, value gjson.Result) bool {
+ if key.Str[0] == '.' {
+ // is an operator
+ // ".foo": {
+ // "bar": "baz"
+ // }
+ opKey := key.Str[1:]
+ op.operands = append(op.operands, OperationNode{"", GetOperatorFactory().Generate(opKey, value)})
+ } else if value.IsObject() {
+ // is an normal key with an object as the value
+ // "foo": {
+ // ".bar": "baz"
+ // }
+ opKey := key.Str
+ op.operands = append(op.operands, OperationNode{opKey, GetOperatorFactory().Generate("and", value)})
+ } else {
+ // is an normal key with a non-object as the value
+ // "foo": "bar"
+ opKey := key.Str
+ op.operands = append(op.operands, OperationNode{opKey, GetOperatorFactory().Generate("eq", value)})
+ }
+ return true
+ })
+ return op
+}
+
+func (andOperator *AndOperator) Eval(payload gjson.Result) bool {
+ log.Debug("and " + payload.Str)
+ res := true
+ for _, operand := range andOperator.operands {
+
+ if len(operand.key) == 0 {
+ // is an operator
+ res = res && operand.filter.Eval(payload)
+ } else {
+ // is an normal key
+ val := payload.Get(operand.key)
+ res = res && operand.filter.Eval(val)
+ }
+
+ if res == false {
+ break
+ }
+ }
+ return res
+}
+
+type OrOperator struct {
+ operands []Filter
+}
+
+func orOperatorConstruct(argument gjson.Result) *OrOperator {
+ if !argument.IsArray() {
+ log.Error("the argument of 'or' operator must be an array")
+ }
+ op := new(OrOperator)
+ argument.ForEach(func(_, value gjson.Result) bool {
+ op.operands = append(op.operands, GetOperatorFactory().Generate("and", value))
+ return true
+ })
+ return op
+}
+
+func (orOperator OrOperator) Eval(payload gjson.Result) bool {
+ log.Debug("or "+ payload.Str)
+ res:= false
+ for _, operand := range orOperator.operands {
+ res = res || operand.Eval(payload)
+
+ if res == true {
+ break
+ }
+ }
+ return res
+}
+
+type EqualOperator struct {
+ value gjson.Result
+}
+
+func equalOperatorConstruct(argument gjson.Result) *EqualOperator {
+ op := new(EqualOperator)
+ op.value = argument
+ return op
+}
+
+func (equalOperator EqualOperator) Eval(payload gjson.Result) bool {
+ log.Debug("eq "+ payload.Str + "==" + equalOperator.value.Str)
+ return payload.Str == equalOperator.value.Str
+}
+
+type NotEqualOperator struct {
+ value gjson.Result
+}
+
+func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator {
+ op := new(NotEqualOperator)
+ op.value = argument
+ return op
+}
+
+func (notEqualOperator NotEqualOperator) Eval(payload gjson.Result) bool {
+ log.Debug("neq " + payload.Str)
+ return !(payload.Str == notEqualOperator.value.Str)
+}
+
+
+type InOperator struct {
+ operand gjson.Result
+}
+
+func inOperatorConstruct(argument gjson.Result) *InOperator {
+ if argument.IsObject() {
+ log.Error("the argument of 'in' operator must be an array or a string")
+ }
+ op := new(InOperator)
+ op.operand = argument
+ return op
+}
+
+func (inOperator InOperator) Eval(payload gjson.Result) bool {
+ log.Debug("in " + payload.Str)
+ if inOperator.operand.IsArray() {
+ res := false
+ inOperator.operand.ForEach(func(key, value gjson.Result) bool {
+ res = res || value.Str == payload.Str
+ return true
+ })
+ return res
+ }
+ return strings.Contains(inOperator.operand.Str, payload.Str)
+}
+
+type ContainsOperator struct {
+ operand string
+}
+
+func containsOperatorConstruct(argument gjson.Result) *ContainsOperator {
+ if argument.IsArray() || argument.IsObject() {
+ log.Error("the argument of 'contains' operator must be a string")
+ }
+ op := new(ContainsOperator)
+ op.operand = argument.Str
+ return op
+}
+
+func (containsOperator ContainsOperator) Eval(payload gjson.Result) bool {
+ log.Debug("contains "+ payload.Str)
+ if payload.IsObject() || payload.IsArray() {
+ return false
+ }
+ return strings.Contains(payload.String(), containsOperator.operand)
+}
+
+type RegexOperator struct {
+ regex string
+}
+
+func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
+ if argument.IsArray() || argument.IsObject() {
+ log.Error("the argument of 'regex' operator must be a string")
+ }
+ op := new(RegexOperator)
+ op.regex = argument.Str
+ return op
+}
+
+func (containsOperator RegexOperator) Eval(payload gjson.Result) bool {
+ log.Debug("regex " + payload.Str)
+ matched, _ := regexp.MatchString(containsOperator.regex, payload.Str)
+ return matched
+}
+// 单例工厂
+type operatorFactory struct{
+}
+
+var instance *operatorFactory = &operatorFactory{}
+
+func GetOperatorFactory() *operatorFactory {
+ return instance
+}
+
+func (o operatorFactory) Generate(opName string, argument gjson.Result) Filter {
+ switch opName {
+ case "not":
+ return notOperatorConstruct(argument)
+ case "and":
+ return andOperatorConstruct(argument)
+ case "or":
+ return orOperatorConstruct(argument)
+ case "neq":
+ return notEqualOperatorConstruct(argument)
+ case "eq":
+ return equalOperatorConstruct(argument)
+ case "in":
+ return inOperatorConstruct(argument)
+ case "contains":
+ return containsOperatorConstruct(argument)
+ case "regex":
+ return regexOperatorConstruct(argument)
+ default:
+ log.Warnf("the operator '%s' is not supported", opName)
+ return nil
+ }
+}
+
+var filter = new(Filter)
+var once sync.Once // 过滤器单例模式
+
+func GetFilter() *Filter {
+ once.Do(func() {
+ f, err := ioutil.ReadFile("filter.json")
+ if err != nil {
+ filter = nil
+ } else {
+ *filter = GetOperatorFactory().Generate("and", gjson.ParseBytes(f))
+ }
+ })
+ return filter
+}
\ No newline at end of file
diff --git a/global/fs.go b/global/fs.go
index 4104511..f3dc6b0 100644
--- a/global/fs.go
+++ b/global/fs.go
@@ -1,15 +1,23 @@
package global
import (
- log "github.com/sirupsen/logrus"
+ "bytes"
"io/ioutil"
"os"
"path"
+
+ log "github.com/sirupsen/logrus"
)
-var IMAGE_PATH = path.Join("data", "images")
+var (
+ IMAGE_PATH = path.Join("data", "images")
+ VOICE_PATH = path.Join("data", "voices")
+ VIDEO_PATH = path.Join("data", "videos")
+ CACHE_PATH = path.Join("data", "cache")
-var VOICE_PATH = path.Join("data", "voices")
+ HEADER_AMR = []byte("#!AMR")
+ HEADER_SILK = []byte("\x02#!SILK_V3")
+)
func PathExists(path string) bool {
_, err := os.Stat(path)
@@ -25,7 +33,7 @@ func ReadAllText(path string) string {
}
func WriteAllText(path, text string) {
- _ = ioutil.WriteFile(path, []byte(text), 0777)
+ _ = ioutil.WriteFile(path, []byte(text), 0644)
}
func Check(err error) {
@@ -34,9 +42,6 @@ func Check(err error) {
}
}
-func IsAMR(b []byte) bool {
- if len(b) <= 6 {
- return false
- }
- return b[0] == 0x23 && b[1] == 0x21 && b[2] == 0x41 && b[3] == 0x4D && b[4] == 0x52 // amr file header
+func IsAMRorSILK(b []byte) bool {
+ return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK)
}
diff --git a/global/net.go b/global/net.go
index e250ef6..e394efd 100644
--- a/global/net.go
+++ b/global/net.go
@@ -3,9 +3,14 @@ package global
import (
"bytes"
"compress/gzip"
+ "fmt"
"io/ioutil"
"net/http"
+ "strconv"
"strings"
+
+ "github.com/Mrs4s/MiraiGo/message"
+ "github.com/tidwall/gjson"
)
func GetBytes(url string) ([]byte, error) {
@@ -32,3 +37,35 @@ func GetBytes(url string) ([]byte, error) {
}
return body, nil
}
+
+func QQMusicSongInfo(id string) (gjson.Result, error) {
+ d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`)
+ if err != nil {
+ return gjson.Result{}, err
+ }
+ return gjson.ParseBytes(d).Get("songinfo.data"), nil
+}
+
+func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
+ d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id))
+ if err != nil {
+ return gjson.Result{}, err
+ }
+ return gjson.ParseBytes(d).Get("songs.0"), nil
+}
+
+func NewXmlMsg(template string, ResId int64) *message.ServiceElement {
+ var serviceid string
+ if ResId == 0 {
+ serviceid = "2" //默认值2
+ } else {
+ serviceid = strconv.FormatInt(ResId, 10)
+ }
+ //println(serviceid)
+ return &message.ServiceElement{
+ Id: int32(ResId),
+ Content: template,
+ ResId: serviceid,
+ SubType: "xml",
+ }
+}
diff --git a/global/param.go b/global/param.go
new file mode 100644
index 0000000..25e138f
--- /dev/null
+++ b/global/param.go
@@ -0,0 +1,50 @@
+package global
+
+import (
+ "github.com/tidwall/gjson"
+ "strings"
+)
+
+var trueSet = map[string]struct{}{
+ "true": {},
+ "yes": {},
+ "1": {},
+}
+
+var falseSet = map[string]struct{}{
+ "false": {},
+ "no": {},
+ "0": {},
+}
+
+func EnsureBool(p interface{}, defaultVal bool) bool {
+ var str string
+ if b, ok := p.(bool); ok {
+ return b
+ }
+ if j, ok := p.(gjson.Result); ok {
+ if !j.Exists() {
+ return defaultVal
+ }
+ if j.Type == gjson.True {
+ return true
+ }
+ if j.Type == gjson.False {
+ return false
+ }
+ if j.Type != gjson.String {
+ return defaultVal
+ }
+ str = j.Str
+ } else if s, ok := p.(string); ok {
+ str = s
+ }
+ str = strings.ToLower(str)
+ if _, ok := trueSet[str]; ok {
+ return true
+ }
+ if _, ok := falseSet[str]; ok {
+ return false
+ }
+ return defaultVal
+}
diff --git a/go.mod b/go.mod
index fa9f430..426ba6c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,18 +3,21 @@ module github.com/Mrs4s/go-cqhttp
go 1.14
require (
- github.com/Mrs4s/MiraiGo v0.0.0-20200810032556-a425f9d1b98e
+ github.com/Mrs4s/MiraiGo v0.0.0-20200825052841-d3b0f5f9e839
+ github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
github.com/gin-gonic/gin v1.6.3
github.com/gorilla/websocket v1.4.2
github.com/guonaihong/gout v0.1.1
+ github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
+ github.com/jonboulle/clockwork v0.2.0 // indirect
github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible
github.com/lestrrat-go/strftime v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
+ github.com/tebeka/strftime v0.1.5 // indirect
github.com/tidwall/gjson v1.6.0
github.com/xujiajun/nutsdb v0.5.0
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189
- golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
)
diff --git a/go.sum b/go.sum
index 8db1332..ef1cb9a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/Mrs4s/MiraiGo v0.0.0-20200809221224-7a84cfae6795 h1:Bu4k9ZS/IIy9Shwd9lS/C2P/2I8fYUwg1OpRF91hr1w=
-github.com/Mrs4s/MiraiGo v0.0.0-20200809221224-7a84cfae6795/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
-github.com/Mrs4s/MiraiGo v0.0.0-20200810032556-a425f9d1b98e h1:5LYDouOL9ZgTL5PwZuuSlFYSfboRQjnXqRIlhviRcGE=
-github.com/Mrs4s/MiraiGo v0.0.0-20200810032556-a425f9d1b98e/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
+github.com/Mrs4s/MiraiGo v0.0.0-20200825052841-d3b0f5f9e839 h1:TDhaPfWcubIMKDz1HU+N07SwIUjj7oVUQ7EXZBbDUxs=
+github.com/Mrs4s/MiraiGo v0.0.0-20200825052841-d3b0f5f9e839/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -12,6 +10,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
@@ -50,6 +49,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/guonaihong/gout v0.1.1 h1:2i3eqQ1KUhTlj7AFeIHqVUFku5QwUhwE2wNgYTVpbxQ=
github.com/guonaihong/gout v0.1.1/go.mod h1:vXvv5Kxr70eM5wrp4F0+t9lnLWmq+YPW2GByll2f/EA=
+github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
+github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -85,6 +86,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
diff --git a/main.go b/main.go
index 8b2cbeb..b45d6e3 100644
--- a/main.go
+++ b/main.go
@@ -37,15 +37,25 @@ func init() {
log.SetOutput(io.MultiWriter(os.Stderr, w))
}
if !global.PathExists(global.IMAGE_PATH) {
- if err := os.MkdirAll(global.IMAGE_PATH, 0677); err != nil {
+ if err := os.MkdirAll(global.IMAGE_PATH, 0755); err != nil {
log.Fatalf("创建图片缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.VOICE_PATH) {
- if err := os.MkdirAll(global.VOICE_PATH, 06777); err != nil {
+ if err := os.MkdirAll(global.VOICE_PATH, 0755); err != nil {
log.Fatalf("创建语音缓存文件夹失败: %v", err)
}
}
+ if !global.PathExists(global.VIDEO_PATH) {
+ if err := os.MkdirAll(global.VIDEO_PATH, 0755); err != nil {
+ log.Fatalf("创建视频缓存文件夹失败: %v", err)
+ }
+ }
+ if !global.PathExists(global.CACHE_PATH) {
+ if err := os.MkdirAll(global.CACHE_PATH, 0755); err != nil {
+ log.Fatalf("创建发送图片缓存文件夹失败: %v", err)
+ }
+ }
if global.PathExists("cqhttp.json") {
log.Info("发现 cqhttp.json 将在五秒后尝试导入配置,按 Ctrl+C 取消.")
log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.json 文件.")
@@ -101,7 +111,8 @@ func main() {
Host: "0.0.0.0",
Port: 6700,
},
- Debug: os.Getenv("DEBUG") == "true",
+ PostMessageFormat: "string",
+ Debug: os.Getenv("DEBUG") == "true",
}
if post != "" {
conf.HttpConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
@@ -125,11 +136,12 @@ func main() {
if conf.Debug {
log.SetLevel(log.DebugLevel)
log.Warnf("已开启Debug模式.")
+ log.Debugf("开发交流群: 192548878")
}
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice()
- _ = ioutil.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0777)
+ _ = ioutil.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0644)
log.Info("已生成设备信息并保存到 device.json 文件.")
} else {
log.Info("将使用 device.json 内的设备信息运行Bot.")
@@ -159,15 +171,26 @@ func main() {
time.Sleep(time.Second * 5)
log.Info("开始尝试登录并同步消息...")
cli := client.NewClient(conf.Uin, conf.Password)
+ cli.OnLog(func(c *client.QQClient, e *client.LogEvent) {
+ switch e.Type {
+ case "INFO":
+ log.Info("Protocol -> " + e.Message)
+ case "ERROR":
+ log.Error("Protocol -> " + e.Message)
+ case "DEBUG":
+ log.Debug("Protocol -> " + e.Message)
+ }
+ })
rsp, err := cli.Login()
for {
global.Check(err)
if !rsp.Success {
switch rsp.Error {
case client.NeedCaptcha:
+ _ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644)
img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage))
fmt.Println(asciiart.New("image", img).Art)
- log.Warn("请输入验证码: (Enter 提交)")
+ log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)")
text, _ := console.ReadString('\n')
rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign)
continue
@@ -191,10 +214,18 @@ func main() {
global.Check(cli.ReloadGroupList())
log.Infof("共加载 %v 个群.", len(cli.GroupList))
b := coolq.NewQQBot(cli, conf)
+ if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" {
+ log.Warnf("post_message_format 配置错误, 将自动使用 string")
+ coolq.SetMessageFormat("string")
+ } else {
+ coolq.SetMessageFormat(conf.PostMessageFormat)
+ }
+ coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode
+ coolq.ForceFragmented = conf.ForceFragmented
if conf.HttpConfig != nil && conf.HttpConfig.Enabled {
server.HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, b)
for k, v := range conf.HttpConfig.PostUrls {
- server.NewHttpClient().Run(k, v, b)
+ server.NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, b)
}
}
if conf.WSConfig != nil && conf.WSConfig.Enabled {
diff --git a/server/http.go b/server/http.go
index b191a62..ad864f5 100644
--- a/server/http.go
+++ b/server/http.go
@@ -4,11 +4,13 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
+ "os"
"strconv"
"strings"
"time"
"github.com/Mrs4s/go-cqhttp/coolq"
+ "github.com/Mrs4s/go-cqhttp/global"
"github.com/gin-gonic/gin"
"github.com/guonaihong/gout"
log "github.com/sirupsen/logrus"
@@ -21,9 +23,10 @@ type httpServer struct {
}
type httpClient struct {
- bot *coolq.CQBot
- secret string
- addr string
+ bot *coolq.CQBot
+ secret string
+ addr string
+ timeout int32
}
var HttpServer = &httpServer{}
@@ -38,7 +41,7 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
c.Status(404)
return
}
- if c.Request.Method == "POST" && c.Request.Header.Get("Content-Type") == "application/json" {
+ if c.Request.Method == "POST" && strings.Contains(c.Request.Header.Get("Content-Type"), "application/json") {
d, err := c.GetRawData()
if err != nil {
log.Warnf("获取请求 %v 的Body时出现错误: %v", c.Request.RequestURI, err)
@@ -132,12 +135,12 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
s.engine.Any("/set_group_leave_async", s.SetGroupLeave)
s.engine.Any("/get_image", s.GetImage)
- s.engine.Any("/get_image_async", s.GetImage)
s.engine.Any("/get_forward_msg", s.GetForwardMessage)
s.engine.Any("/get_group_msg", s.GetGroupMessage)
- s.engine.Any("/get_group_msg_async", s.GetGroupMessage)
+
+ s.engine.Any("/get_group_honor_info", s.GetGroupHonorInfo)
s.engine.Any("/can_send_image", s.CanSendImage)
s.engine.Any("/can_send_image_async", s.CanSendImage)
@@ -155,7 +158,13 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
go func() {
log.Infof("CQ HTTP 服务器已启动: %v", addr)
- log.Fatal(s.engine.Run(addr))
+ err := s.engine.Run(addr)
+ if err != nil {
+ log.Error(err)
+ log.Infof("请检查端口是否被占用.")
+ time.Sleep(time.Second * 5)
+ os.Exit(1)
+ }
}()
}
@@ -163,10 +172,14 @@ func NewHttpClient() *httpClient {
return &httpClient{}
}
-func (c *httpClient) Run(addr, secret string, bot *coolq.CQBot) {
+func (c *httpClient) Run(addr, secret string, timeout int32, bot *coolq.CQBot) {
c.bot = bot
c.secret = secret
c.addr = addr
+ c.timeout = timeout
+ if c.timeout < 5 {
+ c.timeout = 5
+ }
bot.OnEventPush(c.onBotPushEvent)
log.Infof("HTTP POST上报器已启动: %v", addr)
}
@@ -184,7 +197,7 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) {
h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil))
}
return h
- }()).SetTimeout(time.Second * 5).Do()
+ }()).SetTimeout(time.Second * time.Duration(c.timeout)).Do()
if err != nil {
log.Warnf("上报Event数据到 %v 失败: %v", c.addr, err)
return
@@ -203,7 +216,8 @@ func (s *httpServer) GetFriendList(c *gin.Context) {
}
func (s *httpServer) GetGroupList(c *gin.Context) {
- c.JSON(200, s.bot.CQGetGroupList())
+ nc := getParamOrDefault(c, "no_cache", "false")
+ c.JSON(200, s.bot.CQGetGroupList(nc == "true"))
}
func (s *httpServer) GetGroupInfo(c *gin.Context) {
@@ -224,6 +238,14 @@ func (s *httpServer) GetGroupMemberInfo(c *gin.Context) {
}
func (s *httpServer) SendMessage(c *gin.Context) {
+ if getParam(c, "message_type") == "private" {
+ s.SendPrivateMessage(c)
+ return
+ }
+ if getParam(c, "message_type") == "group" {
+ s.SendGroupMessage(c)
+ return
+ }
if getParam(c, "group_id") != "" {
s.SendGroupMessage(c)
return
@@ -235,22 +257,24 @@ func (s *httpServer) SendMessage(c *gin.Context) {
func (s *httpServer) SendPrivateMessage(c *gin.Context) {
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
- msg := getParam(c, "message")
- if gjson.Valid(msg) {
- c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg)))
+ msg, t := getParamWithType(c, "message")
+ autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
+ if t == gjson.JSON {
+ c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg), autoEscape))
return
}
- c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg))
+ c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg, autoEscape))
}
func (s *httpServer) SendGroupMessage(c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
- msg := getParam(c, "message")
- if gjson.Valid(msg) {
- c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg)))
+ msg, t := getParamWithType(c, "message")
+ autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
+ if t == gjson.JSON {
+ c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg), autoEscape))
return
}
- c.JSON(200, s.bot.CQSendGroupMessage(gid, msg))
+ c.JSON(200, s.bot.CQSendGroupMessage(gid, msg, autoEscape))
}
func (s *httpServer) SendGroupForwardMessage(c *gin.Context) {
@@ -269,6 +293,11 @@ func (s *httpServer) GetGroupMessage(c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupMessage(int32(mid)))
}
+func (s *httpServer) GetGroupHonorInfo(c *gin.Context) {
+ gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
+ c.JSON(200, s.bot.CQGetGroupHonorInfo(gid, getParam(c, "type")))
+}
+
func (s *httpServer) ProcessFriendRequest(c *gin.Context) {
flag := getParam(c, "flag")
approve := getParamOrDefault(c, "approve", "true")
@@ -282,7 +311,7 @@ func (s *httpServer) ProcessGroupRequest(c *gin.Context) {
subType = getParam(c, "type")
}
approve := getParamOrDefault(c, "approve", "true")
- c.JSON(200, s.bot.CQProcessGroupRequest(flag, subType, approve == "true"))
+ c.JSON(200, s.bot.CQProcessGroupRequest(flag, subType, getParam(c, "reason"), approve == "true"))
}
func (s *httpServer) SetGroupCard(c *gin.Context) {
@@ -371,37 +400,43 @@ func getParamOrDefault(c *gin.Context, k, def string) string {
return def
}
+
func getParam(c *gin.Context, k string) string {
+ p, _ := getParamWithType(c, k)
+ return p
+}
+
+func getParamWithType(c *gin.Context, k string) (string, gjson.Type) {
if q := c.Query(k); q != "" {
- return q
+ return q, gjson.Null
}
if c.Request.Method == "POST" {
if h := c.Request.Header.Get("Content-Type"); h != "" {
- if h == "application/x-www-form-urlencoded" {
+ if strings.Contains(h, "application/x-www-form-urlencoded") {
if p, ok := c.GetPostForm(k); ok {
- return p
+ return p, gjson.Null
}
}
- if h == "application/json" {
+ if strings.Contains(h, "application/json") {
if obj, ok := c.Get("json_body"); ok {
res := obj.(gjson.Result).Get(k)
if res.Exists() {
switch res.Type {
case gjson.JSON:
- return res.Raw
+ return res.Raw, gjson.JSON
case gjson.String:
- return res.Str
+ return res.Str, gjson.String
case gjson.Number:
- return strconv.FormatInt(res.Int(), 10) // 似乎没有需要接受 float 类型的api
+ return strconv.FormatInt(res.Int(), 10), gjson.Number // 似乎没有需要接受 float 类型的api
case gjson.True:
- return "true"
+ return "true", gjson.True
case gjson.False:
- return "false"
+ return "false", gjson.False
}
}
}
}
}
}
- return ""
+ return "", gjson.Null
}
diff --git a/server/websocket.go b/server/websocket.go
index a1a9e8f..8dab69a 100644
--- a/server/websocket.go
+++ b/server/websocket.go
@@ -2,25 +2,25 @@ package server
import (
"fmt"
- "github.com/Mrs4s/go-cqhttp/coolq"
- "github.com/Mrs4s/go-cqhttp/global"
- "github.com/gorilla/websocket"
- log "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
- wsc "golang.org/x/net/websocket"
"net/http"
"strconv"
"strings"
"sync"
"time"
+
+ "github.com/Mrs4s/go-cqhttp/coolq"
+ "github.com/Mrs4s/go-cqhttp/global"
+ "github.com/gorilla/websocket"
+ log "github.com/sirupsen/logrus"
+ "github.com/tidwall/gjson"
)
type websocketServer struct {
- bot *coolq.CQBot
- token string
- eventConn []*websocket.Conn
- pushLock *sync.Mutex
- handshake string
+ bot *coolq.CQBot
+ token string
+ eventConn []*websocketConn
+ eventConnMutex sync.Mutex
+ handshake string
}
type websocketClient struct {
@@ -28,9 +28,13 @@ type websocketClient struct {
token string
bot *coolq.CQBot
- pushLock *sync.Mutex
- universalConn *wsc.Conn
- eventConn *wsc.Conn
+ universalConn *websocketConn
+ eventConn *websocketConn
+}
+
+type websocketConn struct {
+ *websocket.Conn
+ sync.Mutex
}
var WebsocketServer = &websocketServer{}
@@ -42,7 +46,6 @@ var upgrader = websocket.Upgrader{
func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) {
s.token = authToken
- s.pushLock = new(sync.Mutex)
s.bot = b
s.handshake = fmt.Sprintf(`{"_post_method":2,"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`,
s.bot.Client.Uin, time.Now().Unix())
@@ -57,7 +60,7 @@ func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) {
}
func NewWebsocketClient(conf *global.GoCQReverseWebsocketConfig, authToken string, b *coolq.CQBot) *websocketClient {
- return &websocketClient{conf: conf, token: authToken, bot: b, pushLock: new(sync.Mutex)}
+ return &websocketClient{conf: conf, token: authToken, bot: b}
}
func (c *websocketClient) Run() {
@@ -79,18 +82,15 @@ func (c *websocketClient) Run() {
func (c *websocketClient) connectApi() {
log.Infof("开始尝试连接到反向Websocket API服务器: %v", c.conf.ReverseApiUrl)
- wsConf, err := wsc.NewConfig(c.conf.ReverseApiUrl, c.conf.ReverseApiUrl)
- if err != nil {
- log.Warnf("连接到反向Websocket API服务器 %v 时出现致命错误: %v", c.conf.ReverseApiUrl, err)
- return
+ header := http.Header{
+ "X-Client-Role": []string{"API"},
+ "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
+ "User-Agent": []string{"CQHttp/4.15.0"},
}
- wsConf.Header["X-Client-Role"] = []string{"API"}
- wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
- wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" {
- wsConf.Header["Authorization"] = []string{"Token " + c.token}
+ header["Authorization"] = []string{"Token " + c.token}
}
- conn, err := wsc.DialConfig(wsConf)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseApiUrl, header)
if err != nil {
log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err)
if c.conf.ReverseReconnectInterval != 0 {
@@ -100,49 +100,44 @@ func (c *websocketClient) connectApi() {
return
}
log.Infof("已连接到反向Websocket API服务器 %v", c.conf.ReverseApiUrl)
- go c.listenApi(conn, false)
+ wrappedConn := &websocketConn{Conn: conn}
+ go c.listenApi(wrappedConn, false)
}
func (c *websocketClient) connectEvent() {
log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventUrl)
- wsConf, err := wsc.NewConfig(c.conf.ReverseEventUrl, c.conf.ReverseEventUrl)
- if err != nil {
- log.Warnf("连接到反向Websocket Event服务器 %v 时出现致命错误: %v", c.conf.ReverseApiUrl, err)
- return
+ header := http.Header{
+ "X-Client-Role": []string{"Event"},
+ "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
+ "User-Agent": []string{"CQHttp/4.15.0"},
}
- wsConf.Header["X-Client-Role"] = []string{"Event"}
- wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
- wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" {
- wsConf.Header["Authorization"] = []string{"Token " + c.token}
+ header["Authorization"] = []string{"Token " + c.token}
}
- conn, err := wsc.DialConfig(wsConf)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventUrl, header)
if err != nil {
- log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err)
+ log.Warnf("连接到反向Websocket Event服务器 %v 时出现错误: %v", c.conf.ReverseEventUrl, err)
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
- c.connectApi()
+ c.connectEvent()
}
return
}
log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventUrl)
- c.eventConn = conn
+ c.eventConn = &websocketConn{Conn: conn}
}
func (c *websocketClient) connectUniversal() {
log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseUrl)
- wsConf, err := wsc.NewConfig(c.conf.ReverseUrl, c.conf.ReverseUrl)
- if err != nil {
- log.Warnf("连接到反向Websocket Universal服务器 %v 时出现致命错误: %v", c.conf.ReverseUrl, err)
- return
+ header := http.Header{
+ "X-Client-Role": []string{"Universal"},
+ "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
+ "User-Agent": []string{"CQHttp/4.15.0"},
}
- wsConf.Header["X-Client-Role"] = []string{"Universal"}
- wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
- wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" {
- wsConf.Header["Authorization"] = []string{"Token " + c.token}
+ header["Authorization"] = []string{"Token " + c.token}
}
- conn, err := wsc.DialConfig(wsConf)
+ conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseUrl, header)
if err != nil {
log.Warnf("连接到反向Websocket Universal服务器 %v 时出现错误: %v", c.conf.ReverseUrl, err)
if c.conf.ReverseReconnectInterval != 0 {
@@ -151,61 +146,61 @@ func (c *websocketClient) connectUniversal() {
}
return
}
- go c.listenApi(conn, true)
- c.universalConn = conn
+ wrappedConn := &websocketConn{Conn: conn}
+ go c.listenApi(wrappedConn, true)
+ c.universalConn = wrappedConn
}
-func (c *websocketClient) listenApi(conn *wsc.Conn, u bool) {
+func (c *websocketClient) listenApi(conn *websocketConn, u bool) {
defer conn.Close()
for {
- var buf []byte
- err := wsc.Message.Receive(conn, &buf)
+ _, buf, err := conn.ReadMessage()
if err != nil {
+ log.Warnf("监听反向WS API时出现错误: %v", err)
break
}
- j := gjson.ParseBytes(buf)
- t := strings.ReplaceAll(j.Get("action").Str, "_async", "")
- log.Debugf("反向WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw)
- if f, ok := wsApi[t]; ok {
- ret := f(c.bot, j.Get("params"))
- if j.Get("echo").Exists() {
- ret["echo"] = j.Get("echo").Value()
- }
- c.pushLock.Lock()
- _, _ = conn.Write([]byte(ret.ToJson()))
- c.pushLock.Unlock()
- }
+
+ go conn.handleRequest(c.bot, buf)
+
}
if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
- if u {
- c.connectUniversal()
- return
+ if !u {
+ go c.connectApi()
}
- c.connectApi()
}
}
func (c *websocketClient) onBotPushEvent(m coolq.MSG) {
- c.pushLock.Lock()
- defer c.pushLock.Unlock()
if c.eventConn != nil {
log.Debugf("向WS服务器 %v 推送Event: %v", c.eventConn.RemoteAddr().String(), m.ToJson())
- _ = c.eventConn.SetWriteDeadline(time.Now().Add(time.Second * 3))
- if _, err := c.eventConn.Write([]byte(m.ToJson())); err != nil {
+ conn := c.eventConn
+ conn.Lock()
+ defer conn.Unlock()
+ _ = c.eventConn.SetWriteDeadline(time.Now().Add(time.Second * 15))
+ if err := c.eventConn.WriteJSON(m); err != nil {
+ log.Warnf("向WS服务器 %v 推送Event时出现错误: %v", c.eventConn.RemoteAddr().String(), err)
_ = c.eventConn.Close()
if c.conf.ReverseReconnectInterval != 0 {
- go func() {
- time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
- c.connectEvent()
- }()
+ time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
+ c.connectEvent()
}
}
}
if c.universalConn != nil {
log.Debugf("向WS服务器 %v 推送Event: %v", c.universalConn.RemoteAddr().String(), m.ToJson())
- _ = c.universalConn.SetWriteDeadline(time.Now().Add(time.Second * 3))
- _, _ = c.universalConn.Write([]byte(m.ToJson()))
+ conn := c.universalConn
+ conn.Lock()
+ defer conn.Unlock()
+ _ = c.universalConn.SetWriteDeadline(time.Now().Add(time.Second * 15))
+ if err := c.universalConn.WriteJSON(m); err != nil {
+ log.Warnf("向WS服务器 %v 推送Event时出现错误: %v", c.universalConn.RemoteAddr().String(), err)
+ _ = c.universalConn.Close()
+ if c.conf.ReverseReconnectInterval != 0 {
+ time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
+ c.connectUniversal()
+ }
+ }
}
}
@@ -223,10 +218,19 @@ func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) {
return
}
err = c.WriteMessage(websocket.TextMessage, []byte(s.handshake))
- if err == nil {
- log.Infof("接受 Websocket 连接: %v (/event)", r.RemoteAddr)
- s.eventConn = append(s.eventConn, c)
+ if err != nil {
+ log.Warnf("Websocket 握手时出现错误: %v", err)
+ c.Close()
+ return
}
+
+ log.Infof("接受 Websocket 连接: %v (/event)", r.RemoteAddr)
+
+ conn := &websocketConn{Conn: c}
+
+ s.eventConnMutex.Lock()
+ s.eventConn = append(s.eventConn, conn)
+ s.eventConnMutex.Unlock()
}
func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) {
@@ -243,7 +247,8 @@ func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) {
return
}
log.Infof("接受 Websocket 连接: %v (/api)", r.RemoteAddr)
- go s.listenApi(c)
+ conn := &websocketConn{Conn: c}
+ go s.listenApi(conn)
}
func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) {
@@ -260,52 +265,72 @@ func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) {
return
}
err = c.WriteMessage(websocket.TextMessage, []byte(s.handshake))
- if err == nil {
- log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr)
- s.eventConn = append(s.eventConn, c)
- s.listenApi(c)
+ if err != nil {
+ log.Warnf("Websocket 握手时出现错误: %v", err)
+ c.Close()
+ return
}
+
+ log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr)
+ conn := &websocketConn{Conn: c}
+ s.eventConn = append(s.eventConn, conn)
+ s.listenApi(conn)
}
-func (s *websocketServer) listenApi(c *websocket.Conn) {
+func (s *websocketServer) listenApi(c *websocketConn) {
defer c.Close()
for {
t, payload, err := c.ReadMessage()
if err != nil {
break
}
+
if t == websocket.TextMessage {
- j := gjson.ParseBytes(payload)
- t := strings.ReplaceAll(j.Get("action").Str, "_async", "") //TODO: async support
- log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw)
- if f, ok := wsApi[t]; ok {
- ret := f(s.bot, j.Get("params"))
- if j.Get("echo").Exists() {
- ret["echo"] = j.Get("echo").Value()
- }
- s.pushLock.Lock()
- _ = c.WriteJSON(ret)
- s.pushLock.Unlock()
- }
+ go c.handleRequest(s.bot, payload)
}
}
}
-func (s *websocketServer) onBotPushEvent(m coolq.MSG) {
- s.pushLock.Lock()
- defer s.pushLock.Unlock()
- pos := 0
- for _, conn := range s.eventConn {
- log.Debugf("向WS客户端 %v 推送Event: %v", conn.RemoteAddr().String(), m.ToJson())
- err := conn.WriteMessage(websocket.TextMessage, []byte(m.ToJson()))
- if err != nil {
- _ = conn.Close()
- s.eventConn = append(s.eventConn[:pos], s.eventConn[pos+1:]...)
- if pos > 0 {
- pos++
- }
+func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Printf("处置WS命令时发生无法恢复的异常:%v", err)
+ c.Close()
+ }
+ }()
+
+ j := gjson.ParseBytes(payload)
+ t := strings.ReplaceAll(j.Get("action").Str, "_async", "")
+ log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw)
+ if f, ok := wsApi[t]; ok {
+ ret := f(bot, j.Get("params"))
+ if j.Get("echo").Exists() {
+ ret["echo"] = j.Get("echo").Value()
+ }
+ c.Lock()
+ defer c.Unlock()
+ _ = c.WriteJSON(ret)
+ }
+}
+
+func (s *websocketServer) onBotPushEvent(m coolq.MSG) {
+ s.eventConnMutex.Lock()
+ defer s.eventConnMutex.Unlock()
+ for i, l := 0, len(s.eventConn); i < l; i++ {
+ conn := s.eventConn[i]
+ log.Debugf("向WS客户端 %v 推送Event: %v", conn.RemoteAddr().String(), m.ToJson())
+ if err := conn.WriteMessage(websocket.TextMessage, []byte(m.ToJson())); err != nil {
+ _ = conn.Close()
+ next := i + 1
+ if next >= l {
+ next = l - 1
+ }
+ s.eventConn[i], s.eventConn[next] = s.eventConn[next], s.eventConn[i]
+ s.eventConn = append(s.eventConn[:next], s.eventConn[next+1:]...)
+ i--
+ l--
+ conn = nil
}
- pos++
}
}
@@ -317,7 +342,7 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
return bot.CQGetFriendList()
},
"get_group_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQGetGroupList()
+ return bot.CQGetGroupList(p.Get("no_cache").Bool())
},
"get_group_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupInfo(p.Get("group_id").Int())
@@ -332,22 +357,29 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
)
},
"send_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ autoEscape := global.EnsureBool(p.Get("auto_escape"), false)
+ if p.Get("message_type").Str == "private" {
+ return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), autoEscape)
+ }
+ if p.Get("message_type").Str == "group" {
+ return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), autoEscape)
+ }
if p.Get("group_id").Int() != 0 {
- return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"))
+ return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), autoEscape)
}
if p.Get("user_id").Int() != 0 {
- return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"))
+ return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), autoEscape)
}
return coolq.MSG{}
},
"send_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"))
+ return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), global.EnsureBool(p.Get("auto_escape"), false))
},
"send_group_forward_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSendGroupForwardMessage(p.Get("group_id").Int(), p.Get("messages"))
},
"send_private_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
- return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"))
+ return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), global.EnsureBool(p.Get("auto_escape"), false))
},
"delete_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQDeleteMessage(int32(p.Get("message_id").Int()))
@@ -368,7 +400,7 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
if p.Get("approve").Exists() {
apr = p.Get("approve").Bool()
}
- return bot.CQProcessGroupRequest(p.Get("flag").Str, subType, apr)
+ return bot.CQProcessGroupRequest(p.Get("flag").Str, subType, p.Get("reason").Str, apr)
},
"set_group_card": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSetGroupCard(p.Get("group_id").Int(), p.Get("user_id").Int(), p.Get("card").Str)
@@ -410,6 +442,9 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupMessage(int32(p.Get("message_id").Int()))
},
+ "get_group_honor_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
+ return bot.CQGetGroupHonorInfo(p.Get("group_id").Int(), p.Get("type").Str)
+ },
"can_send_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQCanSendImage()
},