diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 888004f..48c5184 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,5 +26,5 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} - ldflags: -w -s -X "github.com/Mrs4s/go-cqhttp/coolq.version=${{ env.RELEASE_VERSION }}" + ldflags: -w -s -X "github.com/Mrs4s/go-cqhttp/coolq.Version=${{ env.RELEASE_VERSION }}" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 62ddbbd..478327d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.14.2-alpine AS builder +FROM golang:1.14.7-alpine AS builder RUN go env -w GO111MODULE=auto \ && go env -w CGO_ENABLED=0 \ diff --git a/coolq/api.go b/coolq/api.go index ea4aefa..486c069 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -16,7 +16,7 @@ import ( "github.com/tidwall/gjson" ) -var version = "unknown" +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 { @@ -25,7 +25,7 @@ func (bot *CQBot) CQGetLoginInfo() MSG { // https://cqhttp.cc/docs/4.15/#/API?id=get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8 func (bot *CQBot) CQGetFriendList() MSG { - var fs []MSG + fs := make([]MSG, 0) for _, f := range bot.Client.FriendList { fs = append(fs, MSG{ "nickname": f.Nickname, @@ -38,7 +38,7 @@ 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(noCache bool) MSG { - var gs []MSG + gs := make([]MSG, 0) if noCache { _ = bot.Client.ReloadGroupList() } @@ -68,20 +68,7 @@ func (bot *CQBot) CQGetGroupInfo(groupId int64) MSG { } // https://cqhttp.cc/docs/4.15/#/API?id=get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8 -func (bot *CQBot) CQGetGroupMemberList(groupId int64) MSG { - group := bot.Client.FindGroup(groupId) - if group == nil { - return Failed(100) - } - var members []MSG - for _, m := range group.Members { - members = append(members, convertGroupMemberInfo(groupId, m)) - } - return OK(members) -} - -// https://cqhttp.cc/docs/4.15/#/API?id=get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF -func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64, noCache bool) MSG { +func (bot *CQBot) CQGetGroupMemberList(groupId int64, noCache bool) MSG { group := bot.Client.FindGroup(groupId) if group == nil { return Failed(100) @@ -94,6 +81,19 @@ func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64, noCache bool) MSG } group.Members = t } + members := make([]MSG, 0) + for _, m := range group.Members { + members = append(members, convertGroupMemberInfo(groupId, m)) + } + return OK(members) +} + +// https://cqhttp.cc/docs/4.15/#/API?id=get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF +func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64) MSG { + group := bot.Client.FindGroup(groupId) + if group == nil { + return Failed(100) + } member := group.FindMember(userId) if member == nil { return Failed(102) @@ -198,11 +198,24 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG { name := e.Get("data.name").Str content := bot.ConvertObjectMessage(e.Get("data.content"), true) if uin != 0 && name != "" && len(content) > 0 { + var newElem []message.IMessageElement + for _, elem := range content { + if img, ok := elem.(*message.ImageElement); ok { + gm, err := bot.Client.UploadGroupImage(groupId, img.Data) + if err != nil { + log.Warnf("警告:群 %v 图片上传失败: %v", groupId, err) + continue + } + newElem = append(newElem, gm) + continue + } + newElem = append(newElem, elem) + } nodes = append(nodes, &message.ForwardNode{ SenderId: uin, SenderName: name, Time: int32(ts.Unix()), - Message: content, + Message: newElem, }) return } @@ -291,6 +304,14 @@ func (bot *CQBot) CQSetGroupName(groupId int64, name string) MSG { return Failed(100) } +func (bot *CQBot) CQSetGroupMemo(groupId int64, msg string) MSG { + if g := bot.Client.FindGroup(groupId); g != nil { + g.UpdateMemo(msg) + return OK(nil) + } + return Failed(100) +} + // https://cqhttp.cc/docs/4.15/#/API?id=set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA func (bot *CQBot) CQSetGroupKick(groupId, userId int64, msg string) MSG { if g := bot.Client.FindGroup(groupId); g != nil { @@ -383,6 +404,44 @@ func (bot *CQBot) CQDeleteMessage(messageId int32) MSG { return OK(nil) } +// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98 +func (bot *CQBot) CQSetGroupAdmin(groupId, userId int64, enable bool) MSG { + group := bot.Client.FindGroup(groupId) + if group == nil || group.OwnerUin != bot.Client.Uin { + return Failed(100) + } + mem := group.FindMember(userId) + if mem == nil { + return Failed(100) + } + mem.SetAdmin(enable) + t, err := bot.Client.GetGroupMembers(group) + if err != nil { + log.Warnf("刷新群 %v 成员列表失败: %v", groupId, err) + return Failed(100) + } + group.Members = t + return OK(nil) +} + +func (bot *CQBot) CQGetVipInfo(userId int64) MSG { + msg := MSG{} + vip, err := bot.Client.GetVipInfo(userId) + if err != nil { + return Failed(100) + } + msg = MSG{ + "user_id": vip.Uin, + "nickname": vip.Name, + "level": vip.Level, + "level_speed": vip.LevelSpeed, + "vip_level": vip.VipLevel, + "vip_growth_speed": vip.VipGrowthSpeed, + "vip_growth_total": vip.VipGrowthTotal, + } + return OK(msg) +} + // 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} @@ -525,7 +584,7 @@ func (bot *CQBot) CQGetForwardMessage(resId string) MSG { if m == nil { return Failed(100) } - var r []MSG + r := make([]MSG, 0) for _, n := range m.Nodes { bot.checkMedia(n.Message) r = append(r, MSG{ @@ -568,6 +627,11 @@ func (bot *CQBot) CQCanSendRecord() MSG { return OK(MSG{"yes": true}) } +func (bot *CQBot) CQReloadEventFilter() MSG { + global.BootFilter() + return OK(nil) +} + func (bot *CQBot) CQGetStatus() MSG { return OK(MSG{ "app_initialized": true, @@ -590,7 +654,7 @@ func (bot *CQBot) CQGetVersionInfo() MSG { "plugin_build_configuration": "release", "runtime_version": runtime.Version(), "runtime_os": runtime.GOOS, - "version": version, + "version": Version, }) } diff --git a/coolq/bot.go b/coolq/bot.go index 6f36aa2..063b94d 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -5,17 +5,19 @@ import ( "encoding/gob" "encoding/json" "fmt" - "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" - "github.com/xujiajun/nutsdb" "hash/crc32" "path" "sync" "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" + "github.com/xujiajun/nutsdb" ) type CQBot struct { @@ -27,6 +29,7 @@ type CQBot struct { invitedReqCache sync.Map joinReqCache sync.Map tempMsgCache sync.Map + oneWayMsgCache sync.Map } type MSG map[string]interface{} @@ -67,15 +70,23 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot { bot.Client.OnGroupInvited(bot.groupInvitedEvent) bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent) go func() { + i := conf.HeartbeatInterval + if i < 0 { + log.Warn("警告: 心跳功能已关闭,若非预期,请检查配置文件。") + return + } + if i == 0 { + i = 5 + } for { - time.Sleep(time.Second * 5) + time.Sleep(time.Second * i) bot.dispatchEventMessage(MSG{ "time": time.Now().Unix(), "self_id": bot.Client.Uin, "post_type": "meta_event", "meta_event_type": "heartbeat", "status": nil, - "interval": 5000, + "interval": 1000 * i, }) } }() @@ -159,12 +170,15 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in if msg != nil { id = msg.Id } - } else { - if code, ok := bot.tempMsgCache.Load(target); ok { - msg := bot.Client.SendTempMessage(code.(int64), target, m) - if msg != nil { - id = msg.Id - } + } else if code, ok := bot.tempMsgCache.Load(target); ok { + msg := bot.Client.SendTempMessage(code.(int64), target, m) + if msg != nil { + id = msg.Id + } + } else if _, ok := bot.oneWayMsgCache.Load(target); ok { + msg := bot.Client.SendPrivateMessage(target, m) + if msg != nil { + id = msg.Id } } if id == -1 { @@ -212,7 +226,7 @@ func (bot *CQBot) Release() { func (bot *CQBot) dispatchEventMessage(m MSG) { payload := gjson.Parse(m.ToJson()) - filter := global.GetFilter() + filter := global.EventFilter if filter != nil && (*filter).Eval(payload) == false { log.Debug("Event filtered!") return diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 726129d..76929d5 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -6,6 +6,11 @@ import ( "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" @@ -13,12 +18,6 @@ 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+?.*?]`) @@ -32,6 +31,17 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M 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 = append(r, MSG{ + "type": "reply", + "data": map[string]string{"id": fmt.Sprint(ToGlobalId(code, reply.(*message.ReplyElement).ReplySeq))}, + }) + } for _, elem := range e { m := MSG{} switch o := elem.(type) { @@ -40,6 +50,15 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M "type": "text", "data": map[string]string{"text": o.Content}, } + case *message.LightAppElement: + //m = MSG{ + // "type": "text", + // "data": map[string]string{"text": o.Content}, + //} + m = MSG{ + "type": "json", + "data": map[string]string{"data": o.Content}, + } case *message.AtElement: if o.Target == 0 { m = MSG{ @@ -52,11 +71,6 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M "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", @@ -103,6 +117,18 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M "data": map[string]string{"file": o.Filename, "url": o.Url}, } } + case *message.ServiceElement: + if isOk := strings.Contains(o.Content, "`, - 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 + content := "来自go-cqhttp" + if d["content"] != "" { + content = d["content"] + } + json := fmt.Sprintf("{\"app\": \"com.tencent.structmsg\",\"desc\": \"音乐\",\"meta\": {\"music\": {\"desc\": \"%s\",\"jumpUrl\": \"%s\",\"musicUrl\": \"%s\",\"preview\": \"%s\",\"tag\": \"QQ音乐\",\"title\": \"%s\"}},\"prompt\": \"[分享]%s\",\"ver\": \"0.0.0.1\",\"view\": \"music\"}", content, jumpUrl, purl, preview, name, name) + return message.NewLightApp(json), nil } if d["type"] == "163" { info, err := global.NeteaseMusicSongInfo(d["id"]) @@ -469,17 +406,15 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message. return nil, errors.New("song not found") } name := info.Get("name").Str + jumpUrl := "https://y.music.163.com/m/song/" + d["id"] + musicUrl := "http://music.163.com/song/media/outer/url?id=" + d["id"] + picUrl := info.Get("album.picUrl").Str artistName := "" if info.Get("artists.0").Exists() { artistName = info.Get("artists.0.name").Str } - xml := fmt.Sprintf(``, - name, d["id"], info.Get("album.picUrl").Str, d["id"], name, artistName) - return &message.ServiceElement{ - Id: 60, - Content: xml, - SubType: "music", - }, nil + json := fmt.Sprintf("{\"app\": \"com.tencent.structmsg\",\"desc\":\"音乐\",\"view\":\"music\",\"prompt\":\"[分享]%s\",\"ver\":\"0.0.0.1\",\"meta\":{ \"music\": { \"desc\": \"%s\", \"jumpUrl\": \"%s\", \"musicUrl\": \"%s\", \"preview\": \"%s\", \"tag\": \"网易云音乐\", \"title\":\"%s\"}}}", name, artistName, jumpUrl, musicUrl, picUrl, name) + return message.NewLightApp(json), nil } if d["type"] == "custom" { xml := fmt.Sprintf(``, @@ -496,8 +431,44 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message. template := CQCodeEscapeValue(d["data"]) //println(template) i, _ := strconv.ParseInt(resId, 10, 64) - msg := global.NewXmlMsg(template, i) + msg := message.NewRichXml(template, i) return msg, nil + case "json": + resId := d["resid"] + i, _ := strconv.ParseInt(resId, 10, 64) + log.Warnf("json msg=%s", d["data"]) + if i == 0 { + //默认情况下走小程序通道 + msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) + return msg, nil + } + //resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO + msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) + return msg, nil + case "cardimage": + source := d["source"] + icon := d["icon"] + minwidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) + if minwidth == 0 { + minwidth = 200 + } + minheight, _ := strconv.ParseInt(d["minheight"], 10, 64) + if minheight == 0 { + minheight = 200 + } + maxwidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) + if maxwidth == 0 { + maxwidth = 500 + } + maxheight, _ := strconv.ParseInt(d["maxheight"], 10, 64) + if maxheight == 0 { + maxheight = 1000 + } + img, err := bot.makeImageElem(t, d, group) + if err != nil { + return nil, errors.New("send cardimage faild") + } + return bot.SendNewPic(img, source, icon, minwidth, minheight, maxwidth, maxheight, group) default: return nil, errors.New("unsupported cq code: " + t) } @@ -530,3 +501,153 @@ func CQCodeUnescapeValue(content string) string { ret = CQCodeUnescapeText(ret) return ret } + +// 图片 elem 生成器,单独拎出来,用于公用 +func (bot *CQBot) makeImageElem(t string, d map[string]string, group bool) (message.IMessageElement, error) { + 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") { + b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) + if err != nil { + return nil, err + } + return message.NewImage(b), nil + } + if strings.HasPrefix(f, "file") { + fu, err := url.Parse(f) + if err != nil { + return nil, err + } + if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { + fu.Path = fu.Path[1:] + } + b, err := ioutil.ReadFile(fu.Path) + if err != nil { + return nil, err + } + return message.NewImage(b), nil + } + rawPath := path.Join(global.IMAGE_PATH, f) + if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { + rawPath += ".cqimg" + } + if !global.PathExists(rawPath) && d["url"] != "" { + return bot.ToElement(t, map[string]string{"file": d["url"]}, group) + } + if global.PathExists(rawPath) { + b, err := ioutil.ReadFile(rawPath) + if err != nil { + return nil, err + } + if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { + return message.NewImage(b), nil + } + if len(b) < 20 { + return nil, errors.New("invalid local file") + } + 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) + 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() + 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 { + return nil, errors.New("invalid hash") + } + if group { + rsp, err := bot.Client.QueryGroupImage(1, hash, size) + if err != nil { + if url != "" { + return bot.ToElement(t, map[string]string{"file": url}, group) + } + return nil, err + } + return rsp, nil + } + rsp, err := bot.Client.QueryFriendImage(1, hash, size) + if err != nil { + if url != "" { + return bot.ToElement(t, map[string]string{"file": url}, group) + } + return nil, err + } + return rsp, nil + } + return nil, errors.New("invalid image") +} + +//SendNewPic 一种xml 方式发送的群消息图片 +func (bot *CQBot) SendNewPic(elem message.IMessageElement, source string, icon string, minwidth int64, minheigt int64, maxwidth int64, maxheight int64, group bool) (*message.ServiceElement, error) { + var xml string + xml = "" + if i, ok := elem.(*message.ImageElement); ok { + if group == false { + gm, err := bot.Client.UploadPrivateImage(1, i.Data) + if err != nil { + log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minwidth, minheigt, maxwidth, maxheight, source, icon) + + } else { + gm, err := bot.Client.UploadGroupImage(1, i.Data) + if err != nil { + log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minwidth, minheigt, maxwidth, maxheight, source, icon) + } + } + if i, ok := elem.(*message.GroupImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minwidth, minheigt, maxwidth, maxheight, source, icon) + } + if i, ok := elem.(*message.FriendImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minwidth, minheigt, maxwidth, maxheight, source, icon) + } + if xml != "" { + log.Warn(xml) + XmlMsg := message.NewRichXml(xml, 5) + return XmlMsg, nil + } + return nil, errors.New("发送xml图片消息失败") +} diff --git a/coolq/event.go b/coolq/event.go index 142e096..e91a0ed 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -32,6 +32,9 @@ func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) { bot.checkMedia(m.Elements) cqm := ToStringMessage(m.Elements, 0, true) + if !m.Sender.IsFriend { + bot.oneWayMsgCache.Store(m.Sender.Uin, "") + } log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm) fm := MSG{ "post_type": "message", diff --git a/docs/EventFilter.md b/docs/EventFilter.md new file mode 100644 index 0000000..c9ccf05 --- /dev/null +++ b/docs/EventFilter.md @@ -0,0 +1,132 @@ +# 事件过滤器 + +在go-cqhttp同级目录下新建`filter.json`文件即可开启事件过滤器,启动时会读取该文件中定义的过滤规则(使用 JSON 编写),若文件不存在,或过滤规则语法错误,则会暂停所有上报。 +事件过滤器会处理所有事件(包括心跳事件在内的元事件),请谨慎使用!! + +## 示例 + +这节首先给出一些示例,演示过滤器的基本用法,下一节将给出具体语法说明。 + +### 只上报以「!!」开头的消息 + +```json +{ + "raw_message": { + ".regex": "^!!" + } +} +``` + +### 只上报群组的非匿名消息 + +```json +{ + "message_type": "group", + "anonymous": { + ".eq": null + } +} +``` + +### 只上报私聊或特定群组的非匿名消息 + +```json +{ + ".or": [ + { + "message_type": "private" + }, + { + "message_type": "group", + "group_id": { + ".in": [ + 123456 + ] + }, + "anonymous": { + ".eq": null + } + } + ] +} +``` + +### 只上报群组 11111、22222、33333 中不是用户 12345 发送的消息,以及用户 66666 发送的所有消息 + +```json +{ + ".or": [ + { + "group_id": { + ".in": [11111, 22222, 33333] + }, + "user_id": { + ".neq": 12345 + } + }, + { + "user_id": 66666 + } + ] +} +``` + +### 一个更复杂的例子 + +```json +{ + ".or": [ + { + "message_type": "private", + "user_id": { + ".not": { + ".in": [11111, 22222, 33333] + }, + ".neq": 44444 + } + }, + { + "message_type": { + ".regex": "group|discuss" + }, + ".or": [ + { + "group_id": 12345 + }, + { + "raw_message": { + ".contains": "通知" + } + } + ] + } + ] +} +``` + +## 语法说明 + +过滤规则最外层是一个 JSON 对象,其中的键,如果以 `.`(点号)开头,则表示运算符,其值为运算符的参数,如果不以 `.` 开头,则表示对事件数据对象中相应键的过滤。过滤规则中任何一个对象,只有在它的所有项都匹配的情况下,才会让事件通过(等价于一个 `and` 运算);其中,不以 `.` 开头的键,若其值不是对象,则只有在这个值和事件数据相应值相等的情况下,才会通过(等价于一个 `eq` 运算符)。 + +下面列出所有运算符(「要求的参数类型」是指运算符的键所对应的值的类型,「可作用于的类型」是指在过滤时事件对象相应值的类型): + +| 运算符 | 要求的参数类型 | 可作用于的类型 | +| ----- | ------------ | ----------- | +| `.not` | object | 任何 | +| `.and` | object | 若参数中全为运算符,则任何;若不全为运算符,则 object | +| `.or` | array(数组元素为 object) | 任何 | +| `.eq` | 任何 | 任何 | +| `.neq` | 任何 | 任何 | +| `.in` | string/array | 若参数为 string,则 string;若参数为 array,则任何 | +| `.contains` | string | string | +| `.regex` | string | string | + + +## 过滤时的事件数据对象 + +过滤器在go-cqhttp构建好事件数据后运行,各事件的数据字段见[OneBot标准]( https://github.com/howmanybots/onebot/blob/master/v11/specs/event/README.md )。 + +这里有几点需要注意: + +- `message` 字段在运行过滤器时是消息段数组的形式(见 [消息格式]( https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md )) +- `raw_message` 字段为未经**CQ码**处理的原始消息字符串,这意味着其中可能会出现形如 `[CQ:face,id=123]` 的 CQ 码 diff --git a/docs/config.md b/docs/config.md index 245800f..ccdff7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -22,11 +22,20 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为: "password_encrypted": "", "enable_db": true, "access_token": "", - "relogin": false, - "relogin_delay": 0, + "relogin": { + "enabled": true, + "relogin_delay": 3, + "max_relogin_times": 0 + }, + "_rate_limit": { + "enabled": false, + "frequency": 1, + "bucket_size": 1 + }, "post_message_format": "string", "ignore_invalid_cqcode": false, "force_fragmented": true, + "heartbeat_interval": 5, "http_config": { "enabled": true, "host": "0.0.0.0", @@ -55,21 +64,29 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为: | ------------------ | -------- | ------------------------------------------------------------------- | | uin | int64 | 登录用QQ号 | | password | string | 登录用密码 | -| encrypt_password | bool | 是否对密码进行加密. | +| 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码 | +| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 | +| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 | +| relogin | bool | 是否自动重新登录 | +| relogin_delay | int | 重登录延时(秒) | +| max_relogin_times | uint | 最大重登录次数,若0则不设置上限 | +| _rate_limit | bool | 是否启用API调用限速 | +| frequency | float64 | 1s内能调用API的次数 | +| bucket_size | int | 令牌桶的大小,默认为1,修改此值可允许一定程度内连续调用api | +| post_message_format | string | 上报信息类型 | +| ignore_invalid_cqcode| bool | 是否忽略错误的CQ码 | | force_fragmented | bool | 是否强制分片发送群长消息 | -| http_config | object | HTTP API配置 | -| ws_config | object | Websocket API 配置 | +| heartbeat_interval | int64 | 心跳间隔时间,单位秒,若0则关闭心跳 | +| http_config | object | HTTP API配置 | +| ws_config | object | Websocket API 配置 | | ws_reverse_servers | object[] | 反向 Websocket API 配置 | +| log_level | string | 指定日志收集级别,将收集的日志单独存放到固定文件中,便于查看日志线索 当前支持 warn,error| > 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误. > 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取. > 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥 -> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析. \ No newline at end of file +> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析. + +> 注3:关闭心跳服务可能引起断线,请谨慎关闭 diff --git a/docs/cqhttp.md b/docs/cqhttp.md index a0f5c42..6c13a80 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -123,7 +123,7 @@ Type: `node` Type: `xml` -范围: **发送** +范围: **发送/接收** 参数: @@ -135,8 +135,11 @@ Type: `xml` 示例: `[CQ:xml,data=xxxx]` ####一些xml样例 + ####ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中 + #### qq音乐 + ```xml ``` @@ -165,6 +168,62 @@ Type: `xml` ``` +### json消息支持 + +Type: `json` + +范围: **发送/接收** + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| data | string | json内容,json的所有字符串记得实体化处理| +| resid | int32 | 默认不填为0,走小程序通道,填了走富文本通道发送| + +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 接口的消息都存在风控风险,请自行兼容发送失败后的处理(可以失败后走普通图片模式) + +Type: `cardimage` + +范围: **发送** + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| file | string | 和image的file字段对齐,支持也是一样的| +| minwidth | int64 | 默认不填为400,最小width| +| minheight | int64 | 默认不填为400,最小height| +| maxwidth | int64 | 默认不填为500,最大width| +| maxheight | int64 | 默认不填为1000,最大height| +| 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] +``` ## API @@ -177,7 +236,7 @@ Type: `xml` | 字段 | 类型 | 说明 | | -------- | ------ | ---- | | group_id | int64 | 群号 | -| name | string | 新名 | +| group_name | string | 新名 | ### 获取图片信息 diff --git a/global/config.go b/global/config.go index 5f13ac4..31ed776 100644 --- a/global/config.go +++ b/global/config.go @@ -2,25 +2,39 @@ package global import ( "encoding/json" + "os" + "strconv" + "time" + log "github.com/sirupsen/logrus" ) 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"` + 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 struct { + Enabled bool `json:"enabled"` + ReLoginDelay int `json:"relogin_delay"` + MaxReloginTimes uint `json:"max_relogin_times"` + } `json:"relogin"` + RateLimit struct { + Enabled bool `json:"enabled"` + Frequency float64 `json:"frequency"` + BucketSize int `json:"bucket_size"` + } `json:"_rate_limit"` IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"` ForceFragmented bool `json:"force_fragmented"` + HeartbeatInterval time.Duration `json:"heartbeat_interval"` 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"` + LogLevel string `json:"log_level"` } type CQHttpApiConfig struct { @@ -66,9 +80,25 @@ type GoCQReverseWebsocketConfig struct { func DefaultConfig() *JsonConfig { return &JsonConfig{ - EnableDB: true, - ReLogin: true, - ReLoginDelay: 3, + EnableDB: true, + ReLogin: struct { + Enabled bool `json:"enabled"` + ReLoginDelay int `json:"relogin_delay"` + MaxReloginTimes uint `json:"max_relogin_times"` + }{ + Enabled: true, + ReLoginDelay: 3, + MaxReloginTimes: 0, + }, + RateLimit: struct { + Enabled bool `json:"enabled"` + Frequency float64 `json:"frequency"` + BucketSize int `json:"bucket_size"` + }{ + Enabled: false, + Frequency: 1, + BucketSize: 1, + }, PostMessageFormat: "string", ForceFragmented: true, HttpConfig: &GoCQHttpConfig{ @@ -103,6 +133,8 @@ func Load(p string) *JsonConfig { err := json.Unmarshal([]byte(ReadAllText(p)), &c) if err != nil { log.Warnf("尝试加载配置文件 %v 时出现错误: %v", p, err) + log.Infoln("原文件已备份") + os.Rename(p, p+".backup"+strconv.FormatInt(time.Now().Unix(), 10)) return nil } return &c diff --git a/global/filter.go b/global/filter.go index e7fdad6..b2ac4cb 100644 --- a/global/filter.go +++ b/global/filter.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "regexp" "strings" - "sync" ) type Filter interface { @@ -14,7 +13,7 @@ type Filter interface { } type OperationNode struct { - key string + key string filter Filter } @@ -24,15 +23,14 @@ type NotOperator struct { func notOperatorConstruct(argument gjson.Result) *NotOperator { if !argument.IsObject() { - log.Error("the argument of 'not' operator must be an object") + panic("the argument of 'not' operator must be an object") } op := new(NotOperator) - op.operand_ = GetOperatorFactory().Generate("and", argument) + op.operand_ = Generate("and", argument) return op } func (notOperator NotOperator) Eval(payload gjson.Result) bool { - log.Debug("not " + payload.Str) return !(notOperator.operand_).Eval(payload) } @@ -42,7 +40,7 @@ type AndOperator struct { func andOperatorConstruct(argument gjson.Result) *AndOperator { if !argument.IsObject() { - log.Error("the argument of 'and' operator must be an object") + panic("the argument of 'and' operator must be an object") } op := new(AndOperator) argument.ForEach(func(key, value gjson.Result) bool { @@ -52,19 +50,19 @@ func andOperatorConstruct(argument gjson.Result) *AndOperator { // "bar": "baz" // } opKey := key.Str[1:] - op.operands = append(op.operands, OperationNode{"", GetOperatorFactory().Generate(opKey, value)}) + op.operands = append(op.operands, OperationNode{"", 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)}) + opKey := key.String() + op.operands = append(op.operands, OperationNode{opKey, 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)}) + opKey := key.String() + op.operands = append(op.operands, OperationNode{opKey, Generate("eq", value)}) } return true }) @@ -72,7 +70,6 @@ func andOperatorConstruct(argument gjson.Result) *AndOperator { } func (andOperator *AndOperator) Eval(payload gjson.Result) bool { - log.Debug("and " + payload.Str) res := true for _, operand := range andOperator.operands { @@ -98,19 +95,18 @@ type OrOperator struct { func orOperatorConstruct(argument gjson.Result) *OrOperator { if !argument.IsArray() { - log.Error("the argument of 'or' operator must be an array") + panic("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)) + op.operands = append(op.operands, Generate("and", value)) return true }) return op } func (orOperator OrOperator) Eval(payload gjson.Result) bool { - log.Debug("or "+ payload.Str) - res:= false + res := false for _, operand := range orOperator.operands { res = res || operand.Eval(payload) @@ -132,8 +128,7 @@ func equalOperatorConstruct(argument gjson.Result) *EqualOperator { } func (equalOperator EqualOperator) Eval(payload gjson.Result) bool { - log.Debug("eq "+ payload.Str + "==" + equalOperator.value.Str) - return payload.Str == equalOperator.value.Str + return payload.String() == equalOperator.value.String() } type NotEqualOperator struct { @@ -147,18 +142,16 @@ func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator { } func (notEqualOperator NotEqualOperator) Eval(payload gjson.Result) bool { - log.Debug("neq " + payload.Str) - return !(payload.Str == notEqualOperator.value.Str) + return !(payload.String() == notEqualOperator.value.String()) } - 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") + panic("the argument of 'in' operator must be an array or a string") } op := new(InOperator) op.operand = argument @@ -166,16 +159,15 @@ func inOperatorConstruct(argument gjson.Result) *InOperator { } 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 + res = res || value.String() == payload.String() return true }) return res } - return strings.Contains(inOperator.operand.Str, payload.Str) + return strings.Contains(inOperator.operand.String(), payload.String()) } type ContainsOperator struct { @@ -184,15 +176,14 @@ type ContainsOperator struct { func containsOperatorConstruct(argument gjson.Result) *ContainsOperator { if argument.IsArray() || argument.IsObject() { - log.Error("the argument of 'contains' operator must be a string") + panic("the argument of 'contains' operator must be a string") } op := new(ContainsOperator) - op.operand = argument.Str + op.operand = argument.String() return op } func (containsOperator ContainsOperator) Eval(payload gjson.Result) bool { - log.Debug("contains "+ payload.Str) if payload.IsObject() || payload.IsArray() { return false } @@ -205,29 +196,19 @@ type RegexOperator struct { func regexOperatorConstruct(argument gjson.Result) *RegexOperator { if argument.IsArray() || argument.IsObject() { - log.Error("the argument of 'regex' operator must be a string") + panic("the argument of 'regex' operator must be a string") } op := new(RegexOperator) - op.regex = argument.Str + op.regex = argument.String() return op } func (containsOperator RegexOperator) Eval(payload gjson.Result) bool { - log.Debug("regex " + payload.Str) - matched, _ := regexp.MatchString(containsOperator.regex, payload.Str) + matched, _ := regexp.MatchString(containsOperator.regex, payload.String()) return matched } -// 单例工厂 -type operatorFactory struct{ -} -var instance *operatorFactory = &operatorFactory{} - -func GetOperatorFactory() *operatorFactory { - return instance -} - -func (o operatorFactory) Generate(opName string, argument gjson.Result) Filter { +func Generate(opName string, argument gjson.Result) Filter { switch opName { case "not": return notOperatorConstruct(argument) @@ -246,22 +227,25 @@ func (o operatorFactory) Generate(opName string, argument gjson.Result) Filter { case "regex": return regexOperatorConstruct(argument) default: - log.Warnf("the operator '%s' is not supported", opName) - return nil + panic("the operator " + opName + " is not supported") } } -var filter = new(Filter) -var once sync.Once // 过滤器单例模式 +var EventFilter = new(Filter) -func GetFilter() *Filter { - once.Do(func() { - f, err := ioutil.ReadFile("filter.json") - if err != nil { - filter = nil +func BootFilter() { + defer func() { + if e := recover(); e != nil { + log.Warnf("事件过滤器启动失败: %v", e) + EventFilter = nil } else { - *filter = GetOperatorFactory().Generate("and", gjson.ParseBytes(f)) + log.Info("事件过滤器启动成功.") } - }) - return filter -} \ No newline at end of file + }() + f, err := ioutil.ReadFile("filter.json") + if err != nil { + panic(err) + } else { + *EventFilter = Generate("and", gjson.ParseBytes(f)) + } +} diff --git a/global/net.go b/global/net.go index e394efd..55f27de 100644 --- a/global/net.go +++ b/global/net.go @@ -4,13 +4,10 @@ import ( "bytes" "compress/gzip" "fmt" + "github.com/tidwall/gjson" "io/ioutil" "net/http" - "strconv" "strings" - - "github.com/Mrs4s/MiraiGo/message" - "github.com/tidwall/gjson" ) func GetBytes(url string) ([]byte, error) { @@ -54,18 +51,3 @@ func NeteaseMusicSongInfo(id string) (gjson.Result, error) { 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/ratelimit.go b/global/ratelimit.go new file mode 100644 index 0000000..79594be --- /dev/null +++ b/global/ratelimit.go @@ -0,0 +1,20 @@ +package global + +import ( + "context" + "golang.org/x/time/rate" +) + +var limiter *rate.Limiter +var limitEnable = false + +func RateLimit(ctx context.Context) { + if limitEnable { + _ = limiter.Wait(ctx) + } +} + +func InitLimiter(r float64, b int) { + limitEnable = true + limiter = rate.NewLimiter(rate.Limit(r), b) +} diff --git a/go.mod b/go.mod index 426ba6c..5a5723f 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,29 @@ module github.com/Mrs4s/go-cqhttp go 1.14 require ( - github.com/Mrs4s/MiraiGo v0.0.0-20200825052841-d3b0f5f9e839 + github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/gin-gonic/gin v1.6.3 + github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/gorilla/websocket v1.4.2 - github.com/guonaihong/gout v0.1.1 + github.com/guonaihong/gout v0.1.2 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/json-iterator/go v1.1.10 // indirect + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible + github.com/lestrrat-go/strftime v1.0.3 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 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/tidwall/gjson v1.6.1 github.com/xujiajun/nutsdb v0.5.0 github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 - golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect + golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect + golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect + golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index ef1cb9a..7d5ff26 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ 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-20200825052841-d3b0f5f9e839 h1:TDhaPfWcubIMKDz1HU+N07SwIUjj7oVUQ7EXZBbDUxs= -github.com/Mrs4s/MiraiGo v0.0.0-20200825052841-d3b0f5f9e839/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM= +github.com/Mrs4s/MiraiGo v0.0.0-20200909095006-dde8bded28d1 h1:3cmUqA5RaikLx+59SODlBA7tjORQoh4t1w5CzH5bIH8= +github.com/Mrs4s/MiraiGo v0.0.0-20200909095006-dde8bded28d1/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo= +github.com/Mrs4s/MiraiGo v0.0.0-20200909103204-808a63a78efe h1:O2BW87BwpwZDsn7YFHLfRGFGvTS4OUZsG2UiA13OxcQ= +github.com/Mrs4s/MiraiGo v0.0.0-20200909103204-808a63a78efe/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo= +github.com/Mrs4s/MiraiGo v0.0.0-20200910010455-37409b1f6b9c h1:bhVr3W0+WTVN+vgZGlxD4iFSV9L3CmUg/lt91h+Ll18= +github.com/Mrs4s/MiraiGo v0.0.0-20200910010455-37409b1f6b9c/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo= +github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099 h1:b+Tmo9h5leZmQokdUu8c2xSIRkkSYoP1z8G+zcwwyRY= +github.com/Mrs4s/MiraiGo v0.0.0-20200910013944-236c0f629099/go.mod h1:cwYPI2uq6nxNbx0nA6YuAKF1V5szSs6FPlGVLQvRUlo= 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= @@ -23,8 +29,9 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= +github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -47,12 +54,13 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 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/guonaihong/gout v0.1.2 h1:TR2XCRopGgJdj231IayEoeavgbznFXzzzcZVdT/hG10= +github.com/guonaihong/gout v0.1.2/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/json-iterator/go v1.1.10/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= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -62,13 +70,17 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible h1:4mNlp+/SvALIPFpbXV3kxNJJno9iKFWGxSDE13Kl66Q= github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= -github.com/lestrrat-go/strftime v1.0.1 h1:o7qz5pmLzPDLyGW4lG6JvTKPUfTFXwe+vOamIYWtnVU= -github.com/lestrrat-go/strftime v1.0.1/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.0.3 h1:qqOPU7y+TM8Y803I8fG9c/DyKG3xH/xkng6keC1015Q= +github.com/lestrrat-go/strftime v1.0.3/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -76,6 +88,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -87,12 +101,12 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P 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/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws= +github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -107,6 +121,7 @@ github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJl github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if55Fu3HOwbfcaQlJ27p3oeJU2JZqoeT3ITJQk= github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189/go.mod h1:rIrm5geMiBhPQkdfUm8gDFi/WiHneOp1i9KjmJqc+9I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -115,8 +130,10 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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 h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -124,13 +141,19 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -159,7 +182,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index b45d6e3..b8a8271 100644 --- a/main.go +++ b/main.go @@ -7,15 +7,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/Mrs4s/MiraiGo/binary" - "github.com/Mrs4s/MiraiGo/client" - "github.com/Mrs4s/go-cqhttp/coolq" - "github.com/Mrs4s/go-cqhttp/global" - "github.com/Mrs4s/go-cqhttp/server" - rotatelogs "github.com/lestrrat-go/file-rotatelogs" - log "github.com/sirupsen/logrus" - easy "github.com/t-tomalak/logrus-easy-formatter" - asciiart "github.com/yinghau76/go-ascii-art" "image" "io" "io/ioutil" @@ -25,6 +16,18 @@ import ( "strconv" "strings" "time" + + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/client" + "github.com/Mrs4s/go-cqhttp/coolq" + "github.com/Mrs4s/go-cqhttp/global" + "github.com/Mrs4s/go-cqhttp/server" + + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/rifflock/lfshook" + log "github.com/sirupsen/logrus" + easy "github.com/t-tomalak/logrus-easy-formatter" + asciiart "github.com/yinghau76/go-ascii-art" ) func init() { @@ -133,6 +136,46 @@ func main() { time.Sleep(time.Second * 5) return } + + // log classified by level + // Collect all records up to the specified level (default level: warn) + logLevel := conf.LogLevel + if logLevel != "" { + date := time.Now().Format("2006-01-02") + var logPathMap lfshook.PathMap + switch conf.LogLevel { + case "warn": + logPathMap = lfshook.PathMap{ + log.WarnLevel: path.Join("logs", date+"-warn.log"), + log.ErrorLevel: path.Join("logs", date+"-warn.log"), + log.FatalLevel: path.Join("logs", date+"-warn.log"), + log.PanicLevel: path.Join("logs", date+"-warn.log"), + } + case "error": + logPathMap = lfshook.PathMap{ + log.ErrorLevel: path.Join("logs", date+"-error.log"), + log.FatalLevel: path.Join("logs", date+"-error.log"), + log.PanicLevel: path.Join("logs", date+"-error.log"), + } + default: + logPathMap = lfshook.PathMap{ + log.WarnLevel: path.Join("logs", date+"-warn.log"), + log.ErrorLevel: path.Join("logs", date+"-warn.log"), + log.FatalLevel: path.Join("logs", date+"-warn.log"), + log.PanicLevel: path.Join("logs", date+"-warn.log"), + } + } + + log.AddHook(lfshook.NewHook( + logPathMap, + &easy.Formatter{ + TimestampFormat: "2006-01-02 15:04:05", + LogFormat: "[%time%] [%lvl%]: %msg% \n", + }, + )) + } + + log.Info("当前版本:", coolq.Version) if conf.Debug { log.SetLevel(log.DebugLevel) log.Warnf("已开启Debug模式.") @@ -220,6 +263,11 @@ func main() { } else { coolq.SetMessageFormat(conf.PostMessageFormat) } + if conf.RateLimit.Enabled { + global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize) + } + log.Info("正在加载事件过滤器.") + global.BootFilter() coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode coolq.ForceFragmented = conf.ForceFragmented if conf.HttpConfig != nil && conf.HttpConfig.Enabled { @@ -237,24 +285,39 @@ func main() { log.Info("资源初始化完成, 开始处理信息.") log.Info("アトリは、高性能ですから!") cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { - if conf.ReLogin { - log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连.", e.Message, conf.ReLoginDelay) - time.Sleep(time.Second * time.Duration(conf.ReLoginDelay)) - rsp, err := cli.Login() - if err != nil { - log.Fatalf("重连失败: %v", err) - } - if !rsp.Success { - switch rsp.Error { - case client.NeedCaptcha: - log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)") - case client.UnsafeDeviceError: - log.Fatalf("重连失败: 设备锁") - default: - log.Fatalf("重连失败: %v", rsp.ErrorMessage) + if conf.ReLogin.Enabled { + var times uint = 1 + for { + + if conf.ReLogin.MaxReloginTimes == 0 { + } else if times > conf.ReLogin.MaxReloginTimes { + break } + log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v", + e.Message, conf.ReLogin.ReLoginDelay, times) + times++ + time.Sleep(time.Second * time.Duration(conf.ReLogin.ReLoginDelay)) + rsp, err := cli.Login() + if err != nil { + log.Errorf("重连失败: %v", err) + continue + } + if !rsp.Success { + switch rsp.Error { + case client.NeedCaptcha: + log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)") + case client.UnsafeDeviceError: + log.Fatalf("重连失败: 设备锁") + default: + log.Errorf("重连失败: %v", rsp.ErrorMessage) + continue + } + } + log.Info("重连成功") + return + } - return + log.Fatal("重连失败: 重连次数达到设置的上限值") } b.Release() log.Fatalf("Bot已离线:%v", e.Message) diff --git a/server/http.go b/server/http.go index ad864f5..9b10904 100644 --- a/server/http.go +++ b/server/http.go @@ -1,6 +1,7 @@ package server import ( + "context" "crypto/hmac" "crypto/sha1" "encoding/hex" @@ -74,87 +75,7 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) { }) } - s.engine.Any("/get_login_info", s.GetLoginInfo) - s.engine.Any("/get_login_info_async", s.GetLoginInfo) - - s.engine.Any("/get_friend_list", s.GetFriendList) - s.engine.Any("/get_friend_list_async", s.GetFriendList) - - s.engine.Any("/get_group_list", s.GetGroupList) - s.engine.Any("/get_group_list_async", s.GetGroupList) - - s.engine.Any("/get_group_info", s.GetGroupInfo) - s.engine.Any("/get_group_info_async", s.GetGroupInfo) - - s.engine.Any("/get_group_member_list", s.GetGroupMemberList) - s.engine.Any("/get_group_member_list_async", s.GetGroupMemberList) - - s.engine.Any("/get_group_member_info", s.GetGroupMemberInfo) - s.engine.Any("/get_group_member_info_async", s.GetGroupMemberInfo) - - s.engine.Any("/send_msg", s.SendMessage) - s.engine.Any("/send_msg_async", s.SendMessage) - - s.engine.Any("/send_private_msg", s.SendPrivateMessage) - s.engine.Any("/send_private_msg_async", s.SendPrivateMessage) - - s.engine.Any("/send_group_msg", s.SendGroupMessage) - s.engine.Any("/send_group_msg_async", s.SendGroupMessage) - - s.engine.Any("/send_group_forward_msg", s.SendGroupForwardMessage) - s.engine.Any("/send_group_forward_msg_async", s.SendGroupForwardMessage) - - s.engine.Any("/delete_msg", s.DeleteMessage) - s.engine.Any("/delete_msg_async", s.DeleteMessage) - - s.engine.Any("/set_friend_add_request", s.ProcessFriendRequest) - s.engine.Any("/set_friend_add_request_async", s.ProcessFriendRequest) - - s.engine.Any("/set_group_add_request", s.ProcessGroupRequest) - s.engine.Any("/set_group_add_request_async", s.ProcessGroupRequest) - - s.engine.Any("/set_group_card", s.SetGroupCard) - s.engine.Any("/set_group_card_async", s.SetGroupCard) - - s.engine.Any("/set_group_special_title", s.SetSpecialTitle) - s.engine.Any("/set_group_special_title_async", s.SetSpecialTitle) - - s.engine.Any("/set_group_kick", s.SetGroupKick) - s.engine.Any("/set_group_kick_async", s.SetGroupKick) - - s.engine.Any("/set_group_ban", s.SetGroupBan) - s.engine.Any("/set_group_ban_async", s.SetGroupBan) - - s.engine.Any("/set_group_whole_ban", s.SetWholeBan) - s.engine.Any("/set_group_whole_ban_async", s.SetWholeBan) - - s.engine.Any("/set_group_name", s.SetGroupName) - s.engine.Any("/set_group_name_async", s.SetGroupName) - - s.engine.Any("/set_group_leave", s.SetGroupLeave) - s.engine.Any("/set_group_leave_async", s.SetGroupLeave) - - s.engine.Any("/get_image", s.GetImage) - - s.engine.Any("/get_forward_msg", s.GetForwardMessage) - - s.engine.Any("/get_group_msg", 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) - - s.engine.Any("/can_send_record", s.CanSendRecord) - s.engine.Any("/can_send_record_async", s.CanSendRecord) - - s.engine.Any("/get_status", s.GetStatus) - s.engine.Any("/get_status_async", s.GetStatus) - - s.engine.Any("/get_version_info", s.GetVersionInfo) - s.engine.Any("/get_version_info_async", s.GetVersionInfo) - - s.engine.Any("/.handle_quick_operation", s.HandleQuickOperation) + s.engine.Any("/:action", s.HandleActions) go func() { log.Infof("CQ HTTP 服务器已启动: %v", addr) @@ -199,7 +120,7 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) { return h }()).SetTimeout(time.Second * time.Duration(c.timeout)).Do() if err != nil { - log.Warnf("上报Event数据到 %v 失败: %v", c.addr, err) + log.Warnf("上报Event数据 %v 到 %v 失败: %v", m.ToJson(), c.addr, err) return } if gjson.Valid(res) { @@ -207,6 +128,17 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) { } } +func (s *httpServer) HandleActions(c *gin.Context) { + global.RateLimit(context.Background()) + action := strings.ReplaceAll(c.Param("action"), "_async", "") + log.Debugf("HTTPServer接收到API调用: %v", action) + if f, ok := httpApi[action]; ok { + f(s, c) + } else { + c.JSON(200, coolq.Failed(404)) + } +} + func (s *httpServer) GetLoginInfo(c *gin.Context) { c.JSON(200, s.bot.CQGetLoginInfo()) } @@ -227,14 +159,14 @@ func (s *httpServer) GetGroupInfo(c *gin.Context) { func (s *httpServer) GetGroupMemberList(c *gin.Context) { gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) - c.JSON(200, s.bot.CQGetGroupMemberList(gid)) + nc := getParamOrDefault(c, "no_cache", "false") + c.JSON(200, s.bot.CQGetGroupMemberList(gid, nc == "true")) } func (s *httpServer) GetGroupMemberInfo(c *gin.Context) { gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64) - nc := getParamOrDefault(c, "no_cache", "false") - c.JSON(200, s.bot.CQGetGroupMemberInfo(gid, uid, nc == "true")) + c.JSON(200, s.bot.CQGetGroupMemberInfo(gid, uid)) } func (s *httpServer) SendMessage(c *gin.Context) { @@ -347,7 +279,18 @@ func (s *httpServer) SetWholeBan(c *gin.Context) { func (s *httpServer) SetGroupName(c *gin.Context) { gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) - c.JSON(200, s.bot.CQSetGroupName(gid, getParam(c, "name"))) + c.JSON(200, s.bot.CQSetGroupName(gid, getParam(c, "group_name"))) +} + +func (s *httpServer) SetGroupAdmin(c *gin.Context) { + gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) + uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64) + c.JSON(200, s.bot.CQSetGroupAdmin(gid, uid, getParamOrDefault(c, "enable", "true") == "true")) +} + +func (s *httpServer) SendGroupNotice(c *gin.Context) { + gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) + c.JSON(200, s.bot.CQSetGroupMemo(gid, getParam(c, "content"))) } func (s *httpServer) SetGroupLeave(c *gin.Context) { @@ -381,6 +324,15 @@ func (s *httpServer) GetVersionInfo(c *gin.Context) { c.JSON(200, s.bot.CQGetVersionInfo()) } +func (s *httpServer) ReloadEventFilter(c *gin.Context) { + c.JSON(200, s.bot.CQReloadEventFilter()) +} + +func (s *httpServer) GetVipInfo(c *gin.Context) { + uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64) + c.JSON(200, s.bot.CQGetVipInfo(uid)) +} + func (s *httpServer) HandleQuickOperation(c *gin.Context) { if c.Request.Method != "POST" { c.AbortWithStatus(404) @@ -400,7 +352,6 @@ 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 @@ -440,3 +391,105 @@ func getParamWithType(c *gin.Context, k string) (string, gjson.Type) { } return "", gjson.Null } + +var httpApi = map[string]func(s *httpServer, c *gin.Context){ + "get_login_info": func(s *httpServer, c *gin.Context) { + s.GetLoginInfo(c) + }, + "get_friend_list": func(s *httpServer, c *gin.Context) { + s.GetFriendList(c) + }, + "get_group_list": func(s *httpServer, c *gin.Context) { + s.GetGroupList(c) + }, + "get_group_info": func(s *httpServer, c *gin.Context) { + s.GetGroupInfo(c) + }, + "get_group_member_list": func(s *httpServer, c *gin.Context) { + s.GetGroupMemberList(c) + }, + "get_group_member_info": func(s *httpServer, c *gin.Context) { + s.GetGroupMemberInfo(c) + }, + "send_msg": func(s *httpServer, c *gin.Context) { + s.SendMessage(c) + }, + "send_group_msg": func(s *httpServer, c *gin.Context) { + s.SendGroupMessage(c) + }, + "send_group_forward_msg": func(s *httpServer, c *gin.Context) { + s.SendGroupForwardMessage(c) + }, + "send_private_msg": func(s *httpServer, c *gin.Context) { + s.SendPrivateMessage(c) + }, + "delete_msg": func(s *httpServer, c *gin.Context) { + s.DeleteMessage(c) + }, + "set_friend_add_request": func(s *httpServer, c *gin.Context) { + s.ProcessFriendRequest(c) + }, + "set_group_add_request": func(s *httpServer, c *gin.Context) { + s.ProcessGroupRequest(c) + }, + "set_group_card": func(s *httpServer, c *gin.Context) { + s.SetGroupCard(c) + }, + "set_group_special_title": func(s *httpServer, c *gin.Context) { + s.SetSpecialTitle(c) + }, + "set_group_kick": func(s *httpServer, c *gin.Context) { + s.SetGroupKick(c) + }, + "set_group_ban": func(s *httpServer, c *gin.Context) { + s.SetGroupBan(c) + }, + "set_group_whole_ban": func(s *httpServer, c *gin.Context) { + s.SetWholeBan(c) + }, + "set_group_name": func(s *httpServer, c *gin.Context) { + s.SetGroupName(c) + }, + "set_group_admin": func(s *httpServer, c *gin.Context) { + s.SetGroupAdmin(c) + }, + "_send_group_notice": func(s *httpServer, c *gin.Context) { + s.SendGroupNotice(c) + }, + "set_group_leave": func(s *httpServer, c *gin.Context) { + s.SetGroupLeave(c) + }, + "get_image": func(s *httpServer, c *gin.Context) { + s.GetImage(c) + }, + "get_forward_msg": func(s *httpServer, c *gin.Context) { + s.GetForwardMessage(c) + }, + "get_group_msg": func(s *httpServer, c *gin.Context) { + s.GetGroupMessage(c) + }, + "get_group_honor_info": func(s *httpServer, c *gin.Context) { + s.GetGroupHonorInfo(c) + }, + "can_send_image": func(s *httpServer, c *gin.Context) { + s.CanSendImage(c) + }, + "can_send_record": func(s *httpServer, c *gin.Context) { + s.CanSendRecord(c) + }, + "get_status": func(s *httpServer, c *gin.Context) { + s.GetStatus(c) + }, + "get_version_info": func(s *httpServer, c *gin.Context) { + s.GetVersionInfo(c) + }, + "_get_vip_info": func(s *httpServer, c *gin.Context) { + s.GetVipInfo(c) + }, + "reload_event_filter": func(s *httpServer, c *gin.Context) { + s.ReloadEventFilter(c) + }, + ".handle_quick_operation": func(s *httpServer, c *gin.Context) { + s.HandleQuickOperation(c) + }, +} diff --git a/server/websocket.go b/server/websocket.go index 8dab69a..ecad98a 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -1,6 +1,7 @@ package server import ( + "context" "fmt" "net/http" "strconv" @@ -123,6 +124,14 @@ func (c *websocketClient) connectEvent() { } return } + + handshake := fmt.Sprintf(`{"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, + c.bot.Client.Uin, time.Now().Unix()) + err = conn.WriteMessage(websocket.TextMessage, []byte(handshake)) + if err != nil { + log.Warnf("反向Websocket 握手时出现错误: %v", err) + } + log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventUrl) c.eventConn = &websocketConn{Conn: conn} } @@ -146,6 +155,14 @@ func (c *websocketClient) connectUniversal() { } return } + + handshake := fmt.Sprintf(`{"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, + c.bot.Client.Uin, time.Now().Unix()) + err = conn.WriteMessage(websocket.TextMessage, []byte(handshake)) + if err != nil { + log.Warnf("反向Websocket 握手时出现错误: %v", err) + } + wrappedConn := &websocketConn{Conn: conn} go c.listenApi(wrappedConn, true) c.universalConn = wrappedConn @@ -206,10 +223,12 @@ func (c *websocketClient) onBotPushEvent(m coolq.MSG) { func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) { if s.token != "" { - if r.URL.Query().Get("access_token") != s.token && strings.SplitN(r.Header.Get("Authorization"), " ", 2)[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return + if auth := r.URL.Query().Get("access_token"); auth != s.token { + if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { + log.Warnf("已拒绝 %v 的 Websocket 请求: Token鉴权失败", r.RemoteAddr) + w.WriteHeader(401) + return + } } } c, err := upgrader.Upgrade(w, r, nil) @@ -235,10 +254,12 @@ func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) { func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) { if s.token != "" { - if r.URL.Query().Get("access_token") != s.token && strings.SplitN(r.Header.Get("Authorization"), " ", 2)[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return + if auth := r.URL.Query().Get("access_token"); auth != s.token { + if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { + log.Warnf("已拒绝 %v 的 Websocket 请求: Token鉴权失败", r.RemoteAddr) + w.WriteHeader(401) + return + } } } c, err := upgrader.Upgrade(w, r, nil) @@ -253,10 +274,12 @@ func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) { func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) { if s.token != "" { - if r.URL.Query().Get("access_token") != s.token && strings.SplitN(r.Header.Get("Authorization"), " ", 2)[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return + if auth := r.URL.Query().Get("access_token"); auth != s.token { + if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { + log.Warnf("已拒绝 %v 的 Websocket 请求: Token鉴权失败", r.RemoteAddr) + w.WriteHeader(401) + return + } } } c, err := upgrader.Upgrade(w, r, nil) @@ -270,7 +293,6 @@ func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) { c.Close() return } - log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr) conn := &websocketConn{Conn: c} s.eventConn = append(s.eventConn, conn) @@ -298,7 +320,7 @@ func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) { c.Close() } }() - + global.RateLimit(context.Background()) j := gjson.ParseBytes(payload) t := strings.ReplaceAll(j.Get("action").Str, "_async", "") log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw) @@ -319,6 +341,7 @@ func (s *websocketServer) onBotPushEvent(m coolq.MSG) { 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()) + conn.Lock() if err := conn.WriteMessage(websocket.TextMessage, []byte(m.ToJson())); err != nil { _ = conn.Close() next := i + 1 @@ -330,7 +353,9 @@ func (s *websocketServer) onBotPushEvent(m coolq.MSG) { i-- l-- conn = nil + continue } + conn.Unlock() } } @@ -348,12 +373,11 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ return bot.CQGetGroupInfo(p.Get("group_id").Int()) }, "get_group_member_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { - return bot.CQGetGroupMemberList(p.Get("group_id").Int()) + return bot.CQGetGroupMemberList(p.Get("group_id").Int(), p.Get("no_cache").Bool()) }, "get_group_member_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQGetGroupMemberInfo( p.Get("group_id").Int(), p.Get("user_id").Int(), - p.Get("no_cache").Bool(), ) }, "send_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { @@ -428,7 +452,18 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ }()) }, "set_group_name": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { - return bot.CQSetGroupName(p.Get("group_id").Int(), p.Get("name").Str) + return bot.CQSetGroupName(p.Get("group_id").Int(), p.Get("group_name").Str) + }, + "set_group_admin": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQSetGroupAdmin(p.Get("group_id").Int(), p.Get("user_id").Int(), func() bool { + if p.Get("enable").Exists() { + return p.Get("enable").Bool() + } + return true + }()) + }, + "_send_group_notice": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQSetGroupMemo(p.Get("group_id").Int(), p.Get("content").Str) }, "set_group_leave": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQSetGroupLeave(p.Get("group_id").Int()) @@ -457,6 +492,12 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ "get_version_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQGetVersionInfo() }, + "_get_vip_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQGetVipInfo(p.Get("user_id").Int()) + }, + "reload_event_filter": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQReloadEventFilter() + }, ".handle_quick_operation": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQHandleQuickOperation(p.Get("context"), p.Get("operation")) },