From 1e56c4f986e4d268ecad84591d819a16ed13cbea Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Tue, 5 Jan 2021 01:25:29 +0800 Subject: [PATCH 01/11] update doc. --- docs/cqhttp.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cqhttp.md b/docs/cqhttp.md index 48b71b5..ae20d37 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -71,6 +71,7 @@ Type : `image` | `url` | - | 图片 URL | | `cache` | `0` `1` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `1` | | `id` | - | 发送秀图时的特效id,默认为40000 | +| `c` | `2` `3` | 通过网络下载图片时的线程数, 默认单线程. (在资源不支持并发时会自动处理)| 可用的特效ID: From 6113eb9200e6a1035ea82b6084810ef83e24c58f Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Wed, 6 Jan 2021 02:36:56 +0800 Subject: [PATCH 02/11] update MiraiGo. --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index f32f52d..ea327d8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Mrs4s/go-cqhttp go 1.15 require ( - github.com/Mrs4s/MiraiGo v0.0.0-20210104171920-510924ba979c + github.com/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56 github.com/dustin/go-humanize v1.0.0 github.com/gin-contrib/pprof v1.3.0 github.com/gin-gonic/gin v1.6.3 diff --git a/go.sum b/go.sum index 1a68549..2f1571a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ 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/Mrs4s/MiraiGo v0.0.0-20210104164708-e94ea61cc8e9 h1:jzaBgDPLKRuJxQO9UT/2vCFL0ScCO9IThRCa7ViMbGE= -github.com/Mrs4s/MiraiGo v0.0.0-20210104164708-e94ea61cc8e9/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo= -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/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56 h1:U7kObHDk3RfaD81+1hA29gxHf3PfRGpX7dqR2UPNO0c= +github.com/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo= 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 0e5276ea4d6b71fa9da7431494251eb4c18af727 Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Wed, 6 Jan 2021 02:38:21 +0800 Subject: [PATCH 03/11] fix ptt. --- coolq/bot.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/coolq/bot.go b/coolq/bot.go index 03c4a65..0fc72fa 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -5,7 +5,6 @@ import ( "encoding/gob" "fmt" "hash/crc32" - "io/ioutil" "os" "path" "runtime/debug" @@ -150,8 +149,8 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int newElem = append(newElem, gm) continue } - if i, ok := elem.(*LocalVoiceElement); ok { - gv, err := bot.Client.UploadGroupPtt(groupId, i.Stream) + if i, ok := elem.(*message.VoiceElement); ok { + gv, err := bot.Client.UploadGroupPtt(groupId, bytes.NewReader(i.Data)) if err != nil { log.Warnf("警告: 群 %v 消息语音上传失败: %v", groupId, err) continue @@ -270,13 +269,8 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in bot.Client.SendFriendPoke(i.Target) return 0 } - if i, ok := elem.(*LocalVoiceElement); ok { - data, err := ioutil.ReadAll(i.Stream) - if err != nil { - log.Warnf("警告: 好友 %v 消息语音读取失败: %v", target, err) - continue - } - fv, err := bot.Client.UploadPrivatePtt(target, data) + if i, ok := elem.(*message.VoiceElement); ok { + fv, err := bot.Client.UploadPrivatePtt(target, i.Data) if err != nil { log.Warnf("警告: 好友 %v 消息语音上传失败: %v", target, err) continue From f98c334089e36b9a1296ccf04965c694b2581c86 Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Wed, 6 Jan 2021 12:46:10 +0800 Subject: [PATCH 04/11] fix reconnect --- server/apiAdmin.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index 009ebb8..a24b24d 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -266,6 +266,8 @@ func (s *webServer) Dologin() { log.Info("アトリは、高性能ですから!") cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { if conf.ReLogin.Enabled { + conf.ReLogin.Enabled = false + defer func() { conf.ReLogin.Enabled = true }() var times uint = 1 for { if cli.Online { From 2bbf969cfad704763c15e030f6742843c0b07ae0 Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Wed, 6 Jan 2021 21:27:07 +0800 Subject: [PATCH 05/11] feature download_file. --- coolq/api.go | 22 ++++++++++++++++++++++ coolq/cqcode.go | 2 +- global/net.go | 12 +++++++++++- server/http.go | 28 ++++++++++++++++++++++++++++ server/websocket.go | 22 ++++++++++++++++++++++ 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index e3b20d4..b453c78 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -1,9 +1,12 @@ package coolq import ( + "crypto/md5" + "encoding/hex" "io/ioutil" "os" "path" + "path/filepath" "runtime" "strconv" "strings" @@ -741,6 +744,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.CACHE_PATH, 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 { m := bot.Client.GetForwardMessage(resId) if m == nil { diff --git a/coolq/cqcode.go b/coolq/cqcode.go index ef2dd93..16cd7d5 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -833,7 +833,7 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa _ = os.Remove(cacheFile) } thread, _ := strconv.Atoi(c) - if err := global.DownloadFileMultiThreading(f, cacheFile, maxImageSize, thread); err != nil { + if err := global.DownloadFileMultiThreading(f, cacheFile, maxImageSize, thread, nil); err != nil { return nil, err } return &LocalImageElement{File: cacheFile}, nil diff --git a/global/net.go b/global/net.go index 1ce5797..715b1a0 100644 --- a/global/net.go +++ b/global/net.go @@ -99,7 +99,7 @@ func DownloadFile(url, path string, limit int64) error { return nil } -func DownloadFileMultiThreading(url, path string, limit int64, threadCount int) error { +func DownloadFileMultiThreading(url, path string, limit int64, threadCount int, headers map[string]string) error { if threadCount < 2 { return DownloadFile(url, path, limit) } @@ -128,6 +128,11 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int) if err != nil { return err } + if headers != nil { + for k, v := range headers { + req.Header.Set(k, v) + } + } req.Header.Set("range", "bytes=0-") resp, err := client.Do(req) if err != nil { @@ -184,6 +189,11 @@ func DownloadFileMultiThreading(url, path string, limit int64, threadCount int) _, _ = file.Seek(block.BeginOffset, io.SeekStart) writer := bufio.NewWriter(file) defer writer.Flush() + if headers != nil { + for k, v := range headers { + req.Header.Set(k, v) + } + } req.Header.Set("range", "bytes="+strconv.FormatInt(block.BeginOffset, 10)+"-"+strconv.FormatInt(block.EndOffset, 10)) resp, err := client.Do(req) if err != nil { diff --git a/server/http.go b/server/http.go index 80aaf32..b312d82 100644 --- a/server/http.go +++ b/server/http.go @@ -428,6 +428,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) { img := getParam(c, "image") c.JSON(200, s.bot.CQOcrImage(img)) @@ -535,6 +562,7 @@ var httpApi = map[string]func(s *httpServer, c *gin.Context){ "reload_event_filter": ReloadEventFilter, "set_group_portrait": SetGroupPortrait, "set_group_anonymous_ban": SetGroupAnonymousBan, + "download_file": DownloadFile, ".handle_quick_operation": HandleQuickOperation, ".ocr_image": OcrImage, "ocr_image": OcrImage, diff --git a/server/websocket.go b/server/websocket.go index 15fc88d..0ab5789 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -490,6 +490,28 @@ var wsApi = map[string]func(*coolq.CQBot, gjson.Result) coolq.MSG{ "get_msg": func(bot *coolq.CQBot, p gjson.Result) coolq.MSG { 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 { return bot.CQGetGroupHonorInfo(p.Get("group_id").Int(), p.Get("type").Str) }, From 938ebf630f3a2a797c73ee761c70435e1edfb422 Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Wed, 6 Jan 2021 21:43:55 +0800 Subject: [PATCH 06/11] update doc. --- docs/cqhttp.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/cqhttp.md b/docs/cqhttp.md index ae20d37..2ecbc67 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -760,6 +760,46 @@ Type: `tts` | `remain_at_all_count_for_group` | int16 | 群内所有管理当天剩余@全体成员次数 | | `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码作为图片或语音发送 +> 调用后会阻塞直到下载完成后才会返回数据,请注意下载大文件时的超时 + ### 获取用户VIP信息 终结点:`/_get_vip_info` From 1843bd6a4e04aa0383e75bec851b909644abbc15 Mon Sep 17 00:00:00 2001 From: Mrs4s Date: Wed, 6 Jan 2021 22:08:02 +0800 Subject: [PATCH 07/11] fix reconnect loop on account banned. --- server/apiAdmin.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index a24b24d..64af4d6 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -295,6 +295,9 @@ func (s *webServer) Dologin() { log.Fatalf("重连失败: 设备锁") default: log.Errorf("重连失败: %v", rsp.ErrorMessage) + if strings.Contains(rsp.ErrorMessage, "冻结") { + log.Fatalf("账号被冻结, 放弃重连") + } cli.Disconnect() continue } From 2d020bc7f72ff3ee95e827e641a9dde1cb86a164 Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Fri, 8 Jan 2021 12:14:09 +0800 Subject: [PATCH 08/11] drop go-silk --- global/codec.go | 26 ++++------- global/codec/codec.go | 102 +++++++++++++++++++++++++++++++++++++++++ global/codec/codec2.go | 14 ++++++ go.mod | 1 - go.sum | 2 - 5 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 global/codec/codec.go create mode 100644 global/codec/codec2.go diff --git a/global/codec.go b/global/codec.go index bc9b9e0..90d51b4 100644 --- a/global/codec.go +++ b/global/codec.go @@ -4,31 +4,25 @@ import ( "crypto/md5" "errors" "fmt" + "github.com/Mrs4s/go-cqhttp/global/codec" + log "github.com/sirupsen/logrus" "io/ioutil" "path" - "sync" - - log "github.com/sirupsen/logrus" - "github.com/wdvxdr1123/go-silk/silk" ) -var codec silk.Encoder -var useCodec = true -var once sync.Once +var useSilkCodec = true func InitCodec() { - once.Do(func() { - log.Info("正在加载silk编码器...") - err := codec.Init("data/cache", "codec") - if err != nil { - log.Error(err) - useCodec = false - } - }) + log.Info("正在加载silk编码器...") + err := codec.Init("data/cache", "codec") + if err != nil { + log.Error(err) + useSilkCodec = false + } } func Encoder(data []byte) ([]byte, error) { - if useCodec == false { + if useSilkCodec == false { return nil, errors.New("no silk encoder") } h := md5.New() diff --git a/global/codec/codec.go b/global/codec/codec.go new file mode 100644 index 0000000..5f7d2d4 --- /dev/null +++ b/global/codec/codec.go @@ -0,0 +1,102 @@ +// +build linux windows darwin +// +build 386 amd64 arm + +package codec + +import ( + "errors" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path" + "runtime" +) + +var ( + codecDir string + encoderPath string + cachePath string +) + +func downloadCodec(url string, path string) (err error) { + resp, err := http.Get(url) + if runtime.GOOS == "windows" { + path = path + ".exe" + } + if err != nil { + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + err = ioutil.WriteFile(path, body, os.ModePerm) + return +} + +func Init(cachePath, codecPath string) error { + appPath, err := os.Executable() + appPath = path.Dir(appPath) + if err != nil { + return err + } + cachePath = path.Join(appPath, cachePath) + codecDir = path.Join(appPath, codecPath) + if !fileExist(codecDir) { + _ = os.MkdirAll(codecDir, os.ModePerm) + } + if !fileExist(cachePath) { + _ = os.MkdirAll(cachePath, os.ModePerm) + } + encoderFile := runtime.GOOS + "-" + runtime.GOARCH + "-encoder" + encoderPath = path.Join(codecDir, encoderFile) + if !fileExist(encoderPath) { + if err = downloadCodec("https://cdn.jsdelivr.net/gh/wdvxdr1123/tosilk/codec/"+encoderFile, encoderPath); err != nil { + return errors.New("下载依赖失败") + } + } + if runtime.GOOS == "windows" { + encoderPath = encoderPath + ".exe" + } + return nil +} + +func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) { + // 1. 写入缓存文件 + rawPath := path.Join(cachePath, tempName+".wav") + err := ioutil.WriteFile(rawPath, record, os.ModePerm) + if err != nil { + return nil, err + } + defer os.Remove(rawPath) + + // 2.转换pcm + pcmPath := path.Join(cachePath, tempName+".pcm") + cmd := exec.Command("ffmpeg", "-i", rawPath, "-f", "s16le", "-ar", "24000", "-ac", "1", pcmPath) + if err = cmd.Run(); err != nil { + return nil, err + } + defer os.Remove(pcmPath) + + // 3. 转silk + silkPath := path.Join(cachePath, tempName+".silk") + cmd = exec.Command(encoderPath, pcmPath, silkPath, "-rate", "24000", "-quiet", "-tencent") + if err = cmd.Run(); err != nil { + return nil, err + } + if useCache == false { + 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) +} diff --git a/global/codec/codec2.go b/global/codec/codec2.go new file mode 100644 index 0000000..e50133e --- /dev/null +++ b/global/codec/codec2.go @@ -0,0 +1,14 @@ +// +build !linux,!windows,!darwin +// +build !386,!amd64,!arm + +package codec + +import "errors" + +func Init() error { + return errors.New("not support now") +} + +func EncodeToSilk(record []byte, tempName string, useCache bool) ([]byte, error) { + return nil, errors.New("not support now") +} diff --git a/go.mod b/go.mod index ea327d8..c2043a8 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/syndtr/goleveldb v1.0.0 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 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 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 diff --git a/go.sum b/go.sum index 2f1571a..3554dd5 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,6 @@ 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/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 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/go.mod h1:rIrm5geMiBhPQkdfUm8gDFi/WiHneOp1i9KjmJqc+9I= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From f501b311527281a10b8000c84a8a2dfa9260ae67 Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Fri, 8 Jan 2021 14:12:25 +0800 Subject: [PATCH 09/11] encode mp4 --- .gitignore | 5 +++++ coolq/cqcode.go | 3 +-- global/codec.go | 13 ++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 31e3ac6..4b43257 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ vendor/ .idea +config.hjson +device.json +codec/ +data/ +logs/ \ No newline at end of file diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 16cd7d5..ba472ff 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -605,7 +605,6 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf } }() data, err := bot.Client.GetTts(d["text"]) - ioutil.WriteFile("tts.silk", data, 777) if err != nil { return nil, err } @@ -620,7 +619,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf return nil, err } if !global.IsAMRorSILK(data) { - data, err = global.Encoder(data) + data, err = global.EncoderSilk(data) if err != nil { return nil, err } diff --git a/global/codec.go b/global/codec.go index 90d51b4..346166c 100644 --- a/global/codec.go +++ b/global/codec.go @@ -7,6 +7,7 @@ import ( "github.com/Mrs4s/go-cqhttp/global/codec" log "github.com/sirupsen/logrus" "io/ioutil" + "os/exec" "path" ) @@ -21,7 +22,7 @@ func InitCodec() { } } -func Encoder(data []byte) ([]byte, error) { +func EncoderSilk(data []byte) ([]byte, error) { if useSilkCodec == false { return nil, errors.New("no silk encoder") } @@ -37,3 +38,13 @@ func Encoder(data []byte) ([]byte, error) { } return slk, nil } + +func EncodeMP4(src string, dst string) error { + cmd := exec.Command("ffmpeg", "-i", src, "-c", "copy", "-map", "0", dst) + return cmd.Run() +} + +func ExtractCover(src string, dst string) error { + cmd := exec.Command("ffmpeg", "-i", src, "-y", "-r", "1", "-f", "image2", dst) + return cmd.Run() +} From b717e2362657ca973079795dd89436330b3bd31f Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Fri, 8 Jan 2021 15:40:32 +0800 Subject: [PATCH 10/11] feat CQ:video --- coolq/api.go | 2 +- coolq/bot.go | 11 ++++- coolq/cqcode.go | 129 +++++++++++++++++++++++++++++++++++++----------- global/codec.go | 2 +- go.mod | 2 +- go.sum | 2 + 6 files changed, 116 insertions(+), 32 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index b453c78..583122a 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -837,7 +837,7 @@ func (bot *CQBot) CQCanSendRecord() 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 { log.Warnf("load image error: %v", err) return Failed(100, "LOAD_FILE_ERROR", err.Error()) diff --git a/coolq/bot.go b/coolq/bot.go index 0fc72fa..a0d0130 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -158,6 +158,15 @@ func (bot *CQBot) SendGroupMessage(groupId int64, m *message.SendingMessage) int newElem = append(newElem, gv) continue } + if i, ok := elem.(*LocalVideoElement); ok { // todo:cache & multiThread + gv, err := bot.Client.UploadGroupShortVideo(groupId, i.video, i.thumb) + if err != nil { + log.Warnf("警告: 群 %v 消息短视频上传失败: %v", groupId, err) + continue + } + newElem = append(newElem, gv) + continue + } if i, ok := elem.(*PokeElement); ok { if group := bot.Client.FindGroup(groupId); group != nil { if mem := group.FindMember(i.Target); mem != nil { @@ -272,7 +281,7 @@ func (bot *CQBot) SendPrivateMessage(target int64, m *message.SendingMessage) in if i, ok := elem.(*message.VoiceElement); ok { fv, err := bot.Client.UploadPrivatePtt(target, i.Data) if err != nil { - log.Warnf("警告: 好友 %v 消息语音上传失败: %v", target, err) + log.Warnf("警告: 群 %v 消息语音上传失败: %v", target, err) continue } newElem = append(newElem, fv) diff --git a/coolq/cqcode.go b/coolq/cqcode.go index ba472ff..c2d250e 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -21,6 +21,7 @@ import ( "github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" "github.com/Mrs4s/go-cqhttp/global" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" @@ -35,7 +36,8 @@ var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) var IgnoreInvalidCQCode = 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 { Target int64 @@ -77,6 +79,13 @@ type LocalVoiceElement struct { Stream io.ReadSeeker } +type LocalVideoElement struct { + message.ShortVideoElement + File string + video io.ReadSeeker + thumb io.ReadSeeker +} + func (e *GiftElement) Type() message.ElementType { return message.At } @@ -426,7 +435,7 @@ func (bot *CQBot) ConvertStringMessage(msg string, group bool) (r []message.IMes } elem, err := bot.ToElement(t, params, group) if err != nil { - org := "[" + string(cqCode) + "]" + org := "[CQ:" + string(cqCode) + "]" if !IgnoreInvalidCQCode { log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) r = append(r, message.NewText(org)) @@ -547,7 +556,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf } return message.NewText(d["text"]), nil case "image": - img, err := bot.makeImageElem(d, group) + img, err := bot.makeImageOrVideoElem(d, false, group) if err != nil { return nil, err } @@ -769,11 +778,55 @@ func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interf if maxHeight == 0 { maxHeight = 1000 } - img, err := bot.makeImageElem(d, group) + img, err := bot.makeImageOrVideoElem(d, false, group) if err != nil { return nil, errors.New("send cardimage faild") } return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, group) + case "video": + if !group { + return nil, errors.New("unsupported private short 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 cover, ok := d["cover"]; ok { + data, _ := global.FindFile(cover, cache, global.IMAGE_PATH) + v.thumb = bytes.NewReader(data) + } + if v.thumb == nil { + _ = global.ExtractCover(v.File, v.File+".jpg") + v.thumb, _ = os.Open(v.File + ".jpg") + } + v.video, _ = os.Open(v.File) + _, err = v.video.Seek(4, io.SeekStart) + if err != nil { + return nil, err + } + var header = make([]byte, 4) + _, err = v.video.Read(header) + if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // ftyp + _, _ = v.video.Seek(0, io.SeekStart) + hash, _ := utils.GetMd5AndLength(v.video) + cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".mp4") + if global.PathExists(cacheFile) { + goto ok + } + err = global.EncodeMP4(v.File, cacheFile) + if err != nil { + return nil, err + } + ok: + v.video, _ = os.Open(cacheFile) + } + _, _ = v.video.Seek(0, io.SeekStart) + return v, nil default: return nil, errors.New("unsupported cq code: " + t) } @@ -815,7 +868,7 @@ func CQCodeUnescapeValue(content string) string { } // 图片 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"] if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { cache := d["cache"] @@ -832,18 +885,22 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa _ = os.Remove(cacheFile) } thread, _ := strconv.Atoi(c) - if err := global.DownloadFileMultiThreading(f, cacheFile, maxImageSize, thread, nil); err != nil { + var maxSize = func() int64 { + if video { + return maxVideoSize + } + return maxImageSize + }() + if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { return nil, err } + if video { + return &LocalVideoElement{ + 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") { fu, err := url.Parse(f) if err != nil { @@ -856,10 +913,28 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa if err != nil { return nil, err } + if video { + goto videos + } if info.Size() == 0 || info.Size() >= maxImageSize { return nil, errors.New("invalid image size") } return &LocalImageElement{File: fu.Path}, nil + videos: + if info.Size() == 0 || info.Size() >= maxVideoSize { + return nil, errors.New("invalid video size") + } + return &LocalVideoElement{File: fu.Path}, nil + } + if video { // 短视频视频只支持以上两种 + return nil, errors.New("invalid video") + } + 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 } rawPath := path.Join(global.IMAGE_PATH, f) if !global.PathExists(rawPath) && global.PathExists(path.Join(global.IMAGE_PATH_OLD, f)) { @@ -869,7 +944,7 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa rawPath += ".cqimg" } 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) { file, err := os.Open(rawPath) @@ -886,9 +961,11 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa if len(b) < 20 { return nil, errors.New("invalid local file") } - var size int32 - var hash []byte - var url string + var ( + size int32 + hash []byte + url string + ) if path.Ext(rawPath) == ".cqimg" { for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { kv := strings.SplitN(line, "=", 2) @@ -909,27 +986,23 @@ func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessa } if size == 0 { 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") } if len(hash) != 16 { return nil, errors.New("invalid hash") } + var rsp message.IMessageElement if group { - rsp, err := bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - if err != nil { - if url != "" { - return bot.makeImageElem(map[string]string{"file": url}, group) - } - return nil, err - } - return rsp, nil + rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) + goto ok } - 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 url != "" { - return bot.makeImageElem(map[string]string{"file": url}, group) + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) } return nil, err } diff --git a/global/codec.go b/global/codec.go index 346166c..26cd2dd 100644 --- a/global/codec.go +++ b/global/codec.go @@ -40,7 +40,7 @@ func EncoderSilk(data []byte) ([]byte, error) { } func EncodeMP4(src string, dst string) error { - cmd := exec.Command("ffmpeg", "-i", src, "-c", "copy", "-map", "0", dst) + cmd := exec.Command("ffmpeg", "-i", src, "-y", "-c", "copy", "-map", "0", dst) return cmd.Run() } diff --git a/go.mod b/go.mod index c2043a8..12ccf04 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Mrs4s/go-cqhttp go 1.15 require ( - github.com/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56 + github.com/Mrs4s/MiraiGo v0.0.0-20210107163750-ce4834c2ba71 github.com/dustin/go-humanize v1.0.0 github.com/gin-contrib/pprof v1.3.0 github.com/gin-gonic/gin v1.6.3 diff --git a/go.sum b/go.sum index 3554dd5..ced49d9 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56 h1:U7kObHDk3RfaD81+1hA29gxHf3PfRGpX7dqR2UPNO0c= github.com/Mrs4s/MiraiGo v0.0.0-20210105173234-72521dec9b56/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo= +github.com/Mrs4s/MiraiGo v0.0.0-20210107163750-ce4834c2ba71 h1:WAVoBY4G2BC5Dyw1A45r/sVua65hsBVYcncR42hEGrk= +github.com/Mrs4s/MiraiGo v0.0.0-20210107163750-ce4834c2ba71/go.mod h1:HW2e375lCQiRwtuA/LV6ZVTsi7co1TRfBn+L5Ow77Bo= 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 87ada9c57eebf93de0946348e36e0813d0b6716a Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Fri, 8 Jan 2021 16:06:53 +0800 Subject: [PATCH 11/11] update docs --- docs/cqhttp.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/cqhttp.md b/docs/cqhttp.md index 2ecbc67..0a511ac 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -270,6 +270,20 @@ Type: `node` } ] ```` +### 短视频消息 + +Type: `video` + +范围: **发送/接收** + +参数: + +| 参数名 | 类型 | 说明 | +| ------- | ------ | ------------------------------------------------| +| `file` | string | 支持http和file发送 | +| `cover` | string | 视频封面,支持http,file和base64发送,格式必须为jpg | +| `c` | `2` `3`| 通过网络下载视频时的线程数, 默认单线程. (在资源不支持并发时会自动处理)| +示例: `[CQ:image,file=file:///C:\\Users\Richard\Pictures\1.mp4]` ### XML 消息