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(``, + 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(``, + 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(``, + 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() },