mirror of
https://github.com/fumiama/NanoBot.git
synced 2026-06-11 21:50:29 +08:00
✨ QQサポートを追加
This commit is contained in:
@@ -476,20 +476,46 @@ func (ctx *Ctx) GetMyGuilds(before, after string, limit int) (guilds []Guild, er
|
|||||||
|
|
||||||
/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_user.go ^^^^^^^^^^^^^^^^^^^^ */
|
/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 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 */
|
/* 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 ^^^^^^^^^^^^^^^^^^^^ */
|
/* ^^^^^^^^^^^^^^^^^^^^ 生成自文件 openapi_wss.go ^^^^^^^^^^^^^^^^^^^^ */
|
||||||
|
|||||||
8
bot.go
8
bot.go
@@ -68,13 +68,13 @@ func (bot *Bot) getinitinfo() (secret, gw string, shard [2]byte, err error) {
|
|||||||
bot.Secret = ""
|
bot.Secret = ""
|
||||||
}
|
}
|
||||||
if bot.ShardIndex == 0 {
|
if bot.ShardIndex == 0 {
|
||||||
gw, err = bot.GetGeneralWSSGateway()
|
gw, err = bot.GetGeneralWSSGatewayNoContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var sgw *ShardWSSGateway
|
var sgw *ShardWSSGateway
|
||||||
sgw, err = bot.GetShardWSSGateway()
|
sgw, err = bot.GetShardWSSGatewayNoContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ func (bot *Bot) Init(secret, gateway string, shard [2]byte) *Bot {
|
|||||||
bot.Secret = secret
|
bot.Secret = secret
|
||||||
if bot.IsV2() {
|
if bot.IsV2() {
|
||||||
for {
|
for {
|
||||||
err := bot.GetAppAccessToken()
|
err := bot.GetAppAccessTokenNoContext()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Infoln(getLogHeader(), "获得 Token: "+bot.token+", 超时:", bot.expiresec, "秒")
|
log.Infoln(getLogHeader(), "获得 Token: "+bot.token+", 超时:", bot.expiresec, "秒")
|
||||||
bot.exonce.Do(func() {
|
bot.exonce.Do(func() {
|
||||||
@@ -310,7 +310,7 @@ func (bot *Bot) refreshtoken() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(bot.expiresec) * time.Second)
|
time.Sleep(time.Duration(bot.expiresec) * time.Second)
|
||||||
err := bot.GetAppAccessToken()
|
err := bot.GetAppAccessTokenNoContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln(getLogHeader(), "刷新 Token 时出现错误:", err)
|
log.Warnln(getLogHeader(), "刷新 Token 时出现错误:", err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ package nano
|
|||||||
f.WriteString(path)
|
f.WriteString(path)
|
||||||
f.WriteString(" vvvvvvvvvvvvvvvvvvvvv */\n")
|
f.WriteString(" vvvvvvvvvvvvvvvvvvvvv */\n")
|
||||||
for _, define := range apire.FindAllStringSubmatch(nano.BytesToString(data), -1) {
|
for _, define := range apire.FindAllStringSubmatch(nano.BytesToString(data), -1) {
|
||||||
|
if strings.Contains(define[3], "NoContext") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
f.WriteString(define[1]) // 注释
|
f.WriteString(define[1]) // 注释
|
||||||
f.WriteString("func (ctx *Ctx) ") // 函数声明
|
f.WriteString("func (ctx *Ctx) ") // 函数声明
|
||||||
f.WriteString(define[3])
|
f.WriteString(define[3])
|
||||||
|
|||||||
90
context.go
90
context.go
@@ -1,10 +1,12 @@
|
|||||||
package nano
|
package nano
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run codegen/context/main.go
|
//go:generate go run codegen/context/main.go
|
||||||
@@ -14,6 +16,7 @@ type Ctx struct {
|
|||||||
State
|
State
|
||||||
Message *Message
|
Message *Message
|
||||||
IsToMe bool
|
IsToMe bool
|
||||||
|
IsQQ bool
|
||||||
|
|
||||||
caller *Bot
|
caller *Bot
|
||||||
ma *Matcher
|
ma *Matcher
|
||||||
@@ -96,6 +99,9 @@ func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case MessageTypeImageBytes:
|
case MessageTypeImageBytes:
|
||||||
|
if ctx.IsQQ {
|
||||||
|
continue
|
||||||
|
}
|
||||||
reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...)
|
reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...)
|
||||||
if isnextreply {
|
if isnextreply {
|
||||||
isnextreply = false
|
isnextreply = false
|
||||||
@@ -107,6 +113,28 @@ func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) {
|
|||||||
}
|
}
|
||||||
case MessageTypeReply:
|
case MessageTypeReply:
|
||||||
isnextreply = true
|
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 {
|
if len(textlist) > 0 {
|
||||||
@@ -126,7 +154,7 @@ func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err
|
|||||||
msg := ctx.Message
|
msg := ctx.Message
|
||||||
if msg != nil {
|
if msg != nil {
|
||||||
post.ReplyMessageID = msg.ID
|
post.ReplyMessageID = msg.ID
|
||||||
if replytosender {
|
if OnlyGuild(ctx) && replytosender {
|
||||||
post.MessageReference = &MessageReference{
|
post.MessageReference = &MessageReference{
|
||||||
MessageID: msg.ID,
|
MessageID: msg.ID,
|
||||||
}
|
}
|
||||||
@@ -135,12 +163,43 @@ func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err
|
|||||||
post.ReplyMessageID = "MESSAGE_CREATE"
|
post.ReplyMessageID = "MESSAGE_CREATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.SrcGuildID != "" { // dms
|
if OnlyDirect(ctx) { // dms
|
||||||
reply, err = ctx.PostMessageToUser(msg.GuildID, post)
|
reply, err = ctx.PostMessageToUser(msg.GuildID, post)
|
||||||
} else {
|
} else if OnlyChannel(ctx) {
|
||||||
reply, err = ctx.PostMessageToChannel(msg.ChannelID, post)
|
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)
|
logtriggeredmessages(msg.ID, reply.ID)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -155,6 +214,25 @@ func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message
|
|||||||
|
|
||||||
// SendImage 发送带图片消息到对方
|
// SendImage 发送带图片消息到对方
|
||||||
func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (*Message, error) {
|
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{
|
post := &MessagePost{
|
||||||
Content: HideURL(fmt.Sprint(caption...)),
|
Content: HideURL(fmt.Sprint(caption...)),
|
||||||
}
|
}
|
||||||
@@ -170,6 +248,10 @@ func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (*Mes
|
|||||||
|
|
||||||
// SendImageBytes 发送带图片消息到对方
|
// SendImageBytes 发送带图片消息到对方
|
||||||
func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) {
|
func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) {
|
||||||
|
if OnlyQQ(ctx) {
|
||||||
|
return nil, errors.New("QQ暂不支持直接发送图片数据")
|
||||||
|
}
|
||||||
|
|
||||||
post := &MessagePost{
|
post := &MessagePost{
|
||||||
Content: HideURL(fmt.Sprint(caption...)),
|
Content: HideURL(fmt.Sprint(caption...)),
|
||||||
}
|
}
|
||||||
|
|||||||
32
event.go
32
event.go
@@ -47,10 +47,14 @@ func (bot *Bot) processEvent(payload *WebsocketPayload) {
|
|||||||
caller: bot,
|
caller: bot,
|
||||||
}
|
}
|
||||||
switch tp {
|
switch tp {
|
||||||
case "DirectMessageCreate":
|
case "C2cMessageCreate", "GroupAtMessageCreate":
|
||||||
|
ctx.IsQQ = true
|
||||||
|
}
|
||||||
|
switch tp {
|
||||||
|
case "DirectMessageCreate", "C2cMessageCreate":
|
||||||
ctx.IsToMe = true
|
ctx.IsToMe = true
|
||||||
fallthrough
|
fallthrough
|
||||||
case "MessageCreate", "AtMessageCreate":
|
case "MessageCreate", "AtMessageCreate", "GroupAtMessageCreate":
|
||||||
tp = "Message"
|
tp = "Message"
|
||||||
case "DirectMessageDelete":
|
case "DirectMessageDelete":
|
||||||
ctx.IsToMe = true
|
ctx.IsToMe = true
|
||||||
@@ -78,9 +82,27 @@ func (bot *Bot) processEvent(payload *WebsocketPayload) {
|
|||||||
ctx.value = x
|
ctx.value = x
|
||||||
switch tp {
|
switch tp {
|
||||||
case "Message":
|
case "Message":
|
||||||
ctx.Message = (*Message)(x.UnsafePointer())
|
if ctx.IsQQ {
|
||||||
if ctx.Message.MentionEveryone {
|
msgv2 := (*MessageV2)(x.UnsafePointer())
|
||||||
ctx.IsToMe = true
|
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)
|
log.Infoln(getLogHeader(), "=>", ctx.Message)
|
||||||
case "MessageDelete":
|
case "MessageDelete":
|
||||||
|
|||||||
13
handler.go
13
handler.go
@@ -68,6 +68,19 @@ type Handler struct {
|
|||||||
|
|
||||||
OnAudioOrLiveChannelMemberEnter func(s uint32, bot *Bot, d *AudioLiveChannelUsersChange)
|
OnAudioOrLiveChannelMemberEnter func(s uint32, bot *Bot, d *AudioLiveChannelUsersChange)
|
||||||
OnAudioOrLiveChannelMemberExit 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) 事件结构不明
|
// INTERACTION (1 << 26) 事件结构不明
|
||||||
|
|
||||||
// MESSAGE_AUDIT (1 << 27)
|
// MESSAGE_AUDIT (1 << 27)
|
||||||
|
|||||||
20
message.go
20
message.go
@@ -34,6 +34,8 @@ const (
|
|||||||
MessageTypeImage
|
MessageTypeImage
|
||||||
MessageTypeImageBytes
|
MessageTypeImageBytes
|
||||||
MessageTypeReply
|
MessageTypeReply
|
||||||
|
MessageTypeAudio
|
||||||
|
MessageTypeVideo
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message impl the array form of message
|
// 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 回复
|
// Reply 回复
|
||||||
// https://github.com/botuniverse/onebot-11/tree/master/message/segment.md#%E5%9B%9E%E5%A4%8D
|
// https://github.com/botuniverse/onebot-11/tree/master/message/segment.md#%E5%9B%9E%E5%A4%8D
|
||||||
func ReplyTo(id string) MessageSegment {
|
func ReplyTo(id string) MessageSegment {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (bot *Bot) DeleteOpenAPIWithPtr(ep, contenttype string, ptr any, body io.Re
|
|||||||
return bot.dohttprequest(NewHTTPEndpointDeleteRequestWithAuth, ep, contenttype, ptr, body)
|
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
|
// PostOpenAPI 从 ep 得到 json 结构化数据返回值写到 ptr, ptr 除 Slice 外必须在开头继承 CodeMessageBase
|
||||||
func (bot *Bot) PostOpenAPI(ep, contenttype string, ptr any, body io.Reader) error {
|
func (bot *Bot) PostOpenAPI(ep, contenttype string, ptr any, body io.Reader) error {
|
||||||
|
|||||||
@@ -55,3 +55,15 @@ func (bot *Bot) postOpenAPIofDMS(ep, contenttype string, body io.Reader) (*DMS,
|
|||||||
}
|
}
|
||||||
return &resp.DMS, err
|
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
|
||||||
|
}
|
||||||
|
|||||||
14
openapi_v2.go
Normal file
14
openapi_v2.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
75
openapi_v2_files.go
Normal file
75
openapi_v2_files.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
150
openapi_v2_message.go
Normal file
150
openapi_v2_message.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@ var (
|
|||||||
ErrInvalidExpire = errors.New("invalid expire")
|
ErrInvalidExpire = errors.New("invalid expire")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetGeneralWSSGateway 获取通用 WSS 接入点
|
// GetGeneralWSSGatewayNoContext 获取通用 WSS 接入点
|
||||||
//
|
//
|
||||||
// https://bot.q.qq.com/wiki/develop/api/openapi/wss/url_get.html
|
// 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 {
|
resp := struct {
|
||||||
CodeMessageBase
|
CodeMessageBase
|
||||||
U string `json:"url"`
|
U string `json:"url"`
|
||||||
@@ -39,17 +39,17 @@ type ShardWSSGateway struct {
|
|||||||
} `json:"session_start_limit"`
|
} `json:"session_start_limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetShardWSSGateway 获取带分片 WSS 接入点
|
// GetShardWSSGatewayNoContext 获取带分片 WSS 接入点
|
||||||
//
|
//
|
||||||
// https://bot.q.qq.com/wiki/develop/api/openapi/wss/shard_url_get.html
|
// 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")
|
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
|
// 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 {
|
req, err := newHTTPEndpointRequestWithAuth("POST", "", AccessTokenAPI, "", "", WriteBodyFromJSON(&struct {
|
||||||
A string `json:"appId"`
|
A string `json:"appId"`
|
||||||
S string `json:"clientSecret"`
|
S string `json:"clientSecret"`
|
||||||
|
|||||||
30
rules.go
30
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 {
|
func OnlyDirect(ctx *Ctx) bool {
|
||||||
if ctx.Type != "" {
|
if ctx.Type != "" {
|
||||||
return strings.HasPrefix(ctx.Type, "Direct")
|
return strings.HasPrefix(ctx.Type, "Direct")
|
||||||
@@ -295,12 +305,12 @@ func OnlyDirect(ctx *Ctx) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnlyChannel is !OnlyDirect
|
// OnlyChannel 必须是频道 Channel
|
||||||
func OnlyChannel(ctx *Ctx) bool {
|
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 {
|
func OnlyPublic(ctx *Ctx) bool {
|
||||||
if ctx.Type != "" {
|
if ctx.Type != "" {
|
||||||
return strings.HasPrefix(ctx.Type, "At") || strings.HasPrefix(ctx.Type, "Public")
|
return strings.HasPrefix(ctx.Type, "At") || strings.HasPrefix(ctx.Type, "Public")
|
||||||
@@ -308,11 +318,21 @@ func OnlyPublic(ctx *Ctx) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnlyPrivate is !OnlyPublic
|
// OnlyPrivate is !OnlyPublic (包括QQ私聊)
|
||||||
func OnlyPrivate(ctx *Ctx) bool {
|
func OnlyPrivate(ctx *Ctx) bool {
|
||||||
return !OnlyPublic(ctx)
|
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
|
// SuperUserPermission only triggered by the bot's owner
|
||||||
func SuperUserPermission(ctx *Ctx) bool {
|
func SuperUserPermission(ctx *Ctx) bool {
|
||||||
switch msg := ctx.Value.(type) {
|
switch msg := ctx.Value.(type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user