Commit df1cb3de authored by 是甜食哇's avatar 是甜食哇 Committed by GitHub

Merge pull request #1 from Mrs4s/master

跟进
parents 2dca0a3e 7d62db2e
.gitlab-ci.yml
.dockerignore
Dockerfile
README.md
LICENSE
---
name: Bug汇报
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**环境信息**
请根据实际使用环境修改以下信息
go-cqhttp版本: v0.9.10
运行环境: windows_amd64
连接方式: 反向WS
**bug内容**
请在这里详细描述bug的内容
**复现方法**
请在这里分步骤的描述如何复现这个bug
......@@ -17,9 +17,14 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set RELEASE_VERSION env
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- uses: wangyoucao577/go-release-action@master
env:
CGO_ENABLED: 0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
ldflags: "-w -s"
ldflags: -w -s -X "github.com/Mrs4s/go-cqhttp/coolq.Version=${{ env.RELEASE_VERSION }}"
\ No newline at end of file
vendor/
.idea
FROM golang:1.14.7-alpine AS builder
RUN go env -w GO111MODULE=auto \
&& go env -w CGO_ENABLED=0 \
&& mkdir /build
WORKDIR /build
COPY ./ .
RUN cd /build \
&& go build -ldflags "-s -w -extldflags '-static'" -o cqhttp
FROM alpine:latest
COPY --from=builder /build/cqhttp /usr/bin/cqhttp
RUN chmod +x /usr/bin/cqhttp
WORKDIR /data
ENTRYPOINT [ "/usr/bin/cqhttp" ]
......@@ -27,6 +27,8 @@
<summary>已实现CQ码</summary>
- [CQ:image]
- [CQ:record]
- [CQ:video]
- [CQ:face]
- [CQ:at]
- [CQ:share]
......@@ -90,6 +92,16 @@
</details>
# 关于ISSUE
以下ISSUE会被直接关闭
- 提交BUG不使用Template
- 询问已知问题
- 提问找不到重点
- 重复提问
> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。
# 性能
在关闭数据库的情况下, 加载25个好友128个群运行24小时后内存使用为10MB左右. 开启数据库后内存使用将根据消息量增加10-20MB, 如果系统内存小于128M建议关闭数据库使用.
This diff is collapsed.
......@@ -5,15 +5,19 @@ import (
"encoding/gob"
"encoding/json"
"fmt"
"hash/crc32"
"path"
"sync"
"time"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/go-cqhttp/global"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/xujiajun/nutsdb"
"hash/crc32"
"path"
"sync"
)
type CQBot struct {
......@@ -24,10 +28,14 @@ type CQBot struct {
friendReqCache sync.Map
invitedReqCache sync.Map
joinReqCache sync.Map
tempMsgCache sync.Map
oneWayMsgCache sync.Map
}
type MSG map[string]interface{}
var ForceFragmented = false
func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot := &CQBot{
Client: cli,
......@@ -58,8 +66,27 @@ func NewQQBot(cli *client.QQClient, conf *global.JsonConfig) *CQBot {
bot.Client.OnGroupMemberLeaved(bot.memberLeaveEvent)
bot.Client.OnGroupMemberPermissionChanged(bot.memberPermissionChangedEvent)
bot.Client.OnNewFriendRequest(bot.friendRequestEvent)
bot.Client.OnNewFriendAdded(bot.friendAddedEvent)
bot.Client.OnGroupInvited(bot.groupInvitedEvent)
bot.Client.OnUserWantJoinGroup(bot.groupJoinReqEvent)
go func() {
i := conf.HeartbeatInterval
if i < 1 {
log.Warn("警告: 心跳功能已关闭,若非预期,请检查配置文件。")
return
}
for {
time.Sleep(time.Second * i)
bot.dispatchEventMessage(MSG{
"time": time.Now().Unix(),
"self_id": bot.Client.Uin,
"post_type": "meta_event",
"meta_event_type": "heartbeat",
"status": nil,
"interval": 1000 * i,
})
}
}()
return bot
}
......@@ -82,7 +109,7 @@ func (bot *CQBot) GetGroupMessage(mid int32) MSG {
if err == nil {
return m
}
log.Warnf("获取信息时出现错误: %v", err)
log.Warnf("获取信息时出现错误: %v id: %v", err, mid)
}
return nil
}
......@@ -99,10 +126,23 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int
newElem = append(newElem, gm)
continue
}
if i, ok := elem.(*message.VoiceElement); ok {
gv, err := bot.Client.UploadGroupPtt(groupId, i.Data)
if err != nil {
log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err)
continue
}
newElem = append(newElem, gv)
continue
}
newElem = append(newElem, elem)
}
m.Elements = newElem
ret := bot.Client.SendGroupMessage(groupId, m)
ret := bot.Client.SendGroupMessage(groupId, m, ForceFragmented)
if ret == nil || ret.Id == -1 {
log.Warnf("群消息发送失败: 账号可能被风控.")
return -1
}
return bot.InsertGroupMessage(ret)
}
......@@ -112,7 +152,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
if i, ok := elem.(*message.ImageElement); ok {
fm, err := bot.Client.UploadPrivateImage(target, i.Data)
if err != nil {
log.Warnf("警告: 好友 %v 消息图片上传失败.", target)
log.Warnf("警告: 私聊 %v 消息图片上传失败.", target)
continue
}
newElem = append(newElem, fm)
......@@ -121,8 +161,27 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in
newElem = append(newElem, elem)
}
m.Elements = newElem
ret := bot.Client.SendPrivateMessage(target, m)
return ToGlobalId(target, ret.Id)
var id int32 = -1
if bot.Client.FindFriend(target) != nil {
msg := bot.Client.SendPrivateMessage(target, m)
if msg != nil {
id = msg.Id
}
} else if code, ok := bot.tempMsgCache.Load(target); ok {
msg := bot.Client.SendTempMessage(code.(int64), target, m)
if msg != nil {
id = msg.Id
}
} else if _, ok := bot.oneWayMsgCache.Load(target); ok {
msg := bot.Client.SendPrivateMessage(target, m)
if msg != nil {
id = msg.Id
}
}
if id == -1 {
return -1
}
return ToGlobalId(target, id)
}
func (bot *CQBot) InsertGroupMessage(m *message.GroupMessage) int32 {
......@@ -163,8 +222,22 @@ func (bot *CQBot) Release() {
}
func (bot *CQBot) dispatchEventMessage(m MSG) {
payload := gjson.Parse(m.ToJson())
filter := global.GetFilter()
if filter != nil && (*filter).Eval(payload) == false {
log.Debug("Event filtered!")
return
}
for _, f := range bot.events {
f(m)
fn := f
go func() {
start := time.Now()
fn(m)
end := time.Now()
if end.Sub(start) > time.Second*5 {
log.Debugf("警告: 事件处理耗时超过 5 秒 (%v), 请检查应用是否有堵塞.", end.Sub(start))
}
}()
}
}
......@@ -173,6 +246,9 @@ func formatGroupName(group *client.GroupInfo) string {
}
func formatMemberName(mem *client.GroupMemberInfo) string {
if mem == nil {
return "未知"
}
return fmt.Sprintf("%s(%d)", mem.DisplayName(), mem.Uin)
}
......
This diff is collapsed.
......@@ -10,12 +10,31 @@ import (
"io/ioutil"
"path"
"strconv"
"strings"
"time"
)
var format = "string"
func SetMessageFormat(f string) {
format = f
}
func ToFormattedMessage(e []message.IMessageElement, code int64, raw ...bool) (r interface{}) {
if format == "string" {
r = ToStringMessage(e, code, raw...)
} else if format == "array" {
r = ToArrayMessage(e, code, raw...)
}
return
}
func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMessage) {
checkImage(m.Elements)
bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true)
if !m.Sender.IsFriend {
bot.oneWayMsgCache.Store(m.Sender.Uin, "")
}
log.Infof("收到好友 %v(%v) 的消息: %v", m.Sender.DisplayName(), m.Sender.Uin, cqm)
fm := MSG{
"post_type": "message",
......@@ -23,7 +42,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
"sub_type": "friend",
"message_id": ToGlobalId(m.Sender.Uin, m.Id),
"user_id": m.Sender.Uin,
"message": ToStringMessage(m.Elements, 0, false),
"message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
......@@ -39,7 +58,7 @@ func (bot *CQBot) privateMessageEvent(c *client.QQClient, m *message.PrivateMess
}
func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage) {
checkImage(m.Elements)
bot.checkMedia(m.Elements)
for _, elem := range m.Elements {
if file, ok := elem.(*message.GroupFileElement); ok {
log.Infof("群 %v(%v) 内 %v(%v) 上传了文件: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, file.Name)
......@@ -71,7 +90,7 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
"anonymous": nil,
"font": 0,
"group_id": m.GroupCode,
"message": ToStringMessage(m.Elements, m.GroupCode, false),
"message": ToFormattedMessage(m.Elements, m.GroupCode, false),
"message_id": id,
"message_type": "group",
"post_type": "message",
......@@ -117,8 +136,9 @@ func (bot *CQBot) groupMessageEvent(c *client.QQClient, m *message.GroupMessage)
}
func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
checkImage(m.Elements)
bot.checkMedia(m.Elements)
cqm := ToStringMessage(m.Elements, 0, true)
bot.tempMsgCache.Store(m.Sender.Uin, m.GroupCode)
log.Infof("收到来自群 %v(%v) 内 %v(%v) 的临时会话消息: %v", m.GroupName, m.GroupCode, m.Sender.DisplayName(), m.Sender.Uin, cqm)
tm := MSG{
"post_type": "message",
......@@ -126,7 +146,7 @@ func (bot *CQBot) tempMessageEvent(c *client.QQClient, m *message.TempMessage) {
"sub_type": "group",
"message_id": m.Id,
"user_id": m.Sender.Uin,
"message": ToStringMessage(m.Elements, 0, false),
"message": ToFormattedMessage(m.Elements, 0, false),
"raw_message": cqm,
"font": 0,
"self_id": c.Uin,
......@@ -260,6 +280,18 @@ func (bot *CQBot) friendRequestEvent(c *client.QQClient, e *client.NewFriendRequ
})
}
func (bot *CQBot) friendAddedEvent(c *client.QQClient, e *client.NewFriendEvent) {
log.Infof("添加了新好友: %v(%v)", e.Friend.Nickname, e.Friend.Uin)
bot.tempMsgCache.Delete(e.Friend.Uin)
bot.dispatchEventMessage(MSG{
"post_type": "notice",
"notice_type": "friend_add",
"self_id": c.Uin,
"user_id": e.Friend.Uin,
"time": time.Now().Unix(),
})
}
func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRequest) {
log.Infof("收到来自群 %v(%v) 内用户 %v(%v) 的加群邀请.", e.GroupName, e.GroupCode, e.InvitorNick, e.InvitorUin)
flag := strconv.FormatInt(e.RequestId, 10)
......@@ -278,7 +310,7 @@ func (bot *CQBot) groupInvitedEvent(c *client.QQClient, e *client.GroupInvitedRe
}
func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupRequest) {
log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupName, e.RequesterNick, e.RequesterUin)
log.Infof("群 %v(%v) 收到来自用户 %v(%v) 的加群请求.", e.GroupName, e.GroupCode, e.RequesterNick, e.RequesterUin)
flag := strconv.FormatInt(e.RequestId, 10)
bot.joinReqCache.Store(flag, e)
bot.dispatchEventMessage(MSG{
......@@ -287,7 +319,7 @@ func (bot *CQBot) groupJoinReqEvent(c *client.QQClient, e *client.UserJoinGroupR
"sub_type": "add",
"group_id": e.GroupCode,
"user_id": e.RequesterUin,
"comment": "",
"comment": e.Message,
"flag": flag,
"time": time.Now().Unix(),
"self_id": c.Uin,
......@@ -333,9 +365,10 @@ func (bot *CQBot) groupDecrease(groupCode, userUin int64, operator *client.Group
}
}
func checkImage(e []message.IMessageElement) {
func (bot *CQBot) checkMedia(e []message.IMessageElement) {
for _, elem := range e {
if i, ok := elem.(*message.ImageElement); ok {
switch i := elem.(type) {
case *message.ImageElement:
filename := hex.EncodeToString(i.Md5) + ".image"
if !global.PathExists(path.Join(global.IMAGE_PATH, filename)) {
_ = ioutil.WriteFile(path.Join(global.IMAGE_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
......@@ -343,9 +376,32 @@ func checkImage(e []message.IMessageElement) {
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Filename)
w.WriteString(i.Url)
}), 0777)
}), 0644)
}
i.Filename = filename
case *message.VoiceElement:
i.Name = strings.ReplaceAll(i.Name, "{", "")
i.Name = strings.ReplaceAll(i.Name, "}", "")
if !global.PathExists(path.Join(global.VOICE_PATH, i.Name)) {
b, err := global.GetBytes(i.Url)
if err != nil {
log.Warnf("语音文件 %v 下载失败: %v", i.Name, err)
continue
}
_ = ioutil.WriteFile(path.Join(global.VOICE_PATH, i.Name), b, 0644)
}
case *message.ShortVideoElement:
filename := hex.EncodeToString(i.Md5) + ".video"
if !global.PathExists(path.Join(global.VIDEO_PATH, filename)) {
_ = ioutil.WriteFile(path.Join(global.VIDEO_PATH, filename), binary.NewWriterF(func(w *binary.Writer) {
w.Write(i.Md5)
w.WriteUInt32(uint32(i.Size))
w.WriteString(i.Name)
w.Write(i.Uuid)
}), 0644)
}
i.Name = filename
i.Url = bot.Client.GetShortVideoUrl(i.Uuid, i.Md5)
}
}
}
......@@ -18,15 +18,25 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
{
"uin": 0,
"password": "",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"access_token": "",
"relogin": false,
"relogin_delay": 0,
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"post_message_format": "string",
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 5,
"http_config": {
"enabled": true,
"host": "0.0.0.0",
"port": 5700,
"post_urls": []
"timeout": 5,
"post_urls": {"url:port": "secret"}
},
"ws_config": {
"enabled": true,
......@@ -47,14 +57,28 @@ go-cqhttp 支持导入CQHTTP的配置文件, 具体步骤为:
| 字段 | 类型 | 说明 |
| ------------------ | -------- | ------------------------------------------------------------------- |
| uin | int64 | 登录用QQ号 |
| password | string | 登录用密码 |
| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
| relogin | bool | 是否自动重新登录 |
| relogin_delay | int | 重登录延时(秒) |
| http_config | object | HTTP API配置 |
| ws_config | object | Websocket API 配置 |
| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
| uin | int64 | 登录用QQ号 |
| password | string | 登录用密码 |
| encrypt_password | bool | 是否对密码进行加密. |
| password_encrypted | string | 加密后的密码(请勿修改) |
| enable_db | bool | 是否开启内置数据库, 关闭后将无法使用 **回复/撤回** 等上下文相关接口 |
| access_token | string | 同CQHTTP的 `access_token` 用于身份验证 |
| relogin | bool | 是否自动重新登录 |
| relogin_delay | int | 重登录延时(秒) |
| max_relogin_times | uint | 最大重登录次数,若0则不设置上限 |
| post_message_format | string | 上报信息类型 |
| ignore_invalid_cqcode| bool | 是否忽略错误的CQ码 |
| force_fragmented | bool | 是否强制分片发送群长消息 |
| heartbeat_interval | int64 | 心跳间隔时间,单位秒,若0则关闭心跳 |
| http_config | object | HTTP API配置 |
| ws_config | object | Websocket API 配置 |
| ws_reverse_servers | object[] | 反向 Websocket API 配置 |
| log_level | string | 指定日志收集级别,将收集的日志单独存放到固定文件中,便于查看日志线索 当前支持 warn,error|
> 注: 开启密码加密后程序将在每次启动时要求输入解密密钥, 密钥错误会导致登录时提示密码错误.
> 解密后密码将储存在内存中,用于自动重连等功能. 所以此加密并不能防止内存读取.
> 解密密钥在使用完成后并不会留存在内存中, 所以可用相对简单的字符串作为密钥
> 注2: 分片发送为原酷Q发送长消息的老方案, 发送速度更优/兼容性更好。关闭后将优先使用新方案, 能发送更长的消息, 但发送速度更慢,在部分老客户端将无法解析.
> 注3:关闭心跳服务可能引起断线,请谨慎关闭
......@@ -119,6 +119,51 @@ Type: `node`
]
````
### xml支持
Type: `xml`
范围: **发送**
参数:
| 参数名 | 类型 | 说明 |
| ------ | ------ | ------------------------------------------------------------ |
| data | string | xml内容,xml中的value部分,记得实体化处理|
| resid | int32 | 可以不填|
示例: `[CQ:xml,data=xxxx]`
####一些xml样例
####ps:重要:xml中的value部分,记得html实体化处理后,再打加入到cq码中
#### qq音乐
```xml
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0" url="https://i.y.qq.com/v8/playsong.html?_wv=1&amp;songid=4830342&amp;souce=qqshare&amp;source=qqshare&amp;ADTAG=qqshare" flag="0" adverSign="0" multiMsgFlag="0" ><item layout="2"><audio cover="http://imgcache.qq.com/music/photo/album_500/26/500_albumpic_89526_0.jpg" src="http://ws.stream.qqmusic.qq.com/C400003mAan70zUy5O.m4a?guid=1535153710&amp;vkey=D5315B8C0603653592AD4879A8A3742177F59D582A7A86546E24DD7F282C3ACF81526C76E293E57EA1E42CF19881C561275D919233333ADE&amp;uin=&amp;fromtag=3" /><title>十年</title><summary>陈奕迅</summary></item><source name="QQ音乐" 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
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="&#91;分享&#93; 十年" sourceMsgId="0" url="http://music.163.com/m/song/409650368" flag="0" adverSign="0" multiMsgFlag="0" ><item layout="2"><audio cover="http://p2.music.126.net/g-Qgb9ibk9Wp_0HWra0xQQ==/16636710440565853.jpg?param=90y90" src="https://music.163.com/song/media/outer/url?id=409650368.mp3" /><title>十年</title><summary>黄梦之</summary></item><source name="网易云音乐" icon="https://pic.rmb.bdstatic.com/911423bee2bef937975b29b265d737b3.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.netease.cloudmusic" i_actionData="tencent100495085://" appid="100495085" /></msg>
```
#### 卡片消息1
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<msg serviceID="1">
<item><title>生死8秒!女司机高速急刹,他一个操作救下一车性命</title></item>
<source name="官方认证消息" icon="https://qzs.qq.com/ac/qzone_v5/client/auth_icon.png" action="" appid="-1" />
</msg>
```
#### 卡片消息2
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<msg serviceID="1">
<item layout="4">
<title>test title</title>
<picture cover="http://url.cn/5CEwIUy"/>
</item>
</msg>
```
## API
......
......@@ -2,20 +2,34 @@ package global
import (
"encoding/json"
"os"
"strconv"
"time"
log "github.com/sirupsen/logrus"
)
type JsonConfig struct {
Uin int64 `json:"uin"`
Password string `json:"password"`
EnableDB bool `json:"enable_db"`
AccessToken string `json:"access_token"`
ReLogin bool `json:"relogin"`
ReLoginDelay int `json:"relogin_delay"`
HttpConfig *GoCQHttpConfig `json:"http_config"`
WSConfig *GoCQWebsocketConfig `json:"ws_config"`
ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
Debug bool `json:"debug"`
Uin int64 `json:"uin"`
Password string `json:"password"`
EncryptPassword bool `json:"encrypt_password"`
PasswordEncrypted string `json:"password_encrypted"`
EnableDB bool `json:"enable_db"`
AccessToken string `json:"access_token"`
ReLogin struct {
Enabled bool `json:"enabled"`
ReLoginDelay int `json:"relogin_delay"`
MaxReloginTimes uint `json:"max_relogin_times"`
} `json:"relogin"`
IgnoreInvalidCQCode bool `json:"ignore_invalid_cqcode"`
ForceFragmented bool `json:"force_fragmented"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
HttpConfig *GoCQHttpConfig `json:"http_config"`
WSConfig *GoCQWebsocketConfig `json:"ws_config"`
ReverseServers []*GoCQReverseWebsocketConfig `json:"ws_reverse_servers"`
PostMessageFormat string `json:"post_message_format"`
Debug bool `json:"debug"`
LogLevel string `json:"log_level"`
}
type CQHttpApiConfig struct {
......@@ -41,6 +55,7 @@ type GoCQHttpConfig struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port uint16 `json:"port"`
Timeout int32 `json:"timeout"`
PostUrls map[string]string `json:"post_urls"`
}
......@@ -61,6 +76,17 @@ type GoCQReverseWebsocketConfig struct {
func DefaultConfig() *JsonConfig {
return &JsonConfig{
EnableDB: true,
ReLogin: struct {
Enabled bool `json:"enabled"`
ReLoginDelay int `json:"relogin_delay"`
MaxReloginTimes uint `json:"max_relogin_times"`
}{
Enabled: true,
ReLoginDelay: 3,
MaxReloginTimes: 0,
},
PostMessageFormat: "string",
ForceFragmented: true,
HttpConfig: &GoCQHttpConfig{
Enabled: true,
Host: "0.0.0.0",
......@@ -93,6 +119,8 @@ func Load(p string) *JsonConfig {
err := json.Unmarshal([]byte(ReadAllText(p)), &c)
if err != nil {
log.Warnf("尝试加载配置文件 %v 时出现错误: %v", p, err)
log.Infoln("原文件已备份")
os.Rename(p, p+".backup"+strconv.FormatInt(time.Now().Unix(), 10))
return nil
}
return &c
......
package global
import (
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"io/ioutil"
"regexp"
"strings"
"sync"
)
type Filter interface {
Eval(payload gjson.Result) bool
}
type OperationNode struct {
key string
filter Filter
}
type NotOperator struct {
operand_ Filter
}
func notOperatorConstruct(argument gjson.Result) *NotOperator {
if !argument.IsObject() {
log.Error("the argument of 'not' operator must be an object")
}
op := new(NotOperator)
op.operand_ = GetOperatorFactory().Generate("and", argument)
return op
}
func (notOperator NotOperator) Eval(payload gjson.Result) bool {
log.Debug("not " + payload.Str)
return !(notOperator.operand_).Eval(payload)
}
type AndOperator struct {
operands []OperationNode
}
func andOperatorConstruct(argument gjson.Result) *AndOperator {
if !argument.IsObject() {
log.Error("the argument of 'and' operator must be an object")
}
op := new(AndOperator)
argument.ForEach(func(key, value gjson.Result) bool {
if key.Str[0] == '.' {
// is an operator
// ".foo": {
// "bar": "baz"
// }
opKey := key.Str[1:]
op.operands = append(op.operands, OperationNode{"", GetOperatorFactory().Generate(opKey, value)})
} else if value.IsObject() {
// is an normal key with an object as the value
// "foo": {
// ".bar": "baz"
// }
opKey := key.Str
op.operands = append(op.operands, OperationNode{opKey, GetOperatorFactory().Generate("and", value)})
} else {
// is an normal key with a non-object as the value
// "foo": "bar"
opKey := key.Str
op.operands = append(op.operands, OperationNode{opKey, GetOperatorFactory().Generate("eq", value)})
}
return true
})
return op
}
func (andOperator *AndOperator) Eval(payload gjson.Result) bool {
log.Debug("and " + payload.Str)
res := true
for _, operand := range andOperator.operands {
if len(operand.key) == 0 {
// is an operator
res = res && operand.filter.Eval(payload)
} else {
// is an normal key
val := payload.Get(operand.key)
res = res && operand.filter.Eval(val)
}
if res == false {
break
}
}
return res
}
type OrOperator struct {
operands []Filter
}
func orOperatorConstruct(argument gjson.Result) *OrOperator {
if !argument.IsArray() {
log.Error("the argument of 'or' operator must be an array")
}
op := new(OrOperator)
argument.ForEach(func(_, value gjson.Result) bool {
op.operands = append(op.operands, GetOperatorFactory().Generate("and", value))
return true
})
return op
}
func (orOperator OrOperator) Eval(payload gjson.Result) bool {
log.Debug("or "+ payload.Str)
res:= false
for _, operand := range orOperator.operands {
res = res || operand.Eval(payload)
if res == true {
break
}
}
return res
}
type EqualOperator struct {
value gjson.Result
}
func equalOperatorConstruct(argument gjson.Result) *EqualOperator {
op := new(EqualOperator)
op.value = argument
return op
}
func (equalOperator EqualOperator) Eval(payload gjson.Result) bool {
log.Debug("eq "+ payload.Str + "==" + equalOperator.value.Str)
return payload.Str == equalOperator.value.Str
}
type NotEqualOperator struct {
value gjson.Result
}
func notEqualOperatorConstruct(argument gjson.Result) *NotEqualOperator {
op := new(NotEqualOperator)
op.value = argument
return op
}
func (notEqualOperator NotEqualOperator) Eval(payload gjson.Result) bool {
log.Debug("neq " + payload.Str)
return !(payload.Str == notEqualOperator.value.Str)
}
type InOperator struct {
operand gjson.Result
}
func inOperatorConstruct(argument gjson.Result) *InOperator {
if argument.IsObject() {
log.Error("the argument of 'in' operator must be an array or a string")
}
op := new(InOperator)
op.operand = argument
return op
}
func (inOperator InOperator) Eval(payload gjson.Result) bool {
log.Debug("in " + payload.Str)
if inOperator.operand.IsArray() {
res := false
inOperator.operand.ForEach(func(key, value gjson.Result) bool {
res = res || value.Str == payload.Str
return true
})
return res
}
return strings.Contains(inOperator.operand.Str, payload.Str)
}
type ContainsOperator struct {
operand string
}
func containsOperatorConstruct(argument gjson.Result) *ContainsOperator {
if argument.IsArray() || argument.IsObject() {
log.Error("the argument of 'contains' operator must be a string")
}
op := new(ContainsOperator)
op.operand = argument.Str
return op
}
func (containsOperator ContainsOperator) Eval(payload gjson.Result) bool {
log.Debug("contains "+ payload.Str)
if payload.IsObject() || payload.IsArray() {
return false
}
return strings.Contains(payload.String(), containsOperator.operand)
}
type RegexOperator struct {
regex string
}
func regexOperatorConstruct(argument gjson.Result) *RegexOperator {
if argument.IsArray() || argument.IsObject() {
log.Error("the argument of 'regex' operator must be a string")
}
op := new(RegexOperator)
op.regex = argument.Str
return op
}
func (containsOperator RegexOperator) Eval(payload gjson.Result) bool {
log.Debug("regex " + payload.Str)
matched, _ := regexp.MatchString(containsOperator.regex, payload.Str)
return matched
}
// 单例工厂
type operatorFactory struct{
}
var instance *operatorFactory = &operatorFactory{}
func GetOperatorFactory() *operatorFactory {
return instance
}
func (o operatorFactory) Generate(opName string, argument gjson.Result) Filter {
switch opName {
case "not":
return notOperatorConstruct(argument)
case "and":
return andOperatorConstruct(argument)
case "or":
return orOperatorConstruct(argument)
case "neq":
return notEqualOperatorConstruct(argument)
case "eq":
return equalOperatorConstruct(argument)
case "in":
return inOperatorConstruct(argument)
case "contains":
return containsOperatorConstruct(argument)
case "regex":
return regexOperatorConstruct(argument)
default:
log.Warnf("the operator '%s' is not supported", opName)
return nil
}
}
var filter = new(Filter)
var once sync.Once // 过滤器单例模式
func GetFilter() *Filter {
once.Do(func() {
f, err := ioutil.ReadFile("filter.json")
if err != nil {
filter = nil
} else {
*filter = GetOperatorFactory().Generate("and", gjson.ParseBytes(f))
}
})
return filter
}
\ No newline at end of file
package global
import (
log "github.com/sirupsen/logrus"
"bytes"
"io/ioutil"
"os"
"path"
log "github.com/sirupsen/logrus"
)
var IMAGE_PATH = path.Join("data", "images")
var (
IMAGE_PATH = path.Join("data", "images")
VOICE_PATH = path.Join("data", "voices")
VIDEO_PATH = path.Join("data", "videos")
CACHE_PATH = path.Join("data", "cache")
HEADER_AMR = []byte("#!AMR")
HEADER_SILK = []byte("\x02#!SILK_V3")
)
func PathExists(path string) bool {
_, err := os.Stat(path)
......@@ -23,7 +33,7 @@ func ReadAllText(path string) string {
}
func WriteAllText(path, text string) {
_ = ioutil.WriteFile(path, []byte(text), 0777)
_ = ioutil.WriteFile(path, []byte(text), 0644)
}
func Check(err error) {
......@@ -31,3 +41,7 @@ func Check(err error) {
log.Fatalf("遇到错误: %v", err)
}
}
func IsAMRorSILK(b []byte) bool {
return bytes.HasPrefix(b, HEADER_AMR) || bytes.HasPrefix(b, HEADER_SILK)
}
......@@ -3,9 +3,14 @@ package global
import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/Mrs4s/MiraiGo/message"
"github.com/tidwall/gjson"
)
func GetBytes(url string) ([]byte, error) {
......@@ -32,3 +37,35 @@ func GetBytes(url string) ([]byte, error) {
}
return body, nil
}
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}}`)
if err != nil {
return gjson.Result{}, err
}
return gjson.ParseBytes(d).Get("songinfo.data"), nil
}
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))
if err != nil {
return gjson.Result{}, err
}
return gjson.ParseBytes(d).Get("songs.0"), nil
}
func NewXmlMsg(template string, ResId int64) *message.ServiceElement {
var serviceid string
if ResId == 0 {
serviceid = "2" //默认值2
} else {
serviceid = strconv.FormatInt(ResId, 10)
}
//println(serviceid)
return &message.ServiceElement{
Id: int32(ResId),
Content: template,
ResId: serviceid,
SubType: "xml",
}
}
package global
import (
"github.com/tidwall/gjson"
"strings"
)
var trueSet = map[string]struct{}{
"true": {},
"yes": {},
"1": {},
}
var falseSet = map[string]struct{}{
"false": {},
"no": {},
"0": {},
}
func EnsureBool(p interface{}, defaultVal bool) bool {
var str string
if b, ok := p.(bool); ok {
return b
}
if j, ok := p.(gjson.Result); ok {
if !j.Exists() {
return defaultVal
}
if j.Type == gjson.True {
return true
}
if j.Type == gjson.False {
return false
}
if j.Type != gjson.String {
return defaultVal
}
str = j.Str
} else if s, ok := p.(string); ok {
str = s
}
str = strings.ToLower(str)
if _, ok := trueSet[str]; ok {
return true
}
if _, ok := falseSet[str]; ok {
return false
}
return defaultVal
}
......@@ -3,18 +3,24 @@ module github.com/Mrs4s/go-cqhttp
go 1.14
require (
github.com/Mrs4s/MiraiGo v0.0.0-20200804064012-e1e00ed0683b
github.com/Mrs4s/MiraiGo v0.0.0-20200827182935-51e155ef20da
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
github.com/gin-gonic/gin v1.6.3
github.com/gorilla/websocket v1.4.2
github.com/guonaihong/gout v0.1.1
github.com/guonaihong/gout v0.1.2
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
github.com/jonboulle/clockwork v0.2.0 // indirect
github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible
github.com/lestrrat-go/strftime v1.0.1 // indirect
github.com/lestrrat-go/strftime v1.0.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/sirupsen/logrus v1.6.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
github.com/tidwall/gjson v1.6.0
github.com/tebeka/strftime v0.1.5 // indirect
github.com/tidwall/gjson v1.6.1
github.com/xujiajun/nutsdb v0.5.0
github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sys v0.0.0-20200828194041-157a740278f4 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)
This diff is collapsed.
This diff is collapsed.
......@@ -4,11 +4,13 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"os"
"strconv"
"strings"
"time"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/gin-gonic/gin"
"github.com/guonaihong/gout"
log "github.com/sirupsen/logrus"
......@@ -21,9 +23,10 @@ type httpServer struct {
}
type httpClient struct {
bot *coolq.CQBot
secret string
addr string
bot *coolq.CQBot
secret string
addr string
timeout int32
}
var HttpServer = &httpServer{}
......@@ -38,7 +41,7 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
c.Status(404)
return
}
if c.Request.Method == "POST" && c.Request.Header.Get("Content-Type") == "application/json" {
if c.Request.Method == "POST" && strings.Contains(c.Request.Header.Get("Content-Type"), "application/json") {
d, err := c.GetRawData()
if err != nil {
log.Warnf("获取请求 %v 的Body时出现错误: %v", c.Request.RequestURI, err)
......@@ -132,12 +135,12 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
s.engine.Any("/set_group_leave_async", s.SetGroupLeave)
s.engine.Any("/get_image", s.GetImage)
s.engine.Any("/get_image_async", s.GetImage)
s.engine.Any("/get_forward_msg", s.GetForwardMessage)
s.engine.Any("/get_group_msg", s.GetGroupMessage)
s.engine.Any("/get_group_msg_async", s.GetGroupMessage)
s.engine.Any("/get_group_honor_info", s.GetGroupHonorInfo)
s.engine.Any("/can_send_image", s.CanSendImage)
s.engine.Any("/can_send_image_async", s.CanSendImage)
......@@ -155,7 +158,13 @@ func (s *httpServer) Run(addr, authToken string, bot *coolq.CQBot) {
go func() {
log.Infof("CQ HTTP 服务器已启动: %v", addr)
log.Fatal(s.engine.Run(addr))
err := s.engine.Run(addr)
if err != nil {
log.Error(err)
log.Infof("请检查端口是否被占用.")
time.Sleep(time.Second * 5)
os.Exit(1)
}
}()
}
......@@ -163,10 +172,14 @@ func NewHttpClient() *httpClient {
return &httpClient{}
}
func (c *httpClient) Run(addr, secret string, bot *coolq.CQBot) {
func (c *httpClient) Run(addr, secret string, timeout int32, bot *coolq.CQBot) {
c.bot = bot
c.secret = secret
c.addr = addr
c.timeout = timeout
if c.timeout < 5 {
c.timeout = 5
}
bot.OnEventPush(c.onBotPushEvent)
log.Infof("HTTP POST上报器已启动: %v", addr)
}
......@@ -184,7 +197,7 @@ func (c *httpClient) onBotPushEvent(m coolq.MSG) {
h["X-Signature"] = "sha1=" + hex.EncodeToString(mac.Sum(nil))
}
return h
}()).SetTimeout(time.Second * 5).Do()
}()).SetTimeout(time.Second * time.Duration(c.timeout)).Do()
if err != nil {
log.Warnf("上报Event数据到 %v 失败: %v", c.addr, err)
return
......@@ -203,7 +216,8 @@ func (s *httpServer) GetFriendList(c *gin.Context) {
}
func (s *httpServer) GetGroupList(c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupList())
nc := getParamOrDefault(c, "no_cache", "false")
c.JSON(200, s.bot.CQGetGroupList(nc == "true"))
}
func (s *httpServer) GetGroupInfo(c *gin.Context) {
......@@ -224,6 +238,14 @@ func (s *httpServer) GetGroupMemberInfo(c *gin.Context) {
}
func (s *httpServer) SendMessage(c *gin.Context) {
if getParam(c, "message_type") == "private" {
s.SendPrivateMessage(c)
return
}
if getParam(c, "message_type") == "group" {
s.SendGroupMessage(c)
return
}
if getParam(c, "group_id") != "" {
s.SendGroupMessage(c)
return
......@@ -235,22 +257,24 @@ func (s *httpServer) SendMessage(c *gin.Context) {
func (s *httpServer) SendPrivateMessage(c *gin.Context) {
uid, _ := strconv.ParseInt(getParam(c, "user_id"), 10, 64)
msg := getParam(c, "message")
if gjson.Valid(msg) {
c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg)))
msg, t := getParamWithType(c, "message")
autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
if t == gjson.JSON {
c.JSON(200, s.bot.CQSendPrivateMessage(uid, gjson.Parse(msg), autoEscape))
return
}
c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg))
c.JSON(200, s.bot.CQSendPrivateMessage(uid, msg, autoEscape))
}
func (s *httpServer) SendGroupMessage(c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
msg := getParam(c, "message")
if gjson.Valid(msg) {
c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg)))
msg, t := getParamWithType(c, "message")
autoEscape := global.EnsureBool(getParam(c, "auto_escape"), false)
if t == gjson.JSON {
c.JSON(200, s.bot.CQSendGroupMessage(gid, gjson.Parse(msg), autoEscape))
return
}
c.JSON(200, s.bot.CQSendGroupMessage(gid, msg))
c.JSON(200, s.bot.CQSendGroupMessage(gid, msg, autoEscape))
}
func (s *httpServer) SendGroupForwardMessage(c *gin.Context) {
......@@ -269,6 +293,11 @@ func (s *httpServer) GetGroupMessage(c *gin.Context) {
c.JSON(200, s.bot.CQGetGroupMessage(int32(mid)))
}
func (s *httpServer) GetGroupHonorInfo(c *gin.Context) {
gid, _ := strconv.ParseInt(getParam(c, "group_id"), 10, 64)
c.JSON(200, s.bot.CQGetGroupHonorInfo(gid, getParam(c, "type")))
}
func (s *httpServer) ProcessFriendRequest(c *gin.Context) {
flag := getParam(c, "flag")
approve := getParamOrDefault(c, "approve", "true")
......@@ -282,7 +311,7 @@ func (s *httpServer) ProcessGroupRequest(c *gin.Context) {
subType = getParam(c, "type")
}
approve := getParamOrDefault(c, "approve", "true")
c.JSON(200, s.bot.CQProcessGroupRequest(flag, subType, approve == "true"))
c.JSON(200, s.bot.CQProcessGroupRequest(flag, subType, getParam(c, "reason"), approve == "true"))
}
func (s *httpServer) SetGroupCard(c *gin.Context) {
......@@ -371,37 +400,43 @@ func getParamOrDefault(c *gin.Context, k, def string) string {
return def
}
func getParam(c *gin.Context, k string) string {
p, _ := getParamWithType(c, k)
return p
}
func getParamWithType(c *gin.Context, k string) (string, gjson.Type) {
if q := c.Query(k); q != "" {
return q
return q, gjson.Null
}
if c.Request.Method == "POST" {
if h := c.Request.Header.Get("Content-Type"); h != "" {
if h == "application/x-www-form-urlencoded" {
if strings.Contains(h, "application/x-www-form-urlencoded") {
if p, ok := c.GetPostForm(k); ok {
return p
return p, gjson.Null
}
}
if h == "application/json" {
if strings.Contains(h, "application/json") {
if obj, ok := c.Get("json_body"); ok {
res := obj.(gjson.Result).Get(k)
if res.Exists() {
switch res.Type {
case gjson.JSON:
return res.Raw
return res.Raw, gjson.JSON
case gjson.String:
return res.Str
return res.Str, gjson.String
case gjson.Number:
return strconv.FormatInt(res.Int(), 10) // 似乎没有需要接受 float 类型的api
return strconv.FormatInt(res.Int(), 10), gjson.Number // 似乎没有需要接受 float 类型的api
case gjson.True:
return "true"
return "true", gjson.True
case gjson.False:
return "false"
return "false", gjson.False
}
}
}
}
}
}
return ""
return "", gjson.Null
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment