1
0
mirror of https://github.com/Mrs4s/go-cqhttp.git synced 2025-06-30 11:53:25 +00:00

Compare commits

..

71 Commits

Author SHA1 Message Date
470efa07ab url cache supported. 2020-08-20 03:05:46 +08:00
b7572f8d2c fix log. 2020-08-20 02:31:51 +08:00
c9a914b5d5 fix #141. 2020-08-20 02:30:44 +08:00
51696e8054 /get_group_list supported no_cache param. 2020-08-20 02:12:54 +08:00
a6bcd96415 fix event push error. 2020-08-20 01:59:31 +08:00
08694f5ae8 heartbeat event supported. close #131 2020-08-19 18:37:12 +08:00
8c71dbff68 honor api supported. 2020-08-19 16:04:30 +08:00
568b0e479f update MiraiGo. 2020-08-18 17:06:59 +08:00
d1da08a376 fix at. 2020-08-18 13:22:11 +08:00
5768c61bc7 Merge pull request #99 from mirai-qq/master
support silk audio
2020-08-17 14:15:11 +08:00
864120d3fe Merge pull request #127 from richardchien/auto_escape
support "auto_escape" in api and quick operation
2020-08-17 14:12:11 +08:00
99b414530a remove todo comment 2020-08-17 14:10:33 +08:00
4820eb2fec support "auto_escape" in api and quick operation 2020-08-17 13:46:11 +08:00
00d80d5dfc fix #107. 2020-08-16 19:35:38 +08:00
22d9ddb4ea try to fix #115. 2020-08-16 19:31:40 +08:00
fa33dbdd4e Merge branch 'master' of https://github.com/Mrs4s/go-cqhttp 2020-08-16 19:28:34 +08:00
be8b68c8e9 update MiraiGo & async loading supported. close #111 2020-08-16 19:27:43 +08:00
cef129eff7 Merge pull request #109 from SJTU-Plus/master
fix #103
2020-08-16 02:26:42 +08:00
1fac06f58a fix #103 2020-08-15 23:47:29 +08:00
1a1f860dbe fix #108. 2020-08-15 16:07:56 +08:00
f325b26e1a doc update. close #102 2020-08-14 13:41:48 +08:00
b545e05a9d Merge branch 'master' of https://github.com/Mrs4s/go-cqhttp 2020-08-14 08:28:55 +08:00
b290a0a596 try to fix group image upload failed. #65 2020-08-14 08:28:37 +08:00
9af61a336b support silk audio 2020-08-13 23:36:16 +08:00
774a1e32da Merge pull request #96 from mirai-qq/master
remove os.ModePerm
2020-08-13 21:24:15 +08:00
50eee15a67 remove os.ModePerm 2020-08-13 20:53:42 +08:00
edf6180e1c fix ci failed. 2020-08-13 20:09:57 +08:00
3c04573e82 fix #89. 2020-08-13 20:07:21 +08:00
5e49820319 update README.md 2020-08-13 17:21:54 +08:00
1418e36bab cq code 'video' supported. 2020-08-13 17:18:16 +08:00
611d16d79e update README.md 2020-08-13 11:14:21 +08:00
c6f701e8c8 Merge pull request #84 from Shigma/master
adjust unescape order
2020-08-12 12:32:06 +08:00
a49a57b964 adjust unescape order 2020-08-12 12:23:48 +08:00
bb03315930 try to support escape again. #9 2020-08-12 09:52:32 +08:00
5683db6324 fix #80. 2020-08-12 09:33:25 +08:00
f1ffa17d20 update MiraiGo. 2020-08-12 09:19:00 +08:00
69675aa0f9 fix #75. 2020-08-12 05:01:16 +08:00
782a3e2a39 add default 'http_config.post_message_format' value. 2020-08-11 17:47:16 +08:00
1967e687af fix perm error. 2020-08-11 17:38:33 +08:00
23d436972c save captcha to local file. close #71 2020-08-11 17:35:03 +08:00
129622dd24 fix reverse push issue. 2020-08-11 17:32:09 +08:00
19104d00a3 Merge pull request #78 from yyuueexxiinngg/patch-1
移除上报消息为array时的文本转义
2020-08-11 15:32:45 +08:00
6c0e78b60c Remove content escape when using array post message format. 2020-08-11 15:22:26 +08:00
a075f41a51 http post timeout setting supported. close #77 2020-08-11 15:19:54 +08:00
cdbf903361 Merge pull request #74 from Shigma/api-type-check
sendXXX api type check
2020-08-11 11:20:17 +08:00
906247fcf0 Merge pull request #70 from XYenon/master
Add support for post_message_format: array
2020-08-11 04:54:37 +08:00
72a7430841 add api type check 2020-08-10 20:43:31 +08:00
5c4f586c36 Use string format in db 2020-08-10 19:44:54 +08:00
5d2e202b0c Merge pull request #47 from purerosefallen/patch-1
Add Dockerfile
2020-08-10 16:57:34 +08:00
c27ebadbc4 Add support for post_message_format: array 2020-08-10 13:59:46 +08:00
d5a8f3ead2 Merge pull request #69 from Shigma/master
& escape should come first
2020-08-10 12:51:26 +08:00
03bfd9dd3d & escape should come first 2020-08-10 12:48:50 +08:00
0b02178402 supported voice download. 2020-08-10 12:38:58 +08:00
3819f2c2da fix #66 2020-08-10 10:31:50 +08:00
b297aa2e13 fix typo. 2020-08-10 08:06:48 +08:00
c8160550ec update doc. 2020-08-10 08:05:00 +08:00
44b26b70a3 password encrypt supported. close #22 2020-08-10 07:58:00 +08:00
97f7de85a2 fix #12. 2020-08-10 07:24:48 +08:00
ae58d26047 supported event: friend_add. 2020-08-10 07:22:53 +08:00
47cb3a5235 temp message sending supported. #26 2020-08-10 05:53:24 +08:00
4941f0c3f8 try to support escape. #9 2020-08-10 04:21:40 +08:00
bc47267c3c fix /can_send_record response. 2020-08-09 10:39:37 +08:00
8449770b6d fix #61. 2020-08-09 10:21:49 +08:00
a38c74a767 Update issue templates 2020-08-09 05:25:27 +08:00
4ab1812640 Merge pull request #59 from XYenon/patch-1
修正 config 文档 http_config 中 post_urls 的格式
2020-08-09 05:08:07 +08:00
c87836355f try to fix reverse ws delay. #52 2020-08-09 05:05:38 +08:00
8524fbf1ce fix reverse ws connect issue. 2020-08-08 22:42:19 +08:00
92fb69beef 修正 config 文档 http_config 中 post_urls 的格式 2020-08-08 20:46:28 +08:00
dd03efc9c8 update default config. 2020-08-08 15:06:00 +08:00
fdd517ee8c dockerignore 2020-08-07 14:36:00 +08:00
2d010326c7 Add a Dockerfile and more configurable environment variables 2020-08-07 14:34:47 +08:00
17 changed files with 706 additions and 164 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.gitlab-ci.yml
.dockerignore
Dockerfile
README.md
LICENSE

20
.github/ISSUE_TEMPLATE/bug--.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: Bug汇报
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**环境信息**
请根据实际使用环境修改以下信息
go-cqhttp版本: v0.9.10
运行环境: windows_amd64
连接方式: 反向WS
**bug内容**
请在这里详细描述bug的内容
**复现方法**
请在这里分步骤的描述如何复现这个bug

21
Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM golang:1.14.2-alpine AS builder
RUN go env -w GO111MODULE=auto \
&& go env -w CGO_ENABLED=0 \
&& mkdir /build
WORKDIR /build
COPY ./ .
RUN cd /build \
&& go build -ldflags "-s -w -extldflags '-static'" -o cqhttp
FROM alpine:latest
COPY --from=builder /build/cqhttp /usr/bin/cqhttp
RUN chmod +x /usr/bin/cqhttp
WORKDIR /data
ENTRYPOINT [ "/usr/bin/cqhttp" ]

View File

@ -28,6 +28,7 @@
- [CQ:image] - [CQ:image]
- [CQ:record] - [CQ:record]
- [CQ:video]
- [CQ:face] - [CQ:face]
- [CQ:at] - [CQ:at]
- [CQ:share] - [CQ:share]
@ -91,6 +92,16 @@
</details> </details>
# 关于ISSUE
以下ISSUE会被直接关闭
- 提交BUG不使用Template
- 询问已知问题
- 提问找不到重点
- 重复提问
> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。
# 性能 # 性能
在关闭数据库的情况下, 加载25个好友128个群运行24小时后内存使用为10MB左右. 开启数据库后内存使用将根据消息量增加10-20MB, 如果系统内存小于128M建议关闭数据库使用. 在关闭数据库的情况下, 加载25个好友128个群运行24小时后内存使用为10MB左右. 开启数据库后内存使用将根据消息量增加10-20MB, 如果系统内存小于128M建议关闭数据库使用.

View File

@ -34,8 +34,11 @@ func (bot *CQBot) CQGetFriendList() MSG {
} }
// https://cqhttp.cc/docs/4.15/#/API?id=get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8 // https://cqhttp.cc/docs/4.15/#/API?id=get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
func (bot *CQBot) CQGetGroupList() MSG { func (bot *CQBot) CQGetGroupList(noCache bool) MSG {
var gs []MSG var gs []MSG
if noCache {
_ = bot.Client.ReloadGroupList()
}
for _, g := range bot.Client.GroupList { for _, g := range bot.Client.GroupList {
gs = append(gs, MSG{ gs = append(gs, MSG{
"group_id": g.Code, "group_id": g.Code,
@ -96,11 +99,25 @@ func (bot *CQBot) CQGetGroupMemberInfo(groupId, userId int64, noCache bool) MSG
} }
// https://cqhttp.cc/docs/4.15/#/API?id=send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF // https://cqhttp.cc/docs/4.15/#/API?id=send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}) MSG { func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}, autoEscape bool) MSG {
var str string var str string
fixAt := func(elem []message.IMessageElement) {
for _, e := range elem {
if at, ok := e.(*message.AtElement); ok && at.Target != 0 {
at.Display = "@" + func() string {
mem := bot.Client.FindGroup(groupId).FindMember(at.Target)
if mem != nil {
return mem.DisplayName()
}
return strconv.FormatInt(at.Target, 10)
}()
}
}
}
if m, ok := i.(gjson.Result); ok { if m, ok := i.(gjson.Result); ok {
if m.Type == gjson.JSON { if m.Type == gjson.JSON {
elem := bot.ConvertObjectMessage(m, true) elem := bot.ConvertObjectMessage(m, true)
fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem}) mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 { if mid == -1 {
return Failed(100) return Failed(100)
@ -113,20 +130,19 @@ func (bot *CQBot) CQSendGroupMessage(groupId int64, i interface{}) MSG {
} }
return m.Raw return m.Raw
}() }()
} } else if s, ok := i.(string); ok {
if s, ok := i.(string); ok {
str = s str = s
} }
if str == "" { if str == "" {
return Failed(100) return Failed(100)
} }
elem := bot.ConvertStringMessage(str, true) var elem []message.IMessageElement
// fix at display if autoEscape {
for _, e := range elem { elem = append(elem, message.NewText(str))
if at, ok := e.(*message.AtElement); ok && at.Target != 0 { } else {
at.Display = "@" + bot.Client.FindGroup(groupId).FindMember(at.Target).DisplayName() elem = bot.ConvertStringMessage(str, true)
}
} }
fixAt(elem)
mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem}) mid := bot.SendGroupMessage(groupId, &message.SendingMessage{Elements: elem})
if mid == -1 { if mid == -1 {
return Failed(100) return Failed(100)
@ -205,7 +221,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
} }
// https://cqhttp.cc/docs/4.15/#/API?id=send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF // https://cqhttp.cc/docs/4.15/#/API?id=send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}) MSG { func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape bool) MSG {
var str string var str string
if m, ok := i.(gjson.Result); ok { if m, ok := i.(gjson.Result); ok {
if m.Type == gjson.JSON { if m.Type == gjson.JSON {
@ -222,14 +238,18 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}) MSG {
} }
return m.Raw return m.Raw
}() }()
} } else if s, ok := i.(string); ok {
if s, ok := i.(string); ok {
str = s str = s
} }
if str == "" { if str == "" {
return Failed(100) return Failed(100)
} }
elem := bot.ConvertStringMessage(str, false) var elem []message.IMessageElement
if autoEscape {
elem = append(elem, message.NewText(str))
} else {
elem = bot.ConvertStringMessage(str, false)
}
mid := bot.SendPrivateMessage(userId, &message.SendingMessage{Elements: elem}) mid := bot.SendPrivateMessage(userId, &message.SendingMessage{Elements: elem})
if mid == -1 { if mid == -1 {
return Failed(100) return Failed(100)
@ -359,6 +379,61 @@ func (bot *CQBot) CQDeleteMessage(messageId int32) MSG {
return OK(nil) return OK(nil)
} }
// https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
func (bot *CQBot) CQGetGroupHonorInfo(groupId int64, t string) MSG {
msg := MSG{"group_id": groupId}
convertMem := func(memList []client.HonorMemberInfo) (ret []MSG) {
for _, mem := range memList {
ret = append(ret, MSG{
"user_id": mem.Uin,
"nickname": mem.Name,
"avatar": mem.Avatar,
"description": mem.Desc,
})
}
return
}
if t == "talkative" || t == "all" {
if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Talkative); err == nil {
if honor.CurrentTalkative.Uin != 0 {
msg["current_talkative"] = MSG{
"user_id": honor.CurrentTalkative.Uin,
"nickname": honor.CurrentTalkative.Name,
"avatar": honor.CurrentTalkative.Avatar,
"day_count": honor.CurrentTalkative.DayCount,
}
}
msg["talkative_list"] = convertMem(honor.TalkativeList)
}
}
if t == "performer" || t == "all" {
if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Performer); err == nil {
msg["performer_lis"] = convertMem(honor.ActorList)
}
}
if t == "legend" || t == "all" {
if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Legend); err == nil {
msg["legend_list"] = convertMem(honor.LegendList)
}
}
if t == "strong_newbie" || t == "all" {
if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.StrongNewbie); err == nil {
msg["strong_newbie_list"] = convertMem(honor.StrongNewbieList)
}
}
if t == "emotion" || t == "all" {
if honor, err := bot.Client.GetGroupHonorInfo(groupId, client.Emotion); err == nil {
msg["emotion_list"] = convertMem(honor.EmotionList)
}
}
return OK(msg)
}
// https://cqhttp.cc/docs/4.15/#/API?id=-handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C // https://cqhttp.cc/docs/4.15/#/API?id=-handle_quick_operation-%E5%AF%B9%E4%BA%8B%E4%BB%B6%E6%89%A7%E8%A1%8C%E5%BF%AB%E9%80%9F%E6%93%8D%E4%BD%9C
// https://github.com/richardchien/coolq-http-api/blob/master/src/cqhttp/plugins/web/http.cpp#L376 // https://github.com/richardchien/coolq-http-api/blob/master/src/cqhttp/plugins/web/http.cpp#L376
func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
@ -368,6 +443,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
msgType := context.Get("message_type").Str msgType := context.Get("message_type").Str
reply := operation.Get("reply") reply := operation.Get("reply")
if reply.Exists() { if reply.Exists() {
autoEscape := global.EnsureBool(operation.Get("auto_escape"), false)
/* /*
at := true at := true
if operation.Get("at_sender").Exists() { if operation.Get("at_sender").Exists() {
@ -376,10 +452,10 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
*/ */
// TODO: 处理at字段 // TODO: 处理at字段
if msgType == "group" { if msgType == "group" {
bot.CQSendGroupMessage(context.Get("group_id").Int(), reply) bot.CQSendGroupMessage(context.Get("group_id").Int(), reply, autoEscape)
} }
if msgType == "private" { if msgType == "private" {
bot.CQSendPrivateMessage(context.Get("user_id").Int(), reply) bot.CQSendPrivateMessage(context.Get("user_id").Int(), reply, autoEscape)
} }
} }
if msgType == "group" { if msgType == "group" {
@ -404,12 +480,12 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
} }
case "request": case "request":
reqType := context.Get("request_type").Str reqType := context.Get("request_type").Str
if context.Get("approve").Bool() { if operation.Get("approve").Exists() {
if reqType == "friend" { if reqType == "friend" {
bot.CQProcessFriendRequest(context.Get("flag").Str, true) bot.CQProcessFriendRequest(context.Get("flag").Str, operation.Get("approve").Bool())
} }
if reqType == "group" { if reqType == "group" {
bot.CQProcessGroupRequest(context.Get("flag").Str, context.Get("sub_type").Str, true) bot.CQProcessGroupRequest(context.Get("flag").Str, context.Get("sub_type").Str, operation.Get("approve").Bool())
} }
} }
} }
@ -439,14 +515,14 @@ func (bot *CQBot) CQGetForwardMessage(resId string) MSG {
} }
var r []MSG var r []MSG
for _, n := range m.Nodes { for _, n := range m.Nodes {
checkImage(n.Message) bot.checkMedia(n.Message)
r = append(r, MSG{ r = append(r, MSG{
"sender": MSG{ "sender": MSG{
"user_id": n.SenderId, "user_id": n.SenderId,
"nickname": n.SenderName, "nickname": n.SenderName,
}, },
"time": n.Time, "time": n.Time,
"content": ToStringMessage(n.Message, 0, false), "content": ToFormattedMessage(n.Message, 0, false),
}) })
} }
return OK(MSG{ return OK(MSG{
@ -477,7 +553,7 @@ func (bot *CQBot) CQCanSendImage() MSG {
} }
func (bot *CQBot) CQCanSendRecord() MSG { func (bot *CQBot) CQCanSendRecord() MSG {
return OK(MSG{"yes": false}) return OK(MSG{"yes": true})
} }
func (bot *CQBot) CQGetStatus() MSG { func (bot *CQBot) CQGetStatus() MSG {

View File

@ -12,8 +12,10 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/xujiajun/nutsdb" "github.com/xujiajun/nutsdb"
"hash/crc32" "hash/crc32"
"math/rand"
"path" "path"
"sync" "sync"
"time"
) )
type CQBot struct { type CQBot struct {
@ -24,6 +26,7 @@ type CQBot struct {
friendReqCache sync.Map friendReqCache sync.Map
invitedReqCache sync.Map invitedReqCache sync.Map
joinReqCache sync.Map joinReqCache sync.Map
tempMsgCache sync.Map
} }
type MSG map[string]interface{} type MSG map[string]interface{}
@ -58,8 +61,22 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot.Client.OnGroupMemberLeaved(bot.memberLeaveEvent) bot.Client.OnGroupMemberLeaved(bot.memberLeaveEvent)
bot.Client.OnGroupMemberPermissionChanged(bot.memberPermissionChangedEvent) bot.Client.OnGroupMemberPermissionChanged(bot.memberPermissionChangedEvent)
bot.Client.OnNewFriendRequest(bot.friendRequestEvent) bot.Client.OnNewFriendRequest(bot.friendRequestEvent)
bot.Client.OnNewFriendAdded(bot.friendAddedEvent)
bot.Client.OnGroupInvited(bot.groupInvitedEvent) bot.Client.OnGroupInvited(bot.groupInvitedEvent)
bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent) bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent)
go func() {
for {
time.Sleep(time.Second * 5)
bot.dispatchEventMessage(MSG{
"time": time.Now().Unix(),
"self_id": bot.Client.Uin,
"post_type": "meta_event",
"meta_event_type": "heartbeat",
"status": nil,
"interval": 5000,
})
}
}()
return bot return bot
} }
@ -82,7 +99,7 @@ func (bot *CQBot) GetGroupMessage(mid int32) MSG {
if err == nil { if err == nil {
return m return m
} }
log.Warnf("获取信息时出现错误: %v", err) log.Warnf("获取信息时出现错误: %v id: %v", err, mid)
} }
return nil return nil
} }
@ -91,6 +108,7 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
var newElem []message.IMessageElement var newElem []message.IMessageElement
for _, elem := range m.Elements { for _, elem := range m.Elements {
if i, ok := elem.(*message.ImageElement); ok { if i, ok := elem.(*message.ImageElement); ok {
_, _ = bot.Client.UploadGroupImage(int64(rand.Intn(11451419)), i.Data)
gm, err := bot.Client.UploadGroupImage(groupId, i.Data) gm, err := bot.Client.UploadGroupImage(groupId, i.Data)
if err != nil { if err != nil {
log.Warnf("警告: 群 %v 消息图片上传失败: %v", groupId, err) log.Warnf("警告: 群 %v 消息图片上传失败: %v", groupId, err)
@ -100,7 +118,7 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
continue continue
} }
if i, ok := elem.(*message.VoiceElement); ok { if i, ok := elem.(*message.VoiceElement); ok {
gv, err := bot.Client.UploadGroupPtt(groupId, i.Data, int32(len(i.Data))) gv, err := bot.Client.UploadGroupPtt(groupId, i.Data)
if err != nil { if err != nil {
log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err) log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err)
continue continue
@ -112,6 +130,9 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
} }
m.Elements = newElem m.Elements = newElem
ret := bot.Client.SendGroupMessage(groupId, m) ret := bot.Client.SendGroupMessage(groupId, m)
if ret == nil || ret.Id == -1 {
return -1
}
return bot.InsertGroupMessage(ret) return bot.InsertGroupMessage(ret)
} }
@ -121,7 +142,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
if i, ok := elem.(*message.ImageElement); ok { if i, ok := elem.(*message.ImageElement); ok {
fm, err := bot.Client.UploadPrivateImage(target, i.Data) fm, err := bot.Client.UploadPrivateImage(target, i.Data)
if err != nil { if err != nil {
log.Warnf("警告: 好友 %v 消息图片上传失败.", target) log.Warnf("警告: 私聊 %v 消息图片上传失败.", target)
continue continue
} }
newElem = append(newElem, fm) newElem = append(newElem, fm)
@ -130,8 +151,17 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
newElem = append(newElem, elem) newElem = append(newElem, elem)
} }
m.Elements = newElem m.Elements = newElem
ret := bot.Client.SendPrivateMessage(target, m) var id int32
return ToGlobalId(target, ret.Id) if bot.Client.FindFriend(target) != nil {
id = bot.Client.SendPrivateMessage(target, m).Id
} else {
if code, ok := bot.tempMsgCache.Load(target); ok {
id = bot.Client.SendTempMessage(code.(int64), target, m).Id
} else {
return -1
}
}
return ToGlobalId(target, id)
} }
func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 { func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 {
@ -173,7 +203,15 @@ func (bot *CQBot) Release() {
func (bot *CQBot) dispatchEventMessage(m MSG) { func (bot *CQBot) dispatchEventMessage(m MSG) {
for _, f := range bot.events { for _, f := range bot.events {
f(m) fn := f
go func() {
start := time.Now()
fn(m)
end := time.Now()
if end.Sub(start) > time.Second*5 {
log.Debugf("警告: 事件处理耗时超过 5 秒 (%v), 请检查应用是否有堵塞.", end.Sub(start))
}
}()
} }
} }

View File

@ -1,6 +1,7 @@
package coolq package coolq
import ( import (
"crypto/md5"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
@ -23,6 +24,88 @@ var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`)
var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) var typeReg = regexp.MustCompile(`\[CQ:(\w+)`)
var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`)
func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []MSG) {
ur := false
if len(raw) != 0 {
ur = raw[0]
}
for _, elem := range e {
m := MSG{}
switch o := elem.(type) {
case *message.TextElement:
m = MSG{
"type": "text",
"data": map[string]string{"text": o.Content},
}
case *message.AtElement:
if o.Target == 0 {
m = MSG{
"type": "at",
"data": map[string]string{"qq": "all"},
}
} else {
m = MSG{
"type": "at",
"data": map[string]string{"qq": fmt.Sprint(o.Target)},
}
}
case *message.ReplyElement:
m = MSG{
"type": "reply",
"data": map[string]string{"id": fmt.Sprint(ToGlobalId(code, o.ReplySeq))},
}
case *message.ForwardElement:
m = MSG{
"type": "forward",
"data": map[string]string{"id": o.ResId},
}
case *message.FaceElement:
m = MSG{
"type": "face",
"data": map[string]string{"id": fmt.Sprint(o.Index)},
}
case *message.VoiceElement:
if ur {
m = MSG{
"type": "record",
"data": map[string]string{"file": o.Name},
}
} else {
m = MSG{
"type": "record",
"data": map[string]string{"file": o.Name, "url": o.Url},
}
}
case *message.ShortVideoElement:
if ur {
m = MSG{
"type": "video",
"data": map[string]string{"file": o.Name},
}
} else {
m = MSG{
"type": "video",
"data": map[string]string{"file": o.Name, "url": o.Url},
}
}
case *message.ImageElement:
if ur {
m = MSG{
"type": "image",
"data": map[string]string{"file": o.Filename},
}
} else {
m = MSG{
"type": "image",
"data": map[string]string{"file": o.Filename, "url": o.Url},
}
}
}
r = append(r, m)
}
return
}
func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r string) { func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r string) {
ur := false ur := false
if len(raw) != 0 { if len(raw) != 0 {
@ -31,7 +114,7 @@ func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r st
for _, elem := range e { for _, elem := range e {
switch o := elem.(type) { switch o := elem.(type) {
case *message.TextElement: case *message.TextElement:
r += o.Content r += CQCodeEscapeText(o.Content)
case *message.AtElement: case *message.AtElement:
if o.Target == 0 { if o.Target == 0 {
r += "[CQ:at,qq=all]" r += "[CQ:at,qq=all]"
@ -45,12 +128,22 @@ func ToStringMessage(e []message.IMessageElement, code int64, raw ...bool) (r st
case *message.FaceElement: case *message.FaceElement:
r += fmt.Sprintf(`[CQ:face,id=%d]`, o.Index) r += fmt.Sprintf(`[CQ:face,id=%d]`, o.Index)
case *message.VoiceElement: case *message.VoiceElement:
r += fmt.Sprintf(`[CQ:record,file=%s]`, o.Name) if ur {
r += fmt.Sprintf(`[CQ:record,file=%s]`, o.Name)
} else {
r += fmt.Sprintf(`[CQ:record,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
}
case *message.ShortVideoElement:
if ur {
r += fmt.Sprintf(`[CQ:video,file=%s]`, o.Name)
} else {
r += fmt.Sprintf(`[CQ:video,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
}
case *message.ImageElement: case *message.ImageElement:
if ur { if ur {
r += fmt.Sprintf(`[CQ:image,file=%s]`, o.Filename) r += fmt.Sprintf(`[CQ:image,file=%s]`, o.Filename)
} else { } else {
r += fmt.Sprintf(`[CQ:image,file=%s,url=%s]`, o.Filename, o.Url) r += fmt.Sprintf(`[CQ:image,file=%s,url=%s]`, o.Filename, CQCodeEscapeValue(o.Url))
} }
} }
} }
@ -63,7 +156,7 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
for _, idx := range i { for _, idx := range i {
if idx[0] > si { if idx[0] > si {
text := m[si:idx[0]] text := m[si:idx[0]]
r = append(r, message.NewText(text)) r = append(r, message.NewText(CQCodeUnescapeText(text)))
} }
code := m[idx[0]:idx[1]] code := m[idx[0]:idx[1]]
si = idx[1] si = idx[1]
@ -71,7 +164,7 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
ps := paramReg.FindAllStringSubmatch(code, -1) ps := paramReg.FindAllStringSubmatch(code, -1)
d := make(map[string]string) d := make(map[string]string)
for _, p := range ps { for _, p := range ps {
d[p[1]] = p[2] d[p[1]] = CQCodeUnescapeValue(p[2])
} }
if t == "reply" && group { if t == "reply" && group {
if len(r) > 0 { if len(r) > 0 {
@ -105,7 +198,7 @@ func (bot *CQBot) ConvertStringMessage(m string, group bool) (r []message.IMessa
r = append(r, elem) r = append(r, elem)
} }
if si != len(m) { if si != len(m) {
r = append(r, message.NewText(m[si:])) r = append(r, message.NewText(CQCodeUnescapeText(m[si:])))
} }
return return
} }
@ -169,10 +262,23 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
case "image": case "image":
f := d["file"] f := d["file"]
if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") {
cache := d["cache"]
if cache == "" {
cache = "1"
}
hash := md5.Sum([]byte(f))
cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".cache")
if global.PathExists(cacheFile) && cache == "1" {
b, err := ioutil.ReadFile(cacheFile)
if err == nil {
return message.NewImage(b), nil
}
}
b, err := global.GetBytes(f) b, err := global.GetBytes(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_ = ioutil.WriteFile(cacheFile, b, 0644)
return message.NewImage(b), nil return message.NewImage(b), nil
} }
if strings.HasPrefix(f, "base64") { if strings.HasPrefix(f, "base64") {
@ -200,6 +306,9 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") {
rawPath += ".cqimg" rawPath += ".cqimg"
} }
if !global.PathExists(rawPath) && d["url"] != "" {
return bot.ToElement(t, map[string]string{"file": d["url"]}, group)
}
if global.PathExists(rawPath) { if global.PathExists(rawPath) {
b, err := ioutil.ReadFile(rawPath) b, err := ioutil.ReadFile(rawPath)
if err != nil { if err != nil {
@ -283,7 +392,14 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
} }
data = b data = b
} }
if !global.IsAMR(data) { if global.PathExists(path.Join(global.VOICE_PATH, f)) {
b, err := ioutil.ReadFile(path.Join(global.VOICE_PATH, f))
if err != nil {
return nil, err
}
data = b
}
if !global.IsAMRorSILK(data) {
return nil, errors.New("unsupported voice file format (please use AMR file for now)") return nil, errors.New("unsupported voice file format (please use AMR file for now)")
} }
return &message.VoiceElement{Data: data}, nil return &message.VoiceElement{Data: data}, nil
@ -306,3 +422,31 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (message.
return nil, errors.New("unsupported cq code: " + t) return nil, errors.New("unsupported cq code: " + t)
} }
} }
func CQCodeEscapeText(raw string) string {
ret := raw
ret = strings.ReplaceAll(ret, "&", "&amp;")
ret = strings.ReplaceAll(ret, "[", "&#91;")
ret = strings.ReplaceAll(ret, "]", "&#93;")
return ret
}
func CQCodeEscapeValue(value string) string {
ret := CQCodeEscapeText(value)
ret = strings.ReplaceAll(ret, ",", "&#44;")
return ret
}
func CQCodeUnescapeText(content string) string {
ret := content
ret = strings.ReplaceAll(ret, "&#91;", "[")
ret = strings.ReplaceAll(ret, "&#93;", "]")
ret = strings.ReplaceAll(ret, "&amp;", "&")
return ret
}
func CQCodeUnescapeValue(content string) string {
ret := strings.ReplaceAll(content, "&#44;", ",")
ret = CQCodeUnescapeText(ret)
return ret
}

View File

@ -10,11 +10,27 @@ import (
"io/ioutil" "io/ioutil"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
) )
var format = "string"
func SetMessageFormat(f string) {
format = f
}
func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r interface{}) {
if format == "string" {
r = ToStringMessage(e, code, raw...)
} else if format == "array" {
r = ToArrayMessage(e, code, raw...)
}
return
}
func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) { func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) {
checkImage(m.Elements) bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true) cqm := ToStringMessage(m.Elements, 0, true)
log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm) log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm)
fm := MSG{ fm := MSG{
@ -23,7 +39,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
"sub_type": "friend", "sub_type": "friend",
"message_id": ToGlobalId(m.Sender.Uin, m.Id), "message_id": ToGlobalId(m.Sender.Uin, m.Id),
"user_id": m.Sender.Uin, "user_id": m.Sender.Uin,
"message": ToStringMessage(m.Elements, 0, false), "message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm, "raw_message": cqm,
"font": 0, "font": 0,
"self_id": c.Uin, "self_id": c.Uin,
@ -39,7 +55,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
} }
func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) { func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) {
checkImage(m.Elements) bot.checkMedia(m.Elements)
for _, elem := range m.Elements { for _, elem := range m.Elements {
if file, ok := elem.(*message.GroupFileElement); ok { 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) log.Infof("群 %v(%v) 内 %v(%v) 上传了文件: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, file.Name)
@ -71,7 +87,7 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
"anonymous": nil, "anonymous": nil,
"font": 0, "font": 0,
"group_id": m.GroupCode, "group_id": m.GroupCode,
"message": ToStringMessage(m.Elements, m.GroupCode, false), "message": ToFormattedMessage(m.Elements, m.GroupCode, false),
"message_id": id, "message_id": id,
"message_type": "group", "message_type": "group",
"post_type": "message", "post_type": "message",
@ -117,8 +133,9 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
} }
func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) { func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
checkImage(m.Elements) bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true) cqm := ToStringMessage(m.Elements, 0, true)
bot.tempMsgCache.Store(m.Sender.Uin, m.GroupCode)
log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm) log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm)
tm := MSG{ tm := MSG{
"post_type": "message", "post_type": "message",
@ -126,7 +143,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
"sub_type": "group", "sub_type": "group",
"message_id": m.Id, "message_id": m.Id,
"user_id": m.Sender.Uin, "user_id": m.Sender.Uin,
"message": ToStringMessage(m.Elements, 0, false), "message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm, "raw_message": cqm,
"font": 0, "font": 0,
"self_id": c.Uin, "self_id": c.Uin,
@ -260,6 +277,18 @@ func (bot *CQBot) friendRequestEvent(c *client.QQClient, e *client.NewFriendRequ
}) })
} }
func (bot *CQBot) friendAddedEvent(c *client.QQClient, e *client.NewFriendEvent) {
log.Infof("添加了新好友: %v(%v)", e.Friend.Nickname, e.Friend.Uin)
bot.tempMsgCache.Delete(e.Friend.Uin)
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "friend_add",
"self_id": c.Uin,
"user_id": e.Friend.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) { func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) {
log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin) log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin)
flag := strconv.FormatInt(e.RequestId, 10) flag := strconv.FormatInt(e.RequestId, 10)
@ -278,7 +307,7 @@ func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRe
} }
func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) { func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) {
log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupName, e.RequesterNick, e.RequesterUin) log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupCode, e.RequesterNick, e.RequesterUin)
flag := strconv.FormatInt(e.RequestId, 10) flag := strconv.FormatInt(e.RequestId, 10)
bot.joinReqCache.Store(flag, e) bot.joinReqCache.Store(flag, e)
bot.dispatchEventMessage(MSG{ bot.dispatchEventMessage(MSG{
@ -287,7 +316,7 @@ func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupR
"sub_type": "add", "sub_type": "add",
"group_id": e.GroupCode, "group_id": e.GroupCode,
"user_id": e.RequesterUin, "user_id": e.RequesterUin,
"comment": "", "comment": e.Message,
"flag": flag, "flag": flag,
"time": time.Now().Unix(), "time": time.Now().Unix(),
"self_id": c.Uin, "self_id": c.Uin,
@ -333,9 +362,10 @@ func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.Group
} }
} }
func checkImage(e []message.IMessageElement) { func (bot *CQBot) checkMedia(e []message.IMessageElement) {
for _, elem := range e { for _, elem := range e {
if i, ok := elem.(*message.ImageElement); ok { switch i := elem.(type) {
case *message.ImageElement:
filename := hex.EncodeToString(i.Md5) + ".image" filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.IMAGE_PATH, filename)) { if !global.PathExists(path.Join(global.IMAGE_PATH, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
@ -343,9 +373,32 @@ func checkImage(e []message.IMessageElement) {
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename) w.WriteString(i.Filename)
w.WriteString(i.Url) w.WriteString(i.Url)
}), 0777) }), 0644)
} }
i.Filename = filename i.Filename = filename
case *message.VoiceElement:
i.Name = strings.ReplaceAll(i.Name, "{", "")
i.Name = strings.ReplaceAll(i.Name, "}", "")
if !global.PathExists(path.Join(global.VOICE_PATH, i.Name)) {
b, err := global.GetBytes(i.Url)
if err != nil {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue
}
_ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0644)
}
case *message.ShortVideoElement:
filename := hex.EncodeToString(i.Md5) + ".video"
if !global.PathExists(path.Join(global.VIDEO_PATH, filename)) {
_ = ioutil.WriteFile(path.Join(global.VIDEO_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Name)
w.Write(i.Uuid)
}), 0644)
}
i.Name = filename
i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5)
} }
} }
} }

View File

@ -18,15 +18,19 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
{ {
"uin": 0, "uin": 0,
"password": "", "password": "",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true, "enable_db": true,
"access_token": "", "access_token": "",
"relogin": false, "relogin": false,
"relogin_delay": 0, "relogin_delay": 0,
"post_message_format": "string",
"http_config": { "http_config": {
"enabled": true, "enabled": true,
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 5700, "port": 5700,
"post_urls": [] "timeout": 5,
"post_urls": {"url:port": "secret"},
}, },
"ws_config": { "ws_config": {
"enabled": true, "enabled": true,
@ -49,6 +53,8 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
| ------------------ | -------- | ------------------------------------------------------------------- | | ------------------ | -------- | ------------------------------------------------------------------- |
| uin | int64 | 登录用QQ号 | | uin | int64 | 登录用QQ号 |
| password | string | 登录用密码 | | password | string | 登录用密码 |
| encrypt_password | bool | 是否对密码进行加密. |
| password_encrypted | string | 加密后的密码(请勿修改) |
| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 | | enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 | | access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
| relogin | bool | 是否自动重新登录 | | relogin | bool | 是否自动重新登录 |
@ -57,4 +63,6 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
| ws_config | object | Websocket API 配置 | | ws_config | object | Websocket API 配置 |
| ws_reverse_servers | object[] | 反向 Websocket API 配置 | | ws_reverse_servers | object[] | 反向 Websocket API 配置 |
> 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
> 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥

View File

@ -6,16 +6,20 @@ import (
) )
type JsonConfig struct { type JsonConfig struct {
Uin int64 `json:"uin"` Uin int64 `json:"uin"`
Password string `json:"password"` Password string `json:"password"`
EnableDB bool `json:"enable_db"` EncryptPassword bool `json:"encrypt_password"`
AccessToken string `json:"access_token"` PasswordEncrypted string `json:"password_encrypted"`
ReLogin bool `json:"relogin"` EnableDB bool `json:"enable_db"`
ReLoginDelay int `json:"relogin_delay"` AccessToken string `json:"access_token"`
HttpConfig *GoCQHttpConfig `json:"http_config"` ReLogin bool `json:"relogin"`
WSConfig *GoCQWebsocketConfig `json:"ws_config"` ReLoginDelay int `json:"relogin_delay"`
ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"` AsyncLoad bool `json:"async_load"`
Debug bool `json:"debug"` HttpConfig *GoCQHttpConfig `json:"http_config"`
WSConfig *GoCQWebsocketConfig `json:"ws_config"`
ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
PostMessageFormat string `json:"post_message_format"`
Debug bool `json:"debug"`
} }
type CQHttpApiConfig struct { type CQHttpApiConfig struct {
@ -41,6 +45,7 @@ type GoCQHttpConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Host string `json:"host"` Host string `json:"host"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
Timeout int32 `json:"timeout"`
PostUrls map[string]string `json:"post_urls"` PostUrls map[string]string `json:"post_urls"`
} }
@ -60,7 +65,10 @@ type GoCQReverseWebsocketConfig struct {
func DefaultConfig() *JsonConfig { func DefaultConfig() *JsonConfig {
return &JsonConfig{ return &JsonConfig{
EnableDB: true, EnableDB: true,
ReLogin: true,
ReLoginDelay: 3,
PostMessageFormat: "string",
HttpConfig: &GoCQHttpConfig{ HttpConfig: &GoCQHttpConfig{
Enabled: true, Enabled: true,
Host: "0.0.0.0", Host: "0.0.0.0",

View File

@ -1,13 +1,23 @@
package global package global
import ( import (
log "github.com/sirupsen/logrus" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
log "github.com/sirupsen/logrus"
) )
var IMAGE_PATH = path.Join("data", "images") var (
IMAGE_PATH = path.Join("data", "images")
VOICE_PATH = path.Join("data", "voices")
VIDEO_PATH = path.Join("data", "videos")
CACHE_PATH = path.Join("data", "cache")
HEADER_AMR = []byte("#!AMR")
HEADER_SILK = []byte("\x02#!SILK_V3")
)
func PathExists(path string) bool { func PathExists(path string) bool {
_, err := os.Stat(path) _, err := os.Stat(path)
@ -23,7 +33,7 @@ func ReadAllText(path string) string {
} }
func WriteAllText(path, text string) { func WriteAllText(path, text string) {
_ = ioutil.WriteFile(path, []byte(text), 0777) _ = ioutil.WriteFile(path, []byte(text), 0644)
} }
func Check(err error) { func Check(err error) {
@ -32,9 +42,6 @@ func Check(err error) {
} }
} }
func IsAMR(b []byte) bool { func IsAMRorSILK(b []byte) bool {
if len(b) <= 6 { return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK)
return false
}
return b[0] == 0x23 && b[1] == 0x21 && b[2] == 0x41 && b[3] == 0x4D && b[4] == 0x52 // amr file header
} }

50
global/param.go Normal file
View File

@ -0,0 +1,50 @@
package global
import (
"github.com/tidwall/gjson"
"strings"
)
var trueSet = map[string]struct{}{
"true": {},
"yes": {},
"1": {},
}
var falseSet = map[string]struct{}{
"false": {},
"no": {},
"0": {},
}
func EnsureBool(p interface{}, defaultVal bool) bool {
var str string
if b, ok := p.(bool); ok {
return b
}
if j, ok := p.(gjson.Result); ok {
if !j.Exists() {
return defaultVal
}
if j.Type == gjson.True {
return true
}
if j.Type == gjson.False {
return false
}
if j.Type != gjson.String {
return defaultVal
}
str = j.Str
} else if s, ok := p.(string); ok {
str = s
}
str = strings.ToLower(str)
if _, ok := trueSet[str]; ok {
return true
}
if _, ok := falseSet[str]; ok {
return false
}
return defaultVal
}

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/Mrs4s/go-cqhttp
go 1.14 go 1.14
require ( require (
github.com/Mrs4s/MiraiGo v0.0.0-20200808044635-cd20d7d43dbb github.com/Mrs4s/MiraiGo v0.0.0-20200819185537-8d1e5fb04c17
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/guonaihong/gout v0.1.1 github.com/guonaihong/gout v0.1.1

8
go.sum
View File

@ -1,11 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Mrs4s/MiraiGo v0.0.0-20200807030850-ed30f7ad5934 h1:LoNjIsnyEQFGP9IchIQ65yHRCfNKSru3BAOguRepkCM= github.com/Mrs4s/MiraiGo v0.0.0-20200819185537-8d1e5fb04c17 h1:sY4lwW34serGKnD26hP7ZLpnJ4NfVYKdS2SJIW1Mezs=
github.com/Mrs4s/MiraiGo v0.0.0-20200807030850-ed30f7ad5934/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM= github.com/Mrs4s/MiraiGo v0.0.0-20200819185537-8d1e5fb04c17/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
github.com/Mrs4s/MiraiGo v0.0.0-20200808003732-2a32e623270d h1:K9jHdcO13mLqQB0xm0/ZlY852FoVQJ/WSDwfdmfhDlU=
github.com/Mrs4s/MiraiGo v0.0.0-20200808003732-2a32e623270d/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
github.com/Mrs4s/MiraiGo v0.0.0-20200808044635-cd20d7d43dbb h1:XLe/UreYJRT65GStA3+irRL1Ao0pHZwBtCmTc+4prwA=
github.com/Mrs4s/MiraiGo v0.0.0-20200808044635-cd20d7d43dbb/go.mod h1:0je03wji/tSw4bUH4QCF2Z4/EjyNWjSJTyy5tliX6EM=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

88
main.go
View File

@ -3,8 +3,11 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/md5"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
@ -33,14 +36,26 @@ func init() {
if err == nil { if err == nil {
log.SetOutput(io.MultiWriter(os.Stderr, w)) log.SetOutput(io.MultiWriter(os.Stderr, w))
} }
if !global.PathExists("data") { if !global.PathExists(global.IMAGE_PATH) {
if err := os.Mkdir("data", 0777); err != nil { if err := os.MkdirAll(global.IMAGE_PATH, 0755); err != nil {
log.Fatalf("创建数据文件夹失败: %v", err)
}
if err := os.Mkdir(path.Join("data", "images"), 0777); err != nil {
log.Fatalf("创建图片缓存文件夹失败: %v", err) log.Fatalf("创建图片缓存文件夹失败: %v", err)
} }
} }
if !global.PathExists(global.VOICE_PATH) {
if err := os.MkdirAll(global.VOICE_PATH, 0755); err != nil {
log.Fatalf("创建语音缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.VIDEO_PATH) {
if err := os.MkdirAll(global.VIDEO_PATH, 0755); err != nil {
log.Fatalf("创建视频缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.CACHE_PATH) {
if err := os.MkdirAll(global.CACHE_PATH, 0755); err != nil {
log.Fatalf("创建发送图片缓存文件夹失败: %v", err)
}
}
if global.PathExists("cqhttp.json") { if global.PathExists("cqhttp.json") {
log.Info("发现 cqhttp.json 将在五秒后尝试导入配置,按 Ctrl+C 取消.") log.Info("发现 cqhttp.json 将在五秒后尝试导入配置,按 Ctrl+C 取消.")
log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.json 文件.") log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.json 文件.")
@ -96,7 +111,8 @@ func main() {
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 6700, Port: 6700,
}, },
Debug: os.Getenv("DEBUG") == "true", PostMessageFormat: "string",
Debug: os.Getenv("DEBUG") == "true",
} }
if post != "" { if post != "" {
conf.HttpConfig.PostUrls[post] = os.Getenv("HTTP_SECRET") conf.HttpConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
@ -112,7 +128,7 @@ func main() {
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
return return
} }
if conf.Uin == 0 || conf.Password == "" { if conf.Uin == 0 || (conf.Password == "" && conf.PasswordEncrypted == "") {
log.Warnf("请修改 config.json 以添加账号密码.") log.Warnf("请修改 config.json 以添加账号密码.")
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
return return
@ -124,7 +140,7 @@ func main() {
if !global.PathExists("device.json") { if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.") log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice() client.GenRandomDevice()
_ = ioutil.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0777) _ = ioutil.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0644)
log.Info("已生成设备信息并保存到 device.json 文件.") log.Info("已生成设备信息并保存到 device.json 文件.")
} else { } else {
log.Info("将使用 device.json 内的设备信息运行Bot.") log.Info("将使用 device.json 内的设备信息运行Bot.")
@ -132,6 +148,24 @@ func main() {
log.Fatalf("加载设备信息失败: %v", err) log.Fatalf("加载设备信息失败: %v", err)
} }
} }
if conf.EncryptPassword && conf.PasswordEncrypted == "" {
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
strKey, _ := console.ReadString('\n')
key := md5.Sum([]byte(strKey))
if encrypted := EncryptPwd(conf.Password, key[:]); encrypted != "" {
conf.Password = ""
conf.PasswordEncrypted = encrypted
_ = conf.Save("config.json")
} else {
log.Warnf("加密时出现问题.")
}
}
if conf.PasswordEncrypted != "" {
log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
strKey, _ := console.ReadString('\n')
key := md5.Sum([]byte(strKey))
conf.Password = DecryptPwd(conf.PasswordEncrypted, key[:])
}
log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.") log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
log.Info("开始尝试登录并同步消息...") log.Info("开始尝试登录并同步消息...")
@ -142,9 +176,10 @@ func main() {
if !rsp.Success { if !rsp.Success {
switch rsp.Error { switch rsp.Error {
case client.NeedCaptcha: case client.NeedCaptcha:
_ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644)
img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage)) img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage))
fmt.Println(asciiart.New("image", img).Art) fmt.Println(asciiart.New("image", img).Art)
log.Warn("请输入验证码: (Enter 提交)") log.Warn("请输入验证码 (captcha.jpg) (Enter 提交)")
text, _ := console.ReadString('\n') text, _ := console.ReadString('\n')
rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign) rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign)
continue continue
@ -165,13 +200,19 @@ func main() {
global.Check(cli.ReloadFriendList()) global.Check(cli.ReloadFriendList())
log.Infof("共加载 %v 个好友.", len(cli.FriendList)) log.Infof("共加载 %v 个好友.", len(cli.FriendList))
log.Infof("开始加载群列表...") log.Infof("开始加载群列表...")
global.Check(cli.ReloadGroupList()) global.Check(cli.ReloadGroupList(conf.AsyncLoad))
log.Infof("共加载 %v 个群.", len(cli.GroupList)) log.Infof("共加载 %v 个群.", len(cli.GroupList))
b := coolq.NewQQBot(cli, conf) b := coolq.NewQQBot(cli, conf)
if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" {
log.Warnf("post_message_format 配置错误, 将自动使用 string")
coolq.SetMessageFormat("string")
} else {
coolq.SetMessageFormat(conf.PostMessageFormat)
}
if conf.HttpConfig != nil && conf.HttpConfig.Enabled { if conf.HttpConfig != nil && conf.HttpConfig.Enabled {
server.HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, b) server.HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, b)
for k, v := range conf.HttpConfig.PostUrls { for k, v := range conf.HttpConfig.PostUrls {
server.NewHttpClient().Run(k, v, b) server.NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, b)
} }
} }
if conf.WSConfig != nil && conf.WSConfig.Enabled { if conf.WSConfig != nil && conf.WSConfig.Enabled {
@ -210,3 +251,28 @@ func main() {
<-c <-c
b.Release() b.Release()
} }
func EncryptPwd(pwd string, key []byte) string {
tea := binary.NewTeaCipher(key)
if tea == nil {
return ""
}
return base64.StdEncoding.EncodeToString(tea.Encrypt([]byte(pwd)))
}
func DecryptPwd(ePwd string, key []byte) string {
defer func() {
if pan := recover(); pan != nil {
log.Fatalf("密码解密失败: %v", pan)
}
}()
encrypted, err := base64.StdEncoding.DecodeString(ePwd)
if err != nil {
panic(err)
}
tea := binary.NewTeaCipher(key)
if tea == nil {
panic("密钥错误")
}
return string(tea.Decrypt(encrypted))
}

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/guonaihong/gout" "github.com/guonaihong/gout"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -21,9 +22,10 @@ type httpServer struct {
} }
type httpClient struct { type httpClient struct {
bot *coolq.CQBot bot *coolq.CQBot
secret string secret string
addr string addr string
timeout int32
} }
var HttpServer = &httpServer{} var HttpServer = &httpServer{}
@ -132,12 +134,12 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
s.engine.Any("/set_group_leave_async", s.SetGroupLeave) s.engine.Any("/set_group_leave_async", s.SetGroupLeave)
s.engine.Any("/get_image", s.GetImage) s.engine.Any("/get_image", s.GetImage)
s.engine.Any("/get_image_async", s.GetImage)
s.engine.Any("/get_forward_msg", s.GetForwardMessage) s.engine.Any("/get_forward_msg", s.GetForwardMessage)
s.engine.Any("/get_group_msg", s.GetGroupMessage) s.engine.Any("/get_group_msg", s.GetGroupMessage)
s.engine.Any("/get_group_msg_async", s.GetGroupMessage)
s.engine.Any("/get_group_honor_info", s.GetGroupHonorInfo)
s.engine.Any("/can_send_image", s.CanSendImage) s.engine.Any("/can_send_image", s.CanSendImage)
s.engine.Any("/can_send_image_async", s.CanSendImage) s.engine.Any("/can_send_image_async", s.CanSendImage)
@ -163,10 +165,14 @@ func NewHttpClient() *httpClient {
return &httpClient{} return &httpClient{}
} }
func (c *httpClient) Run(addr, secret string, bot *coolq.CQBot) { func (c *httpClient) Run(addr, secret string, timeout int32, bot *coolq.CQBot) {
c.bot = bot c.bot = bot
c.secret = secret c.secret = secret
c.addr = addr c.addr = addr
c.timeout = timeout
if c.timeout < 5 {
c.timeout = 5
}
bot.OnEventPush(c.onBotPushEvent) bot.OnEventPush(c.onBotPushEvent)
log.Infof("HTTP POST上报器已启动: %v", addr) log.Infof("HTTP POST上报器已启动: %v", addr)
} }
@ -184,7 +190,7 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) {
h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil)) h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil))
} }
return h return h
}()).SetTimeout(time.Second * 5).Do() }()).SetTimeout(time.Second * time.Duration(c.timeout)).Do()
if err != nil { if err != nil {
log.Warnf("上报Event数据到 %v 失败: %v", c.addr, err) log.Warnf("上报Event数据到 %v 失败: %v", c.addr, err)
return return
@ -203,7 +209,8 @@ func (s *httpServer) GetFriendList(c *gin.Context) {
} }
func (s *httpServer) GetGroupList(c *gin.Context) { func (s *httpServer) GetGroupList(c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupList()) nc := getParamOrDefault(c, "no_cache", "false")
c.JSON(200, s.bot.CQGetGroupList(nc == "true"))
} }
func (s *httpServer) GetGroupInfo(c *gin.Context) { func (s *httpServer) GetGroupInfo(c *gin.Context) {
@ -224,6 +231,14 @@ func (s *httpServer) GetGroupMemberInfo(c *gin.Context) {
} }
func (s *httpServer) SendMessage(c *gin.Context) { func (s *httpServer) SendMessage(c *gin.Context) {
if getParam(c, "message_type") == "private" {
s.SendPrivateMessage(c)
return
}
if getParam(c, "message_type") == "group" {
s.SendGroupMessage(c)
return
}
if getParam(c, "group_id") != "" { if getParam(c, "group_id") != "" {
s.SendGroupMessage(c) s.SendGroupMessage(c)
return return
@ -235,22 +250,24 @@ func (s *httpServer) SendMessage(c *gin.Context) {
func (s *httpServer) SendPrivateMessage(c *gin.Context) { func (s *httpServer) SendPrivateMessage(c *gin.Context) {
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64) uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
msg := getParam(c, "message") msg, t := getParamWithType(c, "message")
if gjson.Valid(msg) { autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg))) if t == gjson.JSON {
c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg), autoEscape))
return return
} }
c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg)) c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg, autoEscape))
} }
func (s *httpServer) SendGroupMessage(c *gin.Context) { func (s *httpServer) SendGroupMessage(c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64) gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
msg := getParam(c, "message") msg, t := getParamWithType(c, "message")
if gjson.Valid(msg) { autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg))) if t == gjson.JSON {
c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg), autoEscape))
return return
} }
c.JSON(200, s.bot.CQSendGroupMessage(gid, msg)) c.JSON(200, s.bot.CQSendGroupMessage(gid, msg, autoEscape))
} }
func (s *httpServer) SendGroupForwardMessage(c *gin.Context) { func (s *httpServer) SendGroupForwardMessage(c *gin.Context) {
@ -269,6 +286,11 @@ func (s *httpServer) GetGroupMessage(c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupMessage(int32(mid))) c.JSON(200, s.bot.CQGetGroupMessage(int32(mid)))
} }
func (s *httpServer) GetGroupHonorInfo(c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQGetGroupHonorInfo(gid, getParam(c, "type")))
}
func (s *httpServer) ProcessFriendRequest(c *gin.Context) { func (s *httpServer) ProcessFriendRequest(c *gin.Context) {
flag := getParam(c, "flag") flag := getParam(c, "flag")
approve := getParamOrDefault(c, "approve", "true") approve := getParamOrDefault(c, "approve", "true")
@ -372,14 +394,19 @@ func getParamOrDefault(c *gin.Context, k, def string) string {
} }
func getParam(c *gin.Context, k string) string { func getParam(c *gin.Context, k string) string {
p, _ := getParamWithType(c, k)
return p
}
func getParamWithType(c *gin.Context, k string) (string, gjson.Type) {
if q := c.Query(k); q != "" { if q := c.Query(k); q != "" {
return q return q, gjson.Null
} }
if c.Request.Method == "POST" { if c.Request.Method == "POST" {
if h := c.Request.Header.Get("Content-Type"); h != "" { if h := c.Request.Header.Get("Content-Type"); h != "" {
if h == "application/x-www-form-urlencoded" { if h == "application/x-www-form-urlencoded" {
if p, ok := c.GetPostForm(k); ok { if p, ok := c.GetPostForm(k); ok {
return p return p, gjson.Null
} }
} }
if h == "application/json" { if h == "application/json" {
@ -388,20 +415,20 @@ func getParam(c *gin.Context, k string) string {
if res.Exists() { if res.Exists() {
switch res.Type { switch res.Type {
case gjson.JSON: case gjson.JSON:
return res.Raw return res.Raw, gjson.JSON
case gjson.String: case gjson.String:
return res.Str return res.Str, gjson.String
case gjson.Number: case gjson.Number:
return strconv.FormatInt(res.Int(), 10) // 似乎没有需要接受 float 类型的api return strconv.FormatInt(res.Int(), 10), gjson.Number // 似乎没有需要接受 float 类型的api
case gjson.True: case gjson.True:
return "true" return "true", gjson.True
case gjson.False: case gjson.False:
return "false" return "false", gjson.False
} }
} }
} }
} }
} }
} }
return "" return "", gjson.Null
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
wsc "golang.org/x/net/websocket"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -29,8 +28,8 @@ type websocketClient struct {
bot *coolq.CQBot bot *coolq.CQBot
pushLock *sync.Mutex pushLock *sync.Mutex
universalConn *wsc.Conn universalConn *websocket.Conn
eventConn *wsc.Conn eventConn *websocket.Conn
} }
var WebsocketServer = &websocketServer{} var WebsocketServer = &websocketServer{}
@ -64,32 +63,30 @@ func (c *websocketClient) Run() {
if !c.conf.Enabled { if !c.conf.Enabled {
return return
} }
if c.conf.ReverseApiUrl != "" {
c.connectApi()
}
if c.conf.ReverseEventUrl != "" {
c.connectEvent()
}
if c.conf.ReverseUrl != "" { if c.conf.ReverseUrl != "" {
c.connectUniversal() c.connectUniversal()
} else {
if c.conf.ReverseApiUrl != "" {
c.connectApi()
}
if c.conf.ReverseEventUrl != "" {
c.connectEvent()
}
} }
c.bot.OnEventPush(c.onBotPushEvent) c.bot.OnEventPush(c.onBotPushEvent)
} }
func (c *websocketClient) connectApi() { func (c *websocketClient) connectApi() {
log.Infof("开始尝试连接到反向Websocket API服务器: %v", c.conf.ReverseApiUrl) log.Infof("开始尝试连接到反向Websocket API服务器: %v", c.conf.ReverseApiUrl)
wsConf, err := wsc.NewConfig(c.conf.ReverseApiUrl, c.conf.ReverseApiUrl) header := http.Header{
if err != nil { "X-Client-Role": []string{"API"},
log.Warnf("连接到反向Websocket API服务器 %v 时出现致命错误: %v", c.conf.ReverseApiUrl, err) "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
return "User-Agent": []string{"CQHttp/4.15.0"},
} }
wsConf.Header["X-Client-Role"] = []string{"API"}
wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" { if c.token != "" {
wsConf.Header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, err := wsc.DialConfig(wsConf) conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseApiUrl, header)
if err != nil { if err != nil {
log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err) log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err)
if c.conf.ReverseReconnectInterval != 0 { if c.conf.ReverseReconnectInterval != 0 {
@ -104,23 +101,20 @@ func (c *websocketClient) connectApi() {
func (c *websocketClient) connectEvent() { func (c *websocketClient) connectEvent() {
log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventUrl) log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventUrl)
wsConf, err := wsc.NewConfig(c.conf.ReverseEventUrl, c.conf.ReverseEventUrl) header := http.Header{
if err != nil { "X-Client-Role": []string{"Event"},
log.Warnf("连接到反向Websocket Event服务器 %v 时出现致命错误: %v", c.conf.ReverseApiUrl, err) "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
return "User-Agent": []string{"CQHttp/4.15.0"},
} }
wsConf.Header["X-Client-Role"] = []string{"Event"}
wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" { if c.token != "" {
wsConf.Header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, err := wsc.DialConfig(wsConf) conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventUrl, header)
if err != nil { if err != nil {
log.Warnf("连接到反向Websocket API服务器 %v 时出现错误: %v", c.conf.ReverseApiUrl, err) log.Warnf("连接到反向Websocket Event服务器 %v 时出现错误: %v", c.conf.ReverseEventUrl, err)
if c.conf.ReverseReconnectInterval != 0 { if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval)) time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectApi() c.connectEvent()
} }
return return
} }
@ -130,18 +124,15 @@ func (c *websocketClient) connectEvent() {
func (c *websocketClient) connectUniversal() { func (c *websocketClient) connectUniversal() {
log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseUrl) log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseUrl)
wsConf, err := wsc.NewConfig(c.conf.ReverseUrl, c.conf.ReverseUrl) header := http.Header{
if err != nil { "X-Client-Role": []string{"Universal"},
log.Warnf("连接到反向Websocket Universal服务器 %v 时出现致命错误: %v", c.conf.ReverseUrl, err) "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
return "User-Agent": []string{"CQHttp/4.15.0"},
} }
wsConf.Header["X-Client-Role"] = []string{"Universal"}
wsConf.Header["X-Self-ID"] = []string{strconv.FormatInt(c.bot.Client.Uin, 10)}
wsConf.Header["User-Agent"] = []string{"CQHttp/4.15.0"}
if c.token != "" { if c.token != "" {
wsConf.Header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, err := wsc.DialConfig(wsConf) conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseUrl, header)
if err != nil { if err != nil {
log.Warnf("连接到反向Websocket Universal服务器 %v 时出现错误: %v", c.conf.ReverseUrl, err) log.Warnf("连接到反向Websocket Universal服务器 %v 时出现错误: %v", c.conf.ReverseUrl, err)
if c.conf.ReverseReconnectInterval != 0 { if c.conf.ReverseReconnectInterval != 0 {
@ -154,12 +145,12 @@ func (c *websocketClient) connectUniversal() {
c.universalConn = conn c.universalConn = conn
} }
func (c *websocketClient) listenApi(conn *wsc.Conn, u bool) { func (c *websocketClient) listenApi(conn *websocket.Conn, u bool) {
defer conn.Close() defer conn.Close()
for { for {
var buf []byte _, buf, err := conn.ReadMessage()
err := wsc.Message.Receive(conn, &buf)
if err != nil { if err != nil {
log.Warnf("监听反向WS API时出现错误: %v", err)
break break
} }
j := gjson.ParseBytes(buf) j := gjson.ParseBytes(buf)
@ -171,17 +162,16 @@ func (c *websocketClient) listenApi(conn *wsc.Conn, u bool) {
ret["echo"] = j.Get("echo").Value() ret["echo"] = j.Get("echo").Value()
} }
c.pushLock.Lock() c.pushLock.Lock()
_, _ = conn.Write([]byte(ret.ToJson())) log.Debugf("准备发送API %v 处理结果: %v", t, ret.ToJson())
_ = conn.WriteJSON(ret)
c.pushLock.Unlock() c.pushLock.Unlock()
} }
} }
if c.conf.ReverseReconnectInterval != 0 { if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval)) time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
if u { if !u {
c.connectUniversal() c.connectApi()
return
} }
c.connectApi()
} }
} }
@ -190,17 +180,29 @@ func (c *websocketClient) onBotPushEvent(m coolq.MSG) {
defer c.pushLock.Unlock() defer c.pushLock.Unlock()
if c.eventConn != nil { if c.eventConn != nil {
log.Debugf("向WS服务器 %v 推送Event: %v", c.eventConn.RemoteAddr().String(), m.ToJson()) log.Debugf("向WS服务器 %v 推送Event: %v", c.eventConn.RemoteAddr().String(), m.ToJson())
if _, err := c.eventConn.Write([]byte(m.ToJson())); err != nil { if err := c.eventConn.WriteJSON(m); err != nil {
log.Warnf("向WS服务器 %v 推送Event时出现错误: %v", c.eventConn.RemoteAddr().String(), err)
_ = c.eventConn.Close() _ = c.eventConn.Close()
if c.conf.ReverseReconnectInterval != 0 { if c.conf.ReverseReconnectInterval != 0 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval)) go func() {
c.connectEvent() time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectEvent()
}()
} }
} }
} }
if c.universalConn != nil { if c.universalConn != nil {
log.Debugf("向WS服务器 %v 推送Event: %v", c.universalConn.RemoteAddr().String(), m.ToJson()) log.Debugf("向WS服务器 %v 推送Event: %v", c.universalConn.RemoteAddr().String(), m.ToJson())
_, _ = c.universalConn.Write([]byte(m.ToJson())) if err := c.universalConn.WriteJSON(m); err != nil {
log.Warnf("向WS服务器 %v 推送Event时出现错误: %v", c.universalConn.RemoteAddr().String(), err)
_ = c.universalConn.Close()
if c.conf.ReverseReconnectInterval != 0 {
go func() {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectUniversal()
}()
}
}
} }
} }
@ -312,7 +314,7 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
return bot.CQGetFriendList() return bot.CQGetFriendList()
}, },
"get_group_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_group_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupList() return bot.CQGetGroupList(p.Get("no_cache").Bool())
}, },
"get_group_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_group_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupInfo(p.Get("group_id").Int()) return bot.CQGetGroupInfo(p.Get("group_id").Int())
@ -327,22 +329,29 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
) )
}, },
"send_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "send_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
autoEscape := global.EnsureBool(p.Get("auto_escape"), false)
if p.Get("message_type").Str == "private" {
return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), autoEscape)
}
if p.Get("message_type").Str == "group" {
return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), autoEscape)
}
if p.Get("group_id").Int() != 0 { if p.Get("group_id").Int() != 0 {
return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message")) return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), autoEscape)
} }
if p.Get("user_id").Int() != 0 { if p.Get("user_id").Int() != 0 {
return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message")) return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), autoEscape)
} }
return coolq.MSG{} return coolq.MSG{}
}, },
"send_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "send_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message")) return bot.CQSendGroupMessage(p.Get("group_id").Int(), p.Get("message"), global.EnsureBool(p.Get("auto_escape"), false))
}, },
"send_group_forward_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "send_group_forward_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSendGroupForwardMessage(p.Get("group_id").Int(), p.Get("messages")) return bot.CQSendGroupForwardMessage(p.Get("group_id").Int(), p.Get("messages"))
}, },
"send_private_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "send_private_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message")) return bot.CQSendPrivateMessage(p.Get("user_id").Int(), p.Get("message"), global.EnsureBool(p.Get("auto_escape"), false))
}, },
"delete_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "delete_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQDeleteMessage(int32(p.Get("message_id").Int())) return bot.CQDeleteMessage(int32(p.Get("message_id").Int()))
@ -405,6 +414,9 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_group_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupMessage(int32(p.Get("message_id").Int())) return bot.CQGetGroupMessage(int32(p.Get("message_id").Int()))
}, },
"get_group_honor_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupHonorInfo(p.Get("group_id").Int(), p.Get("type").Str)
},
"can_send_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "can_send_image": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQCanSendImage() return bot.CQCanSendImage()
}, },