diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8013951..05bc69a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Go environment uses: actions/setup-go@v2.1.3 with: - go-version: 1.17 + go-version: 1.18 - name: Cache downloaded module uses: actions/cache@v2 with: diff --git a/.github/workflows/golint.yml b/.github/workflows/golint.yml index 5a9ef49..fb9cc4d 100644 --- a/.github/workflows/golint.yml +++ b/.github/workflows/golint.yml @@ -12,13 +12,19 @@ jobs: - name: Setup Go environment uses: actions/setup-go@v2.1.3 with: - go-version: 1.17 + go-version: 1.18 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: latest + - name: Static Check + uses: dominikh/staticcheck-action@v1.2.0 + with: + install-go: false + version: "2022.1" + - name: Tests run: | go test $(go list ./...) diff --git a/.gitignore b/.gitignore index d2ee21b..43a051c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ session.token device.json data/ logs/ +internal/btree/*.lock +internal/btree/*.db \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 4ff3d91..0a87b86 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,35 +21,28 @@ linters: disable-all: true fast: false enable: - - bodyclose - - deadcode - - depguard - - dogsled + #- bodyclose + #- deadcode + #- depguard + #- dogsled + - gofmt + - goimports - errcheck - exportloopref - exhaustive - bidichk - #- funlen - #- goconst - gocritic - #- gocyclo - - gofmt - - goimports - - goprintffuncname - #- gosec - - gosimple + #- gosimple - govet - ineffassign - #- misspell - - nolintlint - - rowserrcheck - - staticcheck + #- nolintlint + #- rowserrcheck + #- staticcheck - structcheck - - stylecheck - - typecheck + #- stylecheck - unconvert - - unparam - - unused + #- unparam + #- unused - varcheck - whitespace - prealloc diff --git a/Dockerfile b/Dockerfile index d5ff7e5..2f2f1ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.17-alpine AS builder +FROM golang:1.18-alpine AS builder RUN go env -w GO111MODULE=auto \ && go env -w CGO_ENABLED=0 \ diff --git a/cmd/gocq/login.go b/cmd/gocq/login.go index 80ac10b..bc19146 100644 --- a/cmd/gocq/login.go +++ b/cmd/gocq/login.go @@ -3,13 +3,14 @@ package gocq import ( "bufio" "bytes" + "image" + "image/png" "os" "strings" "time" - qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" "github.com/Mrs4s/MiraiGo/client" - "github.com/gocq/qrcode" + "github.com/mattn/go-colorable" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -53,12 +54,36 @@ func commonLogin() error { return loginResponseProcessor(res) } -func qrcodeLogin() error { - rsp, err := cli.FetchQRCode() +func printQRCode(imgData []byte) { + const ( + black = "\033[48;5;0m \033[0m" + white = "\033[48;5;7m \033[0m" + ) + img, err := png.Decode(bytes.NewReader(imgData)) if err != nil { - return err + log.Panic(err) } - fi, err := qrcode.Decode(bytes.NewReader(rsp.ImageData)) + data := img.(*image.Gray).Pix + bound := img.Bounds().Max.X + buf := make([]byte, 0, (bound*4+1)*(bound)) + i := 0 + for y := 0; y < bound; y++ { + i = y * bound + for x := 0; x < bound; x++ { + if data[i] != 255 { + buf = append(buf, white...) + } else { + buf = append(buf, black...) + } + i++ + } + buf = append(buf, '\n') + } + _, _ = colorable.NewColorableStdout().Write(buf) +} + +func qrcodeLogin() error { + rsp, err := cli.FetchQRCodeCustomSize(1, 2, 1) if err != nil { return err } @@ -70,7 +95,7 @@ func qrcodeLogin() error { log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ") } time.Sleep(time.Second) - qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.QRCodeRecoveryLevels.Low).Get(fi.Content).Print() + printQRCode(rsp.ImageData) s, err := cli.QueryQRCodeStatus(rsp.Sig) if err != nil { return err diff --git a/cmd/gocq/main.go b/cmd/gocq/main.go index 2ce945b..1cb85ca 100644 --- a/cmd/gocq/main.go +++ b/cmd/gocq/main.go @@ -81,7 +81,7 @@ func Main() { mkCacheDir := func(path string, _type string) { if !global.PathExists(path) { - if err := os.MkdirAll(path, 0o755); err != nil { + if err := os.MkdirAll(path, 0o644); err != nil { log.Fatalf("创建%s缓存文件夹失败: %v", _type, err) } } @@ -130,7 +130,6 @@ func Main() { log.Info("当前版本:", base.Version) if base.Debug { log.SetLevel(log.DebugLevel) - log.SetReportCaller(true) log.Warnf("已开启Debug模式.") // log.Debugf("开发交流群: 192548878") } @@ -259,7 +258,7 @@ func Main() { } var times uint = 1 // 重试次数 var reLoginLock sync.Mutex - cli.OnDisconnected(func(q *client.QQClient, e *client.ClientDisconnectedEvent) { + cli.DisconnectedEvent.Subscribe(func(q *client.QQClient, e *client.ClientDisconnectedEvent) { reLoginLock.Lock() defer reLoginLock.Unlock() times = 1 @@ -325,10 +324,9 @@ func Main() { log.Info("资源初始化完成, 开始处理信息.") log.Info("アトリは、高性能ですから!") - go selfupdate.CheckUpdate() go func() { - time.Sleep(5 * time.Second) - go selfdiagnosis.NetworkDiagnosis(cli) + selfupdate.CheckUpdate() + selfdiagnosis.NetworkDiagnosis(cli) }() <-global.SetupMainSignalHandler() @@ -367,6 +365,7 @@ func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, erro func newClient() *client.QQClient { c := client.NewClientEmpty() + c.UseFragmentMessage = base.ForceFragmented c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool { if !base.UseSSOAddress { log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.") @@ -383,22 +382,36 @@ func newClient() *client.QQClient { } log.Infof("读取到 %v 个自定义地址.", len(addr)) } - c.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) - case "DUMP": - if !global.PathExists(global.DumpsPath) { - _ = os.MkdirAll(global.DumpsPath, 0o755) - } - dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix())) - log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", e.Message, dumpFile) - _ = os.WriteFile(dumpFile, e.Dump, 0o644) - } - }) + c.SetLogger(protocolLogger{}) return c } + +type protocolLogger struct{} + +const fromProtocol = "Protocol -> " + +func (p protocolLogger) Info(format string, arg ...any) { + log.Infof(fromProtocol+format, arg...) +} + +func (p protocolLogger) Warning(format string, arg ...any) { + log.Warnf(fromProtocol+format, arg...) +} + +func (p protocolLogger) Debug(format string, arg ...any) { + log.Debugf(fromProtocol+format, arg...) +} + +func (p protocolLogger) Error(format string, arg ...any) { + log.Errorf(fromProtocol+format, arg...) +} + +func (p protocolLogger) Dump(data []byte, format string, arg ...any) { + if !global.PathExists(global.DumpsPath) { + _ = os.MkdirAll(global.DumpsPath, 0o755) + } + dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix())) + message := fmt.Sprintf(format, arg...) + log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile) + _ = os.WriteFile(dumpFile, data, 0o644) +} diff --git a/coolq/api.go b/coolq/api.go index 2e9677d..1eeb6be 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -12,7 +12,6 @@ import ( "runtime" "strconv" "strings" - "sync" "time" "github.com/segmentio/asm/base64" @@ -39,7 +38,7 @@ type guildMemberPageToken struct { nextQueryParam string } -var defaultPageToken = &guildMemberPageToken{ +var defaultPageToken = guildMemberPageToken{ guildID: 0, nextIndex: 0, nextRoleID: 2, @@ -151,13 +150,13 @@ func (bot *CQBot) CQGetGuildMembers(guildID uint64, nextToken string) global.MSG if guild == nil { return Failed(100, "GUILD_NOT_FOUND") } - token := defaultPageToken + token := &defaultPageToken if nextToken != "" { i, exists := bot.nextTokenCache.Get(nextToken) if !exists { return Failed(100, "NEXT_TOKEN_NOT_EXISTS") } - token = i.(*guildMemberPageToken) + token = i if token.guildID != guildID { return Failed(100, "GUILD_NOT_MATCH") } @@ -408,7 +407,6 @@ func (bot *CQBot) CQGetGroupList(noCache bool) global.MSG { gs = append(gs, global.MSG{ "group_id": g.Code, "group_name": g.Name, - "group_memo": g.Memo, "group_create_time": g.GroupCreateTime, "group_level": g.GroupLevel, "max_member_count": g.MaxMemberCount, @@ -450,7 +448,6 @@ func (bot *CQBot) CQGetGroupInfo(groupID int64, noCache bool) global.MSG { return OK(global.MSG{ "group_id": group.Code, "group_name": group.Name, - "group_memo": group.Memo, "group_create_time": group.GroupCreateTime, "group_level": group.GroupLevel, "max_member_count": group.MaxMemberCount, @@ -687,6 +684,24 @@ func (bot *CQBot) CQSendMessage(groupID, userID int64, m gjson.Result, messageTy return global.MSG{} } +// CQSendForwardMessage 发送合并转发消息 +// +// @route(send_forward_msg) +// @rename(m->messages) +func (bot *CQBot) CQSendForwardMessage(groupID, userID int64, m gjson.Result, messageType string) global.MSG { + switch { + case messageType == "group": + return bot.CQSendGroupForwardMessage(groupID, m) + case messageType == "private": + fallthrough + case userID != 0: + return bot.CQSendPrivateForwardMessage(userID, m) + case groupID != 0: + return bot.CQSendGroupForwardMessage(groupID, m) + } + return global.MSG{} +} + // CQSendGroupMessage 发送群消息 // // https://git.io/Jtz1c @@ -712,7 +727,7 @@ func (bot *CQBot) CQSendGroupMessage(groupID int64, m gjson.Result, autoEscape b var elem []message.IMessageElement if m.Type == gjson.JSON { - elem = bot.ConvertObjectMessage(m, MessageSourceGroup) + elem = bot.ConvertObjectMessage(m, message.SourceGroup) } else { str := m.String() if str == "" { @@ -722,7 +737,7 @@ func (bot *CQBot) CQSendGroupMessage(groupID int64, m gjson.Result, autoEscape b if autoEscape { elem = []message.IMessageElement{message.NewText(str)} } else { - elem = bot.ConvertStringMessage(str, MessageSourceGroup) + elem = bot.ConvertStringMessage(str, message.SourceGroup) } } fixAt(elem) @@ -766,7 +781,7 @@ func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.R var elem []message.IMessageElement if m.Type == gjson.JSON { - elem = bot.ConvertObjectMessage(m, MessageSourceGuildChannel) + elem = bot.ConvertObjectMessage(m, message.SourceGuildChannel) } else { str := m.String() if str == "" { @@ -776,7 +791,7 @@ func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.R if autoEscape { elem = []message.IMessageElement{message.NewText(str)} } else { - elem = bot.ConvertStringMessage(str, MessageSourceGuildChannel) + elem = bot.ConvertStringMessage(str, message.SourceGuildChannel) } } fixAt(elem) @@ -788,116 +803,126 @@ func (bot *CQBot) CQSendGuildChannelMessage(guildID, channelID uint64, m gjson.R return OK(global.MSG{"message_id": mid}) } -func (bot *CQBot) uploadForwardElement(m gjson.Result, groupID int64) *message.ForwardElement { +func (bot *CQBot) uploadForwardElement(m gjson.Result, target int64, sourceType message.SourceType) *message.ForwardElement { ts := time.Now().Add(-time.Minute * 5) - fm := message.NewForwardMessage() - - var lazyUpload []func() - var wg sync.WaitGroup - resolveElement := func(elems []message.IMessageElement) []message.IMessageElement { - for i, elem := range elems { - iescape := i - switch o := elem.(type) { - case *LocalImageElement, *LocalVideoElement: - wg.Add(1) - lazyUpload = append(lazyUpload, func() { - defer wg.Done() - gm, err := bot.uploadMedia(o, groupID, true) - if err != nil { - log.Warnf("警告: 群 %d %s上传失败: %v", groupID, o.Type().String(), err) - } else { - elems[iescape] = gm - } - }) - } - } - return elems + groupID := target + source := message.Source{SourceType: sourceType, PrimaryID: target} + if sourceType == message.SourcePrivate { + groupID = 0 } + builder := bot.Client.NewForwardMessageBuilder(groupID) - convert := func(e gjson.Result) *message.ForwardNode { - if e.Get("type").Str != "node" { - return nil - } - ts.Add(time.Second) - if e.Get("data.id").Exists() { - i := e.Get("data.id").Int() - m, _ := db.GetGroupMessageByGlobalID(int32(i)) - if m != nil { - return &message.ForwardNode{ - SenderId: m.Attribute.SenderUin, - SenderName: m.Attribute.SenderName, - Time: func() int32 { - msgTime := m.Attribute.Timestamp - if msgTime == 0 { - return int32(ts.Unix()) + var convertMessage func(m gjson.Result) *message.ForwardMessage + convertMessage = func(m gjson.Result) *message.ForwardMessage { + fm := message.NewForwardMessage() + var w worker + resolveElement := func(elems []message.IMessageElement) []message.IMessageElement { + for i, elem := range elems { + p := &elems[i] + switch o := elem.(type) { + case *LocalVideoElement: + w.do(func() { + gm, err := bot.uploadLocalVideo(source, o) + if err != nil { + log.Warnf(uploadFailedTemplate, "合并转发", target, "视频", err) + } else { + *p = gm } - return int32(msgTime) - }(), - Message: resolveElement(bot.ConvertContentMessage(m.Content, MessageSourceGroup)), + }) + case *LocalImageElement: + w.do(func() { + gm, err := bot.uploadLocalImage(source, o) + if err != nil { + log.Warnf(uploadFailedTemplate, "合并转发", target, "图片", err) + } else { + *p = gm + } + }) } } - log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str) - return nil + return elems } - uin := e.Get("data.[user_id,uin].0").Int() - msgTime := e.Get("data.time").Int() - if msgTime == 0 { - msgTime = ts.Unix() - } - name := e.Get("data.name").Str - c := e.Get("data.content") - if c.IsArray() { - nested := false - c.ForEach(func(_, value gjson.Result) bool { - if value.Get("type").Str == "node" { - nested = true - return false + + convert := func(e gjson.Result) *message.ForwardNode { + if e.Get("type").Str != "node" { + return nil + } + if e.Get("data.id").Exists() { + i := e.Get("data.id").Int() + m, _ := db.GetGroupMessageByGlobalID(int32(i)) + if m != nil { + msgTime := m.Attribute.Timestamp + if msgTime == 0 { + msgTime = ts.Unix() + } + return &message.ForwardNode{ + SenderId: m.Attribute.SenderUin, + SenderName: m.Attribute.SenderName, + Time: int32(msgTime), + Message: resolveElement(bot.ConvertContentMessage(m.Content, message.SourceGroup)), + } } - return true - }) - if nested { // 处理嵌套 - fe := bot.uploadForwardElement(c, groupID) + log.Warnf("警告: 引用消息 %v 错误或数据库未开启.", e.Get("data.id").Str) + return nil + } + uin := e.Get("data.[user_id,uin].0").Int() + msgTime := e.Get("data.time").Int() + if msgTime == 0 { + msgTime = ts.Unix() + } + name := e.Get("data.[name,nickname].0").Str + c := e.Get("data.content") + if c.IsArray() { + nested := false + c.ForEach(func(_, value gjson.Result) bool { + if value.Get("type").Str == "node" { + nested = true + return false + } + return true + }) + if nested { // 处理嵌套 + nestedNode := builder.NestedNode() + builder.Link(nestedNode, convertMessage(c)) + return &message.ForwardNode{ + SenderId: uin, + SenderName: name, + Time: int32(msgTime), + Message: []message.IMessageElement{nestedNode}, + } + } + } + content := bot.ConvertObjectMessage(c, message.SourceGroup) + if uin != 0 && name != "" && len(content) > 0 { return &message.ForwardNode{ SenderId: uin, SenderName: name, Time: int32(msgTime), - Message: []message.IMessageElement{fe}, + Message: resolveElement(content), } } + log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content)) + return nil } - content := bot.ConvertObjectMessage(c, MessageSourceGroup) - if uin != 0 && name != "" && len(content) > 0 { - return &message.ForwardNode{ - SenderId: uin, - SenderName: name, - Time: int32(msgTime), - Message: resolveElement(content), - } - } - log.Warnf("警告: 非法 Forward node 将跳过. uin: %v name: %v content count: %v", uin, name, len(content)) - return nil - } - if m.IsArray() { - for _, item := range m.Array() { - node := convert(item) + if m.IsArray() { + for _, item := range m.Array() { + node := convert(item) + if node != nil { + fm.AddNode(node) + } + } + } else { + node := convert(m) if node != nil { fm.AddNode(node) } } - } else { - node := convert(m) - if node != nil { - fm.AddNode(node) - } - } - for _, upload := range lazyUpload { - go upload() + w.wait() + return fm } - wg.Wait() - - return bot.Client.UploadGroupForwardMessage(groupID, fm) + return builder.Main(convertMessage(m)) } // CQSendGroupForwardMessage 扩展API-发送合并转发(群) @@ -910,18 +935,39 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa return Failed(100) } - fe := bot.uploadForwardElement(m, groupID) - if fe != nil { - ret := bot.Client.SendGroupForwardMessage(groupID, fe) - if ret == nil || ret.Id == -1 { - log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.") - return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出") - } - return OK(global.MSG{ - "message_id": bot.InsertGroupMessage(ret), - }) + fe := bot.uploadForwardElement(m, groupID, message.SourceGroup) + if fe == nil { + return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息") } - return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息") + ret := bot.Client.SendGroupForwardMessage(groupID, fe) + if ret == nil || ret.Id == -1 { + log.Warnf("合并转发(群)消息发送失败: 账号可能被风控.") + return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出") + } + return OK(global.MSG{ + "message_id": bot.InsertGroupMessage(ret), + }) +} + +// CQSendPrivateForwardMessage 扩展API-发送合并转发(好友) +// +// https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91-%E7%BE%A4 +// @route(send_private_forward_msg) +// @rename(m->messages) +func (bot *CQBot) CQSendPrivateForwardMessage(userID int64, m gjson.Result) global.MSG { + if m.Type != gjson.JSON { + return Failed(100) + } + fe := bot.uploadForwardElement(m, userID, message.SourcePrivate) + if fe == nil { + return Failed(100, "EMPTY_NODES", "未找到任何可发送的合并转发信息") + } + mid := bot.SendPrivateMessage(userID, 0, &message.SendingMessage{Elements: []message.IMessageElement{fe}}) + if mid == -1 { + log.Warnf("合并转发(好友)消息发送失败: 账号可能被风控.") + return Failed(100, "SEND_MSG_API_ERROR", "请参考 go-cqhttp 端输出") + } + return OK(global.MSG{"message_id": mid}) } // CQSendPrivateMessage 发送私聊消息 @@ -932,7 +978,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupID int64, m gjson.Result) globa func (bot *CQBot) CQSendPrivateMessage(userID int64, groupID int64, m gjson.Result, autoEscape bool) global.MSG { var elem []message.IMessageElement if m.Type == gjson.JSON { - elem = bot.ConvertObjectMessage(m, MessageSourcePrivate) + elem = bot.ConvertObjectMessage(m, message.SourcePrivate) } else { str := m.String() if str == "" { @@ -941,7 +987,7 @@ func (bot *CQBot) CQSendPrivateMessage(userID int64, groupID int64, m gjson.Resu if autoEscape { elem = []message.IMessageElement{message.NewText(str)} } else { - elem = bot.ConvertStringMessage(str, MessageSourcePrivate) + elem = bot.ConvertStringMessage(str, message.SourcePrivate) } } mid := bot.SendPrivateMessage(userID, groupID, &message.SendingMessage{Elements: elem}) @@ -994,6 +1040,17 @@ func (bot *CQBot) CQSetGroupName(groupID int64, name string) global.MSG { return Failed(100, "GROUP_NOT_FOUND", "群聊不存在") } +// CQGetGroupMemo 扩展API-获取群公告 +// @route(_get_group_notice) +func (bot *CQBot) CQGetGroupMemo(groupID int64) global.MSG { + r, err := bot.Client.GetGroupNotice(groupID) + if err != nil { + return Failed(100, "获取群公告失败", err.Error()) + } + + return OK(r) +} + // CQSetGroupMemo 扩展API-发送群公告 // // https://docs.go-cqhttp.org/api/#%E5%8F%91%E9%80%81%E7%BE%A4%E5%85%AC%E5%91%8A @@ -1114,9 +1171,9 @@ func (bot *CQBot) CQProcessFriendRequest(flag string, approve bool) global.MSG { return Failed(100, "FLAG_NOT_FOUND", "FLAG不存在") } if approve { - req.(*client.NewFriendRequest).Accept() + req.Accept() } else { - req.(*client.NewFriendRequest).Reject() + req.Reject() } return OK(nil) } @@ -1223,27 +1280,6 @@ func (bot *CQBot) CQSetGroupAdmin(groupID, userID int64, enable bool) global.MSG return OK(nil) } -// CQGetVipInfo 扩展API-获取VIP信息 -// -// https://docs.go-cqhttp.org/api/#%E8%8E%B7%E5%8F%96vip%E4%BF%A1%E6%81%AF -// @route(_get_vip_info) -func (bot *CQBot) CQGetVipInfo(userID int64) global.MSG { - vip, err := bot.Client.GetVipInfo(userID) - if err != nil { - return Failed(100, "VIP_API_ERROR", err.Error()) - } - msg := global.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) -} - // CQGetGroupHonorInfo 获取群荣誉信息 // // https://git.io/Jtz1H @@ -1347,7 +1383,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) global if reply.Exists() { autoEscape := param.EnsureBool(operation.Get("auto_escape"), false) - at := operation.Get("at_sender").Bool() && !isAnonymous && msgType == "group" + at := !isAnonymous && operation.Get("at_sender").Bool() && msgType == "group" if at && reply.IsArray() { // 在 reply 数组头部插入CQ码 replySegments := make([]global.MSG, 0) @@ -1393,7 +1429,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) global if operation.Get("delete").Bool() { bot.CQDeleteMessage(int32(context.Get("message_id").Int())) } - if operation.Get("kick").Bool() && !isAnonymous { + if !isAnonymous && operation.Get("kick").Bool() { bot.CQSetGroupKick(context.Get("group_id").Int(), context.Get("user_id").Int(), "", operation.Get("reject_add_request").Bool()) } if operation.Get("ban").Bool() { @@ -1475,18 +1511,18 @@ func (bot *CQBot) CQDownloadFile(url string, headers gjson.Result, threadCount i h := map[string]string{} if headers.IsArray() { for _, sub := range headers.Array() { - str := strings.SplitN(sub.String(), "=", 2) - if len(str) == 2 { - h[str[0]] = str[1] + first, second, ok := strings.Cut(sub.String(), "=") + if ok { + h[first] = second } } } if headers.Type == gjson.String { lines := strings.Split(headers.String(), "\r\n") for _, sub := range lines { - str := strings.SplitN(sub, "=", 2) - if len(str) == 2 { - h[str[0]] = str[1] + first, second, ok := strings.Cut(sub, "=") + if ok { + h[first] = second } } } @@ -1525,7 +1561,7 @@ func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG { r := make([]global.MSG, len(nodes)) for i, n := range nodes { bot.checkMedia(n.Message, 0) - content := ToFormattedMessage(n.Message, MessageSource{SourceType: MessageSourceGroup}, false) + content := ToFormattedMessage(n.Message, message.Source{SourceType: message.SourceGroup}) if len(n.Message) == 1 { if forward, ok := n.Message[0].(*message.ForwardMessage); ok { content = transformNodes(forward.Nodes) @@ -1536,8 +1572,9 @@ func (bot *CQBot) CQGetForwardMessage(resID string) global.MSG { "user_id": n.SenderId, "nickname": n.SenderName, }, - "time": n.Time, - "content": content, + "time": n.Time, + "content": content, + "group_id": n.GroupId, } } return r @@ -1573,9 +1610,9 @@ 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, MessageSourceGroup), MessageSource{SourceType: MessageSourceGroup, PrimaryID: uint64(o.GroupCode)}, false) + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourceGroup), message.Source{SourceType: message.SourceGroup, PrimaryID: o.GroupCode}) case *db.StoredPrivateMessage: - m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, MessageSourcePrivate), MessageSource{SourceType: MessageSourcePrivate}, false) + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(o.Content, message.SourcePrivate), message.Source{SourceType: message.SourcePrivate}) } return OK(m) } @@ -1584,28 +1621,28 @@ func (bot *CQBot) CQGetMessage(messageID int32) global.MSG { // @route(get_guild_msg) func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) global.MSG { source, seq := decodeGuildMessageID(messageID) - if source == nil { + if source.SourceType == 0 { log.Warnf("获取消息时出现错误: 无效消息ID") return Failed(100, "INVALID_MESSAGE_ID", "无效消息ID") } m := global.MSG{ "message_id": messageID, "message_source": func() string { - if source.SourceType == MessageSourceGuildDirect { + if source.SourceType == message.SourceGuildDirect { return "direct" } return "channel" }(), "message_seq": seq, - "guild_id": fU64(source.PrimaryID), + "guild_id": fU64(uint64(source.PrimaryID)), "reactions": []int{}, } // nolint: exhaustive switch source.SourceType { - case MessageSourceGuildChannel: - m["channel_id"] = fU64(source.SubID) + case message.SourceGuildChannel: + m["channel_id"] = fU64(uint64(source.SecondaryID)) if noCache { - pull, err := bot.Client.GuildService.PullGuildChannelMessage(source.PrimaryID, source.SubID, seq, seq) + pull, err := bot.Client.GuildService.PullGuildChannelMessage(uint64(source.PrimaryID), uint64(source.SecondaryID), seq, seq) if err != nil { log.Warnf("获取消息时出现错误: %v", err) return Failed(100, "API_ERROR", err.Error()) @@ -1620,7 +1657,7 @@ func (bot *CQBot) CQGetGuildMessage(messageID string, noCache bool) global.MSG { "tiny_id": fU64(pull[0].Sender.TinyId), "nickname": pull[0].Sender.Nickname, } - m["message"] = ToFormattedMessage(pull[0].Elements, *source, false) + m["message"] = ToFormattedMessage(pull[0].Elements, source) m["reactions"] = convertReactions(pull[0].Reactions) bot.InsertGuildChannelMessage(pull[0]) } else { @@ -1635,11 +1672,11 @@ 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, MessageSourceGuildChannel), *source) + m["message"] = ToFormattedMessage(bot.ConvertContentMessage(channelMsgByDB.Content, message.SourceGuildChannel), source) } - case MessageSourceGuildDirect: + case message.SourceGuildDirect: // todo(mrs4s): 支持 direct 消息 - m["tiny_id"] = fU64(source.SubID) + m["tiny_id"] = fU64(uint64(source.SecondaryID)) } return OK(m) } @@ -1678,12 +1715,12 @@ func (bot *CQBot) CQGetGroupMessageHistory(groupID int64, seq int64) global.MSG log.Warnf("获取群历史消息失败: %v", err) return Failed(100, "MESSAGES_API_ERROR", err.Error()) } - ms := make([]global.MSG, 0, len(msg)) + ms := make([]*event, 0, len(msg)) for _, m := range msg { bot.checkMedia(m.Elements, groupID) id := bot.InsertGroupMessage(m) t := bot.formatGroupMessage(m) - t["message_id"] = id + t.Others["message_id"] = id ms = append(ms, t) } return OK(global.MSG{ @@ -1737,7 +1774,7 @@ func (bot *CQBot) CQCanSendRecord() global.MSG { // @route(ocr_image,".ocr_image") // @rename(image_id->image) func (bot *CQBot) CQOcrImage(imageID string) global.MSG { - img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageID}, false, MessageSourceGroup) + img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageID}, false, message.SourceGroup) if err != nil { log.Warnf("load image error: %v", err) return Failed(100, "LOAD_FILE_ERROR", err.Error()) @@ -1777,12 +1814,10 @@ func (bot *CQBot) CQSetGroupAnonymousBan(groupID int64, flag string, duration in return Failed(100, "INVALID_FLAG", "无效的flag") } if g := bot.Client.FindGroup(groupID); g != nil { - s := strings.SplitN(flag, "|", 2) - if len(s) != 2 { + id, nick, ok := strings.Cut(flag, "|") + if !ok { return Failed(100, "INVALID_FLAG", "无效的flag") } - id := s[0] - nick := s[1] if err := g.MuteAnonymous(id, nick, duration); err != nil { log.Warnf("anonymous ban error: %v", err) return Failed(100, "CALL_API_ERROR", err.Error()) @@ -1939,6 +1974,15 @@ func (bot *CQBot) CQGetModelShow(model string) global.MSG { }) } +// CQSendGroupSign 群打卡 +// +// https://club.vip.qq.com/onlinestatus/set +// @route(send_group_sign) +func (bot *CQBot) CQSendGroupSign(groupID int64) global.MSG { + bot.Client.SendGroupSign(groupID) + return OK(nil) +} + // CQSetModelShow 设置在线机型 // // https://club.vip.qq.com/onlinestatus/set @@ -1969,6 +2013,27 @@ func (bot *CQBot) CQMarkMessageAsRead(msgID int32) global.MSG { return OK(nil) } +// CQSetQQProfile 设置 QQ 资料 +// +// @route(set_qq_profile) +func (bot *CQBot) CQSetQQProfile(nickname, company, email, college, personalNote gjson.Result) global.MSG { + u := client.NewProfileDetailUpdate() + + fi := func(f gjson.Result, do func(value string) client.ProfileDetailUpdate) { + if f.Exists() { + do(f.String()) + } + } + + fi(nickname, u.Nick) + fi(company, u.Company) + fi(email, u.Email) + fi(college, u.College) + fi(personalNote, u.PersonalNote) + bot.Client.UpdateProfile(u) + return OK(nil) +} + // CQReloadEventFilter 重载事件过滤器 // // @route(reload_event_filter) @@ -1984,8 +2049,7 @@ func OK(data interface{}) global.MSG { // Failed 生成失败返回值 func Failed(code int, msg ...string) global.MSG { - m := "" - w := "" + m, w := "", "" if len(msg) > 0 { m = msg[0] } diff --git a/coolq/bot.go b/coolq/bot.go index 099be8a..7723ea1 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -5,10 +5,9 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io" "os" - "path" "runtime/debug" + "strings" "sync" "time" @@ -16,6 +15,7 @@ import ( "github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/utils" + "github.com/RomiChan/syncx" "github.com/pkg/errors" "github.com/segmentio/asm/base64" log "github.com/sirupsen/logrus" @@ -32,16 +32,15 @@ type CQBot struct { lock sync.RWMutex events []func(*Event) - friendReqCache sync.Map - tempSessionCache sync.Map - nextTokenCache *utils.Cache + friendReqCache syncx.Map[string, *client.NewFriendRequest] + tempSessionCache syncx.Map[int64, *client.TempSessionInfo] + nextTokenCache *utils.Cache[*guildMemberPageToken] } // Event 事件 type Event struct { - RawMsg global.MSG - once sync.Once + Raw *event buffer *bytes.Buffer } @@ -49,7 +48,7 @@ func (e *Event) marshal() { if e.buffer == nil { e.buffer = global.NewBuffer() } - _ = json.NewEncoder(e.buffer).Encode(e.RawMsg) + _ = json.NewEncoder(e.buffer).Encode(e.Raw) } // JSONBytes return byes of json by lazy marshalling. @@ -69,40 +68,40 @@ func (e *Event) JSONString() string { func NewQQBot(cli *client.QQClient) *CQBot { bot := &CQBot{ Client: cli, - nextTokenCache: utils.NewCache(time.Second * 10), + nextTokenCache: utils.NewCache[*guildMemberPageToken](time.Second * 10), } - bot.Client.OnPrivateMessage(bot.privateMessageEvent) - bot.Client.OnGroupMessage(bot.groupMessageEvent) + bot.Client.PrivateMessageEvent.Subscribe(bot.privateMessageEvent) + bot.Client.GroupMessageEvent.Subscribe(bot.groupMessageEvent) if base.ReportSelfMessage { - bot.Client.OnSelfPrivateMessage(bot.privateMessageEvent) - bot.Client.OnSelfGroupMessage(bot.groupMessageEvent) + bot.Client.SelfPrivateMessageEvent.Subscribe(bot.privateMessageEvent) + bot.Client.SelfGroupMessageEvent.Subscribe(bot.groupMessageEvent) } - bot.Client.OnTempMessage(bot.tempMessageEvent) + bot.Client.TempMessageEvent.Subscribe(bot.tempMessageEvent) bot.Client.GuildService.OnGuildChannelMessage(bot.guildChannelMessageEvent) bot.Client.GuildService.OnGuildMessageReactionsUpdated(bot.guildMessageReactionsUpdatedEvent) bot.Client.GuildService.OnGuildMessageRecalled(bot.guildChannelMessageRecalledEvent) bot.Client.GuildService.OnGuildChannelUpdated(bot.guildChannelUpdatedEvent) bot.Client.GuildService.OnGuildChannelCreated(bot.guildChannelCreatedEvent) bot.Client.GuildService.OnGuildChannelDestroyed(bot.guildChannelDestroyedEvent) - bot.Client.OnGroupMuted(bot.groupMutedEvent) - bot.Client.OnGroupMessageRecalled(bot.groupRecallEvent) - bot.Client.OnGroupNotify(bot.groupNotifyEvent) - bot.Client.OnFriendNotify(bot.friendNotifyEvent) - bot.Client.OnMemberSpecialTitleUpdated(bot.memberTitleUpdatedEvent) - bot.Client.OnFriendMessageRecalled(bot.friendRecallEvent) - bot.Client.OnReceivedOfflineFile(bot.offlineFileEvent) - 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) - bot.Client.OnOtherClientStatusChanged(bot.otherClientStatusChangedEvent) - bot.Client.OnGroupDigest(bot.groupEssenceMsg) + bot.Client.GroupMuteEvent.Subscribe(bot.groupMutedEvent) + bot.Client.GroupMessageRecalledEvent.Subscribe(bot.groupRecallEvent) + bot.Client.GroupNotifyEvent.Subscribe(bot.groupNotifyEvent) + bot.Client.FriendNotifyEvent.Subscribe(bot.friendNotifyEvent) + bot.Client.MemberSpecialTitleUpdatedEvent.Subscribe(bot.memberTitleUpdatedEvent) + bot.Client.FriendMessageRecalledEvent.Subscribe(bot.friendRecallEvent) + bot.Client.OfflineFileEvent.Subscribe(bot.offlineFileEvent) + bot.Client.GroupJoinEvent.Subscribe(bot.joinGroupEvent) + bot.Client.GroupLeaveEvent.Subscribe(bot.leaveGroupEvent) + bot.Client.GroupMemberJoinEvent.Subscribe(bot.memberJoinEvent) + bot.Client.GroupMemberLeaveEvent.Subscribe(bot.memberLeaveEvent) + bot.Client.GroupMemberPermissionChangedEvent.Subscribe(bot.memberPermissionChangedEvent) + bot.Client.MemberCardUpdatedEvent.Subscribe(bot.memberCardUpdatedEvent) + bot.Client.NewFriendRequestEvent.Subscribe(bot.friendRequestEvent) + bot.Client.NewFriendEvent.Subscribe(bot.friendAddedEvent) + bot.Client.GroupInvitedEvent.Subscribe(bot.groupInvitedEvent) + bot.Client.UserWantJoinGroupEvent.Subscribe(bot.groupJoinReqEvent) + bot.Client.OtherClientStatusChangedEvent.Subscribe(bot.otherClientStatusChangedEvent) + bot.Client.GroupDigestEvent.Subscribe(bot.groupEssenceMsg) go func() { if base.HeartbeatInterval == 0 { log.Warn("警告: 心跳功能已关闭,若非预期,请检查配置文件。") @@ -111,13 +110,9 @@ func NewQQBot(cli *client.QQClient) *CQBot { t := time.NewTicker(base.HeartbeatInterval) for { <-t.C - bot.dispatchEventMessage(global.MSG{ - "time": time.Now().Unix(), - "self_id": bot.Client.Uin, - "post_type": "meta_event", - "meta_event_type": "heartbeat", - "status": bot.CQGetStatus()["data"], - "interval": base.HeartbeatInterval.Milliseconds(), + bot.dispatchEvent("meta_event/heartbeat", global.MSG{ + "status": bot.CQGetStatus()["data"], + "interval": base.HeartbeatInterval.Milliseconds(), }) } }() @@ -131,8 +126,24 @@ func (bot *CQBot) OnEventPush(f func(e *Event)) { bot.lock.Unlock() } -// UploadLocalImageAsGroup 上传本地图片至群聊 -func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElement) (i *message.GroupImageElement, err error) { +type worker struct { + wg sync.WaitGroup +} + +func (w *worker) do(f func()) { + w.wg.Add(1) + go func() { + defer w.wg.Done() + f() + }() +} + +func (w *worker) wait() { + w.wg.Wait() +} + +// uploadLocalImage 上传本地图片 +func (bot *CQBot) uploadLocalImage(target message.Source, img *LocalImageElement) (message.IMessageElement, error) { if img.File != "" { f, err := os.Open(img.File) if err != nil { @@ -144,89 +155,109 @@ func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElemen if lawful, mime := base.IsLawfulImage(img.Stream); !lawful { return nil, errors.New("image type error: " + mime) } - i, err = bot.Client.UploadGroupImage(groupCode, img.Stream) - if i != nil { + // todo: enable multi-thread upload, now got error code 81 + i, err := bot.Client.UploadImage(target, img.Stream, 4) + if err != nil { + return nil, err + } + switch i := i.(type) { + case *message.GroupImageElement: i.Flash = img.Flash i.EffectID = img.EffectID + case *message.FriendImageElement: + i.Flash = img.Flash } - return + return i, err } -// UploadLocalVideo 上传本地短视频至群聊 -func (bot *CQBot) UploadLocalVideo(target int64, v *LocalVideoElement) (*message.ShortVideoElement, error) { +// uploadLocalVideo 上传本地短视频至群聊 +func (bot *CQBot) uploadLocalVideo(target message.Source, v *LocalVideoElement) (*message.ShortVideoElement, error) { video, err := os.Open(v.File) if err != nil { return nil, err } defer func() { _ = video.Close() }() - hash, _ := utils.ComputeMd5AndLength(io.MultiReader(video, v.thumb)) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".cache") - _, _ = video.Seek(0, io.SeekStart) - _, _ = v.thumb.Seek(0, io.SeekStart) - return bot.Client.UploadGroupShortVideo(target, video, v.thumb, cacheFile) + return bot.Client.UploadShortVideo(target, video, v.thumb, 4) } -// UploadLocalImageAsPrivate 上传本地图片至私聊 -func (bot *CQBot) UploadLocalImageAsPrivate(userID int64, img *LocalImageElement) (i *message.FriendImageElement, err error) { - if img.File != "" { - f, err := os.Open(img.File) - if err != nil { - return nil, errors.Wrap(err, "open image error") +func removeLocalElement(elements []message.IMessageElement) []message.IMessageElement { + var j int + for i, e := range elements { + switch e.(type) { + case *LocalImageElement, *LocalVideoElement: + case *message.VoiceElement: // 未上传的语音消息, 也删除 + case nil: + default: + if j < i { + elements[j] = e + } + j++ } - defer func() { _ = f.Close() }() - img.Stream = f } - if lawful, mime := base.IsLawfulImage(img.Stream); !lawful { - return nil, errors.New("image type error: " + mime) - } - i, err = bot.Client.UploadPrivateImage(userID, img.Stream) - if i != nil { - i.Flash = img.Flash - } - return + return elements[:j] } -// UploadLocalImageAsGuildChannel 上传本地图片至频道 -func (bot *CQBot) UploadLocalImageAsGuildChannel(guildID, channelID uint64, img *LocalImageElement) (*message.GuildImageElement, error) { - if img.File != "" { - f, err := os.Open(img.File) - if err != nil { - return nil, errors.Wrap(err, "open image error") +const uploadFailedTemplate = "警告: %s %d %s上传失败: %v" + +func (bot *CQBot) uploadMedia(target message.Source, elements []message.IMessageElement) []message.IMessageElement { + var w worker + var source string + switch target.SourceType { // nolint:exhaustive + case message.SourceGroup: + source = "群" + case message.SourcePrivate: + source = "私聊" + case message.SourceGuildChannel: + source = "频道" + } + + for i, m := range elements { + p := &elements[i] + switch e := m.(type) { + case *LocalImageElement: + w.do(func() { + m, err := bot.uploadLocalImage(target, e) + if err != nil { + log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "图片", err) + } else { + *p = m + } + }) + case *message.VoiceElement: + w.do(func() { + m, err := bot.Client.UploadVoice(target, bytes.NewReader(e.Data)) + if err != nil { + log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "语音", err) + } else { + *p = m + } + }) + case *LocalVideoElement: + w.do(func() { + m, err := bot.uploadLocalVideo(target, e) + if err != nil { + log.Warnf(uploadFailedTemplate, source, target.PrimaryID, "视频", err) + } else { + *p = m + } + }) } - defer func() { _ = f.Close() }() - img.Stream = f } - if lawful, mime := base.IsLawfulImage(img.Stream); !lawful { - return nil, errors.New("image type error: " + mime) - } - return bot.Client.GuildService.UploadGuildImage(guildID, channelID, img.Stream) -} - -func (bot *CQBot) uploadGuildVideo(i *LocalVideoElement, guildID, channelID uint64) (*message.ShortVideoElement, error) { - video, err := os.Open(i.File) - if err != nil { - return nil, err - } - defer func() { _ = video.Close() }() - _, _ = video.Seek(0, io.SeekStart) - _, _ = i.thumb.Seek(0, io.SeekStart) - n, err := bot.Client.UploadGuildShortVideo(guildID, channelID, video, i.thumb) - return n, err + w.wait() + return removeLocalElement(elements) } // SendGroupMessage 发送群消息 func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) int32 { newElem := make([]message.IMessageElement, 0, len(m.Elements)) group := bot.Client.FindGroup(groupID) + source := message.Source{ + SourceType: message.SourceGroup, + PrimaryID: groupID, + } + m.Elements = bot.uploadMedia(source, m.Elements) for _, e := range m.Elements { switch i := e.(type) { - case *LocalImageElement, *message.VoiceElement, *LocalVideoElement: - i, err := bot.uploadMedia(i, groupID, true) - if err != nil { - log.Warnf("警告: 群 %d 消息%s上传失败: %v", groupID, e.Type().String(), err) - continue - } - e = i case *PokeElement: if group != nil { if mem := group.FindMember(i.Target); mem != nil { @@ -254,7 +285,7 @@ func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) int } m.Elements = newElem bot.checkMedia(newElem, groupID) - ret := bot.Client.SendGroupMessage(groupID, m, base.ForceFragmented) + ret := bot.Client.SendGroupMessage(groupID, m) if ret == nil || ret.Id == -1 { log.Warnf("群消息发送失败: 账号可能被风控.") return -1 @@ -265,15 +296,13 @@ func (bot *CQBot) SendGroupMessage(groupID int64, m *message.SendingMessage) int // SendPrivateMessage 发送私聊消息 func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.SendingMessage) int32 { newElem := make([]message.IMessageElement, 0, len(m.Elements)) + source := message.Source{ + SourceType: message.SourcePrivate, + PrimaryID: target, + } + m.Elements = bot.uploadMedia(source, m.Elements) for _, e := range m.Elements { switch i := e.(type) { - case *LocalImageElement, *message.VoiceElement, *LocalVideoElement: - i, err := bot.uploadMedia(i, target, false) - if err != nil { - log.Warnf("警告: 私聊 %d 消息%s上传失败: %v", target, e.Type().String(), err) - continue - } - e = i case *PokeElement: bot.Client.SendFriendPoke(i.Target) return 0 @@ -328,17 +357,19 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen default: if session == nil && groupID != 0 { msg := bot.Client.SendGroupTempMessage(groupID, target, m) + //lint:ignore SA9003 there is a todo if msg != nil { // nolint // todo(Mrs4s) // id = bot.InsertTempMessage(target, msg) } break } - msg, err := session.(*client.TempSessionInfo).SendMessage(m) + msg, err := session.SendMessage(m) if err != nil { log.Errorf("发送临时会话消息失败: %v", err) break } + //lint:ignore SA9003 there is a todo if msg != nil { // nolint // todo(Mrs4s) // id = bot.InsertTempMessage(target, msg) @@ -362,29 +393,19 @@ func (bot *CQBot) SendPrivateMessage(target int64, groupID int64, m *message.Sen // SendGuildChannelMessage 发送频道消息 func (bot *CQBot) SendGuildChannelMessage(guildID, channelID uint64, m *message.SendingMessage) string { newElem := make([]message.IMessageElement, 0, len(m.Elements)) + source := message.Source{ + SourceType: message.SourceGuildChannel, + PrimaryID: int64(guildID), + SecondaryID: int64(channelID), + } + m.Elements = bot.uploadMedia(source, m.Elements) for _, e := range m.Elements { switch i := e.(type) { - case *LocalImageElement: - n, err := bot.UploadLocalImageAsGuildChannel(guildID, channelID, i) - if err != nil { - log.Warnf("警告: 频道 %d 消息%s上传失败: %v", channelID, e.Type().String(), err) - continue - } - e = n - - case *LocalVideoElement: - n, err := bot.uploadGuildVideo(i, guildID, channelID) - if err != nil { - log.Warnf("警告: 频道 %d 消息%s上传失败: %v", channelID, e.Type().String(), err) - continue - } - e = n - case *message.MusicShareElement: bot.Client.SendGuildMusicShare(guildID, channelID, i) return "-1" // todo: fix this - case *LocalVoiceElement, *PokeElement: + case *message.VoiceElement, *PokeElement: log.Warnf("警告: 频道暂不支持发送 %v 消息", i.Type().String()) continue } @@ -523,7 +544,7 @@ 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, MessageSourceGuildChannel) + id := encodeGuildMessageID(m.GuildId, m.ChannelId, m.Id, message.SourceGuildChannel) msg := &db.StoredGuildChannelMessage{ ID: id, Attribute: &db.StoredGuildMessageAttribute{ @@ -544,15 +565,31 @@ func (bot *CQBot) InsertGuildChannelMessage(m *message.GuildChannelMessage) stri return msg.ID } -// Release 释放Bot实例 -func (bot *CQBot) Release() { +func (bot *CQBot) event(typ string, others global.MSG) *event { + ev := new(event) + post, detail, ok := strings.Cut(typ, "/") + ev.PostType = post + ev.DetailType = detail + if ok { + detail, sub, _ := strings.Cut(detail, "/") + ev.DetailType = detail + ev.SubType = sub + } + ev.Time = time.Now().Unix() + ev.SelfID = bot.Client.Uin + ev.Others = others + return ev } -func (bot *CQBot) dispatchEventMessage(m global.MSG) { +func (bot *CQBot) dispatchEvent(typ string, others global.MSG) { + bot.dispatch(bot.event(typ, others)) +} + +func (bot *CQBot) dispatch(ev *event) { bot.lock.RLock() defer bot.lock.RUnlock() - event := &Event{RawMsg: m} + event := &Event{Raw: ev} wg := sync.WaitGroup{} wg.Add(len(bot.events)) for _, f := range bot.events { @@ -560,7 +597,7 @@ func (bot *CQBot) dispatchEventMessage(m global.MSG) { defer func() { wg.Done() if pan := recover(); pan != nil { - log.Warnf("处理事件 %v 时出现错误: %v \n%s", m, pan, debug.Stack()) + log.Warnf("处理事件 %v 时出现错误: %v \n%s", event.JSONString(), pan, debug.Stack()) } }() @@ -589,24 +626,6 @@ func formatMemberName(mem *client.GroupMemberInfo) string { return fmt.Sprintf("%s(%d)", mem.DisplayName(), mem.Uin) } -func (bot *CQBot) uploadMedia(raw message.IMessageElement, target int64, group bool) (message.IMessageElement, error) { - switch m := raw.(type) { - case *LocalImageElement: - if group { - return bot.UploadLocalImageAsGroup(target, m) - } - return bot.UploadLocalImageAsPrivate(target, m) - case *message.VoiceElement: - if group { - return bot.Client.UploadGroupPtt(target, bytes.NewReader(m.Data)) - } - return bot.Client.UploadPrivatePtt(target, bytes.NewReader(m.Data)) - case *LocalVideoElement: - return bot.UploadLocalVideo(target, m) - } - return nil, errors.New("unsupported message element type") -} - // encodeMessageID 临时先这样, 暂时用不上 func encodeMessageID(target int64, seq int32) string { return hex.EncodeToString(binary.NewWriterF(func(w *binary.Writer) { @@ -618,7 +637,7 @@ func encodeMessageID(target int64, seq int32) string { // encodeGuildMessageID 将频道信息编码为字符串 // 当信息来源为 Channel 时 primaryID 为 guildID , subID 为 channelID // 当信息来源为 Direct 时 primaryID 为 guildID , subID 为 tinyID -func encodeGuildMessageID(primaryID, subID, seq uint64, source MessageSourceType) string { +func encodeGuildMessageID(primaryID, subID, seq uint64, source message.SourceType) string { return base64.StdEncoding.EncodeToString(binary.NewWriterF(func(w *binary.Writer) { w.WriteByte(byte(source)) w.WriteUInt64(primaryID) @@ -627,16 +646,16 @@ func encodeGuildMessageID(primaryID, subID, seq uint64, source MessageSourceType })) } -func decodeGuildMessageID(id string) (source *MessageSource, seq uint64) { +func decodeGuildMessageID(id string) (source message.Source, seq uint64) { b, _ := base64.StdEncoding.DecodeString(id) if len(b) < 25 { return } r := binary.NewReader(b) - source = &MessageSource{ - SourceType: MessageSourceType(r.ReadByte()), - PrimaryID: uint64(r.ReadInt64()), - SubID: uint64(r.ReadInt64()), + source = message.Source{ + SourceType: message.SourceType(r.ReadByte()), + PrimaryID: r.ReadInt64(), + SecondaryID: r.ReadInt64(), } seq = uint64(r.ReadInt64()) return diff --git a/coolq/converter.go b/coolq/converter.go index 44bc17c..d185b64 100644 --- a/coolq/converter.go +++ b/coolq/converter.go @@ -2,6 +2,7 @@ package coolq import ( "strconv" + "strings" "github.com/Mrs4s/MiraiGo/topic" @@ -13,41 +14,35 @@ import ( ) func convertGroupMemberInfo(groupID int64, m *client.GroupMemberInfo) global.MSG { + sex := "unknown" + if m.Gender == 1 { // unknown = 0xff + sex = "female" + } else if m.Gender == 0 { + sex = "male" + } + role := "member" + switch m.Permission { // nolint:exhaustive + case client.Owner: + role = "owner" + case client.Administrator: + role = "admin" + } return global.MSG{ - "group_id": groupID, - "user_id": m.Uin, - "nickname": m.Nickname, - "card": m.CardName, - "sex": func() string { - if m.Gender == 1 { - return "female" - } else if m.Gender == 0 { - return "male" - } - // unknown = 0xff - return "unknown" - }(), + "group_id": groupID, + "user_id": m.Uin, + "nickname": m.Nickname, + "card": m.CardName, + "sex": sex, "age": 0, "area": "", "join_time": m.JoinTime, "last_sent_time": m.LastSpeakTime, "shut_up_timestamp": m.ShutUpTimestamp, "level": strconv.FormatInt(int64(m.Level), 10), - "role": func() string { - switch m.Permission { - case client.Owner: - return "owner" - case client.Administrator: - return "admin" - case client.Member: - return "member" - default: - return "member" - } - }(), + "role": role, "unfriendly": false, "title": m.SpecialTitle, - "title_expire_time": m.SpecialTitleExpireTime, + "title_expire_time": 0, "card_changeable": false, } } @@ -65,27 +60,24 @@ func convertGuildMemberInfo(m []*client.GuildMemberInfo) (r []global.MSG) { return } -func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) global.MSG { - source := MessageSource{ - SourceType: MessageSourceGroup, - PrimaryID: uint64(m.GroupCode), +func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) *event { + source := message.Source{ + SourceType: message.SourceGroup, + PrimaryID: m.GroupCode, + } + cqm := toStringMessage(m.Elements, source) + typ := "message/group/normal" + if m.Sender.Uin == bot.Client.Uin { + typ = "message_sent/group/normal" } - cqm := ToStringMessage(m.Elements, source, true) gm := global.MSG{ "anonymous": nil, "font": 0, "group_id": m.GroupCode, - "message": ToFormattedMessage(m.Elements, source, false), + "message": ToFormattedMessage(m.Elements, source), "message_type": "group", "message_seq": m.Id, - "post_type": func() string { - if m.Sender.Uin == bot.Client.Uin { - return "message_sent" - } - return "message" - }(), - "raw_message": cqm, - "self_id": bot.Client.Uin, + "raw_message": cqm, "sender": global.MSG{ "age": 0, "area": "", @@ -93,9 +85,7 @@ func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) global.MSG { "sex": "unknown", "user_id": m.Sender.Uin, }, - "sub_type": "normal", - "time": m.Time, - "user_id": m.Sender.Uin, + "user_id": m.Sender.Uin, } if m.Sender.IsAnonymous() { gm["anonymous"] = global.MSG{ @@ -122,21 +112,21 @@ func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) global.MSG { } } ms := gm["sender"].(global.MSG) - switch mem.Permission { + role := "member" + switch mem.Permission { // nolint:exhaustive case client.Owner: - ms["role"] = "owner" + role = "owner" case client.Administrator: - ms["role"] = "admin" - case client.Member: - ms["role"] = "member" - default: - ms["role"] = "member" + role = "admin" } + ms["role"] = role ms["nickname"] = mem.Nickname ms["card"] = mem.CardName ms["title"] = mem.SpecialTitle } - return gm + ev := bot.event(typ, gm) + ev.Time = int64(m.Time) + return ev } func convertChannelInfo(c *client.ChannelInfo) global.MSG { @@ -220,6 +210,15 @@ func convertReactions(reactions []*message.GuildMessageEmojiReaction) (r []globa return } +func toStringMessage(m []message.IMessageElement, source message.Source) string { + elems := toElements(m, source) + var sb strings.Builder + for _, elem := range elems { + elem.WriteCQCodeTo(&sb) + } + return sb.String() +} + func fU64(v uint64) string { return strconv.FormatUint(v, 10) } diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 80d5913..17b7777 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/md5" "encoding/hex" - xml2 "encoding/xml" "errors" "fmt" "io" @@ -23,6 +22,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" + "github.com/Mrs4s/go-cqhttp/coolq/cqcode" "github.com/Mrs4s/go-cqhttp/db" "github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/internal/base" @@ -45,43 +45,18 @@ type PokeElement struct { type LocalImageElement struct { Stream io.ReadSeeker File string + URL string Flash bool EffectID int32 } -// LocalVoiceElement 本地语音 -type LocalVoiceElement struct { - message.VoiceElement - Stream io.ReadSeeker -} - // LocalVideoElement 本地视频 type LocalVideoElement struct { File string thumb io.ReadSeeker } -// MessageSource 消息来源 -// 如果为私聊或者群聊, PrimaryID 将代表群号/QQ号 -// 如果为频道, PrimaryID 为 GuildID, SubID 为 ChannelID -type MessageSource struct { - SourceType MessageSourceType - PrimaryID uint64 - SubID uint64 -} - -// MessageSourceType 消息来源类型 -type MessageSourceType byte - -// MessageSourceType 常量 -const ( - MessageSourcePrivate MessageSourceType = 1 << iota - MessageSourceGroup - MessageSourceGuildChannel - MessageSourceGuildDirect -) - const ( maxImageSize = 1024 * 1024 * 30 // 30MB maxVideoSize = 1024 * 1024 * 100 // 100MB @@ -103,43 +78,54 @@ func (e *PokeElement) Type() message.ElementType { return message.At } -// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 -func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []global.MSG) { - r = make([]global.MSG, 0, len(e)) +func replyID(r *message.ReplyElement, source message.Source) int32 { + id := source.PrimaryID + seq := r.ReplySeq + if source.SourceType == message.SourcePrivate { + // 私聊似乎腾讯服务器有bug? + seq = int32(uint16(seq)) + id = r.Sender + } + if r.GroupID != 0 { + id = r.GroupID + } + return db.ToGlobalID(id, seq) +} + +// toElements 将消息元素数组转为MSG数组以用于消息上报 +// +// nolint:govet +func toElements(e []message.IMessageElement, source message.Source) (r []cqcode.Element) { + type pair = cqcode.Pair // simplify code + type pairs = []pair + + r = make([]cqcode.Element, 0, len(e)) m := &message.SendingMessage{Elements: e} reply := m.FirstOrNil(func(e message.IMessageElement) bool { _, ok := e.(*message.ReplyElement) return ok }) - if reply != nil && source.SourceType&(MessageSourceGroup|MessageSourcePrivate) != 0 { + if reply != nil && source.SourceType&(message.SourceGroup|message.SourcePrivate) != 0 { replyElem := reply.(*message.ReplyElement) - rid := int64(source.PrimaryID) - if rid == 0 { - rid = replyElem.Sender - } - if replyElem.GroupID != 0 { - rid = replyElem.GroupID + id := replyID(replyElem, source) + elem := cqcode.Element{ + Type: "reply", + Data: pairs{ + {K: "id", V: strconv.FormatInt(int64(id), 10)}, + }, } if base.ExtraReplyData { - r = append(r, global.MSG{ - "type": "reply", - "data": map[string]string{ - "id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10), - "seq": strconv.FormatInt(int64(replyElem.ReplySeq), 10), - "qq": strconv.FormatInt(replyElem.Sender, 10), - "time": strconv.FormatInt(int64(replyElem.Time), 10), - "text": ToStringMessage(replyElem.Elements, source), - }, - }) - } else { - r = append(r, global.MSG{ - "type": "reply", - "data": map[string]string{"id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10)}, - }) + elem.Data = append(elem.Data, + pair{K: "seq", V: strconv.FormatInt(int64(replyElem.ReplySeq), 10)}, + pair{K: "qq", V: strconv.FormatInt(replyElem.Sender, 10)}, + pair{K: "time", V: strconv.FormatInt(int64(replyElem.Time), 10)}, + pair{K: "text", V: toStringMessage(replyElem.Elements, source)}, + ) } + r = append(r, elem) } for i, elem := range e { - var m global.MSG + var m cqcode.Element switch o := elem.(type) { case *message.ReplyElement: if base.RemoveReplyAt && i+1 < len(e) { @@ -148,234 +134,155 @@ func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []glob e[i+1] = nil } } + continue case *message.TextElement: - m = global.MSG{ - "type": "text", - "data": map[string]string{"text": o.Content}, + m = cqcode.Element{ + Type: "text", + Data: pairs{ + {K: "text", V: o.Content}, + }, } case *message.LightAppElement: - m = global.MSG{ - "type": "json", - "data": map[string]string{"data": o.Content}, + m = cqcode.Element{ + Type: "json", + Data: pairs{ + {K: "data", V: o.Content}, + }, } case *message.AtElement: - if o.Target == 0 { - m = global.MSG{ - "type": "at", - "data": map[string]string{"qq": "all"}, - } - } else { - m = global.MSG{ - "type": "at", - "data": map[string]string{"qq": strconv.FormatUint(uint64(o.Target), 10)}, - } + target := "all" + if o.Target != 0 { + target = strconv.FormatUint(uint64(o.Target), 10) + } + m = cqcode.Element{ + Type: "at", + Data: pairs{ + {K: "qq", V: target}, + }, } case *message.RedBagElement: - m = global.MSG{ - "type": "redbag", - "data": map[string]string{"title": o.Title}, + m = cqcode.Element{ + Type: "redbag", + Data: pairs{ + {K: "title", V: o.Title}, + }, } case *message.ForwardElement: - m = global.MSG{ - "type": "forward", - "data": map[string]string{"id": o.ResId}, + m = cqcode.Element{ + Type: "forward", + Data: pairs{ + {K: "id", V: o.ResId}, + }, } case *message.FaceElement: - m = global.MSG{ - "type": "face", - "data": map[string]string{"id": strconv.FormatInt(int64(o.Index), 10)}, + m = cqcode.Element{ + Type: "face", + Data: pairs{ + {K: "id", V: strconv.FormatInt(int64(o.Index), 10)}, + }, } case *message.VoiceElement: - m = global.MSG{ - "type": "record", - "data": map[string]string{"file": o.Name, "url": o.Url}, + m = cqcode.Element{ + Type: "record", + Data: pairs{ + {K: "file", V: o.Name}, + {K: "url", V: o.Url}, + }, } case *message.ShortVideoElement: - m = global.MSG{ - "type": "video", - "data": map[string]string{"file": o.Name, "url": o.Url}, + m = cqcode.Element{ + Type: "video", + Data: pairs{ + {K: "file", V: o.Name}, + {K: "url", V: o.Url}, + }, } case *message.GroupImageElement: - data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url, "subType": strconv.FormatInt(int64(o.ImageBizType), 10)} + data := pairs{ + {K: "file", V: hex.EncodeToString(o.Md5) + ".image"}, + {K: "subType", V: strconv.FormatInt(int64(o.ImageBizType), 10)}, + {K: "url", V: o.Url}, + } switch { case o.Flash: - data["type"] = "flash" + data = append(data, pair{K: "type", V: "flash"}) case o.EffectID != 0: - data["type"] = "show" - data["id"] = strconv.FormatInt(int64(o.EffectID), 10) + data = append(data, pair{K: "type", V: "show"}) + data = append(data, pair{K: "id", V: strconv.FormatInt(int64(o.EffectID), 10)}) } - m = global.MSG{ - "type": "image", - "data": data, + m = cqcode.Element{ + Type: "image", + Data: data, } case *message.GuildImageElement: - data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url} - m = global.MSG{ - "type": "image", - "data": data, + data := pairs{ + {K: "file", V: hex.EncodeToString(o.Md5) + ".image"}, + {K: "url", V: o.Url}, + } + m = cqcode.Element{ + Type: "image", + Data: data, } case *message.FriendImageElement: - data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url} - if o.Flash { - data["type"] = "flash" + data := pairs{ + {K: "file", V: hex.EncodeToString(o.Md5) + ".image"}, + {K: "url", V: o.Url}, } - m = global.MSG{ - "type": "image", - "data": data, + if o.Flash { + data = append(data, pair{K: "type", V: "flash"}) + } + m = cqcode.Element{ + Type: "image", + Data: data, } case *message.DiceElement: - m = global.MSG{ - "type": "dice", - "data": map[string]string{"value": fmt.Sprint(o.Value)}, + m = cqcode.Element{ + Type: "dice", + Data: pairs{ + {K: "value", V: strconv.FormatInt(int64(o.Value), 10)}, + }, + } + case *message.FingerGuessingElement: + m = cqcode.Element{ + Type: "rps", + Data: pairs{ + {K: "value", V: strconv.FormatInt(int64(o.Value), 10)}, + }, } case *message.MarketFaceElement: - m = global.MSG{ - "type": "text", - "data": map[string]string{"text": o.Name}, + m = cqcode.Element{ + Type: "text", + Data: pairs{ + {K: "text", V: o.Name}, + }, } case *message.ServiceElement: - if isOk := strings.Contains(o.Content, " i+1 { - elem, ok := e[i+1].(*message.AtElement) - if ok && elem.Target == o.Sender { - e[i+1] = nil - } - } - case *message.TextElement: - sb.WriteString(CQCodeEscapeText(o.Content)) - case *message.AtElement: - if o.Target == 0 { - write("[CQ:at,qq=all]") - continue - } - write("[CQ:at,qq=%d]", uint64(o.Target)) - case *message.RedBagElement: - write("[CQ:redbag,title=%s]", o.Title) - case *message.ForwardElement: - write("[CQ:forward,id=%s]", o.ResId) - case *message.FaceElement: - write(`[CQ:face,id=%d]`, o.Index) - case *message.VoiceElement: - if ur { - write(`[CQ:record,file=%s]`, o.Name) - } else { - write(`[CQ:record,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url)) - } - case *message.ShortVideoElement: - if ur { - write(`[CQ:video,file=%s]`, o.Name) - } else { - write(`[CQ:video,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url)) - } - case *message.GroupImageElement: - var arg string - if o.Flash { - arg = ",type=flash" - } else if o.EffectID != 0 { - arg = ",type=show,id=" + strconv.FormatInt(int64(o.EffectID), 10) - } - arg += ",subType=" + strconv.FormatInt(int64(o.ImageBizType), 10) - if ur { - write("[CQ:image,file=%s%s]", hex.EncodeToString(o.Md5)+".image", arg) - } else { - write("[CQ:image,file=%s,url=%s%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url), arg) - } - case *message.FriendImageElement: - var arg string - if o.Flash { - arg = ",type=flash" - } - if ur { - write("[CQ:image,file=%s%s]", hex.EncodeToString(o.Md5)+".image", arg) - } else { - write("[CQ:image,file=%s,url=%s%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url), arg) - } - case *message.GuildImageElement: - write("[CQ:image,file=%s,url=%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url)) - case *message.DiceElement: - write("[CQ:dice,value=%v]", o.Value) - case *message.MarketFaceElement: - sb.WriteString(o.Name) - case *message.ServiceElement: - if isOk := strings.Contains(o.Content, " 0 { if base.SplitURL { - for _, txt := range param.SplitURL(CQCodeUnescapeText(raw[:i])) { + for _, txt := range param.SplitURL(cqcode.UnescapeText(raw[:i])) { r = append(r, message.NewText(txt)) } } else { - r = append(r, message.NewText(CQCodeUnescapeText(raw[:i]))) + r = append(r, message.NewText(cqcode.UnescapeText(raw[:i]))) } } @@ -659,7 +566,7 @@ func (bot *CQBot) ConvertStringMessage(raw string, sourceType MessageSourceType) if i+1 > len(raw) { return } - d[key] = CQCodeUnescapeValue(raw[:i]) + d[key] = cqcode.UnescapeValue(raw[:i]) raw = raw[i:] i = 0 } @@ -668,11 +575,11 @@ func (bot *CQBot) ConvertStringMessage(raw string, sourceType MessageSourceType) } // ConvertObjectMessage 将消息JSON对象转为消息元素数组 -func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType MessageSourceType) (r []message.IMessageElement) { +func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType message.SourceType) (r []message.IMessageElement) { d := make(map[string]string) convertElem := func(e gjson.Result) { t := e.Get("type").Str - if t == "reply" && sourceType&(MessageSourceGroup|MessageSourcePrivate) != 0 { + if t == "reply" && sourceType&(message.SourceGroup|message.SourcePrivate) != 0 { if len(r) > 0 { if _, ok := r[0].(*message.ReplyElement); ok { log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") @@ -788,14 +695,20 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType MessageSourceT } // ConvertContentMessage 将数据库用的 content 转换为消息元素数组 -func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType MessageSourceType) (r []message.IMessageElement) { +func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) { for _, c := range content { data := c["data"].(global.MSG) switch c["type"] { case "text": r = append(r, message.NewText(data["text"].(string))) case "image": - e, err := bot.makeImageOrVideoElem(map[string]string{"file": data["file"].(string)}, false, sourceType) + u, ok := data["url"] + d := make(map[string]string, 2) + if ok { + d["url"] = u.(string) + } + d["file"] = data["file"].(string) + e, err := bot.makeImageOrVideoElem(d, false, sourceType) if err != nil { log.Warnf("make image elem error: %v", err) continue @@ -861,7 +774,7 @@ func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType Message // 返回 interface{} 存在三种类型 // // message.IMessageElement []message.IMessageElement nil -func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSourceType) (m interface{}, err error) { +func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.SourceType) (m interface{}, err error) { switch t { case "text": if base.SplitURL { @@ -921,9 +834,6 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou case "record": f := d["file"] data, err := global.FindFile(f, d["cache"], global.VoicePath) - if err == global.ErrSyntax { - data, err = global.FindFile(f, d["cache"], global.VoicePathOld) - } if err != nil { return nil, err } @@ -932,8 +842,6 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou if !lawful { return nil, errors.New("audio type error: " + mt) } - } - if !global.IsAMRorSILK(data) { data, err = global.EncoderSilk(data) if err != nil { return nil, err @@ -954,7 +862,10 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou if qq == "all" { return message.AtAll(), nil } - t, _ := strconv.ParseInt(qq, 10, 64) + t, err := strconv.ParseInt(qq, 10, 64) + if err != nil { + return nil, err + } name := strings.TrimSpace(d["name"]) if len(name) > 0 { name = "@" + name @@ -1041,7 +952,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou }, nil } xml := fmt.Sprintf(``, - XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) + utils.XmlEscape(d["title"]), d["url"], d["image"], d["audio"], utils.XmlEscape(d["title"]), utils.XmlEscape(d["content"])) return &message.ServiceElement{ Id: 60, Content: xml, @@ -1056,9 +967,16 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou return nil, errors.New("invalid dice value " + value) } return message.NewDice(int32(i)), nil + case "rps": + value := d["value"] + i, _ := strconv.ParseInt(value, 10, 64) + if i < 0 || i > 2 { + return nil, errors.New("invalid finger-guessing value " + value) + } + return message.NewFingerGuessing(int32(i)), nil case "xml": resID := d["resid"] - template := CQCodeEscapeValue(d["data"]) + template := cqcode.EscapeValue(d["data"]) i, _ := strconv.ParseInt(resID, 10, 64) msg := message.NewRichXml(template, i) return msg, nil @@ -1092,7 +1010,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou if err != nil { return nil, errors.New("send cardimage faild") } - return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == MessageSourceGroup) + return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == message.SourceGroup) case "video": file, err := bot.makeImageOrVideoElem(d, true, sourceType) if err != nil { @@ -1109,33 +1027,28 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou if cover, ok := d["cover"]; ok { data, _ = global.FindFile(cover, d["cache"], global.ImagePath) } else { - _ = global.ExtractCover(v.File, v.File+".jpg") + err = global.ExtractCover(v.File, v.File+".jpg") + if err != nil { + return nil, err + } data, _ = os.ReadFile(v.File + ".jpg") } v.thumb = bytes.NewReader(data) video, _ := os.Open(v.File) defer video.Close() - _, err = video.Seek(4, io.SeekStart) - if err != nil { - return nil, err - } + _, _ = video.Seek(4, io.SeekStart) header := make([]byte, 4) - _, err = video.Read(header) - if err != nil { - return nil, err - } + _, _ = video.Read(header) if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp _, _ = video.Seek(0, io.SeekStart) hash, _ := utils.ComputeMd5AndLength(video) cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".mp4") - if global.PathExists(cacheFile) && (d["cache"] == "" || d["cache"] == "1") { - goto ok + if !(d["cache"] == "" || d["cache"] == "1") || !global.PathExists(cacheFile) { + err = global.EncodeMP4(v.File, cacheFile) + if err != nil { + return nil, err + } } - err = global.EncodeMP4(v.File, cacheFile) - if err != nil { - return nil, err - } - ok: v.File = cacheFile } return v, nil @@ -1144,112 +1057,13 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSou } } -// XMLEscape 将字符串c转义为XML字符串 -func XMLEscape(c string) string { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - _ = xml2.EscapeText(buf, utils.S2B(c)) - return buf.String() -} - -/*CQCodeEscapeText 将字符串raw中部分字符转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeEscapeText(s string) string { - count := strings.Count(s, "&") - count += strings.Count(s, "[") - count += strings.Count(s, "]") - if count == 0 { - return s - } - - // Apply replacements to buffer. - var b strings.Builder - b.Grow(len(s) + count*4) - start := 0 - for i := 0; i < count; i++ { - j := start - for index, r := range s[start:] { - if r == '&' || r == '[' || r == ']' { - j += index - break - } - } - b.WriteString(s[start:j]) - switch s[j] { - case '&': - b.WriteString("&") - case '[': - b.WriteString("[") - case ']': - b.WriteString("]") - } - start = j + 1 - } - b.WriteString(s[start:]) - return b.String() -} - -/*CQCodeEscapeValue 将字符串value中部分字符转义 - -, -> , - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeEscapeValue(value string) string { - ret := CQCodeEscapeText(value) - ret = strings.ReplaceAll(ret, ",", ",") - return ret -} - -/*CQCodeUnescapeText 将字符串content中部分字符反转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeUnescapeText(content string) string { - ret := content - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - ret = strings.ReplaceAll(ret, "&", "&") - return ret -} - -/*CQCodeUnescapeValue 将字符串content中部分字符反转义 - -, -> , - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeUnescapeValue(content string) string { - ret := strings.ReplaceAll(content, ",", ",") - ret = CQCodeUnescapeText(ret) - return ret -} - // makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 -func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceType MessageSourceType) (message.IMessageElement, error) { +func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceType message.SourceType) (message.IMessageElement, error) { f := d["file"] + u, ok := d["url"] + if !ok { + u = "" + } if strings.HasPrefix(f, "http") { hash := md5.Sum([]byte(f)) cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") @@ -1272,7 +1086,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy if video { return &LocalVideoElement{File: cacheFile}, nil } - return &LocalImageElement{File: cacheFile}, nil + return &LocalImageElement{File: cacheFile, URL: f}, nil } if strings.HasPrefix(f, "file") { fu, err := url.Parse(f) @@ -1298,14 +1112,14 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy if info.Size() == 0 || info.Size() >= maxImageSize { return nil, errors.New("invalid image size") } - return &LocalImageElement{File: fu.Path}, nil + return &LocalImageElement{File: fu.Path, URL: f}, nil } - if strings.HasPrefix(f, "base64") && !video { + if !video && strings.HasPrefix(f, "base64") { b, err := param.Base64DecodeString(strings.TrimPrefix(f, "base64://")) if err != nil { return nil, err } - return &LocalImageElement{Stream: bytes.NewReader(b)}, nil + return &LocalImageElement{Stream: bytes.NewReader(b), URL: f}, nil } rawPath := path.Join(global.ImagePath, f) if video { @@ -1328,7 +1142,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy return bot.readVideoCache(b), nil } // 目前频道内上传的图片均无法被查询到, 需要单独处理 - if sourceType == MessageSourceGuildChannel { + if sourceType == message.SourceGuildChannel { cacheFile := path.Join(global.ImagePath, "guild-images", f) if global.PathExists(cacheFile) { return &LocalImageElement{File: cacheFile}, nil @@ -1343,10 +1157,6 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy } } exist := global.PathExists(rawPath) - if !exist && global.PathExists(path.Join(global.ImagePathOld, f)) { - exist = true - rawPath = path.Join(global.ImagePathOld, f) - } if !exist { if d["url"] != "" { return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, sourceType) @@ -1354,7 +1164,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy return nil, errors.New("invalid image") } if path.Ext(rawPath) != ".image" { - return &LocalImageElement{File: rawPath}, nil + return &LocalImageElement{File: rawPath, URL: u}, nil } b, err := os.ReadFile(rawPath) if err != nil { @@ -1363,7 +1173,7 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy return bot.readImageCache(b, sourceType) } -func (bot *CQBot) readImageCache(b []byte, sourceType MessageSourceType) (message.IMessageElement, error) { +func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType) (message.IMessageElement, error) { var err error if len(b) < 20 { return nil, errors.New("invalid cache") @@ -1373,38 +1183,27 @@ func (bot *CQBot) readImageCache(b []byte, sourceType MessageSourceType) (messag size := r.ReadInt32() r.ReadString() imageURL := r.ReadString() - if size == 0 { - if imageURL != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) - } - return nil, errors.New("img size is 0") - } - if len(hash) != 16 { - return nil, errors.New("invalid hash") + if size == 0 && imageURL != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) } var rsp message.IMessageElement - if sourceType == MessageSourceGroup { + switch sourceType { // nolint:exhaustive + case message.SourceGroup: rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - goto ok - } - if sourceType == MessageSourceGuildChannel { + case message.SourceGuildChannel: if len(bot.Client.GuildService.Guilds) == 0 { err = errors.New("cannot query guild image: not any joined guild") - goto ok + break } guild := bot.Client.GuildService.Guilds[0] rsp, err = bot.Client.GuildService.QueryImage(guild.GuildId, guild.Channels[0].ChannelId, hash, uint64(size)) - goto ok + default: + rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) } - rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) -ok: - if err != nil { - if imageURL != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) - } - return nil, err + if err != nil && imageURL != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) } - return rsp, nil + return rsp, err } func (bot *CQBot) readVideoCache(b []byte) message.IMessageElement { @@ -1426,9 +1225,13 @@ func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, brief if brief == "" { brief = "[分享]我看到一张很赞的图片,分享给你,快来看!" } - if _, ok := elem.(*LocalImageElement); ok { + if local, ok := elem.(*LocalImageElement); ok { r := rand.Uint32() - e, err := bot.uploadMedia(elem, int64(r), group) + typ := message.SourceGroup + if !group { + typ = message.SourcePrivate + } + e, err := bot.uploadLocalImage(message.Source{SourceType: typ, PrimaryID: int64(r)}, local) if err != nil { log.Warnf("警告: 图片上传失败: %v", err) return nil, err diff --git a/coolq/cqcode/element.go b/coolq/cqcode/element.go new file mode 100644 index 0000000..2605d5a --- /dev/null +++ b/coolq/cqcode/element.go @@ -0,0 +1,67 @@ +package cqcode + +import ( + "bytes" + "strconv" + "strings" + + "github.com/Mrs4s/MiraiGo/binary" +) + +// Element single message +type Element struct { + Type string + Data []Pair +} + +// Pair key value pair +type Pair struct { + K string + V string +} + +// CQCode convert element to cqcode +func (e *Element) CQCode() string { + buf := strings.Builder{} + e.WriteCQCodeTo(&buf) + return buf.String() +} + +// WriteCQCodeTo write element's cqcode into sb +func (e *Element) WriteCQCodeTo(sb *strings.Builder) { + if e.Type == "text" { + sb.WriteString(EscapeText(e.Data[0].V)) // must be {"text": value} + return + } + sb.WriteString("[CQ:") + sb.WriteString(e.Type) + for _, data := range e.Data { + sb.WriteByte(',') + sb.WriteString(data.K) + sb.WriteByte('=') + sb.WriteString(EscapeValue(data.V)) + } + sb.WriteByte(']') +} + +// MarshalJSON see encoding/json.Marshaler +func (e *Element) MarshalJSON() ([]byte, error) { + return binary.NewWriterF(func(w *binary.Writer) { + buf := (*bytes.Buffer)(w) + // fmt.Fprintf(buf, `{"type":"%s","data":{`, e.Type) + buf.WriteString(`{"type":"`) + buf.WriteString(e.Type) + buf.WriteString(`","data":{`) + for i, data := range e.Data { + if i != 0 { + buf.WriteByte(',') + } + // fmt.Fprintf(buf, `"%s":%q`, data.K, data.V) + buf.WriteByte('"') + buf.WriteString(data.K) + buf.WriteString(`":`) + buf.WriteString(strconv.Quote(data.V)) + } + buf.WriteString(`}}`) + }), nil +} diff --git a/coolq/cqcode/escape.go b/coolq/cqcode/escape.go new file mode 100644 index 0000000..cee9033 --- /dev/null +++ b/coolq/cqcode/escape.go @@ -0,0 +1,79 @@ +// Package cqcode provides CQCode util functions. +package cqcode + +import "strings" + +// EscapeText 将字符串raw中部分字符转义 +// +// - & -> & +// - [ -> [ +// - ] -> ] +func EscapeText(s string) string { + count := strings.Count(s, "&") + count += strings.Count(s, "[") + count += strings.Count(s, "]") + if count == 0 { + return s + } + + // Apply replacements to buffer. + var b strings.Builder + b.Grow(len(s) + count*4) + start := 0 + for i := 0; i < count; i++ { + j := start + for index, r := range s[start:] { + if r == '&' || r == '[' || r == ']' { + j += index + break + } + } + b.WriteString(s[start:j]) + switch s[j] { + case '&': + b.WriteString("&") + case '[': + b.WriteString("[") + case ']': + b.WriteString("]") + } + start = j + 1 + } + b.WriteString(s[start:]) + return b.String() +} + +// EscapeValue 将字符串value中部分字符转义 +// +// - , -> , +// - & -> & +// - [ -> [ +// - ] -> ] +func EscapeValue(value string) string { + ret := EscapeText(value) + return strings.ReplaceAll(ret, ",", ",") +} + +// UnescapeText 将字符串content中部分字符反转义 +// +// - & -> & +// - [ -> [ +// - ] -> ] +func UnescapeText(content string) string { + ret := content + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + ret = strings.ReplaceAll(ret, "&", "&") + return ret +} + +// UnescapeValue 将字符串content中部分字符反转义 +// +// - , -> , +// - & -> & +// - [ -> [ +// - ] -> ] +func UnescapeValue(content string) string { + ret := strings.ReplaceAll(content, ",", ",") + return UnescapeText(ret) +} diff --git a/coolq/cqcode_test.go b/coolq/cqcode_test.go index 338ca8f..30c1636 100644 --- a/coolq/cqcode_test.go +++ b/coolq/cqcode_test.go @@ -5,15 +5,18 @@ import ( "strings" "testing" + "github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/utils" "github.com/stretchr/testify/assert" "github.com/tidwall/gjson" + + "github.com/Mrs4s/go-cqhttp/coolq/cqcode" ) var bot = CQBot{} func TestCQBot_ConvertStringMessage(t *testing.T) { - for _, v := range bot.ConvertStringMessage(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`, MessageSourcePrivate) { + for _, v := range bot.ConvertStringMessage(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`, message.SourcePrivate) { fmt.Println(v) } } @@ -25,14 +28,14 @@ var ( func BenchmarkCQBot_ConvertStringMessage(b *testing.B) { for i := 0; i < b.N; i++ { - bot.ConvertStringMessage(bench, MessageSourcePrivate) + bot.ConvertStringMessage(bench, message.SourcePrivate) } b.SetBytes(int64(len(bench))) } func BenchmarkCQBot_ConvertObjectMessage(b *testing.B) { for i := 0; i < b.N; i++ { - bot.ConvertObjectMessage(benchArray, MessageSourcePrivate) + bot.ConvertObjectMessage(benchArray, message.SourcePrivate) } } @@ -41,7 +44,7 @@ const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&98765 func BenchmarkCQCodeEscapeText(b *testing.B) { for i := 0; i < b.N; i++ { ret := bText - CQCodeEscapeText(ret) + cqcode.EscapeText(ret) } } @@ -61,6 +64,6 @@ func TestCQCodeEscapeText(t *testing.T) { ret = strings.ReplaceAll(ret, "&", "&") ret = strings.ReplaceAll(ret, "[", "[") ret = strings.ReplaceAll(ret, "]", "]") - assert.Equal(t, ret, CQCodeEscapeText(rs)) + assert.Equal(t, ret, cqcode.EscapeText(rs)) } } diff --git a/coolq/event.go b/coolq/event.go index d89707d..ebdc960 100644 --- a/coolq/event.go +++ b/coolq/event.go @@ -2,12 +2,12 @@ package coolq import ( "encoding/hex" + "encoding/json" "fmt" "os" "path" "strconv" "strings" - "time" "github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/client" @@ -21,41 +21,73 @@ import ( ) // ToFormattedMessage 将给定[]message.IMessageElement转换为通过coolq.SetMessageFormat所定义的消息上报格式 -func ToFormattedMessage(e []message.IMessageElement, source MessageSource, isRaw ...bool) (r interface{}) { +func ToFormattedMessage(e []message.IMessageElement, source message.Source) (r interface{}) { if base.PostFormat == "string" { - r = ToStringMessage(e, source, isRaw...) + r = toStringMessage(e, source) } else if base.PostFormat == "array" { - r = ToArrayMessage(e, source) + r = toElements(e, source) } return } -func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) { - bot.checkMedia(m.Elements, m.Sender.Uin) - source := MessageSource{ - SourceType: MessageSourcePrivate, - PrimaryID: uint64(m.Sender.Uin), +type event struct { + PostType string + DetailType string + SubType string + Time int64 + SelfID int64 + Others global.MSG +} + +func (ev *event) MarshalJSON() ([]byte, error) { + buf := global.NewBuffer() + defer global.PutBuffer(buf) + + detail := "" + switch ev.PostType { + case "message", "message_sent": + detail = "message_type" + case "notice": + detail = "notice_type" + case "request": + detail = "request_type" + case "meta_event": + detail = "meta_event_type" + default: + panic("unknown post type: " + ev.PostType) } - cqm := ToStringMessage(m.Elements, source, true) + fmt.Fprintf(buf, `{"post_type":"%s","%s":"%s","time":%d,"self_id":%d`, ev.PostType, detail, ev.DetailType, ev.Time, ev.SelfID) + if ev.SubType != "" { + fmt.Fprintf(buf, `,"sub_type":"%s"`, ev.SubType) + } + for k, v := range ev.Others { + v, _ := json.Marshal(v) + fmt.Fprintf(buf, `,"%s":%s`, k, v) + } + buf.WriteByte('}') + return append([]byte(nil), buf.Bytes()...), nil +} + +func (bot *CQBot) privateMessageEvent(_ *client.QQClient, m *message.PrivateMessage) { + bot.checkMedia(m.Elements, m.Sender.Uin) + source := message.Source{ + SourceType: message.SourcePrivate, + PrimaryID: m.Sender.Uin, + } + cqm := toStringMessage(m.Elements, source) id := bot.InsertPrivateMessage(m) 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 { + typ = "message_sent/private/friend" + } fm := global.MSG{ - "post_type": func() string { - if m.Sender.Uin == bot.Client.Uin { - return "message_sent" - } - return "message" - }(), - "message_type": "private", - "sub_type": "friend", - "message_id": id, - "user_id": m.Sender.Uin, - "target_id": m.Target, - "message": ToFormattedMessage(m.Elements, source, false), - "raw_message": cqm, - "font": 0, - "self_id": c.Uin, - "time": time.Now().Unix(), + "message_id": id, + "user_id": m.Sender.Uin, + "target_id": m.Target, + "message": ToFormattedMessage(m.Elements, source), + "raw_message": cqm, + "font": 0, "sender": global.MSG{ "user_id": m.Sender.Uin, "nickname": m.Sender.Nickname, @@ -63,7 +95,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess "age": 0, }, } - bot.dispatchEventMessage(fm) + bot.dispatchEvent(typ, fm) } func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) { @@ -71,11 +103,9 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) for _, elem := range m.Elements { if file, ok := elem.(*message.GroupFileElement); ok { log.Infof("群 %v(%v) 内 %v(%v) 上传了文件: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, file.Name) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "group_upload", - "group_id": m.GroupCode, - "user_id": m.Sender.Uin, + bot.dispatchEvent("notice/group_upload", global.MSG{ + "group_id": m.GroupCode, + "user_id": m.Sender.Uin, "file": global.MSG{ "id": file.Path, "name": file.Name, @@ -83,35 +113,33 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) "busid": file.Busid, "url": c.GetGroupFileUrl(m.GroupCode, file.Path, file.Busid), }, - "self_id": c.Uin, - "time": time.Now().Unix(), }) return } } - source := MessageSource{ - SourceType: MessageSourceGroup, - PrimaryID: uint64(m.GroupCode), + source := message.Source{ + SourceType: message.SourceGroup, + PrimaryID: m.GroupCode, } - cqm := ToStringMessage(m.Elements, source, true) + cqm := toStringMessage(m.Elements, source) id := bot.InsertGroupMessage(m) 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 { return } - gm["message_id"] = id - bot.dispatchEventMessage(gm) + gm.Others["message_id"] = id + bot.dispatch(gm) } func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEvent) { m := e.Message bot.checkMedia(m.Elements, m.Sender.Uin) - source := MessageSource{ - SourceType: MessageSourcePrivate, - PrimaryID: uint64(e.Session.Sender), + source := message.Source{ + SourceType: message.SourcePrivate, + PrimaryID: e.Session.Sender, } - cqm := ToStringMessage(m.Elements, source, true) + cqm := toStringMessage(m.Elements, source) bot.tempSessionCache.Store(m.Sender.Uin, e.Session) id := m.Id // todo(Mrs4s) @@ -120,17 +148,12 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEven // } log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm) tm := global.MSG{ - "post_type": "message", - "message_type": "private", - "sub_type": "group", - "temp_source": e.Session.Source, - "message_id": id, - "user_id": m.Sender.Uin, - "message": ToFormattedMessage(m.Elements, source, false), - "raw_message": cqm, - "font": 0, - "self_id": c.Uin, - "time": time.Now().Unix(), + "temp_source": e.Session.Source, + "message_id": id, + "user_id": m.Sender.Uin, + "message": ToFormattedMessage(m.Elements, source), + "raw_message": cqm, + "font": 0, "sender": global.MSG{ "user_id": m.Sender.Uin, "group_id": m.GroupCode, @@ -139,7 +162,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, e *client.TempMessageEven "age": 0, }, } - bot.dispatchEventMessage(tm) + bot.dispatchEvent("message/private/group", tm) } func (bot *CQBot) guildChannelMessageEvent(c *client.QQClient, m *message.GuildChannelMessage) { @@ -149,31 +172,28 @@ func (bot *CQBot) guildChannelMessageEvent(c *client.QQClient, m *message.GuildC return } channel := guild.FindChannel(m.ChannelId) - source := MessageSource{ - SourceType: MessageSourceGuildChannel, - PrimaryID: m.GuildId, - SubID: m.ChannelId, + source := message.Source{ + SourceType: message.SourceGuildChannel, + PrimaryID: int64(m.GuildId), + SecondaryID: int64(m.ChannelId), } - log.Infof("收到来自频道 %v(%v) 子频道 %v(%v) 内 %v(%v) 的消息: %v", guild.GuildName, guild.GuildId, channel.ChannelName, m.ChannelId, m.Sender.Nickname, m.Sender.TinyId, ToStringMessage(m.Elements, source, true)) + log.Infof("收到来自频道 %v(%v) 子频道 %v(%v) 内 %v(%v) 的消息: %v", guild.GuildName, guild.GuildId, channel.ChannelName, m.ChannelId, m.Sender.Nickname, m.Sender.TinyId, toStringMessage(m.Elements, source)) id := bot.InsertGuildChannelMessage(m) - bot.dispatchEventMessage(global.MSG{ - "post_type": "message", - "message_type": "guild", - "sub_type": "channel", + ev := bot.event("message/guild/channel", global.MSG{ "guild_id": fU64(m.GuildId), "channel_id": fU64(m.ChannelId), "message_id": id, "user_id": fU64(m.Sender.TinyId), - "message": ToFormattedMessage(m.Elements, source, false), // todo: 增加对频道消息 Reply 的支持 - "self_id": bot.Client.Uin, + "message": ToFormattedMessage(m.Elements, source), // todo: 增加对频道消息 Reply 的支持 "self_tiny_id": fU64(bot.Client.GuildService.TinyId), - "time": m.Time, "sender": global.MSG{ "user_id": m.Sender.TinyId, "tiny_id": fU64(m.Sender.TinyId), "nickname": m.Sender.Nickname, }, }) + ev.Time = m.Time + bot.dispatch(ev) } func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *client.GuildMessageReactionsUpdatedEvent) { @@ -181,7 +201,7 @@ func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *clien if guild == nil { return } - msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, MessageSourceGuildChannel) + msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, message.SourceGuildChannel) str := fmt.Sprintf("频道 %v(%v) 消息 %v 表情贴片已更新: ", guild.GuildName, guild.GuildId, msgID) currentReactions := make([]global.MSG, len(e.CurrentReactions)) for i, r := range e.CurrentReactions { @@ -199,16 +219,12 @@ func (bot *CQBot) guildMessageReactionsUpdatedEvent(c *client.QQClient, e *clien str += "无任何表情" } log.Infof(str) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "message_reactions_updated", + bot.dispatchEvent("notice/message_reactions_updated", global.MSG{ "guild_id": fU64(e.GuildId), "channel_id": fU64(e.ChannelId), "message_id": msgID, "operator_id": fU64(e.OperatorId), "current_reactions": currentReactions, - "time": time.Now().Unix(), - "self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId), "user_id": e.OperatorId, }) @@ -228,17 +244,13 @@ func (bot *CQBot) guildChannelMessageRecalledEvent(c *client.QQClient, e *client log.Errorf("处理频道撤回事件时出现错误: 获取操作者资料时出现错误 %v", err) return } - msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, MessageSourceGuildChannel) + msgID := encodeGuildMessageID(e.GuildId, e.ChannelId, e.MessageId, message.SourceGuildChannel) log.Infof("用户 %v(%v) 撤回了频道 %v(%v) 子频道 %v(%v) 的消息 %v", operator.Nickname, operator.TinyId, guild.GuildName, guild.GuildId, channel.ChannelName, channel.ChannelId, msgID) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "guild_channel_recall", + bot.dispatchEvent("notice/guild_channel_recall", global.MSG{ "guild_id": fU64(e.GuildId), "channel_id": fU64(e.ChannelId), "operator_id": fU64(e.OperatorId), "message_id": msgID, - "time": time.Now().Unix(), - "self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId), "user_id": e.OperatorId, }) @@ -250,14 +262,10 @@ func (bot *CQBot) guildChannelUpdatedEvent(c *client.QQClient, e *client.GuildCh return } log.Infof("频道 %v(%v) 子频道 %v(%v) 信息已更新", guild.GuildName, guild.GuildId, e.NewChannelInfo.ChannelName, e.NewChannelInfo.ChannelId) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "channel_updated", + bot.dispatchEvent("notice/channel_updated", global.MSG{ "guild_id": fU64(e.GuildId), "channel_id": fU64(e.ChannelId), "operator_id": fU64(e.OperatorId), - "time": time.Now().Unix(), - "self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId), "user_id": e.OperatorId, "old_info": convertChannelInfo(e.OldChannelInfo), @@ -275,16 +283,12 @@ func (bot *CQBot) guildChannelCreatedEvent(c *client.QQClient, e *client.GuildCh member = &client.GuildUserProfile{Nickname: "未知"} } log.Infof("频道 %v(%v) 内用户 %v(%v) 创建了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "channel_created", + bot.dispatchEvent("notice/channel_created", global.MSG{ "guild_id": fU64(e.GuildId), "channel_id": fU64(e.ChannelInfo.ChannelId), "operator_id": fU64(e.OperatorId), - "self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId), "user_id": e.OperatorId, - "time": time.Now().Unix(), "channel_info": convertChannelInfo(e.ChannelInfo), }) } @@ -299,16 +303,12 @@ func (bot *CQBot) guildChannelDestroyedEvent(c *client.QQClient, e *client.Guild member = &client.GuildUserProfile{Nickname: "未知"} } log.Infof("频道 %v(%v) 内用户 %v(%v) 删除了子频道 %v(%v)", guild.GuildName, guild.GuildId, member.Nickname, member.TinyId, e.ChannelInfo.ChannelName, e.ChannelInfo.ChannelId) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "channel_destroyed", + bot.dispatchEvent("notice/channel_destroyed", global.MSG{ "guild_id": fU64(e.GuildId), "channel_id": fU64(e.ChannelInfo.ChannelId), "operator_id": fU64(e.OperatorId), - "self_id": bot.Client.Uin, "self_tiny_id": fU64(bot.Client.GuildService.TinyId), "user_id": e.OperatorId, - "time": time.Now().Unix(), "channel_info": convertChannelInfo(e.ChannelInfo), }) } @@ -332,22 +332,15 @@ func (bot *CQBot) groupMutedEvent(c *client.QQClient, e *client.GroupMuteEvent) formatGroupName(g), formatMemberName(g.FindMember(e.TargetUin)), formatMemberName(g.FindMember(e.OperatorUin))) } } - - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", + typ := "notice/group_ban/ban" + if e.Time == 0 { + typ = "notice/group_ban/lift_ban" + } + bot.dispatchEvent(typ, global.MSG{ "duration": e.Time, "group_id": e.GroupCode, - "notice_type": "group_ban", "operator_id": e.OperatorUin, - "self_id": c.Uin, "user_id": e.TargetUin, - "time": time.Now().Unix(), - "sub_type": func() string { - if e.Time == 0 { - return "lift_ban" - } - return "ban" - }(), }) } @@ -356,16 +349,15 @@ func (bot *CQBot) groupRecallEvent(c *client.QQClient, e *client.GroupMessageRec gid := db.ToGlobalID(e.GroupCode, e.MessageId) log.Infof("群 %v 内 %v 撤回了 %v 的消息: %v.", formatGroupName(g), formatMemberName(g.FindMember(e.OperatorUin)), formatMemberName(g.FindMember(e.AuthorUin)), gid) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", + + ev := bot.event("notice/group_recall", global.MSG{ "group_id": e.GroupCode, - "notice_type": "group_recall", - "self_id": c.Uin, "user_id": e.AuthorUin, "operator_id": e.OperatorUin, - "time": e.Time, "message_id": gid, }) + ev.Time = int64(e.Time) + bot.dispatch(ev) } func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) { @@ -375,42 +367,27 @@ func (bot *CQBot) groupNotifyEvent(c *client.QQClient, e client.INotifyEvent) { sender := group.FindMember(notify.Sender) receiver := group.FindMember(notify.Receiver) log.Infof("群 %v 内 %v 戳了戳 %v", formatGroupName(group), formatMemberName(sender), formatMemberName(receiver)) - bot.dispatchEventMessage(global.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(), + bot.dispatchEvent("notice/notify/poke", global.MSG{ + "group_id": group.Code, + "user_id": notify.Sender, + "sender_id": notify.Sender, + "target_id": notify.Receiver, }) 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(global.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(), + bot.dispatchEvent("notice/notify/lucky_king", global.MSG{ + "group_id": group.Code, + "user_id": notify.Sender, + "sender_id": notify.Sender, + "target_id": notify.LuckyKing, }) case *client.MemberHonorChangedNotifyEvent: log.Info(notify.Content()) - bot.dispatchEventMessage(global.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(), + bot.dispatchEvent("notice/notify/honor", global.MSG{ + "group_id": group.Code, + "user_id": notify.Uin, "honor_type": func() string { switch notify.Honor { case client.Talkative: @@ -439,15 +416,10 @@ func (bot *CQBot) friendNotifyEvent(c *client.QQClient, e client.INotifyEvent) { } else { log.Infof("好友 %v 戳了戳你.", friend.Nickname) } - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "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(), + bot.dispatchEvent("notice/notify/poke", global.MSG{ + "user_id": notify.Sender, + "sender_id": notify.Sender, + "target_id": notify.Receiver, }) } } @@ -456,15 +428,10 @@ func (bot *CQBot) memberTitleUpdatedEvent(c *client.QQClient, e *client.MemberSp group := c.FindGroup(e.GroupCode) mem := group.FindMember(e.Uin) log.Infof("群 %v(%v) 内成员 %v(%v) 获得了新的头衔: %v", group.Name, group.Code, mem.DisplayName(), mem.Uin, e.NewTitle) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "notify", - "sub_type": "title", - "group_id": group.Code, - "self_id": c.Uin, - "user_id": e.Uin, - "time": time.Now().Unix(), - "title": e.NewTitle, + bot.dispatchEvent("notice/notify/title", global.MSG{ + "group_id": group.Code, + "user_id": e.Uin, + "title": e.NewTitle, }) } @@ -476,14 +443,12 @@ func (bot *CQBot) friendRecallEvent(c *client.QQClient, e *client.FriendMessageR } else { log.Infof("好友 %v 撤回了消息: %v", e.FriendUin, gid) } - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "friend_recall", - "self_id": c.Uin, - "user_id": e.FriendUin, - "time": e.Time, - "message_id": gid, + ev := bot.event("notice/friend_recall", global.MSG{ + "user_id": e.FriendUin, + "message_id": gid, }) + ev.Time = e.Time + bot.dispatch(ev) } func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.OfflineFileEvent) { @@ -492,23 +457,19 @@ func (bot *CQBot) offlineFileEvent(c *client.QQClient, e *client.OfflineFileEven return } log.Infof("好友 %v(%v) 发送了离线文件 %v", f.Nickname, f.Uin, e.FileName) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "offline_file", - "user_id": e.Sender, + bot.dispatchEvent("notice/offline_file", global.MSG{ + "user_id": e.Sender, "file": global.MSG{ "name": e.FileName, "size": e.FileSize, "url": e.DownloadUrl, }, - "self_id": c.Uin, - "time": time.Now().Unix(), }) } func (bot *CQBot) joinGroupEvent(c *client.QQClient, group *client.GroupInfo) { log.Infof("Bot进入了群 %v.", formatGroupName(group)) - bot.dispatchEventMessage(bot.groupIncrease(group.Code, 0, c.Uin)) + bot.dispatch(bot.groupIncrease(group.Code, 0, c.Uin)) } func (bot *CQBot) leaveGroupEvent(c *client.QQClient, e *client.GroupLeaveEvent) { @@ -517,44 +478,33 @@ func (bot *CQBot) leaveGroupEvent(c *client.QQClient, e *client.GroupLeaveEvent) } else { log.Infof("Bot退出了群 %v.", formatGroupName(e.Group)) } - bot.dispatchEventMessage(bot.groupDecrease(e.Group.Code, c.Uin, e.Operator)) + bot.dispatch(bot.groupDecrease(e.Group.Code, c.Uin, e.Operator)) } func (bot *CQBot) memberPermissionChangedEvent(c *client.QQClient, e *client.MemberPermissionChangedEvent) { - st := func() string { - if e.NewPermission == client.Administrator { - return "set" - } - return "unset" - }() - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "group_admin", - "sub_type": st, - "group_id": e.Group.Code, - "user_id": e.Member.Uin, - "time": time.Now().Unix(), - "self_id": c.Uin, + st := "unset" + if e.NewPermission == client.Administrator { + st = "set" + } + bot.dispatchEvent("notice/group_admin/"+st, global.MSG{ + "group_id": e.Group.Code, + "user_id": e.Member.Uin, }) } 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(global.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, + bot.dispatchEvent("notice/group_card", global.MSG{ + "group_id": e.Group.Code, + "user_id": e.Member.Uin, + "card_new": e.Member.CardName, + "card_old": e.OldCard, }) } func (bot *CQBot) memberJoinEvent(_ *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)) + bot.dispatch(bot.groupIncrease(e.Group.Code, 0, e.Member.Uin)) } func (bot *CQBot) memberLeaveEvent(_ *client.QQClient, e *client.MemberLeaveGroupEvent) { @@ -563,65 +513,47 @@ func (bot *CQBot) memberLeaveEvent(_ *client.QQClient, e *client.MemberLeaveGrou } else { log.Infof("成员 %v 离开了群 %v.", formatMemberName(e.Member), formatGroupName(e.Group)) } - bot.dispatchEventMessage(bot.groupDecrease(e.Group.Code, e.Member.Uin, e.Operator)) + bot.dispatch(bot.groupDecrease(e.Group.Code, e.Member.Uin, e.Operator)) } func (bot *CQBot) friendRequestEvent(c *client.QQClient, e *client.NewFriendRequest) { log.Infof("收到来自 %v(%v) 的好友请求: %v", e.RequesterNick, e.RequesterUin, e.Message) flag := strconv.FormatInt(e.RequestId, 10) bot.friendReqCache.Store(flag, e) - bot.dispatchEventMessage(global.MSG{ - "post_type": "request", - "request_type": "friend", - "user_id": e.RequesterUin, - "comment": e.Message, - "flag": flag, - "time": time.Now().Unix(), - "self_id": c.Uin, + bot.dispatchEvent("request/friend", global.MSG{ + "user_id": e.RequesterUin, + "comment": e.Message, + "flag": flag, }) } func (bot *CQBot) friendAddedEvent(c *client.QQClient, e *client.NewFriendEvent) { log.Infof("添加了新好友: %v(%v)", e.Friend.Nickname, e.Friend.Uin) bot.tempSessionCache.Delete(e.Friend.Uin) - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "friend_add", - "self_id": c.Uin, - "user_id": e.Friend.Uin, - "time": time.Now().Unix(), + bot.dispatchEvent("notice/friend_add", global.MSG{ + "user_id": e.Friend.Uin, }) } func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) { log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin) flag := strconv.FormatInt(e.RequestId, 10) - bot.dispatchEventMessage(global.MSG{ - "post_type": "request", - "request_type": "group", - "sub_type": "invite", - "group_id": e.GroupCode, - "user_id": e.InvitorUin, - "comment": "", - "flag": flag, - "time": time.Now().Unix(), - "self_id": c.Uin, + bot.dispatchEvent("request/group/invite", global.MSG{ + "group_id": e.GroupCode, + "user_id": e.InvitorUin, + "comment": "", + "flag": flag, }) } func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) { log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupCode, e.RequesterNick, e.RequesterUin) flag := strconv.FormatInt(e.RequestId, 10) - bot.dispatchEventMessage(global.MSG{ - "post_type": "request", - "request_type": "group", - "sub_type": "add", - "group_id": e.GroupCode, - "user_id": e.RequesterUin, - "comment": e.Message, - "flag": flag, - "time": time.Now().Unix(), - "self_id": c.Uin, + bot.dispatchEvent("request/group/add", global.MSG{ + "group_id": e.GroupCode, + "user_id": e.RequesterUin, + "comment": e.Message, + "flag": flag, }) } @@ -631,17 +563,13 @@ func (bot *CQBot) otherClientStatusChangedEvent(c *client.QQClient, e *client.Ot } else { log.Infof("Bot 账号在客户端 %v (%v) 登出.", e.Client.DeviceName, e.Client.DeviceKind) } - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", - "notice_type": "client_status", - "online": e.Online, + bot.dispatchEvent("notice/client_status", global.MSG{ + "online": e.Online, "client": global.MSG{ "app_id": e.Client.AppId, "device_name": e.Client.DeviceName, "device_kind": e.Client.DeviceKind, }, - "self_id": c.Uin, - "time": time.Now().Unix(), }) } @@ -668,61 +596,44 @@ func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent if e.OperatorUin == bot.Client.Uin { return } - bot.dispatchEventMessage(global.MSG{ - "post_type": "notice", + subtype := "delete" + if e.OperationType == 1 { + subtype = "add" + } + bot.dispatchEvent("notice/essence/"+subtype, global.MSG{ "group_id": e.GroupCode, - "notice_type": "essence", - "sub_type": func() string { - if e.OperationType == 1 { - return "add" - } - return "delete" - }(), - "self_id": c.Uin, "sender_id": e.SenderUin, "operator_id": e.OperatorUin, - "time": time.Now().Unix(), "message_id": gid, }) } -func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) global.MSG { - return global.MSG{ - "post_type": "notice", - "notice_type": "group_increase", +func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) *event { + return bot.event("notice/group_increase/approve", global.MSG{ "group_id": groupCode, "operator_id": operatorUin, - "self_id": bot.Client.Uin, - "sub_type": "approve", - "time": time.Now().Unix(), "user_id": userUin, - } + }) } -func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.GroupMemberInfo) global.MSG { - return global.MSG{ - "post_type": "notice", - "notice_type": "group_decrease", - "group_id": groupCode, - "operator_id": func() int64 { - if operator != nil { - return operator.Uin - } - return userUin - }(), - "self_id": bot.Client.Uin, - "sub_type": func() string { - if operator != nil { - if userUin == bot.Client.Uin { - return "kick_me" - } - return "kick" - } - return "leave" - }(), - "time": time.Now().Unix(), - "user_id": userUin, +func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.GroupMemberInfo) *event { + op := userUin + if operator != nil { + op = operator.Uin } + subtype := "leave" + if operator != nil { + if userUin == bot.Client.Uin { + subtype = "kick_me" + } else { + subtype = "kick" + } + } + return bot.event("notice/group_decrease/"+subtype, global.MSG{ + "group_id": groupCode, + "operator_id": op, + "user_id": userUin, + }) } func (bot *CQBot) checkMedia(e []message.IMessageElement, sourceID int64) { diff --git a/db/database.go b/db/database.go index 6495f82..c19da97 100644 --- a/db/database.go +++ b/db/database.go @@ -103,22 +103,14 @@ func ToGlobalID(code int64, msgID int32) int32 { return int32(crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d-%d", code, msgID)))) } -func (m *StoredGroupMessage) GetID() string { return m.ID } - -func (m *StoredGroupMessage) GetType() string { return "group" } - -func (m *StoredGroupMessage) GetGlobalID() int32 { return m.GlobalID } - +func (m *StoredGroupMessage) GetID() string { return m.ID } +func (m *StoredGroupMessage) GetType() string { return "group" } +func (m *StoredGroupMessage) GetGlobalID() int32 { return m.GlobalID } func (m *StoredGroupMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute } +func (m *StoredGroupMessage) GetContent() []global.MSG { return m.Content } -func (m *StoredGroupMessage) GetContent() []global.MSG { return m.Content } - -func (m *StoredPrivateMessage) GetID() string { return m.ID } - -func (m *StoredPrivateMessage) GetType() string { return "private" } - -func (m *StoredPrivateMessage) GetGlobalID() int32 { return m.GlobalID } - +func (m *StoredPrivateMessage) GetID() string { return m.ID } +func (m *StoredPrivateMessage) GetType() string { return "private" } +func (m *StoredPrivateMessage) GetGlobalID() int32 { return m.GlobalID } func (m *StoredPrivateMessage) GetAttribute() *StoredMessageAttribute { return m.Attribute } - -func (m *StoredPrivateMessage) GetContent() []global.MSG { return m.Content } +func (m *StoredPrivateMessage) GetContent() []global.MSG { return m.Content } diff --git a/db/leveldb/const.go b/db/leveldb/const.go new file mode 100644 index 0000000..1e63031 --- /dev/null +++ b/db/leveldb/const.go @@ -0,0 +1,25 @@ +package leveldb + +const dataVersion = 1 + +const ( + group = 0x0 + private = 0x1 + guildChannel = 0x2 +) + +type coder byte + +const ( + coderNil coder = iota + coderInt + coderUint + coderInt32 + coderUint32 + coderInt64 + coderUint64 + coderString + coderMSG // global.MSG + coderArrayMSG // []global.MSG + coderStruct // struct{} +) diff --git a/db/leveldb/leveldb.go b/db/leveldb/leveldb.go index cfe7716..7853e1b 100644 --- a/db/leveldb/leveldb.go +++ b/db/leveldb/leveldb.go @@ -1,56 +1,42 @@ package leveldb import ( - "bytes" - "encoding/gob" "path" - "github.com/Mrs4s/MiraiGo/utils" - "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/utils" "github.com/pkg/errors" "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/opt" "gopkg.in/yaml.v3" "github.com/Mrs4s/go-cqhttp/db" - "github.com/Mrs4s/go-cqhttp/global" - "github.com/Mrs4s/go-cqhttp/modules/config" ) -type LevelDBImpl struct { +type database struct { db *leveldb.DB } -const ( - group byte = 0x0 - private byte = 0x1 - guildChannel byte = 0x2 -) +// config leveldb 相关配置 +type config struct { + Enable bool `yaml:"enable"` +} func init() { - gob.Register(db.StoredMessageAttribute{}) - gob.Register(db.StoredGuildMessageAttribute{}) - gob.Register(db.QuotedInfo{}) - gob.Register(global.MSG{}) - gob.Register(db.StoredGroupMessage{}) - gob.Register(db.StoredPrivateMessage{}) - gob.Register(db.StoredGuildChannelMessage{}) - db.Register("leveldb", func(node yaml.Node) db.Database { - conf := new(config.LevelDBConfig) + conf := new(config) _ = node.Decode(conf) if !conf.Enable { return nil } - return &LevelDBImpl{} + return &database{} }) } -func (ldb *LevelDBImpl) Open() error { - p := path.Join("data", "leveldb-v2") +func (ldb *database) Open() error { + p := path.Join("data", "leveldb-v3") d, err := leveldb.OpenFile(p, &opt.Options{ - WriteBuffer: 128 * opt.KiB, + WriteBuffer: 32 * opt.KiB, }) if err != nil { return errors.Wrap(err, "open leveldb error") @@ -59,31 +45,31 @@ func (ldb *LevelDBImpl) Open() error { return nil } -func (ldb *LevelDBImpl) GetMessageByGlobalID(id int32) (db.StoredMessage, error) { +func (ldb *database) GetMessageByGlobalID(id int32) (_ db.StoredMessage, err error) { v, err := ldb.db.Get(binary.ToBytes(id), nil) - if err != nil { + if err != nil || len(v) == 0 { return nil, errors.Wrap(err, "get value error") } - r := binary.NewReader(v) - switch r.ReadByte() { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("%v", r) + } + }() + r, err := newReader(utils.B2S(v)) + if err != nil { + return nil, err + } + switch r.uvarint() { case group: - g := &db.StoredGroupMessage{} - if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(g); err != nil { - return nil, errors.Wrap(err, "decode message error") - } - return g, nil + return r.readStoredGroupMessage(), nil case private: - p := &db.StoredPrivateMessage{} - if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(p); err != nil { - return nil, errors.Wrap(err, "decode message error") - } - return p, nil + return r.readStoredPrivateMessage(), nil default: return nil, errors.New("unknown message flag") } } -func (ldb *LevelDBImpl) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) { +func (ldb *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) { i, err := ldb.GetMessageByGlobalID(id) if err != nil { return nil, err @@ -95,7 +81,7 @@ func (ldb *LevelDBImpl) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMess return g, nil } -func (ldb *LevelDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) { +func (ldb *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) { i, err := ldb.GetMessageByGlobalID(id) if err != nil { return nil, err @@ -107,59 +93,48 @@ func (ldb *LevelDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivate return p, nil } -func (ldb *LevelDBImpl) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) { +func (ldb *database) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) { v, err := ldb.db.Get([]byte(id), nil) if err != nil { return nil, errors.Wrap(err, "get value error") } - r := binary.NewReader(v) - switch r.ReadByte() { - case guildChannel: - g := &db.StoredGuildChannelMessage{} - if err = gob.NewDecoder(bytes.NewReader(r.ReadAvailable())).Decode(g); err != nil { - return nil, errors.Wrap(err, "decode message error") + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("%v", r) } - return g, nil + }() + r, err := newReader(utils.B2S(v)) + if err != nil { + return nil, err + } + switch r.uvarint() { + case guildChannel: + return r.readStoredGuildChannelMessage(), nil default: return nil, errors.New("unknown message flag") } } -func (ldb *LevelDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - if err := gob.NewEncoder(buf).Encode(msg); err != nil { - return errors.Wrap(err, "encode message error") - } - err := ldb.db.Put(binary.ToBytes(msg.GlobalID), binary.NewWriterF(func(w *binary.Writer) { - w.WriteByte(group) - w.Write(buf.Bytes()) - }), nil) +func (ldb *database) InsertGroupMessage(msg *db.StoredGroupMessage) error { + w := newWriter() + w.uvarint(group) + w.writeStoredGroupMessage(msg) + err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil) return errors.Wrap(err, "put data error") } -func (ldb *LevelDBImpl) InsertPrivateMessage(msg *db.StoredPrivateMessage) error { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - if err := gob.NewEncoder(buf).Encode(msg); err != nil { - return errors.Wrap(err, "encode message error") - } - err := ldb.db.Put(binary.ToBytes(msg.GlobalID), binary.NewWriterF(func(w *binary.Writer) { - w.WriteByte(private) - w.Write(buf.Bytes()) - }), nil) +func (ldb *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error { + w := newWriter() + w.uvarint(private) + w.writeStoredPrivateMessage(msg) + err := ldb.db.Put(binary.ToBytes(msg.GlobalID), w.bytes(), nil) return errors.Wrap(err, "put data error") } -func (ldb *LevelDBImpl) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error { - buf := global.NewBuffer() - defer global.PutBuffer(buf) - if err := gob.NewEncoder(buf).Encode(msg); err != nil { - return errors.Wrap(err, "encode message error") - } - err := ldb.db.Put(utils.S2B(msg.ID), binary.NewWriterF(func(w *binary.Writer) { - w.WriteByte(guildChannel) - w.Write(buf.Bytes()) - }), nil) +func (ldb *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error { + w := newWriter() + w.uvarint(guildChannel) + w.writeStoredGuildChannelMessage(msg) + err := ldb.db.Put(utils.S2B(msg.ID), w.bytes(), nil) return errors.Wrap(err, "put data error") } diff --git a/db/leveldb/reader.go b/db/leveldb/reader.go new file mode 100644 index 0000000..e891036 --- /dev/null +++ b/db/leveldb/reader.go @@ -0,0 +1,131 @@ +package leveldb + +import ( + "encoding/binary" + "io" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/Mrs4s/go-cqhttp/global" +) + +type intReader struct { + data string + *strings.Reader +} + +func newIntReader(s string) intReader { + return intReader{ + data: s, + Reader: strings.NewReader(s), + } +} + +func (r *intReader) varint() int64 { + i, _ := binary.ReadVarint(r) + return i +} + +func (r *intReader) uvarint() uint64 { + i, _ := binary.ReadUvarint(r) + return i +} + +// reader implements the index read. +// data format is the same as the writer's +type reader struct { + data intReader + strings intReader + stringIndex map[uint64]string +} + +func (r *reader) coder() coder { o, _ := r.data.ReadByte(); return coder(o) } +func (r *reader) varint() int64 { return r.data.varint() } +func (r *reader) uvarint() uint64 { return r.data.uvarint() } +func (r *reader) int32() int32 { return int32(r.varint()) } +func (r *reader) int64() int64 { return r.varint() } +func (r *reader) uint64() uint64 { return r.uvarint() } + +// func (r *reader) uint32() uint32 { return uint32(r.uvarint()) } +// func (r *reader) int() int { return int(r.varint()) } +// func (r *reader) uint() uint { return uint(r.uvarint()) } + +func (r *reader) string() string { + off := r.data.uvarint() + if s, ok := r.stringIndex[off]; ok { + return s + } + _, _ = r.strings.Seek(int64(off), io.SeekStart) + l := int64(r.strings.uvarint()) + whence, _ := r.strings.Seek(0, io.SeekCurrent) + s := r.strings.data[whence : whence+l] + r.stringIndex[off] = s + return s +} + +func (r *reader) msg() global.MSG { + length := r.uvarint() + msg := make(global.MSG, length) + for i := uint64(0); i < length; i++ { + s := r.string() + msg[s] = r.obj() + } + return msg +} + +func (r *reader) arrayMsg() []global.MSG { + length := r.uvarint() + msgs := make([]global.MSG, length) + for i := range msgs { + msgs[i] = r.msg() + } + return msgs +} + +func (r *reader) obj() interface{} { + switch coder := r.coder(); coder { + case coderNil: + return nil + case coderInt: + return int(r.varint()) + case coderUint: + return uint(r.uvarint()) + case coderInt32: + return int32(r.varint()) + case coderUint32: + return uint32(r.uvarint()) + case coderInt64: + return r.varint() + case coderUint64: + return r.uvarint() + case coderString: + return r.string() + case coderMSG: + return r.msg() + case coderArrayMSG: + return r.arrayMsg() + default: + panic("db/leveldb: invalid coder " + strconv.Itoa(int(coder))) + } +} + +func newReader(data string) (*reader, error) { + in := newIntReader(data) + v := in.uvarint() + if v != dataVersion { + return nil, errors.Errorf("db/leveldb: invalid data version %d", v) + } + sl := int64(in.uvarint()) + dl := int64(in.uvarint()) + whence, _ := in.Seek(0, io.SeekCurrent) + sData := data[whence : whence+sl] + dData := data[whence+sl : whence+sl+dl] + r := reader{ + data: newIntReader(dData), + strings: newIntReader(sData), + stringIndex: make(map[uint64]string), + } + return &r, nil +} diff --git a/db/leveldb/structs.go b/db/leveldb/structs.go new file mode 100644 index 0000000..dfb7caa --- /dev/null +++ b/db/leveldb/structs.go @@ -0,0 +1,175 @@ +package leveldb + +import "github.com/Mrs4s/go-cqhttp/db" + +func (w *writer) writeStoredGroupMessage(x *db.StoredGroupMessage) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.string(x.ID) + w.int32(x.GlobalID) + w.writeStoredMessageAttribute(x.Attribute) + w.string(x.SubType) + w.writeQuotedInfo(x.QuotedInfo) + w.int64(x.GroupCode) + w.string(x.AnonymousID) + w.arrayMsg(x.Content) +} + +func (r *reader) readStoredGroupMessage() *db.StoredGroupMessage { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.StoredGroupMessage{} + x.ID = r.string() + x.GlobalID = r.int32() + x.Attribute = r.readStoredMessageAttribute() + x.SubType = r.string() + x.QuotedInfo = r.readQuotedInfo() + x.GroupCode = r.int64() + x.AnonymousID = r.string() + x.Content = r.arrayMsg() + return x +} + +func (w *writer) writeStoredPrivateMessage(x *db.StoredPrivateMessage) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.string(x.ID) + w.int32(x.GlobalID) + w.writeStoredMessageAttribute(x.Attribute) + w.string(x.SubType) + w.writeQuotedInfo(x.QuotedInfo) + w.int64(x.SessionUin) + w.int64(x.TargetUin) + w.arrayMsg(x.Content) +} + +func (r *reader) readStoredPrivateMessage() *db.StoredPrivateMessage { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.StoredPrivateMessage{} + x.ID = r.string() + x.GlobalID = r.int32() + x.Attribute = r.readStoredMessageAttribute() + x.SubType = r.string() + x.QuotedInfo = r.readQuotedInfo() + x.SessionUin = r.int64() + x.TargetUin = r.int64() + x.Content = r.arrayMsg() + return x +} + +func (w *writer) writeStoredGuildChannelMessage(x *db.StoredGuildChannelMessage) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.string(x.ID) + w.writeStoredGuildMessageAttribute(x.Attribute) + w.uint64(x.GuildID) + w.uint64(x.ChannelID) + w.writeQuotedInfo(x.QuotedInfo) + w.arrayMsg(x.Content) +} + +func (r *reader) readStoredGuildChannelMessage() *db.StoredGuildChannelMessage { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.StoredGuildChannelMessage{} + x.ID = r.string() + x.Attribute = r.readStoredGuildMessageAttribute() + x.GuildID = r.uint64() + x.ChannelID = r.uint64() + x.QuotedInfo = r.readQuotedInfo() + x.Content = r.arrayMsg() + return x +} + +func (w *writer) writeStoredMessageAttribute(x *db.StoredMessageAttribute) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.int32(x.MessageSeq) + w.int32(x.InternalID) + w.int64(x.SenderUin) + w.string(x.SenderName) + w.int64(x.Timestamp) +} + +func (r *reader) readStoredMessageAttribute() *db.StoredMessageAttribute { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.StoredMessageAttribute{} + x.MessageSeq = r.int32() + x.InternalID = r.int32() + x.SenderUin = r.int64() + x.SenderName = r.string() + x.Timestamp = r.int64() + return x +} + +func (w *writer) writeStoredGuildMessageAttribute(x *db.StoredGuildMessageAttribute) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.uint64(x.MessageSeq) + w.uint64(x.InternalID) + w.uint64(x.SenderTinyID) + w.string(x.SenderName) + w.int64(x.Timestamp) +} + +func (r *reader) readStoredGuildMessageAttribute() *db.StoredGuildMessageAttribute { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.StoredGuildMessageAttribute{} + x.MessageSeq = r.uint64() + x.InternalID = r.uint64() + x.SenderTinyID = r.uint64() + x.SenderName = r.string() + x.Timestamp = r.int64() + return x +} + +func (w *writer) writeQuotedInfo(x *db.QuotedInfo) { + if x == nil { + w.nil() + return + } + w.coder(coderStruct) + w.string(x.PrevID) + w.int32(x.PrevGlobalID) + w.arrayMsg(x.QuotedContent) +} + +func (r *reader) readQuotedInfo() *db.QuotedInfo { + coder := r.coder() + if coder == coderNil { + return nil + } + x := &db.QuotedInfo{} + x.PrevID = r.string() + x.PrevGlobalID = r.int32() + x.QuotedContent = r.arrayMsg() + return x +} diff --git a/db/leveldb/writer.go b/db/leveldb/writer.go new file mode 100644 index 0000000..6067ca1 --- /dev/null +++ b/db/leveldb/writer.go @@ -0,0 +1,143 @@ +package leveldb + +import ( + "bytes" + + "github.com/Mrs4s/go-cqhttp/global" +) + +type intWriter struct { + bytes.Buffer +} + +func (w *intWriter) varint(x int64) { + w.uvarint(uint64(x)<<1 ^ uint64(x>>63)) +} + +func (w *intWriter) uvarint(x uint64) { + for x >= 0x80 { + w.WriteByte(byte(x) | 0x80) + x >>= 7 + } + w.WriteByte(byte(x)) +} + +// writer implements the index write. +// +// data format(use uvarint to encode integers): +// +// - version +// - string data length +// - index data length +// - string data +// - index data +// +// for string data part, each string is encoded as: +// +// - string length +// - string +// +// for index data part, each object value is encoded as: +// +// - coder +// - value +// +// * coder is the identifier of value's type. +// * specially for string, it's value is the offset in string data part. +type writer struct { + data intWriter + strings intWriter + stringIndex map[string]uint64 +} + +func newWriter() *writer { + return &writer{ + stringIndex: make(map[string]uint64), + } +} + +func (w *writer) coder(o coder) { w.data.WriteByte(byte(o)) } +func (w *writer) varint(x int64) { w.data.varint(x) } +func (w *writer) uvarint(x uint64) { w.data.uvarint(x) } +func (w *writer) nil() { w.coder(coderNil) } +func (w *writer) int(i int) { w.varint(int64(i)) } +func (w *writer) uint(i uint) { w.uvarint(uint64(i)) } +func (w *writer) int32(i int32) { w.varint(int64(i)) } +func (w *writer) uint32(i uint32) { w.uvarint(uint64(i)) } +func (w *writer) int64(i int64) { w.varint(i) } +func (w *writer) uint64(i uint64) { w.uvarint(i) } + +func (w *writer) string(s string) { + off, ok := w.stringIndex[s] + if !ok { + // not found write to string data part + // | string length | string | + off = uint64(w.strings.Len()) + w.strings.uvarint(uint64(len(s))) + _, _ = w.strings.WriteString(s) + w.stringIndex[s] = off + } + // write offset to index data part + w.uvarint(off) +} + +func (w *writer) msg(m global.MSG) { + w.uvarint(uint64(len(m))) + for s, obj := range m { + w.string(s) + w.obj(obj) + } +} + +func (w *writer) arrayMsg(a []global.MSG) { + w.uvarint(uint64(len(a))) + for _, v := range a { + w.msg(v) + } +} + +func (w *writer) obj(o interface{}) { + switch x := o.(type) { + case nil: + w.nil() + case int: + w.coder(coderInt) + w.int(x) + case int32: + w.coder(coderInt32) + w.int32(x) + case int64: + w.coder(coderInt64) + w.int64(x) + case uint: + w.coder(coderUint) + w.uint(x) + case uint32: + w.coder(coderUint32) + w.uint32(x) + case uint64: + w.coder(coderUint64) + w.uint64(x) + case string: + w.coder(coderString) + w.string(x) + case global.MSG: + w.coder(coderMSG) + w.msg(x) + case []global.MSG: + w.coder(coderArrayMSG) + w.arrayMsg(x) + default: + panic("unsupported type") + } +} + +func (w *writer) bytes() []byte { + var out intWriter + out.uvarint(dataVersion) + out.uvarint(uint64(w.strings.Len())) + out.uvarint(uint64(w.data.Len())) + _, _ = w.strings.WriteTo(&out) + _, _ = w.data.WriteTo(&out) + return out.Bytes() +} diff --git a/db/mongodb/mongodb.go b/db/mongodb/mongodb.go index 9c8449d..35ea702 100644 --- a/db/mongodb/mongodb.go +++ b/db/mongodb/mongodb.go @@ -10,15 +10,21 @@ import ( "gopkg.in/yaml.v3" "github.com/Mrs4s/go-cqhttp/db" - "github.com/Mrs4s/go-cqhttp/modules/config" ) -type MongoDBImpl struct { +type database struct { uri string db string mongo *mongo.Database } +// config mongodb 相关配置 +type config struct { + Enable bool `yaml:"enable"` + URI string `yaml:"uri"` + Database string `yaml:"database"` +} + const ( MongoGroupMessageCollection = "group-messages" MongoPrivateMessageCollection = "private-messages" @@ -26,8 +32,8 @@ const ( ) func init() { - db.Register("mongodb", func(node yaml.Node) db.Database { - conf := new(config.MongoDBConfig) + db.Register("database", func(node yaml.Node) db.Database { + conf := new(config) _ = node.Decode(conf) if conf.Database == "" { conf.Database = "gocq-database" @@ -35,11 +41,11 @@ func init() { if !conf.Enable { return nil } - return &MongoDBImpl{uri: conf.URI, db: conf.Database} + return &database{uri: conf.URI, db: conf.Database} }) } -func (m *MongoDBImpl) Open() error { +func (m *database) Open() error { cli, err := mongo.Connect(context.Background(), options.Client().ApplyURI(m.uri)) if err != nil { return errors.Wrap(err, "open mongo connection error") @@ -48,14 +54,14 @@ func (m *MongoDBImpl) Open() error { return nil } -func (m *MongoDBImpl) GetMessageByGlobalID(id int32) (db.StoredMessage, error) { +func (m *database) GetMessageByGlobalID(id int32) (db.StoredMessage, error) { if r, err := m.GetGroupMessageByGlobalID(id); err == nil { return r, nil } return m.GetPrivateMessageByGlobalID(id) } -func (m *MongoDBImpl) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) { +func (m *database) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessage, error) { coll := m.mongo.Collection(MongoGroupMessageCollection) var ret db.StoredGroupMessage if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil { @@ -64,7 +70,7 @@ func (m *MongoDBImpl) GetGroupMessageByGlobalID(id int32) (*db.StoredGroupMessag return &ret, nil } -func (m *MongoDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) { +func (m *database) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMessage, error) { coll := m.mongo.Collection(MongoPrivateMessageCollection) var ret db.StoredPrivateMessage if err := coll.FindOne(context.Background(), bson.D{{"globalId", id}}).Decode(&ret); err != nil { @@ -73,7 +79,7 @@ func (m *MongoDBImpl) GetPrivateMessageByGlobalID(id int32) (*db.StoredPrivateMe return &ret, nil } -func (m *MongoDBImpl) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) { +func (m *database) GetGuildChannelMessageByID(id string) (*db.StoredGuildChannelMessage, error) { coll := m.mongo.Collection(MongoGuildChannelMessageCollection) var ret db.StoredGuildChannelMessage if err := coll.FindOne(context.Background(), bson.D{{"_id", id}}).Decode(&ret); err != nil { @@ -82,19 +88,19 @@ func (m *MongoDBImpl) GetGuildChannelMessageByID(id string) (*db.StoredGuildChan return &ret, nil } -func (m *MongoDBImpl) InsertGroupMessage(msg *db.StoredGroupMessage) error { +func (m *database) InsertGroupMessage(msg *db.StoredGroupMessage) error { coll := m.mongo.Collection(MongoGroupMessageCollection) _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true)) return errors.Wrap(err, "insert error") } -func (m *MongoDBImpl) InsertPrivateMessage(msg *db.StoredPrivateMessage) error { +func (m *database) InsertPrivateMessage(msg *db.StoredPrivateMessage) error { coll := m.mongo.Collection(MongoPrivateMessageCollection) _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true)) return errors.Wrap(err, "insert error") } -func (m *MongoDBImpl) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error { +func (m *database) InsertGuildChannelMessage(msg *db.StoredGuildChannelMessage) error { coll := m.mongo.Collection(MongoGuildChannelMessageCollection) _, err := coll.UpdateOne(context.Background(), bson.D{{"_id", msg.ID}}, bson.D{{"$set", msg}}, options.Update().SetUpsert(true)) return errors.Wrap(err, "insert error") diff --git a/db/multidb.go b/db/multidb.go index 6d3825c..68d0ab3 100644 --- a/db/multidb.go +++ b/db/multidb.go @@ -45,6 +45,7 @@ func Open() error { return errors.Wrap(err, "open backend error") } } + base.Database = nil return nil } diff --git a/docs/config.md b/docs/config.md index 3bd1369..d352b61 100644 --- a/docs/config.md +++ b/docs/config.md @@ -79,13 +79,6 @@ default-middlewares: &default frequency: 1 # 令牌回复频率, 单位秒 bucket: 1 # 令牌桶大小 -database: # 数据库相关设置 - leveldb: - # 是否启用内置leveldb数据库 - # 启用将会增加10-20MB的内存占用和一定的磁盘空间 - # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能 - enable: true - # 连接服务列表 servers: # HTTP 通信设置 diff --git a/global/buffer.go b/global/buffer.go index 73079f3..e899dae 100644 --- a/global/buffer.go +++ b/global/buffer.go @@ -2,26 +2,16 @@ package global import ( "bytes" - "sync" -) -var bufferPool = sync.Pool{ - New: func() interface{} { - return new(bytes.Buffer) - }, -} + "github.com/Mrs4s/MiraiGo/binary" // 和 MiraiGo 共用同一 buffer 池 +) // NewBuffer 从池中获取新 bytes.Buffer func NewBuffer() *bytes.Buffer { - return bufferPool.Get().(*bytes.Buffer) + return (*bytes.Buffer)(binary.SelectWriter()) } // PutBuffer 将 Buffer放入池中 func PutBuffer(buf *bytes.Buffer) { - // See https://golang.org/issue/23199 - const maxSize = 1 << 16 - if buf != nil && buf.Cap() < maxSize { // 对于大Buffer直接丢弃 - buf.Reset() - bufferPool.Put(buf) - } + binary.PutWriter((*binary.Writer)(buf)) } diff --git a/global/codec.go b/global/codec.go index 223c533..6a5f088 100644 --- a/global/codec.go +++ b/global/codec.go @@ -43,6 +43,6 @@ func EncodeMP4(src string, dst string) error { // -y 覆盖文件 // ExtractCover 获取给定视频文件的Cover func ExtractCover(src string, target string) error { - cmd := exec.Command("ffmpeg", "-i", src, "-y", "-r", "1", "-f", "image2", target) + cmd := exec.Command("ffmpeg", "-i", src, "-y", "-ss", "0", "-frames:v", "1", target) return errors.Wrap(cmd.Run(), "extract video cover failed") } diff --git a/global/fs.go b/global/fs.go index f469344..3c19771 100644 --- a/global/fs.go +++ b/global/fs.go @@ -5,12 +5,11 @@ import ( "crypto/md5" "encoding/hex" "errors" - "net" + "net/netip" "net/url" "os" "path" "runtime" - "strconv" "strings" "github.com/Mrs4s/MiraiGo/utils" @@ -22,27 +21,18 @@ import ( const ( // ImagePath go-cqhttp使用的图片缓存目录 ImagePath = "data/images" - // ImagePathOld 兼容旧版go-cqhttp使用的图片缓存目录 - ImagePathOld = "data/image" // VoicePath go-cqhttp使用的语音缓存目录 VoicePath = "data/voices" - // VoicePathOld 兼容旧版go-cqhttp使用的语音缓存目录 - VoicePathOld = "data/record" // VideoPath go-cqhttp使用的视频缓存目录 VideoPath = "data/videos" // CachePath go-cqhttp使用的缓存目录 CachePath = "data/cache" // DumpsPath go-cqhttp使用错误转储目录 DumpsPath = "dumps" -) - -var ( - // ErrSyntax Path语法错误时返回的错误 - ErrSyntax = errors.New("syntax error") // HeaderAmr AMR文件头 - HeaderAmr = []byte("#!AMR") + HeaderAmr = "#!AMR" // HeaderSilk Silkv3文件头 - HeaderSilk = []byte("\x02#!SILK_V3") + HeaderSilk = "\x02#!SILK_V3" ) // PathExists 判断给定path是否存在 @@ -78,13 +68,13 @@ func Check(err error, deleteSession bool) { // IsAMRorSILK 判断给定文件是否为Amr或Silk格式 func IsAMRorSILK(b []byte) bool { - return bytes.HasPrefix(b, HeaderAmr) || bytes.HasPrefix(b, HeaderSilk) + return bytes.HasPrefix(b, []byte(HeaderAmr)) || bytes.HasPrefix(b, []byte(HeaderSilk)) } // FindFile 从给定的File寻找文件,并返回文件byte数组。File是一个合法的URL。p为文件寻找位置。 // 对于HTTP/HTTPS形式的URL,Cache为"1"或空时表示启用缓存 func FindFile(file, cache, p string) (data []byte, err error) { - data, err = nil, ErrSyntax + data, err = nil, os.ErrNotExist switch { case strings.HasPrefix(file, "http"): // https also has prefix http hash := md5.Sum([]byte(file)) @@ -138,19 +128,18 @@ func DelFile(path string) bool { } // ReadAddrFile 从给定path中读取合法的IP地址与端口,每个IP地址以换行符"\n"作为分隔 -func ReadAddrFile(path string) []*net.TCPAddr { +func ReadAddrFile(path string) []netip.AddrPort { d, err := os.ReadFile(path) if err != nil { return nil } str := string(d) lines := strings.Split(str, "\n") - var ret []*net.TCPAddr + var ret []netip.AddrPort for _, l := range lines { - ip := strings.Split(strings.TrimSpace(l), ":") - if len(ip) == 2 { - port, _ := strconv.Atoi(ip[1]) - ret = append(ret, &net.TCPAddr{IP: net.ParseIP(ip[0]), Port: port}) + addr, err := netip.ParseAddrPort(l) + if err == nil { + ret = append(ret, addr) } } return ret diff --git a/global/log_hook.go b/global/log_hook.go index b93f130..44f4f1d 100644 --- a/global/log_hook.go +++ b/global/log_hook.go @@ -195,7 +195,8 @@ func (f LogFormat) Format(entry *logrus.Entry) ([]byte, error) { buf.WriteString(colorReset) } - ret := append([]byte(nil), buf.Bytes()...) // copy buffer + ret := make([]byte, len(buf.Bytes())) + copy(ret, buf.Bytes()) // copy buffer return ret, nil } diff --git a/global/net.go b/global/net.go index ce74edb..c72000a 100644 --- a/global/net.go +++ b/global/net.go @@ -79,7 +79,7 @@ func DownloadFile(url, path string, limit int64, headers map[string]string) erro if limit > 0 && resp.ContentLength > limit { return ErrOverSize } - _, err = io.Copy(file, resp.Body) + _, err = file.ReadFrom(resp.Body) if err != nil { return err } @@ -107,7 +107,7 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, return err } defer file.Close() - if _, err = io.Copy(file, s); err != nil { + if _, err = file.ReadFrom(s); err != nil { return err } return errUnsupportedMultiThreading diff --git a/go.mod b/go.mod index 852d52a..575c539 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,43 @@ module github.com/Mrs4s/go-cqhttp -go 1.17 +go 1.18 require ( - github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Microsoft/go-winio v0.5.1 - github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17 + github.com/Mrs4s/MiraiGo v0.0.0-20220605085305-ae33763fe10a + github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc - github.com/dustin/go-humanize v1.0.0 github.com/fumiama/go-hide-param v0.1.4 - github.com/gabriel-vasile/mimetype v1.4.0 - github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98 - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 - github.com/klauspost/compress v1.13.6 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/mattn/go-colorable v0.1.12 github.com/pkg/errors v0.9.1 github.com/segmentio/asm v1.1.3 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 - github.com/syndtr/goleveldb v1.0.0 - github.com/tidwall/gjson v1.12.1 + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/tidwall/gjson v1.14.0 github.com/wdvxdr1123/go-silk v0.0.0-20210316130616-d47b553def60 - go.mongodb.org/mongo-driver v1.8.1 + go.mongodb.org/mongo-driver v1.8.3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956 // indirect + github.com/RomiChan/protobuf v0.1.1-0.20220602121309-9e3b8cbefd7a // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fumiama/imgsz v0.0.2 // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/gocq/rs v1.0.1 // indirect - github.com/golang/snappy v0.0.1 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.5 // indirect - github.com/google/uuid v1.1.0 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/klauspost/compress v1.13.6 // indirect github.com/lestrrat-go/strftime v1.0.5 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/pierrec/lz4/v4 v4.1.11 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/go.sum b/go.sum index ba46225..9674779 100644 --- a/go.sum +++ b/go.sum @@ -1,49 +1,44 @@ -github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= -github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17 h1:OQVnlWt5if0TOFoNGDjILazBjVTncSlPxGXabD+4MvE= -github.com/Mrs4s/MiraiGo v0.0.0-20220209092529-5d071b034c17/go.mod h1:rtKLkhMEi2YjsrXaNztT4uagUOPBxf6a+TNREkG097I= -github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956 h1:hnaAkKz4t+xpSNVp5mnuloRMd3Rj2Lfg5biZ3emv//c= -github.com/RomiChan/protobuf v0.0.0-20211223055824-048df49a8956/go.mod h1:CKKOWC7mBxd36zxsCB1V8DTrwlTNRQvkSVbYqyUiGEE= +github.com/Mrs4s/MiraiGo v0.0.0-20220605085305-ae33763fe10a h1:DVrEVvLNKb53wTTduRdC+hZ80S/5G9qFmKYmCmIMVws= +github.com/Mrs4s/MiraiGo v0.0.0-20220605085305-ae33763fe10a/go.mod h1:mZp8Lt7uqLCUwSLouB2yuiP467Cwl4mnG9IMAaXUKA0= +github.com/RomiChan/protobuf v0.1.1-0.20220602121309-9e3b8cbefd7a h1:WIfEWYj82oEuPtm5pqlyQmCJCoiw00C6ugZFqHA0cC8= +github.com/RomiChan/protobuf v0.1.1-0.20220602121309-9e3b8cbefd7a/go.mod h1:2Ie+hdBFQpQFDHfeklgxoFmQRCE7O+KwFpISeXq7OwA= +github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c h1:cNPOdTNiVwxLpROLjXCgbIPvdkE+BwvxDvgmdYmWx6Q= +github.com/RomiChan/syncx v0.0.0-20220404072119-d7ea0ae15a4c/go.mod h1:KqZzu7slNKROh3TSYEH/IUMG6f4M+1qubZ5e52QypsE= github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc h1:AAx50/fb/xS4lvsdQg+bFbGvqSDhyV1MF+p2PLCamZ0= github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc/go.mod h1:OMmITAib6POA37xCichWM0aRnoVpSMZO1rB/G01wrr0= -github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY= -github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fumiama/go-hide-param v0.1.4 h1:y7TRTzZMdCH9GOXnIzU3B+1BSkcmvejVGmGsz4t0DGU= github.com/fumiama/go-hide-param v0.1.4/go.mod h1:vJkQlJIEI56nIyp7tCQu1/2QOyKtZpudsnJkGk9U1aY= github.com/fumiama/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak= github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4= -github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= -github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98 h1:NJDZEa7gibUa0w4tie8qKeQFKdeKFUbecWyQDPdRx40= -github.com/gocq/qrcode v0.0.0-20211114040510-366b953fcd98/go.mod h1:E5TBHc60dsWtOL7sbXCb3P9i4xrj2J7Zm5sEJftIc1w= -github.com/gocq/rs v1.0.1 h1:ng7nhXmnx3SnfM0DOqmbP6GmQp1xGwRG9XmBiLFDWuM= -github.com/gocq/rs v1.0.1/go.mod h1:8oaQnRvqn1fMh8i5zsetgQo03OUXksJV1k+dpmExxcY= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= -github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -63,11 +58,15 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pierrec/lz4/v4 v4.1.11 h1:LVs17FAZJFOjgmJXl9Tf13WfLUvZq7/RjfEJrnwZ9OE= github.com/pierrec/lz4/v4 v4.1.11/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -82,19 +81,16 @@ github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= -github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -110,19 +106,20 @@ github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyh github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -go.mongodb.org/mongo-driver v1.8.1 h1:OZE4Wni/SJlrcmSIBRYNzunX5TKxjrTS4jKSnA99oKU= -go.mongodb.org/mongo-driver v1.8.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= +go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -132,23 +129,27 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -156,22 +157,27 @@ golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13W golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/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-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/libc v1.8.1 h1:y9oPIhwcaFXxX7kMp6Qb2ZLKzr0mDkikWN3CV5GS63o= modernc.org/libc v1.8.1/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= diff --git a/internal/base/flag.go b/internal/base/flag.go index c6ff297..5e9bd7b 100644 --- a/internal/base/flag.go +++ b/internal/base/flag.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/Mrs4s/go-cqhttp/global" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -141,7 +141,8 @@ func ResetWorkingDir() { } } p, _ := filepath.Abs(os.Args[0]) - if !global.PathExists(p) { + _, err := os.Stat(p) + if !(err == nil || errors.Is(err, os.ErrExist)) { log.Fatalf("重置工作目录时出现错误: 无法找到路径 %v", p) } proc := exec.Command(p, args...) @@ -149,7 +150,7 @@ func ResetWorkingDir() { proc.Stdout = os.Stdout proc.Stderr = os.Stderr proc.Dir = wd - err := proc.Run() + err = proc.Run() if err != nil { panic(err) } diff --git a/internal/btree/btree.go b/internal/btree/btree.go index 7b3b49c..81adcb9 100644 --- a/internal/btree/btree.go +++ b/internal/btree/btree.go @@ -18,6 +18,10 @@ const ( tableStructSize = int(unsafe.Sizeof(table{})) ) +type fileLock interface { + release() error +} + type item struct { hash [hashSize]byte offset int64 @@ -48,6 +52,7 @@ type DB struct { alloc int64 cache [cacheSlots]cache + flock fileLock inAllocator bool deleteLarger bool fqueue [freeQueueLen]chunk @@ -108,6 +113,10 @@ func (d *DB) flushSuper() { // Open opens an existed btree file func Open(name string) (*DB, error) { + lock, err := newFileLock(name + ".lock") + if err != nil { + return nil, errors.New("文件被其他进程占用") + } btree := new(DB) fd, err := os.OpenFile(name, os.O_RDWR, 0o644) if err != nil { @@ -120,17 +129,23 @@ func Open(name string) (*DB, error) { btree.top = super.top btree.freeTop = super.freeTop btree.alloc = super.alloc + btree.flock = lock return btree, errors.Wrap(err, "btree read meta info failed") } // Create creates a database func Create(name string) (*DB, error) { + lock, err := newFileLock(name + ".lock") + if err != nil { + return nil, errors.New("文件被其他进程占用") + } btree := new(DB) fd, err := os.OpenFile(name, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o644) if err != nil { return nil, errors.Wrap(err, "btree open file failed") } + btree.flock = lock btree.fd = fd btree.alloc = int64(superSize) btree.flushSuper() @@ -140,6 +155,9 @@ func Create(name string) (*DB, error) { // Close closes the database func (d *DB) Close() error { _ = d.fd.Sync() + if err := d.flock.release(); err != nil { + return err + } err := d.fd.Close() for i := 0; i < cacheSlots; i++ { d.cache[i] = cache{} diff --git a/internal/btree/btree_test.go b/internal/btree/btree_test.go index 5d16c54..464c7ac 100644 --- a/internal/btree/btree_test.go +++ b/internal/btree/btree_test.go @@ -16,16 +16,21 @@ func tempfile(t *testing.T) string { return temp.Name() } +func removedb(name string) { + os.Remove(name) + os.Remove(name + ".lock") +} + func TestCreate(t *testing.T) { f := tempfile(t) _, err := Create(f) assert2.NoError(t, err) - defer os.Remove(f) + defer removedb(f) } func TestBtree(t *testing.T) { f := tempfile(t) - defer os.Remove(f) + defer removedb(f) bt, err := Create(f) assert := assert2.New(t) assert.NoError(err) @@ -73,7 +78,7 @@ func testForeach(t *testing.T, elemSize int) { expected[i] = utils.RandomString(20) } f := tempfile(t) - defer os.Remove(f) + defer removedb(f) bt, err := Create(f) assert2.NoError(t, err) for _, v := range expected { diff --git a/internal/btree/file_lock_unix.go b/internal/btree/file_lock_unix.go new file mode 100644 index 0000000..8974284 --- /dev/null +++ b/internal/btree/file_lock_unix.go @@ -0,0 +1,45 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd + +package btree + +import ( + "os" + "syscall" +) + +type unixFileLock struct { + f *os.File +} + +func (fl *unixFileLock) release() error { + if err := setFileLock(fl.f, false); err != nil { + return err + } + return fl.f.Close() +} + +func newFileLock(path string) (fl fileLock, err error) { + flag := os.O_RDWR + f, err := os.OpenFile(path, flag, 0) + if os.IsNotExist(err) { + f, err = os.OpenFile(path, flag|os.O_CREATE, 0644) + } + if err != nil { + return + } + err = setFileLock(f, true) + if err != nil { + f.Close() + return + } + fl = &unixFileLock{f: f} + return +} + +func setFileLock(f *os.File, lock bool) error { + how := syscall.LOCK_UN + if lock { + how = syscall.LOCK_EX + } + return syscall.Flock(int(f.Fd()), how|syscall.LOCK_NB) +} diff --git a/internal/btree/file_lock_windows.go b/internal/btree/file_lock_windows.go new file mode 100644 index 0000000..becdfad --- /dev/null +++ b/internal/btree/file_lock_windows.go @@ -0,0 +1,28 @@ +package btree + +import "syscall" + +type windowsFileLock struct { + fd syscall.Handle +} + +func (fl *windowsFileLock) release() error { + return syscall.Close(fl.fd) +} + +func newFileLock(path string) (fileLock, error) { + pathp, err := syscall.UTF16PtrFromString(path) + if err != nil { + return nil, err + } + + const access uint32 = syscall.GENERIC_READ | syscall.GENERIC_WRITE + fd, err := syscall.CreateFile(pathp, access, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0) + if err == syscall.ERROR_FILE_NOT_FOUND { + fd, err = syscall.CreateFile(pathp, access, 0, nil, syscall.OPEN_ALWAYS, syscall.FILE_ATTRIBUTE_NORMAL, 0) + } + if err != nil { + return nil, err + } + return &windowsFileLock{fd: fd}, nil +} diff --git a/internal/param/param.go b/internal/param/param.go index c38bf1c..fd86b74 100644 --- a/internal/param/param.go +++ b/internal/param/param.go @@ -30,7 +30,7 @@ func EnsureBool(p interface{}, defaultVal bool) bool { if !j.Exists() { return defaultVal } - switch j.Type { // nolint + switch j.Type { // nolint: exhaustive case gjson.True: return true case gjson.False: diff --git a/internal/selfupdate/update.go b/internal/selfupdate/update.go index 2a05f60..8771502 100644 --- a/internal/selfupdate/update.go +++ b/internal/selfupdate/update.go @@ -7,13 +7,12 @@ import ( "fmt" "hash" "io" + "math" "os" "path/filepath" "runtime" "strings" - "github.com/dustin/go-humanize" - "github.com/kardianos/osext" "github.com/sirupsen/logrus" "github.com/tidwall/gjson" @@ -147,13 +146,33 @@ func (wc *writeSumCounter) Write(p []byte) (int, error) { wc.total += uint64(n) wc.hash.Write(p) fmt.Printf("\r ") - fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.total)) + fmt.Printf("\rDownloading... %s complete", humanBytes(wc.total)) return n, nil } +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanBytes(s uint64) string { + sizes := []string{"B", "kB", "MB", "GB"} // GB对于go-cqhttp来说已经够用了 + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), 1000)) + suffix := sizes[int(e)] + val := math.Floor(float64(s)/math.Pow(1000, e)*10+0.5) / 10 + f := "%.0f %s" + if val < 10 { + f = "%.1f %s" + } + return fmt.Sprintf(f, val, suffix) +} + // FromStream copy form getlantern/go-update func fromStream(updateWith io.Reader) (err error, errRecover error) { - updatePath, err := osext.Executable() + updatePath, err := os.Executable() + updatePath = filepath.Clean(updatePath) if err != nil { return } @@ -169,7 +188,7 @@ func fromStream(updateWith io.Reader) (err error, errRecover error) { } // We won't log this error, because it's always going to happen. defer func() { _ = fp.Close() }() - if _, err = io.Copy(fp, bufio.NewReader(updateWith)); err != nil { + if _, err = bufio.NewReader(updateWith).WriteTo(fp); err != nil { logrus.Errorf("Unable to copy data: %v\n", err) } diff --git a/internal/selfupdate/update_others.go b/internal/selfupdate/update_others.go index b18bb6b..1bd535d 100644 --- a/internal/selfupdate/update_others.go +++ b/internal/selfupdate/update_others.go @@ -6,12 +6,11 @@ package selfupdate import ( "archive/tar" "bytes" + "compress/gzip" "crypto/sha256" "errors" "io" "net/http" - - "github.com/klauspost/compress/gzip" ) // update go-cqhttp自我更新 diff --git a/internal/selfupdate/update_windows.go b/internal/selfupdate/update_windows.go index 235b48f..cb32e1a 100644 --- a/internal/selfupdate/update_windows.go +++ b/internal/selfupdate/update_windows.go @@ -1,13 +1,12 @@ package selfupdate import ( + "archive/zip" "bytes" "crypto/sha256" "errors" "io" "net/http" - - "github.com/klauspost/compress/zip" ) // update go-cqhttp自我更新 diff --git a/main.go b/main.go index 09cd2f0..0cd6079 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,12 @@ package main import ( "github.com/Mrs4s/go-cqhttp/cmd/gocq" - _ "github.com/Mrs4s/go-cqhttp/db/leveldb" // leveldb - _ "github.com/Mrs4s/go-cqhttp/modules/mime" // mime检查模块 - _ "github.com/Mrs4s/go-cqhttp/modules/pprof" // pprof 性能分析 - _ "github.com/Mrs4s/go-cqhttp/modules/silk" // silk编码模块 + _ "github.com/Mrs4s/go-cqhttp/db/leveldb" // leveldb + _ "github.com/Mrs4s/go-cqhttp/modules/mime" // mime检查模块 + _ "github.com/Mrs4s/go-cqhttp/modules/silk" // silk编码模块 + // 其他模块 + // _ "github.com/Mrs4s/go-cqhttp/db/mongodb" // mongodb 数据库支持 + // _ "github.com/Mrs4s/go-cqhttp/modules/pprof" // pprof 性能分析 ) func main() { diff --git a/modules/api/api.go b/modules/api/api.go index 5ece476..ae4e928 100644 --- a/modules/api/api.go +++ b/modules/api/api.go @@ -21,12 +21,12 @@ func (c *Caller) call(action string, p Getter) global.MSG { case ".ocr_image", "ocr_image": p0 := p.Get("image").String() return c.bot.CQOcrImage(p0) + case "_get_group_notice": + p0 := p.Get("group_id").Int() + return c.bot.CQGetGroupMemo(p0) case "_get_model_show": p0 := p.Get("model").String() return c.bot.CQGetModelShow(p0) - case "_get_vip_info": - p0 := p.Get("user_id").Int() - return c.bot.CQGetVipInfo(p0) case "_send_group_notice": p0 := p.Get("group_id").Int() p1 := p.Get("content").String() @@ -195,6 +195,12 @@ func (c *Caller) call(action string, p Getter) global.MSG { case "reload_event_filter": p0 := p.Get("file").String() return c.bot.CQReloadEventFilter(p0) + case "send_forward_msg": + p0 := p.Get("group_id").Int() + p1 := p.Get("user_id").Int() + p2 := p.Get("messages") + p3 := p.Get("message_type").String() + return c.bot.CQSendForwardMessage(p0, p1, p2, p3) case "send_group_forward_msg": p0 := p.Get("group_id").Int() p1 := p.Get("messages") @@ -204,6 +210,9 @@ func (c *Caller) call(action string, p Getter) global.MSG { p1 := p.Get("message") p2 := p.Get("auto_escape").Bool() return c.bot.CQSendGroupMessage(p0, p1, p2) + case "send_group_sign": + p0 := p.Get("group_id").Int() + return c.bot.CQSendGroupSign(p0) case "send_guild_channel_msg": p0 := p.Get("guild_id").Uint() p1 := p.Get("channel_id").Uint() @@ -217,6 +226,10 @@ func (c *Caller) call(action string, p Getter) global.MSG { p3 := p.Get("message_type").String() p4 := p.Get("auto_escape").Bool() return c.bot.CQSendMessage(p0, p1, p2, p3, p4) + case "send_private_forward_msg": + p0 := p.Get("user_id").Int() + p1 := p.Get("messages") + return c.bot.CQSendPrivateForwardMessage(p0, p1) case "send_private_msg": p0 := p.Get("user_id").Int() p1 := p.Get("group_id").Int() @@ -304,6 +317,13 @@ func (c *Caller) call(action string, p Getter) global.MSG { p2 := p.Get("role_id").Uint() p3 := p.Get("users") return c.bot.CQSetGuildMemberRole(p0, p1, p2, p3) + case "set_qq_profile": + p0 := p.Get("nickname") + p1 := p.Get("company") + p2 := p.Get("email") + p3 := p.Get("college") + p4 := p.Get("personal_note") + return c.bot.CQSetQQProfile(p0, p1, p2, p3, p4) case "update_guild_role": p0 := p.Get("guild_id").Uint() p1 := p.Get("role_id").Uint() diff --git a/modules/config/config.go b/modules/config/config.go index 84c4a3e..23329d7 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -8,13 +8,13 @@ import ( "os" "regexp" "strings" - "sync" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) // defaultConfig 默认配置文件 +// //go:embed default_config.yml var defaultConfig string @@ -75,18 +75,6 @@ type Server struct { Default string } -// LevelDBConfig leveldb 相关配置 -type LevelDBConfig struct { - Enable bool `yaml:"enable"` -} - -// MongoDBConfig mongodb 相关配置 -type MongoDBConfig struct { - Enable bool `yaml:"enable"` - URI string `yaml:"uri"` - Database string `yaml:"database"` -} - // Parse 从默认配置文件路径中获取 func Parse(path string) *Config { file, err := os.ReadFile(path) @@ -103,16 +91,11 @@ func Parse(path string) *Config { return config } -var ( - serverconfs []*Server - mu sync.Mutex -) +var serverconfs []*Server // AddServer 添加该服务的简介和默认配置 func AddServer(s *Server) { - mu.Lock() serverconfs = append(serverconfs, s) - mu.Unlock() } // generateConfig 生成配置文件 @@ -155,8 +138,7 @@ func expand(s string, mapping func(string) string) string { r := regexp.MustCompile(`\${([a-zA-Z_]+[a-zA-Z0-9_:/.]*)}`) return r.ReplaceAllStringFunc(s, func(s string) string { s = strings.Trim(s, "${}") - // todo: use strings.Cut once go1.18 is released - before, after, ok := cut(s, ":") + before, after, ok := strings.Cut(s, ":") m := mapping(before) if ok && m == "" { return after @@ -164,10 +146,3 @@ func expand(s string, mapping func(string) string) string { return m }) } - -func cut(s, sep string) (before, after string, found bool) { - if i := strings.Index(s, sep); i >= 0 { - return s[:i], s[i+len(sep):], true - } - return s, "", false -} diff --git a/modules/mime/mime.go b/modules/mime/mime.go index 50d762d..dbaa831 100644 --- a/modules/mime/mime.go +++ b/modules/mime/mime.go @@ -3,68 +3,51 @@ package mime import ( "io" - - "github.com/gabriel-vasile/mimetype" - "github.com/sirupsen/logrus" + "net/http" + "strings" "github.com/Mrs4s/go-cqhttp/internal/base" ) func init() { - base.IsLawfulAudio = checkImage + base.IsLawfulImage = checkImage base.IsLawfulAudio = checkAudio } -// keep sync with /docs/file.md#MINE -var lawfulImage = [...]string{ - "image/bmp", - "image/gif", - "image/jpeg", - "image/png", - "image/webp", -} +const limit = 4 * 1024 -var lawfulAudio = [...]string{ - "audio/aac", - "audio/aiff", - "audio/amr", - "audio/ape", - "audio/flac", - "audio/midi", - "audio/mp4", - "audio/mpeg", - "audio/ogg", - "audio/wav", - "audio/x-m4a", -} - -func check(r io.ReadSeeker, list []string) (bool, string) { - if base.SkipMimeScan { - return true, "" - } +func scan(r io.ReadSeeker) string { _, _ = r.Seek(0, io.SeekStart) defer r.Seek(0, io.SeekStart) - t, err := mimetype.DetectReader(r) - if err != nil { - logrus.Debugf("扫描 Mime 时出现问题: %v", err) - return false, "" - } - for _, lt := range list { - if t.Is(lt) { - return true, t.String() - } - } - return false, t.String() + in := make([]byte, limit) + _, _ = r.Read(in) + return http.DetectContentType(in) } // checkImage 判断给定流是否为合法图片 // 返回 是否合法, 实际Mime // 判断后会自动将 Stream Seek 至 0 -func checkImage(r io.ReadSeeker) (bool, string) { - return check(r, lawfulImage[:]) +func checkImage(r io.ReadSeeker) (ok bool, t string) { + if base.SkipMimeScan { + return true, "" + } + t = scan(r) + switch t { + case "image/bmp", "image/gif", "image/jpeg", "image/png", "image/webp": + ok = true + } + return } // checkImage 判断给定流是否为合法音频 func checkAudio(r io.ReadSeeker) (bool, string) { - return check(r, lawfulAudio[:]) + if base.SkipMimeScan { + return true, "" + } + t := scan(r) + // std mime type detection is not full supported for audio + if strings.Contains(t, "text") || strings.Contains(t, "image") { + return false, t + } + return true, t } diff --git a/modules/servers/servers.go b/modules/servers/servers.go index 7a8ae28..e1c28c4 100644 --- a/modules/servers/servers.go +++ b/modules/servers/servers.go @@ -43,4 +43,5 @@ func Run(bot *coolq.CQBot) { for _, fn := range nocfgsvr { go fn(bot) } + base.Servers = nil } diff --git a/server/daemon.go b/server/daemon.go index f2746d0..0a570b6 100644 --- a/server/daemon.go +++ b/server/daemon.go @@ -3,9 +3,9 @@ package server // daemon 功能写在这,目前仅支持了-d 作为后台运行参数,stop,start,restart这些功能目前看起来并不需要,可以通过api控制,后续需要的话再补全。 import ( - "fmt" "os" "os/exec" + "strconv" "strings" "github.com/Mrs4s/go-cqhttp/global" @@ -36,7 +36,7 @@ func Daemon() { log.Info("[PID] ", proc.Process.Pid) // pid写入到pid文件中,方便后续stop的时候kill - pidErr := savePid("go-cqhttp.pid", fmt.Sprintf("%d", proc.Process.Pid)) + pidErr := savePid("go-cqhttp.pid", strconv.FormatInt(int64(proc.Process.Pid), 10)) if pidErr != nil { log.Errorf("save pid file error: %v", pidErr) } diff --git a/server/http.go b/server/http.go index 6971f05..3fd7c37 100644 --- a/server/http.go +++ b/server/http.go @@ -2,12 +2,15 @@ package server import ( "bytes" + "context" "crypto/hmac" "crypto/sha1" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -31,6 +34,7 @@ import ( // HTTPServer HTTP通信相关配置 type HTTPServer struct { Disabled bool `yaml:"disabled"` + Address string `yaml:"address"` Host string `yaml:"host"` Port int `yaml:"port"` Timeout int32 `yaml:"timeout"` @@ -51,7 +55,6 @@ type httpServerPost struct { } type httpServer struct { - HTTP *http.Server api *api.Caller accessToken string } @@ -64,6 +67,7 @@ type HTTPClient struct { filter string apiPort int timeout int32 + client *http.Client MaxRetries uint64 RetriesInterval uint64 } @@ -76,8 +80,7 @@ type httpCtx struct { const httpDefault = ` - http: # HTTP 通信设置 - host: 127.0.0.1 # 服务端监听地址 - port: 5700 # 服务端监听端口 + address: 0.0.0.0:5700 # HTTP监听地址 timeout: 5 # 反向 HTTP 超时时间, 单位秒,<5 时将被忽略 long-polling: # 长轮询拓展 enabled: false # 是否开启 @@ -87,11 +90,11 @@ const httpDefault = ` post: # 反向HTTP POST地址列表 #- url: '' # 地址 # secret: '' # 密钥 - # max-retries: 3 # 最大重试,0 时禁用 + # max-retries: 3 # 最大重试,0 时禁用 # retries-interval: 1500 # 重试时间,单位毫秒,0 时立即 #- url: http://127.0.0.1:5701/ # 地址 # secret: '' # 密钥 - # max-retries: 10 # 最大重试,0 时禁用 + # max-retries: 10 # 最大重试,0 时禁用 # retries-interval: 1000 # 重试时间,单位毫秒,0 时立即 ` @@ -209,9 +212,9 @@ func checkAuth(req *http.Request, token string) int { if auth == "" { auth = req.URL.Query().Get("access_token") } else { - authN := strings.SplitN(auth, " ", 2) - if len(authN) == 2 { - auth = authN[1] + _, after, ok := strings.Cut(auth, " ") + if ok { + auth = after } } @@ -243,12 +246,21 @@ func runHTTP(bot *coolq.CQBot, node yaml.Node) { return } - var addr string + network, addr := "tcp", conf.Address s := &httpServer{accessToken: conf.AccessToken} - if conf.Host == "" || conf.Port == 0 { + switch { + case conf.Address != "": + uri, err := url.Parse(conf.Address) + if err == nil && uri.Scheme != "" { + network = uri.Scheme + addr = uri.Host + uri.Path + } + case conf.Host != "" || conf.Port != 0: + addr = fmt.Sprintf("%s:%d", conf.Host, conf.Port) + log.Warnln("HTTP 服务器使用了过时的配置格式,请更新配置文件!") + default: goto client } - addr = fmt.Sprintf("%s:%d", conf.Host, conf.Port) s.api = api.NewCaller(bot) if conf.RateLimit.Enabled { s.api.Use(rateLimit(conf.RateLimit.Frequency, conf.RateLimit.Bucket)) @@ -256,20 +268,16 @@ func runHTTP(bot *coolq.CQBot, node yaml.Node) { if conf.LongPolling.Enabled { s.api.Use(longPolling(bot, conf.LongPolling.MaxQueueSize)) } - go func() { - log.Infof("CQ HTTP 服务器已启动: %v", addr) - s.HTTP = &http.Server{ - Addr: addr, - Handler: s, - } - if err := s.HTTP.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Error(err) - log.Infof("HTTP 服务启动失败, 请检查端口是否被占用.") + listener, err := net.Listen(network, addr) + if err != nil { + log.Infof("HTTP 服务启动失败, 请检查端口是否被占用: %v", err) log.Warnf("将在五秒后退出.") time.Sleep(time.Second * 5) os.Exit(1) } + log.Infof("CQ HTTP 服务器已启动: %v", listener.Addr()) + log.Fatal(http.Serve(listener, s)) }() client: for _, c := range conf.Post { @@ -294,8 +302,30 @@ func (c HTTPClient) Run() { if c.timeout < 5 { c.timeout = 5 } + rawAddress := c.addr + network, address := resolveURI(c.addr) + client := &http.Client{ + Timeout: time.Second * time.Duration(c.timeout), + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, addr string) (net.Conn, error) { + if network == "unix" { + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr + } + filepath, err := base64.RawURLEncoding.DecodeString(host) + if err == nil { + addr = string(filepath) + } + } + return net.Dial(network, addr) + }, + }, + } + c.addr = address // clean path + c.client = client + log.Infof("HTTP POST上报器已启动: %v", rawAddress) c.bot.OnEventPush(c.onBotPushEvent) - log.Infof("HTTP POST上报器已启动: %v", c.addr) } func (c *HTTPClient) onBotPushEvent(e *coolq.Event) { @@ -307,7 +337,6 @@ func (c *HTTPClient) onBotPushEvent(e *coolq.Event) { } } - client := http.Client{Timeout: time.Second * time.Duration(c.timeout)} header := make(http.Header) header.Set("X-Self-ID", strconv.FormatInt(c.bot.Client.Uin, 10)) header.Set("User-Agent", "CQHttp/4.15.0") @@ -321,22 +350,19 @@ func (c *HTTPClient) onBotPushEvent(e *coolq.Event) { header.Set("X-API-Port", strconv.FormatInt(int64(c.apiPort), 10)) } + var req *http.Request var res *http.Response + var err error for i := uint64(0); i <= c.MaxRetries; i++ { // see https://stackoverflow.com/questions/31337891/net-http-http-contentlength-222-with-body-length-0 // we should create a new request for every single post trial - req, err := http.NewRequest("POST", c.addr, bytes.NewReader(e.JSONBytes())) + req, err = http.NewRequest("POST", c.addr, bytes.NewReader(e.JSONBytes())) if err != nil { log.Warnf("上报 Event 数据到 %v 时创建请求失败: %v", c.addr, err) return } req.Header = header - - res, err = client.Do(req) - if res != nil { - //goland:noinspection GoDeferInLoop - defer res.Body.Close() - } + res, err = c.client.Do(req) if err == nil { break } @@ -348,9 +374,9 @@ func (c *HTTPClient) onBotPushEvent(e *coolq.Event) { } time.Sleep(time.Millisecond * time.Duration(c.RetriesInterval)) } + defer res.Body.Close() log.Debugf("上报Event数据 %s 到 %v", e.JSONBytes(), c.addr) - r, err := io.ReadAll(res.Body) if err != nil { return diff --git a/server/middlewares.go b/server/middlewares.go index d7835a4..7230248 100644 --- a/server/middlewares.go +++ b/server/middlewares.go @@ -39,7 +39,7 @@ func longPolling(bot *coolq.CQBot, maxSize int) api.Handler { bot.OnEventPush(func(event *coolq.Event) { mutex.Lock() defer mutex.Unlock() - queue.PushBack(event.RawMsg) + queue.PushBack(event.Raw) for maxSize != 0 && queue.Len() > maxSize { queue.Remove(queue.Front()) } @@ -50,33 +50,37 @@ func longPolling(bot *coolq.CQBot, maxSize int) api.Handler { return nil } var ( - once sync.Once - ch = make(chan []interface{}, 1) + ch = make(chan []interface{}) timeout = time.Duration(p.Get("timeout").Int()) * time.Second ) - defer close(ch) go func() { mutex.Lock() defer mutex.Unlock() - if queue.Len() == 0 { + for queue.Len() == 0 { cond.Wait() } - once.Do(func() { - limit := int(p.Get("limit").Int()) - if limit <= 0 || queue.Len() < limit { - limit = queue.Len() + limit := int(p.Get("limit").Int()) + if limit <= 0 || queue.Len() < limit { + limit = queue.Len() + } + ret := make([]interface{}, limit) + elem := queue.Front() + for i := 0; i < limit; i++ { + ret[i] = elem.Value + elem = elem.Next() + } + select { + case ch <- ret: + for i := 0; i < limit; i++ { // remove sent msg + queue.Remove(queue.Front()) } - ret := make([]interface{}, limit) - for i := 0; i < limit; i++ { - ret[i] = queue.Remove(queue.Front()) - } - ch <- ret - }) + default: + // don't block if parent already return due to timeout + } }() if timeout != 0 { select { case <-time.After(timeout): - once.Do(func() {}) return coolq.OK([]interface{}{}) case ret := <-ch: return coolq.OK(ret) diff --git a/server/scf.go b/server/scf.go index 29546ae..7d95e7f 100644 --- a/server/scf.go +++ b/server/scf.go @@ -54,7 +54,7 @@ func (l *lambdaResponseWriter) flush() error { buffer := global.NewBuffer() defer global.PutBuffer(buffer) body := utils.B2S(l.buf.Bytes()) - header := make(map[string]string) + header := make(map[string]string, len(l.header)) for k, v := range l.header { header[k] = v[0] } @@ -190,9 +190,9 @@ func (c *lambdaClient) next() *http.Request { if resp.StatusCode != http.StatusOK { return nil } - req := new(http.Request) - invoke := new(lambdaInvoke) - _ = json.NewDecoder(resp.Body).Decode(invoke) + var req http.Request + var invoke lambdaInvoke + _ = json.NewDecoder(resp.Body).Decode(&invoke) if invoke.HTTPMethod == "" { // 不是 api 网关 return nil } @@ -211,5 +211,5 @@ func (c *lambdaClient) next() *http.Request { query[k] = []string{v} } req.URL.RawQuery = query.Encode() - return req + return &req } diff --git a/server/websocket.go b/server/websocket.go index 20d3a73..f600017 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -2,9 +2,12 @@ package server import ( "bytes" + "encoding/base64" "encoding/json" "fmt" + "net" "net/http" + "net/url" "runtime/debug" "strconv" "strings" @@ -75,9 +78,7 @@ var upgrader = websocket.Upgrader{ const wsDefault = ` # 正向WS设置 - ws: # 正向WS服务器监听地址 - host: 127.0.0.1 - # 正向WS服务器监听端口 - port: 6700 + address: 0.0.0.0:8080 middlewares: <<: *default # 引用默认中间件 ` @@ -100,6 +101,7 @@ const wsReverseDefault = ` # 反向WS设置 // WebsocketServer 正向WS相关配置 type WebsocketServer struct { Disabled bool `yaml:"disabled"` + Address string `yaml:"address"` Host string `yaml:"host"` Port int `yaml:"port"` @@ -139,6 +141,17 @@ func runWSServer(b *coolq.CQBot, node yaml.Node) { return } + network, address := "tcp", conf.Address + if conf.Address == "" && (conf.Host != "" || conf.Port != 0) { + log.Warn("正向 Websocket 使用了过时的配置格式,请更新配置文件") + address = fmt.Sprintf("%s:%d", conf.Host, conf.Port) + } else { + uri, err := url.Parse(conf.Address) + if err == nil && uri.Scheme != "" { + network = uri.Scheme + address = uri.Host + uri.Path + } + } s := &webSocketServer{ bot: b, conf: &conf, @@ -146,7 +159,6 @@ func runWSServer(b *coolq.CQBot, node yaml.Node) { filter: conf.Filter, } filter.Add(s.filter) - addr := fmt.Sprintf("%s:%d", conf.Host, conf.Port) s.handshake = fmt.Sprintf(`{"_post_method":2,"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, b.Client.Uin, time.Now().Unix()) b.OnEventPush(s.onBotPushEvent) @@ -154,8 +166,12 @@ func runWSServer(b *coolq.CQBot, node yaml.Node) { mux.HandleFunc("/event", s.event) mux.HandleFunc("/api", s.api) mux.HandleFunc("/", s.any) - log.Infof("CQ WebSocket 服务器已启动: %v", addr) - log.Fatal(http.ListenAndServe(addr, &mux)) + listener, err := net.Listen(network, address) + if err != nil { + log.Fatal(err) + } + log.Infof("CQ WebSocket 服务器已启动: %v", listener.Addr()) + log.Fatal(http.Serve(listener, &mux)) } // runWSClient 运行一个反向向WS client @@ -196,8 +212,26 @@ func runWSClient(b *coolq.CQBot, node yaml.Node) { } } -func (c *websocketClient) connect(typ, url string, conptr **wsConn) { - log.Infof("开始尝试连接到反向WebSocket %s服务器: %v", typ, url) +func resolveURI(addr string) (network, address string) { + network, address = "tcp", addr + uri, err := url.Parse(addr) + if err == nil && uri.Scheme != "" { + scheme, ext, _ := strings.Cut(uri.Scheme, "+") + if ext != "" { + network = ext + uri.Scheme = scheme // remove `+unix`/`+tcp4` + if ext == "unix" { + uri.Host, uri.Path, _ = strings.Cut(uri.Path, ":") + uri.Host = base64.StdEncoding.EncodeToString([]byte(uri.Host)) + } + address = uri.String() + } + } + return +} + +func (c *websocketClient) connect(typ, addr string, conptr **wsConn) { + log.Infof("开始尝试连接到反向WebSocket %s服务器: %v", typ, addr) header := http.Header{ "X-Client-Role": []string{typ}, "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)}, @@ -206,12 +240,30 @@ func (c *websocketClient) connect(typ, url string, conptr **wsConn) { if c.token != "" { header["Authorization"] = []string{"Token " + c.token} } - conn, _, err := websocket.DefaultDialer.Dial(url, header) // nolint + + network, address := resolveURI(addr) + dialer := websocket.Dialer{ + NetDial: func(_, addr string) (net.Conn, error) { + if network == "unix" { + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr + } + filepath, err := base64.RawURLEncoding.DecodeString(host) + if err == nil { + addr = string(filepath) + } + } + return net.Dial(network, addr) // support unix socket transport + }, + } + + conn, _, err := dialer.Dial(address, header) // nolint if err != nil { - log.Warnf("连接到反向WebSocket %s服务器 %v 时出现错误: %v", typ, url, err) + log.Warnf("连接到反向WebSocket %s服务器 %v 时出现错误: %v", typ, addr, err) if c.reconnectInterval != 0 { time.Sleep(c.reconnectInterval) - c.connect(typ, url, conptr) + c.connect(typ, addr, conptr) } return } @@ -225,7 +277,7 @@ func (c *websocketClient) connect(typ, url string, conptr **wsConn) { } } - log.Infof("已连接到反向WebSocket %s服务器 %v", typ, url) + log.Infof("已连接到反向WebSocket %s服务器 %v", typ, addr) var wrappedConn *wsConn if conptr != nil && *conptr != nil { @@ -244,7 +296,7 @@ func (c *websocketClient) connect(typ, url string, conptr **wsConn) { } if typ != "Event" { - go c.listenAPI(typ, url, wrappedConn) + go c.listenAPI(typ, addr, wrappedConn) } }