diff --git a/README.md b/README.md index 4cd982bf..f7aef0be 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,14 @@ | ------------------------------------------------------------ | | [私聊信息](https://cqhttp.cc/docs/4.15/#/Post?id=私聊消息) | | [群消息](https://cqhttp.cc/docs/4.15/#/Post?id=群消息) | -| [群消息撤回(拓展Event)](docs/cqhttp.md#群消息撤回) | -| [好友消息撤回(拓展Event)](docs/cqhttp.md#好友消息撤回) | +| [群消息撤回(拓展Event)](docs/cqhttp.md#群消息撤回) | +| [好友消息撤回(拓展Event)](docs/cqhttp.md#好友消息撤回) | +| [群内提示事件(拓展Event)(龙王等事件)](docs/cqhttp.md#群内戳一戳) | | [群管理员变动](https://cqhttp.cc/docs/4.15/#/Post?id=群管理员变动) | | [群成员减少](https://cqhttp.cc/docs/4.15/#/Post?id=群成员减少) | | [群成员增加](https://cqhttp.cc/docs/4.15/#/Post?id=群成员增加) | | [群禁言](https://cqhttp.cc/docs/4.15/#/Post?id=群禁言) | -| [群文件上传](https://cqhttp.cc/docs/4.15/#/Post?id=群文件上传)| +| [群文件上传](https://cqhttp.cc/docs/4.15/#/Post?id=群文件上传) | | [加好友请求](https://cqhttp.cc/docs/4.15/#/Post?id=加好友请求) | | [加群请求/邀请](https://cqhttp.cc/docs/4.15/#/Post?id=加群请求/邀请) | diff --git a/coolq/api.go b/coolq/api.go index 6be70e9c..50ab3205 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -6,6 +6,7 @@ import ( "path" "runtime" "strconv" + "strings" "time" "github.com/Mrs4s/MiraiGo/binary" @@ -25,7 +26,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 +39,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 +69,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 +82,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) @@ -101,6 +102,17 @@ func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64, noCache bool) MSG return OK(convertGroupMemberInfo(groupId, member)) } +func (bot *CQBot) CQGetWordSlices(content string) MSG { + slices, err := bot.Client.GetWordSegmentation(content) + if err != nil { + return Failed(100) + } + for i := 0; i < len(slices); i++ { + slices[i] = strings.ReplaceAll(slices[i], "\u0000", "") + } + return OK(MSG{"slices": slices}) +} + // 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{}, autoEscape bool) MSG { var str string @@ -125,6 +137,7 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bo if mid == -1 { return Failed(100) } + log.Infof("发送群 %v(%v) 的消息: %v (%v)", groupId, groupId, limitedString(m.String()), mid) return OK(MSG{"message_id": mid}) } str = func() string { @@ -151,6 +164,7 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bo if mid == -1 { return Failed(100) } + log.Infof("发送群 %v(%v) 的消息: %v (%v)", groupId, groupId, limitedString(str), mid) return OK(MSG{"message_id": mid}) } @@ -198,11 +212,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 } @@ -234,6 +261,7 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b if mid == -1 { return Failed(100) } + log.Infof("发送好友 %v(%v) 的消息: %v (%v)", userId, userId, limitedString(m.String()), mid) return OK(MSG{"message_id": mid}) } str = func() string { @@ -258,6 +286,7 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b if mid == -1 { return Failed(100) } + log.Infof("发送好友 %v(%v) 的消息: %v (%v)", userId, userId, limitedString(str), mid) return OK(MSG{"message_id": mid}) } @@ -291,6 +320,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 +420,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} @@ -438,6 +513,27 @@ func (bot *CQBot) CQGetGroupHonorInfo(groupId int64, t string) MSG { return OK(msg) } +// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF +func (bot *CQBot) CQGetStrangerInfo(userId int64) MSG { + info, err := bot.Client.GetSummaryInfo(userId) + if err != nil { + return Failed(100) + } + return OK(MSG{ + "user_id": info.Uin, + "nickname": info.Nickname, + "sex": func() string { + if info.Sex == 1 { + return "female" + } + return "male" + }(), + "age": info.Age, + "level": info.Level, + "login_days": info.LoginDays, + }) +} + // 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 { @@ -525,7 +621,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 +664,38 @@ func (bot *CQBot) CQCanSendRecord() MSG { return OK(MSG{"yes": true}) } +func (bot *CQBot) CQOcrImage(imageId string) MSG { + img, err := bot.makeImageElem("image", map[string]string{"file": imageId}, true) + if err != nil { + log.Warnf("load image error: %v", err) + return Failed(100) + } + rsp, err := bot.Client.ImageOcr(img) + if err != nil { + log.Warnf("ocr image error: %v", err) + return Failed(100) + } + return OK(rsp) +} + +func (bot *CQBot) CQReloadEventFilter() MSG { + global.BootFilter() + return OK(nil) +} + +func (bot *CQBot) CQSetGroupPortrait(groupId int64, file, cache string) MSG { + if g := bot.Client.FindGroup(groupId); g != nil { + img, err := global.FindFile(file, cache, global.IMAGE_PATH) + if err != nil { + log.Warnf("set group portrait error: %v", err) + return Failed(100) + } + g.UpdateGroupHeadPortrait(img) + return OK(nil) + } + return Failed(100) +} + func (bot *CQBot) CQGetStatus() MSG { return OK(MSG{ "app_initialized": true, @@ -591,6 +719,18 @@ func (bot *CQBot) CQGetVersionInfo() MSG { "runtime_version": runtime.Version(), "runtime_os": runtime.GOOS, "version": Version, + "protocol": func() int { + switch client.SystemDeviceInfo.Protocol { + case client.AndroidPad: + return 0 + case client.AndroidPhone: + return 1 + case client.AndroidWatch: + return 2 + default: + return -1 + } + }(), }) } @@ -630,3 +770,12 @@ func convertGroupMemberInfo(groupId int64, m *client.GroupMemberInfo) MSG { "card_changeable": false, } } + +func limitedString(str string) string { + if strings.Count(str, "") <= 10 { + return str + } + limited := []rune(str) + limited = limited[:10] + return string(limited) + " ..." +} diff --git a/coolq/bot.go b/coolq/bot.go index 6d34bda4..b1329987 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -59,22 +59,27 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot { bot.Client.OnTempMessage(bot.tempMessageEvent) bot.Client.OnGroupMuted(bot.groupMutedEvent) bot.Client.OnGroupMessageRecalled(bot.groupRecallEvent) + bot.Client.OnGroupNotify(bot.groupNotifyEvent) bot.Client.OnFriendMessageRecalled(bot.friendRecallEvent) bot.Client.OnJoinGroup(bot.joinGroupEvent) bot.Client.OnLeaveGroup(bot.leaveGroupEvent) bot.Client.OnGroupMemberJoined(bot.memberJoinEvent) bot.Client.OnGroupMemberLeaved(bot.memberLeaveEvent) bot.Client.OnGroupMemberPermissionChanged(bot.memberPermissionChangedEvent) + bot.Client.OnGroupMemberCardUpdated(bot.memberCardUpdatedEvent) bot.Client.OnNewFriendRequest(bot.friendRequestEvent) bot.Client.OnNewFriendAdded(bot.friendAddedEvent) bot.Client.OnGroupInvited(bot.groupInvitedEvent) bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent) go func() { i := conf.HeartbeatInterval - if i < 1 { + if i < 0 { log.Warn("警告: 心跳功能已关闭,若非预期,请检查配置文件。") return } + if i == 0 { + i = 5 + } for { time.Sleep(time.Second * i) bot.dispatchEventMessage(MSG{ @@ -135,6 +140,56 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int newElem = append(newElem, gv) continue } + if i, ok := elem.(*PokeElement); ok { + if group := bot.Client.FindGroup(groupId); group != nil { + if mem := group.FindMember(i.Target); mem != nil { + mem.Poke() + return 0 + } + } + } + if i, ok := elem.(*GiftElement); ok { + bot.Client.SendGroupGift(uint64(groupId), uint64(i.Target), i.GiftId) + return 0 + } + if i, ok := elem.(*QQMusicElement); ok { + ret, err := bot.Client.SendGroupRichMessage(groupId, 100497308, 1, 4, client.RichClientInfo{ + Platform: 1, + SdkVersion: "0.0.0", + PackageName: "com.tencent.qqmusic", + Signature: "cbd27cd7c861227d013a25b2d10f0799", + }, &message.RichMessage{ + Title: i.Title, + Summary: i.Summary, + Url: i.Url, + PictureUrl: i.PictureUrl, + MusicUrl: i.MusicUrl, + }) + if err != nil { + log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err) + return -1 + } + return bot.InsertGroupMessage(ret) + } + if i, ok := elem.(*CloudMusicElement); ok { + ret, err := bot.Client.SendGroupRichMessage(groupId, 100495085, 1, 4, client.RichClientInfo{ + Platform: 1, + SdkVersion: "0.0.0", + PackageName: "com.netease.cloudmusic", + Signature: "da6b069da1e2982db3e386233f68d76d", + }, &message.RichMessage{ + Title: i.Title, + Summary: i.Summary, + Url: i.Url, + PictureUrl: i.PictureUrl, + MusicUrl: i.MusicUrl, + }) + if err != nil { + log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err) + return -1 + } + return bot.InsertGroupMessage(ret) + } newElem = append(newElem, elem) } m.Elements = newElem @@ -223,7 +278,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 40ce3c79..e04eff68 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+?.*?]`) @@ -27,11 +26,71 @@ var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) var IgnoreInvalidCQCode = false +type PokeElement struct { + Target int64 +} + +type GiftElement struct { + Target int64 + GiftId message.GroupGift +} + +type MusicElement struct { + Title string + Summary string + Url string + PictureUrl string + MusicUrl string +} + +type QQMusicElement struct { + MusicElement +} + +type CloudMusicElement struct { + MusicElement +} + +func (e *GiftElement) Type() message.ElementType { + return message.At +} + +func (e *MusicElement) Type() message.ElementType { + return message.Service +} + +var GiftId = []message.GroupGift{ + message.SweetWink, + message.HappyCola, + message.LuckyBracelet, + message.Cappuccino, + message.CatWatch, + message.FleeceGloves, + message.RainbowCandy, + message.Stronger, + message.LoveMicrophone, +} + +func (e *PokeElement) Type() message.ElementType { + return message.At +} + func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []MSG) { 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 = 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 +99,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,10 +120,10 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M "data": map[string]string{"qq": fmt.Sprint(o.Target)}, } } - case *message.ReplyElement: + case *message.RedBagElement: m = MSG{ - "type": "reply", - "data": map[string]string{"id": fmt.Sprint(ToGlobalId(code, o.ReplySeq))}, + "type": "redbag", + "data": map[string]string{"title": o.Title}, } case *message.ForwardElement: m = MSG{ @@ -103,6 +171,20 @@ 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, "= 9 { + return nil, errors.New("invalid gift id") } - rawPath := path.Join(global.IMAGE_PATH, f) - if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { - rawPath += ".cqimg" + return &GiftElement{Target: t, GiftId: GiftId[id]}, nil + case "tts": + if !group { + return nil, errors.New("private voice unsupported now") } - if !global.PathExists(rawPath) && d["url"] != "" { - return bot.ToElement(t, map[string]string{"file": d["url"]}, group) + data, err := bot.Client.GetTts(d["text"]) + ioutil.WriteFile("tts.silk", data, 777) + if err != nil { + return nil, err } - 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") + return &message.VoiceElement{Data: data}, nil case "record": if !group { return nil, errors.New("private voice unsupported now") } f := d["file"] - var data []byte - if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { - b, err := global.GetBytes(f) - if err != nil { - return nil, err - } - data = b - } - if strings.HasPrefix(f, "base64") { - b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) - if err != nil { - return nil, err - } - data = b - } - 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 - } - data = b - } - if global.PathExists(path.Join(global.VOICE_PATH, f)) { - b, err := ioutil.ReadFile(path.Join(global.VOICE_PATH, f)) - if err != nil { - return nil, err - } - data = b + data, err := global.FindFile(f, d["cache"], global.VOICE_PATH) + if err != nil { + return nil, err } if !global.IsAMRorSILK(data) { - return nil, errors.New("unsupported voice file format (please use AMR file for now)") + data, err = global.Encoder(data) + if err != nil { + return nil, err + } } return &message.VoiceElement{Data: data}, nil case "face": @@ -454,17 +435,30 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message. 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 + name := info.Get("track_info.name").Str + " - " + info.Get("track_info.singer.0.name").Str + mid := info.Get("track_info.mid").Str + albumMid := info.Get("track_info.album.mid").Str + pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") + jumpUrl := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" + purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str + preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" 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 + content := "来自go-cqhttp" + if d["content"] != "" { + content = d["content"] + } + if purl == "" { + purl = "https://www.baidu.com" // fix vip song + } + return &QQMusicElement{MusicElement: MusicElement{ + Title: name, + Summary: content, + Url: jumpUrl, + PictureUrl: preview, + MusicUrl: purl, + }}, nil } if d["type"] == "163" { info, err := global.NeteaseMusicSongInfo(d["id"]) @@ -475,17 +469,20 @@ 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 + return &CloudMusicElement{MusicElement{ + Title: name, + Summary: artistName, + Url: jumpUrl, + PictureUrl: picUrl, + MusicUrl: musicUrl, + }}, nil } if d["type"] == "custom" { xml := fmt.Sprintf(``, @@ -502,8 +499,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) } @@ -536,3 +569,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 e91a0ed8..3eb65eea 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -205,6 +205,65 @@ func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMessageRec }) } +func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.IGroupNotifyEvent) { + group := c.FindGroup(e.From()) + switch notify := e.(type) { + case *client.GroupPokeNotifyEvent: + sender := group.FindMember(notify.Sender) + receiver := group.FindMember(notify.Receiver) + log.Infof("群 %v 内 %v 戳了戳 %v", formatGroupName(group), formatMemberName(sender), formatMemberName(receiver)) + bot.dispatchEventMessage(MSG{ + "post_type": "notice", + "group_id": group.Code, + "notice_type": "notify", + "sub_type": "poke", + "self_id": c.Uin, + "user_id": notify.Sender, + "sender_id": notify.Sender, + "target_id": notify.Receiver, + "time": time.Now().Unix(), + }) + case *client.GroupRedBagLuckyKingNotifyEvent: + sender := group.FindMember(notify.Sender) + luckyKing := group.FindMember(notify.LuckyKing) + log.Infof("群 %v 内 %v 的红包被抢完, %v 是运气王", formatGroupName(group), formatMemberName(sender), formatMemberName(luckyKing)) + bot.dispatchEventMessage(MSG{ + "post_type": "notice", + "group_id": group.Code, + "notice_type": "notify", + "sub_type": "lucky_king", + "self_id": c.Uin, + "user_id": notify.Sender, + "sender_id": notify.Sender, + "target_id": notify.LuckyKing, + "time": time.Now().Unix(), + }) + case *client.MemberHonorChangedNotifyEvent: + log.Info(notify.Content()) + bot.dispatchEventMessage(MSG{ + "post_type": "notice", + "group_id": group.Code, + "notice_type": "notify", + "sub_type": "honor", + "self_id": c.Uin, + "user_id": notify.Uin, + "time": time.Now().Unix(), + "honor_type": func() string { + switch notify.Honor { + case client.Talkative: + return "talkative" + case client.Performer: + return "performer" + case client.Emotion: + return "emotion" + default: + return "ERROR" + } + }(), + }) + } +} + func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageRecalledEvent) { f := c.FindFriend(e.FriendUin) gid := ToGlobalId(e.FriendUin, e.MessageId) @@ -251,6 +310,20 @@ func (bot *CQBot) memberPermissionChangedEvent(c *client.QQClient, e *client.Mem }) } +func (bot *CQBot) memberCardUpdatedEvent(c *client.QQClient, e *client.MemberCardUpdatedEvent) { + log.Infof("群 %v 的 %v 更新了名片 %v -> %v", formatGroupName(e.Group), formatMemberName(e.Member), e.OldCard, e.Member.CardName) + bot.dispatchEventMessage(MSG{ + "post_type": "notice", + "notice_type": "group_card", + "group_id": e.Group.Code, + "user_id": e.Member.Uin, + "card_new": e.Member.CardName, + "card_old": e.OldCard, + "time": time.Now().Unix(), + "self_id": c.Uin, + }) +} + func (bot *CQBot) memberJoinEvent(c *client.QQClient, e *client.MemberJoinGroupEvent) { log.Infof("新成员 %v 进入了群 %v.", formatMemberName(e.Member), formatGroupName(e.Group)) bot.dispatchEventMessage(bot.groupIncrease(e.Group.Code, 0, e.Member.Uin)) diff --git a/docs/EventFilter.md b/docs/EventFilter.md new file mode 100644 index 00000000..c9ccf059 --- /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/QA.md b/docs/QA.md new file mode 100644 index 00000000..670722c1 --- /dev/null +++ b/docs/QA.md @@ -0,0 +1,13 @@ +# 常见问题 + +### Q: 为什么登录的时候出现 `客户端版本过低 请升级客户端`? + +### A: 此问题是因为密码输入错误导致的, 信息为服务器返回, 很可能是TX相关的错误, 请检查密码 + +### Q: 为什么登录的时候出现 `为了您的帐号安全,请使用QQ一键登录`? + +### A: 因为目前手机协议不支持图片验证码,切换成平板协议登录成功后,再切换回手机协议即可 + +### Q: 为什么挂一段时间后就会出现 `消息发送失败,账号可能被风控`? + +### A: 如果你刚开始使用 go-cqhttp 建议挂机3-7天,即可解除风控 diff --git a/docs/adminApi.md b/docs/adminApi.md new file mode 100644 index 00000000..68dde266 --- /dev/null +++ b/docs/adminApi.md @@ -0,0 +1,232 @@ +# 管理 API + +> 支持跨域 + +## 公共参数 + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| access_token | string | 校验口令,config.json中配置 | + + + +## admin/do_restart + +### 热重启 + +> 热重启 + +> ps: 目前不支持ws部分的修改生效 + +method:`POST/GET` + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ---- | ------------------------------------- | +| 无||| + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + + +### admin/get_web_write + +> 拉取验证码/设备锁 + +method: `GET` + + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| 无||| + +返回: + +```json +{"data": {"ispic": true,"picbase64":"xxxxx"}, "retcode": 0, "status": "ok"} +``` +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| ispic| bool| 是否是验证码类型 true是,false为不是(比如设备锁| +|picbas64| string| 验证码的base64编码内容,加上头,放入img标签即可显示| + +### admin/do_web_write + +> web输入验证码/设备锁确认 + +method: `POST` formdata + + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| input | string | 输入的类容 | + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + + +### admin/do_restart_docker + +> 冷重启 + +> 注意:此api 会直接结束掉进程,需要依赖docker/supervisor等进程管理工具来自动拉起 + +method: `POST` + + +参数: + +| 参数名 |类型 | 说明 | +| ------ | ------ | -----------| +| 无 | | | + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + +### admin/do_config_base + +> 基础配置 + +method: `POST` formdata + + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| uin | string | qq号 | +| password | string | qq密码 | +| enable_db | string | 是否启动数据库,填 'true' 或者 'false' | +| access_token | string | 授权 token | + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + + +### admin/do_config_http + +> http服务配置 + +method: `POST` formdata + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| port | string | 服务端口 | +| host | string | 服务监听地址 | +| enable | string | 是否启用 ,填 'true' 或者 'false' | +| timeout | string | http请求超时时间 | +| post_url | string | post上报地址 不需要就填空字符串,或者不填| +| post_secret | string | post上报的secret 不需要就填空字符串,或者不填 | + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + + +### admin/do_config_ws + +> 正向ws设置 + +method: `POST` formdata + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| port | string | 服务端口 | +| host | string | 服务监听地址 | +| enable | string | 是否启用 ,填 'true' 或者 'false' | + + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + +### admin/do_config_reverse + +> 反向ws配置 + +method: `POST` formdata + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| port | string | 服务端口 | +| host | string | 服务监听地址 | +| enable | string | 是否启用 ,填 'true' 或者 'false' | + + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + +### admin/do_config_json + +> 直接修改 config.json配置 + +method: `POST` formdata + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| json | string | 完整的config.json的配合,json字符串 | + + +返回: + +```json +{"data": {}, "retcode": 0, "status": "ok"} +``` + +### admin/get_config_json + +> 获取当前 config.json配置 + +method: `GET` + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| 无 | | | + + +返回: + +```json +{"data": {"config":"xxxx"}, "retcode": 0, "status": "ok"} +``` + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ------------------------------------------------------------ | +| config | string | 完整的config.json的配合,json字符串 | + diff --git a/docs/config.md b/docs/config.md index d3b999c3..cedef084 100644 --- a/docs/config.md +++ b/docs/config.md @@ -22,10 +22,15 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为: "password_encrypted": "", "enable_db": true, "access_token": "", - "relogin": { - "enabled": true, - "relogin_delay": 3, - "max_relogin_times": 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, @@ -66,10 +71,13 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为: | 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 | 是否强制分片发送群长消息 | -| heartbeat_interval | int64 | 心跳间隔时间,单位秒,若0则关闭心跳 | +| heartbeat_interval | int64 | 心跳间隔时间,单位秒。小于0则关闭心跳,等于0使用默认值(5秒) | | http_config | object | HTTP API配置 | | ws_config | object | Websocket API 配置 | | ws_reverse_servers | object[] | 反向 Websocket API 配置 | @@ -79,6 +87,31 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为: > 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取. > 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥 -> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析. +> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好,但在有发言频率限制的群里,可能无法发送。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析. > 注3:关闭心跳服务可能引起断线,请谨慎关闭 + +## 设备信息 + +默认生成的设备信息如下所示: + +``` json +{ + "protocol": 0, + "display": "xxx", + "finger_print": "xxx", + "boot_id": "xxx", + "proc_version": "xxx", + "imei": "xxx" +} +``` + +在大部分情况下 我们只需要关心 `protocol` 字段: + +| 值 | 类型 | 限制 | +| ---- | ------------- | ----------------------------------------------------- | +| 0 | Android Pad | 无法接收 `group_notify` 事件、无法接收口令红包 | +| 1 | Android Phone | 无 | +| 2 | Android Watch | 除了 `Android Pad` 有的限制外还包括: 无法接收撤回消息 | + +> 注意, 根据协议的不同, 各类消息有所限制 \ No newline at end of file diff --git a/docs/cqhttp.md b/docs/cqhttp.md index a0f5c42d..46f1008f 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -18,6 +18,68 @@ Type : `reply` 示例: `[CQ:reply,id=123456]` +### 红包 + +Type: `redbag` + +范围: **接收** + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| title | string | 祝福语/口令 | + +示例: `[CQ:redbag,title=恭喜发财]` + +### 戳一戳 + +> 注意:发送戳一戳消息无法撤回,返回的 `message id` 恒定为 `0` + +Type: `poke` + +范围: **发送(仅群聊)** + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| qq | int64 | 需要戳的成员 | + +示例: `[CQ:poke,qq=123456]` + +### 礼物 + +> 注意:仅支持免费礼物,发送群礼物消息无法撤回,返回的 `message id` 恒定为 `0` + +Type: `gift` + +范围: **发送(仅群聊,接收的时候不是CQ码)** + +参数: + +| 参数名 |类型 | 说明 | +| ------ | ------ | -----------| +| qq | int64 | 接收礼物的成员 | +| id | int | 礼物的类型 | + +目前支持的礼物ID: + +| id |类型 | +| ---| ---------| +| 0 | 甜Wink | +| 1 | 快乐肥宅水| +| 2 | 幸运手链 | +| 3 | 卡布奇诺 | +| 4 | 猫咪手表 | +| 5 | 绒绒手套 | +| 6 | 彩虹糖果 | +| 7 | 坚强 | +| 8 | 告白话筒 | + + +示例: `[CQ:gift,qq=123456,id=8]` + ### 合并转发 Type: `forward` @@ -123,7 +185,7 @@ Type: `node` Type: `xml` -范围: **发送** +范围: **发送/接收** 参数: @@ -134,9 +196,12 @@ Type: `xml` 示例: `[CQ:xml,data=xxxx]` -####一些xml样例 -####ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中 +#### 一些xml样例 + +#### ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中 + #### qq音乐 + ```xml ``` @@ -165,6 +230,78 @@ 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] +``` + +### 文本转语音 + +> 注意:通过TX的TTS接口,采用的音源与登录账号的性别有关 + +Type: `tts` + +范围: **发送(仅群聊)** + +参数: + +| 参数名 | 类型 | 说明 | +| ------ | ------ | ----------- | +| text | string | 内容 | + +示例: `[CQ:tts,text=这是一条测试消息]` ## API @@ -177,7 +314,29 @@ Type: `xml` | 字段 | 类型 | 说明 | | -------- | ------ | ---- | | group_id | int64 | 群号 | -| name | string | 新名 | +| group_name | string | 新名 | + +### 设置群头像 + +终结点: `/set_group_portrait` + +**参数** + +| 字段 | 类型 | 说明 | +| -------- | ------ | ---- | +| group_id | int64 | 群号 | +| file | string | 图片文件名 | +| cache | int | 表示是否使用已缓存的文件 | + +[1]`file` 参数支持以下几种格式: + +- 绝对路径,例如 `file:///C:\\Users\Richard\Pictures\1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089) +- 网络 URL,例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg` +- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==` + +[2]`cache`参数: 通过网络 URL 发送时有效,`1`表示使用缓存,`0`关闭关闭缓存,默认 为`1` + +[3] 目前这个API在登录一段时间后因cookie失效而失效,请考虑后使用 ### 获取图片信息 @@ -275,7 +434,21 @@ Type: `xml` | `group_id` | int64 | 群号 | | `messages` | forward node[] | 自定义转发消息, 具体看CQCode | -### +### 获取中文分词 + +终结点: `/.get_word_slices` + +**参数** + +| 字段 | 类型 | 说明 | +| ------------ | ------ | ------ | +| `content` | string | 内容 | + +**响应数据** + +| 字段 | 类型 | 说明 | +| ---------- | ----------------- | -------- | +| `slices` | string[] | 词组 | ## 事件 @@ -303,3 +476,64 @@ Type: `xml` | `user_id` | int64 | | 好友id | | `message_id` | int64 | | 被撤回的消息id | +#### 群内戳一戳 + +> 注意:此事件无法在平板和手表协议上触发 + +**上报数据** + +| 字段 | 类型 | 可能的值 | 说明 | +| ------------- | ------ | -------------- | -------------- | +| `post_type` | string | `notice` | 上报类型 | +| `notice_type` | string | `notify` | 消息类型 | +| `group_id` | int64 | | 群号 | +| `sub_type` | string | `poke` | 提示类型 | +| `user_id` | int64 | | 发送者id | +| `target_id` | int64 | | 被戳者id | + +#### 群红包运气王提示 + +> 注意:此事件无法在平板和手表协议上触发 + +**上报数据** + +| 字段 | 类型 | 可能的值 | 说明 | +| ------------- | ------ | -------------- | -------------- | +| `post_type` | string | `notice` | 上报类型 | +| `notice_type` | string | `notify` | 消息类型 | +| `group_id` | int64 | | 群号 | +| `sub_type` | string | `lucky_king` | 提示类型 | +| `user_id` | int64 | | 红包发送者id | +| `target_id` | int64 | | 运气王id | + +#### 群成员荣誉变更提示 + +> 注意:此事件无法在平板和手表协议上触发 + +**上报数据** + +| 字段 | 类型 | 可能的值 | 说明 | +| ------------- | ------ | -------------- | -------------- | +| `post_type` | string | `notice` | 上报类型 | +| `notice_type` | string | `notify` | 消息类型 | +| `group_id` | int64 | | 群号 | +| `sub_type` | string | `honor` | 提示类型 | +| `user_id` | int64 | | 成员id | +| `honor_type` | string | `talkative:龙王` `performer:群聊之火` `emotion:快乐源泉` | 荣誉类型 | + +#### 群成员名片更新 + +> 注意: 此事件不保证时效性,仅在收到消息时校验卡片 + +**上报数据** + +| 字段 | 类型 | 可能的值 | 说明 | +| ------------- | ------ | -------------- | -------------- | +| `post_type` | string | `notice` | 上报类型 | +| `notice_type` | string | `group_card` | 消息类型 | +| `group_id` | int64 | | 群号 | +| `user_id` | int64 | | 成员id | +| `card_new` | int64 | | 新名片 | +| `card_old` | int64 | | 旧名片 | + +> PS: 当名片为空时 `card_xx` 字段为空字符串, 并不是昵称 \ No newline at end of file diff --git a/docs/quick_start.md b/docs/quick_start.md index ebd81f6c..d95c2b27 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -1,3 +1,123 @@ # 开始 -欢迎来到 go-cqhttp 文档 \ No newline at end of file +欢迎来到 go-cqhttp 文档 目前还在咕 + +# 基础教程 +## 下载 +从[release](https://github.com/Mrs4s/go-cqhttp/releases)界面下载最新版本的go-cqhttp + +- Windows下32位文件为 `go-cqhttp-v*-windows-386.zip` +- Windows下64位文件为 `go-cqhttp-v*-windows-amd64.zip` +- Windows下arm用(如使用高通CPU的笔记本)文件为 `go-cqhttp-v*-windows-arm.zip` +- Linux下32位文件为 `go-cqhttp-v*-linux-386.tar.gz` +- Linux下64位文件为 `go-cqhttp-v*-linux-amd64.tar.gz` +- Linux下arm用(如树莓派)文件为 `go-cqhttp-v*-linux-arm.tar.gz` +- MD5文件为 `*.md5` ,用于校验文件完整性 +- 如果没有你所使用的系统版本或者希望自己构建,请移步[进阶指南-如何自己构建](#如何自己构建) + +## 解压 + +- Windows下请使用自己熟悉的解压软件自行解压 +- Linux下在命令行中输入 `tar -xzvf [文件名]` + +## 使用 + +### Windows + +#### 标准方法 + +1. 双击`go-cqhttp.exe`此时将提示 +``` +[WARNING]: 尝试加载配置文件 config.json 失败: 文件不存在 +[INFO]: 默认配置文件已生成,请编辑 config.json 后重启程序. +``` +2. 参照[config.md](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数 +3. 再次双击`go-cqhttp.exe` +``` +[INFO]: 登录成功 欢迎使用: balabala +``` + +如出现需要认证的信息,请自行认证设备。 + +此时,基础配置完成 + +#### 懒人法 + +1. [下载包含Windows.bat的zip](https://github.com/fkx4-p/go-cqhttp-lazy/archive/master.zip) +2. 解压 +3. 将`Windows.bat`复制/剪切到**go-cqhttp**文件夹 +4. 双击运行 + +效果如下 + +``` +QQ account: +[QQ账号] +QQ password: +[QQ密码] +enable http?(Y/n) +[是否开启http(y/n),默认开启] +enable ws?(Y/n) +[是否开启websocket(y/n),默认开启] +请按任意键继续. . . +``` + +5. 双击`go-cqhttp.exe` +``` +[INFO]: 登录成功 欢迎使用: balabala +``` + +如出现需要认证的信息,请自行认证设备。 + +此时,基础配置完成 + +### Linux + +#### 标准方法 + +1. 打开一个命令行/ssh +2. `cd`到解压目录 +3. 输入 `./go-cqhttp`,`Enter`运行 ,此时将提示 +``` +[WARNING]: 尝试加载配置文件 config.json 失败: 文件不存在 +[INFO]: 默认配置文件已生成,请编辑 config.json 后重启程序. +``` + +4. 参照[config.md](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)和你所用到的插件的 `README` 填入参数 +5. 再次输入 `./go-cqhttp`,`Enter`运行 +``` +[INFO]: 登录成功 欢迎使用: balabala +``` + +如出现需要认证的信息,请自行认证设备。 + +此时,基础配置完成 + +#### 懒人法 + +暂时咕咕咕了 + +## 验证http是否成功配置 + +此时,如果在本地开启的服务器,可以在浏览器输入`http://127.0.0.1:5700/send_private_msg?user_id=[接收者qq号]&message=[发送的信息]`来发送一条测试信息 + +如果出现`{"data":{"message_id":balabala},"retcode":0,"status":"ok"}`则证明已经成功配置HTTP + +# 进阶指南 + +## 如何自己构建 + +1. [下载源码](https://github.com/Mrs4s/go-cqhttp/archive/master.zip)并解压 || 使用`git clone https://github.com/Mrs4s/go-cqhttp.git`来拉取 + +2. [下载golang binary release](https://golang.google.cn/dl/)并安装或者[自己构建golang](https://golang.google.cn/doc/install/source) + +3. 在`cmd`或Linux命令行中,`cd`到目录中 + +4. 输入`go build -ldflags "-s -w -extldflags '-static'"`,`Enter`运行 + +*注:可以使用*`go env -w GOPROXY=https://goproxy.cn,direct`*来加速国内依赖安装速度* + +*注:此时构建后的文件名为*`main`(Linux)或`main.exe`(Windows) + + + diff --git a/global/codec.go b/global/codec.go new file mode 100644 index 00000000..b519b684 --- /dev/null +++ b/global/codec.go @@ -0,0 +1,44 @@ +package global + +import ( + "crypto/md5" + "errors" + "fmt" + log "github.com/sirupsen/logrus" + "github.com/wdvxdr1123/go-silk/silk" + "io/ioutil" + "path" + "sync" +) + +var codec silk.Encoder +var useCodec = true +var once sync.Once + +func InitCodec() { + once.Do(func() { + log.Info("正在加载silk编码器...") + err := codec.Init("data/cache", "codec") + if err != nil { + log.Error(err) + useCodec = false + } + }) +} + +func Encoder(data []byte) ([]byte, error) { + if useCodec == false { + return nil, errors.New("no silk encoder") + } + h := md5.New() + h.Write(data) + tempName := fmt.Sprintf("%x", h.Sum(nil)) + if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) { + return ioutil.ReadFile(silkPath) + } + slk, err := codec.EncodeToSilk(data, tempName, true) + if err != nil { + return nil, err + } + return slk, nil +} diff --git a/global/config.go b/global/config.go index 07ba2095..e380e996 100644 --- a/global/config.go +++ b/global/config.go @@ -21,6 +21,11 @@ type JsonConfig struct { 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"` @@ -30,6 +35,7 @@ type JsonConfig struct { PostMessageFormat string `json:"post_message_format"` Debug bool `json:"debug"` LogLevel string `json:"log_level"` + WebUi *GoCqWebUi `json:"web_ui"` } type CQHttpApiConfig struct { @@ -73,6 +79,13 @@ type GoCQReverseWebsocketConfig struct { ReverseReconnectInterval uint16 `json:"reverse_reconnect_interval"` } +type GoCqWebUi struct { + Enabled bool `json:"enabled"` + Host string `json:"host"` + WebUiPort uint64 `json:"web_ui_port"` + WebInput bool `json:"web_input"` +} + func DefaultConfig() *JsonConfig { return &JsonConfig{ EnableDB: true, @@ -85,8 +98,17 @@ func DefaultConfig() *JsonConfig { 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, + ForceFragmented: false, HttpConfig: &GoCQHttpConfig{ Enabled: true, Host: "0.0.0.0", @@ -107,6 +129,12 @@ func DefaultConfig() *JsonConfig { ReverseReconnectInterval: 3000, }, }, + WebUi: &GoCqWebUi{ + Enabled: true, + Host: "0.0.0.0", + WebInput: false, + WebUiPort: 9999, + }, } } diff --git a/global/filter.go b/global/filter.go index e7fdad69..b2ac4cb2 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/fs.go b/global/fs.go index f3dc6b0b..1b19b364 100644 --- a/global/fs.go +++ b/global/fs.go @@ -2,11 +2,17 @@ package global import ( "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + log "github.com/sirupsen/logrus" "io/ioutil" + "net/url" "os" "path" - - log "github.com/sirupsen/logrus" + "runtime" + "strings" ) var ( @@ -45,3 +51,59 @@ func Check(err error) { func IsAMRorSILK(b []byte) bool { return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK) } + +func FindFile(f, cache, PATH string) (data []byte, err error) { + data, err = nil, errors.New("syntax error") + if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { + if cache == "" { + cache = "1" + } + hash := md5.Sum([]byte(f)) + cacheFile := path.Join(CACHE_PATH, hex.EncodeToString(hash[:])+".cache") + if PathExists(cacheFile) && cache == "1" { + return ioutil.ReadFile(cacheFile) + } + data, err = GetBytes(f) + _ = ioutil.WriteFile(cacheFile, data, 0644) + if err != nil { + return nil, err + } + } else if strings.HasPrefix(f, "base64") { + data, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) + if err != nil { + return nil, err + } + } else if strings.HasPrefix(f, "file") { + var fu *url.URL + fu, err = url.Parse(f) + if err != nil { + return nil, err + } + if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { + fu.Path = fu.Path[1:] + } + data, err = ioutil.ReadFile(fu.Path) + if err != nil { + return nil, err + } + } else if PathExists(path.Join(PATH, f)) { + data, err = ioutil.ReadFile(path.Join(PATH, f)) + if err != nil { + return nil, err + } + } + return +} + +func DelFile(path string) bool { + err := os.Remove(path) + if err != nil { + // 删除失败 + log.Error(err) + return false + } else { + // 删除成功 + log.Info(path + "删除成功") + return true + } +} diff --git a/global/net.go b/global/net.go index e394efdd..7691e89d 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) { @@ -53,19 +50,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 00000000..79594be6 --- /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 0c1dda96..8c00b3bf 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,32 @@ module github.com/Mrs4s/go-cqhttp go 1.14 require ( - github.com/Mrs4s/MiraiGo v0.0.0-20200827182935-51e155ef20da + github.com/Mrs4s/MiraiGo v0.0.0-20201008134448-b53aaceaa1b4 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.4.0 // indirect github.com/gorilla/websocket v1.4.2 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/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/sirupsen/logrus v1.7.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.1 + github.com/ugorji/go v1.1.10 // indirect + github.com/wdvxdr1123/go-silk v0.0.0-20201007123416-b982fd3d91d6 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-20200822124328-c89045814202 // indirect - golang.org/x/sys v0.0.0-20200828194041-157a740278f4 // indirect + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect + golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect + golang.org/x/sys v0.0.0-20201005172224-997123666555 // 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 95a0168e..f2ce6e05 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +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-20200827182935-51e155ef20da h1:T2Qz4w6sMrBxw+oiwbUa/c996jWYulCAtM+x1L0l3R8= -github.com/Mrs4s/MiraiGo v0.0.0-20200827182935-51e155ef20da/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM= +github.com/Mrs4s/MiraiGo v0.0.0-20201008134448-b53aaceaa1b4 h1:vNDY7JAh+e7ac0Dft3GF+s4WZU55SZkwaAI7UmXfwHc= +github.com/Mrs4s/MiraiGo v0.0.0-20201008134448-b53aaceaa1b4/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 +23,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.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kVYAvjEvpT85l70= +github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 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,32 +48,29 @@ 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= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/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= @@ -83,8 +81,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -93,20 +91,22 @@ 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= +github.com/ugorji/go v1.1.10 h1:Mh7W3N/hGJJ8fRQNHIgomNTa0CgZc0aKDFvbgHl+U7A= +github.com/ugorji/go v1.1.10/go.mod h1:/tC+H0R6N4Lcv4DoSdphIa9y/RAs4QFHDtN9W2oQcHw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.1.10 h1:otofY/FAoRTMVqlVeDv/Kpm04D13lfJdrDqPbc3axg4= +github.com/ugorji/go/codec v1.1.10/go.mod h1:jdPQoxvTq1mb8XV6RmofOz5UgNKV2czR6xvxXGwy1Bo= +github.com/wdvxdr1123/go-silk v0.0.0-20201006020916-0398076200ea h1:OqkIV1VL5xm88jhXLaPHRJroeRknxN3EApcAVlNIIOw= +github.com/wdvxdr1123/go-silk v0.0.0-20201006020916-0398076200ea/go.mod h1:5q9LFlBr+yX/J8Jd/9wHdXwkkjFkNyQIS7kX2Lgx/Zs= +github.com/wdvxdr1123/go-silk v0.0.0-20201007123416-b982fd3d91d6 h1:lX18MCdNzT2zIi7K02x4C5cPkDXpL+wCb1YTAMXjLWQ= +github.com/wdvxdr1123/go-silk v0.0.0-20201007123416-b982fd3d91d6/go.mod h1:5q9LFlBr+yX/J8Jd/9wHdXwkkjFkNyQIS7kX2Lgx/Zs= github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM= github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc= github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg= @@ -118,6 +118,8 @@ github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if5 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/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/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= @@ -127,10 +129,9 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw= +golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= 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= @@ -141,14 +142,15 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h 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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw= +golang.org/x/sys v0.0.0-20201005172224-997123666555/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= @@ -177,7 +179,6 @@ 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= diff --git a/main.go b/main.go index f5eae475..46ce32dd 100644 --- a/main.go +++ b/main.go @@ -2,32 +2,27 @@ package main import ( "bufio" - "bytes" "crypto/md5" "encoding/base64" "encoding/json" "fmt" - "image" + "github.com/Mrs4s/go-cqhttp/server" "io" "io/ioutil" "os" "os/signal" "path" "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/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" + "github.com/t-tomalak/logrus-easy-formatter" ) func init() { @@ -213,6 +208,17 @@ func main() { log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.") time.Sleep(time.Second * 5) log.Info("开始尝试登录并同步消息...") + log.Infof("使用协议: %v", func() string { + switch client.SystemDeviceInfo.Protocol { + case client.AndroidPad: + return "Android Pad" + case client.AndroidPhone: + return "Android Phone" + case client.AndroidWatch: + return "Android Watch" + } + return "未知" + }()) cli := client.NewClient(conf.Uin, conf.Password) cli.OnLog(func(c *client.QQClient, e *client.LogEvent) { switch e.Type { @@ -224,100 +230,29 @@ func main() { 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("请输入验证码 (captcha.jpg): (Enter 提交)") - text, _ := console.ReadString('\n') - rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign) - continue - case client.UnsafeDeviceError: - log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) - log.Infof(" 按 Enter 继续....") - _, _ = console.ReadString('\n') - return - case client.OtherLoginError, client.UnknownLoginError: - log.Fatalf("登录失败: %v", rsp.ErrorMessage) - } - } - break - } - log.Infof("登录成功 欢迎使用: %v", cli.Nickname) - time.Sleep(time.Second) - log.Info("开始加载好友列表...") - global.Check(cli.ReloadFriendList()) - log.Infof("共加载 %v 个好友.", len(cli.FriendList)) - log.Infof("开始加载群列表...") - 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, conf.HttpConfig.Timeout, b) - } - } - if conf.WSConfig != nil && conf.WSConfig.Enabled { - server.WebsocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, b) - } - for _, rc := range conf.ReverseServers { - server.NewWebsocketClient(rc, conf.AccessToken, b).Run() - } - log.Info("资源初始化完成, 开始处理信息.") - log.Info("アトリは、高性能ですから!") - cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { - 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 - - } - log.Fatal("重连失败: 重连次数达到设置的上限值") - } - b.Release() - log.Fatalf("Bot已离线:%v", e.Message) + cli.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) { + log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ") }) - c := make(chan os.Signal, 1) + if conf.WebUi == nil { + conf.WebUi = &global.GoCqWebUi{ + Enabled: true, + WebInput: false, + Host: "0.0.0.0", + WebUiPort: 9999, + } + } + if conf.WebUi.WebUiPort <= 0 { + conf.WebUi.WebUiPort = 9999 + } + if conf.WebUi.Host == "" { + conf.WebUi.Host = "0.0.0.0" + } + confErr := conf.Save("config.json") + if confErr != nil { + log.Error("保存配置文件失败") + } + b := server.WebServer.Run(fmt.Sprintf("%s:%d", conf.WebUi.Host, conf.WebUi.WebUiPort), cli) + c := server.Console signal.Notify(c, os.Interrupt, os.Kill) <-c b.Release() diff --git a/server/apiAdmin.go b/server/apiAdmin.go new file mode 100644 index 00000000..0c896fed --- /dev/null +++ b/server/apiAdmin.go @@ -0,0 +1,549 @@ +package server + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/Mrs4s/MiraiGo/client" + "github.com/Mrs4s/go-cqhttp/coolq" + "github.com/Mrs4s/go-cqhttp/global" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "github.com/yinghau76/go-ascii-art" + "image" + "io/ioutil" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "time" +) + +var WebInput = make(chan string, 1) //长度1,用于阻塞 + +var Console = make(chan os.Signal, 1) + +var JsonConfig *global.JsonConfig + +type webServer struct { + engine *gin.Engine + bot *coolq.CQBot + Cli *client.QQClient + Conf *global.JsonConfig //old config + Console *bufio.Reader +} + +var WebServer = &webServer{} + +// admin 子站的 路由映射 +var HttpuriAdmin = map[string]func(s *webServer, c *gin.Context){ + "do_restart": AdminDoRestart, //热重启 + "get_web_write": AdminWebWrite, //获取是否验证码输入 + "do_web_write": AdminDoWebWrite, //web上进行输入操作 + "do_restart_docker": AdminDoRestartDocker, //直接停止(依赖supervisord/docker)重新拉起 + "do_config_base": AdminDoConfigBase, //修改config.json中的基础部分 + "do_config_http": AdminDoConfigHttp, //修改config.json的http部分 + "do_config_ws": AdminDoConfigWs, //修改config.json的正向ws部分 + "do_config_reverse": AdminDoConfigReverse, //修改config.json 中的反向ws部分 + "do_config_json": AdminDoConfigJson, //直接修改 config.json配置 + "get_config_json": AdminGetConfigJson, //拉取 当前的config.json配置 +} + +func Failed(code int, msg string) coolq.MSG { + return coolq.MSG{"data": nil, "retcode": code, "status": "failed", "msg": msg} +} + +func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot { + s.Cli = cli + s.Conf = GetConf() + JsonConfig = s.Conf + gin.SetMode(gin.ReleaseMode) + s.engine = gin.New() + + s.engine.Use(AuthMiddleWare()) + + //通用路由 + s.engine.Any("/admin/:action", s.admin) + + go func() { + //开启端口监听 + if s.Conf.WebUi.Enabled { + log.Infof("Admin API 服务器已启动: %v", addr) + err := s.engine.Run(addr) + if err != nil { + log.Error(err) + log.Infof("请检查端口是否被占用.") + time.Sleep(time.Second * 5) + os.Exit(1) + } + } else { + //关闭端口监听 + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, os.Kill) + <-c + os.Exit(1) + } + }() + s.Dologin() + s.UpServer() + b := s.bot //外部引入 bot对象,用于操作bot + return b +} + +func (s *webServer) Dologin() { + s.Console = bufio.NewReader(os.Stdin) + conf := GetConf() + cli := s.Cli + cli.AllowSlider = true + rsp, err := cli.Login() + for { + global.Check(err) + var text string + if !rsp.Success { + switch rsp.Error { + case client.SliderNeededError: + if client.SystemDeviceInfo.Protocol == client.AndroidPhone { + log.Warnf("警告: Android Phone 强制要求暂不支持的滑条验证码, 请开启设备锁或切换到Watch协议验证通过后再使用.") + log.Infof("按 Enter 继续....") + _, _ = s.Console.ReadString('\n') + os.Exit(0) + } + cli.AllowSlider = false + cli.Disconnect() + rsp, err = cli.Login() + continue + case client.NeedCaptcha: + _ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644) + img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage)) + fmt.Println(asciiart.New("image", img).Art) + if conf.WebUi.WebInput { + log.Warnf("请输入验证码 (captcha.jpg): (http://%s:%d/admin/do_web_write 输入)", conf.WebUi.Host, conf.WebUi.WebUiPort) + text = <-WebInput + } else { + log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)") + text, _ = s.Console.ReadString('\n') + } + rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign) + global.DelFile("captcha.jpg") + continue + case client.SMSNeededError: + log.Warnf("账号已开启设备锁, 按下 Enter 向手机 %v 发送短信验证码.", rsp.SMSPhone) + _, _ = s.Console.ReadString('\n') + if !cli.RequestSMS() { + log.Warnf("发送验证码失败,可能是请求过于频繁.") + time.Sleep(time.Second * 5) + os.Exit(0) + } + log.Warn("请输入短信验证码: (Enter 提交)") + text, _ = s.Console.ReadString('\n') + rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) + continue + case client.SMSOrVerifyNeededError: + log.Warnf("账号已开启设备锁,请选择验证方式:") + log.Warnf("1. 向手机 %v 发送短信验证码", rsp.SMSPhone) + log.Warnf("2. 使用手机QQ扫码验证.") + log.Warn("请输入(1 - 2): ") + text, _ = s.Console.ReadString('\n') + if strings.Contains(text, "1") { + if !cli.RequestSMS() { + log.Warnf("发送验证码失败,可能是请求过于频繁.") + time.Sleep(time.Second * 5) + os.Exit(0) + } + log.Warn("请输入短信验证码: (Enter 提交)") + text, _ = s.Console.ReadString('\n') + rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) + continue + } + log.Warnf("请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) + log.Infof("按 Enter 继续....") + _, _ = s.Console.ReadString('\n') + os.Exit(0) + return + case client.UnsafeDeviceError: + log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) + if conf.WebUi.WebInput { + log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUi.Host, conf.WebUi.WebUiPort) + text = <-WebInput + } else { + log.Infof("按 Enter 继续....") + _, _ = s.Console.ReadString('\n') + } + log.Info(text) + os.Exit(0) + return + case client.OtherLoginError, client.UnknownLoginError: + log.Warnf("登录失败: %v", rsp.ErrorMessage) + log.Infof("按 Enter 继续....") + _, _ = s.Console.ReadString('\n') + os.Exit(0) + return + } + } + break + } + log.Infof("登录成功 欢迎使用: %v", cli.Nickname) + time.Sleep(time.Second) + log.Info("开始加载好友列表...") + global.Check(cli.ReloadFriendList()) + log.Infof("共加载 %v 个好友.", len(cli.FriendList)) + log.Infof("开始加载群列表...") + global.Check(cli.ReloadGroupList()) + log.Infof("共加载 %v 个群.", len(cli.GroupList)) + s.bot = 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) + } + if conf.RateLimit.Enabled { + global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize) + } + log.Info("正在加载事件过滤器.") + global.BootFilter() + global.InitCodec() + coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode + coolq.ForceFragmented = conf.ForceFragmented + log.Info("资源初始化完成, 开始处理信息.") + log.Info("アトリは、高性能ですから!") + cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { + if conf.ReLogin.Enabled { + var times uint = 1 + for { + if cli.Online { + log.Warn("Bot已登录") + return + } + 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 + } + log.Fatal("重连失败: 重连次数达到设置的上限值") + } + s.bot.Release() + log.Fatalf("Bot已离线:%v", e.Message) + }) +} + +func (s *webServer) admin(c *gin.Context) { + action := c.Param("action") + log.Debugf("WebServer接收到cgi调用: %v", action) + if f, ok := HttpuriAdmin[action]; ok { + f(s, c) + } else { + c.JSON(200, coolq.Failed(404)) + } +} + +// 获取当前配置文件信息 +func GetConf() *global.JsonConfig { + if JsonConfig != nil { + return JsonConfig + } + conf := global.Load("config.json") + return conf +} + +// admin 控制器 登录验证 +func AuthMiddleWare() gin.HandlerFunc { + return func(c *gin.Context) { + conf := GetConf() + //处理跨域问题 + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE") + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") + c.Header("Access-Control-Allow-Credentials", "true") + // 放行所有OPTIONS方法,因为有的模板是要请求两次的 + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // 处理请求 + if c.Request.Method != "GET" && c.Request.Method != "POST" { + log.Warnf("已拒绝客户端 %v 的请求: 方法错误", c.Request.RemoteAddr) + c.Status(404) + c.Abort() + } + 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) + c.Status(400) + c.Abort() + } + if !gjson.ValidBytes(d) { + log.Warnf("已拒绝客户端 %v 的请求: 非法Json", c.Request.RemoteAddr) + c.Status(400) + c.Abort() + } + c.Set("json_body", gjson.ParseBytes(d)) + } + authToken := conf.AccessToken + if auth := c.Request.Header.Get("Authorization"); auth != "" { + if strings.SplitN(auth, " ", 2)[1] != authToken { + c.AbortWithStatus(401) + return + } + } else if c.Query("access_token") != authToken { + c.AbortWithStatus(401) + return + } else { + c.Next() + } + } +} + +func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin + JsonConfig = nil + conf := GetConf() + OldConf := s.Conf + cli := client.NewClient(conf.Uin, conf.Password) + log.Info("开始尝试登录并同步消息...") + log.Infof("使用协议: %v", func() string { + switch client.SystemDeviceInfo.Protocol { + case client.AndroidPad: + return "Android Pad" + case client.AndroidPhone: + return "Android Phone" + case client.AndroidWatch: + return "Android Watch" + } + return "未知" + }()) + 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) + } + }) + cli.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) { + log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ") + }) + s.Cli = cli + s.Dologin() + //关闭之前的 server + if OldConf.HttpConfig != nil && OldConf.HttpConfig.Enabled { + HttpServer.ShutDown() + } + //if OldConf.WSConfig != nil && OldConf.WSConfig.Enabled { + // server.WsShutdown() + //} + //s.UpServer() + s.ReloadServer() + s.Conf = conf +} + +func (s *webServer) UpServer() { + conf := GetConf() + if conf.HttpConfig != nil && conf.HttpConfig.Enabled { + go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot) + for k, v := range conf.HttpConfig.PostUrls { + NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot) + } + } + if conf.WSConfig != nil && conf.WSConfig.Enabled { + go WebsocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, s.bot) + } + for _, rc := range conf.ReverseServers { + go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run() + } +} + +// 暂不支持ws服务的重启 +func (s *webServer) ReloadServer() { + conf := GetConf() + if conf.HttpConfig != nil && conf.HttpConfig.Enabled { + go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot) + for k, v := range conf.HttpConfig.PostUrls { + NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot) + } + } + for _, rc := range conf.ReverseServers { + go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run() + } +} + +// 热重启 +func AdminDoRestart(s *webServer, c *gin.Context) { + s.bot = nil + s.Cli = nil + s.DoReLogin() + c.JSON(200, coolq.OK(coolq.MSG{})) + return +} + +// 冷重启 +func AdminDoRestartDocker(s *webServer, c *gin.Context) { + Console <- os.Kill + c.JSON(200, coolq.OK(coolq.MSG{})) + return +} + +// web输入 html 页面 +func AdminWebWrite(s *webServer, c *gin.Context) { + pic := global.ReadAllText("captcha.jpg") + var picbase64 string + var ispic = false + if pic != "" { + input := []byte(pic) + // base64编码 + picbase64 = base64.StdEncoding.EncodeToString(input) + ispic = true + } + c.JSON(200, coolq.OK(coolq.MSG{ + "ispic": ispic, //为空则为 设备锁 或者没有需要输入 + "picbase64": picbase64, //web上显示图片 + })) +} + +// web输入 处理 +func AdminDoWebWrite(s *webServer, c *gin.Context) { + input := c.PostForm("input") + WebInput <- input + c.JSON(200, coolq.OK(coolq.MSG{})) +} + +// 普通配置修改 +func AdminDoConfigBase(s *webServer, c *gin.Context) { + conf := GetConf() + conf.Uin, _ = strconv.ParseInt(c.PostForm("uin"), 10, 64) + conf.Password = c.PostForm("password") + if c.PostForm("enable_db") == "true" { + conf.EnableDB = true + } else { + conf.EnableDB = false + } + conf.AccessToken = c.PostForm("access_token") + if err := conf.Save("config.json"); err != nil { + log.Fatalf("保存 config.json 时出现错误: %v", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + } else { + JsonConfig = nil + c.JSON(200, coolq.OK(coolq.MSG{})) + } +} + +// http配置修改 +func AdminDoConfigHttp(s *webServer, c *gin.Context) { + conf := GetConf() + p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16) + conf.HttpConfig.Port = uint16(p) + conf.HttpConfig.Host = c.PostForm("host") + if c.PostForm("enable") == "true" { + conf.HttpConfig.Enabled = true + } else { + conf.HttpConfig.Enabled = false + } + t, _ := strconv.ParseInt(c.PostForm("timeout"), 10, 32) + conf.HttpConfig.Timeout = int32(t) + if c.PostForm("post_url") != "" { + conf.HttpConfig.PostUrls[c.PostForm("post_url")] = c.PostForm("post_secret") + } + if err := conf.Save("config.json"); err != nil { + log.Fatalf("保存 config.json 时出现错误: %v", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + } else { + JsonConfig = nil + c.JSON(200, coolq.OK(coolq.MSG{})) + } +} + +// ws配置修改 +func AdminDoConfigWs(s *webServer, c *gin.Context) { + conf := GetConf() + p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16) + conf.WSConfig.Port = uint16(p) + conf.WSConfig.Host = c.PostForm("host") + if c.PostForm("enable") == "true" { + conf.WSConfig.Enabled = true + } else { + conf.WSConfig.Enabled = false + } + if err := conf.Save("config.json"); err != nil { + log.Fatalf("保存 config.json 时出现错误: %v", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + } else { + JsonConfig = nil + c.JSON(200, coolq.OK(coolq.MSG{})) + } +} + +// 反向ws配置修改 +func AdminDoConfigReverse(s *webServer, c *gin.Context) { + conf := GetConf() + conf.ReverseServers[0].ReverseApiUrl = c.PostForm("reverse_api_url") + conf.ReverseServers[0].ReverseUrl = c.PostForm("reverse_url") + conf.ReverseServers[0].ReverseEventUrl = c.PostForm("reverse_event_url") + t, _ := strconv.ParseUint(c.PostForm("reverse_reconnect_interval"), 10, 16) + conf.ReverseServers[0].ReverseReconnectInterval = uint16(t) + if c.PostForm("enable") == "true" { + conf.ReverseServers[0].Enabled = true + } else { + conf.ReverseServers[0].Enabled = false + } + if err := conf.Save("config.json"); err != nil { + log.Fatalf("保存 config.json 时出现错误: %v", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + } else { + JsonConfig = nil + c.JSON(200, coolq.OK(coolq.MSG{})) + } +} + +// config.json配置修改 +func AdminDoConfigJson(s *webServer, c *gin.Context) { + conf := GetConf() + Json := c.PostForm("json") + err := json.Unmarshal([]byte(Json), &conf) + if err != nil { + log.Warnf("尝试加载配置文件 %v 时出现错误: %v", "config.json", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + return + } + if err := conf.Save("config.json"); err != nil { + log.Fatalf("保存 config.json 时出现错误: %v", err) + c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err))) + } else { + JsonConfig = nil + c.JSON(200, coolq.OK(coolq.MSG{})) + } +} + +// 拉取config.json配置 +func AdminGetConfigJson(s *webServer, c *gin.Context) { + conf := GetConf() + c.JSON(200, coolq.OK(coolq.MSG{"config": conf})) + +} diff --git a/server/http.go b/server/http.go index ad864f54..c02e1731 100644 --- a/server/http.go +++ b/server/http.go @@ -1,9 +1,11 @@ package server import ( + "context" "crypto/hmac" "crypto/sha1" "encoding/hex" + "net/http" "os" "strconv" "strings" @@ -20,6 +22,7 @@ import ( type httpServer struct { engine *gin.Engine bot *coolq.CQBot + Http *http.Server } type httpClient struct { @@ -74,97 +77,27 @@ 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) - err := s.engine.Run(addr) - if err != nil { + s.Http = &http.Server{ + Addr: addr, + Handler: s.engine, + } + if err := s.Http.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Error(err) log.Infof("请检查端口是否被占用.") time.Sleep(time.Second * 5) os.Exit(1) } + //err := s.engine.Run(addr) + //if err != nil { + // log.Error(err) + // log.Infof("请检查端口是否被占用.") + // time.Sleep(time.Second * 5) + // os.Exit(1) + //} }() } @@ -197,9 +130,11 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) { h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil)) } return h - }()).SetTimeout(time.Second * time.Duration(c.timeout)).Do() + }()).SetTimeout(time.Second * time.Duration(c.timeout)).F().Retry().Attempt(5). + WaitTime(time.Millisecond * 500).MaxWaitTime(time.Second * 5). + 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 +142,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 +173,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 +293,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 +338,20 @@ 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) GetStrangerInfo(c *gin.Context) { + uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64) + c.JSON(200, s.bot.CQGetStrangerInfo(uid)) +} + func (s *httpServer) HandleQuickOperation(c *gin.Context) { if c.Request.Method != "POST" { c.AbortWithStatus(404) @@ -392,6 +363,23 @@ func (s *httpServer) HandleQuickOperation(c *gin.Context) { } } +func (s *httpServer) OcrImage(c *gin.Context) { + img := getParam(c, "image") + c.JSON(200, s.bot.CQOcrImage(img)) +} + +func (s *httpServer) GetWordSlices(c *gin.Context) { + content := getParam(c, "content") + c.JSON(200, s.bot.CQGetWordSlices(content)) +} + +func (s *httpServer) SetGroupPortrait(c *gin.Context) { + gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) + file := getParam(c, "file") + cache := getParam(c, "cache") + c.JSON(200, s.bot.CQSetGroupPortrait(gid, file, cache)) +} + func getParamOrDefault(c *gin.Context, k, def string) string { r := getParam(c, k) if r != "" { @@ -400,7 +388,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 +427,130 @@ 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) + }, + "get_stranger_info": func(s *httpServer, c *gin.Context) { + s.GetStrangerInfo(c) + }, + "reload_event_filter": func(s *httpServer, c *gin.Context) { + s.ReloadEventFilter(c) + }, + "set_group_portrait": func(s *httpServer, c *gin.Context) { + s.SetGroupPortrait(c) + }, + ".handle_quick_operation": func(s *httpServer, c *gin.Context) { + s.HandleQuickOperation(c) + }, + ".ocr_image": func(s *httpServer, c *gin.Context) { + s.OcrImage(c) + }, + ".get_word_slices": func(s *httpServer, c *gin.Context) { + s.GetWordSlices(c) + }, +} + +func (s *httpServer) ShutDown() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.Http.Shutdown(ctx); err != nil { + log.Fatal("http Server Shutdown:", err) + } + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } + log.Println("http Server exiting") +} diff --git a/server/websocket.go b/server/websocket.go index 5c04d52b..8e5d8211 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,20 +223,12 @@ func (c *websocketClient) onBotPushEvent(m coolq.MSG) { func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) { if s.token != "" { - if auth := r.URL.Query().Get("access_token"); auth != s.token && auth != "" { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return - } else if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) == 2 { - if auth[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) + 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 } - } else { - log.Warnf("已拒绝 %v 的 Websocket 请求: 空Token或传入格式错误", r.RemoteAddr) - w.WriteHeader(401) - return } } c, err := upgrader.Upgrade(w, r, nil) @@ -245,20 +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 auth := r.URL.Query().Get("access_token"); auth != s.token && auth != "" { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return - } else if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) == 2 { - if auth[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) + 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 } - } else { - log.Warnf("已拒绝 %v 的 Websocket 请求: 空Token或传入格式错误", r.RemoteAddr) - w.WriteHeader(401) - return } } c, err := upgrader.Upgrade(w, r, nil) @@ -273,20 +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 auth := r.URL.Query().Get("access_token"); auth != s.token && auth != "" { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) - w.WriteHeader(401) - return - } else if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) == 2 { - if auth[1] != s.token { - log.Warnf("已拒绝 %v 的 Websocket 请求: Token错误", r.RemoteAddr) + 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 } - } else { - log.Warnf("已拒绝 %v 的 Websocket 请求: 空Token或传入格式错误", r.RemoteAddr) - w.WriteHeader(401) - return } } c, err := upgrader.Upgrade(w, r, nil) @@ -300,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) @@ -328,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) @@ -381,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 { @@ -461,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()) @@ -484,12 +486,30 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ "can_send_record": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQCanSendRecord() }, + "get_stranger_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQGetStrangerInfo(p.Get("user_id").Int()) + }, "get_status": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQGetStatus() }, "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() + }, + ".ocr_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQOcrImage(p.Get("image").Str) + }, + ".get_word_slices": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQGetWordSlices(p.Get("content").Str) + }, + "set_group_portrait": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { + return bot.CQSetGroupPortrait(p.Get("group_id").Int(), p.Get("file").String(), p.Get("cache").String()) + }, ".handle_quick_operation": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { return bot.CQHandleQuickOperation(p.Get("context"), p.Get("operation")) },