1
0
mirror of https://github.com/Mrs4s/go-cqhttp.git synced 2025-06-30 20:03:24 +00:00

Compare commits

...

120 Commits

Author SHA1 Message Date
ff75b0a2a9 update doc. 2021-02-06 21:55:32 +08:00
4a13f35003 update MiraiGo. 2021-02-06 21:46:28 +08:00
8d0a0b7f4d fix typo. 2021-02-06 19:11:24 +08:00
4c570bdfe6 feature check_url_safely. 2021-02-06 06:40:39 +08:00
d4e8a3df4f update MiraiGo. 2021-02-06 06:34:29 +08:00
307a09dd11 feature upload_group_file. 2021-02-05 22:24:49 +08:00
4445af6ff2 update MiraiGo. 2021-02-05 21:46:25 +08:00
dbd0f85025 Merge pull request #611 from wdvxdr1123/patch/essence
feat group essence msg operate
2021-02-03 01:26:44 +08:00
0bacbde4d7 Merge branch 'dev' into patch/essence 2021-02-03 01:26:27 +08:00
c92dd39191 Merge pull request #610 from wdvxdr1123/group_info
get group info by search
2021-02-03 01:23:27 +08:00
7da1918c0c update docs 2021-02-02 23:59:00 +08:00
89d466d3e1 feat group essence msg operate 2021-02-02 23:36:05 +08:00
5b57aeb1d1 get group info by search 2021-02-02 22:12:47 +08:00
08567ec240 fix message history media. 2021-02-02 07:53:41 +08:00
08dea6dcf1 update MiraiGo. fix #604 2021-02-02 07:51:39 +08:00
252293afb9 update MiraiGo. fix #605 2021-02-02 07:22:38 +08:00
ca0b931fa4 update MiraiGo. 2021-02-02 07:13:42 +08:00
01dca0305c Merge branch 'master' into dev 2021-02-02 07:12:59 +08:00
6053918ee3 Merge branch 'dev' of https://github.com/Mrs4s/go-cqhttp into dev 2021-02-02 07:12:32 +08:00
e7547ed257 Merge pull request #601 from sam01101/patch-2
isFastStart setting appiled to config.hjson gen
2021-01-29 21:28:48 +08:00
96a036201d Move isFastStart outside 2021-01-29 15:16:31 +08:00
ccee8ac77a isFastStart setting appiled to config.hjson gen 2021-01-29 15:13:39 +08:00
ae806c53bb fix typo. 2021-01-28 20:54:38 +08:00
a9619b25bd Merge pull request #598 from wdvxdr1123/music_share
fix null json
2021-01-27 17:58:21 +08:00
69cda4e029 fix null json 2021-01-27 16:08:59 +08:00
dc18944a98 update MiraiGo. 2021-01-26 04:52:11 +08:00
717f816294 fix bugs. 2021-01-26 04:51:39 +08:00
430e734ff6 Merge pull request #591 from wdvxdr1123/music_share
simplify music share
2021-01-26 03:48:57 +08:00
21531fd70a Merge branch 'master' into dev 2021-01-26 03:47:42 +08:00
2214570c4f Merge pull request #590 from zkonge/master
增强保存密码安全性
2021-01-26 03:46:14 +08:00
18a839b699 update MiraiGo 2021-01-25 17:41:15 +08:00
23b90e288c better error 2021-01-25 17:34:23 +08:00
22c9a6728b Make golint happy 2021-01-25 17:05:51 +08:00
81ff71ee3b Merge pull request #580 from xuthus5/master
更加简明的logrus hook
2021-01-25 16:45:27 +08:00
bf65b576f3 update docs 2021-01-25 15:27:45 +08:00
817bb0e493 simplify music share
support Kugou,Kuwo custom
2021-01-25 15:06:30 +08:00
d2529a04c9 Enhance password security
No plain password left in memory, except first run
Attacker can't verify password correctness offline
2021-01-25 05:56:27 +08:00
13df58331c fix 2021-01-24 17:40:38 +08:00
61c9939f03 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	main.go
2021-01-24 17:29:26 +08:00
a2a0b4c86f update doc. 2021-01-24 15:15:09 +08:00
8532c4ecbe update MiraiGo. 2021-01-24 15:04:09 +08:00
5aaade7835 Merge pull request #573 from Diving-Fish/master
fix: get gender of group member
2021-01-24 14:50:13 +08:00
3c309a8e06 update MiraiGo. fix #577 2021-01-24 14:38:21 +08:00
7c794d113f update MiraiGo. fix #586 2021-01-24 02:33:39 +08:00
61625f2e32 feature client status updated event. 2021-01-23 00:19:48 +08:00
8a81b67511 doc update. 2021-01-23 00:11:43 +08:00
164feca7c7 feature get_online_clients. 2021-01-23 00:04:57 +08:00
2ac94a7e95 update MiraiGo. 2021-01-20 23:33:25 +08:00
219005d384 fix #540. 2021-01-20 01:30:18 +08:00
e19e797aac log support print report_line 2021-01-19 22:33:05 +08:00
aa54ed5610 reset default config log_level 2021-01-19 22:17:12 +08:00
360f7188e1 local log hook 2021-01-19 22:07:28 +08:00
0002185e45 feature get_group_msg_history. 2021-01-19 12:07:06 +08:00
5480deff4b fix ua. 2021-01-19 01:59:41 +08:00
9a81b02791 Merge pull request #576 from sam01101/master
添加非破坏性(Unbreakable)功能
2021-01-19 01:54:47 +08:00
5280c68bfc Merge pull request #575 from sam01101/patch-1
Make send_group_forward_msg can use on get_msg
2021-01-19 01:31:00 +08:00
0bd5afbf21 fix #562. 2021-01-19 01:23:09 +08:00
297284f957 update MiraiGo. 2021-01-19 01:18:28 +08:00
eb618393e5 update string message 2021-01-18 23:22:46 +08:00
bdc2b03690 Added unbreakable feature
- Reply message customizable
- Forward message able to customize time

Signed-off-by: Sam <lau.sam745033858@gmail.com>
2021-01-18 23:01:54 +08:00
63d26dfdb5 Merge pull request #572 from Ink-33/dev
fix golint warning
2021-01-18 22:27:01 +08:00
7e49ed4869 Make send_group_forward_msg can use on get_msg
理论上这个是正常支持的, Fix https://github.com/Mrs4s/go-cqhttp/issues/542
2021-01-18 21:29:25 +08:00
19b6106c93 Merge pull request #1 from sam01101/dev
Dev
2021-01-18 18:52:50 +08:00
77a4f9a64f Merge branch 'dev' into dev 2021-01-17 17:40:45 +08:00
c69129f80b Merge pull request #571 from ishkong/patch-4
Try force transcoding
2021-01-17 16:59:28 +08:00
61f4e7826a fix golint 2021-01-17 16:39:01 +08:00
ddd303375d fix get gender of group member 2021-01-17 13:14:35 +08:00
46cae86442 fix get gender of group member 2021-01-17 12:48:54 +08:00
d16fd6f34d fix: golangci-lint errcheck 2021-01-17 02:52:43 +08:00
1001cefb6f fix: golangci-lint staticcheck 2021-01-17 02:46:25 +08:00
1d9f719d13 fix: golangci-lint ineffassign 2021-01-17 02:16:29 +08:00
468befb0fd update import 2021-01-17 01:48:48 +08:00
9f99b1571d fix: golangci-lint gosimple 2021-01-17 01:47:45 +08:00
77445fe96e update go.mod 2021-01-17 01:06:49 +08:00
bbd85eb219 Try force transcoding
Attempt to brute force ffmpeg transcoding after failure
2021-01-17 00:28:31 +08:00
2156b6083b update MiraiGo. 2021-01-16 06:00:33 +08:00
b47c2aeac6 Merge branch 'master' into dev 2021-01-16 05:59:31 +08:00
9beae584de Merge pull request #560 from wdvxdr1123/patch-2
fix #557
2021-01-14 02:31:34 +08:00
59cd986050 fix #561 2021-01-13 22:56:28 +08:00
7a9a021c54 fix codec path 2021-01-13 22:35:44 +08:00
de4de5052d fix #557 & remove some goto 2021-01-13 11:43:39 +08:00
b075280d2b fix build 2021-01-13 11:22:59 +08:00
4cb3d8f33e Merge pull request #559 from Ink-33/master
修复codec编译问题
2021-01-13 08:06:02 +08:00
0cd06daf08 Update github action 2021-01-13 04:05:50 +08:00
7ac31a8b76 Update github action 2021-01-13 04:00:01 +08:00
c9c6e48b8c add codec/codec_unsupported.go 2021-01-13 03:55:28 +08:00
0c3ba714f7 fix #558 2021-01-13 03:52:49 +08:00
1f7dcc4e91 fix forward img & video upload. 2021-01-13 02:58:54 +08:00
ef713bc59c fix show pic error. 2021-01-13 02:58:54 +08:00
85624d1a27 fix forward img & video upload. 2021-01-11 16:58:07 +08:00
b2d8a61a99 fix show pic error. 2021-01-11 14:36:29 +08:00
38a2c3a945 Merge pull request #550 from Ink-33/patch-1
Fix misspell
2021-01-11 00:53:32 +08:00
eaec0e545f fix thumb. 2021-01-11 00:50:50 +08:00
50f0f572bc fix multi-threading upload. 2021-01-11 00:45:49 +08:00
36532a8782 fix message. 2021-01-11 00:33:41 +08:00
620ecdbdbb update MiraiGo. 2021-01-11 00:22:59 +08:00
7737fc8457 update MiraiGo & qid support. 2021-01-10 21:30:35 +08:00
e03a989083 Fix misspell 2021-01-09 20:38:33 +08:00
56da9adeab Merge pull request #549 from wdvxdr1123/patch-2
fix file close
2021-01-09 17:56:11 +08:00
249e528457 support video cache file 2021-01-09 14:26:21 +08:00
1eeae13b4a fix file close 2021-01-09 12:23:24 +08:00
fe83ce716e update MiraiGo & fix user-agent & fix file lock. 2021-01-09 04:25:18 +08:00
a5a842a337 update MiraiGo. 2021-01-09 03:12:16 +08:00
be47ff3437 Merge pull request #548 from wdvxdr1123/patch-2
private video
2021-01-09 00:27:35 +08:00
392904f144 fix typo 2021-01-08 22:37:51 +08:00
a4c3ef8923 fix private video 2021-01-08 22:34:57 +08:00
816c142822 Merge pull request #547 from wdvxdr1123/patch-2
fix video
2021-01-08 22:18:02 +08:00
2e2002668b fix video 2021-01-08 22:14:23 +08:00
c58066e287 Merge pull request #544 from wdvxdr1123/patch-2
cq video
2021-01-08 17:38:30 +08:00
87ada9c57e update docs 2021-01-08 16:06:53 +08:00
b717e23626 feat CQ:video 2021-01-08 15:40:32 +08:00
f501b31152 encode mp4 2021-01-08 14:12:25 +08:00
2d020bc7f7 drop go-silk 2021-01-08 12:14:09 +08:00
1843bd6a4e fix reconnect loop on account banned. 2021-01-06 22:08:02 +08:00
938ebf630f update doc. 2021-01-06 21:43:55 +08:00
2bbf969cfa feature download_file. 2021-01-06 21:27:07 +08:00
416a3460ab Merge pull request #541 from wdvxdr1123/patch-2
fix reconnect
2021-01-06 21:00:21 +08:00
f98c334089 fix reconnect 2021-01-06 12:46:10 +08:00
0e5276ea4d fix ptt. 2021-01-06 02:38:21 +08:00
6113eb9200 update MiraiGo. 2021-01-06 02:36:56 +08:00
25 changed files with 1865 additions and 708 deletions

View File

@ -16,12 +16,16 @@ jobs:
matrix: matrix:
# build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64 # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64
goos: [linux, windows, darwin] goos: [linux, windows, darwin]
goarch: ["386", amd64, arm] goarch: ["386", amd64, arm, arm64]
exclude: exclude:
- goos: darwin - goos: darwin
goarch: arm goarch: arm
- goos: darwin
goarch: arm64
- goos: darwin - goos: darwin
goarch: "386" goarch: "386"
- goos: windows
goarch: arm64
fail-fast: true fail-fast: true
steps: steps:

View File

@ -10,12 +10,16 @@ jobs:
matrix: matrix:
# build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64 # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64
goos: [linux, windows, darwin] goos: [linux, windows, darwin]
goarch: ["386", amd64, arm] goarch: ["386", amd64, arm, arm64]
exclude: exclude:
- goos: darwin - goos: darwin
goarch: arm goarch: arm
- goos: darwin
goarch: arm64
- goos: darwin - goos: darwin
goarch: "386" goarch: "386"
- goos: windows
goarch: arm64
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
vendor/ vendor/
.idea .idea
config.hjson
device.json
codec/
data/
logs/

View File

@ -1,9 +1,13 @@
package coolq package coolq
import ( import (
"crypto/md5"
"encoding/hex"
"io/ioutil" "io/ioutil"
"math"
"os" "os"
"path" "path"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -58,7 +62,22 @@ func (bot *CQBot) CQGetGroupList(noCache bool) MSG {
func (bot *CQBot) CQGetGroupInfo(groupId int64, noCache bool) MSG { func (bot *CQBot) CQGetGroupInfo(groupId int64, noCache bool) MSG {
group := bot.Client.FindGroup(groupId) group := bot.Client.FindGroup(groupId)
if group == nil { if group == nil {
return Failed(100, "GROUP_NOT_FOUND", "群聊不存在") gid := strconv.FormatInt(groupId, 10)
info, err := bot.Client.SearchGroupByKeyword(gid)
if err != nil {
return Failed(100, "GROUP_SEARCH_ERROR", "群聊搜索失败")
}
for _, g := range info {
if g.Code == groupId {
return OK(MSG{
"group_id": g.Code,
"group_name": g.Name,
"max_member_count": 0,
"member_count": 0,
})
}
}
return Failed(100, "GROUP_NOT_FOUND", "群聊不存在失败")
} }
if noCache { if noCache {
var err error var err error
@ -162,6 +181,26 @@ func (bot *CQBot) CQGetGroupFileUrl(groupId int64, fileId string, busId int32) M
}) })
} }
func (bot *CQBot) CQUploadGroupFile(groupId int64, file, name, folder string) MSG {
if !global.PathExists(file) {
log.Errorf("上传群文件 %v 失败: 文件不存在", file)
return Failed(100, "FILE_NOT_FOUND", "文件不存在")
}
fs, err := bot.Client.GetGroupFileSystem(groupId)
if err != nil {
log.Errorf("获取群 %v 文件系统信息失败: %v", groupId, err)
return Failed(100, "FILE_SYSTEM_API_ERROR", err.Error())
}
if folder == "" {
folder = "/"
}
if err = fs.UploadFile(file, name, folder); err != nil {
log.Errorf("上传群 %v 文件 %v 失败: %v", groupId, file, err)
return Failed(100, "FILE_SYSTEM_UPLOAD_API_ERROR", err.Error())
}
return OK(nil)
}
func (bot *CQBot) CQGetWordSlices(content string) MSG { func (bot *CQBot) CQGetWordSlices(content string) MSG {
slices, err := bot.Client.GetWordSegmentation(content) slices, err := bot.Client.GetWordSegmentation(content)
if err != nil { if err != nil {
@ -257,10 +296,11 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
SenderId: sender.Uin, SenderId: sender.Uin,
SenderName: (&sender).DisplayName(), SenderName: (&sender).DisplayName(),
Time: func() int32 { Time: func() int32 {
if hasCustom { msgTime := m["time"].(int32)
if hasCustom && msgTime == 0 {
return int32(ts.Unix()) return int32(ts.Unix())
} }
return m["time"].(int32) return msgTime
}(), }(),
Message: bot.ConvertStringMessage(m["message"].(string), true), Message: bot.ConvertStringMessage(m["message"].(string), true),
}) })
@ -270,6 +310,10 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
return return
} }
uin, _ := strconv.ParseInt(e.Get("data.uin").Str, 10, 64) uin, _ := strconv.ParseInt(e.Get("data.uin").Str, 10, 64)
msgTime, err := strconv.ParseInt(e.Get("data.time").Str, 10, 64)
if err != nil {
msgTime = ts.Unix()
}
name := e.Get("data.name").Str name := e.Get("data.name").Str
c := e.Get("data.content") c := e.Get("data.content")
if c.IsArray() { if c.IsArray() {
@ -289,7 +333,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
nodes = append(nodes, &message.ForwardNode{ nodes = append(nodes, &message.ForwardNode{
SenderId: uin, SenderId: uin,
SenderName: name, SenderName: name,
Time: int32(ts.Unix()), Time: int32(msgTime),
Message: []message.IMessageElement{bot.Client.UploadGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: taowa})}, Message: []message.IMessageElement{bot.Client.UploadGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: taowa})},
}) })
return return
@ -300,7 +344,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
var newElem []message.IMessageElement var newElem []message.IMessageElement
for _, elem := range content { for _, elem := range content {
if img, ok := elem.(*LocalImageElement); ok { if img, ok := elem.(*LocalImageElement); ok {
gm, err := bot.Client.UploadGroupImage(groupId, img.Stream) gm, err := bot.UploadLocalImageAsGroup(groupId, img)
if err != nil { if err != nil {
log.Warnf("警告:群 %v 图片上传失败: %v", groupId, err) log.Warnf("警告:群 %v 图片上传失败: %v", groupId, err)
continue continue
@ -308,12 +352,21 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
newElem = append(newElem, gm) newElem = append(newElem, gm)
continue continue
} }
if video, ok := elem.(*LocalVideoElement); ok {
gm, err := bot.UploadLocalVideo(groupId, video)
if err != nil {
log.Warnf("警告:群 %v 视频上传失败: %v", groupId, err)
continue
}
newElem = append(newElem, gm)
continue
}
newElem = append(newElem, elem) newElem = append(newElem, elem)
} }
nodes = append(nodes, &message.ForwardNode{ nodes = append(nodes, &message.ForwardNode{
SenderId: uin, SenderId: uin,
SenderName: name, SenderName: name,
Time: int32(ts.Unix()), Time: int32(msgTime),
Message: newElem, Message: newElem,
}) })
return return
@ -331,7 +384,7 @@ func (bot *CQBot) CQSendGroupForwardMessage(groupId int64, m gjson.Result) MSG {
if len(sendNodes) > 0 { if len(sendNodes) > 0 {
gm := bot.Client.SendGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: sendNodes}) gm := bot.Client.SendGroupForwardMessage(groupId, &message.ForwardMessage{Nodes: sendNodes})
return OK(MSG{ return OK(MSG{
"message_id": ToGlobalId(groupId, gm.Id), "message_id": bot.InsertGroupMessage(gm),
}) })
} }
return Failed(100) return Failed(100)
@ -342,7 +395,7 @@ func (bot *CQBot) CQSendPrivateMessage(userId int64, i interface{}, autoEscape b
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 {
elem := bot.ConvertObjectMessage(m, true) elem := bot.ConvertObjectMessage(m, 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, "SEND_MSG_API_ERROR", "请参考输出") return Failed(100, "SEND_MSG_API_ERROR", "请参考输出")
@ -646,11 +699,15 @@ func (bot *CQBot) CQGetStrangerInfo(userId int64) MSG {
return OK(MSG{ return OK(MSG{
"user_id": info.Uin, "user_id": info.Uin,
"nickname": info.Nickname, "nickname": info.Nickname,
"qid": info.Qid,
"sex": func() string { "sex": func() string {
if info.Sex == 1 { if info.Sex == 1 {
return "female" return "female"
} else if info.Sex == 0 {
return "male"
} }
return "male" // unknown = 0x2
return "unknown"
}(), }(),
"age": info.Age, "age": info.Age,
"level": info.Level, "level": info.Level,
@ -717,10 +774,10 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG {
} }
func (bot *CQBot) CQGetImage(file string) MSG { func (bot *CQBot) CQGetImage(file string) MSG {
if !global.PathExists(path.Join(global.IMAGE_PATH, file)) { if !global.PathExists(path.Join(global.ImagePath, file)) {
return Failed(100) return Failed(100)
} }
if b, err := ioutil.ReadFile(path.Join(global.IMAGE_PATH, file)); err == nil { if b, err := ioutil.ReadFile(path.Join(global.ImagePath, file)); err == nil {
r := binary.NewReader(b) r := binary.NewReader(b)
r.ReadBytes(16) r.ReadBytes(16)
msg := MSG{ msg := MSG{
@ -728,7 +785,7 @@ func (bot *CQBot) CQGetImage(file string) MSG {
"filename": r.ReadString(), "filename": r.ReadString(),
"url": r.ReadString(), "url": r.ReadString(),
} }
local := path.Join(global.CACHE_PATH, file+"."+path.Ext(msg["filename"].(string))) local := path.Join(global.CachePath, file+"."+path.Ext(msg["filename"].(string)))
if !global.PathExists(local) { if !global.PathExists(local) {
if data, err := global.GetBytes(msg["url"].(string)); err == nil { if data, err := global.GetBytes(msg["url"].(string)); err == nil {
_ = ioutil.WriteFile(local, data, 0644) _ = ioutil.WriteFile(local, data, 0644)
@ -741,6 +798,25 @@ func (bot *CQBot) CQGetImage(file string) MSG {
} }
} }
func (bot *CQBot) CQDownloadFile(url string, headers map[string]string, threadCount int) MSG {
hash := md5.Sum([]byte(url))
file := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
if global.PathExists(file) {
if err := os.Remove(file); err != nil {
log.Warnf("删除缓存文件 %v 时出现错误: %v", file, err)
return Failed(100, "DELETE_FILE_ERROR", err.Error())
}
}
if err := global.DownloadFileMultiThreading(url, file, 0, threadCount, headers); err != nil {
log.Warnf("下载链接 %v 时出现错误: %v", url, err)
return Failed(100, "DOWNLOAD_FILE_ERROR", err.Error())
}
abs, _ := filepath.Abs(file)
return OK(MSG{
"file": abs,
})
}
func (bot *CQBot) CQGetForwardMessage(resId string) MSG { func (bot *CQBot) CQGetForwardMessage(resId string) MSG {
m := bot.Client.GetForwardMessage(resId) m := bot.Client.GetForwardMessage(resId)
if m == nil { if m == nil {
@ -772,10 +848,11 @@ func (bot *CQBot) CQGetMessage(messageId int32) MSG {
gid, isGroup := msg["group"] gid, isGroup := msg["group"]
raw := msg["message"].(string) raw := msg["message"].(string)
return OK(MSG{ return OK(MSG{
"message_id": messageId, "message_id": messageId,
"real_id": msg["message-id"], "real_id": msg["message-id"],
"group": isGroup, "message_seq": msg["message-id"],
"group_id": gid, "group": isGroup,
"group_id": gid,
"message_type": func() string { "message_type": func() string {
if isGroup { if isGroup {
return "group" return "group"
@ -806,6 +883,58 @@ func (bot *CQBot) CQGetGroupSystemMessages() MSG {
return OK(msg) return OK(msg)
} }
func (bot *CQBot) CQGetGroupMessageHistory(groupId int64, seq int64) MSG {
if g := bot.Client.FindGroup(groupId); g == nil {
return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
if seq == 0 {
g, err := bot.Client.GetGroupInfo(groupId)
if err != nil {
return Failed(100, "GROUP_INFO_API_ERROR", err.Error())
}
seq = g.LastMsgSeq
}
msg, err := bot.Client.GetGroupMessages(groupId, int64(math.Max(float64(seq-19), 1)), seq)
if err != nil {
log.Warnf("获取群历史消息失败: %v", err)
return Failed(100, "MESSAGES_API_ERROR", err.Error())
}
var ms []MSG
for _, m := range msg {
id := m.Id
bot.checkMedia(m.Elements)
if bot.db != nil {
id = bot.InsertGroupMessage(m)
}
t := bot.formatGroupMessage(m)
t["message_id"] = id
ms = append(ms, t)
}
return OK(MSG{
"messages": ms,
})
}
func (bot *CQBot) CQGetOnlineClients(noCache bool) MSG {
if noCache {
if err := bot.Client.RefreshStatus(); err != nil {
log.Warnf("刷新客户端状态时出现问题 %v", err)
return Failed(100, "REFRESH_STATUS_ERROR", err.Error())
}
}
var d []MSG
for _, oc := range bot.Client.OnlineClients {
d = append(d, MSG{
"app_id": oc.AppId,
"device_name": oc.DeviceName,
"device_kind": oc.DeviceKind,
})
}
return OK(MSG{
"clients": d,
})
}
func (bot *CQBot) CQCanSendImage() MSG { func (bot *CQBot) CQCanSendImage() MSG {
return OK(MSG{"yes": true}) return OK(MSG{"yes": true})
} }
@ -815,7 +944,7 @@ func (bot *CQBot) CQCanSendRecord() MSG {
} }
func (bot *CQBot) CQOcrImage(imageId string) MSG { func (bot *CQBot) CQOcrImage(imageId string) MSG {
img, err := bot.makeImageElem(map[string]string{"file": imageId}, true) img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageId}, false, true)
if err != nil { if err != nil {
log.Warnf("load image error: %v", err) log.Warnf("load image error: %v", err)
return Failed(100, "LOAD_FILE_ERROR", err.Error()) return Failed(100, "LOAD_FILE_ERROR", err.Error())
@ -835,7 +964,7 @@ func (bot *CQBot) CQReloadEventFilter() MSG {
func (bot *CQBot) CQSetGroupPortrait(groupId int64, file, cache string) MSG { func (bot *CQBot) CQSetGroupPortrait(groupId int64, file, cache string) MSG {
if g := bot.Client.FindGroup(groupId); g != nil { if g := bot.Client.FindGroup(groupId); g != nil {
img, err := global.FindFile(file, cache, global.IMAGE_PATH) img, err := global.FindFile(file, cache, global.ImagePath)
if err != nil { if err != nil {
log.Warnf("set group portrait error: %v", err) log.Warnf("set group portrait error: %v", err)
return Failed(100, "LOAD_FILE_ERROR", err.Error()) return Failed(100, "LOAD_FILE_ERROR", err.Error())
@ -879,6 +1008,74 @@ func (bot *CQBot) CQGetStatus() MSG {
}) })
} }
// CQSetEssenceMessage 设置精华消息
func (bot *CQBot) CQSetEssenceMessage(messageID int32) MSG {
msg := bot.GetMessage(messageID)
if msg == nil {
return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
}
if _, ok := msg["group"]; ok {
if err := bot.Client.SetEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
log.Warnf("设置精华消息 %v 失败: %v", messageID, err)
return Failed(100, "SET_ESSENCE_MSG_ERROR", err.Error())
}
} else {
log.Warnf("设置精华消息 %v 失败: 非群聊", messageID)
return Failed(100, "SET_ESSENCE_MSG_ERROR", "非群聊")
}
return OK(nil)
}
// CQDeleteEssenceMessage 移出精华消息
func (bot *CQBot) CQDeleteEssenceMessage(messageID int32) MSG {
msg := bot.GetMessage(messageID)
if msg == nil {
return Failed(100, "MESSAGE_NOT_FOUND", "消息不存在")
}
if _, ok := msg["group"]; ok {
if err := bot.Client.DeleteEssenceMessage(msg["group"].(int64), msg["message-id"].(int32), msg["internal-id"].(int32)); err != nil {
log.Warnf("移出精华消息 %v 失败: %v", messageID, err)
return Failed(100, "DEL_ESSENCE_MSG_ERROR", err.Error())
}
} else {
log.Warnf("移出精华消息 %v 失败: 非群聊", messageID)
return Failed(100, "DEL_ESSENCE_MSG_ERROR", "非群聊")
}
return OK(nil)
}
// CQGetEssenceMessageList 获取精华消息列表
func (bot *CQBot) CQGetEssenceMessageList(groupCode int64) MSG {
g := bot.Client.FindGroup(groupCode)
if g == nil {
return Failed(100, "GROUP_NOT_FOUND", "群聊不存在")
}
msgList, err := bot.Client.GetGroupEssenceMsgList(groupCode)
if err != nil {
return Failed(100, "GET_ESSENCE_LIST_FOUND", err.Error())
}
list := make([]MSG, 0)
for _, m := range msgList {
var msg = MSG{
"sender_nick": m.SenderNick,
"sender_time": m.SenderTime,
"operator_time": m.AddDigestTime,
"operator_nick": m.AddDigestNick,
}
msg["sender_id"], _ = strconv.ParseUint(m.SenderUin, 10, 64)
msg["operator_id"], _ = strconv.ParseUint(m.AddDigestUin, 10, 64)
msg["message_id"] = ToGlobalId(groupCode, int32(m.MessageID))
list = append(list, msg)
}
return OK(list)
}
func (bot *CQBot) CQCheckUrlSafely(url string) MSG {
return OK(MSG{
"level": bot.Client.CheckUrlSafely(url),
})
}
func (bot *CQBot) CQGetVersionInfo() MSG { func (bot *CQBot) CQGetVersionInfo() MSG {
wd, _ := os.Getwd() wd, _ := os.Getwd()
return OK(MSG{ return OK(MSG{
@ -926,11 +1123,19 @@ func Failed(code int, msg ...string) MSG {
func convertGroupMemberInfo(groupId int64, m *client.GroupMemberInfo) MSG { func convertGroupMemberInfo(groupId int64, m *client.GroupMemberInfo) MSG {
return MSG{ return MSG{
"group_id": groupId, "group_id": groupId,
"user_id": m.Uin, "user_id": m.Uin,
"nickname": m.Nickname, "nickname": m.Nickname,
"card": m.CardName, "card": m.CardName,
"sex": "unknown", "sex": func() string {
if m.Gender == 1 {
return "female"
} else if m.Gender == 0 {
return "male"
}
// unknown = 0xff
return "unknown"
}(),
"age": 0, "age": 0,
"area": "", "area": "",
"join_time": m.JoinTime, "join_time": m.JoinTime,

View File

@ -3,15 +3,18 @@ package coolq
import ( import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"encoding/hex"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io/ioutil" "io"
"os" "os"
"path" "path"
"runtime/debug" "runtime/debug"
"sync" "sync"
"time" "time"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
@ -39,7 +42,7 @@ type MSG map[string]interface{}
var ForceFragmented = false var ForceFragmented = false
func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot { func NewQQBot(cli *client.QQClient, conf *global.JSONConfig) *CQBot {
bot := &CQBot{ bot := &CQBot{
Client: cli, Client: cli,
} }
@ -74,6 +77,8 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot.Client.OnNewFriendAdded(bot.friendAddedEvent) 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)
bot.Client.OnOtherClientStatusChanged(bot.otherClientStatusChangedEvent)
bot.Client.OnGroupDigest(bot.groupEssenceMsg)
go func() { go func() {
i := conf.HeartbeatInterval i := conf.HeartbeatInterval
if i < 0 { if i < 0 {
@ -126,6 +131,22 @@ func (bot *CQBot) UploadLocalImageAsGroup(groupCode int64, img *LocalImageElemen
return bot.Client.UploadGroupImageByFile(groupCode, img.File) return bot.Client.UploadGroupImageByFile(groupCode, img.File)
} }
func (bot *CQBot) UploadLocalVideo(target int64, v *LocalVideoElement) (*message.ShortVideoElement, error) {
if v.File != "" {
video, err := os.Open(v.File)
if err != nil {
return nil, err
}
defer video.Close()
hash, _ := utils.ComputeMd5AndLength(io.MultiReader(video, v.thumb))
cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
_, _ = video.Seek(0, io.SeekStart)
_, _ = v.thumb.Seek(0, io.SeekStart)
return bot.Client.UploadGroupShortVideo(target, video, v.thumb, cacheFile)
}
return &v.ShortVideoElement, nil
}
func (bot *CQBot) UploadLocalImageAsPrivate(userId int64, img *LocalImageElement) (*message.FriendImageElement, error) { func (bot *CQBot) UploadLocalImageAsPrivate(userId int64, img *LocalImageElement) (*message.FriendImageElement, error) {
if img.Stream != nil { if img.Stream != nil {
return bot.Client.UploadPrivateImage(userId, img.Stream) return bot.Client.UploadPrivateImage(userId, img.Stream)
@ -135,6 +156,7 @@ func (bot *CQBot) UploadLocalImageAsPrivate(userId int64, img *LocalImageElement
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close()
return bot.Client.UploadPrivateImage(userId, f) return bot.Client.UploadPrivateImage(userId, f)
} }
@ -150,8 +172,8 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
newElem = append(newElem, gm) newElem = append(newElem, gm)
continue continue
} }
if i, ok := elem.(*LocalVoiceElement); ok { if i, ok := elem.(*message.VoiceElement); ok {
gv, err := bot.Client.UploadGroupPtt(groupId, i.Stream) gv, err := bot.Client.UploadGroupPtt(groupId, bytes.NewReader(i.Data))
if err != nil { if err != nil {
log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err) log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err)
continue continue
@ -159,6 +181,15 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
newElem = append(newElem, gv) newElem = append(newElem, gv)
continue continue
} }
if i, ok := elem.(*LocalVideoElement); ok {
gv, err := bot.UploadLocalVideo(groupId, i)
if err != nil {
log.Warnf("警告: 群 %v 消息短视频上传失败: %v", groupId, err)
continue
}
newElem = append(newElem, gv)
continue
}
if i, ok := elem.(*PokeElement); ok { if i, ok := elem.(*PokeElement); ok {
if group := bot.Client.FindGroup(groupId); group != nil { if group := bot.Client.FindGroup(groupId); group != nil {
if mem := group.FindMember(i.Target); mem != nil { if mem := group.FindMember(i.Target); mem != nil {
@ -171,61 +202,8 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
bot.Client.SendGroupGift(uint64(groupId), uint64(i.Target), i.GiftId) bot.Client.SendGroupGift(uint64(groupId), uint64(i.Target), i.GiftId)
return 0 return 0
} }
if i, ok := elem.(*QQMusicElement); ok { if i, ok := elem.(*message.MusicShareElement); ok {
var msgStyle uint32 = 4 ret, err := bot.Client.SendGroupMusicShare(groupId, i)
if i.MusicUrl == "" {
msgStyle = 0 // fix vip song
}
ret, err := bot.Client.SendGroupRichMessage(groupId, 100497308, 1, msgStyle, client.RichClientInfo{
Platform: 1,
SdkVersion: "0.0.0",
PackageName: "com.tencent.qqmusic",
Signature: "cbd27cd7c861227d013a25b2d10f0799",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
if err != nil {
log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err)
return -1
}
return bot.InsertGroupMessage(ret)
}
if i, ok := elem.(*CloudMusicElement); ok {
ret, err := bot.Client.SendGroupRichMessage(groupId, 100495085, 1, 4, client.RichClientInfo{
Platform: 1,
SdkVersion: "0.0.0",
PackageName: "com.netease.cloudmusic",
Signature: "da6b069da1e2982db3e386233f68d76d",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
if err != nil {
log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err)
return -1
}
return bot.InsertGroupMessage(ret)
}
if i, ok := elem.(*MiguMusicElement); ok {
ret, err := bot.Client.SendGroupRichMessage(groupId, 1101053067, 1, 4, client.RichClientInfo{
Platform: 1,
SdkVersion: "0.0.0",
PackageName: "cmccwm.mobilemusic",
Signature: "6cdc72a439cef99a3418d2a78aa28c73",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
if err != nil { if err != nil {
log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err) log.Warnf("警告: 群 %v 富文本消息发送失败: %v", groupId, err)
return -1 return -1
@ -243,13 +221,7 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
ret := bot.Client.SendGroupMessage(groupId, m, ForceFragmented) ret := bot.Client.SendGroupMessage(groupId, m, ForceFragmented)
if ret == nil || ret.Id == -1 { if ret == nil || ret.Id == -1 {
log.Warnf("群消息发送失败: 账号可能被风控.") log.Warnf("群消息发送失败: 账号可能被风控.")
if !ForceFragmented { return -1
log.Warnf("将尝试分片发送...")
ret = bot.Client.SendGroupMessage(groupId, m, true)
}
if ret == nil || ret.Id == -1 {
return -1
}
} }
return bot.InsertGroupMessage(ret) return bot.InsertGroupMessage(ret)
} }
@ -270,67 +242,26 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
bot.Client.SendFriendPoke(i.Target) bot.Client.SendFriendPoke(i.Target)
return 0 return 0
} }
if i, ok := elem.(*LocalVoiceElement); ok { if i, ok := elem.(*message.VoiceElement); ok {
data, err := ioutil.ReadAll(i.Stream) fv, err := bot.Client.UploadPrivatePtt(target, i.Data)
if err != nil { if err != nil {
log.Warnf("警告: 好友 %v 消息语音读取失败: %v", target, err) log.Warnf("警告: 私聊 %v 消息语音上传失败: %v", target, err)
continue
}
fv, err := bot.Client.UploadPrivatePtt(target, data)
if err != nil {
log.Warnf("警告: 好友 %v 消息语音上传失败: %v", target, err)
continue continue
} }
newElem = append(newElem, fv) newElem = append(newElem, fv)
continue continue
} }
if i, ok := elem.(*QQMusicElement); ok { if i, ok := elem.(*LocalVideoElement); ok {
var msgStyle uint32 = 4 gv, err := bot.UploadLocalVideo(target, i)
if i.MusicUrl == "" { if err != nil {
msgStyle = 0 // fix vip song log.Warnf("警告: 私聊 %v 消息短视频上传失败: %v", target, err)
continue
} }
bot.Client.SendFriendRichMessage(target, 100497308, 1, msgStyle, client.RichClientInfo{ newElem = append(newElem, gv)
Platform: 1, continue
SdkVersion: "0.0.0",
PackageName: "com.tencent.qqmusic",
Signature: "cbd27cd7c861227d013a25b2d10f0799",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
return 0
} }
if i, ok := elem.(*CloudMusicElement); ok { if i, ok := elem.(*message.MusicShareElement); ok {
bot.Client.SendFriendRichMessage(target, 100495085, 1, 4, client.RichClientInfo{ bot.Client.SendFriendMusicShare(target, i)
Platform: 1,
SdkVersion: "0.0.0",
PackageName: "com.netease.cloudmusic",
Signature: "da6b069da1e2982db3e386233f68d76d",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
return 0
}
if i, ok := elem.(*MiguMusicElement); ok {
bot.Client.SendFriendRichMessage(target, 1101053067, 1, 4, client.RichClientInfo{
Platform: 1,
SdkVersion: "0.0.0",
PackageName: "cmccwm.mobilemusic",
Signature: "6cdc72a439cef99a3418d2a78aa28c73",
}, &message.RichMessage{
Title: i.Title,
Summary: i.Summary,
Url: i.Url,
PictureUrl: i.PictureUrl,
MusicUrl: i.MusicUrl,
})
return 0 return 0
} }
newElem = append(newElem, elem) newElem = append(newElem, elem)
@ -424,7 +355,7 @@ func (bot *CQBot) Release() {
} }
func (bot *CQBot) dispatchEventMessage(m MSG) { func (bot *CQBot) dispatchEventMessage(m MSG) {
if global.EventFilter != nil && global.EventFilter.Eval(global.MSG(m)) == false { if global.EventFilter != nil && !global.EventFilter.Eval(global.MSG(m)) {
log.Debug("Event filtered!") log.Debug("Event filtered!")
return return
} }
@ -445,6 +376,57 @@ func (bot *CQBot) dispatchEventMessage(m MSG) {
} }
} }
func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) MSG {
cqm := ToStringMessage(m.Elements, m.GroupCode, true)
gm := MSG{
"anonymous": nil,
"font": 0,
"group_id": m.GroupCode,
"message": ToFormattedMessage(m.Elements, m.GroupCode, false),
"message_type": "group",
"message_seq": m.Id,
"post_type": "message",
"raw_message": cqm,
"self_id": bot.Client.Uin,
"sender": MSG{
"age": 0,
"area": "",
"level": "",
"sex": "unknown",
"user_id": m.Sender.Uin,
},
"sub_type": "normal",
"time": time.Now().Unix(),
"user_id": m.Sender.Uin,
}
if m.Sender.IsAnonymous() {
gm["anonymous"] = MSG{
"flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
"id": m.Sender.Uin,
"name": m.Sender.AnonymousInfo.AnonymousNick,
}
gm["sender"].(MSG)["nickname"] = "匿名消息"
gm["sub_type"] = "anonymous"
} else {
mem := bot.Client.FindGroup(m.GroupCode).FindMember(m.Sender.Uin)
ms := gm["sender"].(MSG)
ms["role"] = func() string {
switch mem.Permission {
case client.Owner:
return "owner"
case client.Administrator:
return "admin"
default:
return "member"
}
}()
ms["nickname"] = mem.Nickname
ms["card"] = mem.CardName
ms["title"] = mem.SpecialTitle
}
return gm
}
func formatGroupName(group *client.GroupInfo) string { func formatGroupName(group *client.GroupInfo) string {
return fmt.Sprintf("%s(%d)", group.Name, group.Code) return fmt.Sprintf("%s(%d)", group.Name, group.Code)
} }

View File

@ -18,9 +18,11 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
@ -35,7 +37,8 @@ var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`)
var IgnoreInvalidCQCode = false var IgnoreInvalidCQCode = false
var SplitUrl = false var SplitUrl = false
const maxImageSize = 1024 * 1024 * 30 // 30MB const maxImageSize = 1024 * 1024 * 30 // 30MB
const maxVideoSize = 1024 * 1024 * 100 // 100MB
type PokeElement struct { type PokeElement struct {
Target int64 Target int64
@ -46,26 +49,6 @@ type GiftElement struct {
GiftId message.GroupGift GiftId message.GroupGift
} }
type MusicElement struct {
Title string
Summary string
Url string
PictureUrl string
MusicUrl string
}
type QQMusicElement struct {
MusicElement
}
type CloudMusicElement struct {
MusicElement
}
type MiguMusicElement struct {
MusicElement
}
type LocalImageElement struct { type LocalImageElement struct {
message.ImageElement message.ImageElement
Stream io.ReadSeeker Stream io.ReadSeeker
@ -77,12 +60,14 @@ type LocalVoiceElement struct {
Stream io.ReadSeeker Stream io.ReadSeeker
} }
func (e *GiftElement) Type() message.ElementType { type LocalVideoElement struct {
return message.At message.ShortVideoElement
File string
thumb io.ReadSeeker
} }
func (e *MusicElement) Type() message.ElementType { func (e *GiftElement) Type() message.ElementType {
return message.Service return message.At
} }
var GiftId = [...]message.GroupGift{ var GiftId = [...]message.GroupGift{
@ -124,7 +109,7 @@ func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []M
}) })
} }
for _, elem := range e { for _, elem := range e {
m := MSG{} var m MSG
switch o := elem.(type) { switch o := elem.(type) {
case *message.TextElement: case *message.TextElement:
m = MSG{ m = MSG{
@ -361,7 +346,7 @@ func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMes
saveTempText := func() { saveTempText := func() {
if len(tempText) != 0 { if len(tempText) != 0 {
if SplitUrl { if SplitUrl {
for _, t := range global.SplitUrl(CQCodeUnescapeValue(string(tempText))) { for _, t := range global.SplitURL(CQCodeUnescapeValue(string(tempText))) {
r = append(r, message.NewText(t)) r = append(r, message.NewText(t))
} }
} else { } else {
@ -403,6 +388,7 @@ func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMes
} }
} }
mid, err := strconv.Atoi(params["id"]) mid, err := strconv.Atoi(params["id"])
customText := params["text"]
if err == nil { if err == nil {
org := bot.GetMessage(int32(mid)) org := bot.GetMessage(int32(mid))
if org != nil { if org != nil {
@ -416,6 +402,25 @@ func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMes
}, r...) }, r...)
return return
} }
} else if customText != "" {
sender, err := strconv.ParseInt(params["qq"], 10, 64)
if err != nil {
log.Warnf("警告:自定义 Reply 元素中必须包含Uin")
return
}
msgTime, err := strconv.ParseInt(params["time"], 10, 64)
if err != nil {
msgTime = time.Now().Unix()
}
r = append([]message.IMessageElement{
&message.ReplyElement{
ReplySeq: int32(0),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, group),
},
}, r...)
return
} }
} }
if t == "forward" { // 单独处理转发 if t == "forward" { // 单独处理转发
@ -426,7 +431,7 @@ func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMes
} }
elem, err := bot.ToElement(t, params, group) elem, err := bot.ToElement(t, params, group)
if err != nil { if err != nil {
org := "[" + string(cqCode) + "]" org := "[CQ:" + string(cqCode) + "]"
if !IgnoreInvalidCQCode { if !IgnoreInvalidCQCode {
log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err)
r = append(r, message.NewText(org)) r = append(r, message.NewText(org))
@ -482,6 +487,7 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
} }
} }
mid, err := strconv.Atoi(e.Get("data").Get("id").String()) mid, err := strconv.Atoi(e.Get("data").Get("id").String())
customText := e.Get("data").Get("text").String()
if err == nil { if err == nil {
org := bot.GetMessage(int32(mid)) org := bot.GetMessage(int32(mid))
if org != nil { if org != nil {
@ -495,6 +501,25 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.
}, r...) }, r...)
return return
} }
} else if customText != "" {
sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64)
if err != nil {
log.Warnf("警告:自定义 Reply 元素中必须包含Uin")
return
}
msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64)
if err != nil {
msgTime = time.Now().Unix()
}
r = append([]message.IMessageElement{
&message.ReplyElement{
ReplySeq: int32(0),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, group),
},
}, r...)
return
} }
} }
if t == "forward" { if t == "forward" {
@ -540,14 +565,14 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
case "text": case "text":
if SplitUrl { if SplitUrl {
var ret []message.IMessageElement var ret []message.IMessageElement
for _, text := range global.SplitUrl(d["text"]) { for _, text := range global.SplitURL(d["text"]) {
ret = append(ret, message.NewText(text)) ret = append(ret, message.NewText(text))
} }
return ret, nil return ret, nil
} }
return message.NewText(d["text"]), nil return message.NewText(d["text"]), nil
case "image": case "image":
img, err := bot.makeImageElem(d, group) img, err := bot.makeImageOrVideoElem(d, false, group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -605,22 +630,21 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
} }
}() }()
data, err := bot.Client.GetTts(d["text"]) data, err := bot.Client.GetTts(d["text"])
ioutil.WriteFile("tts.silk", data, 777)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &message.VoiceElement{Data: data}, nil return &message.VoiceElement{Data: data}, nil
case "record": case "record":
f := d["file"] f := d["file"]
data, err := global.FindFile(f, d["cache"], global.VOICE_PATH) data, err := global.FindFile(f, d["cache"], global.VoicePath)
if err == global.ErrSyntax { if err == global.ErrSyntax {
data, err = global.FindFile(f, d["cache"], global.VOICE_PATH_OLD) data, err = global.FindFile(f, d["cache"], global.VoicePathOld)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !global.IsAMRorSILK(data) { if !global.IsAMRorSILK(data) {
data, err = global.Encoder(data) data, err = global.EncoderSilk(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -651,7 +675,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
return nil, errors.New("song not found") return nil, errors.New("song not found")
} }
aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10)
name := info.Get("track_info.name").Str + " - " + info.Get("track_info.singer.0.name").Str name := info.Get("track_info.name").Str
mid := info.Get("track_info.mid").Str mid := info.Get("track_info.mid").Str
albumMid := info.Get("track_info.album.mid").Str albumMid := info.Get("track_info.album.mid").Str
pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576")
@ -661,17 +685,18 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
if len(aid) < 2 { if len(aid) < 2 {
return nil, errors.New("song error") return nil, errors.New("song error")
} }
content := "来自go-cqhttp" content := info.Get("track_info.singer.0.name").Str
if d["content"] != "" { if d["content"] != "" {
content = d["content"] content = d["content"]
} }
return &QQMusicElement{MusicElement: MusicElement{ return &message.MusicShareElement{
MusicType: message.QQMusic,
Title: name, Title: name,
Summary: content, Summary: content,
Url: jumpUrl, Url: jumpUrl,
PictureUrl: preview, PictureUrl: preview,
MusicUrl: purl, MusicUrl: purl,
}}, nil }, nil
} }
if d["type"] == "163" { if d["type"] == "163" {
info, err := global.NeteaseMusicSongInfo(d["id"]) info, err := global.NeteaseMusicSongInfo(d["id"])
@ -689,41 +714,36 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
if info.Get("artists.0").Exists() { if info.Get("artists.0").Exists() {
artistName = info.Get("artists.0.name").Str artistName = info.Get("artists.0.name").Str
} }
return &CloudMusicElement{MusicElement{ return &message.MusicShareElement{
MusicType: message.CloudMusic,
Title: name, Title: name,
Summary: artistName, Summary: artistName,
Url: jumpUrl, Url: jumpUrl,
PictureUrl: picUrl, PictureUrl: picUrl,
MusicUrl: musicUrl, MusicUrl: musicUrl,
}}, nil }, nil
} }
if d["type"] == "custom" { if d["type"] == "custom" {
if d["subtype"] == "qq" { if d["subtype"] != "" {
return &QQMusicElement{MusicElement{ var subtype = map[string]int{
"qq": message.QQMusic,
"163": message.CloudMusic,
"migu": message.MiguMusic,
"kugou": message.KugouMusic,
"kuwo": message.KuwoMusic,
}
var musicType = 0
if tp, ok := subtype[d["subtype"]]; ok {
musicType = tp
}
return &message.MusicShareElement{
MusicType: musicType,
Title: d["title"], Title: d["title"],
Summary: d["content"], Summary: d["content"],
Url: d["url"], Url: d["url"],
PictureUrl: d["image"], PictureUrl: d["image"],
MusicUrl: d["purl"], MusicUrl: d["purl"],
}}, nil }, nil
}
if d["subtype"] == "163" {
return &CloudMusicElement{MusicElement{
Title: d["title"],
Summary: d["content"],
Url: d["url"],
PictureUrl: d["image"],
MusicUrl: d["purl"],
}}, nil
}
if d["subtype"] == "migu" {
return &MiguMusicElement{MusicElement{
Title: d["title"],
Summary: d["content"],
Url: d["url"],
PictureUrl: d["image"],
MusicUrl: d["purl"],
}}, nil
} }
xml := fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="[分享] %s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2"><audio cover="%s" src="%s"/><title>%s</title><summary>%s</summary></item><source name="音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>`, xml := fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="[分享] %s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2"><audio cover="%s" src="%s"/><title>%s</title><summary>%s</summary></item><source name="音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>`,
XmlEscape(d["title"]), d["url"], d["image"], d["audio"], XmlEscape(d["title"]), XmlEscape(d["content"])) XmlEscape(d["title"]), d["url"], d["image"], d["audio"], XmlEscape(d["title"]), XmlEscape(d["content"]))
@ -770,11 +790,58 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf
if maxHeight == 0 { if maxHeight == 0 {
maxHeight = 1000 maxHeight = 1000
} }
img, err := bot.makeImageElem(d, group) img, err := bot.makeImageOrVideoElem(d, false, group)
if err != nil { if err != nil {
return nil, errors.New("send cardimage faild") return nil, errors.New("send cardimage faild")
} }
return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, group) return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, group)
case "video":
cache := d["cache"]
if cache == "" {
cache = "1"
}
file, err := bot.makeImageOrVideoElem(d, true, group)
if err != nil {
return nil, err
}
v := file.(*LocalVideoElement)
if v.File == "" {
return v, nil
}
var data []byte
if cover, ok := d["cover"]; ok {
data, _ = global.FindFile(cover, cache, global.ImagePath)
} else {
_ = global.ExtractCover(v.File, v.File+".jpg")
data, _ = ioutil.ReadFile(v.File + ".jpg")
}
v.thumb = bytes.NewReader(data)
video, _ := os.Open(v.File)
defer video.Close()
_, err = video.Seek(4, io.SeekStart)
if err != nil {
return nil, err
}
var header = make([]byte, 4)
_, err = video.Read(header)
if err != nil {
return nil, err
}
if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp
_, _ = video.Seek(0, io.SeekStart)
hash, _ := utils.ComputeMd5AndLength(video)
cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4")
if global.PathExists(cacheFile) && cache == "1" {
goto ok
}
err = global.EncodeMP4(v.File, cacheFile)
if err != nil {
return nil, err
}
ok:
v.File = cacheFile
}
return v, nil
default: default:
return nil, errors.New("unsupported cq code: " + t) return nil, errors.New("unsupported cq code: " + t)
} }
@ -816,7 +883,7 @@ func CQCodeUnescapeValue(content string) string {
} }
// 图片 elem 生成器,单独拎出来,用于公用 // 图片 elem 生成器,单独拎出来,用于公用
func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessageElement, error) { func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) {
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"] cache := d["cache"]
@ -825,26 +892,29 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa
cache = "1" cache = "1"
} }
hash := md5.Sum([]byte(f)) hash := md5.Sum([]byte(f))
cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".cache") cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
var maxSize = func() int64 {
if video {
return maxVideoSize
}
return maxImageSize
}()
thread, _ := strconv.Atoi(c)
if global.PathExists(cacheFile) && cache == "1" { if global.PathExists(cacheFile) && cache == "1" {
return &LocalImageElement{File: cacheFile}, nil goto hasCacheFile
} }
if global.PathExists(cacheFile) { if global.PathExists(cacheFile) {
_ = os.Remove(cacheFile) _ = os.Remove(cacheFile)
} }
thread, _ := strconv.Atoi(c) if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil {
if err := global.DownloadFileMultiThreading(f, cacheFile, maxImageSize, thread); err != nil {
return nil, err return nil, err
} }
hasCacheFile:
if video {
return &LocalVideoElement{File: cacheFile}, nil
}
return &LocalImageElement{File: cacheFile}, nil return &LocalImageElement{File: cacheFile}, nil
} }
if strings.HasPrefix(f, "base64") {
b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", ""))
if err != nil {
return nil, err
}
return &LocalImageElement{Stream: bytes.NewReader(b)}, nil
}
if strings.HasPrefix(f, "file") { if strings.HasPrefix(f, "file") {
fu, err := url.Parse(f) fu, err := url.Parse(f)
if err != nil { if err != nil {
@ -855,22 +925,58 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa
} }
info, err := os.Stat(fu.Path) info, err := os.Stat(fu.Path)
if err != nil { if err != nil {
if !os.IsExist(err) {
return nil, errors.New("file not found")
}
return nil, err return nil, err
} }
if video {
if info.Size() == 0 || info.Size() >= maxVideoSize {
return nil, errors.New("invalid video size")
}
return &LocalVideoElement{File: fu.Path}, nil
}
if info.Size() == 0 || info.Size() >= maxImageSize { if info.Size() == 0 || info.Size() >= maxImageSize {
return nil, errors.New("invalid image size") return nil, errors.New("invalid image size")
} }
return &LocalImageElement{File: fu.Path}, nil return &LocalImageElement{File: fu.Path}, nil
} }
rawPath := path.Join(global.IMAGE_PATH, f) rawPath := path.Join(global.ImagePath, f)
if !global.PathExists(rawPath) && global.PathExists(path.Join(global.IMAGE_PATH_OLD, f)) { if video {
rawPath = path.Join(global.IMAGE_PATH_OLD, f) rawPath = path.Join(global.VideoPath, f)
if !global.PathExists(rawPath) {
return nil, errors.New("invalid video")
}
if path.Ext(rawPath) == ".video" {
b, _ := ioutil.ReadFile(rawPath)
r := binary.NewReader(b)
return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效
Md5: r.ReadBytes(16),
ThumbMd5: r.ReadBytes(16),
Size: r.ReadInt32(),
ThumbSize: r.ReadInt32(),
Name: r.ReadString(),
Uuid: r.ReadAvailable(),
}}, nil
} else {
return &LocalVideoElement{File: rawPath}, nil
}
}
if strings.HasPrefix(f, "base64") {
b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", ""))
if err != nil {
return nil, err
}
return &LocalImageElement{Stream: bytes.NewReader(b)}, nil
}
if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) {
rawPath = path.Join(global.ImagePathOld, f)
} }
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"] != "" { if !global.PathExists(rawPath) && d["url"] != "" {
return bot.makeImageElem(map[string]string{"file": d["url"]}, group) return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group)
} }
if global.PathExists(rawPath) { if global.PathExists(rawPath) {
file, err := os.Open(rawPath) file, err := os.Open(rawPath)
@ -887,9 +993,11 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa
if len(b) < 20 { if len(b) < 20 {
return nil, errors.New("invalid local file") return nil, errors.New("invalid local file")
} }
var size int32 var (
var hash []byte size int32
var url string hash []byte
url string
)
if path.Ext(rawPath) == ".cqimg" { if path.Ext(rawPath) == ".cqimg" {
for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") {
kv := strings.SplitN(line, "=", 2) kv := strings.SplitN(line, "=", 2)
@ -910,27 +1018,23 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa
} }
if size == 0 { if size == 0 {
if url != "" { if url != "" {
return bot.makeImageElem(map[string]string{"file": url}, group) return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group)
} }
return nil, errors.New("img size is 0") return nil, errors.New("img size is 0")
} }
if len(hash) != 16 { if len(hash) != 16 {
return nil, errors.New("invalid hash") return nil, errors.New("invalid hash")
} }
var rsp message.IMessageElement
if group { if group {
rsp, err := bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size)
if err != nil { goto ok
if url != "" {
return bot.makeImageElem(map[string]string{"file": url}, group)
}
return nil, err
}
return rsp, nil
} }
rsp, err := bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size)
ok:
if err != nil { if err != nil {
if url != "" { if url != "" {
return bot.makeImageElem(map[string]string{"file": url}, group) return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group)
} }
return nil, err return nil, err
} }
@ -944,8 +1048,8 @@ func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon
xml := "" xml := ""
var suf message.IMessageElement var suf message.IMessageElement
if i, ok := elem.(*LocalImageElement); ok { if i, ok := elem.(*LocalImageElement); ok {
if group == false { if !group {
gm, err := bot.Client.UploadPrivateImage(1, i.Stream) gm, err := bot.UploadLocalImageAsPrivate(1, i)
if err != nil { if err != nil {
log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err)
return nil, err return nil, err
@ -953,7 +1057,7 @@ func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon
suf = gm suf = gm
xml = fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="5" templateID="12345" action="" brief="&#91;分享&#93;我看到一张很赞的图片,分享给你,快来看!" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="0" advertiser_id="0" aid="0"><image uuid="%x" md5="%x" GroupFiledid="0" filesize="%d" local_path="%s" minWidth="%d" minHeight="%d" maxWidth="%d" maxHeight="%d" /></item><source name="%s" icon="%s" action="" appid="-1" /></msg>`, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) xml = fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="5" templateID="12345" action="" brief="&#91;分享&#93;我看到一张很赞的图片,分享给你,快来看!" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="0" advertiser_id="0" aid="0"><image uuid="%x" md5="%x" GroupFiledid="0" filesize="%d" local_path="%s" minWidth="%d" minHeight="%d" maxWidth="%d" maxHeight="%d" /></item><source name="%s" icon="%s" action="" appid="-1" /></msg>`, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
} else { } else {
gm, err := bot.Client.UploadGroupImage(1, i.Stream) gm, err := bot.UploadLocalImageAsGroup(1, i)
if err != nil { if err != nil {
log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err)
return nil, err return nil, err

View File

@ -17,15 +17,17 @@ import (
var format = "string" var format = "string"
//SetMessageFormat 设置消息上报格式默认为string
func SetMessageFormat(f string) { func SetMessageFormat(f string) {
format = f format = f
} }
func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r interface{}) { //ToFormattedMessage 将给定[]message.IMessageElement转换为通过coolq.SetMessageFormat所定义的消息上报格式
func ToFormattedMessage(e []message.IMessageElement, id int64, raw ...bool) (r interface{}) {
if format == "string" { if format == "string" {
r = ToStringMessage(e, code, raw...) r = ToStringMessage(e, id, raw...)
} else if format == "array" { } else if format == "array" {
r = ToArrayMessage(e, code, raw...) r = ToArrayMessage(e, id, raw...)
} }
return return
} }
@ -91,52 +93,8 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
id = bot.InsertGroupMessage(m) id = bot.InsertGroupMessage(m)
} }
log.Infof("收到群 %v(%v) 内 %v(%v) 的消息: %v (%v)", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm, id) log.Infof("收到群 %v(%v) 内 %v(%v) 的消息: %v (%v)", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm, id)
gm := MSG{ gm := bot.formatGroupMessage(m)
"anonymous": nil, gm["message_id"] = id
"font": 0,
"group_id": m.GroupCode,
"message": ToFormattedMessage(m.Elements, m.GroupCode, false),
"message_id": id,
"message_type": "group",
"post_type": "message",
"raw_message": cqm,
"self_id": c.Uin,
"sender": MSG{
"age": 0,
"area": "",
"level": "",
"sex": "unknown",
"user_id": m.Sender.Uin,
},
"sub_type": "normal",
"time": time.Now().Unix(),
"user_id": m.Sender.Uin,
}
if m.Sender.IsAnonymous() {
gm["anonymous"] = MSG{
"flag": m.Sender.AnonymousInfo.AnonymousId + "|" + m.Sender.AnonymousInfo.AnonymousNick,
"id": m.Sender.Uin,
"name": m.Sender.AnonymousInfo.AnonymousNick,
}
gm["sender"].(MSG)["nickname"] = "匿名消息"
gm["sub_type"] = "anonymous"
} else {
mem := c.FindGroup(m.GroupCode).FindMember(m.Sender.Uin)
ms := gm["sender"].(MSG)
ms["role"] = func() string {
switch mem.Permission {
case client.Owner:
return "owner"
case client.Administrator:
return "admin"
default:
return "member"
}
}()
ms["nickname"] = mem.Nickname
ms["card"] = mem.CardName
ms["title"] = mem.SpecialTitle
}
bot.dispatchEventMessage(gm) bot.dispatchEventMessage(gm)
} }
@ -455,6 +413,67 @@ func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupR
}) })
} }
func (bot *CQBot) otherClientStatusChangedEvent(c *client.QQClient, e *client.OtherClientStatusChangedEvent) {
if e.Online {
log.Infof("Bot 账号在客户端 %v (%v) 登录.", e.Client.DeviceName, e.Client.DeviceKind)
} else {
log.Infof("Bot 账号在客户端 %v (%v) 登出.", e.Client.DeviceName, e.Client.DeviceKind)
}
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "client_status",
"online": e.Online,
"client": MSG{
"app_id": e.Client.AppId,
"device_name": e.Client.DeviceName,
"device_kind": e.Client.DeviceKind,
},
"self_id": c.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) groupEssenceMsg(c *client.QQClient, e *client.GroupDigestEvent) {
g := c.FindGroup(e.GroupCode)
gid := ToGlobalId(e.GroupCode, e.MessageID)
if e.OperationType == 1 {
log.Infof(
"群 %v 内 %v 将 %v 的消息(%v)设为了精华消息.",
formatGroupName(g),
formatMemberName(g.FindMember(e.OperatorUin)),
formatMemberName(g.FindMember(e.SenderUin)),
gid,
)
} else {
log.Infof(
"群 %v 内 %v 将 %v 的消息(%v)移出了精华消息.",
formatGroupName(g),
formatMemberName(g.FindMember(e.OperatorUin)),
formatMemberName(g.FindMember(e.SenderUin)),
gid,
)
}
if e.OperatorUin == bot.Client.Uin {
return
}
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"group_id": e.GroupCode,
"notice_type": "essence",
"sub_type": func() string {
if e.OperationType == 1 {
return "add"
}
return "delete"
}(),
"self_id": c.Uin,
"sender_id": e.SenderUin,
"operator_id": e.OperatorUin,
"time": time.Now().Unix(),
"message_id": gid,
})
}
func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) MSG { func (bot *CQBot) groupIncrease(groupCode, operatorUin, userUin int64) MSG {
return MSG{ return MSG{
"post_type": "notice", "post_type": "notice",
@ -499,8 +518,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
switch i := elem.(type) { switch i := elem.(type) {
case *message.ImageElement: 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.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename) w.WriteString(i.Filename)
@ -510,8 +529,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
i.Filename = filename i.Filename = filename
case *message.GroupImageElement: case *message.GroupImageElement:
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.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteString(filename) w.WriteString(filename)
@ -520,8 +539,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
} }
case *message.FriendImageElement: case *message.FriendImageElement:
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.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.WriteUInt32(uint32(0)) // 发送时会调用url, 大概没事 w.WriteUInt32(uint32(0)) // 发送时会调用url, 大概没事
w.WriteString(filename) w.WriteString(filename)
@ -530,8 +549,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
} }
case *message.GroupFlashImgElement: case *message.GroupFlashImgElement:
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.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename) w.WriteString(i.Filename)
@ -541,8 +560,8 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
i.Filename = filename i.Filename = filename
case *message.FriendFlashImgElement: case *message.FriendFlashImgElement:
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.ImagePath, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.ImagePath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename) w.WriteString(i.Filename)
@ -553,20 +572,22 @@ func (bot *CQBot) checkMedia(e []message.IMessageElement) {
case *message.VoiceElement: case *message.VoiceElement:
i.Name = strings.ReplaceAll(i.Name, "{", "") i.Name = strings.ReplaceAll(i.Name, "{", "")
i.Name = strings.ReplaceAll(i.Name, "}", "") i.Name = strings.ReplaceAll(i.Name, "}", "")
if !global.PathExists(path.Join(global.VOICE_PATH, i.Name)) { if !global.PathExists(path.Join(global.VoicePath, i.Name)) {
b, err := global.GetBytes(i.Url) b, err := global.GetBytes(i.Url)
if err != nil { if err != nil {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err) log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue continue
} }
_ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0644) _ = ioutil.WriteFile(path.Join(global.VoicePath, i.Name), b, 0644)
} }
case *message.ShortVideoElement: case *message.ShortVideoElement:
filename := hex.EncodeToString(i.Md5) + ".video" filename := hex.EncodeToString(i.Md5) + ".video"
if !global.PathExists(path.Join(global.VIDEO_PATH, filename)) { if !global.PathExists(path.Join(global.VideoPath, filename)) {
_ = ioutil.WriteFile(path.Join(global.VIDEO_PATH, filename), binary.NewWriterF(func(w *binary.Writer) { _ = ioutil.WriteFile(path.Join(global.VideoPath, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5) w.Write(i.Md5)
w.Write(i.ThumbMd5)
w.WriteUInt32(uint32(i.Size)) w.WriteUInt32(uint32(i.Size))
w.WriteUInt32(uint32(i.ThumbSize))
w.WriteString(i.Name) w.WriteString(i.Name)
w.Write(i.Uuid) w.Write(i.Uuid)
}), 0644) }), 0644)

View File

@ -39,6 +39,9 @@
- [获取群子目录文件列表](#设置群名) - [获取群子目录文件列表](#设置群名)
- [获取用户VIP信息](#获取用户VIP信息) - [获取用户VIP信息](#获取用户VIP信息)
- [发送群公告](#发送群公告) - [发送群公告](#发送群公告)
- [设置精华消息](#设置精华消息)
- [移出精华消息](#移出精华消息)
- [获取精华消息列表](#获取精华消息列表)
- [重载事件过滤器](#重载事件过滤器) - [重载事件过滤器](#重载事件过滤器)
##### 事件 ##### 事件
@ -50,6 +53,7 @@
- [群成员荣誉变更提示](#群成员荣誉变更提示) - [群成员荣誉变更提示](#群成员荣誉变更提示)
- [群成员名片更新](#群成员名片更新) - [群成员名片更新](#群成员名片更新)
- [接收到离线文件](#接收到离线文件) - [接收到离线文件](#接收到离线文件)
- [群精华消息](#精华消息)
</p> </p>
</details> </details>
@ -94,13 +98,71 @@ Type : `reply`
范围: **发送/接收** 范围: **发送/接收**
> 注意: 如果id存在则优先处理id
参数: 参数:
| 参数名 | 类型 | 说明 | | 参数名 | 类型 | 说明 |
| ------ | ---- | ------------------------------------- | | ------ | ---- | ------------------------------------- |
| `id` | int | 回复时所引用的消息id, 必须为本群消息. | | `id` | int | 回复时所引用的消息id, 必须为本群消息. |
| `text` | string | 自定义回复的信息 |
| `qq` | int64 | 自定义回复时的自定义QQ, 如果使用自定义信息必须指定. |
| `time` | int64 | 自定义回复时的时间, 格式为Unix时间 |
示例: `[CQ:reply,id=123456]` 示例: `[CQ:reply,id=123456]`
\
自定义回复示例: `[CQ:reply,text=Hello World,qq=10086,time=3376656000]`
### 音乐分享 <Badge text="发"/>
```json
{
"type": "music",
"data": {
"type": "163",
"id": "28949129"
}
}
```
```
[CQ:music,type=163,id=28949129]
```
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --- | --- | --- | --- | --- |
| `type` | | ✓ | `qq` `163` | 分别表示使用 QQ 音乐、网易云音乐 |
| `id` | | ✓ | - | 歌曲 ID |
### 音乐自定义分享 <Badge text="发"/>
```json
{
"type": "music",
"data": {
"type": "custom",
"url": "http://baidu.com",
"audio": "http://baidu.com/1.mp3",
"title": "音乐标题"
}
}
```
```
[CQ:music,type=custom,url=http://baidu.com,audio=http://baidu.com/1.mp3,title=音乐标题]
```
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --- | --- | --- | --- | --- |
| `type` | | ✓ | `custom` | 表示音乐自定义分享 |
| `subtype` | | ✓ | `qq,163,migu,kugou,kuwo` | 表示分享类型不填写发送为xml卡片推荐填写提高稳定性 |
| `url` | | ✓ | - | 点击后跳转目标 URL |
| `audio` | | ✓ | - | 音乐 URL |
| `title` | | ✓ | - | 标题 |
| `content` | | ✓ | - | 内容描述 |
| `image` | | ✓ | - | 图片 URL |
### 红包 ### 红包
@ -197,7 +259,7 @@ Type: `node`
| `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** | | `id` | int32 | 转发消息id | 直接引用他人的消息合并转发, 实际查看顺序为原消息发送顺序 **与下面的自定义消息二选一** |
| `name` | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) | | `name` | string | 发送者显示名字 | 用于自定义消息 (自定义消息并合并转发,实际查看顺序为自定义消息段顺序) |
| `uin` | int64 | 发送者QQ号 | 用于自定义消息 | | `uin` | int64 | 发送者QQ号 | 用于自定义消息 |
| `content` | message | 具体消息 | 用于自定义消息 **不支持引用回复** | | `content` | message | 具体消息 | 用于自定义消息 |
特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送并且由于消息段较为复杂仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. 另外按 [CQHTTP](https://cqhttp.cc/docs/4.15/#/Message?id=格式) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃** 特殊说明: **需要使用单独的API `/send_group_forward_msg` 发送并且由于消息段较为复杂仅支持Array形式入参。 如果引用消息和自定义消息同时出现,实际查看顺序将取消息段顺序. 另外按 [CQHTTP](https://cqhttp.cc/docs/4.15/#/Message?id=格式) 文档说明, `data` 应全为字符串, 但由于需要接收`message` 类型的消息, 所以 *仅限此Type的content字段* 支持Array套娃**
@ -259,7 +321,8 @@ Type: `node`
"data": { "data": {
"name": "自定义发送者", "name": "自定义发送者",
"uin": "10086", "uin": "10086",
"content": "我是自定义消息" "content": "我是自定义消息",
"time": "3376656000"
} }
}, },
{ {
@ -270,6 +333,20 @@ Type: `node`
} }
] ]
```` ````
### 短视频消息
Type: `video`
范围: **发送/接收**
参数:
| 参数名 | 类型 | 说明 |
| ------- | ------ | ------------------------------------------------|
| `file` | string | 支持http和file发送 |
| `cover` | string | 视频封面支持httpfile和base64发送格式必须为jpg |
| `c` | `2` `3`| 通过网络下载视频时的线程数, 默认单线程. (在资源不支持并发时会自动处理)|
示例: `[CQ:image,file=file:///C:\\Users\Richard\Pictures\1.mp4]`
### XML 消息 ### XML 消息
@ -525,6 +602,12 @@ Type: `tts`
| `group_id` | int64 | 群号 | | `group_id` | int64 | 群号 |
| `messages` | forward node[] | 自定义转发消息, 具体看CQCode | | `messages` | forward node[] | 自定义转发消息, 具体看CQCode |
响应数据
| 字段 | 类型 | 说明 |
| ------------ | ------ | ------ |
| `message_id` | string | 消息id |
### 获取中文分词 ### 获取中文分词
终结点: `/.get_word_slices` 终结点: `/.get_word_slices`
@ -541,11 +624,63 @@ Type: `tts`
| -------- | -------- | ---- | | -------- | -------- | ---- |
| `slices` | string[] | 词组 | | `slices` | string[] | 词组 |
### 设置精华消息
终结点: `/set_essence_msg`
**参数**
| 字段 | 类型 | 说明 |
| --------- | ------ | ---- |
| `message_id` | int32 | 消息ID |
**响应数据**
### 移出精华消息
终结点: `/delete_essence_msg`
**参数**
| 字段 | 类型 | 说明 |
| --------- | ------ | ---- |
| `message_id` | int32 | 消息ID |
**响应数据**
### 获取精华消息列表
终结点: `/get_essence_msg_list`
**参数**
| 字段 | 类型 | 说明 |
| --------- | ------ | ---- |
| `group_id` | int64 | 群号 |
**响应数据**
响应内容为 JSON 数组,每个元素如下:
| 字段名 | 数据类型 | 说明 |
| ----- | ------- | --- |
| `sender_id` |int64 | 发送者QQ 号 |
| `sender_nick` | string | 发送者昵称 |
| `sender_time` | int64 | 消息发送时间 |
| `operator_id` |int64 | 发送者QQ 号 |
| `operator_nick` | string | 发送者昵称 |
| `operator_time` | int64 | 消息发送时间|
| `message_id` | int32 | 消息ID |
### 图片OCR ### 图片OCR
> 注意: 目前图片OCR接口仅支持接受的图片 > 注意: 目前图片OCR接口仅支持接受的图片
终结点: `/.ocr_image` 终结点: `/ocr_image`
**参数** **参数**
@ -711,6 +846,22 @@ Type: `tts`
| `creator_name` | string | 创建者名字 | | `creator_name` | string | 创建者名字 |
| `total_file_count` | int32 | 子文件数量 | | `total_file_count` | int32 | 子文件数量 |
### 上传群文件
终结点: `/upload_group_file`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ------ | ------------------------- |
| `group_id` | int64 | 群号 |
| `file` | string | 本地文件路径 |
| `name` | string | 储存名称 |
| `folder` | string | 父目录ID |
> 在不提供 `folder` 参数的情况下默认上传到根目录
> 只能上传本地文件, 需要上传 `http` 文件的话请先调用 `download_file` API下载
### 获取状态 ### 获取状态
终结点: `/get_status` 终结点: `/get_status`
@ -760,6 +911,105 @@ Type: `tts`
| `remain_at_all_count_for_group` | int16 | 群内所有管理当天剩余@全体成员次数 | | `remain_at_all_count_for_group` | int16 | 群内所有管理当天剩余@全体成员次数 |
| `remain_at_all_count_for_uin` | int16 | BOT当天剩余@全体成员次数 | | `remain_at_all_count_for_uin` | int16 | BOT当天剩余@全体成员次数 |
### 下载文件到缓存目录
终结点: `/download_file`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ------ | ------------------------- |
| `url` | string | 链接地址 |
| `thread_count` | int32 | 下载线程数 |
| `headers` | string or array | 自定义请求头 |
**`headers`格式:**
字符串:
```
User-Agent=YOUR_UA[\r\n]Referer=https://www.baidu.com
```
> `[\r\n]` 为换行符, 使用http请求时请注意编码
JSON数组:
```
[
"User-Agent=YOUR_UA",
"Referer=https://www.baidu.com",
]
```
**响应数据**
| 字段 | 类型 | 说明 |
| ---------- | ---------- | ------------ |
| `file` | string | 下载文件的*绝对路径* |
> 通过这个API下载的文件能直接放入CQ码作为图片或语音发送
> 调用后会阻塞直到下载完成后才会返回数据,请注意下载大文件时的超时
### 获取群消息历史记录
终结点:`/get_group_msg_history`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ------ | ------------------------- |
| `message_seq` | int64 | 起始消息序号, 可通过 `get_msg` 获得 |
| `group_id` | int64 | 群号 |
**响应数据**
| 字段 | 类型 | 说明 |
| ---------- | ---------- | ------------ |
| `messages` | []Message | 从起始序号开始的前19条消息 |
> 不提供起始序号将默认获取最新的消息
### 获取当前账号在线客户端列表
终结点:`/get_online_clients`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ------ | ------------------------- |
| `no_cache` | bool | 是否无视缓存 |
**响应数据**
| 字段 | 类型 | 说明 |
| ---------- | ---------- | ------------ |
| `clients` | []Device | 在线客户端列表 |
**Device**
| 字段 | 类型 | 说明 |
| ---------- | ---------- | ------------ |
| `app_id` | int64 | 客户端ID |
| `device_name` | string | 设备名称 |
| `device_kind` | string | 设备类型 |
### 检查链接安全性
终结点:`/check_url_safely`
**参数**
| 字段 | 类型 | 说明 |
| ---------- | ------ | ------------------------- |
| `url` | string | 需要检查的链接 |
**响应数据**
| 字段 | 类型 | 说明 |
| ---------- | ---------- | ------------ |
| `level` | int | 安全等级, 1: 安全 2: 未知 3: 危险 |
### 获取用户VIP信息 ### 获取用户VIP信息
终结点:`/_get_vip_info` 终结点:`/_get_vip_info`
@ -923,3 +1173,27 @@ Type: `tts`
| `name` | string | | 文件名 | | `name` | string | | 文件名 |
| `size` | int64 | | 文件大小 | | `size` | int64 | | 文件大小 |
| `url` | string | | 下载链接 | | `url` | string | | 下载链接 |
### 其他客户端在线状态变更
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `client_status` | 消息类型 |
| `client` | Device | | 客户端信息 |
| `online` | bool | | 当前是否在线 |
### 精华消息
**上报数据**
| 字段 | 类型 | 可能的值 | 说明 |
| ------------- | ------ | -------------- | -------- |
| `post_type` | string | `notice` | 上报类型 |
| `notice_type` | string | `essence` | 消息类型 |
| `sub_type` | string | `add`,`delete` | 添加为`add`,移出为`delete` |
| `sender_id` | int64 | | 消息发送者ID |
| `operator_id` | int64 | | 操作者ID |
| `message_id` | int32 | | 消息ID |

View File

@ -2,44 +2,62 @@ package global
import ( import (
"crypto/md5" "crypto/md5"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os/exec"
"path" "path"
"sync"
"github.com/Mrs4s/go-cqhttp/global/codec"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/wdvxdr1123/go-silk/silk"
) )
var codec silk.Encoder var useSilkCodec = true
var useCodec = true
var once sync.Once
//InitCodec 初始化Silk编码器
func InitCodec() { func InitCodec() {
once.Do(func() { log.Info("正在加载silk编码器...")
log.Info("正在加载silk编码器...") err := codec.Init()
err := codec.Init("data/cache", "codec") if err != nil {
if err != nil { log.Error(err)
log.Error(err) useSilkCodec = false
useCodec = false }
}
})
} }
func Encoder(data []byte) ([]byte, error) { //EncoderSilk 将音频编码为Silk
if useCodec == false { func EncoderSilk(data []byte) ([]byte, error) {
if !useSilkCodec {
return nil, errors.New("no silk encoder") return nil, errors.New("no silk encoder")
} }
h := md5.New() h := md5.New()
h.Write(data) _, err := h.Write(data)
if err != nil {
return nil, errors.Wrap(err, "calc md5 failed")
}
tempName := fmt.Sprintf("%x", h.Sum(nil)) tempName := fmt.Sprintf("%x", h.Sum(nil))
if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) { if silkPath := path.Join("data/cache", tempName+".silk"); PathExists(silkPath) {
return ioutil.ReadFile(silkPath) return ioutil.ReadFile(silkPath)
} }
slk, err := codec.EncodeToSilk(data, tempName, true) slk, err := codec.EncodeToSilk(data, tempName, true)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "encode silk failed")
} }
return slk, nil return slk, nil
} }
//EncodeMP4 将给定视频文件编码为MP4
func EncodeMP4(src string, dst string) error { // -y 覆盖文件
cmd1 := exec.Command("ffmpeg", "-i", src, "-y", "-c", "copy", "-map", "0", dst)
err := cmd1.Run()
if err != nil {
cmd2 := exec.Command("ffmpeg", "-i", src, "-y", "-c:v", "h264", "-c:a", "mp3", dst)
return errors.Wrap(cmd2.Run(), "convert mp4 failed")
}
return err
}
//ExtractCover 获取给定视频文件的Cover
func ExtractCover(src string, target string) error {
cmd := exec.Command("ffmpeg", "-i", src, "-y", "-r", "1", "-f", "image2", target)
return errors.Wrap(cmd.Run(), "extract video cover failed")
}

97
global/codec/codec.go Normal file
View File

@ -0,0 +1,97 @@
// +build linux windows darwin
// +build 386 amd64 arm arm64
package codec
import (
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"runtime"
)
const (
silkCachePath = "data/cache"
encoderPath = "codec"
)
func downloadCodec(url string) (err error) {
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = ioutil.WriteFile(getEncoderFilePath(), body, os.ModePerm)
return
}
func getEncoderFilePath() string {
encoderFile := path.Join(encoderPath, runtime.GOOS+"-"+runtime.GOARCH+"-encoder")
if runtime.GOOS == "windows" {
encoderFile = encoderFile + ".exe"
}
return encoderFile
}
//Init 下载Silk编码器
func Init() error {
if !fileExist(silkCachePath) {
_ = os.MkdirAll(silkCachePath, os.ModePerm)
}
if !fileExist(encoderPath) {
_ = os.MkdirAll(encoderPath, os.ModePerm)
}
p := getEncoderFilePath()
if !fileExist(p) {
if err := downloadCodec("https://cdn.jsdelivr.net/gh/wdvxdr1123/tosilk/codec/" + runtime.GOOS + "-" + runtime.GOARCH + "-encoder"); err != nil {
return errors.New("下载依赖失败")
}
}
return nil
}
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
// 1. 写入缓存文件
rawPath := path.Join(silkCachePath, tempName+".wav")
err := ioutil.WriteFile(rawPath, record, os.ModePerm)
if err != nil {
return nil, errors.Wrap(err, "write temp file error")
}
defer os.Remove(rawPath)
// 2.转换pcm
pcmPath := path.Join(silkCachePath, tempName+".pcm")
cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath)
if err = cmd.Run(); err != nil {
return nil, errors.Wrap(err, "convert pcm file error")
}
defer os.Remove(pcmPath)
// 3. 转silk
silkPath := path.Join(silkCachePath, tempName+".silk")
cmd = exec.Command(getEncoderFilePath(), pcmPath, silkPath, "-rate", "24000", "-quiet", "-tencent")
if err = cmd.Run(); err != nil {
return nil, errors.Wrap(err, "convert silk file error")
}
if !useCache {
defer os.Remove(silkPath)
}
return ioutil.ReadFile(silkPath)
}
// FileExist 检查文件是否存在
func fileExist(path string) bool {
if runtime.GOOS == "windows" {
path = path + ".exe"
}
_, err := os.Lstat(path)
return !os.IsNotExist(err)
}

View File

@ -0,0 +1,15 @@
// +build !386,!arm64,!amd64,!arm
package codec
import "errors"
//Init 下载silk编码器
func Init() error {
return errors.New("Unsupport arch now")
}
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
return nil, errors.New("Unsupport arch now")
}

View File

@ -0,0 +1,15 @@
// +build !windows,!linux,!darwin
package codec
import "errors"
//Init 下载silk编码器
func Init() error {
return errors.New("not support now")
}
//EncodeToSilk 将音频编码为Silk
func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) {
return nil, errors.New("not support now")
}

View File

@ -12,6 +12,7 @@ import (
var json = jsoniter.ConfigCompatibleWithStandardLibrary var json = jsoniter.ConfigCompatibleWithStandardLibrary
//DefaultConfigWithComments 为go-cqhttp的默认配置文件
var DefaultConfigWithComments = ` var DefaultConfigWithComments = `
/* /*
go-cqhttp 默认配置文件 go-cqhttp 默认配置文件
@ -118,8 +119,8 @@ var DefaultConfigWithComments = `
use_sso_address: false use_sso_address: false
// 是否启用 DEBUG // 是否启用 DEBUG
debug: false debug: false
// 日志等级 // 日志等级 trace,debug,info,warn,error
log_level: "" log_level: "info"
// WebUi 设置 // WebUi 设置
web_ui: { web_ui: {
// 是否启用 WebUi // 是否启用 WebUi
@ -134,7 +135,11 @@ var DefaultConfigWithComments = `
} }
` `
type JsonConfig struct { //PasswordHash 存储QQ密码哈希供登录使用
var PasswordHash [16]byte
//JSONConfig Config对应的结构体
type JSONConfig struct {
Uin int64 `json:"uin"` Uin int64 `json:"uin"`
Password string `json:"password"` Password string `json:"password"`
EncryptPassword bool `json:"encrypt_password"` EncryptPassword bool `json:"encrypt_password"`
@ -153,39 +158,41 @@ type JsonConfig struct {
} `json:"_rate_limit"` } `json:"_rate_limit"`
IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"` IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"`
ForceFragmented bool `json:"force_fragmented"` ForceFragmented bool `json:"force_fragmented"`
FixUrl bool `json:"fix_url"` FixURL bool `json:"fix_url"`
ProxyRewrite string `json:"proxy_rewrite"` ProxyRewrite string `json:"proxy_rewrite"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"` HeartbeatInterval time.Duration `json:"heartbeat_interval"`
HttpConfig *GoCQHttpConfig `json:"http_config"` HTTPConfig *GoCQHTTPConfig `json:"http_config"`
WSConfig *GoCQWebsocketConfig `json:"ws_config"` WSConfig *GoCQWebSocketConfig `json:"ws_config"`
ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"` ReverseServers []*GoCQReverseWebSocketConfig `json:"ws_reverse_servers"`
PostMessageFormat string `json:"post_message_format"` PostMessageFormat string `json:"post_message_format"`
UseSSOAddress bool `json:"use_sso_address"` UseSSOAddress bool `json:"use_sso_address"`
Debug bool `json:"debug"` Debug bool `json:"debug"`
LogLevel string `json:"log_level"` LogLevel string `json:"log_level"`
WebUi *GoCqWebUi `json:"web_ui"` WebUI *GoCQWebUI `json:"web_ui"`
} }
type CQHttpApiConfig struct { //CQHTTPAPIConfig HTTPAPI对应的Config结构体
type CQHTTPAPIConfig struct {
Host string `json:"host"` Host string `json:"host"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
UseHttp bool `json:"use_http"` UseHTTP bool `json:"use_http"`
WSHost string `json:"ws_host"` WSHost string `json:"ws_host"`
WSPort uint16 `json:"ws_port"` WSPort uint16 `json:"ws_port"`
UseWS bool `json:"use_ws"` UseWS bool `json:"use_ws"`
WSReverseUrl string `json:"ws_reverse_url"` WSReverseURL string `json:"ws_reverse_url"`
WSReverseApiUrl string `json:"ws_reverse_api_url"` WSReverseAPIURL string `json:"ws_reverse_api_url"`
WSReverseEventUrl string `json:"ws_reverse_event_url"` WSReverseEventURL string `json:"ws_reverse_event_url"`
WSReverseReconnectInterval uint16 `json:"ws_reverse_reconnect_interval"` WSReverseReconnectInterval uint16 `json:"ws_reverse_reconnect_interval"`
WSReverseReconnectOnCode1000 bool `json:"ws_reverse_reconnect_on_code_1000"` WSReverseReconnectOnCode1000 bool `json:"ws_reverse_reconnect_on_code_1000"`
UseWsReverse bool `json:"use_ws_reverse"` UseWsReverse bool `json:"use_ws_reverse"`
PostUrl string `json:"post_url"` PostURL string `json:"post_url"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
Secret string `json:"secret"` Secret string `json:"secret"`
PostMessageFormat string `json:"post_message_format"` PostMessageFormat string `json:"post_message_format"`
} }
type GoCQHttpConfig struct { //GoCQHTTPConfig 正向HTTP对应config结构体
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"`
@ -193,29 +200,33 @@ type GoCQHttpConfig struct {
PostUrls map[string]string `json:"post_urls"` PostUrls map[string]string `json:"post_urls"`
} }
type GoCQWebsocketConfig struct { //GoCQWebSocketConfig 正向WebSocket对应Config结构体
type GoCQWebSocketConfig 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"`
} }
type GoCQReverseWebsocketConfig struct { //GoCQReverseWebSocketConfig 反向WebSocket对应Config结构体
type GoCQReverseWebSocketConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ReverseUrl string `json:"reverse_url"` ReverseURL string `json:"reverse_url"`
ReverseApiUrl string `json:"reverse_api_url"` ReverseAPIURL string `json:"reverse_api_url"`
ReverseEventUrl string `json:"reverse_event_url"` ReverseEventURL string `json:"reverse_event_url"`
ReverseReconnectInterval uint16 `json:"reverse_reconnect_interval"` ReverseReconnectInterval uint16 `json:"reverse_reconnect_interval"`
} }
type GoCqWebUi struct { //GoCQWebUI WebUI对应Config结构体
type GoCQWebUI struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Host string `json:"host"` Host string `json:"host"`
WebUiPort uint64 `json:"web_ui_port"` WebUIPort uint64 `json:"web_ui_port"`
WebInput bool `json:"web_input"` WebInput bool `json:"web_input"`
} }
func DefaultConfig() *JsonConfig { //DefaultConfig 返回一份默认配置对应结构体
return &JsonConfig{ func DefaultConfig() *JSONConfig {
return &JSONConfig{
EnableDB: true, EnableDB: true,
ReLogin: struct { ReLogin: struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@ -237,42 +248,43 @@ func DefaultConfig() *JsonConfig {
}, },
PostMessageFormat: "string", PostMessageFormat: "string",
ForceFragmented: false, ForceFragmented: false,
HttpConfig: &GoCQHttpConfig{ HTTPConfig: &GoCQHTTPConfig{
Enabled: true, Enabled: true,
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 5700, Port: 5700,
PostUrls: map[string]string{}, PostUrls: map[string]string{},
}, },
WSConfig: &GoCQWebsocketConfig{ WSConfig: &GoCQWebSocketConfig{
Enabled: true, Enabled: true,
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 6700, Port: 6700,
}, },
ReverseServers: []*GoCQReverseWebsocketConfig{ ReverseServers: []*GoCQReverseWebSocketConfig{
{ {
Enabled: false, Enabled: false,
ReverseUrl: "ws://you_websocket_universal.server", ReverseURL: "ws://you_websocket_universal.server",
ReverseApiUrl: "ws://you_websocket_api.server", ReverseAPIURL: "ws://you_websocket_api.server",
ReverseEventUrl: "ws://you_websocket_event.server", ReverseEventURL: "ws://you_websocket_event.server",
ReverseReconnectInterval: 3000, ReverseReconnectInterval: 3000,
}, },
}, },
WebUi: &GoCqWebUi{ WebUI: &GoCQWebUI{
Enabled: true, Enabled: true,
Host: "127.0.0.1", Host: "127.0.0.1",
WebInput: false, WebInput: false,
WebUiPort: 9999, WebUIPort: 9999,
}, },
} }
} }
func Load(p string) *JsonConfig { //LoadConfig 加载配置文件
func LoadConfig(p string) *JSONConfig {
if !PathExists(p) { if !PathExists(p) {
log.Warnf("尝试加载配置文件 %v 失败: 文件不存在", p) log.Warnf("尝试加载配置文件 %v 失败: 文件不存在", p)
return nil return nil
} }
var dat map[string]interface{} var dat map[string]interface{}
var c = JsonConfig{} var c = JSONConfig{}
err := hjson.Unmarshal([]byte(ReadAllText(p)), &dat) err := hjson.Unmarshal([]byte(ReadAllText(p)), &dat)
if err == nil { if err == nil {
b, _ := json.Marshal(dat) b, _ := json.Marshal(dat)
@ -287,7 +299,8 @@ func Load(p string) *JsonConfig {
return &c return &c
} }
func (c *JsonConfig) Save(p string) error { //Save 写入配置文件至path
func (c *JSONConfig) Save(path string) error {
data, err := hjson.MarshalWithOptions(c, hjson.EncoderOptions{ data, err := hjson.MarshalWithOptions(c, hjson.EncoderOptions{
Eol: "\n", Eol: "\n",
BracesSameLine: true, BracesSameLine: true,
@ -296,5 +309,5 @@ func (c *JsonConfig) Save(p string) error {
if err != nil { if err != nil {
return err return err
} }
return WriteAllText(p, string(data)) return WriteAllText(path, string(data))
} }

View File

@ -10,8 +10,14 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
//MSG 消息Map
type MSG map[string]interface{} type MSG map[string]interface{}
//Get 尝试从消息Map中取出key为s的值,若不存在则返回MSG{}
//
//若所给key对应的值的类型是global.MSG,则返回此值
//
//若所给key对应值的类型不是global.MSG,则返回MSG{"__str__": Val}
func (m MSG) Get(s string) MSG { func (m MSG) Get(s string) MSG {
if v, ok := m[s]; ok { if v, ok := m[s]; ok {
if msg, ok := v.(MSG); ok { if msg, ok := v.(MSG); ok {
@ -19,17 +25,25 @@ func (m MSG) Get(s string) MSG {
} }
return MSG{"__str__": v} // 用这个名字应该没问题吧 return MSG{"__str__": v} // 用这个名字应该没问题吧
} }
return MSG{} return nil // 不存在为空
} }
//String 将消息Map转化为String。若Map存在key "__str__",则返回此key对应的值,否则将输出整张消息Map对应的JSON字符串
func (m MSG) String() string { func (m MSG) String() string {
if m == nil {
return "" // 空 JSON
}
if str, ok := m["__str__"]; ok { if str, ok := m["__str__"]; ok {
if str == nil {
return "" // 空 JSON
}
return fmt.Sprint(str) return fmt.Sprint(str)
} }
str, _ := json.MarshalToString(m) str, _ := json.MarshalToString(m)
return str return str
} }
//Filter 定义了一个消息上报过滤接口
type Filter interface { type Filter interface {
Eval(payload MSG) bool Eval(payload MSG) bool
} }
@ -39,6 +53,7 @@ type operationNode struct {
filter Filter filter Filter
} }
//NotOperator 定义了过滤器中Not操作符
type NotOperator struct { type NotOperator struct {
operand Filter operand Filter
} }
@ -52,10 +67,12 @@ func notOperatorConstruct(argument gjson.Result) *NotOperator {
return op return op
} }
//Eval 对payload执行Not过滤
func (op *NotOperator) Eval(payload MSG) bool { func (op *NotOperator) Eval(payload MSG) bool {
return !op.operand.Eval(payload) return !op.operand.Eval(payload)
} }
//AndOperator 定义了过滤器中And操作符
type AndOperator struct { type AndOperator struct {
operands []operationNode operands []operationNode
} }
@ -91,6 +108,7 @@ func andOperatorConstruct(argument gjson.Result) *AndOperator {
return op return op
} }
//Eval 对payload执行And过滤
func (andOperator *AndOperator) Eval(payload MSG) bool { func (andOperator *AndOperator) Eval(payload MSG) bool {
res := true res := true
for _, operand := range andOperator.operands { for _, operand := range andOperator.operands {
@ -104,13 +122,14 @@ func (andOperator *AndOperator) Eval(payload MSG) bool {
res = res && operand.filter.Eval(val) res = res && operand.filter.Eval(val)
} }
if res == false { if !res {
break break
} }
} }
return res return res
} }
//OrOperator 定义了过滤器中Or操作符
type OrOperator struct { type OrOperator struct {
operands []Filter operands []Filter
} }
@ -127,17 +146,19 @@ func orOperatorConstruct(argument gjson.Result) *OrOperator {
return op return op
} }
//Eval 对payload执行Or过滤
func (op *OrOperator) Eval(payload MSG) bool { func (op *OrOperator) Eval(payload MSG) bool {
res := false res := false
for _, operand := range op.operands { for _, operand := range op.operands {
res = res || operand.Eval(payload) res = res || operand.Eval(payload)
if res == true { if res {
break break
} }
} }
return res return res
} }
//EqualOperator 定义了过滤器中Equal操作符
type EqualOperator struct { type EqualOperator struct {
operand string operand string
} }
@ -148,10 +169,12 @@ func equalOperatorConstruct(argument gjson.Result) *EqualOperator {
return op return op
} }
//Eval 对payload执行Equal过滤
func (op *EqualOperator) Eval(payload MSG) bool { func (op *EqualOperator) Eval(payload MSG) bool {
return payload.String() == op.operand return payload.String() == op.operand
} }
//NotEqualOperator 定义了过滤器中NotEqual操作符
type NotEqualOperator struct { type NotEqualOperator struct {
operand string operand string
} }
@ -162,10 +185,12 @@ func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator {
return op return op
} }
//Eval 对payload执行NotEqual过滤
func (op *NotEqualOperator) Eval(payload MSG) bool { func (op *NotEqualOperator) Eval(payload MSG) bool {
return !(payload.String() == op.operand) return !(payload.String() == op.operand)
} }
//InOperator 定义了过滤器中In操作符
type InOperator struct { type InOperator struct {
operandString string operandString string
operandArray []string operandArray []string
@ -188,6 +213,7 @@ func inOperatorConstruct(argument gjson.Result) *InOperator {
return op return op
} }
//Eval 对payload执行In过滤
func (op *InOperator) Eval(payload MSG) bool { func (op *InOperator) Eval(payload MSG) bool {
payloadStr := payload.String() payloadStr := payload.String()
if op.operandArray != nil { if op.operandArray != nil {
@ -201,6 +227,7 @@ func (op *InOperator) Eval(payload MSG) bool {
return strings.Contains(op.operandString, payloadStr) return strings.Contains(op.operandString, payloadStr)
} }
//ContainsOperator 定义了过滤器中Contains操作符
type ContainsOperator struct { type ContainsOperator struct {
operand string operand string
} }
@ -214,10 +241,12 @@ func containsOperatorConstruct(argument gjson.Result) *ContainsOperator {
return op return op
} }
//Eval 对payload执行Contains过滤
func (op *ContainsOperator) Eval(payload MSG) bool { func (op *ContainsOperator) Eval(payload MSG) bool {
return strings.Contains(payload.String(), op.operand) return strings.Contains(payload.String(), op.operand)
} }
//RegexOperator 定义了过滤器中Regex操作符
type RegexOperator struct { type RegexOperator struct {
regex *regexp.Regexp regex *regexp.Regexp
} }
@ -231,11 +260,13 @@ func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
return op return op
} }
//Eval 对payload执行RegexO过滤
func (op *RegexOperator) Eval(payload MSG) bool { func (op *RegexOperator) Eval(payload MSG) bool {
matched := op.regex.MatchString(payload.String()) matched := op.regex.MatchString(payload.String())
return matched return matched
} }
//Generate 根据给定操作符名opName及操作符参数argument创建一个过滤器实例
func Generate(opName string, argument gjson.Result) Filter { func Generate(opName string, argument gjson.Result) Filter {
switch opName { switch opName {
case "not": case "not":
@ -259,8 +290,10 @@ func Generate(opName string, argument gjson.Result) Filter {
} }
} }
//EventFilter 初始化一个nil过滤器
var EventFilter Filter = nil var EventFilter Filter = nil
//BootFilter 启动事件过滤器
func BootFilter() { func BootFilter() {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {

View File

@ -9,7 +9,6 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/kardianos/osext"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -21,75 +20,95 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/kardianos/osext"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var ( const (
IMAGE_PATH = path.Join("data", "images") //ImagePath go-cqhttp使用的图片缓存目录
IMAGE_PATH_OLD = path.Join("data", "image") ImagePath = "data/images"
VOICE_PATH = path.Join("data", "voices") //ImagePathOld 兼容旧版go-cqhtto使用的图片缓存目录
VOICE_PATH_OLD = path.Join("data", "record") ImagePathOld = "data/image"
VIDEO_PATH = path.Join("data", "videos") //VoicePath go-cqhttp使用的语音缓存目录
CACHE_PATH = path.Join("data", "cache") VoicePath = "data/voices"
//VoicePathOld 兼容旧版go-cqhtto使用的语音缓存目录
HEADER_AMR = []byte("#!AMR") VoicePathOld = "data/record"
HEADER_SILK = []byte("\x02#!SILK_V3") //VideoPath go-cqhttp使用的视频缓存目录
VideoPath = "data/videos"
ErrSyntax = errors.New("syntax error") //CachePath go-cqhttp使用的缓存目录
CachePath = "data/cache"
) )
var (
//ErrSyntax Path语法错误时返回的错误
ErrSyntax = errors.New("syntax error")
//HeaderAmr AMR文件头
HeaderAmr = []byte("#!AMR")
//HeaderSilk Silkv3文件头
HeaderSilk = []byte("\x02#!SILK_V3")
)
//PathExists 判断给定path是否存在
func PathExists(path string) bool { func PathExists(path string) bool {
_, err := os.Stat(path) _, err := os.Stat(path)
return err == nil || os.IsExist(err) return err == nil || os.IsExist(err)
} }
//ReadAllText 读取给定path对应文件无法读取时返回空值
func ReadAllText(path string) string { func ReadAllText(path string) string {
b, err := ioutil.ReadFile(path) b, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
log.Error(err)
return "" return ""
} }
return string(b) return string(b)
} }
//WriteAllText 将给定text写入给定path
func WriteAllText(path, text string) error { func WriteAllText(path, text string) error {
return ioutil.WriteFile(path, []byte(text), 0644) return ioutil.WriteFile(path, []byte(text), 0644)
} }
//Check 检测err是否为nil
func Check(err error) { func Check(err error) {
if err != nil { if err != nil {
log.Fatalf("遇到错误: %v", err) log.Fatalf("遇到错误: %v", err)
} }
} }
//IsAMRorSILK 判断给定文件是否为Amr或Silk格式
func IsAMRorSILK(b []byte) bool { func IsAMRorSILK(b []byte) bool {
return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK) return bytes.HasPrefix(b, HeaderAmr) || bytes.HasPrefix(b, HeaderSilk)
} }
func FindFile(f, cache, PATH string) (data []byte, err error) { //FindFile 从给定的File寻找文件并返回文件byte数组。File是一个合法的URL。Path为文件寻找位置。
//对于HTTP/HTTPS形式的URLCache为"1"或空时表示启用缓存
func FindFile(file, cache, PATH string) (data []byte, err error) {
data, err = nil, ErrSyntax data, err = nil, ErrSyntax
if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { if strings.HasPrefix(file, "http") || strings.HasPrefix(file, "https") {
if cache == "" { if cache == "" {
cache = "1" cache = "1"
} }
hash := md5.Sum([]byte(f)) hash := md5.Sum([]byte(file))
cacheFile := path.Join(CACHE_PATH, hex.EncodeToString(hash[:])+".cache") cacheFile := path.Join(CachePath, hex.EncodeToString(hash[:])+".cache")
if PathExists(cacheFile) && cache == "1" { if PathExists(cacheFile) && cache == "1" {
return ioutil.ReadFile(cacheFile) return ioutil.ReadFile(cacheFile)
} }
data, err = GetBytes(f) data, err = GetBytes(file)
_ = ioutil.WriteFile(cacheFile, data, 0644) _ = ioutil.WriteFile(cacheFile, data, 0644)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if strings.HasPrefix(f, "base64") { } else if strings.HasPrefix(file, "base64") {
data, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) data, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(file, "base64://", ""))
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if strings.HasPrefix(f, "file") { } else if strings.HasPrefix(file, "file") {
var fu *url.URL var fu *url.URL
fu, err = url.Parse(f) fu, err = url.Parse(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -100,8 +119,8 @@ func FindFile(f, cache, PATH string) (data []byte, err error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if PathExists(path.Join(PATH, f)) { } else if PathExists(path.Join(PATH, file)) {
data, err = ioutil.ReadFile(path.Join(PATH, f)) data, err = ioutil.ReadFile(path.Join(PATH, file))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -109,19 +128,20 @@ func FindFile(f, cache, PATH string) (data []byte, err error) {
return return
} }
//DelFile 删除一个给定path并返回删除结果
func DelFile(path string) bool { func DelFile(path string) bool {
err := os.Remove(path) err := os.Remove(path)
if err != nil { if err != nil {
// 删除失败 // 删除失败
log.Error(err) log.Error(err)
return false return false
} else {
// 删除成功
log.Info(path + "删除成功")
return true
} }
// 删除成功
log.Info(path + "删除成功")
return true
} }
//ReadAddrFile 从给定path中读取合法的IP地址与端口,每个IP地址以换行符"\n"作为分隔
func ReadAddrFile(path string) []*net.TCPAddr { func ReadAddrFile(path string) []*net.TCPAddr {
d, err := ioutil.ReadFile(path) d, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
@ -140,10 +160,12 @@ func ReadAddrFile(path string) []*net.TCPAddr {
return ret return ret
} }
//WriteCounter 写入量计算实例
type WriteCounter struct { type WriteCounter struct {
Total uint64 Total uint64
} }
//Write 方法将写入的byte长度追加至写入的总长度Total中
func (wc *WriteCounter) Write(p []byte) (int, error) { func (wc *WriteCounter) Write(p []byte) (int, error) {
n := len(p) n := len(p)
wc.Total += uint64(n) wc.Total += uint64(n)
@ -151,12 +173,13 @@ func (wc *WriteCounter) Write(p []byte) (int, error) {
return n, nil return n, nil
} }
func (wc WriteCounter) PrintProgress() { //PrintProgress 方法将打印当前的总写入量
func (wc *WriteCounter) PrintProgress() {
fmt.Printf("\r%s", strings.Repeat(" ", 35)) fmt.Printf("\r%s", strings.Repeat(" ", 35))
fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total)) fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total))
} }
// UpdateFromStream copy form getlantern/go-update //UpdateFromStream copy form getlantern/go-update
func UpdateFromStream(updateWith io.Reader) (err error, errRecover error) { func UpdateFromStream(updateWith io.Reader) (err error, errRecover error) {
updatePath, err := osext.Executable() updatePath, err := osext.Executable()
if err != nil { if err != nil {
@ -216,7 +239,7 @@ func UpdateFromStream(updateWith io.Reader) (err error, errRecover error) {
return return
} }
// move the new exectuable in to become the new program // move the new executable in to become the new program
err = os.Rename(newPath, updatePath) err = os.Rename(newPath, updatePath)
if err != nil { if err != nil {

153
global/log_hook.go Normal file
View File

@ -0,0 +1,153 @@
package global
import (
"fmt"
"github.com/sirupsen/logrus"
"io"
"os"
"path/filepath"
"reflect"
"sync"
)
type LocalHook struct {
lock *sync.Mutex
levels []logrus.Level // hook级别
formatter logrus.Formatter // 格式
path string // 写入path
writer io.Writer // io
}
// ref: logrus/hooks.go. impl Hook interface
func (hook *LocalHook) Levels() []logrus.Level {
if len(hook.levels) == 0 {
return logrus.AllLevels
}
return hook.levels
}
func (hook *LocalHook) ioWrite(entry *logrus.Entry) error {
log, err := hook.formatter.Format(entry)
if err != nil {
return err
}
_, err = hook.writer.Write(log)
if err != nil {
return err
}
return nil
}
func (hook *LocalHook) pathWrite(entry *logrus.Entry) error {
dir := filepath.Dir(hook.path)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
fd, err := os.OpenFile(hook.path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
defer fd.Close()
log, err := hook.formatter.Format(entry)
if err != nil {
return err
}
_, err = fd.Write(log)
return err
}
func (hook *LocalHook) Fire(entry *logrus.Entry) error {
hook.lock.Lock()
defer hook.lock.Unlock()
if hook.writer != nil {
return hook.ioWrite(entry)
}
if hook.path != "" {
return hook.pathWrite(entry)
}
return nil
}
func (hook *LocalHook) SetFormatter(formatter logrus.Formatter) {
hook.lock.Lock()
defer hook.lock.Unlock()
if formatter == nil {
// 用默认的
formatter = &logrus.TextFormatter{DisableColors: true}
} else {
switch f := formatter.(type) {
case *logrus.TextFormatter:
textFormatter := f
textFormatter.DisableColors = true
default:
// todo
}
}
logrus.SetFormatter(formatter)
hook.formatter = formatter
}
func (hook *LocalHook) SetWriter(writer io.Writer) {
hook.lock.Lock()
defer hook.lock.Unlock()
hook.writer = writer
}
func (hook *LocalHook) SetPath(path string) {
hook.lock.Lock()
defer hook.lock.Unlock()
hook.path = path
}
func NewLocalHook(args interface{}, formatter logrus.Formatter, levels ...logrus.Level) *LocalHook {
hook := &LocalHook{
lock: new(sync.Mutex),
}
hook.SetFormatter(formatter)
hook.levels = append(hook.levels, levels...)
switch arg := args.(type) {
case string:
hook.SetPath(arg)
case io.Writer:
hook.SetWriter(arg)
default:
panic(fmt.Sprintf("unsupported type: %v", reflect.TypeOf(args)))
}
return hook
}
func GetLogLevel(level string) []logrus.Level {
switch level {
case "trace":
return []logrus.Level{logrus.TraceLevel, logrus.DebugLevel,
logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel}
case "debug":
return []logrus.Level{logrus.DebugLevel, logrus.InfoLevel,
logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel}
case "info":
return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
case "warn":
return []logrus.Level{logrus.WarnLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.PanicLevel}
case "error":
return []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel,
logrus.PanicLevel}
default:
return []logrus.Level{logrus.InfoLevel, logrus.WarnLevel,
logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel}
}
}

View File

@ -5,11 +5,8 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"github.com/guonaihong/gout"
"github.com/pkg/errors"
"io" "io"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -18,12 +15,14 @@ import (
"sync" "sync"
"time" "time"
"github.com/guonaihong/gout"
"github.com/pkg/errors"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
var ( var (
client = &http.Client{ client = &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: func(request *http.Request) (u *url.URL, e error) { Proxy: func(request *http.Request) (u *url.URL, e error) {
if Proxy == "" { if Proxy == "" {
@ -31,30 +30,30 @@ var (
} }
return url.Parse(Proxy) return url.Parse(Proxy)
}, },
DialContext: (&net.Dialer{ ForceAttemptHTTP2: true,
Timeout: 30 * time.Second, MaxConnsPerHost: 0,
KeepAlive: 30 * time.Second, MaxIdleConns: 0,
}).DialContext, MaxIdleConnsPerHost: 999,
ForceAttemptHTTP2: true,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxConnsPerHost: 0,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 999,
}, },
} }
//Proxy 存储Config.proxy_rewrite,用于设置代理
Proxy string Proxy string
//ErrOverSize 响应主体过大时返回此错误
ErrOverSize = errors.New("oversize") ErrOverSize = errors.New("oversize")
//UserAgent HTTP请求时使用的UA
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66"
) )
//GetBytes 对给定URL发送Get请求返回响应主体
func GetBytes(url string) ([]byte, error) { func GetBytes(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header["User-Agent"] = []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Edg/83.0.478.61"} req.Header["User-Agent"] = []string{UserAgent}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -74,7 +73,8 @@ func GetBytes(url string) ([]byte, error) {
return body, nil return body, nil
} }
func DownloadFile(url, path string, limit int64) error { //DownloadFile 将给定URL对应的文件下载至给定Path
func DownloadFile(url, path string, limit int64, headers map[string]string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil { if err != nil {
return err return err
@ -84,6 +84,14 @@ func DownloadFile(url, path string, limit int64) error {
if err != nil { if err != nil {
return err return err
} }
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; !ok {
req.Header["User-Agent"] = []string{UserAgent}
}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
@ -99,9 +107,10 @@ func DownloadFile(url, path string, limit int64) error {
return nil return nil
} }
func DownloadFileMultiThreading(url, path string, limit int64, threadCount int) error { //DownloadFileMultiThreading 使用threadCount个线程将给定URL对应的文件下载至给定Path
func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, headers map[string]string) error {
if threadCount < 2 { if threadCount < 2 {
return DownloadFile(url, path, limit) return DownloadFile(url, path, limit, headers)
} }
type BlockMetaData struct { type BlockMetaData struct {
BeginOffset int64 BeginOffset int64
@ -128,6 +137,14 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int)
if err != nil { if err != nil {
return err return err
} }
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; !ok {
req.Header["User-Agent"] = []string{UserAgent}
}
req.Header.Set("range", "bytes=0-") req.Header.Set("range", "bytes=0-")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -150,9 +167,9 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int)
blockSize := func() int64 { blockSize := func() int64 {
if contentLength > 1024*1024 { if contentLength > 1024*1024 {
return (contentLength / int64(threadCount)) - 10 return (contentLength / int64(threadCount)) - 10
} else {
return contentLength
} }
return contentLength
}() }()
if blockSize == contentLength { if blockSize == contentLength {
return copyStream(resp.Body) return copyStream(resp.Body)
@ -171,7 +188,7 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int)
}) })
return nil return nil
} }
return errors.New("unknown status code.") return errors.New("unknown status code")
} }
// 下载分块 // 下载分块
downloadBlock := func(block *BlockMetaData) error { downloadBlock := func(block *BlockMetaData) error {
@ -184,6 +201,14 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int)
_, _ = file.Seek(block.BeginOffset, io.SeekStart) _, _ = file.Seek(block.BeginOffset, io.SeekStart)
writer := bufio.NewWriter(file) writer := bufio.NewWriter(file)
defer writer.Flush() defer writer.Flush()
for k, v := range headers {
req.Header.Set(k, v)
}
if _, ok := headers["User-Agent"]; ok {
req.Header["User-Agent"] = []string{UserAgent}
}
req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10)) req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10))
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -240,6 +265,7 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int)
return lastErr return lastErr
} }
//GetSliderTicket 通过给定的验证链接raw和id,获取验证结果Ticket
func GetSliderTicket(raw, id string) (string, error) { func GetSliderTicket(raw, id string) (string, error) {
var rsp string var rsp string
if err := gout.POST("https://api.shkong.com/gocqhttpapi/task").SetJSON(gout.H{ if err := gout.POST("https://api.shkong.com/gocqhttpapi/task").SetJSON(gout.H{
@ -255,6 +281,7 @@ func GetSliderTicket(raw, id string) (string, error) {
return g.Get("ticket").Str, nil return g.Get("ticket").Str, nil
} }
//QQMusicSongInfo 通过给定id在QQ音乐上查找曲目信息
func QQMusicSongInfo(id string) (gjson.Result, error) { func QQMusicSongInfo(id string) (gjson.Result, error) {
d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`) d, err := GetBytes(`https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:` + id + `},%22module%22:%22music.pf_song_detail_svr%22}}`)
if err != nil { if err != nil {
@ -263,6 +290,7 @@ func QQMusicSongInfo(id string) (gjson.Result, error) {
return gjson.ParseBytes(d).Get("songinfo.data"), nil return gjson.ParseBytes(d).Get("songinfo.data"), nil
} }
//NeteaseMusicSongInfo 通过给定id在wdd音乐上查找曲目信息
func NeteaseMusicSongInfo(id string) (gjson.Result, error) { func NeteaseMusicSongInfo(id string) (gjson.Result, error) {
d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id)) d, err := GetBytes(fmt.Sprintf("http://music.163.com/api/song/detail/?id=%s&ids=%%5B%s%%5D", id, id))
if err != nil { if err != nil {

View File

@ -21,6 +21,15 @@ var falseSet = map[string]struct{}{
"0": {}, "0": {},
} }
//EnsureBool 判断给定的p是否可表示为合法Bool类型,否则返回defaultVal
//
//支持的合法类型有
//
//type bool
//
//type gjson.True or gjson.False
//
//type string "true","yes","1" or "false","no","0" (case insensitive)
func EnsureBool(p interface{}, defaultVal bool) bool { func EnsureBool(p interface{}, defaultVal bool) bool {
var str string var str string
if b, ok := p.(bool); ok { if b, ok := p.(bool); ok {
@ -54,9 +63,13 @@ func EnsureBool(p interface{}, defaultVal bool) bool {
} }
// VersionNameCompare 检查版本名是否需要更新, 仅适用于 go-cqhttp 的版本命名规则 // VersionNameCompare 检查版本名是否需要更新, 仅适用于 go-cqhttp 的版本命名规则
//
// 例: v0.9.29-fix2 == v0.9.29-fix2 -> false // 例: v0.9.29-fix2 == v0.9.29-fix2 -> false
//
// v0.9.29-fix1 < v0.9.29-fix2 -> true // v0.9.29-fix1 < v0.9.29-fix2 -> true
//
// v0.9.29-fix2 > v0.9.29-fix1 -> false // v0.9.29-fix2 > v0.9.29-fix1 -> false
//
// v0.9.29-fix2 < v0.9.30 -> true // v0.9.29-fix2 < v0.9.30 -> true
func VersionNameCompare(current, remote string) bool { func VersionNameCompare(current, remote string) bool {
sp := regexp.MustCompile(`[0-9]\d*`) sp := regexp.MustCompile(`[0-9]\d*`)
@ -72,8 +85,9 @@ func VersionNameCompare(current, remote string) bool {
return len(cur) < len(re) return len(cur) < len(re)
} }
func SplitUrl(s string) []string { //SplitURL 将给定URL字符串分割为两部分用于URL预处理防止风控
reg := regexp.MustCompile(`[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?`) func SplitURL(s string) []string {
reg := regexp.MustCompile(`(?i)[a-z\d][-a-z\d]{0,62}(\.[a-z\d][-a-z\d]{0,62})+\.?`)
idx := reg.FindAllStringIndex(s, -1) idx := reg.FindAllStringIndex(s, -1)
if len(idx) == 0 { if len(idx) == 0 {
return []string{s} return []string{s}

View File

@ -9,13 +9,15 @@ import (
var limiter *rate.Limiter var limiter *rate.Limiter
var limitEnable = false var limitEnable = false
//RateLimit 执行API调用速率限制
func RateLimit(ctx context.Context) { func RateLimit(ctx context.Context) {
if limitEnable { if limitEnable {
_ = limiter.Wait(ctx) _ = limiter.Wait(ctx)
} }
} }
func InitLimiter(r float64, b int) { //InitLimiter 初始化速率限制器
func InitLimiter(frequency float64, bucketSize int) {
limitEnable = true limitEnable = true
limiter = rate.NewLimiter(rate.Limit(r), b) limiter = rate.NewLimiter(rate.Limit(frequency), bucketSize)
} }

10
go.mod
View File

@ -3,25 +3,25 @@ module github.com/Mrs4s/go-cqhttp
go 1.15 go 1.15
require ( require (
github.com/Mrs4s/MiraiGo v0.0.0-20210104171920-510924ba979c github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/gin-contrib/pprof v1.3.0 github.com/gin-contrib/pprof v1.3.0
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.4 github.com/guonaihong/gout v0.1.4
github.com/hjson/hjson-go v3.1.0+incompatible github.com/hjson/hjson-go v3.1.0+incompatible
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/json-iterator/go v1.1.10 github.com/json-iterator/go v1.1.10
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lestrrat-go/strftime v1.0.3 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
github.com/tidwall/gjson v1.6.7 github.com/tidwall/gjson v1.6.7
github.com/wdvxdr1123/go-silk v0.0.0-20201210140933-bcdbcb2f1093
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
) )

29
go.sum
View File

@ -1,9 +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-20210104164708-e94ea61cc8e9 h1:jzaBgDPLKRuJxQO9UT/2vCFL0ScCO9IThRCa7ViMbGE= github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e h1:SnN+nyRdqN7sULnHUWCofP+Jxs3VJN/y8AlMpcz0nbk=
github.com/Mrs4s/MiraiGo v0.0.0-20210104164708-e94ea61cc8e9/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo= github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e/go.mod h1:yhqA0NyKxUf7I/0HR/1OMchveFggX8wde04gqdGrNfU=
github.com/Mrs4s/MiraiGo v0.0.0-20210104171920-510924ba979c h1:0O4TonUV7ZTawM+lwy0pNTYqngpbCrZGtYARGRvMQLw=
github.com/Mrs4s/MiraiGo v0.0.0-20210104171920-510924ba979c/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo=
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=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -57,6 +55,7 @@ github.com/guonaihong/gout v0.1.4/go.mod h1:0rFYAYyzbcxEg11eY2qUbffJs7hHRPeugAnl
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw= github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -68,8 +67,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.3 h1:qqOPU7y+TM8Y803I8fG9c/DyKG3xH/xkng6keC1015Q= github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
github.com/lestrrat-go/strftime v1.0.3/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
@ -86,8 +85,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@ -100,10 +97,8 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk=
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA=
github.com/tidwall/gjson v1.6.3/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
github.com/tidwall/gjson v1.6.7 h1:Mb1M9HZCRWEcXQ8ieJo7auYyyiSux6w9XN3AdTpxJrE= github.com/tidwall/gjson v1.6.7 h1:Mb1M9HZCRWEcXQ8ieJo7auYyyiSux6w9XN3AdTpxJrE=
github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
@ -112,13 +107,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/wdvxdr1123/go-silk v0.0.0-20201210140933-bcdbcb2f1093 h1:t38EBwI2hFJz1sQJ402VqzdA3eMidkY1c3w/8z0SftA=
github.com/wdvxdr1123/go-silk v0.0.0-20201210140933-bcdbcb2f1093/go.mod h1:5q9LFlBr+yX/J8Jd/9wHdXwkkjFkNyQIS7kX2Lgx/Zs=
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if55Fu3HOwbfcaQlJ27p3oeJU2JZqoeT3ITJQk= github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if55Fu3HOwbfcaQlJ27p3oeJU2JZqoeT3ITJQk=
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189/go.mod h1:rIrm5geMiBhPQkdfUm8gDFi/WiHneOp1i9KjmJqc+9I= github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189/go.mod h1:rIrm5geMiBhPQkdfUm8gDFi/WiHneOp1i9KjmJqc+9I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -128,7 +120,6 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -140,10 +131,11 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
@ -154,7 +146,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

343
main.go
View File

@ -2,8 +2,11 @@ package main
import ( import (
"bufio" "bufio"
"crypto/aes"
"crypto/md5" "crypto/md5"
"crypto/sha1"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -16,12 +19,14 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/Mrs4s/go-cqhttp/server" "github.com/Mrs4s/go-cqhttp/server"
"github.com/guonaihong/gout" "github.com/guonaihong/gout"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/pbkdf2"
"golang.org/x/term"
"github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/client"
@ -29,64 +34,37 @@ import (
"github.com/Mrs4s/go-cqhttp/global" "github.com/Mrs4s/go-cqhttp/global"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
rotatelogs "github.com/lestrrat-go/file-rotatelogs" rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
easy "github.com/t-tomalak/logrus-easy-formatter" easy "github.com/t-tomalak/logrus-easy-formatter"
) )
var json = jsoniter.ConfigCompatibleWithStandardLibrary var json = jsoniter.ConfigCompatibleWithStandardLibrary
var conf *global.JSONConfig
var isFastStart = false
func init() { func init() {
log.SetFormatter(&easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%time%] [%lvl%]: %msg% \n",
})
w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotatelogs.WithRotationTime(time.Hour*24))
if err == nil {
log.SetOutput(io.MultiWriter(os.Stderr, w))
}
if !global.PathExists(global.IMAGE_PATH) {
if err := os.MkdirAll(global.IMAGE_PATH, 0755); err != nil {
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.hjson 文件.") log.Warn("警告: 该操作会删除 cqhttp.json 并覆盖 config.hjson 文件.")
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
conf := global.CQHttpApiConfig{} conf := global.CQHTTPAPIConfig{}
if err := json.Unmarshal([]byte(global.ReadAllText("cqhttp.json")), &conf); err != nil { if err := json.Unmarshal([]byte(global.ReadAllText("cqhttp.json")), &conf); err != nil {
log.Fatalf("读取文件 cqhttp.json 失败: %v", err) log.Fatalf("读取文件 cqhttp.json 失败: %v", err)
} }
goConf := global.DefaultConfig() goConf := global.DefaultConfig()
goConf.AccessToken = conf.AccessToken goConf.AccessToken = conf.AccessToken
goConf.HttpConfig.Host = conf.Host goConf.HTTPConfig.Host = conf.Host
goConf.HttpConfig.Port = conf.Port goConf.HTTPConfig.Port = conf.Port
goConf.WSConfig.Host = conf.WSHost goConf.WSConfig.Host = conf.WSHost
goConf.WSConfig.Port = conf.WSPort goConf.WSConfig.Port = conf.WSPort
if conf.PostUrl != "" { if conf.PostURL != "" {
goConf.HttpConfig.PostUrls[conf.PostUrl] = conf.Secret goConf.HTTPConfig.PostUrls[conf.PostURL] = conf.Secret
} }
if conf.UseWsReverse { if conf.UseWsReverse {
goConf.ReverseServers[0].Enabled = true goConf.ReverseServers[0].Enabled = true
goConf.ReverseServers[0].ReverseUrl = conf.WSReverseUrl goConf.ReverseServers[0].ReverseURL = conf.WSReverseURL
goConf.ReverseServers[0].ReverseApiUrl = conf.WSReverseApiUrl goConf.ReverseServers[0].ReverseAPIURL = conf.WSReverseAPIURL
goConf.ReverseServers[0].ReverseEventUrl = conf.WSReverseEventUrl goConf.ReverseServers[0].ReverseEventURL = conf.WSReverseEventURL
goConf.ReverseServers[0].ReverseReconnectInterval = conf.WSReverseReconnectInterval goConf.ReverseServers[0].ReverseReconnectInterval = conf.WSReverseReconnectInterval
} }
if err := goConf.Save("config.hjson"); err != nil { if err := goConf.Save("config.hjson"); err != nil {
@ -94,11 +72,54 @@ func init() {
} }
_ = os.Remove("cqhttp.json") _ = os.Remove("cqhttp.json")
} }
conf = getConfig()
if conf == nil {
os.Exit(1)
}
logFormatter := &easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%time%] [%lvl%]: %msg% \n",
}
w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotatelogs.WithRotationTime(time.Hour*24))
if err != nil {
log.Errorf("rotatelogs init err: %v", err)
panic(err)
}
// 在debug模式下,将在标准输出中打印当前执行行数
if conf.Debug {
log.SetReportCaller(true)
}
log.AddHook(global.NewLocalHook(w, logFormatter, global.GetLogLevel(conf.LogLevel)...))
if !global.PathExists(global.ImagePath) {
if err := os.MkdirAll(global.ImagePath, 0755); err != nil {
log.Fatalf("创建图片缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.VoicePath) {
if err := os.MkdirAll(global.VoicePath, 0755); err != nil {
log.Fatalf("创建语音缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.VideoPath) {
if err := os.MkdirAll(global.VideoPath, 0755); err != nil {
log.Fatalf("创建视频缓存文件夹失败: %v", err)
}
}
if !global.PathExists(global.CachePath) {
if err := os.MkdirAll(global.CachePath, 0755); err != nil {
log.Fatalf("创建发送图片缓存文件夹失败: %v", err)
}
}
} }
func main() { func main() {
var byteKey []byte var byteKey []byte
var isFastStart = false
arg := os.Args arg := os.Args
if len(arg) > 1 { if len(arg) > 1 {
for i := range arg { for i := range arg {
@ -120,91 +141,12 @@ func main() {
} }
} }
var conf *global.JsonConfig
if global.PathExists("config.json") {
conf = global.Load("config.json")
_ = conf.Save("config.hjson")
_ = os.Remove("config.json")
} else if os.Getenv("UIN") != "" {
log.Infof("将从环境变量加载配置.")
uin, _ := strconv.ParseInt(os.Getenv("UIN"), 10, 64)
pwd := os.Getenv("PASS")
post := os.Getenv("HTTP_POST")
conf = &global.JsonConfig{
Uin: uin,
Password: pwd,
HttpConfig: &global.GoCQHttpConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 5700,
PostUrls: map[string]string{},
},
WSConfig: &global.GoCQWebsocketConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 6700,
},
PostMessageFormat: "string",
Debug: os.Getenv("DEBUG") == "true",
}
if post != "" {
conf.HttpConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
}
} else {
conf = global.Load("config.hjson")
}
if conf == nil {
err := global.WriteAllText("config.hjson", global.DefaultConfigWithComments)
if err != nil {
log.Fatalf("创建默认配置文件时出现错误: %v", err)
return
}
log.Infof("默认配置文件已生成, 请编辑 config.hjson 后重启程序.")
time.Sleep(time.Second * 5)
return
}
if conf.Uin == 0 || (conf.Password == "" && conf.PasswordEncrypted == "") { if conf.Uin == 0 || (conf.Password == "" && conf.PasswordEncrypted == "") {
log.Warnf("请修改 config.hjson 以添加账号密码.") log.Warnf("请修改 config.hjson 以添加账号密码.")
time.Sleep(time.Second * 5) if !isFastStart {
return time.Sleep(time.Second * 5)
}
// log classified by level
// Collect all records up to the specified level (default level: warn)
logLevel := conf.LogLevel
if logLevel != "" {
date := time.Now().Format("2006-01-02")
var logPathMap lfshook.PathMap
switch conf.LogLevel {
case "warn":
logPathMap = lfshook.PathMap{
log.WarnLevel: path.Join("logs", date+"-warn.log"),
log.ErrorLevel: path.Join("logs", date+"-warn.log"),
log.FatalLevel: path.Join("logs", date+"-warn.log"),
log.PanicLevel: path.Join("logs", date+"-warn.log"),
}
case "error":
logPathMap = lfshook.PathMap{
log.ErrorLevel: path.Join("logs", date+"-error.log"),
log.FatalLevel: path.Join("logs", date+"-error.log"),
log.PanicLevel: path.Join("logs", date+"-error.log"),
}
default:
logPathMap = lfshook.PathMap{
log.WarnLevel: path.Join("logs", date+"-warn.log"),
log.ErrorLevel: path.Join("logs", date+"-warn.log"),
log.FatalLevel: path.Join("logs", date+"-warn.log"),
log.PanicLevel: path.Join("logs", date+"-warn.log"),
}
} }
return
log.AddHook(lfshook.NewHook(
logPathMap,
&easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%time%] [%lvl%]: %msg% \n",
},
))
} }
log.Info("当前版本:", coolq.Version) log.Info("当前版本:", coolq.Version)
@ -213,7 +155,7 @@ func main() {
log.Warnf("已开启Debug模式.") log.Warnf("已开启Debug模式.")
log.Debugf("开发交流群: 192548878") log.Debugf("开发交流群: 192548878")
server.Debug = true server.Debug = true
if conf.WebUi == nil || !conf.WebUi.Enabled { if conf.WebUI == nil || !conf.WebUI.Enabled {
log.Warnf("警告: 在Debug模式下未启用WebUi服务, 将无法进行性能分析.") log.Warnf("警告: 在Debug模式下未启用WebUi服务, 将无法进行性能分析.")
} }
} }
@ -231,15 +173,11 @@ func main() {
} }
if conf.EncryptPassword && conf.PasswordEncrypted == "" { if conf.EncryptPassword && conf.PasswordEncrypted == "" {
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)") log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
byteKey, _ := terminal.ReadPassword(int(os.Stdin.Fd())) byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
key := md5.Sum(byteKey) global.PasswordHash = md5.Sum([]byte(conf.Password))
if encrypted := EncryptPwd(conf.Password, key[:]); encrypted != "" { conf.Password = ""
conf.Password = "" conf.PasswordEncrypted = "AES:" + PasswordHashEncrypt(global.PasswordHash[:], byteKey)
conf.PasswordEncrypted = encrypted _ = conf.Save("config.hjson")
_ = conf.Save("config.hjson")
} else {
log.Warnf("加密时出现问题.")
}
} }
if conf.PasswordEncrypted != "" { if conf.PasswordEncrypted != "" {
if len(byteKey) == 0 { if len(byteKey) == 0 {
@ -255,13 +193,30 @@ func main() {
os.Exit(0) os.Exit(0)
} }
}() }()
byteKey, _ = terminal.ReadPassword(int(os.Stdin.Fd())) byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
cancel <- struct{}{} cancel <- struct{}{}
} else { } else {
log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.") log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
} }
key := md5.Sum(byteKey)
conf.Password = DecryptPwd(conf.PasswordEncrypted, key[:]) //升级客户端密码加密方案MD5+TEA 加密密码 -> PBKDF2+AES 加密 MD5
//升级后的 PasswordEncrypted 字符串以"AES:"开始,其后为 Hex 编码的16字节加密 MD5
if !strings.HasPrefix(conf.PasswordEncrypted, "AES:") {
password := OldPasswordDecrypt(conf.PasswordEncrypted, byteKey)
passwordHash := md5.Sum([]byte(password))
newPasswordHash := PasswordHashEncrypt(passwordHash[:], byteKey)
conf.PasswordEncrypted = "AES:" + newPasswordHash
_ = conf.Save("config.hjson")
log.Debug("密码加密方案升级完成")
}
ph, err := PasswordHashDecrypt(conf.PasswordEncrypted[4:], byteKey)
if err != nil {
log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
}
copy(global.PasswordHash[:], ph)
} else {
global.PasswordHash = md5.Sum([]byte(conf.Password))
} }
if !isFastStart { if !isFastStart {
log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.") log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
@ -281,7 +236,7 @@ func main() {
} }
return "未知" return "未知"
}()) }())
cli := client.NewClient(conf.Uin, conf.Password) cli := client.NewClientMd5(conf.Uin, global.PasswordHash)
cli.OnLog(func(c *client.QQClient, e *client.LogEvent) { cli.OnLog(func(c *client.QQClient, e *client.LogEvent) {
switch e.Type { switch e.Type {
case "INFO": case "INFO":
@ -308,26 +263,26 @@ func main() {
log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ") log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
return true return true
}) })
if conf.WebUi == nil { if conf.WebUI == nil {
conf.WebUi = &global.GoCqWebUi{ conf.WebUI = &global.GoCQWebUI{
Enabled: true, Enabled: true,
WebInput: false, WebInput: false,
Host: "0.0.0.0", Host: "0.0.0.0",
WebUiPort: 9999, WebUIPort: 9999,
} }
} }
if conf.WebUi.WebUiPort <= 0 { if conf.WebUI.WebUIPort <= 0 {
conf.WebUi.WebUiPort = 9999 conf.WebUI.WebUIPort = 9999
} }
if conf.WebUi.Host == "" { if conf.WebUI.Host == "" {
conf.WebUi.Host = "127.0.0.1" conf.WebUI.Host = "127.0.0.1"
} }
global.Proxy = conf.ProxyRewrite global.Proxy = conf.ProxyRewrite
b := server.WebServer.Run(fmt.Sprintf("%s:%d", conf.WebUi.Host, conf.WebUi.WebUiPort), cli) b := server.WebServer.Run(fmt.Sprintf("%s:%d", conf.WebUI.Host, conf.WebUI.WebUIPort), cli)
c := server.Console c := server.Console
r := server.Restart r := server.Restart
go checkUpdate() go checkUpdate()
signal.Notify(c, os.Interrupt, os.Kill) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
select { select {
case <-c: case <-c:
b.Release() b.Release()
@ -338,25 +293,50 @@ func main() {
} }
} }
func EncryptPwd(pwd string, key []byte) string { // PasswordHashEncrypt 使用key加密给定passwordHash
tea := binary.NewTeaCipher(key) func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
if tea == nil { if len(passwordHash) != 16 {
return "" panic("密码加密参数错误")
} }
return base64.StdEncoding.EncodeToString(tea.Encrypt([]byte(pwd)))
key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
cipher, _ := aes.NewCipher(key)
result := make([]byte, 16)
cipher.Encrypt(result, passwordHash)
return hex.EncodeToString(result)
} }
func DecryptPwd(ePwd string, key []byte) string { // PasswordHashDecrypt 使用key解密给定passwordHash
func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, error) {
ciphertext, err := hex.DecodeString(encryptedPasswordHash)
if err != nil {
return nil, err
}
key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
cipher, _ := aes.NewCipher(key)
result := make([]byte, 16)
cipher.Decrypt(result, ciphertext)
return result, nil
}
// OldPasswordDecrypt 使用key解密老password仅供兼容使用
func OldPasswordDecrypt(encryptedPassword string, key []byte) string {
defer func() { defer func() {
if pan := recover(); pan != nil { if pan := recover(); pan != nil {
log.Fatalf("密码解密失败: %v", pan) log.Fatalf("密码解密失败: %v", pan)
} }
}() }()
encrypted, err := base64.StdEncoding.DecodeString(ePwd) encKey := md5.Sum(key)
encrypted, err := base64.StdEncoding.DecodeString(encryptedPassword)
if err != nil { if err != nil {
panic(err) panic(err)
} }
tea := binary.NewTeaCipher(key) tea := binary.NewTeaCipher(encKey[:])
if tea == nil { if tea == nil {
panic("密钥错误") panic("密钥错误")
} }
@ -387,7 +367,7 @@ func checkUpdate() {
log.Infof("检查更新完成. 当前已运行最新版本.") log.Infof("检查更新完成. 当前已运行最新版本.")
} }
func selfUpdate(imageUrl string) { func selfUpdate(imageURL string) {
console := bufio.NewReader(os.Stdin) console := bufio.NewReader(os.Stdin)
readLine := func() (str string) { readLine := func() (str string) {
str, _ = console.ReadString('\n') str, _ = console.ReadString('\n')
@ -415,8 +395,8 @@ func selfUpdate(imageUrl string) {
url := fmt.Sprintf( url := fmt.Sprintf(
"%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp-%v-%v-%v", "%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp-%v-%v-%v",
func() string { func() string {
if imageUrl != "" { if imageURL != "" {
return imageUrl return imageURL
} }
return "https://github.com" return "https://github.com"
}(), }(),
@ -458,7 +438,7 @@ func selfUpdate(imageUrl string) {
} }
func restart(Args []string) { func restart(Args []string) {
cmd := &exec.Cmd{} var cmd *exec.Cmd
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
file, err := exec.LookPath(Args[0]) file, err := exec.LookPath(Args[0])
if err != nil { if err != nil {
@ -485,5 +465,54 @@ func restart(Args []string) {
Stdout: os.Stdout, Stdout: os.Stdout,
} }
} }
cmd.Start() _ = cmd.Start()
}
func getConfig() *global.JSONConfig {
var conf *global.JSONConfig
if global.PathExists("config.json") {
conf = global.LoadConfig("config.json")
_ = conf.Save("config.hjson")
_ = os.Remove("config.json")
} else if os.Getenv("UIN") != "" {
log.Infof("将从环境变量加载配置.")
uin, _ := strconv.ParseInt(os.Getenv("UIN"), 10, 64)
pwd := os.Getenv("PASS")
post := os.Getenv("HTTP_POST")
conf = &global.JSONConfig{
Uin: uin,
Password: pwd,
HTTPConfig: &global.GoCQHTTPConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 5700,
PostUrls: map[string]string{},
},
WSConfig: &global.GoCQWebSocketConfig{
Enabled: true,
Host: "0.0.0.0",
Port: 6700,
},
PostMessageFormat: "string",
Debug: os.Getenv("DEBUG") == "true",
}
if post != "" {
conf.HTTPConfig.PostUrls[post] = os.Getenv("HTTP_SECRET")
}
} else {
conf = global.LoadConfig("config.hjson")
}
if conf == nil {
err := global.WriteAllText("config.hjson", global.DefaultConfigWithComments)
if err != nil {
log.Fatalf("创建默认配置文件时出现错误: %v", err)
return nil
}
log.Infof("默认配置文件已生成, 请编辑 config.hjson 后重启程序.")
if !isFastStart {
time.Sleep(time.Second * 5)
}
return nil
}
return conf
} }

View File

@ -5,8 +5,6 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/gin-contrib/pprof"
"image" "image"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -14,8 +12,12 @@ import (
"os/signal" "os/signal"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/gin-contrib/pprof"
"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"
@ -34,13 +36,13 @@ var Console = make(chan os.Signal, 1)
var Restart = make(chan struct{}, 1) var Restart = make(chan struct{}, 1)
var JsonConfig *global.JsonConfig var JSONConfig *global.JSONConfig
type webServer struct { type webServer struct {
engine *gin.Engine engine *gin.Engine
bot *coolq.CQBot bot *coolq.CQBot
Cli *client.QQClient Cli *client.QQClient
Conf *global.JsonConfig //old config Conf *global.JSONConfig //old config
Console *bufio.Reader Console *bufio.Reader
} }
@ -68,7 +70,7 @@ func Failed(code int, msg string) coolq.MSG {
func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot { func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot {
s.Cli = cli s.Cli = cli
s.Conf = GetConf() s.Conf = GetConf()
JsonConfig = s.Conf JSONConfig = s.Conf
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
s.engine = gin.New() s.engine = gin.New()
@ -79,7 +81,7 @@ func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot {
go func() { go func() {
//开启端口监听 //开启端口监听
if s.Conf.WebUi != nil && s.Conf.WebUi.Enabled { if s.Conf.WebUI != nil && s.Conf.WebUI.Enabled {
if Debug { if Debug {
pprof.Register(s.engine) pprof.Register(s.engine)
log.Debugf("pprof 性能分析服务已启动在 http://%v/debug/pprof, 如果有任何性能问题请下载报告并提交给开发者", addr) log.Debugf("pprof 性能分析服务已启动在 http://%v/debug/pprof, 如果有任何性能问题请下载报告并提交给开发者", addr)
@ -91,14 +93,14 @@ func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot {
log.Error(err) log.Error(err)
log.Infof("请检查端口是否被占用.") log.Infof("请检查端口是否被占用.")
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c <-c
os.Exit(1) os.Exit(1)
} }
} else { } else {
//关闭端口监听 //关闭端口监听
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c <-c
os.Exit(1) os.Exit(1)
} }
@ -155,13 +157,17 @@ func (s *webServer) Dologin() {
os.Exit(0) os.Exit(0)
} }
rsp, err = cli.SubmitTicket(ticket) rsp, err = cli.SubmitTicket(ticket)
if err != nil {
log.Warnf("错误: " + err.Error())
os.Exit(0)
}
continue continue
case client.NeedCaptcha: case client.NeedCaptcha:
_ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644) _ = 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)
if conf.WebUi != nil && conf.WebUi.WebInput { if conf.WebUI != nil && conf.WebUI.WebInput {
log.Warnf("请输入验证码 (captcha.jpg) (http://%s:%d/admin/do_web_write 输入)", conf.WebUi.Host, conf.WebUi.WebUiPort) log.Warnf("请输入验证码 (captcha.jpg) (http://%s:%d/admin/do_web_write 输入)", conf.WebUI.Host, conf.WebUI.WebUIPort)
text = <-WebInput text = <-WebInput
} else { } else {
log.Warn("请输入验证码 (captcha.jpg) (Enter 提交)") log.Warn("请输入验证码 (captcha.jpg) (Enter 提交)")
@ -206,8 +212,8 @@ func (s *webServer) Dologin() {
return return
case client.UnsafeDeviceError: case client.UnsafeDeviceError:
log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
if conf.WebUi != nil && conf.WebUi.WebInput { if conf.WebUI != nil && conf.WebUI.WebInput {
log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUi.Host, conf.WebUi.WebUiPort) log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUI.Host, conf.WebUI.WebUIPort)
text = <-WebInput text = <-WebInput
} else { } else {
log.Infof("按 Enter 继续....") log.Infof("按 Enter 继续....")
@ -260,12 +266,14 @@ func (s *webServer) Dologin() {
global.BootFilter() global.BootFilter()
global.InitCodec() global.InitCodec()
coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode
coolq.SplitUrl = conf.FixUrl coolq.SplitUrl = conf.FixURL
coolq.ForceFragmented = conf.ForceFragmented coolq.ForceFragmented = conf.ForceFragmented
log.Info("资源初始化完成, 开始处理信息.") log.Info("资源初始化完成, 开始处理信息.")
log.Info("アトリは、高性能ですから!") log.Info("アトリは、高性能ですから!")
cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) {
if conf.ReLogin.Enabled { if conf.ReLogin.Enabled {
conf.ReLogin.Enabled = false
defer func() { conf.ReLogin.Enabled = true }()
var times uint = 1 var times uint = 1
for { for {
if cli.Online { if cli.Online {
@ -293,6 +301,9 @@ func (s *webServer) Dologin() {
log.Fatalf("重连失败: 设备锁") log.Fatalf("重连失败: 设备锁")
default: default:
log.Errorf("重连失败: %v", rsp.ErrorMessage) log.Errorf("重连失败: %v", rsp.ErrorMessage)
if strings.Contains(rsp.ErrorMessage, "冻结") {
log.Fatalf("账号被冻结, 放弃重连")
}
cli.Disconnect() cli.Disconnect()
continue continue
} }
@ -318,11 +329,11 @@ func (s *webServer) admin(c *gin.Context) {
} }
// 获取当前配置文件信息 // 获取当前配置文件信息
func GetConf() *global.JsonConfig { func GetConf() *global.JSONConfig {
if JsonConfig != nil { if JSONConfig != nil {
return JsonConfig return JSONConfig
} }
conf := global.Load("config.hjson") conf := global.LoadConfig("config.hjson")
return conf return conf
} }
@ -380,7 +391,7 @@ func AuthMiddleWare() gin.HandlerFunc {
} }
func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin
JsonConfig = nil JSONConfig = nil
conf := GetConf() conf := GetConf()
OldConf := s.Conf OldConf := s.Conf
cli := client.NewClient(conf.Uin, conf.Password) cli := client.NewClient(conf.Uin, conf.Password)
@ -419,7 +430,7 @@ func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin
s.Cli = cli s.Cli = cli
s.Dologin() s.Dologin()
//关闭之前的 server //关闭之前的 server
if OldConf.HttpConfig != nil && OldConf.HttpConfig.Enabled { if OldConf.HTTPConfig != nil && OldConf.HTTPConfig.Enabled {
HttpServer.ShutDown() HttpServer.ShutDown()
} }
//if OldConf.WSConfig != nil && OldConf.WSConfig.Enabled { //if OldConf.WSConfig != nil && OldConf.WSConfig.Enabled {
@ -432,31 +443,31 @@ func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin
func (s *webServer) UpServer() { func (s *webServer) UpServer() {
conf := GetConf() conf := GetConf()
if conf.HttpConfig != nil && conf.HttpConfig.Enabled { if conf.HTTPConfig != nil && conf.HTTPConfig.Enabled {
go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot) go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HTTPConfig.Host, conf.HTTPConfig.Port), conf.AccessToken, s.bot)
for k, v := range conf.HttpConfig.PostUrls { for k, v := range conf.HTTPConfig.PostUrls {
NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot) NewHttpClient().Run(k, v, conf.HTTPConfig.Timeout, s.bot)
} }
} }
if conf.WSConfig != nil && conf.WSConfig.Enabled { if conf.WSConfig != nil && conf.WSConfig.Enabled {
go WebsocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, s.bot) go WebSocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, s.bot)
} }
for _, rc := range conf.ReverseServers { for _, rc := range conf.ReverseServers {
go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run() go NewWebSocketClient(rc, conf.AccessToken, s.bot).Run()
} }
} }
// 暂不支持ws服务的重启 // 暂不支持ws服务的重启
func (s *webServer) ReloadServer() { func (s *webServer) ReloadServer() {
conf := GetConf() conf := GetConf()
if conf.HttpConfig != nil && conf.HttpConfig.Enabled { if conf.HTTPConfig != nil && conf.HTTPConfig.Enabled {
go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot) go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HTTPConfig.Host, conf.HTTPConfig.Port), conf.AccessToken, s.bot)
for k, v := range conf.HttpConfig.PostUrls { for k, v := range conf.HTTPConfig.PostUrls {
NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot) NewHttpClient().Run(k, v, conf.HTTPConfig.Timeout, s.bot)
} }
} }
for _, rc := range conf.ReverseServers { for _, rc := range conf.ReverseServers {
go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run() go NewWebSocketClient(rc, conf.AccessToken, s.bot).Run()
} }
} }
@ -467,7 +478,6 @@ func AdminDoRestart(s *webServer, c *gin.Context) {
s.Cli = nil s.Cli = nil
s.DoReLogin() s.DoReLogin()
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
return
} }
// 进程重启 // 进程重启
@ -480,7 +490,6 @@ func AdminProcessRestart(s *webServer, c *gin.Context) {
func AdminDoRestartDocker(s *webServer, c *gin.Context) { func AdminDoRestartDocker(s *webServer, c *gin.Context) {
Console <- os.Kill Console <- os.Kill
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
return
} }
// web输入 html 页面 // web输入 html 页面
@ -522,7 +531,7 @@ func AdminDoConfigBase(s *webServer, c *gin.Context) {
log.Fatalf("保存 config.hjson 时出现错误: %v", err) log.Fatalf("保存 config.hjson 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err))) c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
} else { } else {
JsonConfig = nil JSONConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
} }
} }
@ -531,23 +540,23 @@ func AdminDoConfigBase(s *webServer, c *gin.Context) {
func AdminDoConfigHttp(s *webServer, c *gin.Context) { func AdminDoConfigHttp(s *webServer, c *gin.Context) {
conf := GetConf() conf := GetConf()
p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16) p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16)
conf.HttpConfig.Port = uint16(p) conf.HTTPConfig.Port = uint16(p)
conf.HttpConfig.Host = c.PostForm("host") conf.HTTPConfig.Host = c.PostForm("host")
if c.PostForm("enable") == "true" { if c.PostForm("enable") == "true" {
conf.HttpConfig.Enabled = true conf.HTTPConfig.Enabled = true
} else { } else {
conf.HttpConfig.Enabled = false conf.HTTPConfig.Enabled = false
} }
t, _ := strconv.ParseInt(c.PostForm("timeout"), 10, 32) t, _ := strconv.ParseInt(c.PostForm("timeout"), 10, 32)
conf.HttpConfig.Timeout = int32(t) conf.HTTPConfig.Timeout = int32(t)
if c.PostForm("post_url") != "" { if c.PostForm("post_url") != "" {
conf.HttpConfig.PostUrls[c.PostForm("post_url")] = c.PostForm("post_secret") conf.HTTPConfig.PostUrls[c.PostForm("post_url")] = c.PostForm("post_secret")
} }
if err := conf.Save("config.hjson"); err != nil { if err := conf.Save("config.hjson"); err != nil {
log.Fatalf("保存 config.hjson 时出现错误: %v", err) log.Fatalf("保存 config.hjson 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err))) c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
} else { } else {
JsonConfig = nil JSONConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
} }
} }
@ -567,7 +576,7 @@ func AdminDoConfigWs(s *webServer, c *gin.Context) {
log.Fatalf("保存 config.hjson 时出现错误: %v", err) log.Fatalf("保存 config.hjson 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err))) c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
} else { } else {
JsonConfig = nil JSONConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
} }
} }
@ -575,9 +584,9 @@ func AdminDoConfigWs(s *webServer, c *gin.Context) {
// 反向ws配置修改 // 反向ws配置修改
func AdminDoConfigReverse(s *webServer, c *gin.Context) { func AdminDoConfigReverse(s *webServer, c *gin.Context) {
conf := GetConf() conf := GetConf()
conf.ReverseServers[0].ReverseApiUrl = c.PostForm("reverse_api_url") conf.ReverseServers[0].ReverseAPIURL = c.PostForm("reverse_api_url")
conf.ReverseServers[0].ReverseUrl = c.PostForm("reverse_url") conf.ReverseServers[0].ReverseURL = c.PostForm("reverse_url")
conf.ReverseServers[0].ReverseEventUrl = c.PostForm("reverse_event_url") conf.ReverseServers[0].ReverseEventURL = c.PostForm("reverse_event_url")
t, _ := strconv.ParseUint(c.PostForm("reverse_reconnect_interval"), 10, 16) t, _ := strconv.ParseUint(c.PostForm("reverse_reconnect_interval"), 10, 16)
conf.ReverseServers[0].ReverseReconnectInterval = uint16(t) conf.ReverseServers[0].ReverseReconnectInterval = uint16(t)
if c.PostForm("enable") == "true" { if c.PostForm("enable") == "true" {
@ -589,7 +598,7 @@ func AdminDoConfigReverse(s *webServer, c *gin.Context) {
log.Fatalf("保存 config.hjson 时出现错误: %v", err) log.Fatalf("保存 config.hjson 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err))) c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
} else { } else {
JsonConfig = nil JSONConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
} }
} }
@ -608,7 +617,7 @@ func AdminDoConfigJson(s *webServer, c *gin.Context) {
log.Fatalf("保存 config.hjson 时出现错误: %v", err) log.Fatalf("保存 config.hjson 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err))) c.JSON(200, Failed(502, "保存 config.hjson 时出现错误:"+fmt.Sprintf("%v", err)))
} else { } else {
JsonConfig = nil JSONConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{})) c.JSON(200, coolq.OK(coolq.MSG{}))
} }
} }

View File

@ -123,7 +123,11 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) {
} }
if c.secret != "" { if c.secret != "" {
mac := hmac.New(sha1.New, []byte(c.secret)) mac := hmac.New(sha1.New, []byte(c.secret))
mac.Write([]byte(m.ToJson())) _, err := mac.Write([]byte(m.ToJson()))
if err != nil {
log.Error(err)
return nil
}
h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil)) h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil))
} }
return h return h
@ -211,6 +215,11 @@ func GetGroupFileUrl(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupFileUrl(gid, fid, int32(busid))) c.JSON(200, s.bot.CQGetGroupFileUrl(gid, fid, int32(busid)))
} }
func UploadGroupFile(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQUploadGroupFile(gid, getParam(c, "file"), getParam(c, "name"), getParam(c, "folder")))
}
func SendMessage(s *httpServer, c *gin.Context) { func SendMessage(s *httpServer, c *gin.Context) {
if getParam(c, "message_type") == "private" { if getParam(c, "message_type") == "private" {
SendPrivateMessage(s, c) SendPrivateMessage(s, c)
@ -417,6 +426,16 @@ func SetGroupAnonymousBan(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQSetGroupAnonymousBan(gid, flag, int32(d))) c.JSON(200, s.bot.CQSetGroupAnonymousBan(gid, flag, int32(d)))
} }
func GetGroupMessageHistory(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
seq, _ := strconv.ParseInt(getParam(c, "message_seq"), 10, 64)
c.JSON(200, s.bot.CQGetGroupMessageHistory(gid, seq))
}
func GetOnlineClients(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQGetOnlineClients(getParamOrDefault(c, "no_cache", "false") == "true"))
}
func HandleQuickOperation(s *httpServer, c *gin.Context) { func HandleQuickOperation(s *httpServer, c *gin.Context) {
if c.Request.Method != "POST" { if c.Request.Method != "POST" {
c.AbortWithStatus(404) c.AbortWithStatus(404)
@ -428,6 +447,33 @@ func HandleQuickOperation(s *httpServer, c *gin.Context) {
} }
} }
func DownloadFile(s *httpServer, c *gin.Context) {
url := getParam(c, "url")
tc, _ := strconv.Atoi(getParam(c, "thread_count"))
h, t := getParamWithType(c, "headers")
headers := map[string]string{}
if t == gjson.Null || t == gjson.String {
lines := strings.Split(h, "\r\n")
for _, sub := range lines {
str := strings.SplitN(sub, "=", 2)
if len(str) == 2 {
headers[str[0]] = str[1]
}
}
}
if t == gjson.JSON {
arr := gjson.Parse(h)
for _, sub := range arr.Array() {
str := strings.SplitN(sub.String(), "=", 2)
if len(str) == 2 {
headers[str[0]] = str[1]
}
}
}
println(url, tc, h, t)
c.JSON(200, s.bot.CQDownloadFile(url, headers, tc))
}
func OcrImage(s *httpServer, c *gin.Context) { func OcrImage(s *httpServer, c *gin.Context) {
img := getParam(c, "image") img := getParam(c, "image")
c.JSON(200, s.bot.CQOcrImage(img)) c.JSON(200, s.bot.CQOcrImage(img))
@ -445,6 +491,25 @@ func SetGroupPortrait(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQSetGroupPortrait(gid, file, cache)) c.JSON(200, s.bot.CQSetGroupPortrait(gid, file, cache))
} }
func SetEssenceMsg(s *httpServer, c *gin.Context) {
mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 64)
c.JSON(200, s.bot.CQSetEssenceMessage(int32(mid)))
}
func DeleteEssenceMsg(s *httpServer, c *gin.Context) {
mid, _ := strconv.ParseInt(getParam(c, "message_id"), 10, 64)
c.JSON(200, s.bot.CQDeleteEssenceMessage(int32(mid)))
}
func GetEssenceMsgList(s *httpServer, c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQGetEssenceMessageList(gid))
}
func CheckUrlSafely(s *httpServer, c *gin.Context) {
c.JSON(200, s.bot.CQCheckUrlSafely(getParam(c, "url")))
}
func getParamOrDefault(c *gin.Context, k, def string) string { func getParamOrDefault(c *gin.Context, k, def string) string {
r := getParam(c, k) r := getParam(c, k)
if r != "" { if r != "" {
@ -504,11 +569,14 @@ var httpApi = map[string]func(s *httpServer, c *gin.Context){
"get_group_root_files": GetGroupRootFiles, "get_group_root_files": GetGroupRootFiles,
"get_group_files_by_folder": GetGroupFilesByFolderId, "get_group_files_by_folder": GetGroupFilesByFolderId,
"get_group_file_url": GetGroupFileUrl, "get_group_file_url": GetGroupFileUrl,
"upload_group_file": UploadGroupFile,
"get_essence_msg_list": GetEssenceMsgList,
"send_msg": SendMessage, "send_msg": SendMessage,
"send_group_msg": SendGroupMessage, "send_group_msg": SendGroupMessage,
"send_group_forward_msg": SendGroupForwardMessage, "send_group_forward_msg": SendGroupForwardMessage,
"send_private_msg": SendPrivateMessage, "send_private_msg": SendPrivateMessage,
"delete_msg": DeleteMessage, "delete_msg": DeleteMessage,
"delete_essence_msg": DeleteEssenceMsg,
"set_friend_add_request": ProcessFriendRequest, "set_friend_add_request": ProcessFriendRequest,
"set_group_add_request": ProcessGroupRequest, "set_group_add_request": ProcessGroupRequest,
"set_group_card": SetGroupCard, "set_group_card": SetGroupCard,
@ -518,6 +586,7 @@ var httpApi = map[string]func(s *httpServer, c *gin.Context){
"set_group_whole_ban": SetWholeBan, "set_group_whole_ban": SetWholeBan,
"set_group_name": SetGroupName, "set_group_name": SetGroupName,
"set_group_admin": SetGroupAdmin, "set_group_admin": SetGroupAdmin,
"set_essence_msg": SetEssenceMsg,
"set_restart": SetRestart, "set_restart": SetRestart,
"_send_group_notice": SendGroupNotice, "_send_group_notice": SendGroupNotice,
"set_group_leave": SetGroupLeave, "set_group_leave": SetGroupLeave,
@ -535,10 +604,14 @@ var httpApi = map[string]func(s *httpServer, c *gin.Context){
"reload_event_filter": ReloadEventFilter, "reload_event_filter": ReloadEventFilter,
"set_group_portrait": SetGroupPortrait, "set_group_portrait": SetGroupPortrait,
"set_group_anonymous_ban": SetGroupAnonymousBan, "set_group_anonymous_ban": SetGroupAnonymousBan,
"get_group_msg_history": GetGroupMessageHistory,
"check_url_safely": CheckUrlSafely,
"download_file": DownloadFile,
".handle_quick_operation": HandleQuickOperation, ".handle_quick_operation": HandleQuickOperation,
".ocr_image": OcrImage, ".ocr_image": OcrImage,
"ocr_image": OcrImage, "ocr_image": OcrImage,
"get_group_at_all_remain": GetGroupAtAllRemain, "get_group_at_all_remain": GetGroupAtAllRemain,
"get_online_clients": GetOnlineClients,
".get_word_slices": GetWordSlices, ".get_word_slices": GetWordSlices,
} }
@ -548,9 +621,7 @@ func (s *httpServer) ShutDown() {
if err := s.Http.Shutdown(ctx); err != nil { if err := s.Http.Shutdown(ctx); err != nil {
log.Fatal("http Server Shutdown:", err) log.Fatal("http Server Shutdown:", err)
} }
select { <-ctx.Done()
case <-ctx.Done(): log.Println("timeout of 5 seconds.")
log.Println("timeout of 5 seconds.")
}
log.Println("http Server exiting") log.Println("http Server exiting")
} }

View File

@ -17,36 +17,38 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
type websocketServer struct { type webSocketServer struct {
bot *coolq.CQBot bot *coolq.CQBot
token string token string
eventConn []*websocketConn eventConn []*webSocketConn
eventConnMutex sync.Mutex eventConnMutex sync.Mutex
handshake string handshake string
} }
type websocketClient struct { //WebSocketClient Websocket客户端实例
conf *global.GoCQReverseWebsocketConfig type WebSocketClient struct {
conf *global.GoCQReverseWebSocketConfig
token string token string
bot *coolq.CQBot bot *coolq.CQBot
universalConn *websocketConn universalConn *webSocketConn
eventConn *websocketConn eventConn *webSocketConn
} }
type websocketConn struct { type webSocketConn struct {
*websocket.Conn *websocket.Conn
sync.Mutex sync.Mutex
} }
var WebsocketServer = &websocketServer{} //WebSocketServer 初始化一个WebSocketServer实例
var WebSocketServer = &webSocketServer{}
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
}, },
} }
func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) { func (s *webSocketServer) Run(addr, authToken string, b *coolq.CQBot) {
s.token = authToken s.token = authToken
s.bot = b s.bot = b
s.handshake = fmt.Sprintf(`{"_post_method":2,"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`, s.handshake = fmt.Sprintf(`{"_post_method":2,"meta_event_type":"lifecycle","post_type":"meta_event","self_id":%d,"sub_type":"connect","time":%d}`,
@ -61,29 +63,31 @@ func (s *websocketServer) Run(addr, authToken string, b *coolq.CQBot) {
}() }()
} }
func NewWebsocketClient(conf *global.GoCQReverseWebsocketConfig, authToken string, b *coolq.CQBot) *websocketClient { //NewWebSocketClient 初始化一个NWebSocket客户端
return &websocketClient{conf: conf, token: authToken, bot: b} func NewWebSocketClient(conf *global.GoCQReverseWebSocketConfig, authToken string, b *coolq.CQBot) *WebSocketClient {
return &WebSocketClient{conf: conf, token: authToken, bot: b}
} }
func (c *websocketClient) Run() { //Run 运行实例
func (c *WebSocketClient) Run() {
if !c.conf.Enabled { if !c.conf.Enabled {
return return
} }
if c.conf.ReverseUrl != "" { if c.conf.ReverseURL != "" {
c.connectUniversal() c.connectUniversal()
} else { } else {
if c.conf.ReverseApiUrl != "" { if c.conf.ReverseAPIURL != "" {
c.connectApi() c.connectAPI()
} }
if c.conf.ReverseEventUrl != "" { if c.conf.ReverseEventURL != "" {
c.connectEvent() 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)
header := http.Header{ header := http.Header{
"X-Client-Role": []string{"API"}, "X-Client-Role": []string{"API"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)}, "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@ -92,22 +96,22 @@ func (c *websocketClient) connectApi() {
if c.token != "" { if c.token != "" {
header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseApiUrl, header) 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 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval)) time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectApi() c.connectAPI()
} }
return return
} }
log.Infof("已连接到反向Websocket API服务器 %v", c.conf.ReverseApiUrl) log.Infof("已连接到反向Websocket API服务器 %v", c.conf.ReverseAPIURL)
wrappedConn := &websocketConn{Conn: conn} wrappedConn := &webSocketConn{Conn: conn}
go c.listenApi(wrappedConn, false) go c.listenAPI(wrappedConn, false)
} }
func (c *websocketClient) connectEvent() { func (c *WebSocketClient) connectEvent() {
log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventUrl) log.Infof("开始尝试连接到反向Websocket Event服务器: %v", c.conf.ReverseEventURL)
header := http.Header{ header := http.Header{
"X-Client-Role": []string{"Event"}, "X-Client-Role": []string{"Event"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)}, "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@ -116,9 +120,9 @@ func (c *websocketClient) connectEvent() {
if c.token != "" { if c.token != "" {
header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventUrl, header) conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseEventURL, header)
if err != nil { if err != nil {
log.Warnf("连接到反向Websocket Event服务器 %v 时出现错误: %v", c.conf.ReverseEventUrl, 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.connectEvent() c.connectEvent()
@ -133,12 +137,12 @@ func (c *websocketClient) connectEvent() {
log.Warnf("反向Websocket 握手时出现错误: %v", err) log.Warnf("反向Websocket 握手时出现错误: %v", err)
} }
log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventUrl) log.Infof("已连接到反向Websocket Event服务器 %v", c.conf.ReverseEventURL)
c.eventConn = &websocketConn{Conn: conn} c.eventConn = &webSocketConn{Conn: conn}
} }
func (c *websocketClient) connectUniversal() { func (c *WebSocketClient) connectUniversal() {
log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseUrl) log.Infof("开始尝试连接到反向Websocket Universal服务器: %v", c.conf.ReverseURL)
header := http.Header{ header := http.Header{
"X-Client-Role": []string{"Universal"}, "X-Client-Role": []string{"Universal"},
"X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)}, "X-Self-ID": []string{strconv.FormatInt(c.bot.Client.Uin, 10)},
@ -147,9 +151,9 @@ func (c *websocketClient) connectUniversal() {
if c.token != "" { if c.token != "" {
header["Authorization"] = []string{"Token " + c.token} header["Authorization"] = []string{"Token " + c.token}
} }
conn, _, err := websocket.DefaultDialer.Dial(c.conf.ReverseUrl, header) 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 {
time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval)) time.Sleep(time.Millisecond * time.Duration(c.conf.ReverseReconnectInterval))
c.connectUniversal() c.connectUniversal()
@ -164,12 +168,12 @@ func (c *websocketClient) connectUniversal() {
log.Warnf("反向Websocket 握手时出现错误: %v", err) log.Warnf("反向Websocket 握手时出现错误: %v", err)
} }
wrappedConn := &websocketConn{Conn: conn} wrappedConn := &webSocketConn{Conn: conn}
go c.listenApi(wrappedConn, true) go c.listenAPI(wrappedConn, true)
c.universalConn = wrappedConn c.universalConn = wrappedConn
} }
func (c *websocketClient) listenApi(conn *websocketConn, u bool) { func (c *WebSocketClient) listenAPI(conn *webSocketConn, u bool) {
defer conn.Close() defer conn.Close()
for { for {
_, buf, err := conn.ReadMessage() _, buf, err := conn.ReadMessage()
@ -184,12 +188,12 @@ func (c *websocketClient) listenApi(conn *websocketConn, u bool) {
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 {
go c.connectApi() go c.connectAPI()
} }
} }
} }
func (c *websocketClient) onBotPushEvent(m coolq.MSG) { func (c *WebSocketClient) onBotPushEvent(m coolq.MSG) {
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())
conn := c.eventConn conn := c.eventConn
@ -222,7 +226,7 @@ func (c *websocketClient) onBotPushEvent(m coolq.MSG) {
} }
} }
func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) { func (s *webSocketServer) event(w http.ResponseWriter, r *http.Request) {
if s.token != "" { if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token { if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@ -246,14 +250,14 @@ func (s *websocketServer) event(w http.ResponseWriter, r *http.Request) {
log.Infof("接受 Websocket 连接: %v (/event)", r.RemoteAddr) log.Infof("接受 Websocket 连接: %v (/event)", r.RemoteAddr)
conn := &websocketConn{Conn: c} conn := &webSocketConn{Conn: c}
s.eventConnMutex.Lock() s.eventConnMutex.Lock()
s.eventConn = append(s.eventConn, conn) s.eventConn = append(s.eventConn, conn)
s.eventConnMutex.Unlock() s.eventConnMutex.Unlock()
} }
func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) { func (s *webSocketServer) api(w http.ResponseWriter, r *http.Request) {
if s.token != "" { if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token { if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@ -269,11 +273,11 @@ func (s *websocketServer) api(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Infof("接受 Websocket 连接: %v (/api)", r.RemoteAddr) log.Infof("接受 Websocket 连接: %v (/api)", r.RemoteAddr)
conn := &websocketConn{Conn: c} conn := &webSocketConn{Conn: c}
go s.listenApi(conn) go s.listenAPI(conn)
} }
func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) { func (s *webSocketServer) any(w http.ResponseWriter, r *http.Request) {
if s.token != "" { if s.token != "" {
if auth := r.URL.Query().Get("access_token"); auth != s.token { if auth := r.URL.Query().Get("access_token"); auth != s.token {
if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token { if auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(auth) != 2 || auth[1] != s.token {
@ -295,12 +299,12 @@ func (s *websocketServer) any(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr) log.Infof("接受 Websocket 连接: %v (/)", r.RemoteAddr)
conn := &websocketConn{Conn: c} conn := &webSocketConn{Conn: c}
s.eventConn = append(s.eventConn, conn) s.eventConn = append(s.eventConn, conn)
s.listenApi(conn) s.listenAPI(conn)
} }
func (s *websocketServer) listenApi(c *websocketConn) { func (s *webSocketServer) listenAPI(c *webSocketConn) {
defer c.Close() defer c.Close()
for { for {
t, payload, err := c.ReadMessage() t, payload, err := c.ReadMessage()
@ -314,7 +318,7 @@ func (s *websocketServer) listenApi(c *websocketConn) {
} }
} }
func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) { func (c *webSocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Printf("处置WS命令时发生无法恢复的异常%v\n%s", err, debug.Stack()) log.Printf("处置WS命令时发生无法恢复的异常%v\n%s", err, debug.Stack())
@ -325,7 +329,7 @@ func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
j := gjson.ParseBytes(payload) j := gjson.ParseBytes(payload)
t := strings.ReplaceAll(j.Get("action").Str, "_async", "") t := strings.ReplaceAll(j.Get("action").Str, "_async", "")
log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw) log.Debugf("WS接收到API调用: %v 参数: %v", t, j.Get("params").Raw)
if f, ok := wsApi[t]; ok { if f, ok := wsAPI[t]; ok {
ret := f(bot, j.Get("params")) ret := f(bot, j.Get("params"))
if j.Get("echo").Exists() { if j.Get("echo").Exists() {
ret["echo"] = j.Get("echo").Value() ret["echo"] = j.Get("echo").Value()
@ -344,7 +348,7 @@ func (c *websocketConn) handleRequest(bot *coolq.CQBot, payload []byte) {
} }
} }
func (s *websocketServer) onBotPushEvent(m coolq.MSG) { func (s *webSocketServer) onBotPushEvent(m coolq.MSG) {
s.eventConnMutex.Lock() s.eventConnMutex.Lock()
defer s.eventConnMutex.Unlock() defer s.eventConnMutex.Unlock()
for i, l := 0, len(s.eventConn); i < l; i++ { for i, l := 0, len(s.eventConn); i < l; i++ {
@ -368,7 +372,7 @@ func (s *websocketServer) onBotPushEvent(m coolq.MSG) {
} }
} }
var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ var wsAPI = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_login_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_login_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetLoginInfo() return bot.CQGetLoginInfo()
}, },
@ -490,11 +494,33 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetMessage(int32(p.Get("message_id").Int())) return bot.CQGetMessage(int32(p.Get("message_id").Int()))
}, },
"download_file": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
headers := map[string]string{}
headersToken := p.Get("headers")
if headersToken.IsArray() {
for _, sub := range headersToken.Array() {
str := strings.SplitN(sub.String(), "=", 2)
if len(str) == 2 {
headers[str[0]] = str[1]
}
}
}
if headersToken.Type == gjson.String {
lines := strings.Split(headersToken.String(), "\r\n")
for _, sub := range lines {
str := strings.SplitN(sub, "=", 2)
if len(str) == 2 {
headers[str[0]] = str[1]
}
}
}
return bot.CQDownloadFile(p.Get("url").Str, headers, int(p.Get("thread_count").Int()))
},
"get_group_honor_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "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) return bot.CQGetGroupHonorInfo(p.Get("group_id").Int(), p.Get("type").Str)
}, },
"set_restart": func(c *coolq.CQBot, p gjson.Result) coolq.MSG { "set_restart": func(c *coolq.CQBot, p gjson.Result) coolq.MSG {
var delay int64 = 0 var delay int64
delay = p.Get("delay").Int() delay = p.Get("delay").Int()
if delay < 0 { if delay < 0 {
delay = 0 delay = 0
@ -536,6 +562,12 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_group_file_url": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_group_file_url": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupFileUrl(p.Get("group_id").Int(), p.Get("file_id").Str, int32(p.Get("busid").Int())) return bot.CQGetGroupFileUrl(p.Get("group_id").Int(), p.Get("file_id").Str, int32(p.Get("busid").Int()))
}, },
"upload_group_file": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQUploadGroupFile(p.Get("group_id").Int(), p.Get("file").Str, p.Get("name").Str, p.Get("folder").Str)
},
"get_group_msg_history": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetGroupMessageHistory(p.Get("group_id").Int(), p.Get("message_seq").Int())
},
"_get_vip_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "_get_vip_info": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetVipInfo(p.Get("user_id").Int()) return bot.CQGetVipInfo(p.Get("user_id").Int())
}, },
@ -551,12 +583,27 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{
"get_group_at_all_remain": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "get_group_at_all_remain": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetAtAllRemain(p.Get("group_id").Int()) return bot.CQGetAtAllRemain(p.Get("group_id").Int())
}, },
"get_online_clients": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetOnlineClients(p.Get("no_cache").Bool())
},
".get_word_slices": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { ".get_word_slices": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetWordSlices(p.Get("content").Str) return bot.CQGetWordSlices(p.Get("content").Str)
}, },
"set_group_portrait": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "set_group_portrait": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSetGroupPortrait(p.Get("group_id").Int(), p.Get("file").String(), p.Get("cache").String()) return bot.CQSetGroupPortrait(p.Get("group_id").Int(), p.Get("file").String(), p.Get("cache").String())
}, },
"set_essence_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQSetEssenceMessage(int32(p.Get("message_id").Int()))
},
"delete_essence_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQDeleteEssenceMessage(int32(p.Get("message_id").Int()))
},
"get_essence_msg_list": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQGetEssenceMessageList(p.Get("group_id").Int())
},
"check_url_safely": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
return bot.CQCheckUrlSafely(p.Get("url").String())
},
"set_group_anonymous_ban": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { "set_group_anonymous_ban": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG {
obj := p.Get("anonymous") obj := p.Get("anonymous")
flag := p.Get("anonymous_flag") flag := p.Get("anonymous_flag")