diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35f4ace..39a7985 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,8 @@ jobs: - name: Checkout run: | git version - git clone https://github.com/Mrs4s/go-cqhttp.git /home/runner/work/go-cqhttp/go-cqhttp + git clone "${{ github.event.repository.html_url }}" /home/runner/work/go-cqhttp/go-cqhttp + git checkout "${{ github.ref }}" - name: Set up Go uses: actions/setup-go@v3 diff --git a/cmd/gocq/login.go b/cmd/gocq/login.go index 5909af0..0bc2d7a 100644 --- a/cmd/gocq/login.go +++ b/cmd/gocq/login.go @@ -8,13 +8,16 @@ import ( "image" "image/png" "net/http" + "net/url" "os" + "strconv" "strings" + "sync" + "sync/atomic" "time" "github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/utils" - "github.com/Mrs4s/go-cqhttp/internal/base" "github.com/mattn/go-colorable" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -22,6 +25,7 @@ import ( "gopkg.ilharper.com/x/isatty" "github.com/Mrs4s/go-cqhttp/global" + "github.com/Mrs4s/go-cqhttp/internal/base" "github.com/Mrs4s/go-cqhttp/internal/download" ) @@ -265,15 +269,26 @@ func fetchCaptcha(id string) string { return "" } -func energy(uin uint64, id string, appVersion string, salt []byte) ([]byte, error) { +func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) { signServer := base.SignServer if !strings.HasSuffix(signServer, "/") { signServer += "/" } - response, err := download.Request{ + headers := make(map[string]string) + signServerBearer := base.SignServerBearer + if signServerBearer != "-" && signServerBearer != "" { + headers["Authorization"] = "Bearer " + signServerBearer + } + req := download.Request{ Method: http.MethodGet, - URL: signServer + "custom_energy" + fmt.Sprintf("?data=%v&salt=%v", id, hex.EncodeToString(salt)), - }.Bytes() + Header: headers, + URL: signServer + "custom_energy" + fmt.Sprintf("?data=%v&salt=%v&uin=%v&android_id=%v&guid=%v", + id, hex.EncodeToString(salt), uin, utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second) + if base.IsBelow110 { + req.URL = signServer + "custom_energy" + fmt.Sprintf("?data=%v&salt=%v", id, hex.EncodeToString(salt)) + } + response, err := req.Bytes() if err != nil { log.Warnf("获取T544 sign时出现错误: %v server: %v", err, signServer) return nil, err @@ -290,23 +305,242 @@ func energy(uin uint64, id string, appVersion string, salt []byte) ([]byte, erro return data, nil } -func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) { +// signSubmit 提交的操作类型 +func signSubmit(uin string, cmd string, callbackID int64, buffer []byte, t string) { signServer := base.SignServer if !strings.HasSuffix(signServer, "/") { signServer += "/" } + buffStr := hex.EncodeToString(buffer) + log.Infof("submit %v: uin=%v, cmd=%v, callbackID=%v, buffer-end=%v", t, uin, cmd, callbackID, + buffStr[len(buffStr)-10:]) + _, err := download.Request{ + Method: http.MethodGet, + URL: signServer + "submit" + fmt.Sprintf("?uin=%v&cmd=%v&callback_id=%v&buffer=%v", + uin, cmd, callbackID, buffStr), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() + if err != nil { + log.Warnf("提交 callback 时出现错误: %v server: %v", err, signServer) + } +} + +// signCallback request token 和签名的回调 +func signCallback(uin string, results []gjson.Result, t string) { + for _, result := range results { + cmd := result.Get("cmd").String() + callbackID := result.Get("callbackId").Int() + body, _ := hex.DecodeString(result.Get("body").String()) + ret, err := cli.SendSsoPacket(cmd, body) + if err != nil { + log.Warnf("callback error: %v", err) + } + signSubmit(uin, cmd, callbackID, ret, t) + } +} + +func signRequset(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) { + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} + signServerBearer := base.SignServerBearer + if signServerBearer != "-" && signServerBearer != "" { + headers["Authorization"] = "Bearer " + signServerBearer + } response, err := download.Request{ Method: http.MethodPost, URL: signServer + "sign", - Header: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, - Body: bytes.NewReader([]byte(fmt.Sprintf("uin=%v&qua=%s&cmd=%s&seq=%v&buffer=%v", uin, qua, cmd, seq, hex.EncodeToString(buff)))), - }.Bytes() + Header: headers, + Body: bytes.NewReader([]byte(fmt.Sprintf("uin=%v&qua=%s&cmd=%s&seq=%v&buffer=%v&android_id=%v&guid=%v", + uin, qua, cmd, seq, hex.EncodeToString(buff), utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)))), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() if err != nil { - log.Warnf("获取sso sign时出现错误: %v server: %v", err, signServer) return nil, nil, nil, err } sign, _ = hex.DecodeString(gjson.GetBytes(response, "data.sign").String()) extra, _ = hex.DecodeString(gjson.GetBytes(response, "data.extra").String()) token, _ = hex.DecodeString(gjson.GetBytes(response, "data.token").String()) + if !base.IsBelow110 { + go signCallback(uin, gjson.GetBytes(response, "data.requestCallback").Array(), "sign") + } return sign, extra, token, nil } + +var registerLock sync.Mutex + +func signRegister(uin int64, androidID, guid []byte, qimei36, key string) { + if base.IsBelow110 { + log.Warn("签名服务器版本低于1.1.0, 跳过实例注册") + return + } + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer + "register" + fmt.Sprintf("?uin=%v&android_id=%v&guid=%v&qimei36=%v&key=%s", + uin, utils.B2S(androidID), hex.EncodeToString(guid), qimei36, key), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() + if err != nil { + log.Warnf("注册QQ实例时出现错误: %v server: %v", err, signServer) + return + } + msg := gjson.GetBytes(resp, "msg") + if gjson.GetBytes(resp, "code").Int() != 0 { + log.Warnf("注册QQ实例时出现错误: %v server: %v", msg, signServer) + return + } + log.Infof("注册QQ实例 %v 成功: %v", uin, msg) +} + +func signRefreshToken(uin string) error { + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + log.Info("正在刷新 token") + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer + "request_token" + fmt.Sprintf("?uin=%v", uin), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() + if err != nil { + return err + } + msg := gjson.GetBytes(resp, "msg") + if gjson.GetBytes(resp, "code").Int() != 0 { + return errors.New(msg.String()) + } + go signCallback(uin, gjson.GetBytes(resp, "data").Array(), "request token") + return nil +} + +var missTokenCount = uint64(0) + +func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) { + i := 0 + for { + sign, extra, token, err = signRequset(seq, uin, cmd, qua, buff) + if err != nil { + log.Warnf("获取sso sign时出现错误: %v server: %v", err, base.SignServer) + } + if i > 0 { + break + } + i++ + if (!base.IsBelow110) && base.Account.AutoRegister && err == nil && len(sign) == 0 { + if registerLock.TryLock() { // 避免并发时多处同时销毁并重新注册 + log.Warn("获取签名为空,实例可能丢失,正在尝试重新注册") + defer registerLock.Unlock() + err := signServerDestroy(uin) + if err != nil { + log.Warnln(err) + return nil, nil, nil, err + } + signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, base.Key) + } + continue + } + if (!base.IsBelow110) && base.Account.AutoRefreshToken && len(token) == 0 { + log.Warnf("token 已过期, 总丢失 token 次数为 %v", atomic.AddUint64(&missTokenCount, 1)) + if registerLock.TryLock() { + defer registerLock.Unlock() + if err := signRefreshToken(uin); err != nil { + log.Warnf("刷新 token 出现错误: %v server: %v", err, base.SignServer) + } else { + log.Info("刷新 token 成功") + } + } + continue + } + break + } + return sign, extra, token, err +} + +func signServerDestroy(uin string) error { + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + signVersion, err := signVersion() + if err != nil { + return errors.Wrapf(err, "获取签名服务版本出现错误, server: %v", signServer) + } + if global.VersionNameCompare("v"+signVersion, "v1.1.6") { + return errors.Errorf("当前签名服务器版本 %v 低于 1.1.6,无法使用 destroy 接口", signVersion) + } + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer + "destroy" + fmt.Sprintf("?uin=%v&key=%v", uin, base.Key), + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() + if err != nil || gjson.GetBytes(resp, "code").Int() != 0 { + return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer) + } + return nil +} + +func signVersion() (version string, err error) { + signServer := base.SignServer + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer, + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes() + if err != nil { + return "", err + } + if gjson.GetBytes(resp, "code").Int() == 0 { + return gjson.GetBytes(resp, "data.version").String(), nil + } + return "", errors.New("empty version") +} + +// 定时刷新 token, interval 为间隔时间(分钟) +func signStartRefreshToken(interval int64) { + if interval <= 0 { + log.Warn("定时刷新 token 已关闭") + return + } + log.Infof("每 %v 分钟将刷新一次签名 token", interval) + if interval < 10 { + log.Warnf("间隔时间 %v 分钟较短,推荐 30~40 分钟", interval) + } + if interval > 60 { + log.Warn("间隔时间不能超过 60 分钟,已自动设置为 60 分钟") + interval = 60 + } + t := time.NewTicker(time.Duration(interval) * time.Minute) + defer t.Stop() + for range t.C { + err := signRefreshToken(strconv.FormatInt(base.Account.Uin, 10)) + if err != nil { + log.Warnf("刷新 token 出现错误: %v server: %v", err, base.SignServer) + } + } +} + +func signWaitServer() bool { + t := time.NewTicker(time.Second * 5) + defer t.Stop() + i := 0 + for range t.C { + if i > 3 { + return false + } + i++ + u, err := url.Parse(base.SignServer) + if err != nil { + log.Warnf("连接到签名服务器出现错误: %v", err) + continue + } + r := utils.RunTCPPingLoop(u.Host, 4) + if r.PacketsLoss > 0 { + log.Warnf("连接到签名服务器出现错误: 丢包%d/%d 时延%dms", r.PacketsLoss, r.PacketsSent, r.AvgTimeMill) + continue + } + break + } + log.Infof("连接至签名服务器: %s", base.SignServer) + return true +} diff --git a/cmd/gocq/main.go b/cmd/gocq/main.go index 10ab254..4e71431 100644 --- a/cmd/gocq/main.go +++ b/cmd/gocq/main.go @@ -166,8 +166,27 @@ func LoginInteract() { if base.SignServer != "-" && base.SignServer != "" { log.Infof("使用服务器 %s 进行数据包签名", base.SignServer) + if base.SignServerBearer != "-" && base.SignServerBearer != "" { + log.Infof("使用 Bearer %s 认证签名服务器 %s ", base.SignServerBearer, base.SignServer) + } + // 等待签名服务器直到连接成功 + if !signWaitServer() { + log.Fatalf("连接签名服务器失败") + } + signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, base.Key) + go signStartRefreshToken(base.Account.RefreshInterval) // 定时刷新 token wrapper.DandelionEnergy = energy wrapper.FekitGetSign = sign + if !base.IsBelow110 { + if !base.Account.AutoRegister { + log.Warn("自动注册实例已关闭,若未配置 sign-server 端自动注册实例则实例丢失时需要重启 go-cqhttp 以正常签名") + } + if !base.Account.AutoRefreshToken { + log.Warn("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token") + } + } else { + log.Warn("签名服务器版本 <= 1.1.0 ,无法使用刷新 token 等操作,建议使用 1.1.6 版本及以上签名服务器") + } } else { log.Warnf("警告: 未配置签名服务器, 这可能会导致登录 45 错误码或发送消息被风控") } @@ -287,6 +306,7 @@ func LoginInteract() { cli.Uin = base.Account.Uin cli.PasswordMd5 = base.PasswordHash } + download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second) if !base.FastStart { log.Infof("正在检查协议更新...") currentVersionName := device.Protocol.Version().SortVersionName @@ -373,7 +393,6 @@ func LoginInteract() { }) saveToken() cli.AllowSlider = true - download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second) // 在登录完成后设置, 防止在堵塞协议更新 log.Infof("登录成功 欢迎使用: %v", cli.Nickname) log.Info("开始加载好友列表...") global.Check(cli.ReloadFriendList(), true) diff --git a/coolq/api.go b/coolq/api.go index 39815e2..4dd2fd2 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -896,7 +896,7 @@ func (bot *CQBot) uploadForwardElement(m gjson.Result, target int64, sourceType SenderId: m.GetAttribute().SenderUin, SenderName: m.GetAttribute().SenderName, Time: int32(msgTime), - Message: resolveElement(bot.ConvertContentMessage(m.GetContent(), mSource)), + Message: resolveElement(bot.ConvertContentMessage(m.GetContent(), mSource, false)), } } log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str) @@ -971,7 +971,10 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa if m.Type != gjson.JSON { return Failed(100) } - + source := message.Source{ + SourceType: message.SourcePrivate, + PrimaryID: 0, + } fe := bot.uploadForwardElement(m, groupID, message.SourceGroup) if fe == nil { return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息") @@ -981,7 +984,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.") return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出") } - mid := bot.InsertGroupMessage(ret) + mid := bot.InsertGroupMessage(ret, source) log.Infof("发送群 %v(%v) 的合并转发消息: %v (%v)", groupID, groupID, limitedString(m.String()), mid) return OK(global.MSG{ "message_id": mid, @@ -1110,17 +1113,18 @@ func (bot *CQBot) CQSetGroupMemo(groupID int64, msg, img string) global.MSG { if err != nil { return Failed(100, "IMAGE_NOT_FOUND", "图片未找到") } - _, err = bot.Client.AddGroupNoticeWithPic(groupID, msg, data) + noticeID, err := bot.Client.AddGroupNoticeWithPic(groupID, msg, data) if err != nil { return Failed(100, "SEND_NOTICE_ERROR", err.Error()) } + return OK(global.MSG{"notice_id": noticeID}) } else { - _, err := bot.Client.AddGroupNoticeSimple(groupID, msg) + noticeID, err := bot.Client.AddGroupNoticeSimple(groupID, msg) if err != nil { return Failed(100, "SEND_NOTICE_ERROR", err.Error()) } + return OK(global.MSG{"notice_id": noticeID}) } - return OK(nil) } return Failed(100, "GROUP_NOT_FOUND", "群聊不存在") } @@ -1149,15 +1153,15 @@ func (bot *CQBot) CQDelGroupMemo(groupID int64, fid string) global.MSG { // @rename(msg->message, block->reject_add_request) func (bot *CQBot) CQSetGroupKick(groupID int64, userID int64, msg string, block bool) global.MSG { if g := bot.Client.FindGroup(groupID); g != nil { - if m := g.FindMember(userID); m == nil { - return Failed(100, "MEMBER_IS_NOT_IN_GROUP", "人员不存在") - } else { - err := m.Kick(msg, block) - if err != nil { - return Failed(100, "NOT_MANAGEABLE", "机器人权限不足") - } - return OK(nil) + m := g.FindMember(userID) + if m == nil { + return Failed(100, "MEMBER_NOT_FOUND", "人员不存在") } + err := m.Kick(msg, block) + if err != nil { + return Failed(100, "NOT_MANAGEABLE", "机器人权限不足") + } + return OK(nil) } return Failed(100, "GROUP_NOT_FOUND", "群聊不存在") } @@ -1390,7 +1394,7 @@ func (bot *CQBot) CQGetGroupHonorInfo(groupID int64, t string) global.MSG { if t == "performer" || t == "all" { if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Performer); err == nil { - msg["performer_lis"] = convertMem(honor.ActorList) + msg["performer_list"] = convertMem(honor.ActorList) } } @@ -1686,9 +1690,27 @@ func (bot *CQBot) CQGetMessage(messageID int32) global.MSG { switch o := msg.(type) { case *db.StoredGroupMessage: m["group_id"] = o.GroupCode - m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourceGroup), message.Source{SourceType: message.SourceGroup, PrimaryID: o.GroupCode}) + if o.QuotedInfo != nil { + elem := global.MSG{ + "type": "reply", + "data": global.MSG{ + "id": strconv.FormatInt(int64(o.QuotedInfo.PrevGlobalID), 10), + }, + } + o.Content = append(o.Content, elem) + } + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourceGroup, false), message.Source{SourceType: message.SourceGroup, PrimaryID: o.GroupCode}) case *db.StoredPrivateMessage: - m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourcePrivate), message.Source{SourceType: message.SourcePrivate}) + if o.QuotedInfo != nil { + elem := global.MSG{ + "type": "reply", + "data": global.MSG{ + "id": strconv.FormatInt(int64(o.QuotedInfo.PrevGlobalID), 10), + }, + } + o.Content = append(o.Content, elem) + } + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourcePrivate, false), message.Source{SourceType: message.SourcePrivate}) } return OK(m) } @@ -1748,7 +1770,7 @@ func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) global.MSG { "tiny_id": fU64(channelMsgByDB.Attribute.SenderTinyID), "nickname": channelMsgByDB.Attribute.SenderName, } - m["message"] = ToFormattedMessage(bot.ConvertContentMessage(channelMsgByDB.Content, message.SourceGuildChannel), source) + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(channelMsgByDB.Content, message.SourceGuildChannel, false), source) } case message.SourceGuildDirect: // todo(mrs4s): 支持 direct 消息 @@ -1791,10 +1813,14 @@ func (bot *CQBot) CQGetGroupMessageHistory(groupID int64, seq int64) global.MSG log.Warnf("获取群历史消息失败: %v", err) return Failed(100, "MESSAGES_API_ERROR", err.Error()) } + source := message.Source{ + SourceType: message.SourcePrivate, + PrimaryID: 0, + } ms := make([]*event, 0, len(msg)) for _, m := range msg { bot.checkMedia(m.Elements, groupID) - id := bot.InsertGroupMessage(m) + id := bot.InsertGroupMessage(m, source) t := bot.formatGroupMessage(m) t.Others["message_id"] = id ms = append(ms, t) @@ -2014,7 +2040,7 @@ func (bot *CQBot) CQGetVersionInfo() global.MSG { "protocol_version": "v11", "coolq_directory": wd, "coolq_edition": "pro", - "go-cqhttp": true, + "go_cqhttp": true, "plugin_version": "4.15.0", "plugin_build_number": 99, "plugin_build_configuration": "release", diff --git a/coolq/bot.go b/coolq/bot.go index 512a1e6..e2e3231 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -288,7 +288,7 @@ func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) (in log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupID, err) return -1, errors.Wrap(err, "send group music share error") } - return bot.InsertGroupMessage(ret), nil + return bot.InsertGroupMessage(ret, source), nil case *message.AtElement: if i.Target == 0 && group.SelfPermission() == client.Member { e = message.NewText("@全体成员") @@ -307,7 +307,7 @@ func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) (in log.Warnf("群消息发送失败: 账号可能被风控.") return -1, errors.New("send group message failed: blocked by server") } - return bot.InsertGroupMessage(ret), nil + return bot.InsertGroupMessage(ret, source), nil } // SendPrivateMessage 发送私聊消息 @@ -357,7 +357,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen case bot.Client.FindFriend(target) != nil: // 双向好友 msg := bot.Client.SendPrivateMessage(target, m) if msg != nil { - id = bot.InsertPrivateMessage(msg) + id = bot.InsertPrivateMessage(msg, source) } case ok || groupID != 0: // 临时会话 if !base.AllowTempSession { @@ -395,7 +395,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen case unidirectionalFriendExists(): // 单向好友 msg := bot.Client.SendPrivateMessage(target, m) if msg != nil { - id = bot.InsertPrivateMessage(msg) + id = bot.InsertPrivateMessage(msg, source) } default: nickname := "Unknown" @@ -444,7 +444,7 @@ func (bot *CQBot) SendGuildChannelMessage(guildID, channelID uint64, m *message. } // InsertGroupMessage 群聊消息入数据库 -func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { +func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage, source message.Source) int32 { t := &message.SendingMessage{Elements: m.Elements} replyElem := t.FirstOrNil(func(e message.IMessageElement) bool { _, ok := e.(*message.ReplyElement) @@ -468,7 +468,7 @@ func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { } return "" }(), - Content: ToMessageContent(m.Elements), + Content: ToMessageContent(m.Elements, source), } if replyElem != nil { reply := replyElem.(*message.ReplyElement) @@ -476,7 +476,7 @@ func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { msg.QuotedInfo = &db.QuotedInfo{ PrevID: encodeMessageID(m.GroupCode, reply.ReplySeq), PrevGlobalID: db.ToGlobalID(m.GroupCode, reply.ReplySeq), - QuotedContent: ToMessageContent(reply.Elements), + QuotedContent: ToMessageContent(reply.Elements, source), } } if err := db.InsertGroupMessage(msg); err != nil { @@ -487,7 +487,7 @@ func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { } // InsertPrivateMessage 私聊消息入数据库 -func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage) int32 { +func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage, source message.Source) int32 { t := &message.SendingMessage{Elements: m.Elements} replyElem := t.FirstOrNil(func(e message.IMessageElement) bool { _, ok := e.(*message.ReplyElement) @@ -511,7 +511,7 @@ func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage) int32 { return m.Sender.Uin }(), TargetUin: m.Target, - Content: ToMessageContent(m.Elements), + Content: ToMessageContent(m.Elements, source), } if replyElem != nil { reply := replyElem.(*message.ReplyElement) @@ -519,7 +519,7 @@ func (bot *CQBot) InsertPrivateMessage(m *message.PrivateMessage) int32 { msg.QuotedInfo = &db.QuotedInfo{ PrevID: encodeMessageID(reply.Sender, reply.ReplySeq), PrevGlobalID: db.ToGlobalID(reply.Sender, reply.ReplySeq), - QuotedContent: ToMessageContent(reply.Elements), + QuotedContent: ToMessageContent(reply.Elements, source), } } if err := db.InsertPrivateMessage(msg); err != nil { @@ -562,6 +562,10 @@ func (bot *CQBot) InsertTempMessage(target int64, m *message.TempMessage) int32 // InsertGuildChannelMessage 频道消息入数据库 func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMessage) string { id := encodeGuildMessageID(m.GuildId, m.ChannelId, m.Id, message.SourceGuildChannel) + source := message.Source{ + SourceType: message.SourceGuildChannel, + PrimaryID: int64(m.Sender.TinyId), + } msg := &db.StoredGuildChannelMessage{ ID: id, Attribute: &db.StoredGuildMessageAttribute{ @@ -573,7 +577,7 @@ func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMessage) stri }, GuildID: m.GuildId, ChannelID: m.ChannelId, - Content: ToMessageContent(m.Elements), + Content: ToMessageContent(m.Elements, source), } if err := db.InsertGuildChannelMessage(msg); err != nil { log.Warnf("记录聊天数据时出现错误: %v", err) diff --git a/coolq/cqcode.go b/coolq/cqcode.go index a08a275..1ce3dd2 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -51,7 +51,7 @@ func replyID(r *message.ReplyElement, source message.Source) int32 { } // 私聊时,部分(不确定)的账号会在 ReplyElement 中带有 GroupID 字段。 // 这里需要判断是由于 “直接回复” 功能,GroupID 为触发直接回复的来源那个群。 - if source.SourceType == message.SourcePrivate && (r.Sender == source.PrimaryID || r.GroupID == source.PrimaryID) { + if source.SourceType == message.SourcePrivate && (r.Sender == source.PrimaryID || r.GroupID == source.PrimaryID || r.GroupID == 0) { // 私聊似乎腾讯服务器有bug? seq = int32(uint16(seq)) id = r.Sender @@ -266,10 +266,15 @@ func toElements(e []message.IMessageElement, source message.Source) (r []msg.Ele // ToMessageContent 将消息转换成 Content. 忽略 Reply // 不同于 onebot 的 Array Message, 此函数转换出来的 Content 的 data 段为实际类型 // 方便数据库查询 -func ToMessageContent(e []message.IMessageElement) (r []global.MSG) { +func ToMessageContent(e []message.IMessageElement, source message.Source) (r []global.MSG) { for _, elem := range e { var m global.MSG switch o := elem.(type) { + case *message.ReplyElement: + m = global.MSG{ + "type": "reply", + "data": global.MSG{"id": replyID(o, source)}, + } case *message.TextElement: m = global.MSG{ "type": "text", @@ -384,7 +389,7 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) { // ConvertStringMessage 将消息字符串转为消息元素数组 func (bot *CQBot) ConvertStringMessage(spec *onebot.Spec, raw string, sourceType message.SourceType) (r []message.IMessageElement) { elems := msg.ParseString(raw) - return bot.ConvertElements(spec, elems, sourceType) + return bot.ConvertElements(spec, elems, sourceType, true) } // ConvertObjectMessage 将消息JSON对象转为消息元素数组 @@ -393,11 +398,11 @@ func (bot *CQBot) ConvertObjectMessage(spec *onebot.Spec, m gjson.Result, source return bot.ConvertStringMessage(spec, m.Str, sourceType) } elems := msg.ParseObject(m) - return bot.ConvertElements(spec, elems, sourceType) + return bot.ConvertElements(spec, elems, sourceType, false) } // ConvertContentMessage 将数据库用的 content 转换为消息元素数组 -func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) { +func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType, noReply bool) (r []message.IMessageElement) { elems := make([]msg.Element, len(content)) for i, v := range content { elem := msg.Element{Type: v["type"].(string)} @@ -407,13 +412,16 @@ func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message } elems[i] = elem } - return bot.ConvertElements(onebot.V11, elems, sourceType) + return bot.ConvertElements(onebot.V11, elems, sourceType, noReply) } // ConvertElements 将解码后的消息数组转换为MiraiGo表示 -func (bot *CQBot) ConvertElements(spec *onebot.Spec, elems []msg.Element, sourceType message.SourceType) (r []message.IMessageElement) { +func (bot *CQBot) ConvertElements(spec *onebot.Spec, elems []msg.Element, sourceType message.SourceType, noReply bool) (r []message.IMessageElement) { var replyCount int for _, elem := range elems { + if noReply && elem.Type == "reply" { + continue + } me, err := bot.ConvertElement(spec, elem, sourceType) if err != nil { // TODO: don't use cqcode format @@ -497,7 +505,7 @@ func (bot *CQBot) reply(spec *onebot.Spec, elem msg.Element, sourceType message. ReplySeq: org.GetAttribute().MessageSeq, Sender: org.GetAttribute().SenderUin, Time: int32(org.GetAttribute().Timestamp), - Elements: bot.ConvertContentMessage(org.GetContent(), sourceType), + Elements: bot.ConvertContentMessage(org.GetContent(), sourceType, true), } default: diff --git a/coolq/event.go b/coolq/event.go index 605ea3e..61e763f 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -79,7 +79,7 @@ func (bot *CQBot) privateMessageEvent(_ *client.QQClient, m *message.PrivateMess PrimaryID: m.Sender.Uin, } cqm := toStringMessage(m.Elements, source) - id := bot.InsertPrivateMessage(m) + id := bot.InsertPrivateMessage(m, source) log.Infof("收到好友 %v(%v) 的消息: %v (%v)", m.Sender.DisplayName(), m.Sender.Uin, cqm, id) typ := "message/private/friend" if m.Sender.Uin == bot.Client.Uin { @@ -126,7 +126,7 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) PrimaryID: m.GroupCode, } cqm := toStringMessage(m.Elements, source) - id := bot.InsertGroupMessage(m) + id := bot.InsertGroupMessage(m, source) log.Infof("收到群 %v(%v) 内 %v(%v) 的消息: %v (%v)", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm, id) gm := bot.formatGroupMessage(m) if gm == nil { diff --git a/go.mod b/go.mod index ffe42a1..8ea6e36 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/FloatTech/sqlite v1.5.7 github.com/Microsoft/go-winio v0.6.0 - github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e + github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc github.com/fumiama/go-base16384 v1.6.1 diff --git a/go.sum b/go.sum index d251a11..61e86ae 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJG github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e h1:99itMjI//+KaFF0+0QCBg/uHhGMJ99jG2lP6z/UnOsU= -github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0= +github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab h1:SLciJTlC5YiG3qqvGJf4sHJDHDXUdH+v4rjqVhE5SIQ= +github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0= github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d h1:/Xuj3fIiMY2ls1TwvPKmaqQrtJsPY+c9s+0lOScVHd8= github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d/go.mod h1:2Ie+hdBFQpQFDHfeklgxoFmQRCE7O+KwFpISeXq7OwA= github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA= diff --git a/internal/base/flag.go b/internal/base/flag.go index e622eca..8e0c37a 100644 --- a/internal/base/flag.go +++ b/internal/base/flag.go @@ -39,7 +39,11 @@ var ( AllowTempSession bool // 是否允许发送临时会话信息 UpdateProtocol bool // 是否更新协议 SignServer string // 使用特定的服务器进行签名 - HTTPTimeout int + SignServerBearer string // 认证签名服务器的 Bearer Token + Key string // 签名服务器密钥 + IsBelow110 bool // 签名服务器版本是否低于1.1.0及以下 + HTTPTimeout int // download 超时时间 + SignServerTimeout int // 签名服务器超时时间 PostFormat string // 上报格式 string or array Proxy string // 存储 proxy_rewrite,用于设置代理 @@ -89,7 +93,11 @@ func Init() { UseSSOAddress = conf.Account.UseSSOAddress AllowTempSession = conf.Account.AllowTempSession SignServer = conf.Account.SignServer + SignServerBearer = conf.Account.SignServerBearer + Key = conf.Account.Key + IsBelow110 = conf.Account.IsBelow110 HTTPTimeout = conf.Message.HTTPTimeout + SignServerTimeout = conf.Message.SignServerTimeout } { // others Proxy = conf.Message.ProxyRewrite diff --git a/internal/download/download.go b/internal/download/download.go index a82af5a..786658f 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -21,20 +21,7 @@ import ( "github.com/Mrs4s/go-cqhttp/internal/base" ) -var client = &http.Client{ - Transport: &http.Transport{ - Proxy: func(request *http.Request) (*url.URL, error) { - if base.Proxy == "" { - return http.ProxyFromEnvironment(request) - } - return url.Parse(base.Proxy) - }, - // Disable http2 - TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, - MaxIdleConnsPerHost: 999, - }, - Timeout: time.Second * 5, -} +var client = newcli(time.Second * 15) var clienth2 = &http.Client{ Transport: &http.Transport{ @@ -47,7 +34,24 @@ var clienth2 = &http.Client{ ForceAttemptHTTP2: true, MaxIdleConnsPerHost: 999, }, - Timeout: time.Second * 5, + Timeout: time.Second * 15, +} + +func newcli(t time.Duration) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + Proxy: func(request *http.Request) (*url.URL, error) { + if base.Proxy == "" { + return http.ProxyFromEnvironment(request) + } + return url.Parse(base.Proxy) + }, + // Disable http2 + TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{}, + MaxIdleConnsPerHost: 999, + }, + Timeout: t, + } } // ErrOverSize 响应主体过大时返回此错误 @@ -56,6 +60,12 @@ var ErrOverSize = errors.New("oversize") // UserAgent HTTP请求时使用的UA const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66" +// WithTimeout get a download instance with timeout t +func (r Request) WithTimeout(t time.Duration) *Request { + r.custcli = newcli(t) + return &r +} + // SetTimeout set internal/download client timeout func SetTimeout(t time.Duration) { if t == 0 { @@ -67,14 +77,18 @@ func SetTimeout(t time.Duration) { // Request is a file download request type Request struct { - Method string - URL string - Header map[string]string - Limit int64 - Body io.Reader + Method string + URL string + Header map[string]string + Limit int64 + Body io.Reader + custcli *http.Client } func (r Request) client() *http.Client { + if r.custcli != nil { + return r.custcli + } if strings.Contains(r.URL, "go-cqhttp.org") { return clienth2 } diff --git a/modules/config/config.go b/modules/config/config.go index b438359..4bfbc18 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -36,6 +36,12 @@ type Account struct { UseSSOAddress bool `yaml:"use-sso-address"` AllowTempSession bool `yaml:"allow-temp-session"` SignServer string `yaml:"sign-server"` + SignServerBearer string `yaml:"sign-server-bearer"` + Key string `yaml:"key"` + IsBelow110 bool `yaml:"is-below-110"` + AutoRegister bool `yaml:"auto-register"` + AutoRefreshToken bool `yaml:"auto-refresh-token"` + RefreshInterval int64 `yaml:"refresh-interval"` } // Config 总配置文件 @@ -58,6 +64,7 @@ type Config struct { SkipMimeScan bool `yaml:"skip-mime-scan"` ConvertWebpImage bool `yaml:"convert-webp-image"` HTTPTimeout int `yaml:"http-timeout"` + SignServerTimeout int `yaml:"sign-server-timeout"` } `yaml:"message"` Output struct { diff --git a/modules/config/default_config.yml b/modules/config/default_config.yml index 6873052..5acb845 100644 --- a/modules/config/default_config.yml +++ b/modules/config/default_config.yml @@ -24,6 +24,25 @@ account: # 账号相关 # sign-server: 'https://signserver.example.com' # 线上签名服务器 # 服务器可使用docker在本地搭建或者使用他人开放的服务 sign-server: '-' + # 签名服务器认证 Bearer Token + # 使用开放的服务可能需要提供此 Token 进行认证 + sign-server-bearer: '-' + # 如果签名服务器的版本在1.1.0及以下, 请将下面的参数改成true + is-below-110: false + # 签名服务器所需要的apikey, 如果签名服务器的版本在1.1.0及以下则此项无效 + # 本地部署的默认为114514 + key: '114514' + # 在实例可能丢失(获取到的签名为空)时是否尝试重新注册 + # 为 true 时,在签名服务不可用时可能每次发消息都会尝试重新注册并签名。 + # 为 false 时,将不会自动注册实例,在签名服务器重启或实例被销毁后需要重启 go-cqhttp 以获取实例 + # 否则后续消息将不会正常签名。关闭此项后可以考虑开启签名服务器端 auto_register 避免需要重启 + auto-register: false + # 是否在 token 过期后立即自动刷新签名 token(在需要签名时才会检测到,主要防止 token 意外丢失) + # 独立于定时刷新 + auto-refresh-token: false + # 定时刷新 token 间隔时间,单位为分钟, 建议 30~40 分钟, 不可超过 60 分钟 + # 目前丢失token也不会有太大影响,可设置为 0 以关闭,推荐开启 + refresh-interval: 40 heartbeat: # 心跳频率, 单位秒 @@ -54,8 +73,10 @@ message: skip-mime-scan: false # 是否自动转换 WebP 图片 convert-webp-image: false - # http超时时间 - http-timeout: 0 + # download 超时时间(s) + http-timeout: 15 + # 签名服务超时时间(s) + sign-server-timeout: 60 output: # 日志等级 trace,debug,info,warn,error