From af82abd3b0a627be7f381df305c5f1448270dfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Thu, 16 Nov 2023 01:31:07 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20QQ=E3=82=B5=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api_generated.go | 54 ++++++++--- bot.go | 8 +- codegen/context/main.go | 3 + context.go | 90 ++++++++++++++++++- event.go | 32 +++++-- handler.go | 13 +++ message.go | 20 +++++ openapi.go | 2 +- openapi_codegen_postopenapiof.go | 12 +++ openapi_v2.go | 14 +++ openapi_v2_files.go | 75 ++++++++++++++++ openapi_v2_message.go | 150 +++++++++++++++++++++++++++++++ openapi_wss.go | 12 +-- rules.go | 30 +++++-- 14 files changed, 476 insertions(+), 39 deletions(-) create mode 100644 openapi_v2.go create mode 100644 openapi_v2_files.go create mode 100644 openapi_v2_message.go diff --git a/api_generated.go b/api_generated.go index c4182c2..fda7bc2 100644 --- a/api_generated.go +++ b/api_generated.go @@ -476,20 +476,46 @@ func (ctx *Ctx) GetMyGuilds(before, after string, limit int) (guilds []Guild, er /* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_user.go ^^^^^^^^^^^^^^^^^^^^ */ +/* vvvvvvvvvvvvvvvvvvvv 生成自文件 openapi_v2.go vvvvvvvvvvvvvvvvvvvvv */ + +/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_v2.go ^^^^^^^^^^^^^^^^^^^^ */ + +/* vvvvvvvvvvvvvvvvvvvv 生成自文件 openapi_v2_files.go vvvvvvvvvvvvvvvvvvvvv */ + +// PostFileToQQUser 发送文件到 QQ 用户的 openid +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html#%E5%8F%91%E9%80%81%E5%88%B0%E5%8D%95%E8%81%8A +func (ctx *Ctx) PostFileToQQUser(id string, content *FilePost) (*IDTimestampMessageResult, error) { + return ctx.caller.PostFileToQQUser(id, content) +} + +// PostFileToQQGroup 发送文件到 QQ 群的 openid +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html#%E5%8F%91%E9%80%81%E5%88%B0%E7%BE%A4%E8%81%8A +func (ctx *Ctx) PostFileToQQGroup(id string, content *FilePost) (*IDTimestampMessageResult, error) { + return ctx.caller.PostFileToQQGroup(id, content) +} + +/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_v2_files.go ^^^^^^^^^^^^^^^^^^^^ */ + +/* vvvvvvvvvvvvvvvvvvvv 生成自文件 openapi_v2_message.go vvvvvvvvvvvvvvvvvvvvv */ + +// PostMessageToQQUser 向 openid 指定的用户发送消息 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/send.html#%E5%8D%95%E8%81%8A +func (ctx *Ctx) PostMessageToQQUser(id string, content *MessagePostV2) (*IDTimestampMessageResult, error) { + return ctx.caller.PostMessageToQQUser(id, content) +} + +// PostMessageToQQGroup 向 openid 指定的群发送消息 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/send.html#%E7%BE%A4%E8%81%8A +func (ctx *Ctx) PostMessageToQQGroup(id string, content *MessagePostV2) (*IDTimestampMessageResult, error) { + return ctx.caller.PostMessageToQQGroup(id, content) +} + +/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_v2_message.go ^^^^^^^^^^^^^^^^^^^^ */ + /* vvvvvvvvvvvvvvvvvvvv 生成自文件 openapi_wss.go vvvvvvvvvvvvvvvvvvvvv */ -// GetGeneralWSSGateway 获取通用 WSS 接入点 -// -// https://bot.q.qq.com/wiki/develop/api/openapi/wss/url_get.html -func (ctx *Ctx) GetGeneralWSSGateway() (string, error) { - return ctx.caller.GetGeneralWSSGateway() -} - -// GetShardWSSGateway 获取带分片 WSS 接入点 -// -// https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html -func (ctx *Ctx) GetShardWSSGateway() (*ShardWSSGateway, error) { - return ctx.caller.GetShardWSSGateway() -} - /* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_wss.go ^^^^^^^^^^^^^^^^^^^^ */ diff --git a/bot.go b/bot.go index 661c14c..16e0089 100644 --- a/bot.go +++ b/bot.go @@ -68,13 +68,13 @@ func (bot *Bot) getinitinfo() (secret, gw string, shard [2]byte, err error) { bot.Secret = "" } if bot.ShardIndex == 0 { - gw, err = bot.GetGeneralWSSGateway() + gw, err = bot.GetGeneralWSSGatewayNoContext() if err != nil { return } } else { var sgw *ShardWSSGateway - sgw, err = bot.GetShardWSSGateway() + sgw, err = bot.GetShardWSSGatewayNoContext() if err != nil { return } @@ -178,7 +178,7 @@ func (bot *Bot) Init(secret, gateway string, shard [2]byte) *Bot { bot.Secret = secret if bot.IsV2() { for { - err := bot.GetAppAccessToken() + err := bot.GetAppAccessTokenNoContext() if err == nil { log.Infoln(getLogHeader(), "获得 Token: "+bot.token+", 超时:", bot.expiresec, "秒") bot.exonce.Do(func() { @@ -310,7 +310,7 @@ func (bot *Bot) refreshtoken() { continue } time.Sleep(time.Duration(bot.expiresec) * time.Second) - err := bot.GetAppAccessToken() + err := bot.GetAppAccessTokenNoContext() if err != nil { log.Warnln(getLogHeader(), "刷新 Token 时出现错误:", err) } else { diff --git a/codegen/context/main.go b/codegen/context/main.go index 758781d..f06cb7d 100644 --- a/codegen/context/main.go +++ b/codegen/context/main.go @@ -38,6 +38,9 @@ package nano f.WriteString(path) f.WriteString(" vvvvvvvvvvvvvvvvvvvvv */\n") for _, define := range apire.FindAllStringSubmatch(nano.BytesToString(data), -1) { + if strings.Contains(define[3], "NoContext") { + continue + } f.WriteString(define[1]) // 注释 f.WriteString("func (ctx *Ctx) ") // 函数声明 f.WriteString(define[3]) diff --git a/context.go b/context.go index 5bb6381..e6936b2 100644 --- a/context.go +++ b/context.go @@ -1,10 +1,12 @@ package nano import ( + "errors" "fmt" "reflect" "strings" "sync" + "time" ) //go:generate go run codegen/context/main.go @@ -14,6 +16,7 @@ type Ctx struct { State Message *Message IsToMe bool + IsQQ bool caller *Bot ma *Matcher @@ -96,6 +99,9 @@ func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) { return } case MessageTypeImageBytes: + if ctx.IsQQ { + continue + } reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...) if isnextreply { isnextreply = false @@ -107,6 +113,28 @@ func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) { } case MessageTypeReply: isnextreply = true + case MessageTypeAudio, MessageTypeVideo: + if !ctx.IsQQ { + continue + } + fp := &FilePost{ + URL: msg.Data, + } + if msg.Type == MessageTypeAudio { + fp.Type = FileTypeAudio + } else if msg.Type == MessageTypeVideo { + fp.Type = FileTypeVideo + } + var idts *IDTimestampMessageResult + if OnlyQQGroup(ctx) { + idts, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp) + } else if OnlyQQPrivate(ctx) { + idts, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp) + } + if err != nil { + return + } + reply = &Message{ID: idts.ID, Timestamp: time.Unix(int64(idts.Timestamp), 0)} } } if len(textlist) > 0 { @@ -126,7 +154,7 @@ func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err msg := ctx.Message if msg != nil { post.ReplyMessageID = msg.ID - if replytosender { + if OnlyGuild(ctx) && replytosender { post.MessageReference = &MessageReference{ MessageID: msg.ID, } @@ -135,12 +163,43 @@ func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err post.ReplyMessageID = "MESSAGE_CREATE" } - if msg.SrcGuildID != "" { // dms + if OnlyDirect(ctx) { // dms reply, err = ctx.PostMessageToUser(msg.GuildID, post) - } else { + } else if OnlyChannel(ctx) { reply, err = ctx.PostMessageToChannel(msg.ChannelID, post) + } else { // v2 + var idts *IDTimestampMessageResult + typ := MessageTypeV2Text + switch { + case post.Markdown != nil: + typ = MessageTypeV2Markdown + case post.Ark != nil: + typ = MessageTypeV2Ark + case post.Embed != nil: + typ = MessageTypeV2Embed + } + v2post := &MessagePostV2{ + Type: typ, + Seq: len(GetTriggeredMessages(msg.ID)) + 1, + Content: post.Content, + ReplyMessageID: post.ReplyMessageID, + MessageReference: post.MessageReference, + Markdown: post.Markdown, + KeyBoard: post.KeyBoard, + Ark: post.Ark, + Embed: post.Embed, + } + if OnlyQQGroup(ctx) { + idts, err = ctx.PostMessageToQQGroup(msg.ChannelID, v2post) + } else if OnlyQQPrivate(ctx) { + idts, err = ctx.PostMessageToQQUser(msg.ChannelID, v2post) + } + reply = &Message{ + ID: idts.ID, + Timestamp: time.Unix(int64(idts.Timestamp), 0), + } } - if msg != nil && reply != nil && reply.ID != "" { + if err != nil && msg != nil && reply != nil && reply.ID != "" { logtriggeredmessages(msg.ID, reply.ID) } return @@ -155,6 +214,25 @@ func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message // SendImage 发送带图片消息到对方 func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (*Message, error) { + if OnlyQQ(ctx) { + var idts *IDTimestampMessageResult + var err error + fp := &FilePost{ + Type: FileTypeImage, + URL: file, + } + _, _ = ctx.SendPlainMessage(replytosender, caption...) + if OnlyQQGroup(ctx) { + idts, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp) + } else if OnlyQQPrivate(ctx) { + idts, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp) + } + if err != nil { + return nil, err + } + return &Message{ID: idts.ID, Timestamp: time.Unix(int64(idts.Timestamp), 0)}, nil + } + post := &MessagePost{ Content: HideURL(fmt.Sprint(caption...)), } @@ -170,6 +248,10 @@ func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (*Mes // SendImageBytes 发送带图片消息到对方 func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) { + if OnlyQQ(ctx) { + return nil, errors.New("QQ暂不支持直接发送图片数据") + } + post := &MessagePost{ Content: HideURL(fmt.Sprint(caption...)), } diff --git a/event.go b/event.go index 8fe2a0c..0a91630 100644 --- a/event.go +++ b/event.go @@ -47,10 +47,14 @@ func (bot *Bot) processEvent(payload *WebsocketPayload) { caller: bot, } switch tp { - case "DirectMessageCreate": + case "C2cMessageCreate", "GroupAtMessageCreate": + ctx.IsQQ = true + } + switch tp { + case "DirectMessageCreate", "C2cMessageCreate": ctx.IsToMe = true fallthrough - case "MessageCreate", "AtMessageCreate": + case "MessageCreate", "AtMessageCreate", "GroupAtMessageCreate": tp = "Message" case "DirectMessageDelete": ctx.IsToMe = true @@ -78,9 +82,27 @@ func (bot *Bot) processEvent(payload *WebsocketPayload) { ctx.value = x switch tp { case "Message": - ctx.Message = (*Message)(x.UnsafePointer()) - if ctx.Message.MentionEveryone { - ctx.IsToMe = true + if ctx.IsQQ { + msgv2 := (*MessageV2)(x.UnsafePointer()) + ctx.Message = &Message{ + ID: msgv2.ID, + Content: msgv2.Content, + ChannelID: msgv2.GroupOpenID, + GuildID: payload.T, + Timestamp: msgv2.Timestamp, + Attachments: msgv2.Attachments, + Author: &User{}, + } + if msgv2.Author.UserOpenID != "" { + ctx.Message.Author.ID = msgv2.Author.UserOpenID + } else if msgv2.Author.MemberOpenID != "" { + ctx.Message.Author.ID = msgv2.Author.MemberOpenID + } + } else { + ctx.Message = (*Message)(x.UnsafePointer()) + if ctx.Message.MentionEveryone { + ctx.IsToMe = true + } } log.Infoln(getLogHeader(), "=>", ctx.Message) case "MessageDelete": diff --git a/handler.go b/handler.go index f888f08..6e30897 100644 --- a/handler.go +++ b/handler.go @@ -68,6 +68,19 @@ type Handler struct { OnAudioOrLiveChannelMemberEnter func(s uint32, bot *Bot, d *AudioLiveChannelUsersChange) OnAudioOrLiveChannelMemberExit func(s uint32, bot *Bot, d *AudioLiveChannelUsersChange) + // QQ (1<<25) QQ 的一堆事件 + + OnC2cMessageCreate func(s uint32, bot *Bot, d *MessageV2) + OnGroupAtMessageCreate func(s uint32, bot *Bot, d *MessageV2) + OnGroupAddRobot func(s uint32, bot *Bot, d *QQRobotStatus) + OnGroupDelRobot func(s uint32, bot *Bot, d *QQRobotStatus) + OnGroupMsgReject func(s uint32, bot *Bot, d *QQRobotStatus) + OnGroupMsgReceive func(s uint32, bot *Bot, d *QQRobotStatus) + OnFriendAdd func(s uint32, bot *Bot, d *QQRobotStatus) + OnFriendDel func(s uint32, bot *Bot, d *QQRobotStatus) + OnC2cMsgReject func(s uint32, bot *Bot, d *QQRobotStatus) + OnC2cMsgReceive func(s uint32, bot *Bot, d *QQRobotStatus) + // INTERACTION (1 << 26) 事件结构不明 // MESSAGE_AUDIT (1 << 27) diff --git a/message.go b/message.go index 6b392f5..333441b 100644 --- a/message.go +++ b/message.go @@ -34,6 +34,8 @@ const ( MessageTypeImage MessageTypeImageBytes MessageTypeReply + MessageTypeAudio + MessageTypeVideo ) // Message impl the array form of message @@ -114,6 +116,24 @@ func AtChannel(id string) MessageSegment { } } +// Record QQ 语音 +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html +func Record(url string) MessageSegment { + return MessageSegment{ + Type: MessageTypeAudio, + Data: url, + } +} + +// Video QQ 视频 +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html +func Video(url string) MessageSegment { + return MessageSegment{ + Type: MessageTypeVideo, + Data: url, + } +} + // Reply 回复 // https://github.com/botuniverse/onebot-11/tree/master/message/segment.md#%E5%9B%9E%E5%A4%8D func ReplyTo(id string) MessageSegment { diff --git a/openapi.go b/openapi.go index ff79235..0f39a5d 100644 --- a/openapi.go +++ b/openapi.go @@ -120,7 +120,7 @@ func (bot *Bot) DeleteOpenAPIWithPtr(ep, contenttype string, ptr any, body io.Re return bot.dohttprequest(NewHTTPEndpointDeleteRequestWithAuth, ep, contenttype, ptr, body) } -//go:generate go run codegen/postopenapiof/main.go Channel GuildRoleCreate Message DMS +//go:generate go run codegen/postopenapiof/main.go Channel GuildRoleCreate Message DMS IDTimestampMessageResult // PostOpenAPI 从 ep 得到 json 结构化数据返回值写到 ptr, ptr 除 Slice 外必须在开头继承 CodeMessageBase func (bot *Bot) PostOpenAPI(ep, contenttype string, ptr any, body io.Reader) error { diff --git a/openapi_codegen_postopenapiof.go b/openapi_codegen_postopenapiof.go index 7d305e5..05f6ba2 100644 --- a/openapi_codegen_postopenapiof.go +++ b/openapi_codegen_postopenapiof.go @@ -55,3 +55,15 @@ func (bot *Bot) postOpenAPIofDMS(ep, contenttype string, body io.Reader) (*DMS, } return &resp.DMS, err } + +func (bot *Bot) postOpenAPIofIDTimestampMessageResult(ep, contenttype string, body io.Reader) (*IDTimestampMessageResult, error) { + resp := &struct { + CodeMessageBase + IDTimestampMessageResult + }{} + err := bot.PostOpenAPI(ep, contenttype, resp, body) + if err != nil { + err = errors.Wrap(err, getCallerFuncName()) + } + return &resp.IDTimestampMessageResult, err +} diff --git a/openapi_v2.go b/openapi_v2.go new file mode 100644 index 0000000..f85fe46 --- /dev/null +++ b/openapi_v2.go @@ -0,0 +1,14 @@ +package nano + +type IDTimestampMessageResult struct { + ID string `json:"id"` + Timestamp int `json:"timestamp"` +} + +// QQRobotStatus https://bot.q.qq.com/wiki/develop/api-231017/server-inter/group.html#%E4%BA%8B%E4%BB%B6 +type QQRobotStatus struct { + OpenID string `json:"openid"` + GroupOpenID string `json:"group_openid"` + OpMemberOpenID string `json:"op_member_openid"` + Timestamp int `json:"timestamp"` +} diff --git a/openapi_v2_files.go b/openapi_v2_files.go new file mode 100644 index 0000000..021b167 --- /dev/null +++ b/openapi_v2_files.go @@ -0,0 +1,75 @@ +package nano + +import ( + "strconv" + "strings" + + "github.com/sirupsen/logrus" +) + +// FileType 媒体类型 +type FileType int + +const ( + FileTypeImage = iota + 1 // png/jpg + FileTypeVideo // mp4 + FileTypeAudio // silk + FileTypeFile // 暂不开放 +) + +func (ft FileType) String() string { + switch ft { + case FileTypeImage: + return "图片" + case FileTypeVideo: + return "视频" + case FileTypeAudio: + return "语音" + case FileTypeFile: + return "文件" + default: + return "未知类型" + strconv.Itoa(int(ft)) + } +} + +// FilePost QQ 富媒体消息发送请求参数 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html +type FilePost struct { + Type FileType `json:"file_type"` + URL string `json:"url"` + MotherFuckingAlwaysTrue bool `json:"srv_send_msg"` + // file_data 否 【暂未支持】 +} + +func (fp *FilePost) String() string { + sb := strings.Builder{} + sb.WriteString("[v2.") + sb.WriteString(fp.Type.String()) + sb.WriteString("]") + if fp.URL == "" { + sb.WriteString("无链接") + } else { + sb.WriteString("链接: ") + sb.WriteString(fp.URL) + } + return sb.String() +} + +// PostFileToQQUser 发送文件到 QQ 用户的 openid +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html#%E5%8F%91%E9%80%81%E5%88%B0%E5%8D%95%E8%81%8A +func (bot *Bot) PostFileToQQUser(id string, content *FilePost) (*IDTimestampMessageResult, error) { + logrus.Infoln(getLogHeader(), "<= [Q]单:", id+",", content) + content.MotherFuckingAlwaysTrue = true + return bot.postOpenAPIofIDTimestampMessageResult("/v2/users/"+id+"/files", "", WriteBodyFromJSON(content)) +} + +// PostFileToQQGroup 发送文件到 QQ 群的 openid +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/rich-text-media.html#%E5%8F%91%E9%80%81%E5%88%B0%E7%BE%A4%E8%81%8A +func (bot *Bot) PostFileToQQGroup(id string, content *FilePost) (*IDTimestampMessageResult, error) { + logrus.Infoln(getLogHeader(), "<= [Q]群:", id+",", content) + content.MotherFuckingAlwaysTrue = true + return bot.postOpenAPIofIDTimestampMessageResult("/v2/groups/"+id+"/files", "", WriteBodyFromJSON(content)) +} diff --git a/openapi_v2_message.go b/openapi_v2_message.go new file mode 100644 index 0000000..b02aba9 --- /dev/null +++ b/openapi_v2_message.go @@ -0,0 +1,150 @@ +package nano + +import ( + "strconv" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +type MessageTypeV2 int + +const ( + MessageTypeV2Text MessageTypeV2 = iota + MessageTypeV2TextImage + MessageTypeV2Markdown + MessageTypeV2Ark + MessageTypeV2Embed +) + +func (mt2 MessageTypeV2) String() string { + switch mt2 { + case MessageTypeV2Text: + return "文本" + case MessageTypeV2TextImage: + return "图文混排" + case MessageTypeV2Markdown: + return "MD" + case MessageTypeV2Ark: + return "模版" + case MessageTypeV2Embed: + return "嵌入" + default: + return "未知类型" + strconv.Itoa(int(mt2)) + } +} + +type MessageV2 struct { + Author struct { + UserOpenID string `json:"user_openid"` + MemberOpenID string `json:"member_openid"` + } `json:"author"` + Content string `json:"content"` + ID string `json:"id"` + GroupOpenID string `json:"group_openid"` + Timestamp time.Time `json:"timestamp"` + Attachments []MessageAttachment `json:"attachments"` +} + +// MessagePostV2 V2 发消息结构体 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/send.html +type MessagePostV2 struct { + Type MessageTypeV2 `json:"msg_type"` + Seq int `json:"msg_seq,omitempty"` // 回复消息的序号,与 msg_id 联合使用,避免相同消息id回复重复发送,不填默认是1。相同的 msg_id + msg_seq 重复发送会失败。 + Content string `json:"content,omitempty"` + ReplyEventID string `json:"event_id,omitempty"` // 前置收到的事件ID,用于发送被动消息 + ReplyMessageID string `json:"msg_id,omitempty"` + + // image 否 【暂不支持】 + MessageReference *MessageReference `json:"message_reference,omitempty"` // 【暂未支持】消息引用 + + Markdown *MessageMarkdown `json:"markdown,omitempty"` + KeyBoard *MessageKeyboard `json:"keyboard,omitempty"` + Ark *MessageArk `json:"ark,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` +} + +func (mp *MessagePostV2) String() string { + sb := strings.Builder{} + sb.WriteString("[v2.") + sb.WriteString(mp.Type.String()) + sb.WriteString(".") + sb.WriteString(strconv.Itoa(mp.Seq)) + sb.WriteString("]") + if mp.Content == "" { + sb.WriteString("无文本") + } else { + sb.WriteString("文本: ") + sb.WriteString(mp.Content) + } + if mp.ReplyMessageID != "" { + sb.WriteString(", 回应消息: ") + sb.WriteString(mp.ReplyMessageID) + } + if mp.ReplyEventID != "" { + sb.WriteString(", 回应事件: ") + sb.WriteString(mp.ReplyEventID) + } + if mp.Embed != nil { + sb.WriteString(", 嵌入: <标题:") + sb.WriteString(mp.Embed.Title) + sb.WriteString(",提示:") + sb.WriteString(mp.Embed.Prompt) + sb.WriteByte('>') + } + if mp.Ark != nil { + sb.WriteString(", 模版: ") + sb.WriteString(strconv.Itoa(mp.Ark.TemplateID)) + } + if mp.MessageReference != nil { + sb.WriteString(", 回复: ") + sb.WriteString(mp.MessageReference.MessageID) + } + /*if mp.Image != "" { + sb.WriteString(", 图片URL: ") + sb.WriteString(mp.Image) + } + if mp.ImageFile != "" { + sb.WriteString(", 图片内容: ") + x := mp.ImageFile + if len(x) > 64 { + x = x[:64] + "..." + } + sb.WriteString(x) + } + if len(mp.ImageBytes) > 0 { + sb.WriteString(", 图片大小: ") + sb.WriteString(strconv.Itoa(len(mp.ImageBytes))) + }*/ + if mp.Markdown != nil { + sb.WriteString(", MD模版: ") + sb.WriteString(strconv.Itoa(mp.Markdown.TemplateID)) + } + if mp.KeyBoard != nil { + sb.WriteString(", KB模版: ") + sb.WriteString(mp.KeyBoard.ID) + } + return sb.String() +} + +func (bot *Bot) postV2MessageTo(ep string, content *MessagePostV2) (*IDTimestampMessageResult, error) { + return bot.postOpenAPIofIDTimestampMessageResult(ep, "", WriteBodyFromJSON(content)) +} + +// PostMessageToQQUser 向 openid 指定的用户发送消息 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/send.html#%E5%8D%95%E8%81%8A +func (bot *Bot) PostMessageToQQUser(id string, content *MessagePostV2) (*IDTimestampMessageResult, error) { + logrus.Infoln(getLogHeader(), "<= [Q]单:", id+",", content) + return bot.postV2MessageTo("/v2/users/"+id+"/messages", content) +} + +// PostMessageToQQGroup 向 openid 指定的群发送消息 +// +// https://bot.q.qq.com/wiki/develop/api-231017/server-inter/message/send-receive/send.html#%E7%BE%A4%E8%81%8A +func (bot *Bot) PostMessageToQQGroup(id string, content *MessagePostV2) (*IDTimestampMessageResult, error) { + logrus.Infoln(getLogHeader(), "<= [Q]群:", id+",", content) + return bot.postV2MessageTo("/v2/groups/"+id+"/messages", content) +} diff --git a/openapi_wss.go b/openapi_wss.go index 02a035d..42b5327 100644 --- a/openapi_wss.go +++ b/openapi_wss.go @@ -13,10 +13,10 @@ var ( ErrInvalidExpire = errors.New("invalid expire") ) -// GetGeneralWSSGateway 获取通用 WSS 接入点 +// GetGeneralWSSGatewayNoContext 获取通用 WSS 接入点 // // https://bot.q.qq.com/wiki/develop/api/openapi/wss/url_get.html -func (bot *Bot) GetGeneralWSSGateway() (string, error) { +func (bot *Bot) GetGeneralWSSGatewayNoContext() (string, error) { resp := struct { CodeMessageBase U string `json:"url"` @@ -39,17 +39,17 @@ type ShardWSSGateway struct { } `json:"session_start_limit"` } -// GetShardWSSGateway 获取带分片 WSS 接入点 +// GetShardWSSGatewayNoContext 获取带分片 WSS 接入点 // // https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html -func (bot *Bot) GetShardWSSGateway() (*ShardWSSGateway, error) { +func (bot *Bot) GetShardWSSGatewayNoContext() (*ShardWSSGateway, error) { return bot.getOpenAPIofShardWSSGateway("/gateway/bot") } -// GetAppAccessToken 获取接口凭证并保存到 bot.Token +// GetAppAccessTokenNoContext 获取接口凭证并保存到 bot.Token // // https://bot.q.qq.com/wiki/develop/api-231017/dev-prepare/interface-framework/api-use.html#%E8%8E%B7%E5%8F%96%E6%8E%A5%E5%8F%A3%E5%87%AD%E8%AF%81 -func (bot *Bot) GetAppAccessToken() error { +func (bot *Bot) GetAppAccessTokenNoContext() error { req, err := newHTTPEndpointRequestWithAuth("POST", "", AccessTokenAPI, "", "", WriteBodyFromJSON(&struct { A string `json:"appId"` S string `json:"clientSecret"` diff --git a/rules.go b/rules.go index 7616f3e..edcf070 100644 --- a/rules.go +++ b/rules.go @@ -287,7 +287,17 @@ func CheckGuild(guildID ...string) Rule { } } -// OnlyDirect requires that the ctx.Type is direct message +// OnlyQQ 必须是 QQ 消息 +func OnlyQQ(ctx *Ctx) bool { + return ctx.IsQQ +} + +// OnlyGuild 必须是频道消息 +func OnlyGuild(ctx *Ctx) bool { + return !ctx.IsQQ +} + +// OnlyDirect 必须是频道私聊 func OnlyDirect(ctx *Ctx) bool { if ctx.Type != "" { return strings.HasPrefix(ctx.Type, "Direct") @@ -295,12 +305,12 @@ func OnlyDirect(ctx *Ctx) bool { return false } -// OnlyChannel is !OnlyDirect +// OnlyChannel 必须是频道 Channel func OnlyChannel(ctx *Ctx) bool { - return !OnlyDirect(ctx) + return !OnlyDirect(ctx) && !OnlyQQ(ctx) } -// OnlyPublic requires that the ctx.Type is at/public message +// OnlyPublic 消息类型包含 At 或 Public (包括QQ群) func OnlyPublic(ctx *Ctx) bool { if ctx.Type != "" { return strings.HasPrefix(ctx.Type, "At") || strings.HasPrefix(ctx.Type, "Public") @@ -308,11 +318,21 @@ func OnlyPublic(ctx *Ctx) bool { return false } -// OnlyPrivate is !OnlyPublic +// OnlyPrivate is !OnlyPublic (包括QQ私聊) func OnlyPrivate(ctx *Ctx) bool { return !OnlyPublic(ctx) } +// OnlyQQGroup 只在 QQ 群 +func OnlyQQGroup(ctx *Ctx) bool { + return ctx.Type == "GroupAtMessageCreate" +} + +// OnlyQQPrivate 只在 QQ 私聊 +func OnlyQQPrivate(ctx *Ctx) bool { + return ctx.Type == "C2cMessageCreate" +} + // SuperUserPermission only triggered by the bot's owner func SuperUserPermission(ctx *Ctx) bool { switch msg := ctx.Value.(type) {