diff --git a/.github/workflows/nightly-docker.yml b/.github/workflows/nightly-docker.yml index 970bd96ce4..88178fd194 100644 --- a/.github/workflows/nightly-docker.yml +++ b/.github/workflows/nightly-docker.yml @@ -4,7 +4,7 @@ on: [push] jobs: docker-builder: name: build docker - runs-on: ubuntu-latest + runs-on: ubuntu-23.04 steps: - name: Check out code into the Go module directory uses: actions/checkout@master diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 844d450d44..d1f958b802 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -33,6 +33,9 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} + - name: Tidy Modules + run: go mod tidy + - name: golangci-lint uses: golangci/golangci-lint-action@master with: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 144cebf38e..cf3d4c79f3 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -13,6 +13,9 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@master + - name: Tidy Modules + run: go mod tidy + - name: Run Lint uses: golangci/golangci-lint-action@master with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d56b29854f..7610b89072 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Run GoReleaser uses: goreleaser/goreleaser-action@master with: - version: latest + version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index fd5c16ae86..5dbaff2ffb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,7 +20,7 @@ linters: #- depguard - dogsled - errcheck - - exportloopref + #- exportloopref - exhaustive #- funlen #- goconst diff --git a/.goreleaser.yml b/.goreleaser.yml index f55901e517..ecf18f77ed 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -62,7 +62,7 @@ archives: name_template: "zbp_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" format_overrides: - goos: windows - format: zip + formats: zip nfpms: - license: AGPL 3.0 diff --git a/README.md b/README.md index 09168e3272..322fef207e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ [![go](https://goreportcard.com/badge/github.com/FloatTech/ZeroBot-Plugin?style=flat-square&logo=go)](https://goreportcard.com/badge/github.com/FloatTech/ZeroBot-Plugin) [![onebot](https://img.shields.io/badge/onebot-v11-black?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABwCAMAAADxPgR5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRF////29vbr6+vAAAAk1hCcwAAAAR0Uk5T////AEAqqfQAAAKcSURBVHja7NrbctswDATQXfD//zlpO7FlmwAWIOnOtNaTM5JwDMa8E+PNFz7g3waJ24fviyDPgfhz8fHP39cBcBL9KoJbQUxjA2iYqHL3FAnvzhL4GtVNUcoSZe6eSHizBcK5LL7dBr2AUZlev1ARRHCljzRALIEog6H3U6bCIyqIZdAT0eBuJYaGiJaHSjmkYIZd+qSGWAQnIaz2OArVnX6vrItQvbhZJtVGB5qX9wKqCMkb9W7aexfCO/rwQRBzsDIsYx4AOz0nhAtWu7bqkEQBO0Pr+Ftjt5fFCUEbm0Sbgdu8WSgJ5NgH2iu46R/o1UcBXJsFusWF/QUaz3RwJMEgngfaGGdSxJkE/Yg4lOBryBiMwvAhZrVMUUvwqU7F05b5WLaUIN4M4hRocQQRnEedgsn7TZB3UCpRrIJwQfqvGwsg18EnI2uSVNC8t+0QmMXogvbPg/xk+Mnw/6kW/rraUlvqgmFreAA09xW5t0AFlHrQZ3CsgvZm0FbHNKyBmheBKIF2cCA8A600aHPmFtRB1XvMsJAiza7LpPog0UJwccKdzw8rdf8MyN2ePYF896LC5hTzdZqxb6VNXInaupARLDNBWgI8spq4T0Qb5H4vWfPmHo8OyB1ito+AysNNz0oglj1U955sjUN9d41LnrX2D/u7eRwxyOaOpfyevCWbTgDEoilsOnu7zsKhjRCsnD/QzhdkYLBLXjiK4f3UWmcx2M7PO21CKVTH84638NTplt6JIQH0ZwCNuiWAfvuLhdrcOYPVO9eW3A67l7hZtgaY9GZo9AFc6cryjoeFBIWeU+npnk/nLE0OxCHL1eQsc1IciehjpJv5mqCsjeopaH6r15/MrxNnVhu7tmcslay2gO2Z1QfcfX0JMACG41/u0RrI9QAAAABJRU5ErkJggg==)](https://t.me/zerobotplugin) - [![zerobot](https://img.shields.io/badge/zerobot-v1.7.4-black?style=flat-square&logo=go)](https://github.com/wdvxdr1123/ZeroBot) + [![zerobot](https://img.shields.io/badge/zerobot-v1.8.1-black?style=flat-square&logo=go)](https://github.com/wdvxdr1123/ZeroBot) @@ -43,8 +43,6 @@ > 专为[后 go-cqhttp 时代](https://github.com/Mrs4s/go-cqhttp/issues/2471)开发迁移的`类zbp`新机器人现已出炉,基于官方api,稳定不风控: [NanoBot-Plugin](https://github.com/FloatTech/NanoBot-Plugin) -> 如果您不知道什么是 [OneBot](https://github.com/howmanybots/onebot) 或不希望运行多个程序,还可以直接前往 [gocqzbp](https://github.com/FloatTech/gocqzbp) 的 [Release](https://github.com/FloatTech/gocqzbp/releases) 页面下载单一可执行文件或前往 [Packages](https://github.com/FloatTech/gocqzbp/pkgs/container/gocqzbp) 页面使用`docker`,运行后按提示登录即可。 - > 如果您对开发插件感兴趣,欢迎加入[ZeroBot-Plugin-Playground](https://github.com/FloatTech/ZeroBot-Plugin-Playground) > webui持续开发中, 欢迎加入[ZeroBot-Plugin-Webui](https://github.com/FloatTech/ZeroBot-Plugin-Webui) @@ -176,6 +174,16 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - [x] 设置温度[正整数] + +
+ 聊天时长统计 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chatcount"` + + - [x] 查询水群@xxx + + - [x] 查看水群排名 +
睡眠管理 @@ -184,6 +192,18 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - [x] 早安 | 晚安 +
+
+ 违禁词检测 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/antiabuse" + ` + - [x] 添加违禁词 + + - [x] 删除违禁词 + + - [x] 查看违禁词 +
ATRI @@ -247,6 +267,8 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - [x] 翻牌 - [x] 赞我 + + - [x] 群签到 - [x] [开启 | 关闭]入群验证 @@ -268,6 +290,20 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - 设置欢迎语可选添加参数说明:{at}可在发送时艾特被欢迎者 {nickname}是被欢迎者名字 {avatar}是被欢迎者头像 {uid}是被欢迎者QQ号 {gid}是当前群群号 {groupname} 是当前群群名 +
+
+ 群应用:AI声聊 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord"` + + - [x] 设置AI语音群号1048452984(tips:机器人任意所在群聊即可) + + - [x] 设置AI语音模型 + + - [x] 查看AI语音配置 + + - [x] 发送AI语音xxx +
定时指令触发器 @@ -376,6 +412,18 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 设置默认限速为每 m [分钟 | 秒] n 次触发 +
+
+ aiimage + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiimage"` + + - [x] 设置AI画图密钥xxx + - [x] 设置AI画图接口地址https://api.siliconflow.cn/v1/images/generations + - [x] 设置AI画图模型名Kwai-Kolors/Kolors + - [x] 查看AI画图配置 + - [x] AI画图 [描述] +
AIWife @@ -392,6 +440,18 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 支付宝到账 1 +
+
+ AnimeTrace 动画/Galgame识别 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace"` + + 基于[AnimeTrace](https://ai.animedb.cn/)API 的识图搜索插件 + + - [x] Gal识图 | Gal识图 [模型名] + + - [x] 动漫识图 | 动漫识图 2 | 动漫识图 [模型名] +
触发者撤回时也自动撤回 @@ -585,6 +645,17 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 磕cp大老师 雪乃 +
+
+ 奇怪语言加解密 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/crypter"` + + - [x] 齁语加密 [文本] 或 h加密 [文本] + - [x] 齁语解密 [密文] 或 h解密 [密文] + - [x] fumo加密 [文本] + - [x] fumo解密 [文本] +
今日早报 @@ -659,6 +730,16 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] [emoji][emoji] +
+
+ 颜文字抽象转写 + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi"` + + - [x] 抽象转写[文段] + - [x] 抽象还原[文段] + - [x] 抽象登录[用户名] +
好友申请及群聊邀请事件处理 @@ -933,12 +1014,26 @@ print("run[CQ:image,file="+j["img"]+"]")
- 日韩 VITS 模型拟声 + Minecraft服务器监控&订阅 + +`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver"` - `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moegoe"` +- [x] mc服务器状态 [服务器IP/URI] +- [x] mc服务器添加订阅 [服务器IP/URI] +- [x] mc服务器取消订阅 [服务器IP/URI] +- [x] mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个) + - 使用job插件设置定时, 对话例子如下:: + - 记录在"@every 1m"触发的指令 + - (机器人回答:您的下一条指令将被记录,在@@every 1m时触发) + - mc服务器订阅拉取 +
+
+ Movies猫眼电影查询 - - [x] 让[派蒙|空|荧|阿贝多|枫原万叶|温迪|八重神子|纳西妲|钟离|诺艾尔|凝光|托马|北斗|莫娜|荒泷一斗|提纳里|芭芭拉|艾尔海森|雷电将军|赛诺|琴|班尼特|五郎|神里绫华|迪希雅|夜兰|辛焱|安柏|宵宫|云堇|妮露|烟绯|鹿野院平藏|凯亚|达达利亚|迪卢克|可莉|早柚|香菱|重云|刻晴|久岐忍|珊瑚宫心海|迪奥娜|戴因斯雷布|魈|神里绫人|丽莎|优菈|凯瑟琳|雷泽|菲谢尔|九条裟罗|甘雨|行秋|胡桃|迪娜泽黛|柯莱|申鹤|砂糖|萍姥姥|奥兹|罗莎莉亚|式大将|哲平|坎蒂丝|托克|留云借风真君|昆钧|塞琉斯|多莉|大肉丸|莱依拉|散兵|拉赫曼|杜拉夫|阿守|玛乔丽|纳比尔|海芭夏|九条镰治|阿娜耶|阿晃|阿扎尔|七七|博士|白术|埃洛伊|大慈树王|女士|丽塔|失落迷迭|缭乱星棘|伊甸|伏特加女孩|狂热蓝调|莉莉娅|萝莎莉娅|八重樱|八重霞|卡莲|第六夜想曲|卡萝尔|姬子|极地战刃|布洛妮娅|次生银翼|理之律者|迷城骇兔|希儿|魇夜星渊|黑希儿|帕朵菲莉丝|天元骑英|幽兰黛尔|德丽莎|月下初拥|朔夜观星|暮光骑士|明日香|李素裳|格蕾修|梅比乌斯|渡鸦|人之律者|爱莉希雅|爱衣|天穹游侠|琪亚娜|空之律者|薪炎之律者|云墨丹心|符华|识之律者|维尔薇|芽衣|雷之律者|阿波尼亚]说(中文) +`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies"` +- [x] 今日电影 +- [x] 预售电影
摸鱼 @@ -982,6 +1077,10 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 酷我点歌[xxx] - [x] 酷狗点歌[xxx] + + - [x] qq点歌[xxx] + + - [x] 咪咕点歌[xxx]
@@ -1001,22 +1100,6 @@ print("run[CQ:image,file="+j["img"]+"]") - 注:刷新文件夹较慢,请耐心等待刷新完成,会提示“成功”。 -
-
- 抽wife - - `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativewife"` - - - [x] 抽wife[@xxx] - - - [x] 添加wife[名字][图片] - - - [x] 删除wife[名字] - - - [x] [让 | 不让]所有人均可添加wife - - - 注:不同群添加后不会重叠 -
拼音首字母释义工具 @@ -1035,6 +1118,40 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 搜索日语语法 [xxx] +
+
+ 牛牛大作战 + +`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" ` + +- [x] 打胶 + +- [x] 使用[道具名称]打胶 + +- [x] jj[@xxx] + +- [x] 使用[道具名称]jj[@xxx] + +- [x] 赎牛牛 + +- [x] 牛牛拍卖行 + +- [x] 出售牛牛 + +- [x] 牛牛商店 + +- [x] 牛牛背包 + +- [x] 注册牛牛 + +- [x] 注销牛牛 + +- [x] 牛子长度排行 + +- [x] 牛子深度排行 + +- [x] 查看我的牛牛 +
小说 @@ -1059,6 +1176,22 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 当图片属于非 neutral 类别时自动发送评价(默认禁用,启用输入 /启用 nsfwauto) +
+
+ 抽wife + + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife"` + + - [x] 抽wife[@xxx] + + - [x] 添加wife[名字][图片] + + - [x] 删除wife[名字] + + - [x] [让 | 不让]所有人均可添加wife + + - 注:不同群添加后不会重叠 +
浅草寺求签 @@ -1151,6 +1284,17 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 打劫[对方Q号|@对方QQ] +
+
+ RSSHub + +`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/rsshub"` + +- [x] 添加rsshub订阅-/bookfere/weekly +- [x] 删除rsshub订阅-/bookfere/weekly +- [x] 查看rsshub订阅列表 +- [x] rsshub同步 (使用job执行定时任务------记录在"@every 10m"触发的指令) +
在线代码运行 @@ -1271,14 +1415,6 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] >TL 你好 -
-
- vits猫雷 - - `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/vitsnyaru"` - - - [x] 让猫雷说[xxxx] -
vtb语录 @@ -1297,10 +1433,18 @@ print("run[CQ:image,file="+j["img"]+"]") `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet"` - - [x] 查看我的钱包 - - [x] 查看钱包排名 + - [x] 设置硬币名称[ATRI币] + + - [x] 管理钱包余额[+金额|-金额][@xxx] + + - [x] 查看我的钱包|查看钱包余额[@xxx] + + - [x] 钱包转账[金额][@xxx] + + - 注:仅超级用户能"管理钱包余额", +
据意查句 @@ -1386,7 +1530,7 @@ print("run[CQ:image,file="+j["img"]+"]") `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/word_count"` - - [x] 热词 [群号] [消息数目]|热词 123456 1000 + - [x] 热词 [消息数目]|热词 1000
@@ -1494,29 +1638,42 @@ print("run[CQ:image,file="+j["img"]+"]") ### *低优先级*
- 骂人 + OpenAI聊天 - `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/curse"` + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aichat"` - - [x] 骂我 - - - [x] 大力骂我 + - [x] 设置AI聊天触发概率10 + - [x] 设置AI聊天温度80 + - [x] 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI] + - [x] 设置AI聊天(不)支持系统提示词 + - [x] 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions + - [x] 设置AI聊天密钥xxx + - [x] 设置AI聊天模型名Qwen/Qwen3-8B + - [x] 查看AI聊天系统提示词 + - [x] 重置AI聊天系统提示词 + - [x] 设置AI聊天系统提示词xxx + - [x] 设置AI聊天分隔符``(留空则清除) + - [x] 设置AI聊天(不)响应AT + - [x] 设置AI聊天最大长度4096 + - [x] 设置AI聊天TopP 0.9 + - [x] 设置AI聊天(不)以AI语音输出 + - [x] 查看AI聊天配置 + - [x] 重置AI聊天 + - [x] 群聊总结 [消息数目]|群聊总结 1000
- 人工智能回复 - - `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aireply"` + 骂人 - - [x] @Bot 任意文本(任意一句话回复) + `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/curse"` - - [x] 设置文字回复模式[婧枫|沫沫|青云客|小爱|ChatGPT] + - [x] 骂我 - - [x] 设置 ChatGPT api key xxx + - [x] 大力骂我
- 词典匹配回复 + 词典匹配回复, 仅@触发 `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/thesaurus"` @@ -1537,8 +1694,7 @@ print("run[CQ:image,file="+j["img"]+"]") ### 1. 使用稳定版/测试版 (推荐) -可以前往[Release](https://github.com/FloatTech/ZeroBot-Plugin/releases)页面下载对应系统版本可执行文件,编译时开启了全部插件。您还可以选择 [gocqzbp](https://github.com/FloatTech/gocqzbp) 的 [Release](https://github.com/FloatTech/gocqzbp/releases) 或 [Package](https://github.com/FloatTech/gocqzbp/pkgs/container/gocqzbp),它是 [Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 与本插件的合体。 - +可以前往[Release](https://github.com/FloatTech/ZeroBot-Plugin/releases)页面下载对应系统版本可执行文件,编译时开启了全部插件。 ### 2. 本地直接运行 1. 下载安装最新 [Go](https://studygolang.com/dl) 环境 diff --git a/console/console_windows.go b/console/console_windows.go index e4b5ed1b34..4b93161ec1 100644 --- a/console/console_windows.go +++ b/console/console_windows.go @@ -38,12 +38,18 @@ func setConsoleTitle(title string) (err error) { } func init() { + debugMode := os.Getenv("DEBUG_MODE") == "1" stdin := windows.Handle(os.Stdin.Fd()) var mode uint32 err := windows.GetConsoleMode(stdin, &mode) if err != nil { - panic(err) + if debugMode { + logrus.Warnf("调试模式下忽略控制台模式获取失败: %v", err) + return // 调试模式下直接返回,跳过后续配置 + } else { + panic(err) // 非调试模式下 panic + } } mode &^= windows.ENABLE_QUICK_EDIT_MODE // 禁用快速编辑模式 diff --git a/custom/.gitignore b/custom/.gitignore new file mode 100644 index 0000000000..8229bdc7e4 --- /dev/null +++ b/custom/.gitignore @@ -0,0 +1,4 @@ +!.gitignore +!doc.go +!plugin +* diff --git a/custom/doc.go b/custom/doc.go new file mode 100644 index 0000000000..4eaacf6d95 --- /dev/null +++ b/custom/doc.go @@ -0,0 +1,2 @@ +// Package custom 注册用户自定义插件于此 +package custom diff --git a/custom/plugin/.gitignore b/custom/plugin/.gitignore new file mode 100644 index 0000000000..593bcf0e80 --- /dev/null +++ b/custom/plugin/.gitignore @@ -0,0 +1,2 @@ +!.gitignore +* diff --git a/data b/data index 69b0c8a9fc..328d7638e6 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 69b0c8a9fc24214db185aeccd6836ee9d38e7c3a +Subproject commit 328d7638e6947e8ac1bc8d3f5ecd6a351a4a3b6f diff --git a/default.nix b/default.nix index ecde3a4a81..5d8ebfad95 100644 --- a/default.nix +++ b/default.nix @@ -11,6 +11,7 @@ } ), buildGoApplication ? pkgs.buildGoApplication, + ... }: buildGoApplication { pname = "ZeroBot-Plugin"; diff --git a/flake.lock b/flake.lock index 4f1a905faa..36b302f6df 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1705314449, - "narHash": "sha256-yfQQ67dLejP0FLK76LKHbkzcQqNIrux6MFe32MMFGNQ=", + "lastModified": 1742209644, + "narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=", "owner": "nix-community", "repo": "gomod2nix", - "rev": "30e3c3a9ec4ac8453282ca7f67fca9e1da12c3e6", + "rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1705856552, - "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", + "lastModified": 1745391562, + "narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", + "rev": "8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7", "type": "github" }, "original": { @@ -57,11 +57,28 @@ "type": "github" } }, + "nixpkgs-with-go_1_20": { + "locked": { + "lastModified": 1710843028, + "narHash": "sha256-CMbK45c4nSkGvayiEHFkGFH+doGPbgo3AWfecd2t1Fk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "33c51330782cb486764eb598d5907b43dc87b4c2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "33c51330782cb486764eb598d5907b43dc87b4c2", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", "gomod2nix": "gomod2nix", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "nixpkgs-with-go_1_20": "nixpkgs-with-go_1_20" } }, "systems": { diff --git a/flake.nix b/flake.nix index 4d7480a98a..21b6eec41c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,7 @@ { description = "基于 ZeroBot 的 OneBot 插件"; + inputs.nixpkgs-with-go_1_20.url = "github:NixOS/nixpkgs/33c51330782cb486764eb598d5907b43dc87b4c2"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.gomod2nix.url = "github:nix-community/gomod2nix"; @@ -10,14 +11,25 @@ outputs = { self, nixpkgs, + nixpkgs-with-go_1_20, flake-utils, gomod2nix, - }: let + ... + } @ inputs: let allSystems = flake-utils.lib.allSystems; in ( flake-utils.lib.eachSystem allSystems (system: let - pkgs = nixpkgs.legacyPackages.${system}; + old-nixpkgs = nixpkgs-with-go_1_20.legacyPackages.${system}; + pkgs = import nixpkgs { + inherit system; + + overlays = [ + (_: _: { + go_1_20 = old-nixpkgs.go_1_20; + }) + ]; + }; # The current default sdk for macOS fails to compile go projects, so we use a newer one for now. # This has no effect on other platforms. @@ -25,11 +37,10 @@ in { # doCheck will fail at write files packages = rec { - - ZeroBot-Plugin = - (callPackage ./. { + ZeroBot-Plugin = (callPackage ./. (inputs + // { inherit (gomod2nix.legacyPackages.${system}) buildGoApplication; - }) + })) .overrideAttrs (_: {doCheck = false;}); default = ZeroBot-Plugin; @@ -42,7 +53,6 @@ pkgs.cacert ]; }; - }; devShells.default = callPackage ./shell.nix { inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix; diff --git a/go.mod b/go.mod index ccebaf5048..d73abcea98 100644 --- a/go.mod +++ b/go.mod @@ -4,95 +4,104 @@ go 1.20 require ( github.com/Baidu-AIP/golang-sdk v1.1.1 - github.com/FloatTech/AnimeAPI v1.7.1-0.20240530072450-71c23d2f01f8 - github.com/FloatTech/floatbox v0.0.0-20240505082030-226ec6713e14 - github.com/FloatTech/gg v1.1.3-0.20230226151425-6ea91286ba08 + github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860 + github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80 + github.com/FloatTech/gg v1.1.3 github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef - github.com/FloatTech/rendercard v0.0.10-0.20230223064326-45d29fa4ede9 - github.com/FloatTech/sqlite v1.6.3 - github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 - github.com/FloatTech/zbpctrl v1.6.1 - github.com/FloatTech/zbputils v1.7.2-0.20240530064059-af6f6773ba94 + github.com/FloatTech/rendercard v0.2.0 + github.com/FloatTech/sqlite v1.7.1 + github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562 + github.com/FloatTech/zbpctrl v1.7.0 + github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 - github.com/antchfx/htmlquery v1.3.1 + github.com/Tnze/go-mc v1.20.2 + github.com/antchfx/htmlquery v1.3.4 github.com/corona10/goimagehash v1.1.0 github.com/davidscholberg/go-durationfmt v0.0.0-20170122144659-64843a2083d3 github.com/disintegration/imaging v1.6.2 github.com/fumiama/ahsai v0.1.0 github.com/fumiama/cron v1.3.0 + github.com/fumiama/deepinfra v0.0.0-20250910144855-27a4e697106d github.com/fumiama/go-base16384 v1.7.0 github.com/fumiama/go-registry v0.2.7 github.com/fumiama/gotracemoe v0.0.3 github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565 - github.com/fumiama/terasu v0.0.0-20240507144117-547a591149c0 + github.com/fumiama/slowdo v0.0.0-20241001074058-27c4fe5259a4 + github.com/fumiama/terasu v0.0.0-20241027183601-987ab91031ce github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6 + github.com/go-ego/gse v0.80.3 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/google/uuid v1.6.0 github.com/jinzhu/gorm v1.9.16 github.com/jozsefsallai/gophersauce v1.0.1 github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 github.com/lithammer/fuzzysearch v1.1.8 github.com/liuzl/gocc v0.0.0-20231231122217-0372e1059ca5 + github.com/mmcdole/gofeed v1.3.0 github.com/mroth/weightedrand v1.0.0 github.com/notnil/chess v1.9.0 github.com/pkg/errors v0.9.1 - github.com/shirou/gopsutil/v3 v3.24.4 + github.com/shirou/gopsutil/v3 v3.24.5 github.com/sirupsen/logrus v1.9.3 - github.com/tidwall/gjson v1.17.1 - github.com/wcharczuk/go-chart/v2 v2.1.1 - github.com/wdvxdr1123/ZeroBot v1.7.5-0.20240505070304-562ffeb33dcd + github.com/tidwall/gjson v1.18.0 + github.com/wcharczuk/go-chart/v2 v2.1.2 + github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20 gitlab.com/gomidi/midi/v2 v2.1.7 - golang.org/x/image v0.16.0 - golang.org/x/sys v0.20.0 - golang.org/x/text v0.15.0 + golang.org/x/image v0.24.0 + golang.org/x/sys v0.30.0 + golang.org/x/text v0.22.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca // indirect - github.com/antchfx/xpath v1.3.0 // indirect - github.com/blend/go-sdk v1.20220411.3 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/antchfx/xpath v1.3.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 // indirect github.com/faiface/beep v1.1.0 // indirect github.com/fumiama/go-simple-protobuf v0.2.0 // indirect github.com/fumiama/gofastTEA v0.0.10 // indirect - github.com/fumiama/imgsz v0.0.4 // indirect + github.com/fumiama/imgsz v0.0.2 // indirect github.com/gabriel-vasile/mimetype v1.0.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hajimehoshi/oto v0.7.1 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/vorbis v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pkumza/numcn v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tetratelabs/wazero v1.5.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/vcaesar/cedar v0.20.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect - golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect - golang.org/x/net v0.24.0 // indirect - modernc.org/libc v1.49.3 // indirect + golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect + golang.org/x/net v0.33.0 // indirect + modernc.org/libc v1.61.0 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.20.0 // indirect + modernc.org/sqlite v1.33.1 // indirect ) replace modernc.org/sqlite => github.com/fumiama/sqlite3 v1.29.10-simp diff --git a/go.sum b/go.sum index 9eef0d5c5f..d337514073 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,46 @@ github.com/Baidu-AIP/golang-sdk v1.1.1 h1:RQsAmgDSAkiq22I6n7XJ2t3afgzFeqjY46FGhvrx4cw= github.com/Baidu-AIP/golang-sdk v1.1.1/go.mod h1:bXnGw7xPeKt8aF7UCELKrV6UZ/46spItONK1RQBQj1Y= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/FloatTech/AnimeAPI v1.7.1-0.20240530072450-71c23d2f01f8 h1:2i36tl5VlBWxWxv4WyfWmCA23NaV1fB5/smJtdORHf4= -github.com/FloatTech/AnimeAPI v1.7.1-0.20240530072450-71c23d2f01f8/go.mod h1:Ru6q5pZUnfMg1iu0M1Hp73q9N3LNIbDr16kjkzyG6Xk= -github.com/FloatTech/floatbox v0.0.0-20240505082030-226ec6713e14 h1:8O0Iq9MnKsKowltY9txhOqcJdmGTjxHPQ4gEYzbJc9A= -github.com/FloatTech/floatbox v0.0.0-20240505082030-226ec6713e14/go.mod h1:OzGLhvmtz1TKIdGaJDd8pQumvD36UqK+dWsiCISmzQQ= -github.com/FloatTech/gg v1.1.3-0.20230226151425-6ea91286ba08 h1:dPLeoiTVSBlgls+66EB/UJ2e38BaASmBN5nANaycSBU= -github.com/FloatTech/gg v1.1.3-0.20230226151425-6ea91286ba08/go.mod h1:uzPzAeT35egARdRuu+1oyjU3CmTwCceoq3Vvje7LpcI= +github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860 h1:ddthsMzYC2LZ517/71W//9VsXT82CSBALVt3sQY5vfA= +github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860/go.mod h1:CzpSeo5Pvslnq7Ho14E438Yn/flFMKzjGeX2nbC1mzk= +github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80 h1:lFD1pd8NkYCrw0QpTX/T5pJ67I7AL5eGxQ4v0r9f81Q= +github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80/go.mod h1:IWoFFqu+0FeaHHQdddyiTRL5z7gJME6qHC96qh0R2sc= +github.com/FloatTech/gg v1.1.3 h1:+GlL02lTKsxJQr4WCuNwVxC1/eBZrCvypCIBtxuOFb4= +github.com/FloatTech/gg v1.1.3/go.mod h1:/9oLP54CMfq4r+71XL26uaFTJ1uL1boAyX67680/1HE= github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef h1:CJbK/2FRwPuZpeb6M4sWK2d7oXDnBEGhpkQuQrgc91A= github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef/go.mod h1:el5hGpj1C1bDRxcTXYRwEivDCr40zZeJpcrLrB1fajs= -github.com/FloatTech/rendercard v0.0.10-0.20230223064326-45d29fa4ede9 h1:hffajvmQFfP68U6wUwHemPuuwCUoss+SEFfoLYwbGwE= -github.com/FloatTech/rendercard v0.0.10-0.20230223064326-45d29fa4ede9/go.mod h1:NBFPhWae4hqVMeG8ELBBnUQkKce3nDjkljVn6PdiUNs= -github.com/FloatTech/sqlite v1.6.3 h1:MQkqBNlkPuCoKQQgoNLuTL/2Ci3tBTFAnVYBdD0Wy4M= -github.com/FloatTech/sqlite v1.6.3/go.mod h1:zFbHzRfB+CJ+VidfjuVbrcin3DAz283F7hF1hIeHzpY= -github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 h1:g4pTnDJUW4VbJ9NvoRfUvdjDrHz/6QhfN/LoIIpICbo= -github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= -github.com/FloatTech/zbpctrl v1.6.1 h1:SilK5R2poO8iUT6JPzpgr/BSzxYAaybBYNZkMyZ8STw= -github.com/FloatTech/zbpctrl v1.6.1/go.mod h1:I+MetM++1sJhNPg3zww1aw04BicYsNohvHC4Jh52XSo= -github.com/FloatTech/zbputils v1.7.2-0.20240530064059-af6f6773ba94 h1:ITQPmNSHE5bNFBpdwldUpfTDLXROEahSKspkJrSLWvQ= -github.com/FloatTech/zbputils v1.7.2-0.20240530064059-af6f6773ba94/go.mod h1:nHWYtF4g2NRv3GXZiAZDvgPjdcHGUaQHxGgD0aHz30I= +github.com/FloatTech/rendercard v0.2.0 h1:PBTZ2gCEy/dAEGSfWecrGTrWDYpiBJD1dVzNDDaOxh4= +github.com/FloatTech/rendercard v0.2.0/go.mod h1:Sbojcy1t3NfFz7/WicZRmR/uKFxNMYkKF8qHx69dxY0= +github.com/FloatTech/sqlite v1.7.1 h1:XKUY0+MNaRmvEIgRv7QLbl7PFVpUfQ72+XQg+no2Vq0= +github.com/FloatTech/sqlite v1.7.1/go.mod h1:/4tzfCGhrZnnjC1U8vcfwGQeF6eR649fhOsS3+Le0+s= +github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562 h1:snfw7FNFym1eNnLrQ/VCf80LiQo9C7jHgrunZDwiRcY= +github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= +github.com/FloatTech/zbpctrl v1.7.0 h1:Hxo6EIhJo+pHjcQP9QgIJgluaT1pHH99zkk3njqTNMo= +github.com/FloatTech/zbpctrl v1.7.0/go.mod h1:xmM4dSwHA02Gei3ogCRiG+RTrw/7Z69PfrN5NYf8BPE= +github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f h1:5jnrFe9FTydb/pcUhxkWHuQVCwmYIZmneOkvmgHOwGI= +github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f/go.mod h1:HG/yZwExV3b1Vqu4chbqwhfX4hx7gDS07QO436JkwIg= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 h1:S/ferNiehVjNaBMNNBxUjLtVmP/YWD6Yh79RfPv4ehU= github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w= github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 h1:bBmmB7he0iVN4m5mcehfheeRUEer/Avo4ujnxI3uCqs= github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5/go.mod h1:0UcFaCkhp6vZw6l5Dpq0Dp673CoF9GdvA8lTfst0GiU= +github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= +github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5UaBEQLjcvn7aAU01nqU/NUyOBEU+ew= github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc= -github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8= -github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc= -github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= -github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc= -github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ= +github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM= +github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs= +github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -60,6 +63,8 @@ github.com/fumiama/ahsai v0.1.0 h1:LXD61Kaj6kJHa3AEGsLIfKNzcgaVxg7JB72OR4yNNZ4= github.com/fumiama/ahsai v0.1.0/go.mod h1:fFeNnqgo44i8FIaguK659aQryuZeFy+4klYLQu/rfdk= github.com/fumiama/cron v1.3.0 h1:ZWlwuexF+HQHl3cYytEE5HNwD99q+3vNZF1GrEiXCFo= github.com/fumiama/cron v1.3.0/go.mod h1:bz5Izvgi/xEUI8tlBN8BI2jr9Moo8N4or0KV8xXuPDY= +github.com/fumiama/deepinfra v0.0.0-20250910144855-27a4e697106d h1:iGxnST620IHrJ47DXkjzrZJ2rskBogWze+UyvnAxT6g= +github.com/fumiama/deepinfra v0.0.0-20250910144855-27a4e697106d/go.mod h1:wW05PQSn8mo1mZIoa6LBUE+3xIBjkoONvnfPTV5ZOhY= github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA= github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM= github.com/fumiama/go-registry v0.2.7 h1:tLEqgEpsiybQMqBv0dLHm5leia/z1DhajMupwnOHeNs= @@ -70,16 +75,18 @@ github.com/fumiama/gofastTEA v0.0.10 h1:JJJ+brWD4kie+mmK2TkspDXKzqq0IjXm89aGYfoG github.com/fumiama/gofastTEA v0.0.10/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk= github.com/fumiama/gotracemoe v0.0.3 h1:iI5EbE9A3UUbfukG6+/soYPjp1S31eCNYf4tw7s6/Jc= github.com/fumiama/gotracemoe v0.0.3/go.mod h1:tyqahdUzHf0bQIAVY/GYmDWvYYe5ik1ZbhnGYh+zl40= -github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI= -github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E= +github.com/fumiama/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak= +github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4= github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565 h1:sQuR2+N5HurnvsZhiKdEg+Ig354TaqgCQRxd/0KgIOQ= github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565/go.mod h1:UUEvyLTJ7yoOA/viKG4wEis4ERydM7+Ny6gZUWgkS80= github.com/fumiama/libc v0.0.0-20240530081950-6f6d8586b5c5 h1:jDxsIupsT84A6WHcs6kWbst+KqrRQ8/o0VyoFMnbBOA= github.com/fumiama/libc v0.0.0-20240530081950-6f6d8586b5c5/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE= +github.com/fumiama/slowdo v0.0.0-20241001074058-27c4fe5259a4 h1:zN9e09TYKXI1mNkuS6YbH+Sn+4k5tBir+ovhZZcRYAs= +github.com/fumiama/slowdo v0.0.0-20241001074058-27c4fe5259a4/go.mod h1:iZf1H/Jcw5gjOOFb4C5nlweJtViWc7uwUxRCe14pbYk= github.com/fumiama/sqlite3 v1.29.10-simp h1:c5y3uKyU0q9t0/SyfynzYyuslQ5zP+5CD8e0yYY554A= github.com/fumiama/sqlite3 v1.29.10-simp/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= -github.com/fumiama/terasu v0.0.0-20240507144117-547a591149c0 h1:So/3Bg/m2ZcUvqCzzEjjkjHBjcvnV3AN5tCxwsdMwYU= -github.com/fumiama/terasu v0.0.0-20240507144117-547a591149c0/go.mod h1:UVx8YP1jKKL1Cj+uy+OnQRM2Ih6U36Mqy9GSf7jabsI= +github.com/fumiama/terasu v0.0.0-20241027183601-987ab91031ce h1:T6iDDU16rFyxV/FwfJJR6qcgkIlXJEIFlUTSmTD1h6s= +github.com/fumiama/terasu v0.0.0-20241027183601-987ab91031ce/go.mod h1:UVx8YP1jKKL1Cj+uy+OnQRM2Ih6U36Mqy9GSf7jabsI= github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6 h1:LtDgr628eji8jRpjPCxsk7ibjcfi97QieZVCTjxLCBw= github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6/go.mod h1:lEaZsT4FRSqcjnQ5q8y+mkenkzR/r1D3BJmfdp0vqDg= github.com/gabriel-vasile/mimetype v1.0.4 h1:uBejfH8l3/2f+5vjl1e4xIaSyNEhRBZ5N/ij7ohpNd8= @@ -89,6 +96,8 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= +github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg= +github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -101,9 +110,9 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= @@ -120,15 +129,14 @@ github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jozsefsallai/gophersauce v1.0.1 h1:BA3ovtQRrAb1qYU9JoRLbDHpxnDunlNcEkEfhCvDDCM= github.com/jozsefsallai/gophersauce v1.0.1/go.mod h1:YVEI7djliMTmZ1Vh01YPF8bUHi+oKhe3yXgKf1T49vg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY= github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5/go.mod h1:c9+VS9GaommgIOzNWb5ze4lYwfT8BZ2UDyGiuQTT7yc= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= @@ -149,6 +157,15 @@ github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/ github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= +github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= +github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk= +github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mroth/weightedrand v1.0.0 h1:V8JeHChvl2MP1sAoXq4brElOcza+jxLkRuwvtQu8L3E= github.com/mroth/weightedrand v1.0.0/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -170,30 +187,22 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -202,10 +211,13 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= -github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= -github.com/wdvxdr1123/ZeroBot v1.7.5-0.20240505070304-562ffeb33dcd h1:atmeLC1rrs5XIk61rYDgFZDTaezYtSQzndvQ+L7fgaU= -github.com/wdvxdr1123/ZeroBot v1.7.5-0.20240505070304-562ffeb33dcd/go.mod h1:J6uHaXS/Am2VsLxF9TcU6il19PbOeC4SvgxHJ1E2jaE= +github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4= +github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik= +github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4= +github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E= +github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= +github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20 h1:Yzd+cbiJQYtf6cZDP5ZB/LqjNWiV752+5P6Eua+wnic= +github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -215,20 +227,29 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8= +golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= -golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= -golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= +golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -236,14 +257,23 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -253,6 +283,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -261,29 +292,45 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gomod2nix.toml b/gomod2nix.toml index 7d85c7286f..12c63f8e72 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -5,38 +5,41 @@ schema = 3 version = "v1.1.1" hash = "sha256-hKshA0K92bKuK92mmtM0osVmqLJcSbeobeWSDpQoRCo=" [mod."github.com/FloatTech/AnimeAPI"] - version = "v1.7.1-0.20240530072450-71c23d2f01f8" - hash = "sha256-NUYNGhjVW5bdpWIeKjBhnVTsjf6OXNqCcqzrRd3c+gE=" + version = "v1.7.1-0.20250901143505-180d33844860" + hash = "sha256-k1MlgaBGwpaqoVk+8WYfXoVLfzqyEQW5LQaJgBKlhUA=" [mod."github.com/FloatTech/floatbox"] - version = "v0.0.0-20240505082030-226ec6713e14" - hash = "sha256-v296D9T1QzFmcHQJNxJvx7sMtK+Jd1TUHXWqZtIvvf4=" + version = "v0.0.0-20250513111443-adba80e84e80" + hash = "sha256-Zt9zkUa3qqldrSttAq66YLPZPxrnkOR2MaU7oapIWEE=" [mod."github.com/FloatTech/gg"] - version = "v1.1.3-0.20230226151425-6ea91286ba08" - hash = "sha256-AeMzjMK1ZwFERb5xuNAV5PdHp7rzoT4ZF7kZ6Kj2/0s=" + version = "v1.1.3" + hash = "sha256-7K/R2mKjUHVnoJ3b1wDObJ5Un2Htj59Y97G1Ja1tuPo=" [mod."github.com/FloatTech/imgfactory"] version = "v0.2.2-0.20230413152719-e101cc3606ef" hash = "sha256-2okFyPQSYIxrc8hxICsbjEM9xq25a3I2A4wmDIYFCg8=" [mod."github.com/FloatTech/rendercard"] - version = "v0.0.10-0.20230223064326-45d29fa4ede9" - hash = "sha256-Zn8agmyWWEC2QQfIBa60jlQrEap9Bps+z1Ekay6Y0cg=" + version = "v0.2.0" + hash = "sha256-fgntEYGh2mEl618hM13kb0GGeQEXdP+lochYX8F2OXs=" [mod."github.com/FloatTech/sqlite"] - version = "v1.6.3" - hash = "sha256-zWPByEMi89ms67ubPg0fAPIRxfpBC2IRKc0iNVLqkPU=" + version = "v1.7.1" + hash = "sha256-1x8xH5fFDlLts8YfzgO3vLF45Q7Ah+mYI6Wn8JG/qE0=" [mod."github.com/FloatTech/ttl"] - version = "v0.0.0-20230307105452-d6f7b2b647d1" - hash = "sha256-BQzWUzoIOLwfZa7WElqaa3EYrcz1Ql6JApgxZIQHBro=" + version = "v0.0.0-20240716161252-965925764562" + hash = "sha256-/XjfdVXEzYgeM+OYuyy76tf13lO91vCcwpjWgkRGteU=" [mod."github.com/FloatTech/zbpctrl"] - version = "v1.6.1" - hash = "sha256-S5KSLZSUt6s7i6ZfKovlJawXF5NYkenPSNVWk+xtO8o=" + version = "v1.7.0" + hash = "sha256-HDDnE0oktWJH1tkxuQwUUbeJhmVwY5fyc/vR72D2mkU=" [mod."github.com/FloatTech/zbputils"] - version = "v1.7.2-0.20240530064059-af6f6773ba94" - hash = "sha256-cpanspZVMKWAXUtXUFKmIiCS+Xmzr6JNNYc6xxRSMb0=" + version = "v1.7.2-0.20250812085410-2741050f465f" + hash = "sha256-NoCU7tqzihm2xEr1LelrfMzeg9RDQ9OsFBVXfNDcxvs=" [mod."github.com/RomiChan/syncx"] version = "v0.0.0-20240418144900-b7402ffdebc7" hash = "sha256-L1j1vgiwqXpF9pjMoRRlrQUHzoULisw/01plaEAwxs4=" [mod."github.com/RomiChan/websocket"] version = "v1.4.3-0.20220227141055-9b2c6168c9c5" hash = "sha256-Adx+gvqB+CCoUXx7ebIaBDjVkav+wS5qZPmaqcApBWA=" + [mod."github.com/Tnze/go-mc"] + version = "v1.20.2" + hash = "sha256-Nu4PXNxeARH0itm6yIIplFaywL2yQnPJFksmmuyIptI=" [mod."github.com/adamzy/cedar-go"] version = "v0.0.0-20170805034717-80a9c64b256d" hash = "sha256-N19KTxh70IUBqnchFuWkrJD8uuFOIVqv1iSuN3YFIT0=" @@ -44,14 +47,11 @@ schema = 3 version = "v0.0.0-20200320125537-f189e35d30ca" hash = "sha256-ALeRuEJN9jHjGb4wNKJcxC59vVx8Tj7hHikEGkaZZ0s=" [mod."github.com/antchfx/htmlquery"] - version = "v1.3.1" - hash = "sha256-4ZzKk7Z+vH8ytisdtcZz/Y0MbnVVhruiO/7gtUy3ouQ=" + version = "v1.3.4" + hash = "sha256-nrtIgRgdOvo0iIQyrhHOFKOmoT8e2gduUsct3f5zDNA=" [mod."github.com/antchfx/xpath"] - version = "v1.3.0" - hash = "sha256-SU+Tnf5c9vsDCrY1BVKjqYLhB91xt9oHBS5bicbs2cA=" - [mod."github.com/blend/go-sdk"] - version = "v1.20220411.3" - hash = "sha256-yxrf24hru8NeTPUmoaJG1PcmHE5pn/U36Sj9Qg+JVqg=" + version = "v1.3.3" + hash = "sha256-Ent9bgBTjKS8/61LKrIu/JcBI/Qsv6EEIojwsMjCgdY=" [mod."github.com/corona10/goimagehash"] version = "v1.1.0" hash = "sha256-HyS8nc7kUNnDaVBDzJ9Ym4pRs83YB4M2vHSRwfm6mr4=" @@ -76,6 +76,9 @@ schema = 3 [mod."github.com/fumiama/cron"] version = "v1.3.0" hash = "sha256-/sN7X8dKXQgv8J+EDzVUB+o+AY9gBC8e1C6sYhaTy1k=" + [mod."github.com/fumiama/deepinfra"] + version = "v0.0.0-20250910022828-8cde75e137f4" + hash = "sha256-1CV8t3R91maqJztHg7whECqvS4+sxWcSvq+EyO4PyZ8=" [mod."github.com/fumiama/go-base16384"] version = "v1.7.0" hash = "sha256-vTAsBBYe2ISzb2Nba5E96unodZSkhMcqo6hbwR01nz8=" @@ -92,20 +95,26 @@ schema = 3 version = "v0.0.3" hash = "sha256-O3cDkVXu5NG1ZtzubxhH+S91zfgu4uH1L+OiSGYSNXQ=" [mod."github.com/fumiama/imgsz"] - version = "v0.0.4" - hash = "sha256-rrGx+v41OEl0ATwL6u5TNcpfkCQbj3jFNnGiQUNu2qs=" + version = "v0.0.2" + hash = "sha256-eYUjP1TKWUrsY++rzg4rezOvmvmjADZFBizIIDHnZtY=" [mod."github.com/fumiama/jieba"] version = "v0.0.0-20221203025406-36c17a10b565" hash = "sha256-DvDx1pdldkdaSszrbadM/VwqT9TTSmWl6G6a+ysXYEM=" + [mod."github.com/fumiama/slowdo"] + version = "v0.0.0-20241001074058-27c4fe5259a4" + hash = "sha256-rsV3MKRCSOBMIgJXFCGbCHRY2aBAb32ftU49hT3GjqY=" [mod."github.com/fumiama/terasu"] - version = "v0.0.0-20240507144117-547a591149c0" - hash = "sha256-ZZG5/Ckq4R0eojmiuli5ZRToDNQt4VeRwdy0jjVCvbg=" + version = "v0.0.0-20241027183601-987ab91031ce" + hash = "sha256-WiG5BD1Icwq61KpqkQdf6dl64jEhaDJb2zAQROqXwvc=" [mod."github.com/fumiama/unibase2n"] version = "v0.0.0-20240530074540-ec743fd5a6d6" hash = "sha256-I3xNzjrj5y0fy0dfa75V57GanfmHIHmubEn9/y0BBHw=" [mod."github.com/gabriel-vasile/mimetype"] version = "v1.0.4" hash = "sha256-5hl9zBo3nkPt8dZfcLoOix8lAKLm3qIkWhopoS4V34E=" + [mod."github.com/go-ego/gse"] + version = "v0.80.3" + hash = "sha256-uxTQN4cxE/ZReZqjlIEQ3WYD9w2Ec37LRHQftJXsSZQ=" [mod."github.com/go-ole/go-ole"] version = "v1.2.6" hash = "sha256-+oxitLeJxYF19Z6g+6CgmCHJ1Y5D8raMi2Cb3M6nXCs=" @@ -139,9 +148,6 @@ schema = 3 [mod."github.com/kanrichan/resvg-go"] version = "v0.0.2-0.20231001163256-63db194ca9f5" hash = "sha256-plRZ3yhyCafCXmAD4vnFUoCTRsHmLp7Jn9gFKcEKbds=" - [mod."github.com/kr/text"] - version = "v0.2.0" - hash = "sha256-fadcWxZOORv44oak3jTxm6YcITcFxdGt4bpn869HxUE=" [mod."github.com/lithammer/fuzzysearch"] version = "v1.1.8" hash = "sha256-aMMRcrlUc9CBiiNkcnWWn4hfNMNyVhrAt67kvP4D4Do=" @@ -187,12 +193,9 @@ schema = 3 [mod."github.com/remyoudompheng/bigfft"] version = "v0.0.0-20230129092748-24d4a6f8daec" hash = "sha256-vYmpyCE37eBYP/navhaLV4oX4/nu0Z/StAocLIFqrmM=" - [mod."github.com/rogpeppe/go-internal"] - version = "v1.12.0" - hash = "sha256-qvDNCe3l84/LgrA8X4O15e1FeDcazyX91m9LmXGXX6M=" [mod."github.com/shirou/gopsutil/v3"] - version = "v3.24.4" - hash = "sha256-ubkBxu9X4LRhI1HqkjsIShR4e8rQsuKQs4VNOIIhZCU=" + version = "v3.24.5" + hash = "sha256-tc+t1u7gf5A+Bd956dYeM8pGbxs9ezQHqKAKfLQLpuQ=" [mod."github.com/shoenig/go-m1cpu"] version = "v0.1.6" hash = "sha256-hT+JP30BBllsXosK/lo89HV/uxxPLsUyO3dRaDiLnCg=" @@ -203,8 +206,8 @@ schema = 3 version = "v1.5.0" hash = "sha256-fGdJM4LJrZA9jxHuYVo4EUQ3I1k0IVG3QQCBCgZkeZI=" [mod."github.com/tidwall/gjson"] - version = "v1.17.1" - hash = "sha256-5R38cFZFaVbdem2B+9rsbr+0hRxbtDQ0i5PYWPT6kj0=" + version = "v1.18.0" + hash = "sha256-CO6hqDu8Y58Po6A01e5iTpwiUBQ5khUZsw7czaJHw0I=" [mod."github.com/tidwall/match"] version = "v1.1.1" hash = "sha256-M2klhPId3Q3T3VGkSbOkYl/2nLHnsG+yMbXkPkyrRdg=" @@ -217,36 +220,39 @@ schema = 3 [mod."github.com/tklauser/numcpus"] version = "v0.6.1" hash = "sha256-8eFcw4YI0w6+GPhU5xMMQjiio94q/O5PpNO3QsvXve0=" + [mod."github.com/vcaesar/cedar"] + version = "v0.20.2" + hash = "sha256-3WblBdkR9AZcvZCKSteBV5kdhahiFHG2dbLWfwrVkwM=" [mod."github.com/wcharczuk/go-chart/v2"] - version = "v2.1.1" - hash = "sha256-emvjt/ze8skM+MBflwV0EgS/svpaEGU/mn27Ie4VTXs=" + version = "v2.1.2" + hash = "sha256-GXWWea/u6BezTsPPrWhTYiTetPP/YW6P+Sj4YdocPaM=" [mod."github.com/wdvxdr1123/ZeroBot"] - version = "v1.7.5-0.20240505070304-562ffeb33dcd" - hash = "sha256-2VKVJJ9jqbWjEPrvqLaMEK+Qpl4HiB4nJX7ebHcbDYA=" + version = "v1.8.2-0.20250804063440-ccc03e33ac20" + hash = "sha256-2bFcPmcDsZxTD3sU3i2QD4M/ehSF43Ohf5ltuq1QtOQ=" [mod."github.com/yusufpapurcu/wmi"] version = "v1.2.4" hash = "sha256-N+YDBjOW59YOsZ2lRBVtFsEEi48KhNQRb63/0ZSU3bA=" [mod."gitlab.com/gomidi/midi/v2"] version = "v2.1.7" hash = "sha256-fbgxSMCk7PVII3sNEKuGWbN56fy3eM564Xb+lnYTxRQ=" - [mod."golang.org/x/exp"] - version = "v0.0.0-20190306152737-a1d7652674e8" - hash = "sha256-VJ0sxFsqnx2O/NmXamL2F5bQeUw5sizVQ7NLusceK5Q=" + [mod."golang.org/x/exp/shiny"] + version = "v0.0.0-20250305212735-054e65f0b394" + hash = "sha256-+xzaSlgRHFa+sGnQG90/72vcJMhletsob/L+KG24P/A=" [mod."golang.org/x/image"] - version = "v0.16.0" - hash = "sha256-+BOLefaFM/c+AV3kmnNvztbhZ+a9GCNwkEya8hZSKYg=" + version = "v0.24.0" + hash = "sha256-nhcznNf4ePM7d0Jy2Si0dpMt7KQfRF5Y5QzMpwFCAVg=" [mod."golang.org/x/mobile"] - version = "v0.0.0-20190415191353-3e0bab5405d6" - hash = "sha256-Ds7JS9muxzDc7WgCncAd0rMSFeBI88/I0dQsk13/56k=" + version = "v0.0.0-20231127183840-76ac6878050a" + hash = "sha256-GdXSvrqQiJX6pOqc2Yr8gG0ZWysEE81YRl5qkt3JCMA=" [mod."golang.org/x/net"] - version = "v0.24.0" - hash = "sha256-w1c21ljta5wNIyel9CSIn/crPzwOCRofNKhqmfs4aEQ=" + version = "v0.33.0" + hash = "sha256-9swkU9vp6IflUUqAzK+y8PytSmrKLuryidP3RmRfe0w=" [mod."golang.org/x/sys"] - version = "v0.20.0" - hash = "sha256-mowlaoG2k4n1c1rApWef5EMiXd3I77CsUi8jPh6pTYA=" + version = "v0.30.0" + hash = "sha256-BuhWtwDkciVioc03rxty6G2vcZVnPX85lI7tgQOFVP8=" [mod."golang.org/x/text"] - version = "v0.15.0" - hash = "sha256-pBnj0AEkfkvZf+3bN7h6epCD2kurw59clDP7yWvxKlk=" + version = "v0.22.0" + hash = "sha256-kUwLNFk9K/YuWmO5/u2IshrmhT2CCuk+mAShSlTTeZo=" [mod."gopkg.in/yaml.v3"] version = "v3.0.1" hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" diff --git a/kanban/banner/banner.go b/kanban/banner/banner.go index 79c6e2ef08..cf6bd49f1c 100644 --- a/kanban/banner/banner.go +++ b/kanban/banner/banner.go @@ -3,13 +3,13 @@ package banner // Version ... -var Version = "v1.8.1" +var Version = "v1.9.9" // Copyright ... -var Copyright = "© 2020 - 2024 FloatTech" +var Copyright = "© 2020 - 2025 FloatTech" // Banner ... var Banner = "* OneBot + ZeroBot + Golang\n" + - "* Version " + Version + " - 2024-05-30 16:47:27 +0900 JST\n" + + "* Version " + Version + " - 2025-09-10 10:40:39 +0800 CST\n" + "* Copyright " + Copyright + ". All Rights Reserved.\n" + "* Project: https://github.com/FloatTech/ZeroBot-Plugin" diff --git a/kanban/gen/banner.go b/kanban/gen/banner.go index a39a1c343c..3af4a9984c 100644 --- a/kanban/gen/banner.go +++ b/kanban/gen/banner.go @@ -27,7 +27,7 @@ var Banner = "* OneBot + ZeroBot + Golang\n" + "* Project: https://github.com/FloatTech/ZeroBot-Plugin" ` -const timeformat = `2006-01-02 15:04:05 +0900 JST` +const timeformat = `2006-01-02 15:04:05 +0800 CST` func main() { f, err := os.Create("banner/banner.go") diff --git a/main.go b/main.go index 7bd6a05b7e..01b5511e27 100644 --- a/main.go +++ b/main.go @@ -34,8 +34,12 @@ import ( _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chat" // 基础词库 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chatcount" // 聊天时长统计 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/sleepmanage" // 统计睡眠时间 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord" // 群应用:AI声聊 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/atri" // ATRI词库 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/manager" // 群管 @@ -60,89 +64,96 @@ import ( // vvvvvvvvvvvvvv // // vvvv // - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ahsai" // ahsai tts - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moegoe" // 日韩 VITS 模型拟声 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/vitsnyaru" // vits猫雷 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API + _ "github.com/FloatTech/ZeroBot-Plugin/custom" // 自定义插件合集 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ahsai" // ahsai tts + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiimage" // AI画图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace" // AnimeTrace 动画/Galgame识别 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/crypter" // 奇怪语言加解密 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi" // 颜文字抽象转写 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver" // Minecraft服务器监控&订阅 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies" // 电影插件 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" // 牛牛大作战 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/rsshub" // RSSHub订阅姬 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API // _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wtf" // 鬼东西 @@ -164,9 +175,9 @@ import ( // vvvvvvvvvvvvvv // // vvvv // - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/curse" // 骂人 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aichat" // AI聊天 - _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aireply" // 人工智能回复 + _ "github.com/FloatTech/ZeroBot-Plugin/plugin/curse" // 骂人 _ "github.com/FloatTech/ZeroBot-Plugin/plugin/thesaurus" // 词典匹配回复 diff --git a/plugin/aichat/cfg.go b/plugin/aichat/cfg.go new file mode 100644 index 0000000000..f92875320e --- /dev/null +++ b/plugin/aichat/cfg.go @@ -0,0 +1,198 @@ +package aichat + +import ( + "fmt" + "strconv" + "strings" + + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/chat" + "github.com/fumiama/deepinfra" + "github.com/fumiama/deepinfra/model" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +var ( + cfg = newconfig() +) + +type config struct { + ModelName string + Type int + MaxN uint + TopP float32 + SystemP string + API string + Key string + Separator string + NoReplyAT bool + NoSystemP bool + NoRecord bool +} + +func newconfig() config { + return config{ + ModelName: model.ModelDeepDeek, + SystemP: chat.SystemPrompt, + API: deepinfra.OpenAIDeepInfra, + } +} + +func (c *config) isvalid() bool { + return c.ModelName != "" && c.API != "" && c.Key != "" +} + +func ensureconfig(ctx *zero.Ctx) bool { + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + return false + } + if !cfg.isvalid() { + err := c.GetExtra(&cfg) + if err != nil { + logrus.Warnln("ERROR: get extra err:", err) + } + if !cfg.isvalid() { + cfg = newconfig() + } + } + return true +} + +func newextrasetstr(ptr *string) func(ctx *zero.Ctx) { + return func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + *ptr = args + err := c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + } +} + +func newextrasetbool(ptr *bool) func(ctx *zero.Ctx) { + return func(ctx *zero.Ctx) { + args := ctx.State["regex_matched"].([]string) + isno := args[1] == "不" + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + *ptr = isno + err := c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + } +} + +func newextrasetuint(ptr *uint) func(ctx *zero.Ctx) { + return func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + n, err := strconv.ParseUint(args, 10, 64) + if err != nil { + ctx.SendChain(message.Text("ERROR: parse args err: ", err)) + return + } + *ptr = uint(n) + err = c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + } +} + +func newextrasetfloat32(ptr *float32) func(ctx *zero.Ctx) { + return func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + n, err := strconv.ParseFloat(args, 32) + if err != nil { + ctx.SendChain(message.Text("ERROR: parse args err: ", err)) + return + } + *ptr = float32(n) + err = c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + } +} + +func printConfig(rate int64, temperature int64, cfg config) string { + maxn := cfg.MaxN + if maxn == 0 { + maxn = 4096 + } + topp := cfg.TopP + if topp == 0 { + topp = 0.9 + } + var builder strings.Builder + builder.WriteString("当前AI聊天配置:\n") + builder.WriteString(fmt.Sprintf("• 模型名:%s\n", cfg.ModelName)) + builder.WriteString(fmt.Sprintf("• 接口类型:%d(%s)\n", cfg.Type, apilist[cfg.Type])) + builder.WriteString(fmt.Sprintf("• 触发概率:%d%%\n", rate)) + builder.WriteString(fmt.Sprintf("• 温度:%.2f\n", float32(temperature)/100)) + builder.WriteString(fmt.Sprintf("• 最大长度:%d\n", maxn)) + builder.WriteString(fmt.Sprintf("• TopP:%.1f\n", topp)) + builder.WriteString(fmt.Sprintf("• 系统提示词:%s\n", cfg.SystemP)) + builder.WriteString(fmt.Sprintf("• 接口地址:%s\n", cfg.API)) + builder.WriteString(fmt.Sprintf("• 密钥:%s\n", maskKey(cfg.Key))) + builder.WriteString(fmt.Sprintf("• 分隔符:%s\n", cfg.Separator)) + builder.WriteString(fmt.Sprintf("• 响应@:%s\n", yesNo(!cfg.NoReplyAT))) + builder.WriteString(fmt.Sprintf("• 支持系统提示词:%s\n", yesNo(!cfg.NoSystemP))) + builder.WriteString(fmt.Sprintf("• 以AI语音输出:%s\n", yesNo(!cfg.NoRecord))) + return builder.String() +} + +func maskKey(key string) string { + if len(key) <= 4 { + return "****" + } + return key[:2] + strings.Repeat("*", len(key)-4) + key[len(key)-2:] +} + +func yesNo(b bool) string { + if b { + return "是" + } + return "否" +} diff --git a/plugin/aichat/main.go b/plugin/aichat/main.go new file mode 100644 index 0000000000..fc78af58c6 --- /dev/null +++ b/plugin/aichat/main.go @@ -0,0 +1,541 @@ +// Package aichat OpenAI聊天和群聊总结 +package aichat + +import ( + "errors" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/fumiama/deepinfra" + "github.com/fumiama/deepinfra/model" + "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/AnimeAPI/airecord" + "github.com/FloatTech/floatbox/process" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/chat" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" +) + +var ( + // en data [8 temp] [8 rate] LSB + en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Extra: control.ExtraFromString("aichat"), + Brief: "OpenAI聊天", + Help: "- 设置AI聊天触发概率10\n" + + "- 设置AI聊天温度80\n" + + "- 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]\n" + + "- 设置AI聊天(不)支持系统提示词\n" + + "- 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions\n" + + "- 设置AI聊天密钥xxx\n" + + "- 设置AI聊天模型名Qwen/Qwen3-8B\n" + + "- 查看AI聊天系统提示词\n" + + "- 重置AI聊天系统提示词\n" + + "- 设置AI聊天系统提示词xxx\n" + + "- 设置AI聊天分隔符(留空则清除)\n" + + "- 设置AI聊天(不)响应AT\n" + + "- 设置AI聊天最大长度4096\n" + + "- 设置AI聊天TopP 0.9\n" + + "- 设置AI聊天(不)以AI语音输出\n" + + "- 查看AI聊天配置\n" + + "- 重置AI聊天\n" + + "- 群聊总结 [消息数目]|群聊总结 1000\n" + + "- /gpt [内容] (使用大模型聊天)\n", + + PrivateDataFolder: "aichat", + }) +) + +var ( + apitypes = map[string]uint8{ + "OpenAI": 0, + "OLLaMA": 1, + "GenAI": 2, + } + apilist = [3]string{"OpenAI", "OLLaMA", "GenAI"} + limit = ctxext.NewLimiterManager(time.Second*30, 1) +) + +// getModelParams 获取模型参数:温度(float32(temp)/100)、TopP和最大长度 +func getModelParams(temp int64) (temperature float32, topp float32, maxn uint) { + // 处理温度参数 + if temp <= 0 { + temp = 70 // default setting + } + if temp > 100 { + temp = 100 + } + temperature = float32(temp) / 100 + + // 处理TopP参数 + topp = cfg.TopP + if topp == 0 { + topp = 0.9 + } + + // 处理最大长度参数 + maxn = cfg.MaxN + if maxn == 0 { + maxn = 4096 + } + + return temperature, topp, maxn +} + +func init() { + en.OnMessage(ensureconfig, func(ctx *zero.Ctx) bool { + return ctx.ExtractPlainText() != "" && + (!cfg.NoReplyAT || (cfg.NoReplyAT && !ctx.Event.IsToMe)) + }).SetBlock(false).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + return + } + rate := c.GetData(gid) + temp := (rate >> 8) & 0xff + rate &= 0xff + if !ctx.Event.IsToMe && rand.Intn(100) >= int(rate) { + return + } + if ctx.Event.IsToMe { + ctx.Block() + } + if cfg.Key == "" { + logrus.Warnln("ERROR: get extra err: empty key") + return + } + + temperature, topp, maxn := getModelParams(temp) + + x := deepinfra.NewAPI(cfg.API, cfg.Key) + var mod model.Protocol + switch cfg.Type { + case 0: + mod = model.NewOpenAI( + cfg.ModelName, cfg.Separator, + temperature, topp, maxn, + ) + case 1: + mod = model.NewOLLaMA( + cfg.ModelName, cfg.Separator, + temperature, topp, maxn, + ) + case 2: + mod = model.NewGenAI( + cfg.ModelName, + temperature, topp, maxn, + ) + default: + logrus.Warnln("[aichat] unsupported AI type", cfg.Type) + return + } + + data, err := x.Request(chat.Ask(mod, gid, cfg.SystemP, cfg.NoSystemP)) + if err != nil { + logrus.Warnln("[aichat] post err:", err) + return + } + + txt := chat.Sanitize(strings.Trim(data, "\n  ")) + if len(txt) > 0 { + chat.Reply(gid, txt) + nick := zero.BotConfig.NickName[rand.Intn(len(zero.BotConfig.NickName))] + txt = strings.ReplaceAll(txt, "{name}", ctx.CardOrNickName(ctx.Event.UserID)) + txt = strings.ReplaceAll(txt, "{me}", nick) + id := any(nil) + if ctx.Event.IsToMe { + id = ctx.Event.MessageID + } + for _, t := range strings.Split(txt, "{segment}") { + if t == "" { + continue + } + logrus.Infoln("[aichat] 回复内容:", t) + recCfg := airecord.GetConfig() + record := "" + if !cfg.NoRecord { + record = ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, t) + } + if record != "" { + ctx.SendChain(message.Record(record)) + } else { + if id != nil { + id = ctx.SendChain(message.Reply(id), message.Text(t)) + } else { + id = ctx.SendChain(message.Text(t)) + } + } + process.SleepAbout1sTo2s() + } + } + }) + en.OnPrefix("设置AI聊天触发概率", zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + r, err := strconv.Atoi(args) + if err != nil { + ctx.SendChain(message.Text("ERROR: parse rate err: ", err)) + return + } + if r > 100 { + r = 100 + } else if r < 0 { + r = 0 + } + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + val := c.GetData(gid) & (^0xff) + err = c.SetData(gid, val|int64(r&0xff)) + if err != nil { + ctx.SendChain(message.Text("ERROR: set data err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + }) + en.OnPrefix("设置AI聊天温度", zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + r, err := strconv.Atoi(args) + if err != nil { + ctx.SendChain(message.Text("ERROR: parse rate err: ", err)) + return + } + if r > 100 { + r = 100 + } else if r < 0 { + r = 0 + } + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + val := c.GetData(gid) & (^0xff00) + err = c.SetData(gid, val|(int64(r&0xff)<<8)) + if err != nil { + ctx.SendChain(message.Text("ERROR: set data err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + }) + en.OnPrefix("设置AI聊天接口类型", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + args := strings.TrimSpace(ctx.State["args"].(string)) + if args == "" { + ctx.SendChain(message.Text("ERROR: empty args")) + return + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + typ, ok := apitypes[args] + if !ok { + ctx.SendChain(message.Text("ERROR: 未知类型 ", args)) + return + } + cfg.Type = int(typ) + err := c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + }) + en.OnPrefix("设置AI聊天接口地址", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetstr(&cfg.API)) + en.OnPrefix("设置AI聊天密钥", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetstr(&cfg.Key)) + en.OnPrefix("设置AI聊天模型名", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetstr(&cfg.ModelName)) + en.OnPrefix("设置AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetstr(&cfg.SystemP)) + en.OnFullMatch("查看AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text(cfg.SystemP)) + }) + en.OnFullMatch("重置AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + cfg.SystemP = chat.SystemPrompt + err := c.SetExtra(&cfg) + if err != nil { + ctx.SendChain(message.Text("ERROR: set extra err: ", err)) + return + } + ctx.SendChain(message.Text("成功")) + }) + en.OnPrefix("设置AI聊天分隔符", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetstr(&cfg.Separator)) + en.OnRegex("^设置AI聊天(不)?响应AT$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetbool(&cfg.NoReplyAT)) + en.OnRegex("^设置AI聊天(不)?支持系统提示词$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetbool(&cfg.NoSystemP)) + en.OnPrefix("设置AI聊天最大长度", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetuint(&cfg.MaxN)) + en.OnPrefix("设置AI聊天TopP", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetfloat32(&cfg.TopP)) + en.OnRegex("^设置AI聊天(不)?以AI语音输出$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(newextrasetbool(&cfg.NoRecord)) + en.OnFullMatch("查看AI聊天配置", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("ERROR: no such plugin")) + return + } + gid := ctx.Event.GroupID + rate := c.GetData(gid) & 0xff + temp := (c.GetData(gid) >> 8) & 0xff + if temp <= 0 { + temp = 70 // default setting + } + if temp > 100 { + temp = 100 + } + ctx.SendChain(message.Text(printConfig(rate, temp, cfg))) + }) + en.OnFullMatch("重置AI聊天", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { + chat.Reset() + ctx.SendChain(message.Text("成功")) + }) + + // 添加群聊总结功能 + en.OnRegex(`^群聊总结\s?(\d*)$`, ensureconfig, zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(limit.LimitByGroup).Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text("少女思考中...")) + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + return + } + rate := c.GetData(gid) + temp := (rate >> 8) & 0xff + p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) + if p > 1000 { + p = 1000 + } + if p == 0 { + p = 200 + } + group := ctx.GetGroupInfo(gid, false) + if group.MemberCount == 0 { + ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "未加入", group.Name, "(", gid, "),无法获取总结")) + return + } + + var messages []string + + h := ctx.GetGroupMessageHistory(gid, 0, p, false) + h.Get("messages").ForEach(func(_, msgObj gjson.Result) bool { + nickname := msgObj.Get("sender.nickname").Str + text := strings.TrimSpace(message.ParseMessageFromString(msgObj.Get("raw_message").Str).ExtractPlainText()) + if text != "" { + messages = append(messages, nickname+": "+text) + } + return true + }) + + if len(messages) == 0 { + ctx.SendChain(message.Text("ERROR: 历史消息为空或者无法获得历史消息")) + return + } + + // 构造总结请求提示 + summaryPrompt := "请总结这个群聊内容,要求按发言顺序梳理,明确标注每个发言者的昵称,并完整呈现其核心观点、提出的问题、发表的看法或做出的回应,确保不遗漏关键信息,且能体现成员间的对话逻辑和互动关系:\n" + + strings.Join(messages, "\n") + + // 调用大模型API进行总结 + summary, err := llmchat(summaryPrompt, temp) + + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + + var b strings.Builder + b.WriteString("群 ") + b.WriteString(group.Name) + b.WriteByte('(') + b.WriteString(strconv.FormatInt(gid, 10)) + b.WriteString(") 的 ") + b.WriteString(strconv.FormatInt(p, 10)) + b.WriteString(" 条消息总结:\n\n") + b.WriteString(summary) + + // 分割总结内容为多段(按1000字符长度切割) + summaryText := b.String() + msg := make(message.Message, 0) + for len(summaryText) > 0 { + if len(summaryText) <= 1000 { + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(summaryText))) + break + } + + // 查找1000字符内的最后一个换行符,尽量在换行处分割 + chunk := summaryText[:1000] + lastNewline := strings.LastIndex(chunk, "\n") + if lastNewline > 0 { + chunk = summaryText[:lastNewline+1] + } + + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk))) + summaryText = summaryText[len(chunk):] + } + if len(msg) > 0 { + ctx.Send(msg) + } + }) + + // 添加 /gpt 命令处理(同时支持回复消息和直接使用) + en.OnKeyword("/gpt", ensureconfig).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + if gid == 0 { + gid = -ctx.Event.UserID + } + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + return + } + rate := c.GetData(gid) + temp := (rate >> 8) & 0xff + text := ctx.MessageString() + + var query string + var replyContent string + + // 检查是否是回复消息 (使用MessageElement检查而不是CQ码) + for _, elem := range ctx.Event.Message { + if elem.Type == "reply" { + // 提取被回复的消息ID + replyIDStr := elem.Data["id"] + replyID, err := strconv.ParseInt(replyIDStr, 10, 64) + if err == nil { + // 获取被回复的消息内容 + replyMsg := ctx.GetMessage(replyID) + if replyMsg.Elements != nil { + replyContent = replyMsg.Elements.ExtractPlainText() + } + } + break // 找到回复元素后退出循环 + } + } + + // 提取 /gpt 后面的内容 + parts := strings.SplitN(text, "/gpt", 2) + + var gContent string + if len(parts) > 1 { + gContent = strings.TrimSpace(parts[1]) + } + + // 组合内容:优先使用回复内容,如果同时有/gpt内容则拼接 + switch { + case replyContent != "" && gContent != "": + query = replyContent + "\n" + gContent + case replyContent != "": + query = replyContent + case gContent != "": + query = gContent + default: + return + } + + // 调用大模型API进行聊天 + reply, err := llmchat(query, temp) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + + // 分割总结内容为多段(按1000字符长度切割) + msg := make(message.Message, 0) + for len(reply) > 0 { + if len(reply) <= 1000 { + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(reply))) + break + } + + // 查找1000字符内的最后一个换行符,尽量在换行处分割 + chunk := reply[:1000] + lastNewline := strings.LastIndex(chunk, "\n") + if lastNewline > 0 { + chunk = reply[:lastNewline+1] + } + + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk))) + reply = reply[len(chunk):] + } + if len(msg) > 0 { + ctx.Send(msg) + } + }) +} + +// llmchat 调用大模型API包装 +func llmchat(prompt string, temp int64) (string, error) { + temperature, topp, maxn := getModelParams(temp) // 使用默认温度70 + + x := deepinfra.NewAPI(cfg.API, cfg.Key) + var mod model.Protocol + switch cfg.Type { + case 0: + mod = model.NewOpenAI( + cfg.ModelName, cfg.Separator, + temperature, topp, maxn, + ) + case 1: + mod = model.NewOLLaMA( + cfg.ModelName, cfg.Separator, + temperature, topp, maxn, + ) + case 2: + mod = model.NewGenAI( + cfg.ModelName, + temperature, topp, maxn, + ) + default: + logrus.Warnln("[aichat] unsupported AI type", cfg.Type) + return "", errors.New("不支持的AI类型") + } + + data, err := x.Request(mod.User(prompt)) + if err != nil { + return "", err + } + + return strings.TrimSpace(data), nil +} diff --git a/plugin/aifalse/main.go b/plugin/aifalse/main.go index a8addb24da..dc93720751 100644 --- a/plugin/aifalse/main.go +++ b/plugin/aifalse/main.go @@ -20,7 +20,6 @@ import ( "github.com/FloatTech/floatbox/web" "github.com/FloatTech/gg" "github.com/FloatTech/imgfactory" - "github.com/FloatTech/rendercard" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" @@ -41,7 +40,7 @@ import ( ) const ( - backgroundURL = "https://iw233.cn/api.php?sort=mp" + backgroundURL = "https://pic.re/image" referer = "https://weibo.com/" ) @@ -184,7 +183,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta return } - data, err = web.GetData("http://q4.qlogo.cn/g?b=qq&nk=" + strconv.FormatInt(uid, 10) + "&s=640") + data, err = web.GetData("https://q4.qlogo.cn/g?b=qq&nk=" + strconv.FormatInt(uid, 10) + "&s=640") if err != nil { return } @@ -237,14 +236,17 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta defer wg.Done() titlecard := gg.NewContext(cardw, titlecardh) bwg.Wait() - titlecard.DrawImage(blurback, -70, -70) titlecard.DrawRoundedRectangle(1, 1, float64(titlecard.W()-1*2), float64(titlecardh-1*2), 16) + titlecard.ClipPreserve() + titlecard.DrawImage(blurback, -70, -70) + titlecard.SetColor(colorswitch(140)) + titlecard.FillPreserve() + titlecard.SetLineWidth(3) titlecard.SetColor(colorswitch(100)) - titlecard.StrokePreserve() - titlecard.SetColor(colorswitch(140)) - titlecard.Fill() + titlecard.ResetClip() + titlecard.Stroke() titlecard.DrawImage(avatarf.Circle(0).Image(), (titlecardh-avatarf.H())/2, (titlecardh-avatarf.H())/2) @@ -288,20 +290,23 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta fw, _ = titlecard.MeasureString(bs) titlecard.DrawStringAnchored(bs, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.75/2), 0.5, 0.5) - titleimg = rendercard.Fillet(titlecard.Image(), 16) + titleimg = titlecard.Image() }() go func() { defer wg.Done() basiccard := gg.NewContext(cardw, basiccardh) bwg.Wait() - basiccard.DrawImage(blurback, -70, -70-titlecardh-40) basiccard.DrawRoundedRectangle(1, 1, float64(basiccard.W()-1*2), float64(basiccardh-1*2), 16) + basiccard.ClipPreserve() + basiccard.DrawImage(blurback, -70, -70-titlecardh-40) + basiccard.SetColor(colorswitch(140)) + basiccard.FillPreserve() + basiccard.SetLineWidth(3) basiccard.SetColor(colorswitch(100)) - basiccard.StrokePreserve() - basiccard.SetColor(colorswitch(140)) - basiccard.Fill() + basiccard.ResetClip() + basiccard.Stroke() bslen := len(basicstate) for i, v := range basicstate { @@ -361,20 +366,23 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta basiccard.DrawStringAnchored(s, (float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200+15+fw+15+basiccard.FontHeight()/2+float64(k)*textoffsety, 0.5, 0.5) } } - basicimg = rendercard.Fillet(basiccard.Image(), 16) + basicimg = basiccard.Image() }() go func() { defer wg.Done() diskcard := gg.NewContext(cardw, diskcardh) bwg.Wait() - diskcard.DrawImage(blurback, -70, -70-titlecardh-40-basiccardh-40) diskcard.DrawRoundedRectangle(1, 1, float64(diskcard.W()-1*2), float64(diskcardh-1*2), 16) + diskcard.ClipPreserve() + diskcard.DrawImage(blurback, -70, -70-titlecardh-40-basiccardh-40) + diskcard.SetColor(colorswitch(140)) + diskcard.FillPreserve() + diskcard.SetLineWidth(3) diskcard.SetColor(colorswitch(100)) - diskcard.StrokePreserve() - diskcard.SetColor(colorswitch(140)) - diskcard.Fill() + diskcard.ResetClip() + diskcard.Stroke() err = diskcard.ParseFontFace(fontbyte, 32) if err != nil { @@ -427,6 +435,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta } diskcard.DrawRoundedRectangle(40, 40+(float64(diskcardh-40*2)-50*float64(dslen))/float64(dslen-1)+offset, float64(diskcard.W())-40-100, 50, 12) + diskcard.ClipPreserve() diskcard.Fill() colors := darkcolor @@ -445,6 +454,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta diskcard.DrawRoundedRectangle(40, 40+(float64(diskcardh-40*2)-50*float64(dslen))/float64(dslen-1)+offset, (float64(diskcard.W())-40-100)*v.precent*0.01, 50, 12) diskcard.Fill() + diskcard.ResetClip() diskcard.SetColor(fontcolorswitch()) @@ -456,20 +466,23 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta diskcard.DrawStringAnchored(strconv.FormatFloat(v.precent, 'f', 0, 64)+"%", float64(diskcard.W())-100/2, 40+(float64(diskcardh-40*2)-50*float64(dslen))/float64(dslen-1)+50/2+offset, 0.5, 0.5) } } - diskimg = rendercard.Fillet(diskcard.Image(), 16) + diskimg = diskcard.Image() }() go func() { defer wg.Done() moreinfocard := gg.NewContext(cardw, moreinfocardh) bwg.Wait() - moreinfocard.DrawImage(blurback, -70, -70-titlecardh-40-basiccardh-40-diskcardh-40) moreinfocard.DrawRoundedRectangle(1, 1, float64(moreinfocard.W()-1*2), float64(moreinfocard.H()-1*2), 16) + moreinfocard.ClipPreserve() + moreinfocard.DrawImage(blurback, -70, -70-titlecardh-40-basiccardh-40-diskcardh-40) + moreinfocard.SetColor(colorswitch(140)) + moreinfocard.FillPreserve() + moreinfocard.SetLineWidth(3) moreinfocard.SetColor(colorswitch(100)) - moreinfocard.StrokePreserve() - moreinfocard.SetColor(colorswitch(140)) - moreinfocard.Fill() + moreinfocard.ResetClip() + moreinfocard.Stroke() err = moreinfocard.ParseFontFace(fontbyte, 32) if err != nil { @@ -488,7 +501,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunsta moreinfocard.DrawStringAnchored(v.name, 20+fw/2, 30+(float64(moreinfocardh-30*2)-moreinfocard.FontHeight()*float64(milen))/float64(milen-1)+moreinfocard.FontHeight()/2+offset, 0.5, 0.5) moreinfocard.DrawStringAnchored(v.text[0], float64(moreinfocard.W())-20-fw1/2, 30+(float64(moreinfocardh-30*2)-moreinfocard.FontHeight()*float64(milen))/float64(milen-1)+moreinfocard.FontHeight()/2+offset, 0.5, 0.5) } - moreinfoimg = rendercard.Fillet(moreinfocard.Image(), 16) + moreinfoimg = moreinfocard.Image() }() go func() { defer wg.Done() @@ -668,7 +681,7 @@ func diskstate() (stateinfo []*status, err error) { func moreinfo(m *ctrl.Control[*zero.Ctx]) (stateinfo []*status, err error) { var mems runtime.MemStats runtime.ReadMemStats(&mems) - fmtmem := storagefmt(float64(mems.Sys)) + fmtmem := storagefmt(float64(mems.Alloc)) hostinfo, err := host.Info() if err != nil { diff --git a/plugin/aiimage/config.go b/plugin/aiimage/config.go new file mode 100644 index 0000000000..e48ec5c926 --- /dev/null +++ b/plugin/aiimage/config.go @@ -0,0 +1,56 @@ +// Package aiimage 提供AI画图功能配置 +package aiimage + +import ( + "fmt" + "strings" + "sync" + + sql "github.com/FloatTech/sqlite" +) + +// storage 管理画图配置存储 +type storage struct { + sync.RWMutex + db sql.Sqlite +} + +// imageConfig 存储AI画图配置信息 +type imageConfig struct { + ID int64 `db:"id"` // 主键ID + APIKey string `db:"apiKey"` // API密钥 + APIURL string `db:"apiUrl"` // API地址 + ModelName string `db:"modelName"` // 画图模型名称 +} + +// getConfig 获取当前配置 +func (sdb *storage) getConfig() imageConfig { + sdb.RLock() + defer sdb.RUnlock() + cfg := imageConfig{} + _ = sdb.db.Find("config", &cfg, "WHERE id = 1") + return cfg +} + +// setConfig 设置AI画图配置 +func (sdb *storage) setConfig(apiKey, apiURL, modelName string) error { + sdb.Lock() + defer sdb.Unlock() + return sdb.db.Insert("config", &imageConfig{ + ID: 1, + APIKey: apiKey, + APIURL: apiURL, + ModelName: modelName, + }) +} + +// PrintConfig 返回格式化后的配置信息 +func (sdb *storage) PrintConfig() string { + cfg := sdb.getConfig() + var builder strings.Builder + builder.WriteString("当前AI画图配置:\n") + builder.WriteString(fmt.Sprintf("• 密钥: %s\n", cfg.APIKey)) + builder.WriteString(fmt.Sprintf("• 接口地址: %s\n", cfg.APIURL)) + builder.WriteString(fmt.Sprintf("• 模型名: %s\n", cfg.ModelName)) + return builder.String() +} diff --git a/plugin/aiimage/main.go b/plugin/aiimage/main.go new file mode 100644 index 0000000000..519065e92a --- /dev/null +++ b/plugin/aiimage/main.go @@ -0,0 +1,171 @@ +// Package aiimage AI画图 +package aiimage + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" + "time" + + fcext "github.com/FloatTech/floatbox/ctxext" + "github.com/FloatTech/floatbox/web" + sql "github.com/FloatTech/sqlite" + "github.com/tidwall/gjson" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" +) + +func init() { + var sdb = &storage{} + + en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Extra: control.ExtraFromString("aiimage"), + Brief: "AI画图", + Help: "- 设置AI画图密钥xxx\n" + + "- 设置AI画图接口地址https://api.siliconflow.cn/v1/images/generations\n" + + "- 设置AI画图模型名Kwai-Kolors/Kolors\n" + + "- 查看AI画图配置\n" + + "- AI画图 [描述]", + PrivateDataFolder: "aiimage", + }) + + getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { + sdb.db = sql.New(en.DataFolder() + "aiimage.db") + err := sdb.db.Open(time.Hour) + if err == nil { + // 创建配置表 + err = sdb.db.Create("config", &imageConfig{}) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + return false + } + return true + } + ctx.SendChain(message.Text("[ERROR]:", err)) + return false + }) + + en.OnPrefix("设置AI画图密钥", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + apiKey := strings.TrimSpace(ctx.State["args"].(string)) + cfg := sdb.getConfig() + err := sdb.setConfig(apiKey, cfg.APIURL, cfg.ModelName) + if err != nil { + ctx.SendChain(message.Text("ERROR: 设置API密钥失败: ", err)) + return + } + ctx.SendChain(message.Text("成功设置API密钥")) + }) + + en.OnPrefix("设置AI画图接口地址", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + apiURL := strings.TrimSpace(ctx.State["args"].(string)) + cfg := sdb.getConfig() + err := sdb.setConfig(cfg.APIKey, apiURL, cfg.ModelName) + if err != nil { + ctx.SendChain(message.Text("ERROR: 设置API地址失败: ", err)) + return + } + ctx.SendChain(message.Text("成功设置API地址")) + }) + + en.OnPrefix("设置AI画图模型名", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + modelName := strings.TrimSpace(ctx.State["args"].(string)) + cfg := sdb.getConfig() + err := sdb.setConfig(cfg.APIKey, cfg.APIURL, modelName) + if err != nil { + ctx.SendChain(message.Text("ERROR: 设置模型失败: ", err)) + return + } + ctx.SendChain(message.Text("成功设置模型: ", modelName)) + }) + + en.OnFullMatch("查看AI画图配置", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text(sdb.PrintConfig())) + }) + + en.OnPrefix("AI画图", getdb).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text("少女思考中...")) + prompt := strings.TrimSpace(ctx.State["args"].(string)) + if prompt == "" { + ctx.SendChain(message.Text("请输入图片描述")) + return + } + + cfg := sdb.getConfig() + if cfg.APIKey == "" || cfg.APIURL == "" || cfg.ModelName == "" { + ctx.SendChain(message.Text("请先配置API密钥、地址和模型")) + return + } + + // 准备请求数据 + reqBytes, _ := json.Marshal(map[string]interface{}{ + "model": cfg.ModelName, + "prompt": prompt, + "image_size": "1024x1024", + "batch_size": 4, + "num_inference_steps": 20, + "guidance_scale": 7.5, + }) + + // 发送API请求 + data, err := web.RequestDataWithHeaders( + web.NewDefaultClient(), + cfg.APIURL, + "POST", + func(req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+cfg.APIKey) + req.Header.Set("Content-Type", "application/json") + return nil + }, + bytes.NewReader(reqBytes), + ) + if err != nil { + ctx.SendChain(message.Text("API请求失败: ", err)) + return + } + + // 解析API响应 + jsonData := gjson.ParseBytes(data) + images := jsonData.Get("images") + if !images.Exists() { + images = jsonData.Get("data") + if !images.Exists() { + ctx.SendChain(message.Text("未获取到图片URL")) + return + } + } + + // 发送生成的图片和相关信息 + inferenceTime := jsonData.Get("timings.inference").Float() + seed := jsonData.Get("seed").Int() + msg := make(message.Message, 0, 1) + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text("图片生成成功!\n", + "提示词: ", prompt, "\n", + "模型: ", cfg.ModelName, "\n", + "推理时间: ", inferenceTime, "秒\n", + "种子: ", seed))) + + // 添加所有图片 + images.ForEach(func(_, value gjson.Result) bool { + url := value.Get("url").String() + if url != "" { + msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Image(url))) + } + return true + }) + + if len(msg) > 0 { + ctx.Send(msg) + } + }) +} diff --git a/plugin/airecord/record.go b/plugin/airecord/record.go new file mode 100644 index 0000000000..908784d483 --- /dev/null +++ b/plugin/airecord/record.go @@ -0,0 +1,134 @@ +// Package airecord 群应用:AI声聊 +package airecord + +import ( + "strconv" + "strings" + "time" + + "github.com/tidwall/gjson" + + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/AnimeAPI/airecord" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" +) + +func init() { + en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Extra: control.ExtraFromString("airecord"), + Brief: "群应用:AI声聊", + Help: "- 设置AI语音群号1048452984(tips:机器人任意所在群聊即可)\n" + + "- 设置AI语音模型\n" + + "- 查看AI语音配置\n" + + "- 发送AI语音xxx", + PrivateDataFolder: "airecord", + }) + + en.OnPrefix("设置AI语音群号", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + u := strings.TrimSpace(ctx.State["args"].(string)) + num, err := strconv.ParseInt(u, 10, 64) + if err != nil { + ctx.SendChain(message.Text("ERROR: parse gid err: ", err)) + return + } + err = airecord.SetCustomGID(num) + if err != nil { + ctx.SendChain(message.Text("ERROR: set gid err: ", err)) + return + } + ctx.SendChain(message.Text("设置AI语音群号为", num)) + }) + en.OnFullMatch("设置AI语音模型", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession()) + recv, cancel := next.Repeat() + defer cancel() + jsonData := ctx.GetAICharacters(0, 1) + + // 转换为字符串数组 + var names []string + // 初始化两个映射表 + nameToID := make(map[string]string) + nameToURL := make(map[string]string) + characters := jsonData.Get("#.characters") + + // 遍历每个角色对象 + characters.ForEach(func(_, group gjson.Result) bool { + group.ForEach(func(_, character gjson.Result) bool { + // 提取当前角色的三个字段 + name := character.Get("character_name").String() + names = append(names, name) + // 存入映射表(重复名称会覆盖,保留最后出现的条目) + nameToID[name] = character.Get("character_id").String() + nameToURL[name] = character.Get("preview_url").String() + return true // 继续遍历 + }) + return true // 继续遍历 + }) + var builder strings.Builder + // 写入开头文本 + builder.WriteString("请选择语音模型序号:\n") + + // 遍历names数组,拼接序号和名称 + for i, v := range names { + // 将数字转换为字符串(不依赖fmt) + numStr := strconv.Itoa(i) + // 拼接格式:"序号. 名称\n" + builder.WriteString(numStr) + builder.WriteString(". ") + builder.WriteString(v) + builder.WriteString("\n") + } + // 获取最终字符串 + ctx.SendChain(message.Text(builder.String())) + for { + select { + case <-time.After(time.Second * 120): + ctx.SendChain(message.Text("设置AI语音模型指令过期")) + return + case ct := <-recv: + msg := ct.Event.Message.ExtractPlainText() + num, err := strconv.Atoi(msg) + if err != nil { + ctx.SendChain(message.Text("请输入数字!")) + continue + } + if num < 0 || num >= len(names) { + ctx.SendChain(message.Text("序号非法!")) + continue + } + err = airecord.SetRecordModel(names[num], nameToID[names[num]]) + if err != nil { + ctx.SendChain(message.Text("ERROR: set model err: ", err)) + continue + } + ctx.SendChain(message.Text("已选择语音模型: ", names[num])) + ctx.SendChain(message.Record(nameToURL[names[num]])) + return + } + } + }) + en.OnFullMatch("查看AI语音配置", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + ctx.SendChain(message.Text(airecord.PrintRecordConfig())) + }) + en.OnPrefix("发送AI语音", zero.UserOrGrpAdmin).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + u := strings.TrimSpace(ctx.State["args"].(string)) + recCfg := airecord.GetConfig() + record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, u) + if record == "" { + id := ctx.SendGroupAIRecord(recCfg.ModelID, ctx.Event.GroupID, u) + if id == "" { + ctx.SendChain(message.Text("ERROR: get record err: empty record")) + return + } + } + ctx.SendChain(message.Record(record)) + }) +} diff --git a/plugin/aireply/ai_tts.go b/plugin/aireply/ai_tts.go deleted file mode 100644 index dcf9a2a154..0000000000 --- a/plugin/aireply/ai_tts.go +++ /dev/null @@ -1,285 +0,0 @@ -package aireply - -import ( - "errors" - "strings" - - "github.com/RomiChan/syncx" - zero "github.com/wdvxdr1123/ZeroBot" - - "github.com/FloatTech/AnimeAPI/aireply" - "github.com/FloatTech/AnimeAPI/tts" - "github.com/FloatTech/AnimeAPI/tts/baidutts" - "github.com/FloatTech/AnimeAPI/tts/genshin" - "github.com/FloatTech/AnimeAPI/tts/lolimi" - "github.com/FloatTech/AnimeAPI/tts/ttscn" - ctrl "github.com/FloatTech/zbpctrl" - "github.com/FloatTech/zbputils/control" -) - -// 数据结构: [8 bits] [8 bits] [8 bits] -// [具体人物] [tts模式] [回复模式] - -// defaultttsindexkey -// 数据结构: [8 bits] [8 bits] -// [具体人物] [tts模式] - -// [tts模式]: 0~200 genshin 201 baidu 202 ttscn 203 lolimi - -const ( - baiduttsindex = 201 + iota - ttscnttsindex - lolimittsindex -) - -// extrattsname is the tts other than genshin vits -var extrattsname = []string{"百度", "TTSCN", "桑帛云"} - -var ttscnspeakers = [...]string{ - "晓晓(女 - 年轻人)", - "云扬(男 - 年轻人)", - "晓辰(女 - 年轻人 - 抖音热门)", - "晓涵(女 - 年轻人)", - "晓墨(女 - 年轻人)", - "晓秋(女 - 中年人)", - "晓睿(女 - 老年)", - "晓双(女 - 儿童)", - "晓萱(女 - 年轻人)", - "晓颜(女 - 年轻人)", - "晓悠(女 - 儿童)", - "云希(男 - 年轻人 - 抖音热门)", - "云野(男 - 中年人)", - "晓梦(女 - 年轻人)", - "晓伊(女 - 儿童)", - "晓甄(女 - 年轻人)", -} - -const defaultttsindexkey = -2905 - -var ( - 原 = newapikeystore("./data/tts/o.txt") - ཆཏ = newapikeystore("./data/tts/c.txt") - 百 = newapikeystore("./data/tts/b.txt") -) - -type replymode []string - -func (r replymode) setReplyMode(ctx *zero.Ctx, name string) error { - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - var ok bool - var index int64 - for i, s := range r { - if s == name { - ok = true - index = int64(i) - break - } - } - if !ok { - return errors.New("no such mode") - } - m, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - if !ok { - return errors.New("no such plugin") - } - return m.SetData(gid, (m.GetData(gid)&^0xff)|(index&0xff)) -} - -func (r replymode) getReplyMode(ctx *zero.Ctx) aireply.AIReply { - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - m, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - if ok { - switch m.GetData(gid) & 0xff { - case 0: - return aireply.NewLolimiAi(aireply.JingfengURL, aireply.JingfengBotName) - case 1: - return aireply.NewLolimiAi(aireply.MomoURL, aireply.MomoBotName) - case 2: - return aireply.NewQYK(aireply.QYKURL, aireply.QYKBotName) - case 3: - return aireply.NewXiaoAi(aireply.XiaoAiURL, aireply.XiaoAiBotName) - case 4: - k := ཆཏ.k - if k != "" { - return aireply.NewChatGPT(aireply.ChatGPTURL, k) - } - return aireply.NewLolimiAi(aireply.JingfengURL, aireply.JingfengBotName) - } - } - return aireply.NewLolimiAi(aireply.JingfengURL, aireply.JingfengBotName) -} - -var ttsins = func() map[string]tts.TTS { - m := make(map[string]tts.TTS, 512) - for _, mode := range append(genshin.SoundList[:], extrattsname...) { - m[mode] = nil - } - return m -}() - -var ttsModes = func() []string { - s := append(genshin.SoundList[:], make([]string, baiduttsindex-len(genshin.SoundList))...) // 0-200 - s = append(s, extrattsname...) // 201 202 ... - return s -}() - -type ttsmode syncx.Map[int64, int64] - -func list(list []string, num int) string { - s := "" - for i, value := range list { - s += value - if (i+1)%num == 0 { - s += "\n" - } else { - s += " | " - } - } - return s -} - -func newttsmode() *ttsmode { - t := &ttsmode{} - m, ok := control.Lookup("tts") - (*syncx.Map[int64, int64])(t).Store(defaultttsindexkey, 0) - if ok { - index := m.GetData(defaultttsindexkey) - msk := index & 0xff - if msk >= 0 && (msk < int64(len(ttsModes))) { - (*syncx.Map[int64, int64])(t).Store(defaultttsindexkey, index) - } - } - return t -} - -func (t *ttsmode) setSoundMode(ctx *zero.Ctx, name string, character int) error { - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - _, ok := ttsins[name] - if !ok { - return errors.New("不支持设置语音人物" + name) - } - var index = int64(-1) - for i, s := range genshin.SoundList { - if s == name { - index = int64(i + 1) - break - } - } - if index == -1 { - switch name { - case extrattsname[0]: - index = baiduttsindex - case extrattsname[1]: - index = ttscnttsindex - case extrattsname[2]: - index = lolimittsindex - default: - return errors.New("语音人物" + name + "未注册index") - } - } - m := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - // 按原来的逻辑map存的是前16位 - storeIndex := (m.GetData(gid) &^ 0xffff00) | ((index << 8) & 0xff00) | ((int64(character) << 16) & 0xff0000) - (*syncx.Map[int64, int64])(t).Store(gid, (storeIndex>>8)&0xffff) - return m.SetData(gid, storeIndex) -} - -func (t *ttsmode) getSoundMode(ctx *zero.Ctx) (tts.TTS, error) { - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - i, ok := (*syncx.Map[int64, int64])(t).Load(gid) - if !ok { - m := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - i = m.GetData(gid) >> 8 - } - m := i & 0xff - if m <= 0 || (m >= int64(len(ttsModes))) { - i, _ = (*syncx.Map[int64, int64])(t).Load(defaultttsindexkey) - if i == 0 { - i = ctx.State["manager"].(*ctrl.Control[*zero.Ctx]).GetData(defaultttsindexkey) - (*syncx.Map[int64, int64])(t).Store(defaultttsindexkey, i) - } - m = i & 0xff - } - mode := ttsModes[m] - ins, ok := ttsins[mode] - if !ok || ins == nil { - switch mode { - case extrattsname[0]: - id, sec, _ := strings.Cut(百.k, ",") - ins = baidutts.NewBaiduTTS(int(i&0xff00)>>8, id, sec) - case extrattsname[1]: - var err error - ins, err = ttscn.NewTTSCN("中文(普通话,简体)", ttscnspeakers[int(i&0xff00)>>8], ttscn.KBRates[0]) - if err != nil { - return nil, err - } - case extrattsname[2]: - ins = lolimi.NewLolimi(int(i&0xff00) >> 8) - default: // 原神 - k := 原.k - if k != "" { - ins = genshin.NewGenshin(int(m-1), 原.k) - ttsins[mode] = ins - } else { - ins = lolimi.NewLolimi(int(i&0xff00) >> 8) - } - } - } - return ins, nil -} - -func (t *ttsmode) resetSoundMode(ctx *zero.Ctx) error { - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - m := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - // 只保留后面8位 - (*syncx.Map[int64, int64])(t).Delete(gid) - return m.SetData(gid, (m.GetData(gid) & 0xff)) // 重置数据 -} - -func (t *ttsmode) setDefaultSoundMode(name string, character int) error { - _, ok := ttsins[name] - if !ok { - return errors.New("不支持设置语音人物" + name) - } - index := int64(-1) - for i, s := range genshin.SoundList { - if s == name { - index = int64(i + 1) - break - } - } - if index == -1 { - switch name { - case extrattsname[0]: - index = baiduttsindex - case extrattsname[1]: - index = ttscnttsindex - case extrattsname[2]: - index = lolimittsindex - default: - return errors.New("语音人物" + name + "未注册index") - } - } - m, ok := control.Lookup("tts") - if !ok { - return errors.New("[tts] service not found") - } - storeIndex := (index & 0xff) | ((int64(character) << 8) & 0xff00) - (*syncx.Map[int64, int64])(t).Store(defaultttsindexkey, storeIndex) - return m.SetData(defaultttsindexkey, storeIndex) -} diff --git a/plugin/aireply/main.go b/plugin/aireply/main.go deleted file mode 100644 index e7e3158ea2..0000000000 --- a/plugin/aireply/main.go +++ /dev/null @@ -1,227 +0,0 @@ -// Package aireply AI 回复 -package aireply - -import ( - "os" - "regexp" - "strconv" - "strings" - "time" - - "github.com/FloatTech/AnimeAPI/tts/genshin" - ctrl "github.com/FloatTech/zbpctrl" - "github.com/FloatTech/zbputils/control" - "github.com/FloatTech/zbputils/ctxext" - "github.com/sirupsen/logrus" - zero "github.com/wdvxdr1123/ZeroBot" - "github.com/wdvxdr1123/ZeroBot/message" -) - -var replmd = replymode([]string{"婧枫", "沫沫", "青云客", "小爱", "ChatGPT"}) - -var ttsmd = newttsmode() - -func init() { // 插件主体 - ent := control.Register("tts", &ctrl.Options[*zero.Ctx]{ - DisableOnDefault: true, - Brief: "人工智能语音回复", - Help: "- @Bot 任意文本(任意一句话回复)\n" + - "- 设置语音模式[原神人物/百度/TTSCN/桑帛云] 数字(百度/TTSCN说话人/桑帛云)\n" + - "- 设置默认语音模式[原神人物/百度/TTSCN/桑帛云] 数字(百度/TTSCN说话人/桑帛云)\n" + - "- 恢复成默认语音模式\n" + - "- 设置语音回复模式[沫沫|婧枫|青云客|小爱|ChatGPT]\n" + - "- 设置原神语音 api key xxxxxx (key请加开发群获得)\n" + - "- 设置百度语音 api id xxxxxx secret xxxxxx (请自行获得)\n" + - "当前适用的原神人物含有以下: \n" + list(genshin.SoundList[:], 5) + - "\n当前适用的TTSCN人物含有以下(以数字顺序代表): \n" + list(ttscnspeakers[:], 5), - PrivateDataFolder: "tts", - }) - - enr := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ - DisableOnDefault: false, - Brief: "人工智能回复", - Help: "- @Bot 任意文本(任意一句话回复)\n- 设置文字回复模式[婧枫|沫沫|青云客|小爱|ChatGPT]\n- 设置 ChatGPT api key xxx", - PrivateDataFolder: "aireply", - }) - - enr.OnMessage(zero.OnlyToMe).SetBlock(true).Limit(ctxext.LimitByUser). - Handle(func(ctx *zero.Ctx) { - aireply := replmd.getReplyMode(ctx) - reply := message.ParseMessageFromString(aireply.Talk(ctx.Event.UserID, ctx.ExtractPlainText(), zero.BotConfig.NickName[0])) - // 回复 - time.Sleep(time.Second * 1) - reply = append(reply, message.Reply(ctx.Event.MessageID)) - ctx.Send(reply) - }) - setReplyMode := func(ctx *zero.Ctx) { - param := ctx.State["args"].(string) - err := replmd.setReplyMode(ctx, param) - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) - return - } - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("成功")) - } - enr.OnPrefix("设置文字回复模式", zero.AdminPermission).SetBlock(true).Handle(setReplyMode) - enr.OnRegex(`^设置\s*ChatGPT\s*api\s*key\s*(.*)$`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - err := ཆཏ.set(ctx.State["regex_matched"].([]string)[1]) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("设置成功")) - }) - - endpre := regexp.MustCompile(`\pP$`) - ttscachedir := ent.DataFolder() + "cache/" - _ = os.RemoveAll(ttscachedir) - err := os.MkdirAll(ttscachedir, 0755) - if err != nil { - panic(err) - } - ent.OnMessage(zero.OnlyToMe).SetBlock(true).Limit(ctxext.LimitByUser). - Handle(func(ctx *zero.Ctx) { - msg := ctx.ExtractPlainText() - // 获取回复模式 - r := replmd.getReplyMode(ctx) - // 获取回复的文本 - reply := message.ParseMessageFromString(r.TalkPlain(ctx.Event.UserID, msg, zero.BotConfig.NickName[0])) - // 过滤掉文字消息 - filterMsg := make([]message.MessageSegment, 0, len(reply)) - sb := strings.Builder{} - for _, v := range reply { - if v.Type != "text" { - filterMsg = append(filterMsg, v) - } else { - sb.WriteString(v.Data["text"]) - } - } - // 纯文本 - plainReply := sb.String() - plainReply = strings.ReplaceAll(plainReply, "\n", "") - // 获取语音 - speaker, err := ttsmd.getSoundMode(ctx) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - rec, err := speaker.Speak(ctx.Event.UserID, func() string { - if !endpre.MatchString(plainReply) { - return plainReply + "。" - } - return plainReply - }) - // 发送前面的图片 - if len(filterMsg) != 0 { - filterMsg = append(filterMsg, message.Reply(ctx.Event.MessageID)) - ctx.Send(filterMsg) - } - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(plainReply)) - return - } - // 发送语音 - if id := ctx.SendChain(message.Record(rec)); id.ID() == 0 { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(plainReply)) - } - }) - ent.OnPrefix("设置语音回复模式", zero.AdminPermission).SetBlock(true).Handle(setReplyMode) - ent.OnRegex(`^设置语音模式\s*([\S\D]*)\s*(\d*)$`, zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - param := ctx.State["regex_matched"].([]string)[1] - num := ctx.State["regex_matched"].([]string)[2] - n := 0 - var err error - if num != "" { - n, err = strconv.Atoi(num) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - } - // 保存设置 - logrus.Debugln("[tts] t.setSoundMode( ctx", param, n, ")") - err = ttsmd.setSoundMode(ctx, param, n) - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) - return - } - banner := genshin.TestRecord[param] - if banner == "" { - banner = genshin.TestRecord["默认"] - } - logrus.Debugln("[tts] banner:", banner, "get sound mode...") - // 设置验证 - speaker, err := ttsmd.getSoundMode(ctx) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - logrus.Debugln("[tts] got sound mode, speaking...") - rec, err := speaker.Speak(ctx.Event.UserID, func() string { return banner }) - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("无法发送测试语音,请重试。")) - return - } - logrus.Debugln("[tts] sending...") - if id := ctx.SendChain(message.Record(rec).Add("cache", 0)); id.ID() == 0 { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("无法发送测试语音,请重试。")) - return - } - time.Sleep(time.Second * 2) - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("设置成功,当前为", speaker)) - }) - - ent.OnRegex(`^设置默认语音模式\s*([\S\D]*)\s+(\d*)$`, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - param := ctx.State["regex_matched"].([]string)[1] - num := ctx.State["regex_matched"].([]string)[2] - n := 0 - var err error - if num != "" { - n, err = strconv.Atoi(num) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - } - // 保存设置 - err = ttsmd.setDefaultSoundMode(param, n) - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) - return - } - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("设置成功")) - }) - - ent.OnFullMatch("恢复成默认语音模式", zero.AdminPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - err := ttsmd.resetSoundMode(ctx) - if err != nil { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(err)) - return - } - // 设置验证 - speaker, err := ttsmd.getSoundMode(ctx) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("设置成功,当前为", speaker)) - }) - - ent.OnRegex(`^设置原神语音\s*api\s*key\s*([0-9a-zA-Z-_]{54}==)$`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - err := 原.set(ctx.State["regex_matched"].([]string)[1]) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("设置成功")) - }) - - ent.OnRegex(`^设置百度语音\s*api\s*id\s*(.*)\s*secret\s*(.*)\s*$`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) { - err := 百.set(ctx.State["regex_matched"].([]string)[1] + "," + ctx.State["regex_matched"].([]string)[2]) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("设置成功")) - }) -} diff --git a/plugin/aireply/model.go b/plugin/aireply/model.go deleted file mode 100644 index 1e79d5e3ff..0000000000 --- a/plugin/aireply/model.go +++ /dev/null @@ -1,29 +0,0 @@ -package aireply - -import ( - "os" - - "github.com/FloatTech/floatbox/binary" - "github.com/FloatTech/floatbox/file" -) - -type apikeystore struct { - k string - p string -} - -func newapikeystore(p string) (s apikeystore) { - s.p = p - if file.IsExist(p) { - data, err := os.ReadFile(p) - if err == nil { - s.k = binary.BytesToString(data) - } - } - return -} - -func (s *apikeystore) set(k string) error { - s.k = k - return os.WriteFile(s.p, binary.StringToBytes(k), 0644) -} diff --git a/plugin/animetrace/main.go b/plugin/animetrace/main.go new file mode 100644 index 0000000000..e738d18f23 --- /dev/null +++ b/plugin/animetrace/main.go @@ -0,0 +1,145 @@ +// Package animetrace AnimeTrace 动画/Galgame识别 +package animetrace + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "image" + "image/jpeg" + "mime/multipart" + "strings" + + "github.com/FloatTech/floatbox/web" + "github.com/FloatTech/imgfactory" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/disintegration/imaging" + "github.com/tidwall/gjson" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +func init() { + engine := control.Register("animetrace", &ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "AnimeTrace 动画/Galgame识别插件", + Help: "- Gal识图\n- 动漫识图\n- 动漫识图 2\n- 动漫识图 [模型名]\n- Gal识图 [模型名]", + }) + + engine.OnPrefix("gal识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) { + args := ctx.State["args"].(string) + var model string + switch strings.TrimSpace(args) { + case "": + model = "full_game_model_kira" // 默认使用的模型 + default: + model = args // 自定义设置模型 + } + processImageRecognition(ctx, model) + }) + + engine.OnPrefix("动漫识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) { + args := ctx.State["args"].(string) + var model string + switch strings.TrimSpace(args) { + case "": + model = "anime_model_lovelive" + case "2": + model = "pre_stable" + default: + model = args + } + processImageRecognition(ctx, model) + }) +} + +// 处理图片识别 +func processImageRecognition(ctx *zero.Ctx, model string) { + urls := ctx.State["image_url"].([]string) + if len(urls) == 0 { + return + } + imageData, err := imgfactory.Load(urls[0]) + if err != nil { + ctx.Send(message.Text("下载图片失败: ", err)) + return + } + // ctx.Send(message.Text(model)) + respBody, err := createAndSendMultipartRequest("https://api.animetrace.com/v1/search", imageData, map[string]string{ + "is_multi": "0", + "model": model, + "ai_detect": "0", + }) + if err != nil { + ctx.Send(message.Text("识别请求失败: ", err)) + return + } + code := gjson.Get(string(respBody), "code").Int() + if code != 0 { + ctx.Send(message.Text("错误: ", gjson.Get(string(respBody), "zh_message").String())) + return + } + dataArray := gjson.Get(string(respBody), "data").Array() + if len(dataArray) == 0 { + ctx.Send(message.Text("未识别到任何角色")) + return + } + var sk message.Message + sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Text("共识别到 ", len(dataArray), " 个角色,可能是以下来源"))) + for _, value := range dataArray { + boxArray := value.Get("box").Array() + imgWidth, imgHeight := imageData.Bounds().Dx(), imageData.Bounds().Dy() // 你可以从 `imageData.Bounds()` 获取 + box := []int{ + int(boxArray[0].Float() * float64(imgWidth)), + int(boxArray[1].Float() * float64(imgHeight)), + int(boxArray[2].Float() * float64(imgWidth)), + int(boxArray[3].Float() * float64(imgHeight)), + } + croppedImg := imaging.Crop(imageData, image.Rect(box[0], box[1], box[2], box[3])) + var buf bytes.Buffer + if err := imaging.Encode(&buf, croppedImg, imaging.JPEG, imaging.JPEGQuality(80)); err != nil { + ctx.Send(message.Text("图片编码失败: ", err)) + continue + } + + base64Str := base64.StdEncoding.EncodeToString(buf.Bytes()) + var sb strings.Builder + value.Get("character").ForEach(func(_, character gjson.Result) bool { + sb.WriteString(fmt.Sprintf("《%s》的角色 %s\n", character.Get("work").String(), character.Get("character").String())) + return true + }) + sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Image("base64://"+base64Str), message.Text(sb.String()))) + } + ctx.SendGroupForwardMessage(ctx.Event.GroupID, sk) +} + +// 发送图片识别请求 +func createAndSendMultipartRequest(url string, img image.Image, formFields map[string]string) ([]byte, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 直接编码图片 + part, err := writer.CreateFormFile("file", "image.jpg") + if err != nil { + return nil, errors.New("创建文件字段失败: " + err.Error()) + } + if err := jpeg.Encode(part, img, &jpeg.Options{Quality: 80}); err != nil { + return nil, errors.New("图片编码失败: " + err.Error()) + } + + // 写入其他字段 + for key, value := range formFields { + if err := writer.WriteField(key, value); err != nil { + return nil, errors.New("写入表单字段失败 (" + key + "): " + err.Error()) + } + } + + if err := writer.Close(); err != nil { + return nil, errors.New("关闭 multipart writer 失败: " + err.Error()) + } + + return web.PostData(url, writer.FormDataContentType(), body) +} diff --git a/plugin/antiabuse/anti.go b/plugin/antiabuse/anti.go index b4ba459f68..dfe64fbdff 100644 --- a/plugin/antiabuse/anti.go +++ b/plugin/antiabuse/anti.go @@ -17,7 +17,12 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" ) -const bandur time.Duration = time.Minute * 10 +const ( + bandur time.Duration = time.Minute * 2 + add = "添加违禁词" + del = "删除违禁词" + list = "查看违禁词" +) var ( managers *ctrl.Manager[*zero.Ctx] // managers lazy load @@ -41,7 +46,7 @@ func init() { engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "违禁词检测", - Help: "- /[添加|删除|查看]违禁词", + Help: "- [添加|删除|查看]违禁词", PrivateDataFolder: "anti_abuse", }) @@ -56,10 +61,14 @@ func init() { return true }) - engine.OnMessage(onceRule, zero.OnlyGroup, func(ctx *zero.Ctx) bool { - if !ctx.Event.IsToMe { - return true + notAntiabuse := func(ctx *zero.Ctx) bool { + if zero.PrefixRule(add)(ctx) || zero.PrefixRule(del)(ctx) || zero.PrefixRule(list)(ctx) { + return false } + return true + } + + engine.OnMessage(onceRule, notAntiabuse, zero.OnlyGroup, func(ctx *zero.Ctx) bool { uid := ctx.Event.UserID gid := ctx.Event.GroupID msg := strings.ReplaceAll(ctx.MessageString(), "\n", "") @@ -70,7 +79,8 @@ func init() { if err := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]).Manager.DoBlock(uid); err == nil { t := time.Now().Unix() cache.Set(uid, struct{}{}) - ctx.SetThisGroupBan(uid, int64(bandur.Minutes())) + ctx.SetThisGroupBan(uid, int64(bandur.Seconds())) + ctx.DeleteMessage(ctx.Event.MessageID) ctx.SendChain(message.Text("检测到违禁词, 已封禁/屏蔽", bandur)) db.Lock() defer db.Unlock() @@ -92,9 +102,9 @@ func init() { return true }) - engine.OnCommand("添加违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle( + engine.OnPrefix(add, zero.OnlyGroup, zero.AdminPermission, onceRule).SetBlock(true).Handle( func(ctx *zero.Ctx) { - args := ctx.State["args"].(string) + args := strings.TrimSpace(ctx.State["args"].(string)) if err := db.insertWord(ctx.Event.GroupID, args); err != nil { ctx.SendChain(message.Text("ERROR: ", err)) } else { @@ -102,9 +112,9 @@ func init() { } }) - engine.OnCommand("删除违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle( + engine.OnPrefix(del, zero.OnlyGroup, zero.AdminPermission, onceRule).SetBlock(true).Handle( func(ctx *zero.Ctx) { - args := ctx.State["args"].(string) + args := strings.TrimSpace(ctx.State["args"].(string)) if err := db.deleteWord(ctx.Event.GroupID, args); err != nil { ctx.SendChain(message.Text("ERROR: ", err)) } else { @@ -112,7 +122,7 @@ func init() { } }) - engine.OnCommand("查看违禁词", zero.OnlyGroup, onceRule).Handle( + engine.OnPrefix(list, zero.OnlyGroup, onceRule).SetBlock(true).Handle( func(ctx *zero.Ctx) { b, err := text.RenderToBase64(db.listWords(ctx.Event.GroupID), text.FontFile, 400, 20) if err != nil { diff --git a/plugin/antiabuse/db.go b/plugin/antiabuse/db.go index 8f48577884..d18201ce82 100644 --- a/plugin/antiabuse/db.go +++ b/plugin/antiabuse/db.go @@ -30,7 +30,7 @@ var ( ) func newantidb(path string) (*antidb, error) { - db := &antidb{Sqlite: sqlite.Sqlite{DBPath: path}} + db := &antidb{Sqlite: sqlite.New(path)} err := db.Open(bandur) if err != nil { return nil, err @@ -46,7 +46,7 @@ func newantidb(path string) (*antidb, error) { cache.Touch(nilbt.ID, -time.Since(t)) return nil }) - _ = db.Del("__bantime__", "WHERE time<="+strconv.FormatInt(time.Now().Add(time.Minute-bandur).Unix(), 10)) + _ = db.Del("__bantime__", "WHERE time <= ?", strconv.FormatInt(time.Now().Add(time.Minute-bandur).Unix(), 10)) return db, nil } diff --git a/plugin/atri/atri.go b/plugin/atri/atri.go index 9367315889..571c4a8f09 100644 --- a/plugin/atri/atri.go +++ b/plugin/atri/atri.go @@ -19,7 +19,7 @@ import ( type datagetter func(string, bool) ([]byte, error) -func (dgtr datagetter) randImage(file ...string) message.MessageSegment { +func (dgtr datagetter) randImage(file ...string) message.Segment { data, err := dgtr(file[rand.Intn(len(file))], true) if err != nil { return message.Text("ERROR: ", err) @@ -27,7 +27,7 @@ func (dgtr datagetter) randImage(file ...string) message.MessageSegment { return message.ImageBytes(data) } -func (dgtr datagetter) randRecord(file ...string) message.MessageSegment { +func (dgtr datagetter) randRecord(file ...string) message.Segment { data, err := dgtr(file[rand.Intn(len(file))], true) if err != nil { return message.Text("ERROR: ", err) @@ -35,7 +35,7 @@ func (dgtr datagetter) randRecord(file ...string) message.MessageSegment { return message.Record("base64://" + base64.StdEncoding.EncodeToString(data)) } -func randText(text ...string) message.MessageSegment { +func randText(text ...string) message.Segment { return message.Text(text[rand.Intn(len(text))]) } diff --git a/plugin/baiduaudit/model.go b/plugin/baiduaudit/model.go index e5dd8caf77..15b6ff05c9 100644 --- a/plugin/baiduaudit/model.go +++ b/plugin/baiduaudit/model.go @@ -224,7 +224,7 @@ func (g *group) reply(bdres *baiduRes) message.Message { g.mu.Lock() defer g.mu.Unlock() // 建立消息段 - msgs := make([]message.MessageSegment, 0, 8) + msgs := make([]message.Segment, 0, 8) // 生成简略审核结果回复 msgs = append(msgs, message.Text(bdres.Conclusion, "\n")) // 查看是否开启详细审核内容提示, 并确定审核内容值为疑似, 或者不合规 diff --git a/plugin/bilibili/bilibili_parse.go b/plugin/bilibili/bilibili_parse.go index ad628edafd..81dd03bdaa 100644 --- a/plugin/bilibili/bilibili_parse.go +++ b/plugin/bilibili/bilibili_parse.go @@ -2,25 +2,33 @@ package bilibili import ( + "bytes" "encoding/json" "fmt" "net/http" + "os" + "os/exec" "regexp" "strings" "time" bz "github.com/FloatTech/AnimeAPI/bilibili" + "github.com/FloatTech/floatbox/file" "github.com/FloatTech/floatbox/web" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" + "github.com/pkg/errors" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" ) const ( - enableHex = 0x10 - unableHex = 0x7fffffff_fffffffd + enableVideoSummary = int64(0x10) + disableVideoSummary = ^enableVideoSummary + enableVideoDownload = int64(0x20) + disableVideoDownload = ^enableVideoDownload + bilibiliparseReferer = "https://www.bilibili.com" ) var ( @@ -33,6 +41,7 @@ var ( searchDynamicRe = regexp.MustCompile(searchDynamic) searchArticleRe = regexp.MustCompile(searchArticle) searchLiveRoomRe = regexp.MustCompile(searchLiveRoom) + cachePath string ) // 插件主体 @@ -42,6 +51,9 @@ func init() { Brief: "b站链接解析", Help: "例:- t.bilibili.com/642277677329285174\n- bilibili.com/read/cv17134450\n- bilibili.com/video/BV13B4y1x7pS\n- live.bilibili.com/22603245 ", }) + cachePath = en.DataFolder() + "cache/" + _ = os.RemoveAll(cachePath) + _ = os.MkdirAll(cachePath, 0755) en.OnRegex(`((b23|acg).tv|bili2233.cn)\\?/[0-9a-zA-Z]+`).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { u := ctx.State["regex_matched"].([]string)[0] @@ -82,9 +94,9 @@ func init() { data := c.GetData(ctx.Event.GroupID) switch option { case "开启", "打开", "启用": - data |= enableHex + data |= enableVideoSummary case "关闭", "关掉", "禁用": - data &= unableHex + data &= disableVideoSummary default: return } @@ -95,6 +107,35 @@ func init() { } ctx.SendChain(message.Text("已", option, "视频总结")) }) + en.OnRegex(`^(开启|打开|启用|关闭|关掉|禁用)视频上传$`, zero.AdminPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + if gid <= 0 { + // 个人用户设为负数 + gid = -ctx.Event.UserID + } + option := ctx.State["regex_matched"].([]string)[1] + c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) + if !ok { + ctx.SendChain(message.Text("找不到服务!")) + return + } + data := c.GetData(ctx.Event.GroupID) + switch option { + case "开启", "打开", "启用": + data |= enableVideoDownload + case "关闭", "关掉", "禁用": + data &= disableVideoDownload + default: + return + } + err := c.SetData(gid, data) + if err != nil { + ctx.SendChain(message.Text("出错啦: ", err)) + return + } + ctx.SendChain(message.Text("已", option, "视频上传")) + }) en.OnRegex(searchVideo).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleVideo) en.OnRegex(searchDynamic).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleDynamic) en.OnRegex(searchArticle).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleArticle) @@ -117,7 +158,7 @@ func handleVideo(ctx *zero.Ctx) { return } c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]) - if ok && c.GetData(ctx.Event.GroupID)&enableHex == enableHex { + if ok && c.GetData(ctx.Event.GroupID)&enableVideoSummary == enableVideoSummary { summaryMsg, err := getVideoSummary(cfg, card) if err != nil { msg = append(msg, message.Text("ERROR: ", err)) @@ -126,6 +167,14 @@ func handleVideo(ctx *zero.Ctx) { } } ctx.SendChain(msg...) + if ok && c.GetData(ctx.Event.GroupID)&enableVideoDownload == enableVideoDownload { + downLoadMsg, err := getVideoDownload(cfg, card, cachePath) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(downLoadMsg...) + } } func handleDynamic(ctx *zero.Ctx) { @@ -147,7 +196,12 @@ func handleArticle(ctx *zero.Ctx) { } func handleLive(ctx *zero.Ctx) { - card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1]) + cookie, err := cfg.Load() + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1], cookie) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return @@ -156,7 +210,7 @@ func handleLive(ctx *zero.Ctx) { } // getVideoSummary AI视频总结 -func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.MessageSegment, err error) { +func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.Segment, err error) { var ( data []byte videoSummary bz.VideoSummary @@ -177,7 +231,7 @@ func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.Me return } err = json.Unmarshal(data, &videoSummary) - msg = make([]message.MessageSegment, 0, 16) + msg = make([]message.Segment, 0, 16) msg = append(msg, message.Text("已为你生成视频总结\n\n")) msg = append(msg, message.Text(videoSummary.Data.ModelResult.Summary, "\n\n")) for _, v := range videoSummary.Data.ModelResult.Outline { @@ -189,3 +243,47 @@ func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.Me } return } + +func getVideoDownload(cookiecfg *bz.CookieConfig, card bz.Card, cachePath string) (msg []message.Segment, err error) { + var ( + data []byte + videoDownload bz.VideoDownload + stderr bytes.Buffer + ) + today := time.Now().Format("20060102") + videoFile := fmt.Sprintf("%s%s%s.mp4", cachePath, card.BvID, today) + if file.IsExist(videoFile) { + msg = append(msg, message.Video("file:///"+file.BOTPATH+"/"+videoFile)) + return + } + data, err = web.RequestDataWithHeaders(web.NewDefaultClient(), bz.SignURL(fmt.Sprintf(bz.VideoDownloadURL, card.BvID, card.CID)), "GET", func(req *http.Request) error { + if cookiecfg != nil { + cookie := "" + cookie, err = cookiecfg.Load() + if err != nil { + return err + } + req.Header.Add("cookie", cookie) + } + req.Header.Set("User-Agent", ua) + return nil + }, nil) + if err != nil { + return + } + err = json.Unmarshal(data, &videoDownload) + if err != nil { + return + } + headers := fmt.Sprintf("User-Agent: %s\nReferer: %s", ua, bilibiliparseReferer) + // 限制最多下载8分钟视频 + cmd := exec.Command("ffmpeg", "-ss", "0", "-t", "480", "-headers", headers, "-i", videoDownload.Data.Durl[0].URL, "-c", "copy", videoFile) + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + err = errors.Errorf("未配置ffmpeg,%v", stderr) + return + } + msg = append(msg, message.Video("file:///"+file.BOTPATH+"/"+videoFile)) + return +} diff --git a/plugin/bilibili/bilibilipush.go b/plugin/bilibili/bilibilipush.go index 8a944305fb..52310db211 100644 --- a/plugin/bilibili/bilibilipush.go +++ b/plugin/bilibili/bilibilipush.go @@ -388,7 +388,7 @@ func sendLive(ctx *zero.Ctx) error { if lCover == "" { lCover = value.Get("keyframe").String() } - var msg []message.MessageSegment + var msg []message.Segment msg = append(msg, message.Text(lName+" 正在直播:\n")) msg = append(msg, message.Text(lTitle)) msg = append(msg, message.Image(lCover)) @@ -399,7 +399,7 @@ func sendLive(ctx *zero.Ctx) error { switch { case gid > 0: if res := bdb.getAtAll(gid); res == 1 { - msg = append([]message.MessageSegment{message.AtAll()}, msg...) + msg = append([]message.Segment{message.AtAll()}, msg...) } ctx.SendGroupMessage(gid, msg) case gid < 0: diff --git a/plugin/bilibili/card2msg.go b/plugin/bilibili/card2msg.go index 5d3b2f3fe0..d2dcc5fe30 100644 --- a/plugin/bilibili/card2msg.go +++ b/plugin/bilibili/card2msg.go @@ -2,10 +2,12 @@ package bilibili import ( "encoding/json" + "fmt" "time" bz "github.com/FloatTech/AnimeAPI/bilibili" "github.com/FloatTech/floatbox/binary" + "github.com/FloatTech/floatbox/web" "github.com/wdvxdr1123/ZeroBot/message" ) @@ -25,13 +27,13 @@ var ( ) // dynamicCard2msg 处理DynCard -func dynamicCard2msg(dynamicCard *bz.DynamicCard) (msg []message.MessageSegment, err error) { +func dynamicCard2msg(dynamicCard *bz.DynamicCard) (msg []message.Segment, err error) { var ( card bz.Card vote bz.Vote cType int ) - msg = make([]message.MessageSegment, 0, 16) + msg = make([]message.Segment, 0, 16) // 初始化结构体 err = json.Unmarshal(binary.StringToBytes(dynamicCard.Card), &card) if err != nil { @@ -50,7 +52,7 @@ func dynamicCard2msg(dynamicCard *bz.DynamicCard) (msg []message.MessageSegment, msg = append(msg, message.Text(card.User.Uname, msgType[cType], "\n", card.Item.Content, "\n", "转发的内容: \n")) - var originMsg []message.MessageSegment + var originMsg []message.Segment var co bz.Card co, err = bz.LoadCardDetail(card.Origin) if err != nil { @@ -146,18 +148,18 @@ func dynamicCard2msg(dynamicCard *bz.DynamicCard) (msg []message.MessageSegment, } // card2msg cType=1, 2, 4, 8, 16, 64, 256, 2048, 4200, 4308时,处理Card字符串,cType为card类型 -func card2msg(dynamicCard *bz.DynamicCard, card *bz.Card, cType int) (msg []message.MessageSegment, err error) { +func card2msg(dynamicCard *bz.DynamicCard, card *bz.Card, cType int) (msg []message.Segment, err error) { var ( vote bz.Vote ) - msg = make([]message.MessageSegment, 0, 16) + msg = make([]message.Segment, 0, 16) // 生成消息 switch cType { case 1: msg = append(msg, message.Text(card.User.Uname, msgType[cType], "\n", card.Item.Content, "\n", "转发的内容: \n")) - var originMsg []message.MessageSegment + var originMsg []message.Segment var co bz.Card co, err = bz.LoadCardDetail(card.Origin) if err != nil { @@ -253,7 +255,7 @@ func card2msg(dynamicCard *bz.DynamicCard, card *bz.Card, cType int) (msg []mess } // dynamicDetail 用动态id查动态信息 -func dynamicDetail(cookiecfg *bz.CookieConfig, dynamicIDStr string) (msg []message.MessageSegment, err error) { +func dynamicDetail(cookiecfg *bz.CookieConfig, dynamicIDStr string) (msg []message.Segment, err error) { dyc, err := bz.GetDynamicDetail(cookiecfg, dynamicIDStr) if err != nil { return @@ -262,8 +264,8 @@ func dynamicDetail(cookiecfg *bz.CookieConfig, dynamicIDStr string) (msg []messa } // articleCard2msg 专栏转消息 -func articleCard2msg(card bz.Card, defaultID string) (msg []message.MessageSegment) { - msg = make([]message.MessageSegment, 0, 16) +func articleCard2msg(card bz.Card, defaultID string) (msg []message.Segment) { + msg = make([]message.Segment, 0, 16) for i := 0; i < len(card.OriginImageUrls); i++ { msg = append(msg, message.Image(card.OriginImageUrls[i])) } @@ -274,8 +276,8 @@ func articleCard2msg(card bz.Card, defaultID string) (msg []message.MessageSegme } // liveCard2msg 直播卡片转消息 -func liveCard2msg(card bz.RoomCard) (msg []message.MessageSegment) { - msg = make([]message.MessageSegment, 0, 16) +func liveCard2msg(card bz.RoomCard) (msg []message.Segment) { + msg = make([]message.Segment, 0, 16) msg = append(msg, message.Image(card.RoomInfo.Keyframe)) msg = append(msg, message.Text("\n", card.RoomInfo.Title, "\n", "主播: ", card.AnchorInfo.BaseInfo.Uname, "\n", @@ -302,25 +304,39 @@ func liveCard2msg(card bz.RoomCard) (msg []message.MessageSegment) { } // videoCard2msg 视频卡片转消息 -func videoCard2msg(card bz.Card) (msg []message.MessageSegment, err error) { - var mCard bz.MemberCard - msg = make([]message.MessageSegment, 0, 16) +func videoCard2msg(card bz.Card) (msg []message.Segment, err error) { + var ( + mCard bz.MemberCard + onlineTotal bz.OnlineTotal + ) + msg = make([]message.Segment, 0, 16) mCard, err = bz.GetMemberCard(card.Owner.Mid) - if err != nil { - return - } msg = append(msg, message.Text("标题: ", card.Title, "\n")) if card.Rights.IsCooperation == 1 { for i := 0; i < len(card.Staff); i++ { msg = append(msg, message.Text(card.Staff[i].Title, ": ", card.Staff[i].Name, " 粉丝: ", bz.HumanNum(card.Staff[i].Follower), "\n")) } } else { - msg = append(msg, message.Text("UP主: ", card.Owner.Name, " 粉丝: ", bz.HumanNum(mCard.Fans), "\n")) + if err != nil { + msg = append(msg, message.Text("UP主: ", card.Owner.Name, "\n")) + } else { + msg = append(msg, message.Text("UP主: ", card.Owner.Name, " 粉丝: ", bz.HumanNum(mCard.Fans), "\n")) + } } - msg = append(msg, message.Text("播放: ", bz.HumanNum(card.Stat.View), " 弹幕: ", bz.HumanNum(card.Stat.Danmaku))) msg = append(msg, message.Image(card.Pic)) - msg = append(msg, message.Text("\n点赞: ", bz.HumanNum(card.Stat.Like), " 投币: ", bz.HumanNum(card.Stat.Coin), "\n", - "收藏: ", bz.HumanNum(card.Stat.Favorite), " 分享: ", bz.HumanNum(card.Stat.Share), "\n", + data, err := web.GetData(fmt.Sprintf(bz.OnlineTotalURL, card.BvID, card.CID)) + if err != nil { + return + } + err = json.Unmarshal(data, &onlineTotal) + if err != nil { + return + } + msg = append(msg, message.Text("👀播放: ", bz.HumanNum(card.Stat.View), " 💬弹幕: ", bz.HumanNum(card.Stat.Danmaku), + "\n👍点赞: ", bz.HumanNum(card.Stat.Like), " 💰投币: ", bz.HumanNum(card.Stat.Coin), + "\n📁收藏: ", bz.HumanNum(card.Stat.Favorite), " 🔗分享: ", bz.HumanNum(card.Stat.Share), + "\n📝简介: ", card.Desc, + "\n🏄‍♂️ 总共 ", onlineTotal.Data.Total, " 人在观看,", onlineTotal.Data.Count, " 人在网页端观看\n", bz.VURL, card.BvID, "\n\n")) return } diff --git a/plugin/bilibili/card2msg_test.go b/plugin/bilibili/card2msg_test.go index 5c43c8491c..5d85d77507 100644 --- a/plugin/bilibili/card2msg_test.go +++ b/plugin/bilibili/card2msg_test.go @@ -47,7 +47,7 @@ func TestVideoInfo(t *testing.T) { } func TestLiveRoomInfo(t *testing.T) { - card, err := bz.GetLiveRoomInfo("83171") + card, err := bz.GetLiveRoomInfo("83171", "b_ut=7;buvid3=0;i-wanna-go-back=-1;innersign=0;") if err != nil { t.Fatal(err) } diff --git a/plugin/bookreview/book_review.go b/plugin/bookreview/book_review.go index 7336f68cbf..3778a903ea 100644 --- a/plugin/bookreview/book_review.go +++ b/plugin/bookreview/book_review.go @@ -10,6 +10,7 @@ import ( "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/img/text" @@ -24,7 +25,7 @@ func init() { }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "bookreview.db" + db = sql.New(engine.DataFolder() + "bookreview.db") // os.RemoveAll(dbpath) _, _ = engine.GetLazyData("bookreview.db", true) err := db.Open(time.Hour) diff --git a/plugin/bookreview/model.go b/plugin/bookreview/model.go index 93d3993223..afd0217c74 100644 --- a/plugin/bookreview/model.go +++ b/plugin/bookreview/model.go @@ -7,11 +7,11 @@ type book struct { BookReview string `db:"bookreview"` } -var db = &sql.Sqlite{} +var db sql.Sqlite // 暂时随机选择一个书评 func getBookReviewByKeyword(keyword string) (b book) { - _ = db.Find("bookreview", &b, "where bookreview LIKE '%"+keyword+"%'") + _ = db.Find("bookreview", &b, "WHERE bookreview LIKE ?", "%"+keyword+"%") return } diff --git a/plugin/chatcount/chatcount.go b/plugin/chatcount/chatcount.go new file mode 100644 index 0000000000..caf88ebeed --- /dev/null +++ b/plugin/chatcount/chatcount.go @@ -0,0 +1,108 @@ +// Package chatcount 聊天时长统计 +package chatcount + +import ( + "fmt" + "image" + "net/http" + "strconv" + "sync" + + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/imgfactory" + "github.com/FloatTech/rendercard" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/FloatTech/zbputils/img/text" +) + +const ( + rankSize = 10 +) + +func init() { + engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "聊天时长统计", + Help: "- 查询水群@xxx\n- 查看水群排名", + PrivateDataFolder: "chatcount", + }) + go func() { + ctdb = initialize(engine.DataFolder() + "chatcount.db") + }() + engine.OnMessage(zero.OnlyGroup).SetBlock(false). + Handle(func(ctx *zero.Ctx) { + remindTime, remindFlag := ctdb.updateChatTime(ctx.Event.GroupID, ctx.Event.UserID) + if remindFlag { + ctx.SendChain(message.At(ctx.Event.UserID), message.Text(fmt.Sprintf("BOT提醒:你今天已经水群%d分钟了!", remindTime))) + } + }) + + engine.OnPrefix(`查询水群`, zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + param := ctx.State["args"].(string) + var uid int64 + if len(ctx.Event.Message) > 1 && ctx.Event.Message[1].Type == "at" { + uid, _ = strconv.ParseInt(ctx.Event.Message[1].Data["qq"], 10, 64) + } else if param == "" { + uid = ctx.Event.UserID + } + name := ctx.NickName() + todayTime, todayMessage, totalTime, totalMessage := ctdb.getChatTime(ctx.Event.GroupID, uid) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(fmt.Sprintf("%s今天水了%d分%d秒,发了%d条消息;总计水了%d分%d秒,发了%d条消息。", name, todayTime/60, todayTime%60, todayMessage, totalTime/60, totalTime%60, totalMessage))) + }) + engine.OnFullMatch("查看水群排名", zero.OnlyGroup).Limit(ctxext.LimitByGroup).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + chatTimeList := ctdb.getChatRank(ctx.Event.GroupID) + if len(chatTimeList) == 0 { + ctx.SendChain(message.Text("ERROR: 没有水群数据")) + return + } + rankinfo := make([]*rendercard.RankInfo, len(chatTimeList)) + + wg := &sync.WaitGroup{} + wg.Add(len(chatTimeList)) + for i := 0; i < len(chatTimeList) && i < rankSize; i++ { + go func(i int) { + defer wg.Done() + resp, err := http.Get("https://q4.qlogo.cn/g?b=qq&nk=" + strconv.FormatInt(chatTimeList[i].UserID, 10) + "&s=100") + if err != nil { + return + } + defer resp.Body.Close() + img, _, err := image.Decode(resp.Body) + if err != nil { + return + } + rankinfo[i] = &rendercard.RankInfo{ + TopLeftText: ctx.CardOrNickName(chatTimeList[i].UserID), + BottomLeftText: "消息数: " + strconv.FormatInt(chatTimeList[i].TodayMessage, 10) + " 条", + RightText: strconv.FormatInt(chatTimeList[i].TodayTime/60, 10) + "分" + strconv.FormatInt(chatTimeList[i].TodayTime%60, 10) + "秒", + Avatar: img, + } + }(i) + } + wg.Wait() + fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + img, err := rendercard.DrawRankingCard(fontbyte, "今日水群排行榜", rankinfo) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + sendimg, err := imgfactory.ToBytes(img) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + if id := ctx.SendChain(message.ImageBytes(sendimg)); id.ID() == 0 { + ctx.SendChain(message.Text("ERROR: 可能被风控了")) + } + }) +} diff --git a/plugin/chatcount/model.go b/plugin/chatcount/model.go new file mode 100644 index 0000000000..8fba57d17c --- /dev/null +++ b/plugin/chatcount/model.go @@ -0,0 +1,225 @@ +package chatcount + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/RomiChan/syncx" + + "github.com/jinzhu/gorm" +) + +const ( + chatInterval = 300 +) + +var ( + // ctdb 聊天时长数据库全局变量 + ctdb *chattimedb + // l 水群提醒时间提醒段,单位分钟 + l = newLeveler(60, 120, 180, 240, 300) +) + +// chattimedb 聊天时长数据库结构体 +type chattimedb struct { + // ctdb.userTimestampMap 每个人发言的时间戳 key=groupID_userID + userTimestampMap syncx.Map[string, int64] + // ctdb.userTodayTimeMap 每个人今日水群时间 key=groupID_userID + userTodayTimeMap syncx.Map[string, int64] + // ctdb.userTodayMessageMap 每个人今日水群次数 key=groupID_userID + userTodayMessageMap syncx.Map[string, int64] + // db 数据库 + db *gorm.DB + // chatmu 读写添加锁 + chatmu sync.Mutex +} + +// initialize 初始化 +func initialize(dbpath string) *chattimedb { + var err error + if _, err = os.Stat(dbpath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbpath) + if err != nil { + return nil + } + defer f.Close() + } + gdb, err := gorm.Open("sqlite3", dbpath) + if err != nil { + panic(err) + } + gdb.AutoMigrate(&chatTime{}) + return &chattimedb{ + db: gdb, + } +} + +// Close 关闭 +func (ctdb *chattimedb) Close() error { + db := ctdb.db + return db.Close() +} + +// chatTime 聊天时长,时间的单位都是秒 +type chatTime struct { + ID uint `gorm:"primary_key"` + GroupID int64 `gorm:"column:group_id"` + UserID int64 `gorm:"column:user_id"` + TodayTime int64 `gorm:"-"` + TodayMessage int64 `gorm:"-"` + TotalTime int64 `gorm:"column:total_time;default:0"` + TotalMessage int64 `gorm:"column:total_message;default:0"` +} + +// TableName 表名 +func (chatTime) TableName() string { + return "chat_time" +} + +// updateChatTime 更新发言时间,todayTime的单位是分钟 +func (ctdb *chattimedb) updateChatTime(gid, uid int64) (remindTime int64, remindFlag bool) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + db := ctdb.db + now := time.Now() + keyword := fmt.Sprintf("%v_%v", gid, uid) + ts, ok := ctdb.userTimestampMap.Load(keyword) + if !ok { + ctdb.userTimestampMap.Store(keyword, now.Unix()) + ctdb.userTodayMessageMap.Store(keyword, 1) + return + } + lastTime := time.Unix(ts, 0) + todayTime, _ := ctdb.userTodayTimeMap.Load(keyword) + totayMessage, _ := ctdb.userTodayMessageMap.Load(keyword) + // 这个消息数是必须统计的 + ctdb.userTodayMessageMap.Store(keyword, totayMessage+1) + st := chatTime{ + GroupID: gid, + UserID: uid, + TotalTime: todayTime, + TotalMessage: totayMessage, + } + + // 如果不是同一天,把TotalTime,TotalMessage重置 + if lastTime.YearDay() != now.YearDay() { + if err := db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).First(&st).Error; err != nil { + if gorm.IsRecordNotFoundError(err) { + db.Model(&st).Create(&st) + } + } else { + db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).Update( + map[string]any{ + "total_time": st.TotalTime + todayTime, + "total_message": st.TotalMessage + totayMessage, + }) + } + ctdb.userTimestampMap.Store(keyword, now.Unix()) + ctdb.userTodayTimeMap.Delete(keyword) + ctdb.userTodayMessageMap.Delete(keyword) + return + } + + userChatTime := int64(now.Sub(lastTime).Seconds()) + // 当聊天时间在一定范围内的话,则计入时长 + if userChatTime < chatInterval { + ctdb.userTodayTimeMap.Store(keyword, todayTime+userChatTime) + remindTime = (todayTime + userChatTime) / 60 + remindFlag = l.level(int((todayTime+userChatTime)/60)) > l.level(int(todayTime/60)) + } + ctdb.userTimestampMap.Store(keyword, now.Unix()) + return +} + +// getChatTime 获得用户聊天时长和消息次数,todayTime,totalTime的单位是秒,todayMessage,totalMessage单位是条数 +func (ctdb *chattimedb) getChatTime(gid, uid int64) (todayTime, todayMessage, totalTime, totalMessage int64) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + db := ctdb.db + st := chatTime{} + db.Model(&st).Where("group_id = ? and user_id = ?", gid, uid).First(&st) + keyword := fmt.Sprintf("%v_%v", gid, uid) + todayTime, _ = ctdb.userTodayTimeMap.Load(keyword) + todayMessage, _ = ctdb.userTodayMessageMap.Load(keyword) + totalTime = st.TotalTime + totalMessage = st.TotalMessage + return +} + +// getChatRank 获得水群排名,时间单位为秒 +func (ctdb *chattimedb) getChatRank(gid int64) (chatTimeList []chatTime) { + ctdb.chatmu.Lock() + defer ctdb.chatmu.Unlock() + chatTimeList = make([]chatTime, 0, 100) + keyList := make([]string, 0, 100) + ctdb.userTimestampMap.Range(func(key string, value int64) bool { + t := time.Unix(value, 0) + if strings.Contains(key, strconv.FormatInt(gid, 10)) && t.YearDay() == time.Now().YearDay() { + keyList = append(keyList, key) + } + return true + }) + for _, v := range keyList { + _, a, _ := strings.Cut(v, "_") + uid, _ := strconv.ParseInt(a, 10, 64) + todayTime, _ := ctdb.userTodayTimeMap.Load(v) + todayMessage, _ := ctdb.userTodayMessageMap.Load(v) + chatTimeList = append(chatTimeList, chatTime{ + GroupID: gid, + UserID: uid, + TodayTime: todayTime, + TodayMessage: todayMessage, + }) + } + sort.Sort(sortChatTime(chatTimeList)) + return +} + +// leveler 结构体,包含一个 levelArray 字段 +type leveler struct { + levelArray []int +} + +// newLeveler 构造函数,用于创建 Leveler 实例 +func newLeveler(levels ...int) *leveler { + return &leveler{ + levelArray: levels, + } +} + +// level 方法,封装了 getLevel 函数的逻辑 +func (l *leveler) level(t int) int { + for i := len(l.levelArray) - 1; i >= 0; i-- { + if t >= l.levelArray[i] { + return i + 1 + } + } + return 0 +} + +// sortChatTime chatTime排序数组 +type sortChatTime []chatTime + +// Len 实现 sort.Interface +func (a sortChatTime) Len() int { + return len(a) +} + +// Less 实现 sort.Interface,按 TodayTime 降序,TodayMessage 降序 +func (a sortChatTime) Less(i, j int) bool { + if a[i].TodayTime == a[j].TodayTime { + return a[i].TodayMessage > a[j].TodayMessage + } + return a[i].TodayTime > a[j].TodayTime +} + +// Swap 实现 sort.Interface +func (a sortChatTime) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} diff --git a/plugin/chess/core.go b/plugin/chess/core.go index d3e0926b04..44ecfd0a03 100644 --- a/plugin/chess/core.go +++ b/plugin/chess/core.go @@ -235,7 +235,7 @@ func play(groupCode, senderUin int64, moveStr string) (msg message.Message, err chessRoomMap.Store(groupCode, room) } // 生成棋盘图片 - var boardImgEle message.MessageSegment + var boardImgEle message.Segment if !room.isBlindfold { boardImgEle, err = getBoardElement(groupCode) if err != nil { @@ -400,7 +400,7 @@ func createGame(isBlindfold bool, groupCode, senderUin int64, senderName string) room.blackPlayer = senderUin room.blackName = senderName chessRoomMap.Store(groupCode, room) - var boardImgEle message.MessageSegment + var boardImgEle message.Segment if !room.isBlindfold { boardImgEle, err = getBoardElement(groupCode) if err != nil { @@ -442,7 +442,7 @@ func abortGame(room chessRoom, groupCode int64, hint string) (message.Message, e } // getBoardElement 获取棋盘图片的消息内容 -func getBoardElement(groupCode int64) (imgMsg message.MessageSegment, err error) { +func getBoardElement(groupCode int64) (imgMsg message.Segment, err error) { fontdata, err := file.GetLazyData(text.GNUUnifontFontFile, control.Md5File, true) if err != nil { return diff --git a/plugin/chouxianghua/chouxianghua.go b/plugin/chouxianghua/chouxianghua.go index c7e0710542..8cffb4032e 100644 --- a/plugin/chouxianghua/chouxianghua.go +++ b/plugin/chouxianghua/chouxianghua.go @@ -9,6 +9,7 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" ) @@ -23,7 +24,7 @@ func init() { en.OnRegex("^抽象翻译((\\s|[\\r\\n]|[\\p{Han}\\p{P}A-Za-z0-9])+)$", fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = en.DataFolder() + "cxh.db" + db = sql.New(en.DataFolder() + "cxh.db") // os.RemoveAll(dbpath) _, _ = en.GetLazyData("cxh.db", true) err := db.Open(time.Hour) diff --git a/plugin/chouxianghua/model.go b/plugin/chouxianghua/model.go index e7796a0dfb..9329fb433e 100644 --- a/plugin/chouxianghua/model.go +++ b/plugin/chouxianghua/model.go @@ -11,11 +11,11 @@ type emoji struct { Emoji string `db:"emoji"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func getPinyinByWord(word string) string { var p pinyin - _ = db.Find("pinyin", &p, "where word = '"+word+"'") + _ = db.Find("pinyin", &p, "WHERE word = ?", word) return p.Pronun } @@ -25,6 +25,6 @@ func getPronunByDWord(w0, w1 rune) string { func getEmojiByPronun(pronun string) string { var e emoji - _ = db.Find("emoji", &e, "where pronunciation = '"+pronun+"'") + _ = db.Find("emoji", &e, "WHERE pronunciation = ?", pronun) return e.Emoji } diff --git a/plugin/cpstory/cpstory.go b/plugin/cpstory/cpstory.go index 1d38ad9706..3bb2a8e5b3 100644 --- a/plugin/cpstory/cpstory.go +++ b/plugin/cpstory/cpstory.go @@ -11,6 +11,7 @@ import ( fcext "github.com/FloatTech/floatbox/ctxext" "github.com/FloatTech/floatbox/math" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" ) @@ -24,7 +25,7 @@ func init() { }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "cp.db" + db = sql.New(engine.DataFolder() + "cp.db") // os.RemoveAll(dbpath) _, _ = engine.GetLazyData("cp.db", true) err := db.Open(time.Hour) diff --git a/plugin/cpstory/model.go b/plugin/cpstory/model.go index 0eb11de8e0..31921bc985 100644 --- a/plugin/cpstory/model.go +++ b/plugin/cpstory/model.go @@ -9,7 +9,7 @@ type cpstory struct { Story string `db:"story"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func getRandomCpStory() (cs cpstory) { _ = db.Pick("cp_story", &cs) diff --git a/plugin/crypter/fumo.go b/plugin/crypter/fumo.go new file mode 100644 index 0000000000..a96a1dfbf8 --- /dev/null +++ b/plugin/crypter/fumo.go @@ -0,0 +1,95 @@ +// Package crypter Fumo语 +package crypter + +import ( + "encoding/base64" + "fmt" + "regexp" + "strings" +) + +// Base64字符表 +const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +// Fumo语字符表 - 使用各种fumo变体来表示base64字符 +var fumoChars = []string{ + "fumo-", "Fumo-", "fUmo-", "fuMo-", "fumO-", "FUmo-", "FuMo-", "FumO-", + "fUMo-", "fUmO-", "fuMO-", "FUMo-", "FUmO-", "fUMO-", "FUMO-", "fumo.", + "Fumo.", "fUmo.", "fuMo.", "fumO.", "FUmo.", "FuMo.", "FumO.", "fUMo.", + "fUmO.", "fuMO.", "FUMo.", "FUmO.", "fUMO.", "FUMO.", "fumo,", "Fumo,", + "fUmo,", "fuMo,", "fumO,", "FUmo,", "FuMo,", "FumO,", "fUMo,", "fUmO,", + "fuMO,", "FUMo,", "FuMO,", "fUMO,", "FUMO,", "fumo+", "Fumo+", "fUmo+", + "fuMo+", "fumO+", "FUmo+", "FuMo+", "FumO+", "fUMo+", "fUmO+", "fuMO+", + "FUMo+", "FUmO+", "fUMO+", "FUMO+", "fumo|", "Fumo|", "fUmo|", "fuMo|", + "fumO|", "FUmo|", "FuMo|", "FumO|", "fUMo|", "fUmO|", "fuMO|", "fumo/", + "Fumo/", "fUmo/", +} + +// Base64 2 Fumo +// 创建编码映射表 +var encodeMap = make(map[byte]string) + +// 创建解码映射表 +var decodeMap = make(map[string]byte) + +func init() { + for i := 0; i < 64 && i < len(fumoChars); i++ { + base64Char := base64Chars[i] + fumoChar := fumoChars[i] + + encodeMap[base64Char] = fumoChar + decodeMap[fumoChar] = base64Char + } +} + +// 加密 +func encryptFumo(text string) string { + if text == "" { + return "请输入要加密的文本" + } + textBytes := []byte(text) + base64String := base64.StdEncoding.EncodeToString(textBytes) + base64Body := strings.TrimRight(base64String, "=") + paddingCount := len(base64String) - len(base64Body) + var fumoBody strings.Builder + for _, char := range base64Body { + if fumoChar, exists := encodeMap[byte(char)]; exists { + fumoBody.WriteString(fumoChar) + } else { + return fmt.Sprintf("Fumo加密失败: 未知字符 %c", char) + } + } + result := fumoBody.String() + strings.Repeat("=", paddingCount) + + return result +} + +// 解密 +func decryptFumo(fumoText string) string { + if fumoText == "" { + return "请输入要解密的Fumo语密文" + } + fumoBody := strings.TrimRight(fumoText, "=") + paddingCount := len(fumoText) - len(fumoBody) + fumoPattern := regexp.MustCompile(`(\w+[-.,+|/])`) + fumoWords := fumoPattern.FindAllString(fumoBody, -1) + reconstructed := strings.Join(fumoWords, "") + if reconstructed != fumoBody { + return "Fumo解密失败: 包含无效的Fumo字符或格式错误" + } + var base64Body strings.Builder + for _, fumoWord := range fumoWords { + if base64Char, exists := decodeMap[fumoWord]; exists { + base64Body.WriteByte(base64Char) + } else { + return fmt.Sprintf("Fumo解密失败: 包含无效的Fumo字符 %s", fumoWord) + } + } + base64String := base64Body.String() + strings.Repeat("=", paddingCount) + decodedBytes, err := base64.StdEncoding.DecodeString(base64String) + if err != nil { + return fmt.Sprintf("Fumo解密失败: Base64解码错误 %v", err) + } + originalText := string(decodedBytes) + return originalText +} diff --git a/plugin/crypter/handlers.go b/plugin/crypter/handlers.go new file mode 100644 index 0000000000..1cec8a47ee --- /dev/null +++ b/plugin/crypter/handlers.go @@ -0,0 +1,42 @@ +// Package crypter 处理函数 +package crypter + +import ( + "github.com/FloatTech/AnimeAPI/airecord" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +// hou +func houEncryptHandler(ctx *zero.Ctx) { + text := ctx.State["regex_matched"].([]string)[1] + result := encodeHou(text) + logrus.Infoln("[crypter] 回复内容:", result) + recCfg := airecord.GetConfig() + record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, result) + if record != "" { + ctx.SendChain(message.Record(record)) + } else { + ctx.SendChain(message.Text(result)) + } +} + +func houDecryptHandler(ctx *zero.Ctx) { + text := ctx.State["regex_matched"].([]string)[1] + result := decodeHou(text) + ctx.SendChain(message.Text(result)) +} + +// fumo +func fumoEncryptHandler(ctx *zero.Ctx) { + text := ctx.State["regex_matched"].([]string)[1] + result := encryptFumo(text) + ctx.SendChain(message.Text(result)) +} + +func fumoDecryptHandler(ctx *zero.Ctx) { + text := ctx.State["regex_matched"].([]string)[1] + result := decryptFumo(text) + ctx.SendChain(message.Text(result)) +} diff --git a/plugin/crypter/hou.go b/plugin/crypter/hou.go new file mode 100644 index 0000000000..6302375bcd --- /dev/null +++ b/plugin/crypter/hou.go @@ -0,0 +1,88 @@ +// Package crypter 齁语加解密 +package crypter + +import ( + "strings" +) + +// 齁语密码表 +var houCodebook = []string{ + "齁", "哦", "噢", "喔", "咕", "咿", "嗯", "啊", + "~", "哈", "!", "唔", "哼", "❤", "呃", "呼", +} + +// 索引: 0 1 2 3 4 5 6 7 +// 8 9 10 11 12 13 14 15 + +// 创建映射表 +var houCodebookMap = make(map[string]int) + +// 初始化映射表 +func init() { + for idx, ch := range houCodebook { + houCodebookMap[ch] = idx + } +} + +func encodeHou(text string) string { + if text == "" { + return "请输入要加密的文本" + } + var encoded strings.Builder + textBytes := []byte(text) + for _, b := range textBytes { + high := (b >> 4) & 0x0F + low := b & 0x0F + encoded.WriteString(houCodebook[high]) + encoded.WriteString(houCodebook[low]) + } + + return encoded.String() +} + +func decodeHou(code string) string { + if code == "" { + return "请输入要解密的齁语密文" + } + + // 过滤出有效的齁语字符 + var validChars []string + for _, r := range code { + charStr := string(r) + if _, exists := houCodebookMap[charStr]; exists { + validChars = append(validChars, charStr) + } + } + + if len(validChars)%2 != 0 { + return "齁语密文长度错误,无法解密" + } + + // 解密过程 + var byteList []byte + for i := 0; i < len(validChars); i += 2 { + highIdx, highExists := houCodebookMap[validChars[i]] + lowIdx, lowExists := houCodebookMap[validChars[i+1]] + + if !highExists || !lowExists { + return "齁语密文包含无效字符" + } + + originalByte := byte((highIdx << 4) | lowIdx) + byteList = append(byteList, originalByte) + } + + result := string(byteList) + + if !isValidUTF8(result) { + return "齁语解密失败,结果不是有效的文本" + } + + return result +} + +// 检查字符串是否为有效的UTF-8编码 +func isValidUTF8(s string) bool { + // Go的string类型默认就是UTF-8,如果转换没有出错说明是有效的 + return len(s) > 0 || s == "" +} diff --git a/plugin/crypter/main.go b/plugin/crypter/main.go new file mode 100644 index 0000000000..4a94ffcaaa --- /dev/null +++ b/plugin/crypter/main.go @@ -0,0 +1,31 @@ +// Package crypter 奇怪语言加解密 +package crypter + +import ( + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + zero "github.com/wdvxdr1123/ZeroBot" +) + +func init() { + engine := control.Register("crypter", &ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "奇怪语言加解密", + Help: "多种语言加解密插件\n" + + "- 齁语加解密:\n" + + "- 齁语加密 [文本] 或 h加密 [文本]\n" + + "- 齁语解密 [密文] 或 h解密 [密文]\n\n" + + "- Fumo语加解密:\n" + + "- fumo加密 [文本]\n" + + "- fumo解密 [密文]\n\n", + PublicDataFolder: "Crypter", + }) + + // hou + engine.OnRegex(`^(?:齁语加密|h加密)\s*(.+)$`).SetBlock(true).Handle(houEncryptHandler) + engine.OnRegex(`^(?:齁语解密|h解密)\s*(.+)$`).SetBlock(true).Handle(houDecryptHandler) + + // Fumo + engine.OnRegex(`^fumo加密\s*(.+)$`).SetBlock(true).Handle(fumoEncryptHandler) + engine.OnRegex(`^fumo解密\s*(.+)$`).SetBlock(true).Handle(fumoDecryptHandler) +} diff --git a/plugin/curse/curse.go b/plugin/curse/curse.go index e0761d033f..31425408ff 100644 --- a/plugin/curse/curse.go +++ b/plugin/curse/curse.go @@ -10,6 +10,7 @@ import ( fcext "github.com/FloatTech/floatbox/ctxext" "github.com/FloatTech/floatbox/process" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" @@ -29,7 +30,7 @@ func init() { }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "curse.db" + db = sql.New(engine.DataFolder() + "curse.db") _, err := engine.GetLazyData("curse.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/curse/model.go b/plugin/curse/model.go index 8405aa5a6c..6cb0cea705 100644 --- a/plugin/curse/model.go +++ b/plugin/curse/model.go @@ -8,9 +8,9 @@ type curse struct { Level string `db:"level"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func getRandomCurseByLevel(level string) (c curse) { - _ = db.Find("curse", &c, "where level = '"+level+"' ORDER BY RANDOM() limit 1") + _ = db.Find("curse", &c, "WHERE level = ? ORDER BY RANDOM() limit 1", level) return } diff --git a/plugin/diana/data/text.go b/plugin/diana/data/text.go index 29b8f621c5..0b6f211dcf 100644 --- a/plugin/diana/data/text.go +++ b/plugin/diana/data/text.go @@ -23,7 +23,7 @@ type text struct { // LoadText 加载小作文 func LoadText(dbfile string) error { _, err := file.GetLazyData(dbfile, control.Md5File, false) - db.DBPath = dbfile + db = sql.New(dbfile) if err != nil { return err } @@ -63,7 +63,7 @@ func RandText() string { // HentaiText 发大病 func HentaiText() string { var t text - err := db.Find("text", &t, "where id = -3802576048116006195") + err := db.Find("text", &t, "WHERE id = -3802576048116006195") if err != nil { return err.Error() } diff --git a/plugin/dish/dish.go b/plugin/dish/dish.go index 6fd9985837..731aef6d4d 100644 --- a/plugin/dish/dish.go +++ b/plugin/dish/dish.go @@ -2,7 +2,6 @@ package dish import ( - "fmt" "strings" "time" @@ -25,7 +24,7 @@ type dish struct { } var ( - db = &sql.Sqlite{} + db sql.Sqlite initialized = false ) @@ -37,7 +36,7 @@ func init() { PublicDataFolder: "Dish", }) - db.DBPath = en.DataFolder() + "dishes.db" + db = sql.New(en.DataFolder() + "dishes.db") if _, err := en.GetLazyData("dishes.db", true); err != nil { logrus.Warnln("[dish]获取菜谱数据库文件失败") @@ -62,7 +61,7 @@ func init() { return } - name := ctx.NickName() + name := ctx.CardOrNickName(ctx.Event.UserID) dishName := ctx.State["args"].(string) if dishName == "" { @@ -77,17 +76,15 @@ func init() { } var d dish - if err := db.Find("dish", &d, fmt.Sprintf("WHERE name like '%%%s%%'", dishName)); err != nil { + if err := db.Find("dish", &d, "WHERE name LIKE ?", "%"+dishName+"%"); err != nil { ctx.SendChain(message.Text("客官,本店没有" + dishName)) return } - ctx.SendChain(message.Text(fmt.Sprintf( - "已为客官%s找到%s的做法辣!\n"+ - "原材料:%s\n"+ - "步骤:\n"+ - "%s", - name, d.Name, d.Materials, d.Steps), + ctx.SendChain(message.Text( + "已为客官", name, "找到", d.Name, "的做法辣!\n", + "原材料:", d.Materials, "\n", + "步骤:", d.Steps, )) }) @@ -105,12 +102,10 @@ func init() { return } - ctx.SendChain(message.Text(fmt.Sprintf( - "已为客官%s送上%s的做法:\n"+ - "原材料:%s\n"+ - "步骤:\n"+ - "%s", - name, d.Name, d.Materials, d.Steps), + ctx.SendChain(message.Text( + "已为客官", name, "送上", d.Name, "的做法:\n", + "原材料:", d.Materials, "\n", + "步骤:", d.Steps, )) }) } diff --git a/plugin/drawlots/main.go b/plugin/drawlots/main.go index 67edaea2a9..dde3530d58 100644 --- a/plugin/drawlots/main.go +++ b/plugin/drawlots/main.go @@ -252,17 +252,17 @@ func randGif(gifName string, uid int64) (image.Image, error) { // https://zhuanlan.zhihu.com/p/27718135 rect := image.Rect(0, 0, config.Width, config.Height) if rect.Min == rect.Max { - var max image.Point + var maxP image.Point for _, frame := range im.Image { maxF := frame.Bounds().Max - if max.X < maxF.X { - max.X = maxF.X + if maxP.X < maxF.X { + maxP.X = maxF.X } - if max.Y < maxF.Y { - max.Y = maxF.Y + if maxP.Y < maxF.Y { + maxP.Y = maxF.Y } } - rect.Max = max + rect.Max = maxP } img := image.NewRGBA(rect) b := fcext.RandSenderPerDayN(uid, len(im.Image)) + 1 diff --git a/plugin/driftbottle/main.go b/plugin/driftbottle/main.go index 8aba986d55..38429e55ea 100644 --- a/plugin/driftbottle/main.go +++ b/plugin/driftbottle/main.go @@ -27,7 +27,7 @@ type sea struct { Time string `db:"time"` // we need to know the current time,master> } -var seaSide = &sql.Sqlite{} +var seaSide sql.Sqlite var seaLocker sync.RWMutex // We need a container to inject what we need :( @@ -39,15 +39,15 @@ func init() { Help: "- @bot pick" + "- @bot throw xxx (xxx为投递内容)", PrivateDataFolder: "driftbottle", }) - seaSide.DBPath = en.DataFolder() + "sea.db" + seaSide = sql.New(en.DataFolder() + "sea.db") err := seaSide.Open(time.Hour) if err != nil { panic(err) } - _ = createChannel(seaSide) + _ = createChannel(&seaSide) en.OnFullMatch("pick", zero.OnlyToMe, zero.OnlyGroup).Limit(ctxext.LimitByGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { - be, err := fetchBottle(seaSide) + be, err := fetchBottle(&seaSide) if err != nil { ctx.SendChain(message.Text("ERR:", err)) } @@ -75,7 +75,7 @@ func init() { senderFormatTime, ctx.CardOrNickName(ctx.Event.UserID), rawMessageCallBack, - ).throw(seaSide) + ).throw(&seaSide) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return diff --git a/plugin/emojimix/mix.go b/plugin/emojimix/mix.go index b9aab0a627..69b5dde821 100644 --- a/plugin/emojimix/mix.go +++ b/plugin/emojimix/mix.go @@ -79,7 +79,7 @@ func match(ctx *zero.Ctx) bool { return false } -func face2emoji(face message.MessageSegment) rune { +func face2emoji(face message.Segment) rune { if face.Type == "text" { r := []rune(face.Data["text"]) if len(r) != 1 { diff --git a/plugin/emozi/main.go b/plugin/emozi/main.go new file mode 100644 index 0000000000..de61b41804 --- /dev/null +++ b/plugin/emozi/main.go @@ -0,0 +1,118 @@ +// Package emozi 颜文字抽象转写 +package emozi + +import ( + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/FloatTech/AnimeAPI/emozi" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +func init() { + en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "颜文字抽象转写", + Help: "- 抽象转写[文段]\n- 抽象还原[文段]\n- 抽象登录[用户名]", + PrivateDataFolder: "emozi", + }) + usr := emozi.Anonymous() + data, err := os.ReadFile(en.DataFolder() + "user.txt") + refresh := func() { + go func() { + t := time.NewTicker(time.Hour) + defer t.Stop() + for range t.C { + if !usr.IsValid() { + time.Sleep(time.Second * 2) + err := usr.Login() + if err != nil { + logrus.Warnln("[emozi] 重新登录账号失败:", err) + } + } + } + }() + } + refresher := sync.Once{} + if err == nil { + arr := strings.Split(string(data), "\n") + if len(arr) >= 2 { + usr = emozi.NewUser(arr[0], arr[1]) + err = usr.Login() + if err != nil { + logrus.Infoln("[emozi]", "以", arr[0], "身份登录失败:", err) + usr = emozi.Anonymous() + } else { + logrus.Infoln("[emozi]", "以", arr[0], "身份登录成功") + refresher.Do(refresh) + } + } + } + + en.OnPrefix("抽象转写").Limit(ctxext.LimitByUser).SetBlock(true).Handle(func(ctx *zero.Ctx) { + txt := strings.TrimSpace(ctx.State["args"].(string)) + out, chs, err := usr.Marshal(false, txt) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + if len(chs) == 0 { + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text(out))) + return + } + for i, c := range chs { + ch := ctx.Get("请选择第" + strconv.Itoa(i) + "个多音字(1~" + strconv.Itoa(c) + ")") + n, err := strconv.Atoi(ch) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + if n < 1 || n > c { + ctx.SendChain(message.Text("ERROR: 输入越界")) + return + } + chs[i] = n - 1 + } + out, _, err = usr.Marshal(false, txt, chs...) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text(out))) + }) + en.OnPrefix("抽象还原").Limit(ctxext.LimitByUser).SetBlock(true).Handle(func(ctx *zero.Ctx) { + txt := strings.TrimSpace(ctx.State["args"].(string)) + out, err := usr.Unmarshal(false, txt) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text(out))) + }) + en.OnPrefix("抽象登录", zero.OnlyPrivate).Limit(ctxext.LimitByUser).SetBlock(true).Handle(func(ctx *zero.Ctx) { + name := strings.TrimSpace(ctx.State["args"].(string)) + pswd := strings.TrimSpace(ctx.Get("请输入密码")) + newusr := emozi.NewUser(name, pswd) + err := newusr.Login() + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + err = os.WriteFile(en.DataFolder()+"user.txt", []byte(name+"\n"+pswd), 0644) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + usr = newusr + refresher.Do(refresh) + ctx.SendChain(message.Text("成功")) + }) +} diff --git a/plugin/fortune/fortune.go b/plugin/fortune/fortune.go index 705f852d70..17bae08f07 100644 --- a/plugin/fortune/fortune.go +++ b/plugin/fortune/fortune.go @@ -146,7 +146,7 @@ func init() { digest := md5.Sum(helper.StringToBytes(zipfile + strconv.Itoa(index) + title + text)) cachefile := cache + hex.EncodeToString(digest[:]) - err = pool.SendImageFromPool(cachefile, cachefile, func() error { + err = pool.SendImageFromPool(cachefile, func(cachefile string) error { f, err := os.Create(cachefile) if err != nil { return err @@ -154,7 +154,7 @@ func init() { _, err = draw(background, fontdata, title, text, f) _ = f.Close() return err - }, ctxext.Send(ctx), ctxext.GetMessage(ctx)) + }, ctxext.Send(ctx)) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return diff --git a/plugin/funny/laugh.go b/plugin/funny/laugh.go index 52de4b14b1..12da99f609 100644 --- a/plugin/funny/laugh.go +++ b/plugin/funny/laugh.go @@ -21,7 +21,7 @@ type joke struct { Text string `db:"text"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func init() { en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ @@ -32,7 +32,7 @@ func init() { }) en.OnPrefixGroup([]string{"讲个笑话", "夸夸"}, fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = en.DataFolder() + "jokes.db" + db = sql.New(en.DataFolder() + "jokes.db") _, err := en.GetLazyData("jokes.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/gif/context.go b/plugin/gif/context.go index a1161053fc..6db373bf2c 100644 --- a/plugin/gif/context.go +++ b/plugin/gif/context.go @@ -20,7 +20,7 @@ func dlchan(name string, s *string, wg *sync.WaitGroup, exit func(error)) { defer wg.Done() target := datapath + `materials/` + name if file.IsNotExist(target) { - data, err := web.RequestDataWith(web.NewTLS12Client(), `https://gitcode.net/m0_60838134/imagematerials/-/raw/main/`+name, "GET", "gitcode.net", web.RandUA(), nil) + data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/master/` + name) if err != nil { _ = os.Remove(target) exit(err) @@ -48,7 +48,7 @@ func dlchan(name string, s *string, wg *sync.WaitGroup, exit func(error)) { func dlblock(name string) (string, error) { target := datapath + `materials/` + name if file.IsNotExist(target) { - data, err := web.RequestDataWith(web.NewTLS12Client(), `https://gitcode.net/m0_60838134/imagematerials/-/raw/main/`+name, "GET", "gitcode.net", web.RandUA(), nil) + data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/master/` + name) if err != nil { _ = os.Remove(target) return "", err diff --git a/plugin/gif/logo.go b/plugin/gif/logo.go index f0fe05b654..7bf668d003 100644 --- a/plugin/gif/logo.go +++ b/plugin/gif/logo.go @@ -16,7 +16,7 @@ func (cc *context) prepareLogos(s ...string) error { if err != nil { err = file.DownloadTo("https://gchat.qpic.cn/gchatpic_new//--"+strings.ToUpper(v)+"/0", cc.headimgsdir[i]) } else { - err = file.DownloadTo("http://q4.qlogo.cn/g?b=qq&nk="+v+"&s=640", cc.headimgsdir[i]) + err = file.DownloadTo("https://q4.qlogo.cn/g?b=qq&nk="+v+"&s=640", cc.headimgsdir[i]) } if err != nil { return err diff --git a/plugin/gif/run.go b/plugin/gif/run.go index 04524c2cd1..ad87880cac 100644 --- a/plugin/gif/run.go +++ b/plugin/gif/run.go @@ -150,7 +150,7 @@ func init() { // 插件主体 PrivateDataFolder: "gif", }).ApplySingle(ctxext.DefaultSingle) datapath = file.BOTPATH + "/" + en.DataFolder() - en.OnRegex(`^(` + strings.Join(cmd, "|") + `)[\s\S]*?(\[CQ:(image\,file=([0-9a-zA-Z]{32}).*|at.+?(\d{5,11}))\].*|(\d+))$`). + en.OnRegex(`^(` + strings.Join(cmd, "|") + `)[\s\S]*?(\[CQ:(image\,file=([0-9a-zA-Z]{32}).*|at.+?qq=(\d{5,11})).*\].*|(\d+))$`). SetBlock(true).Handle(func(ctx *zero.Ctx) { list := ctx.State["regex_matched"].([]string) atUserID, _ := strconv.ParseInt(list[4]+list[5]+list[6], 10, 64) diff --git a/plugin/guessmusic/apiservice.go b/plugin/guessmusic/apiservice.go index 98617de3a1..d0443f2975 100644 --- a/plugin/guessmusic/apiservice.go +++ b/plugin/guessmusic/apiservice.go @@ -246,11 +246,7 @@ func init() { ctx.SendChain(message.Text(serviceErr, err)) return } - if err == nil { - ctx.SendChain(message.Text("成功!")) - } else { - ctx.SendChain(message.Text(serviceErr, err)) - } + ctx.SendChain(message.Text("成功!")) }) // 下载歌曲到对应的歌单里面 engine.OnRegex(`^下载歌单\s*((https:\/\/music\.163\.com\/#\/playlist\?id=)?(\d+)|http:\/\/music\.163\.com\/playlist\/(\d+).*[^\s$])\s*到\s*(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). diff --git a/plugin/guessmusic/guessmusic.go b/plugin/guessmusic/guessmusic.go index 07de481406..d18d763e5d 100644 --- a/plugin/guessmusic/guessmusic.go +++ b/plugin/guessmusic/guessmusic.go @@ -130,10 +130,10 @@ func init() { after := time.NewTimer(120 * time.Second) wg := sync.WaitGroup{} var ( - messageStr message.MessageSegment // 文本信息 - tickCount = 0 // 音频数量 - answerCount = 0 // 问答次数 - win bool // 是否赢得游戏 + messageStr message.Segment // 文本信息 + tickCount = 0 // 音频数量 + answerCount = 0 // 问答次数 + win bool // 是否赢得游戏 ) for { select { @@ -281,7 +281,7 @@ func cutMusic(musicName, pathOfMusic, outputPath string) (err error) { } // 数据匹配(结果信息,答题次数,提示次数,是否结束游戏) -func gameMatch(c *zero.Ctx, beginner int64, musicInfo []string, answerTimes, tickTimes int) (message.MessageSegment, int, int, bool) { +func gameMatch(c *zero.Ctx, beginner int64, musicInfo []string, answerTimes, tickTimes int) (message.Segment, int, int, bool) { answer := strings.Replace(c.Event.Message.String(), "-", "", 1) // 回答内容转小写,比对时再把标准答案转小写 answer = ConvertText(answer) diff --git a/plugin/guessmusic/main.go b/plugin/guessmusic/main.go index 211da1279c..3c758d5553 100644 --- a/plugin/guessmusic/main.go +++ b/plugin/guessmusic/main.go @@ -535,7 +535,7 @@ func getFileURLbyFileName(ctx *zero.Ctx, fileName string) (fileSearchName, fileU for _, fileNameOflist := range files { if strings.Contains(fileNameOflist.Get("file_name").String(), fileName) { fileSearchName = fileNameOflist.Get("file_name").String() - fileURL = ctx.GetThisGroupFileUrl(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) + fileURL = ctx.GetThisGroupFileURL(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) return } } @@ -561,7 +561,7 @@ func getFileURLbyfolderID(ctx *zero.Ctx, fileName, folderid string) (fileSearchN for _, fileNameOflist := range files { if strings.Contains(fileNameOflist.Get("file_name").String(), fileName) { fileSearchName = fileNameOflist.Get("file_name").String() - fileURL = ctx.GetThisGroupFileUrl(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) + fileURL = ctx.GetThisGroupFileURL(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) return } } diff --git a/plugin/jandan/data.go b/plugin/jandan/data.go index 21534d15fc..eeb2d14b79 100644 --- a/plugin/jandan/data.go +++ b/plugin/jandan/data.go @@ -6,7 +6,7 @@ import ( sql "github.com/FloatTech/sqlite" ) -var db = &sql.Sqlite{} +var db sql.Sqlite var mu sync.RWMutex type picture struct { diff --git a/plugin/jandan/jandan.go b/plugin/jandan/jandan.go index 873f98b1c2..9963520b62 100644 --- a/plugin/jandan/jandan.go +++ b/plugin/jandan/jandan.go @@ -10,6 +10,7 @@ import ( "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/antchfx/htmlquery" @@ -31,7 +32,7 @@ func init() { }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "pics.db" + db = sql.New(engine.DataFolder() + "pics.db") _, _ = engine.GetLazyData("pics.db", false) err := db.Open(time.Hour) if err != nil { @@ -95,7 +96,7 @@ func init() { u := "https:" + v.Attr[0].Val i := crc64.Checksum(binary.StringToBytes(u), crc64.MakeTable(crc64.ISO)) mu.RLock() - ok := db.CanFind("picture", "where id="+strconv.FormatUint(i, 10)) + ok := db.CanFind("picture", "WHERE id = ?", i) mu.RUnlock() if !ok { mu.Lock() diff --git a/plugin/jptingroom/jptingroom.go b/plugin/jptingroom/jptingroom.go index 611b184c1f..4f7ab8cdcb 100644 --- a/plugin/jptingroom/jptingroom.go +++ b/plugin/jptingroom/jptingroom.go @@ -6,6 +6,7 @@ import ( "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/img/text" @@ -26,7 +27,7 @@ func init() { // 插件主体 }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "item.db" + db = sql.New(engine.DataFolder() + "item.db") _, err := engine.GetLazyData("item.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/jptingroom/model.go b/plugin/jptingroom/model.go index 4ab9de4afa..80ac149e68 100644 --- a/plugin/jptingroom/model.go +++ b/plugin/jptingroom/model.go @@ -17,14 +17,16 @@ type item struct { Datetime time.Time `db:"datetime"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func getRandomAudioByCategory(category string) (t item) { - _ = db.Find("item", &t, "where category = '"+category+"' ORDER BY RANDOM() limit 1") + _ = db.Find("item", &t, "WHERE category = ? ORDER BY RANDOM() limit 1", category) return } func getRandomAudioByCategoryAndKeyword(category string, keyword string) (t item) { - _ = db.Find("item", &t, "where category = '"+category+"' and (title like '%"+keyword+"%' or content like '%"+keyword+"%') ORDER BY RANDOM() limit 1") + _ = db.Find("item", &t, + "WHERE category = ? and (title LIKE ? OR content LIKE ?) ORDER BY RANDOM() limit 1", + category, "%"+keyword+"%", "%"+keyword+"%") return } diff --git a/plugin/kfccrazythursday/kfccrazythursday.go b/plugin/kfccrazythursday/kfccrazythursday.go index 971dd1e452..d144cdaebb 100644 --- a/plugin/kfccrazythursday/kfccrazythursday.go +++ b/plugin/kfccrazythursday/kfccrazythursday.go @@ -2,7 +2,6 @@ package kfccrazythursday import ( - "github.com/FloatTech/floatbox/binary" "github.com/FloatTech/floatbox/web" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" @@ -11,7 +10,7 @@ import ( ) const ( - crazyURL = "https://api.jixs.cc/api/wenan-fkxqs/index.php" + crazyURL = "https://api.pearktrue.cn/api/kfc/" ) func init() { @@ -26,6 +25,8 @@ func init() { ctx.SendChain(message.Text("ERROR: ", err)) return } - ctx.SendChain(message.Text(binary.BytesToString(data))) + + // 根据来源API修改返回方式到直接输出文本 + ctx.SendChain(message.Text(string(data))) }) } diff --git a/plugin/lolicon/lolicon.go b/plugin/lolicon/lolicon.go index 25f5eb3465..040f9d9659 100644 --- a/plugin/lolicon/lolicon.go +++ b/plugin/lolicon/lolicon.go @@ -18,7 +18,6 @@ import ( ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" - imagepool "github.com/FloatTech/zbputils/img/pool" ) const ( @@ -68,18 +67,7 @@ func init() { ctx.SendChain(message.Text("ERROR: ", err)) continue } - name := imageurl[strings.LastIndex(imageurl, "/")+1 : len(imageurl)-4] - m, err := imagepool.GetImage(name) - if err != nil { - m.SetFile(imageurl) - _, _ = m.Push(ctxext.SendToSelf(ctx), ctxext.GetMessage(ctx)) - process.SleepAbout1sTo2s() - } - if err == nil { - queue <- m.String() - } else { - queue <- imageurl - } + queue <- imageurl } }() select { diff --git a/plugin/manager/gist.go b/plugin/manager/gist.go index 61fc60c064..7dde8b0164 100644 --- a/plugin/manager/gist.go +++ b/plugin/manager/gist.go @@ -18,7 +18,7 @@ import ( const gistraw = "https://gist.githubusercontent.com/%s/%s/raw/%s" func checkNewUser(qq, gid int64, ghun, hash string) (bool, string) { - if db.CanFind("member", "where ghun="+ghun) { + if db.CanFind("member", "WHERE ghun = ?", ghun) { return false, "该github用户已入群" } gidsum := md5.Sum(helper.StringToBytes(strconv.FormatInt(gid, 10))) diff --git a/plugin/manager/manager.go b/plugin/manager/manager.go index 415796d6df..2521412bdb 100644 --- a/plugin/manager/manager.go +++ b/plugin/manager/manager.go @@ -50,6 +50,7 @@ const ( "- 列出所有提醒\n" + "- 翻牌\n" + "- 赞我\n" + + "- 群签到\n" + "- 对信息回复: 回应表情 [表情]\n" + "- 设置欢迎语XXX 可选添加 [{at}] [{nickname}] [{avatar}] [{uid}] [{gid}] [{groupname}]\n" + "- 测试欢迎语\n" + @@ -63,7 +64,7 @@ const ( ) var ( - db = &sql.Sqlite{} + db sql.Sqlite clock timer.Clock ) @@ -76,12 +77,12 @@ func init() { // 插件主体 }) go func() { - db.DBPath = engine.DataFolder() + "config.db" + db = sql.New(engine.DataFolder() + "config.db") err := db.Open(time.Hour) if err != nil { panic(err) } - clock = timer.NewClock(db) + clock = timer.NewClock(&db) err = db.Create("welcome", &welcome{}) if err != nil { panic(err) @@ -156,10 +157,11 @@ func init() { // 插件主体 ctx.SendChain(message.Text("全员自闭结束~")) }) // 禁言 - engine.OnRegex(`^禁言.*?(\d+).*?\s(\d+)(.*)`, zero.OnlyGroup, zero.AdminPermission).SetBlock(true). + engine.OnMessage(zero.NewPattern(nil).Text("^禁言").At().Text("(\\d+)\\s*(.*)").AsRule(), zero.OnlyGroup, zero.AdminPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - duration := math.Str2Int64(ctx.State["regex_matched"].([]string)[2]) - switch ctx.State["regex_matched"].([]string)[3] { + parsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + duration := math.Str2Int64(parsed[2].Text()[1]) + switch parsed[2].Text()[2] { case "分钟": // case "小时": @@ -173,8 +175,8 @@ func init() { // 插件主体 duration = 43199 // qq禁言最大时长为一个月 } ctx.SetThisGroupBan( - math.Str2Int64(ctx.State["regex_matched"].([]string)[1]), // 要禁言的人的qq - duration*60, // 要禁言的时间(分钟) + math.Str2Int64(parsed[1].At()), // 要禁言的人的qq + duration*60, // 要禁言的时间(分钟) ) ctx.SendChain(message.Text("小黑屋收留成功~")) }) @@ -404,6 +406,12 @@ func init() { // 插件主体 ctx.SendLike(ctx.Event.UserID, 10) ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("给你赞了10下哦,记得回我~")) }) + // 群签到 + engine.OnFullMatch("群签到", zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByUser). + Handle(func(ctx *zero.Ctx) { + ctx.SetGroupSign(ctx.Event.GroupID) + ctx.SendChain(message.Text("群签到成功,可在手机端输入框中的打卡查看")) + }) facere := regexp.MustCompile(`\[CQ:face,id=(\d+)\]`) // 给消息回应表情 engine.OnRegex(`^\[CQ:reply,id=(-?\d+)\].*回应表情\s*(.+)\s*$`, zero.AdminPermission, zero.OnlyGroup).SetBlock(true). @@ -442,7 +450,7 @@ func init() { // 插件主体 Handle(func(ctx *zero.Ctx) { if ctx.Event.NoticeType == "group_increase" && ctx.Event.SelfID != ctx.Event.UserID { var w welcome - err := db.Find("welcome", &w, "where gid = "+strconv.FormatInt(ctx.Event.GroupID, 10)) + err := db.Find("welcome", &w, "WHERE gid = ?", ctx.Event.GroupID) if err == nil { ctx.SendGroupMessage(ctx.Event.GroupID, message.ParseMessageFromString(welcometocq(ctx, w.Msg))) } else { @@ -494,12 +502,12 @@ func init() { // 插件主体 Handle(func(ctx *zero.Ctx) { if ctx.Event.NoticeType == "group_decrease" { var w welcome - err := db.Find("farewell", &w, "where gid = "+strconv.FormatInt(ctx.Event.GroupID, 10)) + err := db.Find("farewell", &w, "WHERE gid = ?", ctx.Event.GroupID) if err == nil { - ctx.SendGroupMessage(ctx.Event.GroupID, message.ParseMessageFromString(welcometocq(ctx, w.Msg))) + collectsend(ctx, message.ParseMessageFromString(welcometocq(ctx, w.Msg))...) } else { userid := ctx.Event.UserID - ctx.SendChain(message.Text(ctx.CardOrNickName(userid), "(", userid, ")", "离开了我们...")) + collectsend(ctx, message.Text(ctx.CardOrNickName(userid), "(", userid, ")", "离开了我们...")) } } }) @@ -523,7 +531,7 @@ func init() { // 插件主体 engine.OnFullMatch("测试欢迎语", zero.OnlyGroup, zero.AdminPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { var w welcome - err := db.Find("welcome", &w, "where gid = "+strconv.FormatInt(ctx.Event.GroupID, 10)) + err := db.Find("welcome", &w, "WHERE gid = ?", ctx.Event.GroupID) if err == nil { ctx.SendGroupMessage(ctx.Event.GroupID, message.ParseMessageFromString(welcometocq(ctx, w.Msg))) } else { @@ -550,7 +558,7 @@ func init() { // 插件主体 engine.OnFullMatch("测试告别辞", zero.OnlyGroup, zero.AdminPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { var w welcome - err := db.Find("farewell", &w, "where gid = "+strconv.FormatInt(ctx.Event.GroupID, 10)) + err := db.Find("farewell", &w, "WHERE gid = ?", ctx.Event.GroupID) if err == nil { ctx.SendGroupMessage(ctx.Event.GroupID, message.ParseMessageFromString(welcometocq(ctx, w.Msg))) } else { @@ -649,7 +657,7 @@ func init() { // 插件主体 if rsp.RetCode == 0 { ctx.SendChain(message.Text(option, "成功")) } else { - ctx.SendChain(message.Text(option, "失败, 信息: ", rsp.Msg, "解释: ", rsp.Wording)) + ctx.SendChain(message.Text(option, "失败, 信息: ", rsp.Message, "解释: ", rsp.Wording)) } }) engine.OnCommand("精华列表", zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { @@ -698,19 +706,19 @@ func init() { // 插件主体 if rsp.RetCode == 0 { ctx.SendChain(message.Text("取消成功")) } else { - ctx.SendChain(message.Text("取消失败, 信息: ", rsp.Msg, "解释: ", rsp.Wording)) + ctx.SendChain(message.Text("取消失败, 信息: ", rsp.Message, "解释: ", rsp.Wording)) } }) } // 传入 ctx 和 welcome格式string 返回cq格式string 使用方法:welcometocq(ctx,w.Msg) func welcometocq(ctx *zero.Ctx, welcome string) string { - uid := strconv.FormatInt(ctx.Event.UserID, 10) // 用户id - nickname := ctx.CardOrNickName(ctx.Event.UserID) // 用户昵称 - at := "[CQ:at,qq=" + uid + "]" // at用户 - avatar := "[CQ:image,file=" + "http://q4.qlogo.cn/g?b=qq&nk=" + uid + "&s=640]" // 用户头像 - gid := strconv.FormatInt(ctx.Event.GroupID, 10) // 群id - groupname := ctx.GetThisGroupInfo(true).Name // 群名 + uid := strconv.FormatInt(ctx.Event.UserID, 10) // 用户id + nickname := ctx.CardOrNickName(ctx.Event.UserID) // 用户昵称 + at := "[CQ:at,qq=" + uid + "]" // at用户 + avatar := "[CQ:image,file=" + "https://q4.qlogo.cn/g?b=qq&nk=" + uid + "&s=640]" // 用户头像 + gid := strconv.FormatInt(ctx.Event.GroupID, 10) // 群id + groupname := ctx.GetThisGroupInfo(true).Name // 群名 cqstring := strings.ReplaceAll(welcome, "{at}", at) cqstring = strings.ReplaceAll(cqstring, "{nickname}", nickname) cqstring = strings.ReplaceAll(cqstring, "{avatar}", avatar) diff --git a/plugin/manager/manager.db.go b/plugin/manager/model.go similarity index 100% rename from plugin/manager/manager.db.go rename to plugin/manager/model.go diff --git a/plugin/manager/slow.go b/plugin/manager/slow.go new file mode 100644 index 0000000000..5c2e2a850f --- /dev/null +++ b/plugin/manager/slow.go @@ -0,0 +1,46 @@ +package manager + +import ( + "time" + + "github.com/RomiChan/syncx" + "github.com/fumiama/slowdo" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +var slowsenders = syncx.Map[int64, *syncx.Lazy[*slowdo.Job[*zero.Ctx, message.Segment]]]{} + +func collectsend(ctx *zero.Ctx, msgs ...message.Segment) { + id := ctx.Event.GroupID + if id == 0 { + // only support group + return + } + lazy, _ := slowsenders.LoadOrStore(id, &syncx.Lazy[*slowdo.Job[*zero.Ctx, message.Segment]]{ + Init: func() *slowdo.Job[*zero.Ctx, message.Segment] { + x, err := slowdo.NewJob(time.Second*5, ctx, func(ctx *zero.Ctx, msg []message.Segment) { + if len(msg) == 1 { + ctx.Send(msg) + return + } + m := make(message.Message, len(msg)) + for i, item := range msg { + m[i] = message.CustomNode( + zero.BotConfig.NickName[0], + ctx.Event.SelfID, + message.Message{item}) + } + ctx.SendGroupForwardMessage(id, m) + }) + if err != nil { + panic(err) + } + return x + }, + }) + job := lazy.Get() + for _, msg := range msgs { + job.Add(msg) + } +} diff --git a/plugin/manager/timer/parse.go b/plugin/manager/timer/parse.go index 83a296fbf5..585a88edb6 100644 --- a/plugin/manager/timer/parse.go +++ b/plugin/manager/timer/parse.go @@ -96,12 +96,12 @@ func GetFilledTimer(dateStrs []string, botqq, grp int64, matchDateOnly bool) *Ti if len(minuteStr) == 3 { minuteStr = []rune{minuteStr[0], minuteStr[2]} // 去除中间的十 } - min := chineseNum2Int(minuteStr) - if min < -1 || min > 59 { // 分钟非法 + minute := chineseNum2Int(minuteStr) + if minute < -1 || minute > 59 { // 分钟非法 t.Alert = "分钟非法!" return &t } - t.SetMinute(min) + t.SetMinute(minute) if !matchDateOnly { urlStr := dateStrs[5] if urlStr != "" { // 是图片url diff --git a/plugin/manager/timer/timer.db.go b/plugin/manager/timer/timer.db.go index 86305325e7..7a5afa9688 100644 --- a/plugin/manager/timer/timer.db.go +++ b/plugin/manager/timer/timer.db.go @@ -22,7 +22,7 @@ func (t *Timer) InsertInto(db *sql.Sqlite) error { /* func getTimerFrom(db *sql.Sqlite, id uint32) (t Timer, err error) { - err = db.Find("timer", &t, "where id = "+strconv.Itoa(int(id))) + err = db.Find("timer", &t, "WHERE id = "+strconv.Itoa(int(id))) return } */ diff --git a/plugin/manager/timer/timer.go b/plugin/manager/timer/timer.go index 281d33d2e0..43a7254017 100644 --- a/plugin/manager/timer/timer.go +++ b/plugin/manager/timer/timer.go @@ -2,7 +2,6 @@ package timer import ( - "strconv" "strings" "sync" "time" @@ -29,7 +28,7 @@ type Clock struct { var ( // @全体成员 - atall = message.MessageSegment{ + atall = message.Segment{ Type: "at", Data: map[string]string{ "qq": "all", @@ -133,7 +132,7 @@ func (c *Clock) CancelTimer(key uint32) bool { } c.timersmu.Lock() delete(*c.timers, key) // 避免重复取消 - e := c.db.Del("timer", "where id = "+strconv.Itoa(int(key))) + e := c.db.Del("timer", "WHERE id = ?", key) c.timersmu.Unlock() return e == nil } diff --git a/plugin/manager/timer/timer_test.go b/plugin/manager/timer/timer_test.go index fabfad5c1d..ceb87c7482 100644 --- a/plugin/manager/timer/timer_test.go +++ b/plugin/manager/timer/timer_test.go @@ -25,8 +25,8 @@ func TestNextWakeTime(t *testing.T) { } func TestClock(t *testing.T) { - db := &sql.Sqlite{DBPath: "test.db"} - c := NewClock(db) + db := sql.New("test.db") + c := NewClock(&db) c.AddTimerIntoDB(GetFilledTimer([]string{"", "12", "-1", "12", "0", "", "test"}, 0, 0, false)) t.Log(c.ListTimers(0)) t.Fail() diff --git a/plugin/manager/timer/wrap.go b/plugin/manager/timer/wrap.go index bb48f29d1b..8c1c42c4fc 100644 --- a/plugin/manager/timer/wrap.go +++ b/plugin/manager/timer/wrap.go @@ -44,10 +44,10 @@ func (t *Timer) Hour() (h int) { } // Minute 6bits -func (t *Timer) Minute() (min int) { - min = int(t.En1Month4Day5Week3Hour5Min6 & 0x00003f) - if min == 0b111111 { - min = -1 +func (t *Timer) Minute() (m int) { + m = int(t.En1Month4Day5Week3Hour5Min6 & 0x00003f) + if m == 0b111111 { + m = -1 } return } @@ -82,6 +82,6 @@ func (t *Timer) SetHour(h int) { } // SetMinute ... -func (t *Timer) SetMinute(min int) { - t.En1Month4Day5Week3Hour5Min6 = (int32(min) & 0x00003f) | (t.En1Month4Day5Week3Hour5Min6 & 0xffffc0) +func (t *Timer) SetMinute(m int) { + t.En1Month4Day5Week3Hour5Min6 = (int32(m) & 0x00003f) | (t.En1Month4Day5Week3Hour5Min6 & 0xffffc0) } diff --git a/plugin/mcfish/fish.go b/plugin/mcfish/fish.go index df8b4c48e1..29cf4cf605 100644 --- a/plugin/mcfish/fish.go +++ b/plugin/mcfish/fish.go @@ -59,7 +59,7 @@ func init() { for { select { case <-time.After(time.Second * 120): - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消钓鱼"))) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消购买"))) return case e := <-recv: nextcmd := e.Event.Message.String() @@ -69,7 +69,7 @@ func init() { } money := wallet.GetWalletOf(uid) if money < 100 { - ctx.SendChain(message.Text("你钱包当前只有", money, "ATRI币,无法完成支付")) + ctx.SendChain(message.Text("你钱包当前只有", money, wallet.GetWalletName(), ",无法完成支付")) return } err = wallet.InsertWalletOf(uid, -100) @@ -129,12 +129,12 @@ func init() { fishNumber *= 3 } } else { - fishNmaes, err := dbdata.pickFishFor(uid, fishNumber) + fishNames, err := dbdata.pickFishFor(uid, fishNumber*3) if err != nil { ctx.SendChain(message.Text("[ERROR at fish.go.5.1]:", err)) return } - if len(fishNmaes) == 0 { + if len(fishNames) == 0 { equipInfo.Durable = 0 err = dbdata.updateUserEquip(equipInfo) if err != nil { @@ -143,14 +143,14 @@ func init() { ctx.SendChain(message.Text("美西螈因为没吃到鱼,钓鱼时一直没回来,你失去了美西螈")) return } - msg = "(美西螈吃掉了" + msg = "(美西螈掉落翻5倍,吃3倍鱼:\n吃掉了:" fishNumber = 0 - for name, number := range fishNmaes { + for name, number := range fishNames { fishNumber += number - msg += strconv.Itoa(number) + name + "、" + msg += strconv.Itoa(number) + name + " " } msg += ")" - fishNumber /= 2 + fishNumber /= 3 } waitTime := 120 / (equipInfo.Induce + 1) ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("你开始去钓鱼了,请耐心等待鱼上钩(预计要", time.Second*time.Duration(waitTime), ")")) @@ -267,8 +267,8 @@ func init() { thingName = "金竿" case dice >= probabilities["钻石竿"].Min && dice < probabilities["钻石竿"].Max: thingName = "钻石竿" - case dice >= probabilities["下界合金竿竿竿"].Min && dice < probabilities["下界合金竿竿竿"].Max: - thingName = "下界合金竿竿竿" + case dice >= probabilities["下界合金竿"].Min && dice < probabilities["下界合金竿"].Max: + thingName = "下界合金竿" default: thingName = "木竿" } @@ -323,7 +323,7 @@ func init() { newThing = thingInfo[0] } if equipInfo.Equip == "美西螈" && thingName != "美西螈" { - number += 2 + number += 4 } newThing.Number += number } diff --git a/plugin/mcfish/main.go b/plugin/mcfish/main.go index 8b58a58585..3b4d3db4af 100644 --- a/plugin/mcfish/main.go +++ b/plugin/mcfish/main.go @@ -6,6 +6,7 @@ import ( "math/rand" "os" "strconv" + "strings" "sync" "time" @@ -20,15 +21,15 @@ import ( ) type fishdb struct { - db *sql.Sqlite sync.RWMutex + db sql.Sqlite } // FishLimit 钓鱼次数上限 const FishLimit = 50 // version 规则版本号 -const version = "5.4.2" +const version = "5.6.2" // 各物品信息 type jsonInfo struct { @@ -121,39 +122,31 @@ var ( durationList = make(map[string]int, 50) // 装备耐久分布 discountList = make(map[string]int, 50) // 价格波动信息 enchantLevel = []string{"0", "Ⅰ", "Ⅱ", "Ⅲ"} - dbdata = &fishdb{ - db: &sql.Sqlite{}, - } + dbdata fishdb ) var ( engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "钓鱼", - Help: "一款钓鱼模拟器\n----------指令----------\n" + - "- 钓鱼看板/钓鱼商店\n- 购买xxx\n- 购买xxx [数量]\n- 出售xxx\n- 出售xxx [数量]\n- 出售所有垃圾\n" + - "- 钓鱼背包\n- 装备[xx竿|三叉戟|美西螈]\n- 附魔[诱钓|海之眷顾]\n- 修复鱼竿\n- 合成[xx竿|三叉戟]\n- 消除[绑定|宝藏]诅咒\n- 消除[绑定|宝藏]诅咒 [数量]\n" + - "- 进行钓鱼\n- 进行n次钓鱼\n- 当前装备概率明细\n" + - "规则V" + version + ":\n" + - "1.每日的商店价格是波动的!!如何最大化收益自己考虑一下喔\n" + - "2.装备信息:\n-> 木竿 : 耐久上限:30 均价:100 上钩概率:0.7%\n-> 铁竿 : 耐久上限:50 均价:300 上钩概率:0.2%\n-> 金竿 : 耐久上限:70 均价700 上钩概率:0.06%\n" + - "-> 钻石竿 : 耐久上限:100 均价1500 上钩概率:0.03%\n-> 下界合金竿 : 耐久上限:150 均价3100 上钩概率:0.01%\n-> 三叉戟 : 可使1次钓鱼视为3次钓鱼. 耐久上限:300 均价4000 只能合成、修复和交易\n" + - "3.附魔书信息:\n-> 诱钓 : 减少上钩时间. 均价:1000, 上钩概率:0.25%\n-> 海之眷顾 : 增加宝藏上钩概率. 均价:2500, 上钩概率:0.10%\n" + - "4.稀有物品:\n-> 唱片 : 出售物品时使用该物品使价格翻倍. 均价:3000, 上钩概率:0.01%\n" + - "-> 美西螈 : 可装备,获得隐形[钓鱼佬]buff,并让钓到除鱼竿和美西螈外的物品数量变成3,无耐久上限.不可修复/附魔,每次钓鱼消耗两任意鱼类物品. 均价:3000, 上钩概率:0.01%\n" + - "-> 海豚 : 使空竿概率变成垃圾概率. 均价:1000, 上钩概率:0.19%\n" + - "-> 宝藏诅咒 : 无法交易,每一层就会增加购买时10%价格和减少出售时10%价格(超过10层会变为倒贴钱). 上钩概率:0.25%\n-> 净化书 : 用于消除宝藏诅咒. 均价:5000, 上钩概率:0.19%\n" + - "5.鱼类信息:\n-> 鳕鱼 : 均价:10 上钩概率:0.69%\n-> 鲑鱼 : 均价:50 上钩概率:0.2%\n-> 热带鱼 : 均价:100 上钩概率:0.06%\n-> 河豚 : 均价:300 上钩概率:0.03%\n-> 鹦鹉螺 : 均价:500 上钩概率:0.01%\n-> 墨鱼 : 均价:500 上钩概率:0.01%\n" + - "6.垃圾:\n-> 均价:10 上钩概率:30%\n" + - "7.物品BUFF:\n-> 钓鱼佬 : 当背包名字含有'鱼'的物品数量超过100时激活,钓到物品概率提高至90%\n-> 修复大师 : 当背包鱼竿数量超过10时激活,修复物品时耐久百分百继承\n" + - "8.合成:\n-> 铁竿 : 3x木竿\n-> 金竿 : 3x铁竿\n-> 钻石竿 : 3x金竿\n-> 下界合金竿 : 3x钻石竿\n-> 三叉戟 : 3x下界合金竿\n注:合成成功率90%,继承附魔等级合/3的等级\n" + - "9.杂项:\n-> 无装备的情况下,每人最多可以购买3次100块钱的鱼竿\n-> 默认状态钓鱼上钩概率为60%(理论值!!!)\n-> 附魔的鱼竿会因附魔变得昂贵,每个附魔最高3级\n-> 三叉戟不算鱼竿,修复时可直接满耐久\n" + - "-> 鱼竿数量大于50的不能买东西;\n 鱼竿数量大于30的不能钓鱼;\n 每购/售10次鱼竿获得1层宝藏诅咒;\n 每购买20次物品将获得3次价格减半福利;\n 每钓鱼75次获得1本净化书;\n" + - " 每天最多只可出售5个鱼竿和购买15次物品;", + Help: "一款钓鱼模拟器,规则:V" + version + + "\n----------指令----------\n" + + "- 钓鱼背包\n" + + "- 进行钓鱼 / 进行n次钓鱼\n" + + "- 修复鱼竿\n" + + "- 钓鱼商店 / 钓鱼看板\n" + + "- 购买xxx / 购买xxx [数量]\n- 出售xxx / 出售xxx [数量]\n" + + "- 消除[绑定|宝藏]诅咒 / 消除[绑定|宝藏]诅咒 [数量]\n" + + "- 装备[xx竿|三叉戟|美西螈]\n" + + "- 附魔[诱钓|海之眷顾]\n" + + "- 合成[xx竿|三叉戟]\n" + + "- 出售所有垃圾\n" + + "- 当前装备概率明细\n" + + "- 查看钓鱼规则\n", PublicDataFolder: "McFish", }).ApplySingle(ctxext.DefaultSingle) getdb = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - dbdata.db.DBPath = engine.DataFolder() + "fishdata.db" + dbdata.db = sql.New(engine.DataFolder() + "fishdata.db") err := dbdata.db.Open(time.Hour * 24) if err != nil { ctx.SendChain(message.Text("[ERROR at main.go.1]:", err)) @@ -208,7 +201,7 @@ func init() { Min: probableList[2], Max: probableList[3], } - min := make(map[string]int, 4) + minMap := make(map[string]int, 4) for _, info := range articlesInfo.ArticleInfo { switch { case info.Type == "pole" || info.Name == "美西螈": @@ -228,10 +221,10 @@ func init() { durationList[info.Name] = info.Durable } probabilities[info.Name] = probabilityLimit{ - Min: min[info.Type], - Max: min[info.Type] + info.Probability, + Min: minMap[info.Type], + Max: minMap[info.Type] + info.Probability, } - min[info.Type] += info.Probability + minMap[info.Type] += info.Probability } // }() } @@ -245,7 +238,7 @@ func (sql *fishdb) updateFishInfo(uid int64, number int) (residue int, err error if err != nil { return 0, err } - _ = sql.db.Find("fishState", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + _ = sql.db.Find("fishState", &userInfo, "WHERE ID = ?", uid) if time.Unix(userInfo.Duration, 0).Day() != time.Now().Day() { userInfo.Fish = 0 userInfo.Duration = time.Now().Unix() @@ -278,7 +271,7 @@ func (sql *fishdb) updateCurseFor(uid int64, info string, number int) (err error changeCheck := false add := 0 buffName := "宝藏诅咒" - _ = sql.db.Find("fishState", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + _ = sql.db.Find("fishState", &userInfo, "WHERE ID = ?", uid) if info == "fish" { userInfo.Bless += number for userInfo.Bless >= 75 { @@ -306,7 +299,7 @@ func (sql *fishdb) updateCurseFor(uid int64, info string, number int) (err error Name: buffName, Type: "treasure", } - _ = sql.db.Find(table, &thing, "where Name = '"+buffName+"'") + _ = sql.db.Find(table, &thing, "WHERE Name = ?", buffName) thing.Number += add return sql.db.Insert(table, &thing) } @@ -325,10 +318,10 @@ func (sql *fishdb) checkEquipFor(uid int64) (ok bool, err error) { if err != nil { return false, err } - if !sql.db.CanFind("fishState", "where ID = "+strconv.FormatInt(uid, 10)) { + if !sql.db.CanFind("fishState", "WHERE ID = ?", uid) { return true, nil } - err = sql.db.Find("fishState", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + err = sql.db.Find("fishState", &userInfo, "WHERE ID = ?", uid) if err != nil { return false, err } @@ -346,10 +339,7 @@ func (sql *fishdb) setEquipFor(uid int64) (err error) { if err != nil { return err } - _ = sql.db.Find("fishState", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) - if err != nil { - return err - } + _ = sql.db.Find("fishState", &userInfo, "WHERE ID = ?", uid) userInfo.Equip++ return sql.db.Insert("fishState", &userInfo) } @@ -362,10 +352,10 @@ func (sql *fishdb) getUserEquip(uid int64) (userInfo equip, err error) { if err != nil { return } - if !sql.db.CanFind("equips", "where ID = "+strconv.FormatInt(uid, 10)) { + if !sql.db.CanFind("equips", "WHERE ID = ?", uid) { return } - err = sql.db.Find("equips", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + err = sql.db.Find("equips", &userInfo, "WHERE ID = ?", uid) return } @@ -378,7 +368,7 @@ func (sql *fishdb) updateUserEquip(userInfo equip) (err error) { return } if userInfo.Durable == 0 { - return sql.db.Del("equips", "where ID = "+strconv.FormatInt(userInfo.ID, 10)) + return sql.db.Del("equips", "WHERE ID = ?", userInfo.ID) } return sql.db.Insert("equips", &userInfo) } @@ -400,29 +390,29 @@ func (sql *fishdb) pickFishFor(uid int64, number int) (fishNames map[string]int, if count == 0 { return } - if !sql.db.CanFind(name, "where Type is 'fish'") { + if !sql.db.CanFind(name, "WHERE Type = 'fish'") { return } fishInfo := article{} k := 0 - for i := number * 2; i > 0 && k < len(fishList); { - _ = sql.db.Find(name, &fishInfo, "where Name is '"+fishList[k]+"'") + for i := number; i > 0 && k < len(fishList); { + _ = sql.db.Find(name, &fishInfo, "WHERE Name = ?", fishList[k]) if fishInfo.Number <= 0 { k++ continue } if fishInfo.Number < i { k++ - fishInfo.Number = 0 i -= fishInfo.Number fishNames[fishInfo.Name] += fishInfo.Number + fishInfo.Number = 0 } else { fishNames[fishInfo.Name] += i fishInfo.Number -= i i = 0 } if fishInfo.Number <= 0 { - err = sql.db.Del(name, "where Duration = "+strconv.FormatInt(fishInfo.Duration, 10)) + err = sql.db.Del(name, "WHERE Duration = ?", fishInfo.Duration) } else { err = sql.db.Insert(name, &fishInfo) } @@ -477,13 +467,13 @@ func (sql *fishdb) getUserThingInfo(uid int64, thing string) (thingInfos []artic if count == 0 { return } - if !sql.db.CanFind(name, "where Name = '"+thing+"'") { + if !sql.db.CanFind(name, "WHERE Name = ?", thing) { return } - err = sql.db.FindFor(name, &userInfo, "where Name = '"+thing+"'", func() error { + err = sql.db.FindFor(name, &userInfo, "WHERE Name = ?", func() error { thingInfos = append(thingInfos, userInfo) return nil - }) + }, thing) return } @@ -497,7 +487,7 @@ func (sql *fishdb) updateUserThingInfo(uid int64, userInfo article) (err error) return } if userInfo.Number == 0 { - return sql.db.Del(name, "where Duration = "+strconv.FormatInt(userInfo.Duration, 10)) + return sql.db.Del(name, "WHERE Duration = ?", userInfo.Duration) } return sql.db.Insert(name, &userInfo) } @@ -519,14 +509,14 @@ func (sql *fishdb) getNumberFor(uid int64, thing string) (number int, err error) if count == 0 { return } - if !sql.db.CanFind(name, "where Name glob '*"+thing+"*'") { + if !sql.db.CanFind(name, "WHERE Name glob ?", "*"+thing+"*") { return } info := article{} - err = sql.db.FindFor(name, &info, "where Name glob '*"+thing+"*'", func() error { + err = sql.db.FindFor(name, &info, "WHERE Name glob ?", func() error { number += info.Number return nil - }) + }, "*"+thing+"*") return } @@ -540,13 +530,13 @@ func (sql *fishdb) getUserTypeInfo(uid int64, thingType string) (thingInfos []ar if err != nil { return } - if !sql.db.CanFind(name, "where Type = '"+thingType+"'") { + if !sql.db.CanFind(name, "WHERE Type = ?", thingType) { return } - err = sql.db.FindFor(name, &userInfo, "where Type = '"+thingType+"'", func() error { + err = sql.db.FindFor(name, &userInfo, "WHERE Type = ?", func() error { thingInfos = append(thingInfos, userInfo) return nil - }) + }, thingType) return } @@ -562,8 +552,12 @@ func (sql *fishdb) refreshStroeInfo() (ok bool, err error) { if err != nil { return false, err } + err = sql.db.Create("store", &store{}) + if err != nil { + return false, err + } lastTime := storeDiscount{} - _ = sql.db.Find("stroeDiscount", &lastTime, "where Name = 'lastTime'") + _ = sql.db.Find("stroeDiscount", &lastTime, "WHERE Name = 'lastTime'") refresh := false timeNow := time.Now().Day() if timeNow != lastTime.Discount { @@ -586,12 +580,18 @@ func (sql *fishdb) refreshStroeInfo() (ok bool, err error) { Name: name, Discount: thingDiscount, } + thingInfo := store{} + _ = sql.db.Find("store", &thingInfo, "WHERE Name = ?", name) + if thingInfo.Number > 150 { + // 控制价格浮动区间: -10%到10% + thing.Discount = 90 + rand.Intn(20) + } err = sql.db.Insert("stroeDiscount", &thing) if err != nil { return } default: - _ = sql.db.Find("stroeDiscount", &thing, "where Name = '"+name+"'") + _ = sql.db.Find("stroeDiscount", &thing, "WHERE Name = ?", name) } if thing.Discount != 0 { discountList[name] = thing.Discount @@ -600,21 +600,17 @@ func (sql *fishdb) refreshStroeInfo() (ok bool, err error) { } } thing := store{} - oldThing := []store{} - _ = sql.db.FindFor("stroeDiscount", &thing, "where type = 'pole'", func() error { + var oldThing []store + _ = sql.db.FindFor("stroeDiscount", &thing, "WHERE type = 'pole'", func() error { if time.Since(time.Unix(thing.Duration, 0)) > 24 { oldThing = append(oldThing, thing) } return nil }) for _, info := range oldThing { - _ = sql.db.Del("stroeDiscount", "where Duration = "+strconv.FormatInt(info.Duration, 10)) + _ = sql.db.Del("stroeDiscount", "WHERE Duration = ?", info.Duration) } if refresh { - err = sql.db.Create("store", &store{}) - if err != nil { - return - } // 每天调控1种鱼 fish := fishList[rand.Intn(len(fishList))] thingInfo := store{ @@ -623,21 +619,25 @@ func (sql *fishdb) refreshStroeInfo() (ok bool, err error) { Type: "fish", Price: priceList[fish] * discountList[fish] / 100, } - _ = sql.db.Find("store", &thingInfo, "where Name = '"+fish+"'") - thingInfo.Number += (100 - discountList[fish]) + _ = sql.db.Find("store", &thingInfo, "WHERE Name = ?", fish) + thingInfo.Number += 100 - discountList[fish] if thingInfo.Number < 1 { thingInfo.Number = 100 } _ = sql.db.Insert("store", &thingInfo) - // 每天上架20本净化书 + // 每天上架1木竿 thingInfo = store{ Duration: time.Now().Unix(), - Name: "净化书", - Type: "article", - Price: priceList["净化书"] * discountList["净化书"] / 100, + Name: "初始木竿", + Type: "pole", + Price: priceList["木竿"] + priceList["木竿"]*discountList["木竿"]/100, + Other: "30/0/0/0", + } + _ = sql.db.Find("store", &thingInfo, "WHERE Name = '初始木竿'") + thingInfo.Number++ + if thingInfo.Number > 5 { + thingInfo.Number = 1 } - _ = sql.db.Find("store", &thingInfo, "where Name = '净化书'") - thingInfo.Number = 20 _ = sql.db.Insert("store", &thingInfo) } return true, nil @@ -682,13 +682,13 @@ func (sql *fishdb) getStoreThingInfo(thing string) (thingInfos []store, err erro if count == 0 { return } - if !sql.db.CanFind("store", "where Name = '"+thing+"'") { + if !sql.db.CanFind("store", "WHERE Name = ?", thing) { return } - err = sql.db.FindFor("store", &thingInfo, "where Name = '"+thing+"'", func() error { + err = sql.db.FindFor("store", &thingInfo, "WHERE Name = ?", func() error { thingInfos = append(thingInfos, thingInfo) return nil - }) + }, thing) return } @@ -707,10 +707,10 @@ func (sql *fishdb) checkStoreFor(thing store, number int) (ok bool, err error) { if count == 0 { return false, nil } - if !sql.db.CanFind("store", "where Duration = "+strconv.FormatInt(thing.Duration, 10)) { + if !sql.db.CanFind("store", "WHERE Duration = ?", thing.Duration) { return false, nil } - err = sql.db.Find("store", &thing, "where Duration = "+strconv.FormatInt(thing.Duration, 10)) + err = sql.db.Find("store", &thing, "WHERE Duration = ?", thing.Duration) if err != nil { return } @@ -729,7 +729,7 @@ func (sql *fishdb) updateStoreInfo(thingInfo store) (err error) { return } if thingInfo.Number == 0 { - return sql.db.Del("store", "where Duration = "+strconv.FormatInt(thingInfo.Duration, 10)) + return sql.db.Del("store", "WHERE Duration = ?", thingInfo.Duration) } return sql.db.Insert("store", &thingInfo) } @@ -743,7 +743,7 @@ func (sql *fishdb) updateBuyTimeFor(uid int64, add int) (err error) { if err != nil { return err } - _ = sql.db.Find("buff", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + _ = sql.db.Find("buff", &userInfo, "WHERE ID = ?", uid) userInfo.BuyTimes += add if userInfo.BuyTimes > 20 { userInfo.BuyTimes -= 20 @@ -762,7 +762,7 @@ func (sql *fishdb) useCouponAt(uid int64, times int) (int, error) { if err != nil { return useTimes, err } - _ = sql.db.Find("buff", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + _ = sql.db.Find("buff", &userInfo, "WHERE ID = ?", uid) if userInfo.Coupon > 0 { useTimes = math.Min(userInfo.Coupon, times) userInfo.Coupon -= useTimes @@ -770,27 +770,66 @@ func (sql *fishdb) useCouponAt(uid int64, times int) (int, error) { return useTimes, sql.db.Insert("buff", &userInfo) } -// 检测上限 -func (sql *fishdb) checkCanSalesFor(uid int64, sales bool) (int, error) { - residue := 0 +// 买卖上限检测 +func (sql *fishdb) checkCanSalesFor(uid int64, saleName string, salesNum int) (int, error) { sql.Lock() defer sql.Unlock() userInfo := buffInfo{ID: uid} err := sql.db.Create("buff", &userInfo) if err != nil { - return residue, err + return salesNum, err } - _ = sql.db.Find("buff", &userInfo, "where ID = "+strconv.FormatInt(uid, 10)) + _ = sql.db.Find("buff", &userInfo, "WHERE ID = ?", uid) if time.Now().Day() != time.Unix(userInfo.Duration, 0).Day() { + userInfo.Duration = time.Now().Unix() userInfo.SalesPole = 0 userInfo.BuyTing = 0 + err := sql.db.Insert("buff", &userInfo) + if err != nil { + return salesNum, err + } } - if sales && userInfo.SalesPole < 5 { - residue = 5 - userInfo.SalesPole - userInfo.SalesPole++ - } else if userInfo.BuyTing < 15 { - residue = 15 - userInfo.SalesPole + if strings.Contains(saleName, "竿") { + if userInfo.SalesPole >= 10 { + salesNum = -1 + } + } else if !checkIsWaste(saleName) { + maxSales := 30 - userInfo.BuyTing + if maxSales < 0 { + salesNum = 0 + } + if salesNum > maxSales { + salesNum = maxSales + } + } + + return salesNum, err +} + +// 更新买卖鱼上限,假定sales变量已经在 checkCanSalesFor 进行了防护 +func (sql *fishdb) updateCanSalesFor(uid int64, saleName string, sales int) error { + sql.Lock() + defer sql.Unlock() + userInfo := buffInfo{ID: uid} + err := sql.db.Create("buff", &userInfo) + if err != nil { + return err + } + _ = sql.db.Find("buff", &userInfo, "WHERE ID = ?", uid) + if strings.Contains(saleName, "竿") { userInfo.SalesPole++ + } else if !checkIsWaste(saleName) { + userInfo.BuyTing += sales + } + return sql.db.Insert("buff", &userInfo) +} + +// 检测物品是否是垃圾 +func checkIsWaste(thing string) bool { + for _, v := range wasteList { + if v == thing { + return true + } } - return residue, sql.db.Insert("buff", &userInfo) + return false } diff --git a/plugin/mcfish/pack.go b/plugin/mcfish/pack.go index c873e391f3..61a0108bf4 100644 --- a/plugin/mcfish/pack.go +++ b/plugin/mcfish/pack.go @@ -42,9 +42,9 @@ func init() { } ctx.SendChain(message.ImageBytes(pic)) }) - engine.OnRegex(`^消除绑定诅咒(\d*)$`, getdb).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { + engine.OnRegex(`^消除(绑定|宝藏)诅咒(\d*)$`, getdb).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { uid := ctx.Event.UserID - number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[1]) + number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[2]) if number == 0 { number = 1 } @@ -171,6 +171,31 @@ func init() { msg = append(msg, message.Text("-----------")) ctx.Send(msg) }) + engine.OnFullMatch("查看钓鱼规则", getdb).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) { + msg := "一款钓鱼模拟器\n----------指令----------\n" + + "- 钓鱼看板/钓鱼商店\n- 购买xxx\n- 购买xxx [数量]\n- 出售xxx\n- 出售xxx [数量]\n- 出售所有垃圾\n" + + "- 钓鱼背包\n- 装备[xx竿|三叉戟|美西螈]\n- 附魔[诱钓|海之眷顾]\n- 修复鱼竿\n- 合成[xx竿|三叉戟]\n- 消除[绑定|宝藏]诅咒\n- 消除[绑定|宝藏]诅咒 [数量]\n" + + "- 进行钓鱼\n- 进行n次钓鱼\n- " + + "当前装备概率明细\n" + + "规则V" + version + ":\n" + + "1.每日的商店价格是波动的!!如何最大化收益自己考虑一下喔\n" + + "2.装备信息:\n-> 木竿 : 耐久上限:30 均价:100 上钩概率:0.7%\n-> 铁竿 : 耐久上限:50 均价:300 上钩概率:0.2%\n-> 金竿 : 耐久上限:70 均价700 上钩概率:0.06%\n" + + "-> 钻石竿 : 耐久上限:100 均价1500 上钩概率:0.03%\n-> 下界合金竿 : 耐久上限:150 均价3100 上钩概率:0.01%\n-> 三叉戟 : 可使1次钓鱼视为3次钓鱼. 耐久上限:300 均价4000 只能合成、修复和交易\n" + + "3.附魔书信息:\n-> 诱钓 : 减少上钩时间. 均价:1000, 上钩概率:0.25%\n-> 海之眷顾 : 增加宝藏上钩概率. 均价:2500, 上钩概率:0.10%\n" + + "4.稀有物品:\n-> 唱片 : 出售物品时使用该物品使价格翻倍. 均价:3000, 上钩概率:0.01%\n" + + "-> 美西螈 : 可装备,获得隐形[钓鱼佬]buff,并让钓到除鱼竿和美西螈外的物品数量变成5,无耐久上限.不可修复/附魔,每次钓鱼消耗3条鱼. 均价:3000, 上钩概率:0.01%\n" + + "-> 海豚 : 使空竿概率变成垃圾概率. 均价:1000, 上钩概率:0.19%\n" + + "-> 宝藏诅咒 : 无法交易,每一层就会增加购买时10%价格和减少出售时10%价格(超过10层会变为倒贴钱). 上钩概率:0.25%\n-> 净化书 : 用于消除宝藏诅咒. 均价:5000, 上钩概率:0.19%\n" + + "5.鱼类信息:\n-> 鳕鱼 : 均价:10 上钩概率:0.69%\n-> 鲑鱼 : 均价:50 上钩概率:0.2%\n-> 热带鱼 : 均价:100 上钩概率:0.06%\n-> 河豚 : 均价:300 上钩概率:0.03%\n-> 鹦鹉螺 : 均价:500 上钩概率:0.01%\n-> 墨鱼 : 均价:500 上钩概率:0.01%\n" + + "6.垃圾:\n-> 均价:10 上钩概率:30%\n" + + "7.物品BUFF:\n-> 钓鱼佬 : 当背包名字含有'鱼'的物品数量超过100时激活,钓到物品概率提高至90%\n-> 修复大师 : 当背包鱼竿数量超过10时激活,修复物品时耐久百分百继承\n" + + "8.合成:\n-> 铁竿 : 3x木竿\n-> 金竿 : 3x铁竿\n-> 钻石竿 : 3x金竿\n-> 下界合金竿 : 3x钻石竿\n-> 三叉戟 : 3x下界合金竿\n注:合成成功率90%(包括梭哈),合成鱼竿的附魔等级=(附魔等级合/合成鱼竿数量)\n" + + "9.杂项:\n-> 无装备的情况下,每人最多可以购买3次100块钱的鱼竿,商店每日会上架1木竿\n-> 默认状态钓鱼上钩概率为60%(理论值!!!)\n-> 附魔的鱼竿会因附魔变得昂贵,每个附魔最高3级\n-> 三叉戟不算鱼竿,修复时可直接满耐久\n" + + "-> 鱼竿数量大于50的不能买东西;\n 鱼竿数量大于30的不能钓鱼;\n 每购/售10次鱼竿获得1层宝藏诅咒;\n 每购买20次物品将获得3次价格减半福利;\n 每钓鱼75次获得1本净化书;\n" + + " 每天可交易鱼竿10个,购买物品30件(垃圾除外)." + + ctx.Send(msg) + }) } func drawPackImage(uid int64, equipInfo equip, articles []article) (imagePicByte []byte, err error) { diff --git a/plugin/mcfish/pole.go b/plugin/mcfish/pole.go index c1ca4692a5..cad6938975 100644 --- a/plugin/mcfish/pole.go +++ b/plugin/mcfish/pole.go @@ -63,7 +63,10 @@ func init() { msg = append(msg, message.Text("[", i, "] ", info.Equip, " : 耐", info.Durable, "/修", info.Maintenance, "/诱", enchantLevel[info.Induce], "/眷顾", enchantLevel[info.Favor], "\n")) } - msg = append(msg, message.Text("————————\n输入对应序号进行装备,或回复“取消”取消")) + msg = append(msg, message.Text("————————\n")) + msg = append(msg, message.Text("- 输入对应序号进行装备\n")) + msg = append(msg, message.Text("- 输入“取消”终止本次操作\n")) + msg = append(msg, message.Text("- 鱼竿数量请使用钓鱼背包查看")) ctx.Send(msg) // 等待用户下一步选择 recv, cancel := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`^(取消|\d+)$`), zero.CheckUser(ctx.Event.UserID)).Repeat() @@ -316,13 +319,15 @@ func init() { case "诱钓": equipInfo.Induce++ if equipInfo.Induce > 3 { - equipInfo.Induce = 3 + ctx.SendChain(message.Text("诱钓等级已达到上限,你浪费了一本附魔书")) + return } number = equipInfo.Induce case "海之眷顾": equipInfo.Favor++ if equipInfo.Favor > 3 { - equipInfo.Favor = 3 + ctx.SendChain(message.Text("海之眷顾等级已达到上限,你浪费了一本附魔书")) + return } number = equipInfo.Favor default: @@ -356,12 +361,12 @@ func init() { ctx.SendChain(message.Text("[ERROR at pole.go.10]:", err)) return } - max := len(articles) - if max < 3 { + maxCount := len(articles) + if maxCount < 3 { ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("你的合成材料不足")) return } - poles := make([]equip, 0, max) + poles := make([]equip, 0, maxCount) for _, info := range articles { poleInfo := strings.Split(info.Other, "/") durable, _ := strconv.Atoi(poleInfo[0]) @@ -386,10 +391,13 @@ func init() { msg = append(msg, message.Text("[", i, "] ", info.Equip, " : 耐", info.Durable, "/修", info.Maintenance, "/诱", enchantLevel[info.Induce], "/眷顾", enchantLevel[info.Favor], "\n")) } - msg = append(msg, message.Text("————————\n输入3个序号进行合成(用空格分割),或回复“取消”取消")) + msg = append(msg, message.Text("————————\n")) + msg = append(msg, message.Text("- 输入3个序号进行合成(用空格分割)\n")) + msg = append(msg, message.Text("- 输入“取消”,终止本次合成\n")) + msg = append(msg, message.Text("- 输入“梭哈“,合成所有鱼竿")) ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, msg...)) // 等待用户下一步选择 - recv, cancel := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`^(取消|\d+ \d+ \d+)$`), zero.CheckUser(ctx.Event.UserID)).Repeat() + recv, cancel := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`^(梭哈|取消|\d+ \d+ \d+)$`), zero.CheckUser(ctx.Event.UserID)).Repeat() defer cancel() for { select { @@ -410,6 +418,14 @@ func init() { ) return } + if nextcmd == "梭哈" { + // len(list)取3的倍数,表示能够用于合成鱼竿的最大数量,note:此处未对article.Number>1的情况做处理 + for i := 3; i < (len(articles)/3)*3; i++ { + list = append(list, i) + } + check = true + break + } chooseList := strings.Split(nextcmd, " ") first, err := strconv.Atoi(chooseList[0]) if err != nil { @@ -431,8 +447,8 @@ func init() { ctx.SendChain(message.At(ctx.Event.UserID), message.Text("[0]请输入正确的序号\n", list)) continue } - if first > max || second > max || third > max { - ctx.SendChain(message.At(ctx.Event.UserID), message.Text("[", max, "]请输入正确的序号\n", list)) + if first >= maxCount || second >= maxCount || third >= maxCount { + ctx.SendChain(message.At(ctx.Event.UserID), message.Text("[", maxCount, "]请输入正确的序号\n", list)) continue } check = true @@ -442,6 +458,7 @@ func init() { } } } + upgradeNum := len(list) favorLevel := 0 induceLevel := 0 for _, index := range list { @@ -463,7 +480,7 @@ func init() { ) return } - attribute := strconv.Itoa(durationList[thingName]) + "/0/" + strconv.Itoa(induceLevel/3) + "/" + strconv.Itoa(favorLevel/3) + attribute := strconv.Itoa(durationList[thingName]) + "/0/" + strconv.Itoa(induceLevel/upgradeNum) + "/" + strconv.Itoa(favorLevel/upgradeNum) newthing := article{ Duration: time.Now().Unix(), Type: "pole", @@ -471,14 +488,19 @@ func init() { Number: 1, Other: attribute, } - err = dbdata.updateUserThingInfo(uid, newthing) - if err != nil { - ctx.SendChain(message.Text("[ERROR at pole.go.12]:", err)) - return + // 代码未对article.Number>1的情况做处理,直接生成多个Number=1的鱼竿 + for i := 0; i < upgradeNum/3; i++ { + // 使用时间戳作为主键,增加固定值避免主键冲突 + newthing.Duration += int64(i * 10) + err = dbdata.updateUserThingInfo(uid, newthing) + if err != nil { + ctx.SendChain(message.Text("[ERROR at pole.go.12]:", err)) + return + } } ctx.Send( message.ReplyWithMessage(ctx.Event.MessageID, - message.Text(thingName, "合成成功", list, "\n属性: ", attribute), + message.Text("成功合成:", upgradeNum/3, "个", thingName, "\n属性: ", attribute), ), ) }) diff --git a/plugin/mcfish/store.go b/plugin/mcfish/store.go index 85e90dc2a6..102d9477fe 100644 --- a/plugin/mcfish/store.go +++ b/plugin/mcfish/store.go @@ -70,21 +70,24 @@ func init() { engine.OnRegex(`^出售(`+strings.Join(thingList, "|")+`)\s*(\d*)$`, getdb, refreshFish).SetBlock(true).Limit(limitSet).Handle(func(ctx *zero.Ctx) { uid := ctx.Event.UserID thingName := ctx.State["regex_matched"].([]string)[1] + number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[2]) + if number == 0 || strings.Contains(thingName, "竿") { + number = 1 + } + + // 检测物品交易次数 if strings.Contains(thingName, "竿") { - times, err := dbdata.checkCanSalesFor(uid, true) + number, err := dbdata.checkCanSalesFor(uid, thingName, number) if err != nil { - ctx.SendChain(message.Text("[ERROR at store.go.75]:", err)) + ctx.SendChain(message.Text("[ERROR,查询购买资质失败]:", err)) return } - if times <= 0 { - ctx.SendChain(message.Text("出售次数已达到上限,明天再来售卖吧")) + if number <= 0 { + ctx.SendChain(message.Text("一天只能交易10把鱼竿,明天再来售卖吧")) return } } - number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[2]) - if number == 0 || strings.Contains(thingName, "竿") { - number = 1 - } + articles, err := dbdata.getUserThingInfo(uid, thingName) if err != nil { ctx.SendChain(message.Text("[ERROR at store.go.5]:", err)) @@ -107,7 +110,7 @@ func init() { "[", i, "]", info.Name, " 数量: ", info.Number, "\n")) } } - msg = append(msg, message.Text("————————\n输入对应序号进行装备,或回复“取消”取消")) + msg = append(msg, message.Text("————————\n输入对应序号进行出售,或回复“取消”取消")) ctx.Send(msg) // 等待用户下一步选择 sell := false @@ -157,7 +160,9 @@ func init() { maintenance, _ := strconv.Atoi(poleInfo[1]) induceLevel, _ := strconv.Atoi(poleInfo[2]) favorLevel, _ := strconv.Atoi(poleInfo[3]) - pice = (priceList[thingName] - (durationList[thingName] - durable) - maintenance*2 + induceLevel*600 + favorLevel*1800) * discountList[thingName] / 100 + pice = (priceList[thingName] - (durationList[thingName] - durable) - maintenance*2 + + induceLevel*600*discountList["诱钓"]/100 + + favorLevel*1800*discountList["海之眷顾"]/100) * discountList[thingName] / 100 } else { pice = priceList[thingName] * discountList[thingName] / 100 } @@ -169,7 +174,7 @@ func init() { for { select { case <-time.After(time.Second * 60): - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消钓鱼"))) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消出售"))) return case e := <-recv: nextcmd := e.Event.Message.String() @@ -304,7 +309,13 @@ func init() { logrus.Warnln(err) } } - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("出售成功,你赚到了", pice*number, msg))) + // 更新交易限制 + err = dbdata.updateCanSalesFor(uid, thingName, number) + if err != nil { + ctx.SendChain(message.Text("[ERROR,记录鱼类交易数量失败,此次交易不记录]:", err)) + } + + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("成功出售", thingName, ":", number, "个", ",你赚到了", pice*number, msg))) }) engine.OnRegex(`^出售所有垃圾`, getdb, refreshFish).SetBlock(true).Limit(limitSet).Handle(func(ctx *zero.Ctx) { uid := ctx.Event.UserID @@ -333,7 +344,7 @@ func init() { pice += (priceList[info.Name] * discountList[info.Name] / 100) * info.Number * 8 / 10 } - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("是否接受商店将以", pice, "收购全部垃圾", "?\n回答\"是\"或\"否\""))) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("是否接受回收站将以", pice, "收购全部垃圾", "?\n回答\"是\"或\"否\""))) // 等待用户下一步选择 recv, cancel1 := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`^(是|否)$`), zero.CheckUser(ctx.Event.UserID)).Repeat() defer cancel1() @@ -341,7 +352,7 @@ func init() { for { select { case <-time.After(time.Second * 60): - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消钓鱼"))) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("等待超时,取消出售垃圾"))) return case e := <-recv: nextcmd := e.Event.Message.String() @@ -375,10 +386,21 @@ func init() { return } } + err = wallet.InsertWalletOf(uid, pice) + if err != nil { + ctx.SendChain(message.Text("[ERROR,出售垃圾失败,回收站卷款跑路了]:", err)) + return + } ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("出售成功,你赚到了", pice, msg))) }) - engine.OnRegex(`^购买(`+strings.Join(thingList, "|")+`)\s*(\d*)$`, getdb, refreshFish).SetBlock(true).Limit(limitSet).Handle(func(ctx *zero.Ctx) { + engine.OnRegex(`^购买(`+strings.Join(thingList, "|")+`|初始木竿)\s*(\d*)$`, getdb, refreshFish).SetBlock(true).Limit(limitSet).Handle(func(ctx *zero.Ctx) { uid := ctx.Event.UserID + thingName := ctx.State["regex_matched"].([]string)[1] + number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[2]) + if number == 0 || strings.Contains(thingName, "竿") { + number = 1 + } + numberOfPole, err := dbdata.getNumberFor(uid, "竿") if err != nil { ctx.SendChain(message.Text("[ERROR at store.go.9.3]:", err)) @@ -388,20 +410,24 @@ func init() { ctx.SendChain(message.Text("你有", numberOfPole, "支鱼竿,大于50支的玩家不允许购买东西")) return } - buytimes, err := dbdata.checkCanSalesFor(uid, false) + + // 检测物品交易次数 + number, err = dbdata.checkCanSalesFor(uid, thingName, number) if err != nil { ctx.SendChain(message.Text("[ERROR at store.go.75]:", err)) return } - if buytimes <= 0 { - ctx.SendChain(message.Text("出售次数已达到上限,明天再来购买吧")) + if number <= 0 { + var msg string + if strings.Contains(thingName, "竿") { + msg = "一天只能交易10把鱼竿,明天再来购买吧" + } else { + msg = "一天只能购买30次物品,明天再来吧~" + } + ctx.SendChain(message.Text(msg)) return } - thingName := ctx.State["regex_matched"].([]string)[1] - number, _ := strconv.Atoi(ctx.State["regex_matched"].([]string)[2]) - if number == 0 { - number = 1 - } + thingInfos, err := dbdata.getStoreThingInfo(thingName) if err != nil { ctx.SendChain(message.Text("[ERROR at store.go.11]:", err)) @@ -443,7 +469,12 @@ func init() { maintenance, _ := strconv.Atoi(poleInfo[1]) induceLevel, _ := strconv.Atoi(poleInfo[2]) favorLevel, _ := strconv.Atoi(poleInfo[3]) - thingPice := (priceList[info.Name] - (durationList[info.Name] - durable) - maintenance*2 + induceLevel*600 + favorLevel*1800) * discountList[info.Name] / 100 + thingPice := (priceList[info.Name] - (durationList[info.Name] - durable) - maintenance*2 + + induceLevel*600*discountList["诱钓"]/100 + + favorLevel*1800*discountList["海之眷顾"]/100) * discountList[info.Name] / 100 + if strings.Contains(thingName, "初始木竿") { + thingPice = priceList["木竿"] + priceList["木竿"]*discountList["木竿"]/100 + } pice = append(pice, thingPice) } else { thingPice := priceList[info.Name] * discountList[info.Name] / 100 @@ -462,7 +493,7 @@ func init() { "[", i, "]", info.Name, " 数量:", info.Number, " 价格:", pice[i], "\n")) } } - msg = append(msg, message.Text("————————\n输入对应序号进行装备,或回复“取消”取消")) + msg = append(msg, message.Text("————————\n输入对应序号进行购买,或回复“取消”取消")) ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, msg...)) // 等待用户下一步选择 sell := false @@ -590,6 +621,9 @@ func init() { Number: 1, Other: thing.Other, } + if thingName == "初始木竿" { + newCommodity.Name = "木竿" + } } else { things, err1 := dbdata.getUserThingInfo(uid, thingName) if err1 != nil { @@ -617,6 +651,11 @@ func init() { logrus.Warnln(err) } } + // 更新交易限制 + err = dbdata.updateCanSalesFor(uid, thingName, number) + if err != nil { + ctx.SendChain(message.Text("[ERROR,记录鱼类交易数量失败,此次交易不记录]:", err)) + } ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.Text("你用", price, "购买了", number, thingName))) }) } @@ -761,6 +800,9 @@ func drawStroeInfoImage(stroeInfo []store) (picImage image.Image, err error) { induceLevel, _ := strconv.Atoi(poleInfo[2]) favorLevel, _ := strconv.Atoi(poleInfo[3]) pice = (priceList[info.Name] - (durationList[info.Name] - durable) - maintenance*2 + induceLevel*600 + favorLevel*1800) * discountList[info.Name] / 100 + if strings.Contains(name, "初始木竿") { + pice = priceList["木竿"] + priceList["木竿"]*discountList["木竿"]/100 + } } else { pice = priceList[info.Name] * discountList[info.Name] / 100 } diff --git a/plugin/midicreate/midicreate.go b/plugin/midicreate/midicreate.go index 73cf1acf2c..c18e1013d2 100644 --- a/plugin/midicreate/midicreate.go +++ b/plugin/midicreate/midicreate.go @@ -229,7 +229,7 @@ func init() { return path.Ext(ctx.Event.File.Name) == ".mid" }).SetBlock(false).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { - fileURL := ctx.GetThisGroupFileUrl(ctx.Event.File.BusID, ctx.Event.File.ID) + fileURL := ctx.GetThisGroupFileURL(ctx.Event.File.BusID, ctx.Event.File.ID) data, err := web.GetData(fileURL) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) @@ -242,12 +242,12 @@ func init() { } for i := 0; i < int(s.NumTracks()); i++ { midStr := mid2txt(data, i) + fileName := strings.ReplaceAll(cachePath+"/"+ctx.Event.File.Name, ".mid", fmt.Sprintf("-%d.txt", i)) + err := os.WriteFile(fileName, binary.StringToBytes(midStr), 0666) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return } - fileName := strings.ReplaceAll(cachePath+"/"+ctx.Event.File.Name, ".mid", fmt.Sprintf("-%d.txt", i)) - _ = os.WriteFile(fileName, binary.StringToBytes(midStr), 0666) ctx.UploadThisGroupFile(file.BOTPATH+"/"+fileName, filepath.Base(fileName), "") } }) @@ -255,7 +255,7 @@ func init() { return path.Ext(ctx.Event.File.Name) == ".txt" && strings.Contains(ctx.Event.File.Name, "midi制作") }).SetBlock(false).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { - fileURL := ctx.GetThisGroupFileUrl(ctx.Event.File.BusID, ctx.Event.File.ID) + fileURL := ctx.GetThisGroupFileURL(ctx.Event.File.BusID, ctx.Event.File.ID) data, err := web.GetData(fileURL) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/minecraftobserver/minecraftobserver.go b/plugin/minecraftobserver/minecraftobserver.go new file mode 100644 index 0000000000..65b80b5dba --- /dev/null +++ b/plugin/minecraftobserver/minecraftobserver.go @@ -0,0 +1,301 @@ +// Package minecraftobserver 通过mc服务器地址获取服务器状态信息并绘制图片发送到QQ群 +package minecraftobserver + +import ( + "fmt" + "strings" + "time" + + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + zbpCtxExt "github.com/FloatTech/zbputils/ctxext" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +const ( + name = "minecraftobserver" +) + +var ( + // 注册插件 + engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + // 默认不启动 + DisableOnDefault: false, + Brief: "Minecraft服务器状态查询/订阅", + // 详细帮助 + Help: "- mc服务器状态 [服务器IP/URI]\n" + + "- mc服务器添加订阅 [服务器IP/URI]\n" + + "- mc服务器取消订阅 [服务器IP/URI]\n" + + "- mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)" + + "-----------------------\n" + + "使用job插件设置定时, 例:" + + "记录在\"@every 1m\"触发的指令\n" + + "(机器人回答:您的下一条指令将被记录,在@@every 1m时触发)" + + "mc服务器订阅拉取", + // 插件数据存储路径 + PrivateDataFolder: name, + }).ApplySingle(zbpCtxExt.DefaultSingle) +) + +func init() { + // 状态查询 + engine.OnRegex("^[mM][cC]服务器状态 (.+)$").SetBlock(true).Handle(func(ctx *zero.Ctx) { + // 关键词查找 + addr := ctx.State["regex_matched"].([]string)[1] + resp, err := getMinecraftServerStatus(addr) + if err != nil { + ctx.Send(message.Text("服务器状态获取失败... 错误信息: ", err)) + return + } + status := resp.genServerSubscribeSchema(addr, 0) + textMsg, iconBase64 := status.generateServerStatusMsg() + var msg message.Message + if iconBase64 != "" { + msg = append(msg, message.Image(iconBase64)) + } + msg = append(msg, message.Text(textMsg)) + if id := ctx.Send(msg); id.ID() == 0 { + // logrus.Errorln(logPrefix + "Send failed") + return + } + }) + // 添加订阅 + engine.OnRegex(`^[mM][cC]服务器添加订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) { + // 关键词查找 + addr := ctx.State["regex_matched"].([]string)[1] + status, err := getMinecraftServerStatus(addr) + if err != nil { + ctx.Send(message.Text("服务器信息初始化失败,请检查服务器是否可用!\n错误信息: ", err)) + return + } + targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID) + err = dbInstance.newSubscribe(addr, targetID, targetType) + if err != nil { + ctx.Send(message.Text("订阅添加失败... 错误信息: ", err)) + return + } + // 插入数据库(首条,需要更新状态) + err = dbInstance.updateServerStatus(status.genServerSubscribeSchema(addr, 0)) + if err != nil { + ctx.Send(message.Text("服务器状态更新失败... 错误信息: ", err)) + return + } + if sid := ctx.Send(message.Text(fmt.Sprintf("服务器 %s 订阅添加成功", addr))); sid.ID() == 0 { + // logrus.Errorln(logPrefix + "Send failed") + return + } + // 成功后立即发送一次状态 + textMsg, iconBase64 := status.genServerSubscribeSchema(addr, 0).generateServerStatusMsg() + var msg message.Message + if iconBase64 != "" { + msg = append(msg, message.Image(iconBase64)) + } + msg = append(msg, message.Text(textMsg)) + if id := ctx.Send(msg); id.ID() == 0 { + // logrus.Errorln(logPrefix + "Send failed") + return + } + }) + // 删除 + engine.OnRegex(`^[mM][cC]服务器取消订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) { + addr := ctx.State["regex_matched"].([]string)[1] + // 通过群组id和服务器地址获取服务器状态 + targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID) + err := dbInstance.deleteSubscribe(addr, targetID, targetType) + if err != nil { + ctx.Send(message.Text("取消订阅失败...", fmt.Sprintf("错误信息: %v", err))) + return + } + ctx.Send(message.Text("取消订阅成功")) + }) + // 查看当前渠道的所有订阅 + engine.OnRegex(`^[mM][cC]服务器订阅列表$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) { + subList, err := dbInstance.getSubscribesByTarget(warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)) + if err != nil { + ctx.Send(message.Text("获取订阅列表失败... 错误信息: ", err)) + return + } + if len(subList) == 0 { + ctx.Send(message.Text("当前没有订阅哦")) + return + } + stringBuilder := strings.Builder{} + stringBuilder.WriteString("[订阅列表]\n") + for _, v := range subList { + stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", v.ServerAddr)) + } + if sid := ctx.Send(message.Text(stringBuilder.String())); sid.ID() == 0 { + // logrus.Errorln(logPrefix + "Send failed") + return + } + }) + // 查看全局订阅情况(仅限管理员私聊可用) + engine.OnRegex(`^[mM][cC]服务器全局订阅列表$`, zero.OnlyPrivate, zero.SuperUserPermission, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) { + subList, err := dbInstance.getAllSubscribes() + if err != nil { + ctx.Send(message.Text("获取全局订阅列表失败... 错误信息: ", err)) + return + } + if len(subList) == 0 { + ctx.Send(message.Text("当前一个订阅都没有哦")) + return + } + userID := ctx.Event.UserID + userName := ctx.CardOrNickName(userID) + msg := make(message.Message, 0) + + // 按照群组or用户分组来定 + groupSubMap := make(map[int64][]serverSubscribe) + userSubMap := make(map[int64][]serverSubscribe) + for _, v := range subList { + switch v.TargetType { + case targetTypeGroup: + groupSubMap[v.TargetID] = append(groupSubMap[v.TargetID], v) + case targetTypeUser: + userSubMap[v.TargetID] = append(userSubMap[v.TargetID], v) + default: + } + } + + // 群 + for k, v := range groupSubMap { + stringBuilder := strings.Builder{} + stringBuilder.WriteString(fmt.Sprintf("[群 %d]存在以下订阅:\n", k)) + for _, sub := range v { + stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr)) + } + msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String())) + } + // 个人 + for k, v := range userSubMap { + stringBuilder := strings.Builder{} + stringBuilder.WriteString(fmt.Sprintf("[用户 %d]存在以下订阅:\n", k)) + for _, sub := range v { + stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr)) + } + msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String())) + } + // 合并发送 + ctx.SendPrivateForwardMessage(ctx.Event.UserID, msg) + }) + // 状态变更通知,全局触发,逐个服务器检查,检查到变更则逐个发送通知 + engine.OnRegex(`^[mM][cC]服务器订阅拉取$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) { + serverList, err := dbInstance.getAllSubscribes() + if err != nil { + su := zero.BotConfig.SuperUsers[0] + // 如果订阅列表获取失败,通知管理员 + ctx.SendPrivateMessage(su, message.Text(logPrefix, "获取订阅列表失败...")) + return + } + // logrus.Debugln(logPrefix+"global get ", len(serverList), " subscribe(s)") + serverMap := make(map[string][]serverSubscribe) + for _, v := range serverList { + serverMap[v.ServerAddr] = append(serverMap[v.ServerAddr], v) + } + changedCount := 0 + for subAddr, oneServerSubList := range serverMap { + // 查询当前存储的状态 + storedStatus, sErr := dbInstance.getServerStatus(subAddr) + if sErr != nil { + // logrus.Errorln(logPrefix+fmt.Sprintf("getServerStatus ServerAddr(%s) error: ", subAddr), sErr) + continue + } + isChanged, changedNotifyMsg, sErr := singleServerScan(storedStatus) + if sErr != nil { + // logrus.Errorln(logPrefix+"singleServerScan error: ", sErr) + continue + } + if !isChanged { + continue + } + changedCount++ + // 发送变化信息 + for _, subInfo := range oneServerSubList { + time.Sleep(100 * time.Millisecond) + if subInfo.TargetType == targetTypeUser { + ctx.SendPrivateMessage(subInfo.TargetID, changedNotifyMsg) + } else if subInfo.TargetType == targetTypeGroup { + m, ok := control.Lookup(name) + if !ok { + continue + } + if !m.IsEnabledIn(subInfo.TargetID) { + continue + } + ctx.SendGroupMessage(subInfo.TargetID, changedNotifyMsg) + } + } + } + }) +} + +// singleServerScan 单个服务器状态扫描 +func singleServerScan(oldSubStatus *serverStatus) (changed bool, notifyMsg message.Message, err error) { + notifyMsg = make(message.Message, 0) + newSubStatus := &serverStatus{} + // 获取服务器状态 & 检查是否需要更新 + rawServerStatus, err := getMinecraftServerStatus(oldSubStatus.ServerAddr) + if err != nil { + // logrus.Warnln(logPrefix+"getMinecraftServerStatus error: ", err) + err = nil + // 计数器没有超限,增加计数器并跳过 + if cnt, ts := addPingServerUnreachableCounter(oldSubStatus.ServerAddr, time.Now()); cnt < pingServerUnreachableCounterThreshold && + time.Since(ts) < pingServerUnreachableCounterTimeThreshold { + // logrus.Warnln(logPrefix+"server ", oldSubStatus.ServerAddr, " unreachable, counter: ", cnt, " ts:", ts) + return + } + // 不可达计数器已经超限,则更新服务器状态 + // 深拷贝,设置PingDelay为不可达 + newSubStatus = oldSubStatus.deepCopy() + newSubStatus.PingDelay = pingDelayUnreachable + } else { + newSubStatus = rawServerStatus.genServerSubscribeSchema(oldSubStatus.ServerAddr, oldSubStatus.ID) + } + if newSubStatus == nil { + // logrus.Errorln(logPrefix + "newSubStatus is nil") + return + } + // 检查是否有订阅信息变化 + if oldSubStatus.isServerStatusSpecChanged(newSubStatus) { + // logrus.Warnf(logPrefix+"server subscribe spec changed: (%+v) -> (%+v)", oldSubStatus, newSubStatus) + changed = true + // 更新数据库 + err = dbInstance.updateServerStatus(newSubStatus) + if err != nil { + // logrus.Errorln(logPrefix+"updateServerSubscribeStatus error: ", err) + return + } + // 纯文本信息 + notifyMsg = append(notifyMsg, message.Text(formatSubStatusChangeText(oldSubStatus, newSubStatus))) + // 如果有图标变更 + if oldSubStatus.FaviconMD5 != newSubStatus.FaviconMD5 { + // 有图标变更 + notifyMsg = append(notifyMsg, message.Text("\n-----[图标变更]-----\n")) + // 旧图标 + notifyMsg = append(notifyMsg, message.Text("[旧]\n")) + if oldSubStatus.FaviconRaw != "" { + notifyMsg = append(notifyMsg, message.Image(oldSubStatus.FaviconRaw.toBase64String())) + } else { + notifyMsg = append(notifyMsg, message.Text("(空)\n")) + } + // 新图标 + notifyMsg = append(notifyMsg, message.Text("[新]\n")) + if newSubStatus.FaviconRaw != "" { + notifyMsg = append(notifyMsg, message.Image(newSubStatus.FaviconRaw.toBase64String())) + } else { + notifyMsg = append(notifyMsg, message.Text("(空)\n")) + } + } + notifyMsg = append(notifyMsg, message.Text("\n-------最新状态-------\n")) + // 服务状态 + textMsg, iconBase64 := newSubStatus.generateServerStatusMsg() + if iconBase64 != "" { + notifyMsg = append(notifyMsg, message.Image(iconBase64)) + } + notifyMsg = append(notifyMsg, message.Text(textMsg)) + } + // 逻辑到达这里,说明状态已经变更 or 无变更且服务器可达,重置不可达计数器 + resetPingServerUnreachableCounter(oldSubStatus.ServerAddr) + return +} diff --git a/plugin/minecraftobserver/minecraftobserver_test.go b/plugin/minecraftobserver/minecraftobserver_test.go new file mode 100644 index 0000000000..aa7babc968 --- /dev/null +++ b/plugin/minecraftobserver/minecraftobserver_test.go @@ -0,0 +1,127 @@ +package minecraftobserver + +import ( + "fmt" + "github.com/wdvxdr1123/ZeroBot/message" + "testing" +) + +func Test_singleServerScan(t *testing.T) { + initErr := initializeDB("data/minecraftobserver/" + dbPath) + if initErr != nil { + t.Fatalf("initializeDB() error = %v", initErr) + } + if dbInstance == nil { + t.Fatalf("initializeDB() got = %v, want not nil", dbInstance) + } + t.Run("状态变更", func(t *testing.T) { + cleanTestData(t) + newSS1 := &serverStatus{ + ServerAddr: "cn.nekoland.top", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "", + } + err := dbInstance.updateServerStatus(newSS1) + if err != nil { + t.Fatalf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1) + if err != nil { + t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err) + } + changed, msg, err := singleServerScan(newSS1) + if err != nil { + t.Fatalf("singleServerScan() error = %v", err) + } + if !changed { + t.Fatalf("singleServerScan() got = %v, want true", changed) + } + if len(msg) == 0 { + t.Fatalf("singleServerScan() got = %v, want not empty", msg) + } + fmt.Printf("msg: %v\n", msg) + }) + + t.Run("可达 -> 不可达", func(t *testing.T) { + cleanTestData(t) + newSS1 := &serverStatus{ + ServerAddr: "dx.123213213123123.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "", + PingDelay: 123, + } + err := dbInstance.updateServerStatus(newSS1) + if err != nil { + t.Fatalf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("dx.123213213123123.net", 123456, 1) + if err != nil { + t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err) + } + var msg message.Message + changed, _, err := singleServerScan(newSS1) + if err != nil { + t.Fatalf("singleServerScan() error = %v", err) + } + if changed { + t.Fatalf("singleServerScan() got = %v, want false", changed) + } + // 第二次 + changed, _, err = singleServerScan(newSS1) + if err != nil { + t.Fatalf("singleServerScan() error = %v", err) + } + if changed { + t.Fatalf("singleServerScan() got = %v, want false", changed) + } + // 第三次 + changed, msg, err = singleServerScan(newSS1) + if err != nil { + t.Fatalf("singleServerScan() error = %v", err) + } + if !changed { + t.Fatalf("singleServerScan() got = %v, want true", changed) + } + if len(msg) == 0 { + t.Fatalf("singleServerScan() got = %v, want not empty", msg) + } + fmt.Printf("msg: %v\n", msg) + + }) + + t.Run("不可达 -> 可达", func(t *testing.T) { + cleanTestData(t) + newSS1 := &serverStatus{ + ServerAddr: "cn.nekoland.top", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "", + PingDelay: pingDelayUnreachable, + } + err := dbInstance.updateServerStatus(newSS1) + if err != nil { + t.Fatalf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1) + if err != nil { + t.Fatalf("newSubscribe() error = %v", err) + } + changed, msg, err := singleServerScan(newSS1) + if err != nil { + t.Fatalf("singleServerScan() error = %v", err) + } + if !changed { + t.Fatalf("singleServerScan() got = %v, want true", changed) + } + if len(msg) == 0 { + t.Fatalf("singleServerScan() got = %v, want not empty", msg) + } + fmt.Printf("msg: %v\n", msg) + }) + +} diff --git a/plugin/minecraftobserver/model.go b/plugin/minecraftobserver/model.go new file mode 100644 index 0000000000..f4a937cf67 --- /dev/null +++ b/plugin/minecraftobserver/model.go @@ -0,0 +1,253 @@ +package minecraftobserver + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/Tnze/go-mc/chat" + "github.com/google/uuid" + "github.com/wdvxdr1123/ZeroBot/utils/helper" +) + +// ==================== +// DB Schema + +// serverStatus 服务器状态 +type serverStatus struct { + // ID 主键 + ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"` + // 服务器地址 + ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_server_addr"` + // 服务器描述 + Description string `json:"description" gorm:"column:description;default:null;type:CLOB"` + // 在线玩家 + Players string `json:"players" gorm:"column:players;default:''"` + // 版本 + Version string `json:"version" gorm:"column:version;default:''"` + // FaviconMD5 Favicon MD5 + FaviconMD5 string `json:"favicon_md5" gorm:"column:favicon_md5;default:''"` + // FaviconRaw 原始数据 + FaviconRaw icon `json:"favicon_raw" gorm:"column:favicon_raw;default:null;type:CLOB"` + // 延迟,不可达时为-1 + PingDelay int64 `json:"ping_delay" gorm:"column:ping_delay;default:-1"` + // 更新时间 + LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"` +} + +// serverSubscribe 订阅信息 +type serverSubscribe struct { + // ID 主键 + ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"` + // 服务器地址 + ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_ait"` + // 推送目标id + TargetID int64 `json:"target_id" gorm:"column:target_id;default:0;unique_index:udx_ait"` + // 类型 1:群组 2:个人 + TargetType int64 `json:"target_type" gorm:"column:target_type;default:0;unique_index:udx_ait"` + // 更新时间 + LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"` +} + +const ( + // pingDelayUnreachable 不可达 + pingDelayUnreachable = -1 +) + +// isServerStatusSpecChanged 检查是否有状态变化 +func (ss *serverStatus) isServerStatusSpecChanged(newStatus *serverStatus) (res bool) { + res = false + if ss == nil || newStatus == nil { + res = false + return + } + // 描述变化、版本变化、Favicon变化 + if ss.Description != newStatus.Description || ss.Version != newStatus.Version || ss.FaviconMD5 != newStatus.FaviconMD5 { + res = true + return + } + // 状态由不可达变为可达 or 反之 + if (ss.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable) || + (ss.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable) { + res = true + return + } + return +} + +// deepCopy 深拷贝 +func (ss *serverStatus) deepCopy() (dst *serverStatus) { + if ss == nil { + return + } + dst = &serverStatus{} + *dst = *ss + return +} + +// generateServerStatusMsg 生成服务器状态消息 +func (ss *serverStatus) generateServerStatusMsg() (msg string, iconBase64 string) { + var msgBuilder strings.Builder + if ss == nil { + return + } + msgBuilder.WriteString(ss.Description) + msgBuilder.WriteString("\n") + msgBuilder.WriteString("服务器地址:") + msgBuilder.WriteString(ss.ServerAddr) + msgBuilder.WriteString("\n") + // 版本 + msgBuilder.WriteString("版本:") + msgBuilder.WriteString(ss.Version) + msgBuilder.WriteString("\n") + // Ping + if ss.PingDelay < 0 { + msgBuilder.WriteString("Ping延迟:超时\n") + } else { + msgBuilder.WriteString("Ping延迟:") + msgBuilder.WriteString(fmt.Sprintf("%d 毫秒\n", ss.PingDelay)) + msgBuilder.WriteString("在线人数:") + msgBuilder.WriteString(ss.Players) + } + // 图标 + if ss.FaviconRaw != "" && ss.FaviconRaw.checkPNG() { + iconBase64 = ss.FaviconRaw.toBase64String() + } + msg = msgBuilder.String() + return +} + +// DB Schema End + +// ==================== +// Ping & List Response DTO + +// serverPingAndListResp 服务器状态数据传输对象 From mc server response +type serverPingAndListResp struct { + Description chat.Message + Players struct { + Max int + Online int + Sample []struct { + ID uuid.UUID + Name string + } + } + Version struct { + Name string + Protocol int + } + Favicon icon + Delay time.Duration +} + +// icon should be a PNG image that is Base64 encoded +// (without newlines: \n, new lines no longer work since 1.13) +// and prepended with "data:image/png;base64,". +type icon string + +// func (i icon) toImage() (icon image.Image, err error) { +// const prefix = "data:image/png;base64," +// if !strings.HasPrefix(string(i), prefix) { +// return nil, errors.Errorf("server icon should prepended with %s", prefix) +// } +// base64png := strings.TrimPrefix(string(i), prefix) +// r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64png)) +// icon, err = png.Decode(r) +// return +//} + +// checkPNG 检查是否为PNG +func (i icon) checkPNG() bool { + const prefix = "data:image/png;base64," + return strings.HasPrefix(string(i), prefix) +} + +// toBase64String 转换为base64字符串 +func (i icon) toBase64String() string { + return "base64://" + strings.TrimPrefix(string(i), "data:image/png;base64,") +} + +// genServerSubscribeSchema 将DTO转换为DB Schema +func (dto *serverPingAndListResp) genServerSubscribeSchema(addr string, id int64) *serverStatus { + if dto == nil { + return nil + } + faviconMD5 := md5.Sum(helper.StringToBytes(string(dto.Favicon))) + return &serverStatus{ + ID: id, + ServerAddr: addr, + Description: dto.Description.ClearString(), + Version: dto.Version.Name, + Players: fmt.Sprintf("%d/%d", dto.Players.Online, dto.Players.Max), + FaviconMD5: hex.EncodeToString(faviconMD5[:]), + FaviconRaw: dto.Favicon, + PingDelay: dto.Delay.Milliseconds(), + LastUpdate: time.Now().Unix(), + } +} + +// Ping & List Response DTO End +// ==================== + +// ==================== +// Biz Model +const ( + logPrefix = "[minecraft observer] " +) + +// warpTargetIDAndType 转换消息信息到订阅的目标ID和类型 +func warpTargetIDAndType(groupID, userID int64) (int64, int64) { + // 订阅 + var targetID int64 + var targetType int64 + if groupID == 0 { + targetType = targetTypeUser + targetID = userID + } else { + targetType = targetTypeGroup + targetID = groupID + } + return targetID, targetType +} + +// formatSubStatusChangeText 格式化状态变更文本 +func formatSubStatusChangeText(oldStatus, newStatus *serverStatus) string { + var msgBuilder strings.Builder + if oldStatus == nil || newStatus == nil { + return "" + } + // 变更通知 + msgBuilder.WriteString("[Minecraft服务器状态变更通知]\n") + // 地址 + msgBuilder.WriteString(fmt.Sprintf("服务器地址: %v\n", oldStatus.ServerAddr)) + // 描述 + if oldStatus.Description != newStatus.Description { + msgBuilder.WriteString("\n-----[描述变更]-----\n") + msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Description)) + msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Description)) + } + // 版本 + if oldStatus.Version != newStatus.Version { + msgBuilder.WriteString("\n-----[版本变更]-----\n") + msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Version)) + msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Version)) + } + // 状态由不可达变为可达,反之 + if oldStatus.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable { + msgBuilder.WriteString("\n-----[Ping延迟]-----\n") + msgBuilder.WriteString("[旧]\n超时\n") + msgBuilder.WriteString(fmt.Sprintf("[新]\n%v毫秒\n", newStatus.PingDelay)) + } + if oldStatus.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable { + msgBuilder.WriteString("\n-----[Ping延迟]-----\n") + msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v毫秒\n", oldStatus.PingDelay)) + msgBuilder.WriteString("[新]\n超时\n") + } + return msgBuilder.String() +} + +// Biz Model End +// ==================== diff --git a/plugin/minecraftobserver/ping.go b/plugin/minecraftobserver/ping.go new file mode 100644 index 0000000000..d691a56283 --- /dev/null +++ b/plugin/minecraftobserver/ping.go @@ -0,0 +1,63 @@ +package minecraftobserver + +import ( + "encoding/json" + "time" + + "github.com/RomiChan/syncx" + "github.com/Tnze/go-mc/bot" +) + +var ( + // pingServerUnreachableCounter Ping服务器不可达计数器,防止bot本体网络抖动导致误报 + pingServerUnreachableCounter = syncx.Map[string, pingServerUnreachableCounterDef]{} + // 计数器阈值 + pingServerUnreachableCounterThreshold = int64(3) + // 时间阈值 + pingServerUnreachableCounterTimeThreshold = time.Minute * 30 +) + +type pingServerUnreachableCounterDef struct { + count int64 + firstUnreachableTime time.Time +} + +func addPingServerUnreachableCounter(addr string, ts time.Time) (int64, time.Time) { + key := addr + get, ok := pingServerUnreachableCounter.Load(key) + if !ok { + pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{ + count: 1, + firstUnreachableTime: ts, + }) + return 1, ts + } + // 存在则更新,时间戳不变 + pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{ + count: get.count + 1, + firstUnreachableTime: get.firstUnreachableTime, + }) + return get.count + 1, get.firstUnreachableTime +} + +func resetPingServerUnreachableCounter(addr string) { + key := addr + pingServerUnreachableCounter.Delete(key) +} + +// getMinecraftServerStatus 获取Minecraft服务器状态 +func getMinecraftServerStatus(addr string) (*serverPingAndListResp, error) { + var s serverPingAndListResp + resp, delay, err := bot.PingAndListTimeout(addr, time.Second*5) + if err != nil { + // logrus.Errorln(logPrefix+"PingAndList error: ", err) + return nil, err + } + err = json.Unmarshal(resp, &s) + if err != nil { + // logrus.Errorln(logPrefix+"Parse json response fail: ", err) + return nil, err + } + s.Delay = delay + return &s, nil +} diff --git a/plugin/minecraftobserver/ping_test.go b/plugin/minecraftobserver/ping_test.go new file mode 100644 index 0000000000..8f44078935 --- /dev/null +++ b/plugin/minecraftobserver/ping_test.go @@ -0,0 +1,27 @@ +package minecraftobserver + +import ( + "fmt" + "testing" +) + +func Test_PingListInfo(t *testing.T) { + t.Run("normal", func(t *testing.T) { + resp, err := getMinecraftServerStatus("cn.nekoland.top") + if err != nil { + t.Fatalf("getMinecraftServerStatus() error = %v", err) + } + msg, iconBase64 := resp.genServerSubscribeSchema("cn.nekoland.top", 123456).generateServerStatusMsg() + fmt.Printf("msg: %v\n", msg) + fmt.Printf("iconBase64: %v\n", iconBase64) + }) + t.Run("不可达", func(t *testing.T) { + ss, err := getMinecraftServerStatus("dx.123213213123123.net") + if err == nil { + t.Fatalf("getMinecraftServerStatus() error = %v", err) + } + if ss != nil { + t.Fatalf("getMinecraftServerStatus() got = %v, want nil", ss) + } + }) +} diff --git a/plugin/minecraftobserver/store.go b/plugin/minecraftobserver/store.go new file mode 100644 index 0000000000..f48cca1c9d --- /dev/null +++ b/plugin/minecraftobserver/store.go @@ -0,0 +1,220 @@ +package minecraftobserver + +import ( + "errors" + "os" + "sync" + "time" + + fcext "github.com/FloatTech/floatbox/ctxext" + "github.com/jinzhu/gorm" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +const ( + dbPath = "minecraft_observer" + + targetTypeGroup = 1 + targetTypeUser = 2 +) + +var ( + // 数据库连接失败 + errDBConn = errors.New("数据库连接失败") + // 参数错误 + errParam = errors.New("参数错误") +) + +type db struct { + sdb *gorm.DB + statusLock sync.RWMutex + subscribeLock sync.RWMutex +} + +// initializeDB 初始化数据库 +func initializeDB(dbpath string) error { + if _, err := os.Stat(dbpath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbpath) + if err != nil { + return err + } + defer f.Close() + } + gdb, err := gorm.Open("sqlite3", dbpath) + if err != nil { + // logrus.Errorln(logPrefix+"initializeDB ERROR: ", err) + return err + } + gdb.AutoMigrate(&serverStatus{}, &serverSubscribe{}) + dbInstance = &db{ + sdb: gdb, + statusLock: sync.RWMutex{}, + subscribeLock: sync.RWMutex{}, + } + return nil +} + +var ( + // dbInstance 数据库实例 + dbInstance *db + // 开启并检查数据库链接 + getDB = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { + var err = initializeDB(engine.DataFolder() + dbPath) + if err != nil { + // logrus.Errorln(logPrefix+"initializeDB ERROR: ", err) + ctx.SendChain(message.Text("[mc-ob] ERROR: ", err)) + return false + } + return true + }) +) + +// 通过群组id和服务器地址获取状态 +func (d *db) getServerStatus(addr string) (*serverStatus, error) { + if d == nil { + return nil, errDBConn + } + if addr == "" { + return nil, errParam + } + var ss serverStatus + if err := d.sdb.Model(&ss).Where("server_addr = ?", addr).First(&ss).Error; err != nil { + // logrus.Errorln(logPrefix+"getServerStatus ERROR: ", err) + return nil, err + } + return &ss, nil +} + +// 更新服务器状态 +func (d *db) updateServerStatus(ss *serverStatus) (err error) { + if d == nil { + return errDBConn + } + d.statusLock.Lock() + defer d.statusLock.Unlock() + if ss == nil || ss.ServerAddr == "" { + return errParam + } + ss.LastUpdate = time.Now().Unix() + ss2 := ss.deepCopy() + if err = d.sdb.Where(&serverStatus{ServerAddr: ss.ServerAddr}).Assign(ss2).FirstOrCreate(ss).Debug().Error; err != nil { + // logrus.Errorln(logPrefix, fmt.Sprintf("updateServerStatus %v ERROR: %v", ss, err)) + return + } + return +} + +func (d *db) delServerStatus(addr string) (err error) { + if d == nil { + return errDBConn + } + if addr == "" { + return errParam + } + d.statusLock.Lock() + defer d.statusLock.Unlock() + if err = d.sdb.Where("server_addr = ?", addr).Delete(&serverStatus{}).Error; err != nil { + // logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err) + return + } + return +} + +// 新增订阅 +func (d *db) newSubscribe(addr string, targetID, targetType int64) (err error) { + if d == nil { + return errDBConn + } + if targetID == 0 || (targetType != 1 && targetType != 2) { + // logrus.Errorln(logPrefix+"newSubscribe ERROR: 参数错误 ", targetID, " ", targetType) + return errParam + } + d.subscribeLock.Lock() + defer d.subscribeLock.Unlock() + // 如果已经存在,需要报错 + existedRec := &serverSubscribe{} + err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(existedRec).Error + if err != nil && !gorm.IsRecordNotFoundError(err) { + // logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err) + return + } + if existedRec.ID != 0 { + return errors.New("已经存在的订阅") + } + ss := &serverSubscribe{ + ServerAddr: addr, + TargetID: targetID, + TargetType: targetType, + LastUpdate: time.Now().Unix(), + } + if err = d.sdb.Model(&ss).Create(ss).Error; err != nil { + // logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err) + return + } + return +} + +// 删除订阅 +func (d *db) deleteSubscribe(addr string, targetID int64, targetType int64) (err error) { + if d == nil { + return errDBConn + } + if addr == "" || targetID == 0 || targetType == 0 { + return errParam + } + d.subscribeLock.Lock() + defer d.subscribeLock.Unlock() + // 检查是否存在 + if err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(&serverSubscribe{}).Error; err != nil { + if gorm.IsRecordNotFoundError(err) { + return errors.New("未找到订阅") + } + // logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err) + return + } + + if err = d.sdb.Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).Delete(&serverSubscribe{}).Error; err != nil { + // logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err) + return + } + + // 扫描是否还有订阅,如果没有则删除服务器状态 + var cnt int + err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ?", addr).Count(&cnt).Error + if err != nil { + // logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err) + return + } + if cnt == 0 { + _ = d.delServerStatus(addr) + } + return +} + +// 获取所有订阅 +func (d *db) getAllSubscribes() (subs []serverSubscribe, err error) { + if d == nil { + return nil, errDBConn + } + subs = []serverSubscribe{} + if err = d.sdb.Find(&subs).Error; err != nil { + // logrus.Errorln(logPrefix+"getAllSubscribes ERROR: ", err) + return + } + return +} + +// 获取渠道对应的订阅列表 +func (d *db) getSubscribesByTarget(targetID, targetType int64) (subs []serverSubscribe, err error) { + if d == nil { + return nil, errDBConn + } + subs = []serverSubscribe{} + if err = d.sdb.Model(&serverSubscribe{}).Where("target_id = ? and target_type = ?", targetID, targetType).Find(&subs).Error; err != nil { + // logrus.Errorln(logPrefix+"getSubscribesByTarget ERROR: ", err) + return + } + return +} diff --git a/plugin/minecraftobserver/store_test.go b/plugin/minecraftobserver/store_test.go new file mode 100644 index 0000000000..d96c82d132 --- /dev/null +++ b/plugin/minecraftobserver/store_test.go @@ -0,0 +1,317 @@ +package minecraftobserver + +import ( + "errors" + "fmt" + "github.com/jinzhu/gorm" + "testing" +) + +func cleanTestData(t *testing.T) { + err := dbInstance.sdb.Delete(&serverStatus{}).Where("id > 0").Error + if err != nil { + t.Fatalf("cleanTestData() error = %v", err) + } + err = dbInstance.sdb.Delete(&serverSubscribe{}).Where("id > 0").Error + if err != nil { + t.Fatalf("cleanTestData() error = %v", err) + } +} + +func Test_DAO(t *testing.T) { + initErr := initializeDB("data/minecraftobserver/" + dbPath) + if initErr != nil { + t.Fatalf("initializeDB() error = %v", initErr) + } + if dbInstance == nil { + t.Fatalf("initializeDB() got = %v, want not nil", dbInstance) + } + t.Run("insert", func(t *testing.T) { + cleanTestData(t) + newSS1 := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + } + newSS2 := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.8", + FaviconMD5: "1234567", + } + err := dbInstance.updateServerStatus(newSS1) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + err = dbInstance.updateServerStatus(newSS2) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + + // check insert + queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net") + if err != nil { + t.Fatalf("getServerStatus() error = %v", err) + } + if queryResult == nil { + t.Fatalf("getServerStatus() got = %v, want not nil", queryResult) + } + if queryResult.Version != "1.16.8" { + t.Fatalf("getServerStatus() got = %v, want 1.16.8", queryResult.Version) + } + + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeUser) + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + // check insert + res, err := dbInstance.getAllSubscribes() + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + if len(res) != 2 { + t.Fatalf("getAllServer() got = %v, want 2", len(res)) + } + // 检查是否符合预期 + if res[0].ServerAddr != "dx.zhaomc.net" { + t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[0].ServerAddr) + } + if res[0].TargetType != targetTypeGroup { + t.Fatalf("getAllServer() got = %v, want %v", res[0].TargetType, targetTypeGroup) + } + if res[1].ServerAddr != "dx.zhaomc.net" { + t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[1].ServerAddr) + } + if res[1].TargetType != targetTypeUser { + t.Fatalf("getAllServer() got = %v, want %v", res[1].TargetType, targetTypeUser) + } + + // 顺带验证一下 byTarget + res2, err := dbInstance.getSubscribesByTarget(123456, targetTypeGroup) + if err != nil { + t.Fatalf("getSubscribesByTarget() error = %v", err) + } + if len(res2) != 1 { + t.Fatalf("getSubscribesByTarget() got = %v, want 1", len(res2)) + } + + }) + // 重复添加订阅 + t.Run("insert dup", func(t *testing.T) { + cleanTestData(t) + newSS := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + } + err := dbInstance.updateServerStatus(newSS) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err == nil { + t.Fatalf("getAllServer() error = %v", err) + } + fmt.Printf("insert dup error: %+v", err) + }) + + t.Run("update", func(t *testing.T) { + cleanTestData(t) + newSS := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + } + err := dbInstance.updateServerStatus(newSS) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + err = dbInstance.updateServerStatus(&serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "更新测试", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + }) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + // check update + queryResult2, err := dbInstance.getServerStatus("dx.zhaomc.net") + if err != nil { + t.Errorf("getAllServer() error = %v", err) + } + if queryResult2.Description != "更新测试" { + t.Errorf("getAllServer() got = %v, want 更新测试", queryResult2.Description) + } + }) + t.Run("delete status", func(t *testing.T) { + cleanTestData(t) + newSS := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + } + err := dbInstance.updateServerStatus(newSS) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + // check insert + queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net") + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + if queryResult == nil { + t.Fatalf("getAllServer() got = %v, want not nil", queryResult) + } + err = dbInstance.delServerStatus("dx.zhaomc.net") + if err != nil { + t.Fatalf("deleteServerStatus() error = %v", err) + } + // check delete + _, err = dbInstance.getServerStatus("dx.zhaomc.net") + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("getAllServer() error = %v", err) + } + + }) + + // 删除订阅 + t.Run("delete subscribe", func(t *testing.T) { + cleanTestData(t) + newSS := &serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + } + err := dbInstance.updateServerStatus(newSS) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("deleteSubscribe() error = %v", err) + } + // check delete + _, err = dbInstance.getServerStatus("dx.zhaomc.net") + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("getAllServer() error = %v", err) + } + }) + + // 重复删除订阅 + t.Run("delete subscribe dup", func(t *testing.T) { + cleanTestData(t) + err := dbInstance.updateServerStatus(&serverStatus{ + ServerAddr: "dx.zhaomc.net", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + }) + if err != nil { + t.Errorf("upsertServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("newSubscribe() error = %v", err) + } + + err = dbInstance.newSubscribe("dx.zhaomc.net123", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("newSubscribe() error = %v", err) + } + err = dbInstance.updateServerStatus(&serverStatus{ + ServerAddr: "dx.zhaomc.net123", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + }) + if err != nil { + t.Fatalf("updateServerStatus() error = %v", err) + } + err = dbInstance.newSubscribe("dx.zhaomc.net4567", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("newSubscribe() error = %v", err) + } + err = dbInstance.updateServerStatus(&serverStatus{ + ServerAddr: "dx.zhaomc.net4567", + Description: "测试服务器", + Players: "1/20", + Version: "1.16.5", + FaviconMD5: "1234567", + }) + if err != nil { + t.Fatalf("updateServerStatus() error = %v", err) + } + + // 检查是不是3个 + allSub, err := dbInstance.getAllSubscribes() + if err != nil { + t.Fatalf("getAllSubscribes() error = %v", err) + } + if len(allSub) != 3 { + t.Fatalf("getAllSubscribes() got = %v, want 3", len(allSub)) + } + err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err != nil { + t.Fatalf("deleteSubscribe() error = %v", err) + } + err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup) + if err == nil { + t.Fatalf("deleteSubscribe() error = %v", err) + } + fmt.Println("delete dup error: ", err) + + // 检查其他的没有被删 + allSub, err = dbInstance.getAllSubscribes() + if err != nil { + t.Fatalf("getAllSubscribes() error = %v", err) + } + // 检查是否符合预期 + if len(allSub) != 2 { + t.Fatalf("getAllSubscribes() got = %v, want 2", len(allSub)) + } + // 状态 + _, err = dbInstance.getServerStatus("dx.zhaomc.net") + if !gorm.IsRecordNotFoundError(err) { + t.Fatalf("getAllServer() error = %v", err) + } + status1, err := dbInstance.getServerStatus("dx.zhaomc.net123") + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + status2, err := dbInstance.getServerStatus("dx.zhaomc.net4567") + if err != nil { + t.Fatalf("getAllServer() error = %v", err) + } + if status1 == nil || status2 == nil { + t.Fatalf("getAllServer() want not nil") + } + + }) +} diff --git a/plugin/moegoe/main.go b/plugin/moegoe/main.go deleted file mode 100644 index fa8038edc1..0000000000 --- a/plugin/moegoe/main.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package moegoe 日韩中 VITS 模型拟声 -package moegoe - -import ( - "crypto/md5" - "encoding/hex" - "fmt" - "net/url" - - zero "github.com/wdvxdr1123/ZeroBot" - "github.com/wdvxdr1123/ZeroBot/message" - - "github.com/FloatTech/AnimeAPI/tts/genshin" - "github.com/FloatTech/floatbox/binary" - "github.com/FloatTech/floatbox/file" - ctrl "github.com/FloatTech/zbpctrl" - "github.com/FloatTech/zbputils/control" - "github.com/FloatTech/zbputils/ctxext" -) - -var 原 = newapikeystore("./data/tts/o.txt") - -func init() { - en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ - DisableOnDefault: false, - Brief: "日韩中 VITS 模型拟声", - Help: "- 让[空|荧|派蒙|纳西妲|阿贝多|温迪|枫原万叶|钟离|荒泷一斗|八重神子|艾尔海森|提纳里|迪希雅|卡维|宵宫|莱依拉|赛诺|诺艾尔|托马|凝光|莫娜|北斗|神里绫华|雷电将军|芭芭拉|鹿野院平藏|五郎|迪奥娜|凯亚|安柏|班尼特|琴|柯莱|夜兰|妮露|辛焱|珐露珊|魈|香菱|达达利亚|砂糖|早柚|云堇|刻晴|丽莎|迪卢克|烟绯|重云|珊瑚宫心海|胡桃|可莉|流浪者|久岐忍|神里绫人|甘雨|戴因斯雷布|优菈|菲谢尔|行秋|白术|九条裟罗|雷泽|申鹤|迪娜泽黛|凯瑟琳|多莉|坎蒂丝|萍姥姥|罗莎莉亚|留云借风真君|绮良良|瑶瑶|七七|奥兹|米卡|夏洛蒂|埃洛伊|博士|女士|大慈树王|三月七|娜塔莎|希露瓦|虎克|克拉拉|丹恒|希儿|布洛妮娅|瓦尔特|杰帕德|佩拉|姬子|艾丝妲|白露|星|穹|桑博|伦纳德|停云|罗刹|卡芙卡|彦卿|史瓦罗|螺丝咕姆|阿兰|银狼|素裳|丹枢|黑塔|景元|帕姆|可可利亚|半夏|符玄|公输师傅|奥列格|青雀|大毫|青镞|费斯曼|绿芙蓉|镜流|信使|丽塔|失落迷迭|缭乱星棘|伊甸|伏特加女孩|狂热蓝调|莉莉娅|萝莎莉娅|八重樱|八重霞|卡莲|第六夜想曲|卡萝尔|姬子|极地战刃|布洛妮娅|次生银翼|理之律者|真理之律者|迷城骇兔|希儿|魇夜星渊|黑希儿|帕朵菲莉丝|天元骑英|幽兰黛尔|德丽莎|月下初拥|朔夜观星|暮光骑士|明日香|李素裳|格蕾修|梅比乌斯|渡鸦|人之律者|爱莉希雅|爱衣|天穹游侠|琪亚娜|空之律者|终焉之律者|薪炎之律者|云墨丹心|符华|识之律者|维尔薇|始源之律者|芽衣|雷之律者|苏莎娜|阿波尼亚|陆景和|莫弈|夏彦|左然|标贝]说(中文)", - }).ApplySingle(ctxext.DefaultSingle) - en.OnRegex("^让(空|荧|派蒙|纳西妲|阿贝多|温迪|枫原万叶|钟离|荒泷一斗|八重神子|艾尔海森|提纳里|迪希雅|卡维|宵宫|莱依拉|赛诺|诺艾尔|托马|凝光|莫娜|北斗|神里绫华|雷电将军|芭芭拉|鹿野院平藏|五郎|迪奥娜|凯亚|安柏|班尼特|琴|柯莱|夜兰|妮露|辛焱|珐露珊|魈|香菱|达达利亚|砂糖|早柚|云堇|刻晴|丽莎|迪卢克|烟绯|重云|珊瑚宫心海|胡桃|可莉|流浪者|久岐忍|神里绫人|甘雨|戴因斯雷布|优菈|菲谢尔|行秋|白术|九条裟罗|雷泽|申鹤|迪娜泽黛|凯瑟琳|多莉|坎蒂丝|萍姥姥|罗莎莉亚|留云借风真君|绮良良|瑶瑶|七七|奥兹|米卡|夏洛蒂|埃洛伊|博士|女士|大慈树王|三月七|娜塔莎|希露瓦|虎克|克拉拉|丹恒|希儿|布洛妮娅|瓦尔特|杰帕德|佩拉|姬子|艾丝妲|白露|星|穹|桑博|伦纳德|停云|罗刹|卡芙卡|彦卿|史瓦罗|螺丝咕姆|阿兰|银狼|素裳|丹枢|黑塔|景元|帕姆|可可利亚|半夏|符玄|公输师傅|奥列格|青雀|大毫|青镞|费斯曼|绿芙蓉|镜流|信使|丽塔|失落迷迭|缭乱星棘|伊甸|伏特加女孩|狂热蓝调|莉莉娅|萝莎莉娅|八重樱|八重霞|卡莲|第六夜想曲|卡萝尔|姬子|极地战刃|布洛妮娅|次生银翼|理之律者|真理之律者|迷城骇兔|希儿|魇夜星渊|黑希儿|帕朵菲莉丝|天元骑英|幽兰黛尔|德丽莎|月下初拥|朔夜观星|暮光骑士|明日香|李素裳|格蕾修|梅比乌斯|渡鸦|人之律者|爱莉希雅|爱衣|天穹游侠|琪亚娜|空之律者|终焉之律者|薪炎之律者|云墨丹心|符华|识之律者|维尔薇|始源之律者|芽衣|雷之律者|苏莎娜|阿波尼亚|陆景和|莫弈|夏彦|左然|标贝)说([\\s\u4e00-\u9fa5\\pP]+)$").Limit(ctxext.LimitByGroup).SetBlock(true). - Handle(func(ctx *zero.Ctx) { - if 原.k == "" { - return - } - text := ctx.State["regex_matched"].([]string)[2] - name := ctx.State["regex_matched"].([]string)[1] - rec := fmt.Sprintf(genshin.CNAPI, url.QueryEscape(name), url.QueryEscape(text), url.QueryEscape(原.k)) - b := md5.Sum(binary.StringToBytes(rec)) - fn := hex.EncodeToString(b[:]) - fp := "data/tts/" + fn - if file.IsNotExist(fp) { - if file.DownloadTo(rec, fp) != nil { - return - } - } - rec = "file:///" + file.BOTPATH + "/" + fp - ctx.SendChain(message.Record(rec)) - }) -} diff --git a/plugin/moegoe/model.go b/plugin/moegoe/model.go deleted file mode 100644 index cd09b72acd..0000000000 --- a/plugin/moegoe/model.go +++ /dev/null @@ -1,24 +0,0 @@ -package moegoe - -import ( - "os" - - "github.com/FloatTech/floatbox/binary" - "github.com/FloatTech/floatbox/file" -) - -type apikeystore struct { - k string - p string -} - -func newapikeystore(p string) (s apikeystore) { - s.p = p - if file.IsExist(p) { - data, err := os.ReadFile(p) - if err == nil { - s.k = binary.BytesToString(data) - } - } - return -} diff --git a/plugin/movies/main.go b/plugin/movies/main.go new file mode 100644 index 0000000000..b17cbb4631 --- /dev/null +++ b/plugin/movies/main.go @@ -0,0 +1,435 @@ +// Package movies 电影查询 +package movies + +import ( + "encoding/json" + "image" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "sync" + "time" + + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/floatbox/web" + "github.com/FloatTech/gg" + "github.com/FloatTech/imgfactory" + "github.com/FloatTech/rendercard" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/img/text" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +const ( + apiURL = "https://m.maoyan.com/ajax/" + ua = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36" +) + +var ( + mu sync.RWMutex + todayPic = make([][]byte, 2) + lasttime time.Time + en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "电影查询", + Help: "- 今日电影\n" + + "- 预售电影", + PrivateDataFolder: "movies", + }) +) + +func init() { + en.OnFullMatch("今日电影").SetBlock(true).Handle(func(ctx *zero.Ctx) { + if todayPic != nil && time.Since(lasttime) < 12*time.Hour { + ctx.SendChain(message.ImageBytes(todayPic[0])) + return + } + lasttime = time.Now() + movieComingList, err := getMovieList("今日电影") + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + return + } + if len(movieComingList) == 0 { + ctx.SendChain(message.Text("没有今日电影")) + return + } + pic, err := drawOnListPic(movieComingList) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + return + } + todayPic[0] = pic + ctx.SendChain(message.ImageBytes(pic)) + }) + en.OnFullMatch("预售电影").SetBlock(true).Handle(func(ctx *zero.Ctx) { + if todayPic[1] != nil && time.Since(lasttime) < 12*time.Hour { + ctx.SendChain(message.ImageBytes(todayPic[1])) + return + } + lasttime = time.Now() + movieComingList, err := getMovieList("预售电影") + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + return + } + if len(movieComingList) == 0 { + ctx.SendChain(message.Text("没有预售电影")) + return + } + pic, err := drawComListPic(movieComingList) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + return + } + todayPic[1] = pic + ctx.SendChain(message.ImageBytes(pic)) + }) +} + +type movieInfo struct { + ID int64 `json:"id"` // 电影ID + Img string `json:"img"` // 海报 + + Nm string `json:"nm"` // 名称 + + Dir string `json:"dir"` // 导演 + Star string `json:"star"` // 演员 + + OriLang string `json:"oriLang"` // 原语言 + Cat string `json:"cat"` // 类型 + + Version string `json:"version"` // 电影格式 + Rt string `json:"rt"` // 上映时间 + + ShowInfo string `json:"showInfo"` // 今日上映信息 + ComingTitle string `json:"comingTitle"` // 预售信息 + + Sc float64 `json:"sc"` // 评分 + Wish int64 `json:"wish"` // 观看人数 + Watched int64 `json:"watched"` // 观看数 +} +type movieOnList struct { + MovieList []movieInfo `json:"movieList"` +} +type comingList struct { + MovieList []movieInfo `json:"coming"` +} +type movieShow struct { + MovieInfo movieInfo `json:"detailMovie"` +} + +type cardInfo struct { + Avatar image.Image + TopLeftText string + BottomLeftText []string + RightText string + Rank string +} + +func getMovieList(mode string) (movieList []movieInfo, err error) { + var data []byte + if mode == "今日电影" { + data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"movieOnInfoList", "", "GET", ua, nil) + if err != nil { + return + } + var parsed movieOnList + err = json.Unmarshal(data, &parsed) + if err != nil { + return + } + movieList = parsed.MovieList + } else { + data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"comingList?token=", "", "GET", ua, nil) + if err != nil { + return + } + var parsed comingList + err = json.Unmarshal(data, &parsed) + if err != nil { + return + } + movieList = parsed.MovieList + } + if len(movieList) == 0 { + return + } + for i, info := range movieList { + movieID := strconv.FormatInt(info.ID, 10) + data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"detailmovie?movieId="+movieID, "", "GET", ua, nil) + if err != nil { + return + } + var movieInfo movieShow + err = json.Unmarshal(data, &movieInfo) + if err != nil { + return + } + if mode != "今日电影" { + movieInfo.MovieInfo.ComingTitle = movieList[i].ComingTitle + } + movieList[i] = movieInfo.MovieInfo + } + // 整理数据,进行排序 + sort.Slice(movieList, func(i, j int) bool { + if movieList[i].Sc != movieList[j].Sc { + return movieList[i].Sc > movieList[j].Sc + } + if mode == "今日电影" { + return movieList[i].Watched > movieList[j].Watched + } + return movieList[i].Wish > movieList[j].Wish + }) + return movieList, nil +} +func drawOnListPic(lits []movieInfo) (data []byte, err error) { + rankinfo := make([]*cardInfo, len(lits)) + + wg := &sync.WaitGroup{} + wg.Add(len(lits)) + for i := 0; i < len(lits); i++ { + go func(i int) { + info := lits[i] + defer wg.Done() + img, err := avatar(&info) + if err != nil { + return + } + movieType := "2D" + if info.Version != "" { + movieType = info.Version + } + watched := "" + switch { + case info.Watched > 100000000: + watched = strconv.FormatFloat(float64(info.Watched)/100000000, 'f', 2, 64) + "亿" + case info.Watched > 10000: + watched = strconv.FormatFloat(float64(info.Watched)/10000, 'f', 2, 64) + "万" + default: + watched = strconv.FormatInt(info.Watched, 10) + } + rankinfo[i] = &cardInfo{ + TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")", + BottomLeftText: []string{ + "导演:" + info.Dir, + "演员:" + info.Star, + "标签:" + info.Cat, + "语言: " + info.OriLang + " 类型: " + movieType, + "上映时间: " + info.Rt, + }, + RightText: watched + "人已看", + Avatar: img, + Rank: strconv.FormatFloat(info.Sc, 'f', 1, 64), + } + }(i) + } + wg.Wait() + fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true) + if err != nil { + return + } + img, err := drawRankingCard(fontbyte, "今日电影", rankinfo) + if err != nil { + return + } + data, err = imgfactory.ToBytes(img) + return +} + +func drawComListPic(lits []movieInfo) (data []byte, err error) { + rankinfo := make([]*cardInfo, len(lits)) + + wg := &sync.WaitGroup{} + wg.Add(len(lits)) + for i := 0; i < len(lits); i++ { + go func(i int) { + info := lits[i] + defer wg.Done() + img, err := avatar(&info) + if err != nil { + return + } + movieType := "2D" + if info.Version != "" { + movieType = info.Version + } + wish := "" + switch { + case info.Wish > 100000000: + wish = strconv.FormatFloat(float64(info.Wish)/100000000, 'f', 2, 64) + "亿" + case info.Wish > 10000: + wish = strconv.FormatFloat(float64(info.Wish)/10000, 'f', 2, 64) + "万" + default: + wish = strconv.FormatInt(info.Wish, 10) + } + rankinfo[i] = &cardInfo{ + TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")", + BottomLeftText: []string{ + "导演:" + info.Dir, + "演员:" + info.Star, + "标签:" + info.Cat, + "语言: " + info.OriLang + " 类型: " + movieType, + "上映时间: " + info.Rt + " 播放时间: " + info.ComingTitle, + }, + RightText: wish + "人期待", + Avatar: img, + Rank: strconv.Itoa(i + 1), + } + }(i) + } + wg.Wait() + fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true) + if err != nil { + return + } + img, err := drawRankingCard(fontbyte, "预售电影", rankinfo) + if err != nil { + return + } + data, err = imgfactory.ToBytes(img) + return +} + +func drawRankingCard(fontdata []byte, title string, rankinfo []*cardInfo) (img image.Image, err error) { + line := len(rankinfo) + const lineh = 130 + const w = 800 + h := 64 + (lineh+14)*line + 20 - 14 + canvas := gg.NewContext(w, h) + canvas.SetRGBA255(255, 255, 255, 255) + canvas.Clear() + + cardh, cardw := lineh, 770 + cardspac := 14 + hspac, wspac := 64.0, 16.0 + r := 16.0 + + wg := &sync.WaitGroup{} + wg.Add(line) + cardimgs := make([]image.Image, line) + for i := 0; i < line; i++ { + go func(i int) { + defer wg.Done() + card := gg.NewContext(w, cardh) + + card.NewSubPath() + + card.MoveTo(wspac+float64(cardh)/2, 0) + + card.LineTo(wspac+float64(cardw)-r, 0) + card.DrawArc(wspac+float64(cardw)-r, r, r, gg.Radians(-90), gg.Radians(0)) + card.LineTo(wspac+float64(cardw), float64(cardh)-r) + card.DrawArc(wspac+float64(cardw)-r, float64(cardh)-r, r, gg.Radians(0), gg.Radians(90)) + card.LineTo(wspac+float64(cardh)/2, float64(cardh)) + card.DrawArc(wspac+r, float64(cardh)-r, r, gg.Radians(90), gg.Radians(180)) + card.LineTo(wspac, r) + card.DrawArc(wspac+r, r, r, gg.Radians(180), gg.Radians(270)) + + card.ClosePath() + + card.ClipPreserve() + + avatar := rankinfo[i].Avatar + + PicH := cardh - 20 + picW := int(float64(avatar.Bounds().Dx()) * float64(PicH) / float64(avatar.Bounds().Dy())) + card.DrawImageAnchored(imgfactory.Size(avatar, picW, PicH).Image(), int(wspac)+10+picW/2, cardh/2, 0.5, 0.5) + + card.ResetClip() + card.SetRGBA255(0, 0, 0, 127) + card.Stroke() + + card.SetRGBA255(240, 210, 140, 200) + card.DrawRoundedRectangle(wspac+float64(cardw-8-250), (float64(cardh)-50)/2, 250, 50, 25) + card.Fill() + card.SetRGB255(rendercard.RandJPColor()) + card.DrawRoundedRectangle(wspac+float64(cardw-8-60), (float64(cardh)-50)/2, 60, 50, 25) + card.Fill() + cardimgs[i] = card.Image() + }(i) + } + + canvas.SetRGBA255(0, 0, 0, 255) + err = canvas.ParseFontFace(fontdata, 32) + if err != nil { + return + } + canvas.DrawStringAnchored(title, w/2, 64/2, 0.5, 0.5) + + err = canvas.ParseFontFace(fontdata, 22) + if err != nil { + return + } + wg.Wait() + for i := 0; i < line; i++ { + canvas.DrawImageAnchored(cardimgs[i], w/2, int(hspac)+((cardh+cardspac)*i), 0.5, 0) + canvas.DrawStringAnchored(rankinfo[i].TopLeftText, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*3/16), 0, 0.5) + } + + // canvas.SetRGBA255(63, 63, 63, 255) + err = canvas.ParseFontFace(fontdata, 14) + if err != nil { + return + } + for i := 0; i < line; i++ { + for j, text := range rankinfo[i].BottomLeftText { + canvas.DrawStringAnchored(text, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*6/16)+float64(j*16), 0, 0.5) + } + } + canvas.SetRGBA255(0, 0, 0, 255) + err = canvas.ParseFontFace(fontdata, 20) + if err != nil { + return + } + for i := 0; i < line; i++ { + canvas.DrawStringAnchored(rankinfo[i].RightText, w-wspac-8-60-8, hspac+float64((cardspac+cardh)*i+cardh/2), 1, 0.5) + } + + canvas.SetRGBA255(255, 255, 255, 255) + err = canvas.ParseFontFace(fontdata, 28) + if err != nil { + return + } + for i := 0; i < line; i++ { + canvas.DrawStringAnchored(rankinfo[i].Rank, w-wspac-8-30, hspac+float64((cardspac+cardh)*i+cardh/2), 0.5, 0.5) + } + + img = canvas.Image() + return +} + +// avatar 获取电影海报,图片大且多,存本地增加响应速度 +func avatar(movieInfo *movieInfo) (pic image.Image, err error) { + mu.Lock() + defer mu.Unlock() + + aimgfile := filepath.Join(en.DataFolder(), movieInfo.Nm+"("+strconv.FormatInt(movieInfo.ID, 10)+").jpg") + if file.IsNotExist(aimgfile) { + err = file.DownloadTo(movieInfo.Img, aimgfile) + if err != nil { + return urlToImg(movieInfo.Img) + } + } + f, err := os.Open(filepath.Join(file.BOTPATH, aimgfile)) + if err != nil { + return urlToImg(movieInfo.Img) + } + defer f.Close() + pic, _, err = image.Decode(f) + return +} + +func urlToImg(url string) (img image.Image, err error) { + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + img, _, err = image.Decode(resp.Body) + return +} diff --git a/plugin/music/selecter.go b/plugin/music/selecter.go index d4b40a933a..5f43844404 100644 --- a/plugin/music/selecter.go +++ b/plugin/music/selecter.go @@ -21,6 +21,10 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" ) +var ( + longZhuURL = "https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg=%v" +) + func init() { control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, @@ -29,7 +33,8 @@ func init() { "- 网易点歌[xxx]\n" + "- 酷我点歌[xxx]\n" + "- 酷狗点歌[xxx]\n" + - "- 咪咕点歌[xxx]", + "- 咪咕点歌[xxx]\n" + + "- qq点歌[xxx]\n", }).OnRegex(`^(.{0,2})点歌\s?(.{1,25})$`).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { // switch 平台 @@ -42,14 +47,39 @@ func init() { ctx.SendChain(kugou(ctx.State["regex_matched"].([]string)[2])) case "网易": ctx.SendChain(cloud163(ctx.State["regex_matched"].([]string)[2])) - default: // 默认 QQ音乐 + case "qq": ctx.SendChain(qqmusic(ctx.State["regex_matched"].([]string)[2])) + default: // 默认聚合点歌 + ctx.SendChain(longzhu(ctx.State["regex_matched"].([]string)[2])) } }) } +// longzhu 聚合平台 +func longzhu(keyword string) message.Segment { + data, _ := web.GetData(fmt.Sprintf(longZhuURL, url.QueryEscape(keyword))) + // 假设 data 是包含整个 JSON 数组的字节切片 + results := gjson.ParseBytes(data).Array() + for _, result := range results { + if strings.Contains(strings.ToLower(result.Get("title").String()), strings.ToLower(keyword)) { + if musicURL := result.Get("full_track").String(); musicURL != "" { + return message.Record(musicURL) + } + } + } + + results = gjson.GetBytes(data, "#.full_track").Array() + if len(results) > 0 { + if musicURL := results[0].String(); musicURL != "" { + return message.Record(musicURL) + } + } + + return message.Text("点歌失败, 找不到 ", keyword, " 的相关结果") +} + // migu 返回咪咕音乐卡片 -func migu(keyword string) message.MessageSegment { +func migu(keyword string) message.Segment { headers := http.Header{ "Cookie": []string{"audioplayer_exist=1; audioplayer_open=0; migu_cn_cookie_id=3ad476db-f021-4bda-ab91-c485ac3d56a0; Hm_lvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; Hm_lpvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; WT_FPC=id=279ef92eaf314cbb8d01671116477485:lv=1671119583092:ss=1671116477485"}, "csrf": []string{"LWKACV45JSQ"}, @@ -75,7 +105,7 @@ func migu(keyword string) message.MessageSegment { } // kuwo 返回酷我音乐卡片 -func kuwo(keyword string) message.MessageSegment { +func kuwo(keyword string) message.Segment { headers := http.Header{ "Cookie": []string{"Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1610284708,1610699237; _ga=GA1.2.1289529848.1591618534; kw_token=LWKACV45JSQ; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1610699468; _gid=GA1.2.1868980507.1610699238; _gat=1"}, "csrf": []string{"LWKACV45JSQ"}, @@ -109,7 +139,7 @@ func kuwo(keyword string) message.MessageSegment { } // kugou 返回酷狗音乐卡片 -func kugou(keyword string) message.MessageSegment { +func kugou(keyword string) message.Segment { stamp := time.Now().UnixNano() / 1e6 hash := md5str( fmt.Sprintf( @@ -163,7 +193,7 @@ func kugou(keyword string) message.MessageSegment { } // cloud163 返回网易云音乐卡片 -func cloud163(keyword string) (msg message.MessageSegment) { +func cloud163(keyword string) (msg message.Segment) { requestURL := "http://music.163.com/api/search/get/web?type=1&limit=1&s=" + url.QueryEscape(keyword) data, err := web.GetData(requestURL) if err != nil { @@ -175,7 +205,7 @@ func cloud163(keyword string) (msg message.MessageSegment) { } // qqmusic 返回QQ音乐卡片 -func qqmusic(keyword string) (msg message.MessageSegment) { +func qqmusic(keyword string) (msg message.Segment) { requestURL := "https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?platform=yqq.json&key=" + url.QueryEscape(keyword) data, err := web.RequestDataWith(web.NewDefaultClient(), requestURL, "GET", "", web.RandUA(), nil) if err != nil { diff --git a/plugin/nativesetu/data.go b/plugin/nativesetu/data.go index e9a37c66e4..f1d50482d9 100644 --- a/plugin/nativesetu/data.go +++ b/plugin/nativesetu/data.go @@ -24,15 +24,15 @@ type setuclass struct { Path string `db:"path"` // Path 图片路径 } -var ns = &nsetu{db: &sql.Sqlite{}} +var ns nsetu type nsetu struct { - db *sql.Sqlite + db sql.Sqlite mu sync.RWMutex } func (n *nsetu) List() (l []string) { - if file.IsExist(n.db.DBPath) { + if file.IsExist(dbpath) { var err error l, err = n.db.ListTables() if err != nil { @@ -46,7 +46,7 @@ func (n *nsetu) scanall(path string) error { model := &setuclass{} root := os.DirFS(path) _ = n.db.Close() - _ = os.Remove(n.db.DBPath) + _ = os.Remove(dbpath) err := n.db.Open(time.Hour) if err != nil { return err diff --git a/plugin/nativesetu/main.go b/plugin/nativesetu/main.go index 3f98341b48..c791d8d568 100644 --- a/plugin/nativesetu/main.go +++ b/plugin/nativesetu/main.go @@ -13,6 +13,7 @@ import ( fcext "github.com/FloatTech/floatbox/ctxext" "github.com/FloatTech/floatbox/file" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" @@ -20,6 +21,7 @@ import ( var ( setupath = "/tmp" // 绝对路径,图片根目录 + dbpath = "" ) func init() { @@ -34,7 +36,8 @@ func init() { PrivateDataFolder: "nsetu", }) - ns.db.DBPath = engine.DataFolder() + "data.db" + dbpath = engine.DataFolder() + "data.db" + ns.db = sql.New(dbpath) cfgfile := engine.DataFolder() + "setupath.txt" if file.IsExist(cfgfile) { b, err := os.ReadFile(cfgfile) @@ -48,7 +51,7 @@ func init() { panic(err) } - engine.OnRegex(`^本地(.*)$`, fcext.ValueInList(func(ctx *zero.Ctx) string { return ctx.State["regex_matched"].([]string)[1] }, ns)).SetBlock(true). + engine.OnRegex(`^本地(.*)$`, fcext.ValueInList(func(ctx *zero.Ctx) string { return ctx.State["regex_matched"].([]string)[1] }, &ns)).SetBlock(true). Handle(func(ctx *zero.Ctx) { imgtype := ctx.State["regex_matched"].([]string)[1] sc := new(setuclass) @@ -69,7 +72,7 @@ func init() { ctx.SendChain(message.Text(imgtype, ": ", sc.Name, "\n"), message.Image(p)) } }) - engine.OnRegex(`^刷新本地(.*)$`, fcext.ValueInList(func(ctx *zero.Ctx) string { return ctx.State["regex_matched"].([]string)[1] }, ns), zero.SuperUserPermission).SetBlock(true). + engine.OnRegex(`^刷新本地(.*)$`, fcext.ValueInList(func(ctx *zero.Ctx) string { return ctx.State["regex_matched"].([]string)[1] }, &ns), zero.SuperUserPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { imgtype := ctx.State["regex_matched"].([]string)[1] err := ns.scanclass(os.DirFS(setupath), imgtype, imgtype) diff --git a/plugin/nihongo/model.go b/plugin/nihongo/model.go index d843ea778f..910a3c2ecb 100644 --- a/plugin/nihongo/model.go +++ b/plugin/nihongo/model.go @@ -22,14 +22,14 @@ func (g *grammar) string() string { return fmt.Sprintf("ID:\n%d\n\n标签:\n%s\n\n语法名:\n%s\n\n发音:\n%s\n\n用法:\n%s\n\n意思:\n%s\n\n解说:\n%s\n\n示例:\n%s", g.ID, g.Tag, g.Name, g.Pronunciation, g.Usage, g.Meaning, g.Explanation, g.Example) } -var db = &sql.Sqlite{} +var db sql.Sqlite func getRandomGrammarByTag(tag string) (g grammar) { - _ = db.Find("grammar", &g, "WHERE tag LIKE '%"+tag+"%' ORDER BY RANDOM() limit 1") + _ = db.Find("grammar", &g, "WHERE tag LIKE ? ORDER BY RANDOM() limit 1", "%"+tag+"%") return } func getRandomGrammarByKeyword(keyword string) (g grammar) { - _ = db.Find("grammar", &g, "WHERE (name LIKE '%"+keyword+"%' or pronunciation LIKE '%"+keyword+"%') ORDER BY RANDOM() limit 1") + _ = db.Find("grammar", &g, "WHERE (name LIKE ? OR pronunciation LIKE ?) ORDER BY RANDOM() limit 1", "%"+keyword+"%", "%"+keyword+"%") return } diff --git a/plugin/nihongo/nihongo.go b/plugin/nihongo/nihongo.go index 453262515e..6829a9f730 100644 --- a/plugin/nihongo/nihongo.go +++ b/plugin/nihongo/nihongo.go @@ -6,6 +6,7 @@ import ( "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/img/text" @@ -24,7 +25,7 @@ func init() { }) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "nihongo.db" + db = sql.New(engine.DataFolder() + "nihongo.db") _, err := engine.GetLazyData("nihongo.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/niuniu/draw.go b/plugin/niuniu/draw.go new file mode 100644 index 0000000000..0fe284a38d --- /dev/null +++ b/plugin/niuniu/draw.go @@ -0,0 +1,54 @@ +package niuniu + +import ( + "bytes" + "fmt" + "image" + "image/png" + "net/http" + + "github.com/FloatTech/AnimeAPI/niu" + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/rendercard" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/img/text" + zero "github.com/wdvxdr1123/ZeroBot" +) + +func processRankingImg(allUsers niu.BaseInfos, ctx *zero.Ctx, t bool) ([]byte, error) { + fontByte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true) + if err != nil { + return nil, err + } + s := "牛牛长度" + title := "牛牛长度排行" + if !t { + s = "牛牛深度" + title = "牛牛深度排行" + } + ri := make([]*rendercard.RankInfo, len(allUsers)) + for i, user := range allUsers { + resp, err := http.Get(fmt.Sprintf("https://q1.qlogo.cn/g?b=qq&nk=%d&s=100", user.UID)) + if err != nil { + return nil, err + } + decode, _, err := image.Decode(resp.Body) + _ = resp.Body.Close() + if err != nil { + return nil, err + } + ri[i] = &rendercard.RankInfo{ + Avatar: decode, + TopLeftText: ctx.CardOrNickName(user.UID), + BottomLeftText: fmt.Sprintf("QQ:%d", user.UID), + RightText: fmt.Sprintf("%s:%.2fcm", s, user.Length), + } + } + img, err := rendercard.DrawRankingCard(fontByte, title, ri) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = png.Encode(&buf, img) + return buf.Bytes(), err +} diff --git a/plugin/niuniu/main.go b/plugin/niuniu/main.go new file mode 100644 index 0000000000..d49b5a062f --- /dev/null +++ b/plugin/niuniu/main.go @@ -0,0 +1,430 @@ +// Package niuniu 牛牛大作战 +package niuniu + +import ( + "errors" + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/FloatTech/AnimeAPI/niu" + "github.com/FloatTech/AnimeAPI/wallet" + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + "github.com/FloatTech/zbputils/ctxext" + "github.com/RomiChan/syncx" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/extension/rate" + "github.com/wdvxdr1123/ZeroBot/message" +) + +var ( + en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ + DisableOnDefault: false, + Brief: "牛牛大作战", + Help: "- 打胶\n" + + "- 使用[道具名称]打胶\n" + + "- jj@xxx\n" + + "- 使用[道具名称]jj@xxx\n" + + "- 注册牛牛\n" + + "- 赎牛牛(cd:60分钟)\n" + + "- 出售牛牛\n" + + "- 牛牛拍卖行\n" + + "- 牛牛商店\n" + + "- 牛牛背包\n" + + "- 注销牛牛\n" + + "- 查看我的牛牛\n" + + "- 牛子长度排行\n" + + "- 牛子深度排行\n" + + "\n ps : 出售后的牛牛都会进入牛牛拍卖行哦", + PrivateDataFolder: "niuniu", + }) + dajiaoLimiter = rate.NewManager[string](time.Second*90, 1) + jjLimiter = rate.NewManager[string](time.Second*150, 1) + jjCount = syncx.Map[string, *niu.PKRecord]{} + register = syncx.Map[string, *niu.PKRecord]{} +) + +func init() { + en.OnFullMatch("牛牛拍卖行", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + auction, err := niu.ShowAuction(gid) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + + var messages message.Message + messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text("牛牛拍卖行有以下牛牛"))) + for _, info := range auction { + msg := fmt.Sprintf("商品序号: %d\n牛牛原所属: %d\n牛牛价格: %d%s\n牛牛大小: %.2fcm", + info.ID, info.UserID, info.Money, wallet.GetWalletName(), info.Length) + messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text(msg))) + } + if id := ctx.Send(messages).ID(); id == 0 { + ctx.Send(message.Text("发送拍卖行失败")) + return + } + ctx.SendChain(message.Reply(ctx.Event.Message), message.Text("请输入对应序号进行购买")) + recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(\d+)$`)).Repeat() + defer cancel() + timer := time.NewTimer(120 * time.Second) + answer := "" + defer timer.Stop() + for { + select { + case <-timer.C: + ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消")) + return + case r := <-recv: + answer = r.Event.Message.String() + n, err := strconv.Atoi(answer) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + msg, err := niu.Auction(gid, uid, n) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.Message), message.Text(msg)) + return + } + } + }) + en.OnFullMatch("出售牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + key := fmt.Sprintf("%d_%d", gid, uid) + sell, err := niu.Sell(gid, uid) + if errors.Is(err, niu.ErrCanceled) || errors.Is(err, niu.ErrNoNiuNiu) { + ctx.SendChain(message.Text(err)) + jjCount.Delete(key) + return + } else if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + + // 数据库操作成功之后,及时删除残留的缓存 + if _, ok := jjCount.Load(key); ok { + jjCount.Delete(key) + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(sell)) + }) + en.OnFullMatch("牛牛背包", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + bag, err := niu.Bag(gid, uid) + if err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(bag)) + }) + en.OnFullMatch("牛牛商店", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + + if _, err := niu.GetWordNiuNiu(gid, uid); err != nil { + ctx.SendChain(message.Text(niu.ErrNoNiuNiu)) + return + } + + propMap := map[int]struct { + name string + cost int + scope string + description string + }{ + 1: {"伟哥", 100, "打胶", "可以让你打胶每次都增长"}, + 2: {"媚药", 100, "打胶", "可以让你打胶每次都减少"}, + 3: {"击剑神器", 300, "jj", "可以让你每次击剑都立于不败之地"}, + 4: {"击剑神稽", 300, "jj", "可以让你每次击剑都失败"}, + } + + var messages message.Message + messages = append(messages, ctxext.FakeSenderForwardNode(ctx, + message.Text("输入对应序号进行购买商品"), + message.Text( + "使用说明:\n"+ + "商品id-商品数量\n"+ + "如想购买10个伟哥\n"+ + "即:1-10"))) + messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text("牛牛商店当前售卖的物品如下"))) + for id := 1; id <= len(propMap); id++ { + product := propMap[id] + productInfo := fmt.Sprintf("商品%d\n商品名: %s\n商品价格: %dATRI币\n商品作用域: %s\n商品描述: %s", + id, product.name, product.cost, product.scope, product.description) + messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text(productInfo))) + } + if id := ctx.Send(messages).ID(); id == 0 { + ctx.Send(message.Text("发送商店失败")) + return + } + recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(\d+)-(\d+)$`)).Repeat() + defer cancel() + timer := time.NewTimer(120 * time.Second) + answer := "" + defer timer.Stop() + for { + select { + case <-timer.C: + ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消")) + return + case r := <-recv: + answer = r.Event.Message.String() + + // 解析输入的商品ID和数量 + parts := strings.Split(answer, "-") + productID, _ := strconv.Atoi(parts[0]) + quantity, _ := strconv.Atoi(parts[1]) + + if err := niu.Store(gid, uid, productID, quantity); err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + + ctx.SendChain(message.Text("购买成功!")) + return + } + } + }) + en.OnFullMatch("赎牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + last, ok := jjCount.Load(fmt.Sprintf("%d_%d", gid, uid)) + + if !ok { + ctx.SendChain(message.Text("你还没有被厥呢")) + return + } + + if time.Since(last.TimeLimit) > time.Hour { + ctx.SendChain(message.Text("时间已经过期了,牛牛已被收回!")) + jjCount.Delete(fmt.Sprintf("%d_%d", gid, uid)) + return + } + + if last.Count < 4 { + ctx.SendChain(message.Text("你还没有被厥够4次呢,不能赎牛牛")) + return + } + ctx.SendChain(message.Text("再次确认一下哦,这次赎牛牛,牛牛长度将会变成", last.Length, "cm\n还需要嘛【是|否】")) + recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(是|否)$`)).Repeat() + defer cancel() + timer := time.NewTimer(2 * time.Minute) + defer timer.Stop() + for { + select { + case <-timer.C: + ctx.SendChain(message.Text("操作超时,已自动取消")) + return + case c := <-recv: + answer := c.Event.Message.String() + if answer == "否" { + ctx.SendChain(message.Text("取消成功!")) + return + } + + if err := niu.Redeem(gid, uid, *last); err != nil { + ctx.SendChain(message.Text("ERROR:", err)) + return + } + // 成功赎回,删除残留的缓存。 + jjCount.Delete(fmt.Sprintf("%d_%d", gid, uid)) + + ctx.SendChain(message.At(uid), message.Text(fmt.Sprintf("恭喜你!成功赎回牛牛,当前长度为:%.2fcm", last.Length))) + return + } + } + }) + en.OnFullMatch("牛子长度排行", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + infos, err := niu.GetRankingInfo(gid, true) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + img, err := processRankingImg(infos, ctx, true) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.ImageBytes(img)) + }) + en.OnFullMatch("牛子深度排行", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + infos, err := niu.GetRankingInfo(gid, false) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + img, err := processRankingImg(infos, ctx, false) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.ImageBytes(img)) + }) + en.OnFullMatch("查看我的牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + uid := ctx.Event.UserID + gid := ctx.Event.GroupID + view, err := niu.View(gid, uid, ctx.CardOrNickName(uid)) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(view)) + }) + en.OnRegex(`^(?:.*使用(.*))??打胶$`, zero.OnlyGroup).SetBlock(true).Limit(func(ctx *zero.Ctx) *rate.Limiter { + lt := dajiaoLimiter.Load(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID)) + ctx.State["dajiao_last_touch"] = lt.LastTouch() + return lt + }, func(ctx *zero.Ctx) { + timePass := int(time.Since(time.Unix(ctx.State["dajiao_last_touch"].(int64), 0)).Seconds()) + ctx.SendChain(message.Text(randomChoice([]string{ + fmt.Sprintf("才过去了%ds时间,你就又要打🦶了,身体受得住吗", timePass), + fmt.Sprintf("不行不行,你的身体会受不了的,歇%ds再来吧", 90-timePass), + fmt.Sprintf("休息一下吧,会炸膛的!%ds后再来吧", 90-timePass), + fmt.Sprintf("打咩哟,你的牛牛会爆炸的,休息%ds再来吧", 90-timePass), + }))) + }).Handle(func(ctx *zero.Ctx) { + // 获取群号和用户ID + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + fiancee := ctx.State["regex_matched"].([]string) + + msg, err := niu.HitGlue(gid, uid, fiancee[1]) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + dajiaoLimiter.Delete(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg)) + }) + en.OnFullMatch("注册牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + gid := ctx.Event.GroupID + uid := ctx.Event.UserID + msg, err := niu.Register(gid, uid) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg)) + }) + en.OnMessage(zero.NewPattern(nil).Text(`^(?:.*使用(.*))??jj`).At().AsRule(), + zero.OnlyGroup).SetBlock(true).Limit(func(ctx *zero.Ctx) *rate.Limiter { + lt := jjLimiter.Load(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID)) + ctx.State["jj_last_touch"] = lt.LastTouch() + return lt + }, func(ctx *zero.Ctx) { + timePass := int(time.Since(time.Unix(ctx.State["jj_last_touch"].(int64), 0)).Seconds()) + ctx.SendChain(message.Text(randomChoice([]string{ + fmt.Sprintf("才过去了%ds时间,你就又要击剑了,真是饥渴难耐啊", timePass), + fmt.Sprintf("不行不行,你的身体会受不了的,歇%ds再来吧", 150-timePass), + fmt.Sprintf("你这种男同就应该被送去集中营!等待%ds再来吧", 150-timePass), + fmt.Sprintf("打咩哟!你的牛牛会炸的,休息%ds再来吧", 150-timePass), + }))) + }, + ).Handle(func(ctx *zero.Ctx) { + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + adduser, err := strconv.ParseInt(patternParsed[1].At(), 10, 64) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + jjLimiter.Delete(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID)) + return + } + uid := ctx.Event.UserID + gid := ctx.Event.GroupID + msg, length, niuID, err := niu.JJ(gid, uid, adduser, patternParsed[0].Text()[1]) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + jjLimiter.Delete(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg)) + j := fmt.Sprintf("%d_%d", gid, adduser) + count, ok := jjCount.Load(j) + var c niu.PKRecord + // 按照最后一次被 jj 时的时间计算,超过60分钟则重置 + if !ok { + // 第一次被 jj + c = niu.PKRecord{ + NiuID: niuID, + TimeLimit: time.Now(), + Count: 1, + Length: length, + } + } else { + c = niu.PKRecord{ + NiuID: niuID, + TimeLimit: time.Now(), + Count: count.Count + 1, + Length: count.Length, + } + // 超时了,重置 + if time.Since(c.TimeLimit) > time.Hour { + c = niu.PKRecord{ + NiuID: niuID, + TimeLimit: time.Now(), + Count: 1, + Length: length, + } + } + } + + jjCount.Store(j, &c) + if c.Count > 2 { + ctx.SendChain(message.Text(randomChoice([]string{ + fmt.Sprintf("你们太厉害了,对方已经被你们打了%d次了,你们可以继续找他🤺", c.Count), + "你们不要再找ta🤺啦!"}, + ))) + + if c.Count >= 4 { + if c.Count == 6 { + return + } + id := ctx.SendPrivateMessage(adduser, + message.Text(fmt.Sprintf("你在%d群里已经被厥冒烟了,快去群里赎回你原本的牛牛!\n发送:`赎牛牛`即可!", gid))) + if id == 0 { + ctx.SendChain(message.At(adduser), message.Text("快发送`赎牛牛`来赎回你原本的牛牛!")) + } + } + } + }) + en.OnFullMatch("注销牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + uid := ctx.Event.UserID + gid := ctx.Event.GroupID + key := fmt.Sprintf("%d_%d", gid, uid) + data, ok := register.Load(key) + switch { + case !ok || time.Since(data.TimeLimit) > time.Hour*24: + data = &niu.PKRecord{ + TimeLimit: time.Now(), + Count: 1, + } + default: + if err := wallet.InsertWalletOf(uid, -data.Count*50); err != nil { + ctx.SendChain(message.Text("你的钱不够你注销牛牛了,这次注销需要", data.Count*50, wallet.GetWalletName())) + return + } + data.Count++ + } + register.Store(key, data) + msg, err := niu.Cancel(gid, uid) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg)) + }) +} + +func randomChoice(options []string) string { + return options[rand.Intn(len(options))] +} diff --git a/plugin/omikuji/model.go b/plugin/omikuji/model.go index 2f204acf53..847f004e08 100644 --- a/plugin/omikuji/model.go +++ b/plugin/omikuji/model.go @@ -1,8 +1,6 @@ package omikuji import ( - "strconv" - sql "github.com/FloatTech/sqlite" ) @@ -11,12 +9,12 @@ type kuji struct { Text string `db:"text"` } -var db = &sql.Sqlite{} +var db sql.Sqlite // 返回一个解签 func getKujiByBango(id uint8) string { var s kuji - err := db.Find("kuji", &s, "where id = "+strconv.Itoa(int(id))) + err := db.Find("kuji", &s, "WHERE id = ?", id) if err != nil { return err.Error() } diff --git a/plugin/omikuji/sensou.go b/plugin/omikuji/sensou.go index 5b3e754ee7..11d95ebe73 100644 --- a/plugin/omikuji/sensou.go +++ b/plugin/omikuji/sensou.go @@ -11,6 +11,7 @@ import ( "github.com/wdvxdr1123/ZeroBot/utils/helper" fcext "github.com/FloatTech/floatbox/ctxext" + sql "github.com/FloatTech/sqlite" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" @@ -48,7 +49,7 @@ func init() { // 插件主体 }) engine.OnFullMatch("解签", fcext.DoOnceOnSuccess( func(ctx *zero.Ctx) bool { - db.DBPath = engine.DataFolder() + "kuji.db" + db = sql.New(engine.DataFolder() + "kuji.db") _, err := engine.GetLazyData("kuji.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/poker/poker.go b/plugin/poker/poker.go index 32def3c247..161827c37f 100644 --- a/plugin/poker/poker.go +++ b/plugin/poker/poker.go @@ -28,12 +28,12 @@ func init() { getImg := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { data, err := engine.GetLazyData("imgdata.json", true) if err != nil { - ctx.SendChain(message.Text("ERROR:", err)) + ctx.SendChain(message.Text("ERROR: ", err)) return false } err = json.Unmarshal(data, &cardImgPathList) if err != nil { - ctx.SendChain(message.Text("ERROR:", err)) + ctx.SendChain(message.Text("ERROR: ", err)) return false } return true diff --git a/plugin/qqwife/command.go b/plugin/qqwife/command.go index 6915272b22..9f548bd08b 100644 --- a/plugin/qqwife/command.go +++ b/plugin/qqwife/command.go @@ -5,7 +5,6 @@ import ( "math/rand" "sort" "strconv" - "strings" "sync" "time" @@ -29,8 +28,8 @@ import ( ) type 婚姻登记 struct { - db *sql.Sqlite sync.RWMutex + db sql.Sqlite } // 群设置 @@ -53,9 +52,7 @@ type userinfo struct { } var ( - 民政局 = &婚姻登记{ - db: &sql.Sqlite{}, - } + 民政局 婚姻登记 engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "一群一天一夫一妻制群老婆", @@ -67,7 +64,7 @@ var ( "- 买礼物给[对方Q号|@对方QQ]\n使用小熊饼干获取好感度\n" + "- 做媒 @攻方QQ @受方QQ\n身为管理, 群友的xing福是要搭把手的(攻受双方好感度越高成功率越高,保底30%概率)\n" + "--------------------------------\n好感度规则\n--------------------------------\n" + - "\"娶群友\"指令好感度随机增加1~5。\n\"A牛B的C\"会导致C恨A, 好感度-5;\nB为了报复A, 好感度+5(什么柜子play)\nA为BC做媒,成功B、C对A好感度+1反之-1\n做媒成功BC好感度+1" + + "\"娶群友\"&\"(娶|嫁)@对方QQ\"指令好感度随机增加1~5。\n\"A牛B的C\"会导致C恨A, 好感度-5;\nB为了报复A, 好感度+5(什么柜子play)\nA为BC做媒,成功B、C对A好感度+1反之-1\n做媒成功BC好感度+1" + "\nTips: 群老婆列表过0点刷新", PrivateDataFolder: "qqwife", }).ApplySingle(single.New( @@ -81,7 +78,7 @@ var ( }), )) getdb = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - 民政局.db.DBPath = engine.DataFolder() + "结婚登记表.db" + 民政局.db = sql.New(engine.DataFolder() + "结婚登记表.db") err := 民政局.db.Open(time.Hour) if err == nil { // 创建群配置表 @@ -128,7 +125,7 @@ func init() { ctx.SendChain( message.At(uid), message.Text("\n今天你在", userInfo.Updatetime, "娶了群友"), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(userInfo.Target, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(userInfo.Target, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", userInfo.Targetname, "]", @@ -140,7 +137,7 @@ func init() { ctx.SendChain( message.At(uid), message.Text("\n今天你在", userInfo.Updatetime, "被群友"), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(userInfo.User, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(userInfo.User, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", userInfo.Username, "]", @@ -200,7 +197,7 @@ func init() { ctx.SendChain( message.At(uid), message.Text("今天你的群老婆是"), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", ctx.CardOrNickName(fiancee), "]", @@ -281,7 +278,7 @@ func init() { ctx.SendChain(message.Text("该功能只能在群组使用或者指定群组")) return } - err = 民政局.清理花名册("group" + strconv.FormatInt(ctx.Event.GroupID, 10)) + err = 民政局.清理花名册(ctx.Event.GroupID) default: cmd := ctx.State["regex_matched"].([]string)[1] gid, _ := strconv.ParseInt(cmd, 10, 64) // 判断是否为群号 @@ -289,7 +286,7 @@ func init() { ctx.SendChain(message.Text("请输入正确的群号")) return } - err = 民政局.清理花名册("group" + cmd) + err = 民政局.清理花名册(gid) } if err != nil { ctx.SendChain(message.Text("[ERROR]:", err)) @@ -307,7 +304,7 @@ func (sql *婚姻登记) 查看设置(gid int64) (dbinfo updateinfo, err error) if err != nil { return } - if !sql.db.CanFind("updateinfo", "where gid is "+strconv.FormatInt(gid, 10)) { + if !sql.db.CanFind("updateinfo", "WHERE gid = ?", gid) { // 没有记录 return updateinfo{ GID: gid, @@ -316,7 +313,7 @@ func (sql *婚姻登记) 查看设置(gid int64) (dbinfo updateinfo, err error) CDtime: 12, }, nil } - _ = sql.db.Find("updateinfo", &dbinfo, "where gid is "+strconv.FormatInt(gid, 10)) + _ = sql.db.Find("updateinfo", &dbinfo, "WHERE gid = ?", gid) return } @@ -334,7 +331,7 @@ func (sql *婚姻登记) 开门时间(gid int64) error { sql.Lock() defer sql.Unlock() dbinfo := updateinfo{} - _ = sql.db.Find("updateinfo", &dbinfo, "where gid is "+strconv.FormatInt(gid, 10)) + _ = sql.db.Find("updateinfo", &dbinfo, "WHERE gid = ?", gid) if time.Now().Format("2006/01/02") != dbinfo.Updatetime { // 如果跨天了就删除 _ = sql.db.Drop("group" + strconv.FormatInt(gid, 10)) @@ -355,10 +352,9 @@ func (sql *婚姻登记) 查户口(gid, uid int64) (info userinfo, err error) { if err != nil { return } - uidstr := strconv.FormatInt(uid, 10) - err = sql.db.Find(gidstr, &info, "where user = "+uidstr) + err = sql.db.Find(gidstr, &info, "WHERE user = ?", uid) if err != nil { - err = sql.db.Find(gidstr, &info, "where target = "+uidstr) + err = sql.db.Find(gidstr, &info, "WHERE target = ?", uid) } return } @@ -423,7 +419,7 @@ func slicename(name string, canvas *gg.Context) (resultname string) { return } -func (sql *婚姻登记) 清理花名册(gid ...string) error { +func (sql *婚姻登记) 清理花名册(gid ...int64) error { sql.Lock() defer sql.Unlock() switch gid { @@ -439,9 +435,9 @@ func (sql *婚姻登记) 清理花名册(gid ...string) error { } return err default: - err := sql.db.Drop(gid[0]) + err := sql.db.Drop("group" + strconv.FormatInt(gid[0], 10)) if err == nil { - _ = sql.db.Del("cdsheet", "where GroupID is "+strings.ReplaceAll(gid[0], "group", "")) + _ = sql.db.Del("cdsheet", "WHERE GroupID = ?", gid[0]) } return err } diff --git a/plugin/qqwife/favorSystem.go b/plugin/qqwife/favorSystem.go index 6e66c3d954..af370c1f57 100644 --- a/plugin/qqwife/favorSystem.go +++ b/plugin/qqwife/favorSystem.go @@ -9,6 +9,7 @@ import ( "github.com/FloatTech/floatbox/math" "github.com/FloatTech/imgfactory" + sql "github.com/FloatTech/sqlite" control "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" zero "github.com/wdvxdr1123/ZeroBot" @@ -31,9 +32,10 @@ type favorability struct { func init() { // 好感度系统 - engine.OnRegex(`^查好感度\s*(\[CQ:at,qq=)?(\d+)`, zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnMessage(zero.NewPattern(nil).Text(`^查好感度`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { - fiancee, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + fiancee, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64) uid := ctx.Event.UserID favor, err := 民政局.查好感度(uid, fiancee) if err != nil { @@ -47,12 +49,12 @@ func init() { ) }) // 礼物系统 - engine.OnRegex(`^买礼物给\s?(\[CQ:at,qq=(\d+)\]|(\d+))`, getdb).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnMessage(zero.NewPattern(nil).Text(`^买礼物给`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID uid := ctx.Event.UserID - fiancee := ctx.State["regex_matched"].([]string) - gay, _ := strconv.ParseInt(fiancee[2]+fiancee[3], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + gay, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64) if gay == uid { ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, message.At(uid), message.Text("你想给自己买什么礼物呢?"))) return @@ -117,9 +119,9 @@ func init() { } // 输出结果 if mood == 0 { - ctx.SendChain(message.Text("你花了", moneyToFavor, "ATRI币买了一件女装送给了ta,ta很不喜欢,你们的好感度降低至", lastfavor)) + ctx.SendChain(message.Text("你花了", moneyToFavor, wallet.GetWalletName(), "买了一件女装送给了ta,ta很不喜欢,你们的好感度降低至", lastfavor)) } else { - ctx.SendChain(message.Text("你花了", moneyToFavor, "ATRI币买了一件女装送给了ta,ta很喜欢,你们的好感度升至", lastfavor)) + ctx.SendChain(message.Text("你花了", moneyToFavor, wallet.GetWalletName(), "买了一件女装送给了ta,ta很喜欢,你们的好感度升至", lastfavor)) } }) engine.OnFullMatch("好感度列表", zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser). @@ -214,7 +216,7 @@ func init() { favor := favorability{} delInfo := make([]string, 0, count*2) favorInfo := make(map[string]int, count*2) - _ = 民政局.db.FindFor("favorability", &favor, "group by Userinfo", func() error { + _ = 民政局.db.FindFor("favorability", &favor, "GROUP BY Userinfo", func() error { delInfo = append(delInfo, favor.Userinfo) // 解析旧数据 userList := strings.Split(favor.Userinfo, "+") @@ -236,15 +238,11 @@ func init() { } return nil }) - for _, updateinfo := range delInfo { - // 删除旧数据 - err = 民政局.db.Del("favorability", "where Userinfo = '"+updateinfo+"'") - if err != nil { - userList := strings.Split(favor.Userinfo, "+") - uid1, _ := strconv.ParseInt(userList[0], 10, 64) - uid2, _ := strconv.ParseInt(userList[1], 10, 64) - ctx.SendChain(message.Text("[ERROR]: 删除", ctx.CardOrNickName(uid1), "和", ctx.CardOrNickName(uid2), "的好感度时发生了错误。\n错误信息:", err)) - } + // 删除旧数据 + q, s := sql.QuerySet("WHERE Userinfo", "IN", delInfo) + err = 民政局.db.Del("favorability", q, s...) + if err != nil { + ctx.SendChain(message.Text("[ERROR]: 删除好感度时发生了错误。\n错误信息:", err)) } for userInfo, favor := range favorInfo { favorInfo := favorability{ @@ -273,15 +271,15 @@ func (sql *婚姻登记) 查好感度(uid, target int64) (int, error) { info := favorability{} if uid > target { userinfo := strconv.FormatInt(uid, 10) + "+" + strconv.FormatInt(target, 10) - err = sql.db.Find("favorability", &info, "where Userinfo is '"+userinfo+"'") + err = sql.db.Find("favorability", &info, "WHERE Userinfo = ?", userinfo) if err != nil { - _ = sql.db.Find("favorability", &info, "where Userinfo glob '*"+userinfo+"*'") + _ = sql.db.Find("favorability", &info, "WHERE Userinfo glob ?", "*"+userinfo+"*") } } else { userinfo := strconv.FormatInt(target, 10) + "+" + strconv.FormatInt(uid, 10) - err = sql.db.Find("favorability", &info, "where Userinfo is '"+userinfo+"'") + err = sql.db.Find("favorability", &info, "WHERE Userinfo = ?", userinfo) if err != nil { - _ = sql.db.Find("favorability", &info, "where Userinfo glob '*"+userinfo+"*'") + _ = sql.db.Find("favorability", &info, "WHERE Userinfo glob ?", "*"+userinfo+"*") } } return info.Favor, nil @@ -304,7 +302,7 @@ func (sql *婚姻登记) getGroupFavorability(uid int64) (list favorList, err er sql.RLock() defer sql.RUnlock() info := favorability{} - err = sql.db.FindFor("favorability", &info, "where Userinfo glob '*"+uidStr+"*'", func() error { + err = sql.db.FindFor("favorability", &info, "WHERE Userinfo glob ?", func() error { var target string userList := strings.Split(info.Userinfo, "+") switch { @@ -320,7 +318,7 @@ func (sql *婚姻登记) getGroupFavorability(uid int64) (list favorList, err er Favor: info.Favor, }) return nil - }) + }, "*"+uidStr+"*") sort.Sort(list) return } @@ -338,15 +336,15 @@ func (sql *婚姻登记) 更新好感度(uid, target int64, score int) (favor in targstr := strconv.FormatInt(target, 10) if uid > target { info.Userinfo = uidstr + "+" + targstr - err = sql.db.Find("favorability", &info, "where Userinfo is '"+info.Userinfo+"'") + err = sql.db.Find("favorability", &info, "WHERE Userinfo = ?", info.Userinfo) } else { info.Userinfo = targstr + "+" + uidstr - err = sql.db.Find("favorability", &info, "where Userinfo is '"+info.Userinfo+"'") + err = sql.db.Find("favorability", &info, "WHERE Userinfo = ?", info.Userinfo) } if err != nil { - err = sql.db.Find("favorability", &info, "where Userinfo glob '*"+targstr+"+"+uidstr+"*'") + err = sql.db.Find("favorability", &info, "WHERE Userinfo glob ?", "*"+targstr+"+"+uidstr+"*") if err == nil { // 如果旧数据存在就删除旧数据 - err = 民政局.db.Del("favorability", "where Userinfo = '"+info.Userinfo+"'") + err = 民政局.db.Del("favorability", "WHERE Userinfo = ?", info.Userinfo) } } info.Favor += score diff --git a/plugin/qqwife/function.go b/plugin/qqwife/function.go index 73582c89f2..dd9a339288 100644 --- a/plugin/qqwife/function.go +++ b/plugin/qqwife/function.go @@ -93,12 +93,13 @@ func init() { ctx.SendChain(message.Text("设置成功")) }) // 单身技能 - engine.OnRegex(`^(娶|嫁)\[CQ:at,qq=(\d+)\]`, zero.OnlyGroup, getdb, checkSingleDog).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnMessage(zero.NewPattern(nil).Text(`^(娶|嫁)`).At().AsRule(), zero.OnlyGroup, getdb, checkSingleDog).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID uid := ctx.Event.UserID - choice := ctx.State["regex_matched"].([]string)[1] - fiancee, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + choice := patternParsed[0].Text()[0] + fiancee, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64) // 写入CD err := 民政局.记录CD(gid, uid, "嫁娶") if err != nil { @@ -148,26 +149,31 @@ func init() { } choicetext = "\n今天你的群老公是" } + favor, err = 民政局.更新好感度(uid, fiancee, 1+rand.Intn(5)) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:", err)) + } // 请大家吃席 ctx.SendChain( message.Text(sendtext[0][rand.Intn(len(sendtext[0]))]), message.At(uid), message.Text(choicetext), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", ctx.CardOrNickName(fiancee), "]", "(", fiancee, ")哒", + "(", fiancee, ")哒\n当前你们好感度为", favor, ), ) }) // NTR技能 - engine.OnRegex(`^当(\[CQ:at,qq=(\d+)\]\s?|(\d+))的小三`, zero.OnlyGroup, getdb, checkMistress).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnMessage(zero.NewPattern(nil).Text(`^当`).At().Text(`的小三`).AsRule(), zero.OnlyGroup, getdb, checkMistress).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID uid := ctx.Event.UserID - fid := ctx.State["regex_matched"].([]string) - fiancee, _ := strconv.ParseInt(fid[2]+fid[3], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + fiancee, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64) // 写入CD err := 民政局.记录CD(gid, uid, "NTR") if err != nil { @@ -239,7 +245,7 @@ func init() { message.Text(sendtext[2][rand.Intn(len(sendtext[2]))]), message.At(uid), message.Text("今天你的群"+choicetext+"是"), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(fiancee, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", ctx.CardOrNickName(fiancee), "]", @@ -248,12 +254,13 @@ func init() { ) }) // 做媒技能 - engine.OnRegex(`^做媒\s?\[CQ:at,qq=(\d+)\]\s?\[CQ:at,qq=(\d+)\]`, zero.OnlyGroup, zero.AdminPermission, getdb, checkMatchmaker).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnMessage(zero.NewPattern(nil).Text(`做媒`).At().At().AsRule(), zero.OnlyGroup, zero.AdminPermission, getdb, checkMatchmaker).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { gid := ctx.Event.GroupID uid := ctx.Event.UserID - gayOne, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) - gayZero, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + gayOne, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64) + gayZero, _ := strconv.ParseInt(patternParsed[2].At(), 10, 64) // 写入CD err := 民政局.记录CD(gid, uid, "做媒") if err != nil { @@ -303,7 +310,7 @@ func init() { message.Text("恭喜你成功撮合了一对CP\n\n"), message.At(gayOne), message.Text("今天你的群老婆是"), - message.Image("http://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(gayZero, 10)+"&s=640").Add("cache", 0), + message.Image("https://q4.qlogo.cn/g?b=qq&nk="+strconv.FormatInt(gayZero, 10)+"&s=640").Add("cache", 0), message.Text( "\n", "[", ctx.CardOrNickName(gayZero), "]", @@ -367,18 +374,16 @@ func (sql *婚姻登记) 判断CD(gid, uid int64, model string, cdtime float64) if err != nil { return false, err } - limitID := "where GroupID is " + strconv.FormatInt(gid, 10) + - " and UserID is " + strconv.FormatInt(uid, 10) + - " and ModeID is '" + model + "'" - if !sql.db.CanFind("cdsheet", limitID) { + limitID := "WHERE GroupID = ? AND UserID = ? AND ModeID = ?" + if !sql.db.CanFind("cdsheet", limitID, gid, uid, model) { // 没有记录即不用比较 return true, nil } cdinfo := cdsheet{} - _ = sql.db.Find("cdsheet", &cdinfo, limitID) + _ = sql.db.Find("cdsheet", &cdinfo, limitID, gid, uid, model) if time.Since(time.Unix(cdinfo.Time, 0)).Hours() > cdtime { // 如果CD已过就删除 - err = sql.db.Del("cdsheet", limitID) + err = sql.db.Del("cdsheet", limitID, gid, uid, model) return true, err } return false, nil @@ -399,23 +404,22 @@ func (sql *婚姻登记) 离婚休妻(gid, wife int64) error { sql.Lock() defer sql.Unlock() gidstr := "group" + strconv.FormatInt(gid, 10) - wifestr := strconv.FormatInt(wife, 10) - return sql.db.Del(gidstr, "where target = "+wifestr) + return sql.db.Del(gidstr, "WHERE target = ?", wife) } func (sql *婚姻登记) 离婚休夫(gid, husband int64) error { sql.Lock() defer sql.Unlock() gidstr := "group" + strconv.FormatInt(gid, 10) - husbandstr := strconv.FormatInt(husband, 10) - return sql.db.Del(gidstr, "where user = "+husbandstr) + return sql.db.Del(gidstr, "WHERE user = ?", husband) } // 注入判断 是否单身条件 func checkSingleDog(ctx *zero.Ctx) bool { gid := ctx.Event.GroupID uid := ctx.Event.UserID - fiancee, err := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + fiancee, err := strconv.ParseInt(patternParsed[1].At(), 10, 64) if err != nil { ctx.SendChain(message.Text("额,你的target好像不存在?")) return false @@ -481,7 +485,8 @@ func checkSingleDog(ctx *zero.Ctx) bool { func checkMistress(ctx *zero.Ctx) bool { gid := ctx.Event.GroupID uid := ctx.Event.UserID - fiancee, err := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + fiancee, err := strconv.ParseInt(patternParsed[1].At(), 10, 64) if err != nil { ctx.SendChain(message.Text("额,你的target好像不存在?")) return false @@ -577,12 +582,13 @@ func checkDivorce(ctx *zero.Ctx) bool { func checkMatchmaker(ctx *zero.Ctx) bool { gid := ctx.Event.GroupID uid := ctx.Event.UserID - gayOne, err := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) + patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed) + gayOne, err := strconv.ParseInt(patternParsed[1].At(), 10, 64) if err != nil { ctx.SendChain(message.Text("额,攻方好像不存在?")) return false } - gayZero, err := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + gayZero, err := strconv.ParseInt(patternParsed[2].At(), 10, 64) if err != nil { ctx.SendChain(message.Text("额,受方好像不存在?")) return false diff --git a/plugin/qzone/qzone.go b/plugin/qzone/qzone.go index 166ebd5636..fdfcf22bdd 100644 --- a/plugin/qzone/qzone.go +++ b/plugin/qzone/qzone.go @@ -31,7 +31,7 @@ const ( agreeStatus disagreeStatus loveTag = "表白" - faceURL = "http://q4.qlogo.cn/g?b=qq&nk=%v&s=640" + faceURL = "https://q4.qlogo.cn/g?b=qq&nk=%v&s=640" anonymousURL = "https://gitcode.net/anto_july/avatar/-/raw/master/%v.png" ) diff --git a/plugin/robbery/robbery.go b/plugin/robbery/robbery.go index 0db5a28fbd..e393161ba0 100644 --- a/plugin/robbery/robbery.go +++ b/plugin/robbery/robbery.go @@ -21,8 +21,8 @@ import ( ) type robberyRepo struct { - db *sql.Sqlite sync.RWMutex + db sql.Sqlite } type robberyRecord struct { @@ -32,12 +32,10 @@ type robberyRecord struct { } func init() { - police := &robberyRepo{ - db: &sql.Sqlite{}, - } + var police robberyRepo engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, - Brief: "打劫别人的ATRI币", + Brief: "打劫别人的钱包", Help: "- 打劫[对方Q号|@对方QQ]\n" + "1. 受害者钱包少于1000不能被打劫\n" + "2. 打劫成功率 40%\n" + @@ -58,7 +56,7 @@ func init() { }), )) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - police.db.DBPath = engine.DataFolder() + "robbery.db" + police.db = sql.New(engine.DataFolder() + "robbery.db") err := police.db.Open(time.Hour) if err == nil { // 创建CD表 @@ -74,7 +72,7 @@ func init() { }) // 打劫功能 - engine.OnRegex(`^打劫\s?(\[CQ:at,qq=(\d+)\]|(\d+))`, getdb).SetBlock(true).Limit(ctxext.LimitByUser). + engine.OnRegex(`^打劫\s?(\[CQ:at,(?:\S*,)?qq=(\d+)(?:,\S*)?\]|(\d+))`, getdb).SetBlock(true).Limit(ctxext.LimitByUser). Handle(func(ctx *zero.Ctx) { uid := ctx.Event.UserID fiancee := ctx.State["regex_matched"].([]string) @@ -90,8 +88,13 @@ func init() { ctx.SendChain(message.Text("[ERROR]:", err)) return } - if !ok { - ctx.SendChain(message.Text("你已经打劫过了/对方已经被打劫过了")) + + if ok == 1 { + ctx.SendChain(message.Text("对方今天已经被打劫了,给人家留点后路吧")) + return + } + if ok >= 2 { + ctx.SendChain(message.Text("你今天已经成功打劫过了,贪心没有好果汁吃!")) return } @@ -116,16 +119,16 @@ func init() { } return } - userIncrMonry := math.Min(rand.Intn(victimWallet/20)+500, 10000) - victimDecrMonry := userIncrMonry / (rand.Intn(4) + 1) + userIncrMoney := math.Min(rand.Intn(victimWallet/20)+500, 10000) + victimDecrMoney := userIncrMoney / (rand.Intn(4) + 1) // 记录结果 - err = wallet.InsertWalletOf(victimID, -victimDecrMonry) + err = wallet.InsertWalletOf(victimID, -victimDecrMoney) if err != nil { ctx.SendChain(message.Text("[ERROR]:钱包坏掉力:\n", err)) return } - err = wallet.InsertWalletOf(uid, +userIncrMonry) + err = wallet.InsertWalletOf(uid, +userIncrMoney) if err != nil { ctx.SendChain(message.Text("[ERROR]:打劫失败,脏款掉入虚无\n", err)) return @@ -137,33 +140,45 @@ func init() { ctx.SendChain(message.At(uid), message.Text("[ERROR]:犯罪记录写入失败\n", err)) } - ctx.SendChain(message.At(uid), message.Text("打劫成功,钱包增加:", userIncrMonry, "ATRI币")) - ctx.SendChain(message.At(victimID), message.Text("保险公司对您进行了赔付,您实际损失:", victimDecrMonry, "ATRI币")) + ctx.SendChain(message.At(uid), message.Text("打劫成功,钱包增加:", userIncrMoney, wallet.GetWalletName())) + ctx.SendChain(message.At(victimID), message.Text("保险公司对您进行了赔付,您实际损失:", victimDecrMoney, wallet.GetWalletName())) }) } -func (sql *robberyRepo) getRecord(victimID, uid int64) (ok bool, err error) { +// ok==0 可以打劫;ok==1 程序错误 or 受害者进入CD;ok==2 用户进入CD; ok==3 用户和受害者都进入CD; +func (sql *robberyRepo) getRecord(victimID, uid int64) (ok int, err error) { sql.Lock() defer sql.Unlock() // 创建群表格 err = sql.db.Create("criminal_record", &robberyRecord{}) if err != nil { - return false, err + return 1, err } - limitID := "where victim_id is " + strconv.FormatInt(victimID, 10) + - " or user_id is " + strconv.FormatInt(uid, 10) - if !sql.db.CanFind("criminal_record", limitID) { + // 拼接查询SQL + limitID := "WHERE victim_id = ? OR user_id = ?" + if !sql.db.CanFind("criminal_record", limitID, victimID, uid) { // 没有记录即不用比较 - return true, nil - } - cdinfo := robberyRecord{} - _ = sql.db.Find("criminal_record", &cdinfo, limitID) - if time.Now().Format("2006/01/02") != cdinfo.Time { - // // 如果跨天了就删除 - err = sql.db.Del("criminal_record", limitID) - return true, err + return 0, nil } - return false, nil + cdInfo := robberyRecord{} + + err = sql.db.FindFor("criminal_record", &cdInfo, limitID, func() error { + if time.Now().Format("2006/01/02") != cdInfo.Time { + // // 如果跨天了就删除 + err = sql.db.Del("criminal_record", limitID, victimID, uid) + return nil + } + // 俩个if是为了保证,重复打劫同一个人,ok == 3 + if cdInfo.UserID == uid { + ok += 2 + } + if cdInfo.VictimID == victimID { + // lint 不允许使用 ok += 1 + ok++ + } + return nil + }, victimID, uid) + return ok, err } func (sql *robberyRepo) insertRecord(vid int64, uid int64) error { diff --git a/plugin/rsshub/domain/job.go b/plugin/rsshub/domain/job.go new file mode 100644 index 0000000000..078523fa10 --- /dev/null +++ b/plugin/rsshub/domain/job.go @@ -0,0 +1,134 @@ +// Package domain rsshub领域逻辑 +package domain + +import ( + "context" + + "github.com/mmcdole/gofeed" + "github.com/sirupsen/logrus" +) + +// syncRss 同步所有频道 +// 返回:更新的频道&订阅信息 map[int64]*RssClientView +// 1. 获取所有频道 +// 2. 遍历所有频道,检查频道是否更新 +// 3. 如果更新,获取更新的内容,但是返回的数据 +func (repo *rssDomain) syncRss(ctx context.Context) (updated map[int64]*RssClientView, err error) { + updated = make(map[int64]*RssClientView) + // 获取所有频道 + sources, err := repo.storage.GetSources(ctx) + if err != nil { + return + } + // 遍历所有源,获取每个channel对应的rss内容 + rssView := make([]*RssClientView, len(sources)) + for i, channel := range sources { + var feed *gofeed.Feed + // 从site获取rss内容 + feed, err = repo.rssHubClient.FetchFeed(channel.RssHubFeedPath) + // 如果获取失败,则跳过 + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] fetch path(%+v) error: %v", channel.RssHubFeedPath, err) + continue + } + rv := convertFeedToRssView(0, channel.RssHubFeedPath, feed) + rssView[i] = rv + } + // 检查频道是否更新 + for _, cv := range rssView { + if cv == nil { + continue + } + var needUpdate bool + needUpdate, err = repo.checkSourceNeedUpdate(ctx, cv.Source) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] checkSourceNeedUpdate error: %v", err) + err = nil + continue + } + // 保存 + logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %+v, need update(real): %v", cv.Source, needUpdate) + // 如果需要更新,更新channel 和 content + if needUpdate { + err = repo.storage.UpsertSource(ctx, cv.Source) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert source error: %v", err) + } + } + var updateChannelView = &RssClientView{Source: cv.Source, Contents: []*RssContent{}} + err = repo.processContentsUpdate(ctx, cv, updateChannelView) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] processContentsUpdate error: %v", err) + continue + } + if len(updateChannelView.Contents) == 0 { + logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %s, no new content", cv.Source.RssHubFeedPath) + continue + } + updateChannelView.Sort() + updated[updateChannelView.Source.ID] = updateChannelView + logrus.WithContext(ctx).Debugf("[rsshub syncRss] cv %s, new contents: %v", cv.Source.RssHubFeedPath, len(updateChannelView.Contents)) + } + return +} + +// checkSourceNeedUpdate 检查频道是否需要更新 +func (repo *rssDomain) checkSourceNeedUpdate(ctx context.Context, source *RssSource) (needUpdate bool, err error) { + var sourceInDB *RssSource + sourceInDB, err = repo.storage.GetSourceByRssHubFeedLink(ctx, source.RssHubFeedPath) + if err != nil { + return + } + if sourceInDB == nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] source not found: %v", source.RssHubFeedPath) + return + } + source.ID = sourceInDB.ID + // 检查是否需要更新到db + if sourceInDB.IfNeedUpdate(source) { + needUpdate = true + } + return +} + +// processContentsUpdate 处理内容(s)更新 +func (repo *rssDomain) processContentsUpdate(ctx context.Context, cv *RssClientView, updateChannelView *RssClientView) error { + var err error + for _, content := range cv.Contents { + if content == nil { + continue + } + content.RssSourceID = cv.Source.ID + var existed bool + existed, err = repo.processContentItemUpdate(ctx, content) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert content error: %v", err) + err = nil + continue + } + if !existed { + updateChannelView.Contents = append(updateChannelView.Contents, content) + logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %s, add new content: %v", cv.Source.RssHubFeedPath, content.Title) + } + } + return err +} + +// processContentItemUpdate 处理单个内容更新 +func (repo *rssDomain) processContentItemUpdate(ctx context.Context, content *RssContent) (existed bool, err error) { + existed, err = repo.storage.IsContentHashIDExist(ctx, content.HashID) + if err != nil { + return + } + // 不需要更新&不需要发送 + if existed { + return + } + // 保存 + err = repo.storage.UpsertContent(ctx, content) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert content error: %v", err) + return + } + return +} diff --git a/plugin/rsshub/domain/model.go b/plugin/rsshub/domain/model.go new file mode 100644 index 0000000000..3e3e2cd662 --- /dev/null +++ b/plugin/rsshub/domain/model.go @@ -0,0 +1,118 @@ +package domain + +import ( + "encoding/hex" + "hash/fnv" + "sort" + "time" +) + +// ======== RSS ========[START] + +func genHashForFeedItem(link, guid string) string { + h := fnv.New32() + // 分三次写入数据:link、分隔符、guid + _, _ = h.Write([]byte(link)) + _, _ = h.Write([]byte("||")) + _, _ = h.Write([]byte(guid)) + + encoded := hex.EncodeToString(h.Sum(nil)) + return encoded +} + +// RssClientView 频道视图 +type RssClientView struct { + Source *RssSource + Contents []*RssContent +} + +// ======== RSS ========[END] + +// ======== DB ========[START] + +const ( + tableNameRssSource = "rss_source" + tableNameRssContent = "rss_content" + tableNameRssSubscribe = "rss_subscribe" +) + +// RssSource RSS频道 +type RssSource struct { + // Id 自增id + ID int64 `gorm:"column:id;primary_key;AUTO_INCREMENT"` + // RssHubFeedPath 频道路由 用于区分rss_hub 不同的频道 例如: `/bangumi/tv/calendar/today` + RssHubFeedPath string `gorm:"column:rss_hub_feed_path;not null;unique;" json:"rss_hub_feed_path"` + // Title 频道标题 + Title string `gorm:"column:title" json:"title"` + // ChannelDesc 频道描述 + ChannelDesc string `gorm:"column:channel_desc" json:"channel_desc"` + // ImageURL 频道图片 + ImageURL string `gorm:"column:image_url" json:"image_url"` + // Link 频道链接 + Link string `gorm:"column:link" json:"link"` + // UpdatedParsed RSS页面更新时间 + UpdatedParsed time.Time `gorm:"column:updated_parsed" json:"updated_parsed"` + // Mtime update time + Mtime time.Time `gorm:"column:mtime;default:current_timestamp;" json:"mtime"` +} + +// TableName ... +func (RssSource) TableName() string { + return tableNameRssSource +} + +// IfNeedUpdate ... +func (r RssSource) IfNeedUpdate(cmp *RssSource) bool { + if r.Link != cmp.Link { + return false + } + return r.UpdatedParsed.Unix() < cmp.UpdatedParsed.Unix() +} + +// RssContent 订阅的RSS频道的推送信息 +type RssContent struct { + // Id 自增id + ID int64 `gorm:"column:id;primary_key;AUTO_INCREMENT"` + HashID string `gorm:"column:hash_id;unique" json:"hash_id"` + RssSourceID int64 `gorm:"column:rss_source_id;not null" json:"rss_source_id"` + Title string `gorm:"column:title" json:"title"` + Description string `gorm:"column:description" json:"description"` + Link string `gorm:"column:link" json:"link"` + Date time.Time `gorm:"column:date" json:"date"` + Author string `gorm:"column:author" json:"author"` + Thumbnail string `gorm:"column:thumbnail" json:"thumbnail"` + Content string `gorm:"column:content" json:"content"` + // Mtime update time + Mtime time.Time `gorm:"column:mtime;default:current_timestamp;" json:"mtime"` +} + +// TableName ... +func (RssContent) TableName() string { + return tableNameRssContent +} + +// Sort ... order by Date desc +func (r *RssClientView) Sort() { + sort.Slice(r.Contents, func(i, j int) bool { + return r.Contents[i].Date.Unix() > r.Contents[j].Date.Unix() + }) +} + +// RssSubscribe 订阅关系表:群组-RSS频道 +type RssSubscribe struct { + // Id 自增id + ID int64 `gorm:"column:id;primary_key;AUTO_INCREMENT"` + // 订阅群组 + GroupID int64 `gorm:"column:group_id;not null;uniqueIndex:uk_sid_gid"` + // 订阅频道 + RssSourceID int64 `gorm:"column:rss_source_id;not null;uniqueIndex:uk_sid_gid"` + // Mtime update time + Mtime time.Time `gorm:"column:mtime;default:current_timestamp;" json:"mtime"` +} + +// TableName ... +func (RssSubscribe) TableName() string { + return tableNameRssSubscribe +} + +// ======== DB ========[END] diff --git a/plugin/rsshub/domain/rawFeed.go b/plugin/rsshub/domain/rawFeed.go new file mode 100644 index 0000000000..0b29fe32b6 --- /dev/null +++ b/plugin/rsshub/domain/rawFeed.go @@ -0,0 +1,101 @@ +package domain + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/FloatTech/floatbox/web" + "github.com/mmcdole/gofeed" + "github.com/sirupsen/logrus" +) + +var ( + // RSSHubMirrors RSSHub镜像站地址列表,第一个为默认地址 + rssHubMirrors = []string{ + "https://rsshub.rssforever.com", + "https://rss.injahow.cn", + } +) + +// RssHubClient rss hub client (http) +type RssHubClient struct { + *http.Client +} + +// FetchFeed 获取rss feed信息 +func (c *RssHubClient) FetchFeed(path string) (feed *gofeed.Feed, err error) { + var data []byte + // 遍历 rssHubMirrors,直到获取成功 + for _, mirror := range rssHubMirrors { + data, err = web.RequestDataWith(c.Client, mirror+path, "GET", "", web.RandUA(), nil) + if err == nil && len(data) > 0 { + break + } + } + if err != nil { + logrus.Errorf("[rsshub FetchFeed] fetch feed error: %v", err) + return nil, err + } + if len(data) == 0 { + logrus.Errorf("[rsshub FetchFeed] fetch feed error: data is empty") + return nil, errors.New("feed data is empty") + } + feed, err = gofeed.NewParser().Parse(bytes.NewBuffer(data)) + if err != nil { + return + } + return +} + +func convertFeedToRssView(channelID int64, cPath string, feed *gofeed.Feed) (view *RssClientView) { + var imgURL string + if feed.Image != nil { + imgURL = feed.Image.URL + } + view = &RssClientView{ + Source: &RssSource{ + ID: channelID, + RssHubFeedPath: cPath, + Title: feed.Title, + ChannelDesc: feed.Description, + ImageURL: imgURL, + Link: feed.Link, + UpdatedParsed: *(feed.UpdatedParsed), + Mtime: time.Now(), + }, + // 不用定长,后面可能会过滤一些元素再append + Contents: []*RssContent{}, + } + // convert feed items to rss content + for _, item := range feed.Items { + if item.Link == "" || item.Title == "" { + continue + } + var thumbnail string + if item.Image != nil { + thumbnail = item.Image.URL + } + var publishedParsed = item.PublishedParsed + if publishedParsed == nil { + publishedParsed = &time.Time{} + } + aus, _ := json.Marshal(item.Authors) + view.Contents = append(view.Contents, &RssContent{ + ID: 0, + HashID: genHashForFeedItem(item.Link, item.GUID), + RssSourceID: channelID, + Title: item.Title, + Description: item.Description, + Link: item.Link, + Date: *publishedParsed, + Author: string(aus), + Thumbnail: thumbnail, + Content: item.Content, + Mtime: time.Now(), + }) + } + return +} diff --git a/plugin/rsshub/domain/rssHub.go b/plugin/rsshub/domain/rssHub.go new file mode 100644 index 0000000000..1f652032ad --- /dev/null +++ b/plugin/rsshub/domain/rssHub.go @@ -0,0 +1,192 @@ +package domain + +import ( + "context" + "errors" + "net/http" + "os" + "time" + + "github.com/jinzhu/gorm" + "github.com/sirupsen/logrus" +) + +// RssDomain RssRepo定义 +type RssDomain interface { + // Subscribe 订阅Rss频道 + Subscribe(ctx context.Context, gid int64, route string) (rv *RssClientView, isChannelExisted, + isSubExisted bool, err error) + // Unsubscribe 取消订阅Rss频道 + Unsubscribe(ctx context.Context, gid int64, route string) (err error) + // GetSubscribedChannelsByGroupID 获取群组订阅的Rss频道 + GetSubscribedChannelsByGroupID(ctx context.Context, gid int64) (rv []*RssClientView, err error) + // Sync 同步Rss频道 + // 返回群组-频道推送视图 map[群组]推送内容数组 + Sync(ctx context.Context) (groupView map[int64][]*RssClientView, err error) +} + +// rssDomain RssRepo定义 +type rssDomain struct { + storage RepoStorage + rssHubClient *RssHubClient +} + +// NewRssDomain 新建RssDomain,调用方保证单例模式 +func NewRssDomain(dbPath string) (RssDomain, error) { + return newRssDomain(dbPath) +} + +func newRssDomain(dbPath string) (*rssDomain, error) { + if _, err := os.Stat(dbPath); err != nil || os.IsNotExist(err) { + // 生成文件 + f, err := os.Create(dbPath) + if err != nil { + return nil, err + } + defer f.Close() + } + orm, err := gorm.Open("sqlite3", dbPath) + if err != nil { + logrus.Errorf("[rsshub NewRssDomain] open db error: %v", err) + panic(err) + } + repo := &rssDomain{ + storage: &repoStorage{orm: orm}, + rssHubClient: &RssHubClient{Client: http.DefaultClient}, + } + err = repo.storage.initDB() + if err != nil { + logrus.Errorf("[rsshub NewRssDomain] open db error: %v", err) + panic(err) + } + return repo, nil +} + +// Subscribe QQ群订阅Rss频道 +func (repo *rssDomain) Subscribe(ctx context.Context, gid int64, feedPath string) ( + rv *RssClientView, isChannelExisted, isSubExisted bool, err error) { + // 验证 + feed, err := repo.rssHubClient.FetchFeed(feedPath) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] add source error: %v", err) + return + } + logrus.WithContext(ctx).Infof("[rsshub Subscribe] try get source success: %v", len(feed.Title)) + // 新建source结构体 + rv = convertFeedToRssView(0, feedPath, feed) + feedChannel, err := repo.storage.GetSourceByRssHubFeedLink(ctx, feedPath) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query source by feedPath error: %v", err) + return + } + // 如果已经存在 + if feedChannel != nil { + logrus.WithContext(ctx).Warningf("[rsshub Subscribe] source existed: %v", feedChannel) + isChannelExisted = true + } else { + // 不存在的情况,要把更新时间置空,保证下一次同步时能够更新 + rv.Source.UpdatedParsed = time.Time{} + } + // 保存 + err = repo.storage.UpsertSource(ctx, rv.Source) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] save source error: %v", err) + return + } + logrus.Infof("[rsshub Subscribe] save/update source success %v", rv.Source.ID) + // 添加群号到订阅 + subscribe, err := repo.storage.GetSubscribeByID(ctx, gid, rv.Source.ID) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query subscribe error: %v", err) + return + } + logrus.WithContext(ctx).Infof("[rsshub Subscribe] query subscribe success: %v", subscribe) + // 如果已经存在,直接返回 + if subscribe != nil { + isSubExisted = true + logrus.WithContext(ctx).Infof("[rsshub Subscribe] subscribe existed: %v", subscribe) + return + } + // 如果不存在,保存 + err = repo.storage.CreateSubscribe(ctx, gid, rv.Source.ID) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] save subscribe error: %v", err) + return + } + logrus.WithContext(ctx).Infof("[rsshub Subscribe] success: %v", len(rv.Contents)) + return +} + +// Unsubscribe 群组取消订阅 +func (repo *rssDomain) Unsubscribe(ctx context.Context, gid int64, feedPath string) (err error) { + existedSubscribes, ifExisted, err := repo.storage.GetIfExistedSubscribe(ctx, gid, feedPath) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query sub by route error: %v", err) + return errors.New("数据库错误") + } + logrus.WithContext(ctx).Infof("[rsshub Subscribe] query source by route success: %v", existedSubscribes) + // 如果不存在订阅关系,直接返回 + if !ifExisted || existedSubscribes == nil { + logrus.WithContext(ctx).Infof("[rsshub Subscribe] source existed: %v", ifExisted) + return errors.New("频道不存在") + } + err = repo.storage.DeleteSubscribe(ctx, existedSubscribes.ID) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] delete source error: %v", err) + return errors.New("删除失败") + } + // 查询是否还有群订阅这个频道 + subscribesNeedsToDel, err := repo.storage.GetSubscribesBySource(ctx, feedPath) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query source by route error: %v", err) + return + } + // 没有群订阅的时候,把频道删除 + if len(subscribesNeedsToDel) == 0 { + err = repo.storage.DeleteSource(ctx, existedSubscribes.RssSourceID) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Subscribe] delete source error: %v", err) + return errors.New("清除频道信息失败") + } + } + return +} + +// GetSubscribedChannelsByGroupID 获取群对应的订阅的频道信息 +func (repo *rssDomain) GetSubscribedChannelsByGroupID(ctx context.Context, gid int64) ([]*RssClientView, error) { + channels, err := repo.storage.GetSubscribedChannelsByGroupID(ctx, gid) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub GetSubscribedChannelsByGroupID] GetSubscribedChannelsByGroupID error: %v", err) + return nil, err + } + rv := make([]*RssClientView, len(channels)) + logrus.WithContext(ctx).Infof("[rsshub GetSubscribedChannelsByGroupID] query subscribe success: %v", len(channels)) + for i, cn := range channels { + rv[i] = &RssClientView{ + Source: cn, + } + } + return rv, nil +} + +// Sync 同步任务,按照群组订阅情况做好map切片 +func (repo *rssDomain) Sync(ctx context.Context) (groupView map[int64][]*RssClientView, err error) { + groupView = make(map[int64][]*RssClientView) + // 获取所有Rss频道 + // 获取所有频道 + updatedViews, err := repo.syncRss(ctx) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Sync] sync rss feed error: %v", err) + return + } + logrus.WithContext(ctx).Infof("[rsshub Sync] updated channels: %v", len(updatedViews)) + subscribes, err := repo.storage.GetSubscribes(ctx) + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub Sync] get subscribes error: %v", err) + return + } + for _, subscribe := range subscribes { + groupView[subscribe.GroupID] = append(groupView[subscribe.GroupID], updatedViews[subscribe.RssSourceID]) + } + return +} diff --git a/plugin/rsshub/domain/rssHub_test.go b/plugin/rsshub/domain/rssHub_test.go new file mode 100644 index 0000000000..451795931d --- /dev/null +++ b/plugin/rsshub/domain/rssHub_test.go @@ -0,0 +1,105 @@ +package domain + +import ( + "context" + "encoding/json" + "testing" +) + +func TestNewRssDomain(t *testing.T) { + dm, err := newRssDomain("rsshub.db") + if err != nil { + t.Fatal(err) + return + } + if dm == nil { + t.Fatal("domain is nil") + } +} + +//var testRssHubChannelUrl = "https://rsshub.rssforever.com/bangumi/tv/calendar/today" + +var dm, _ = newRssDomain("rsshub.db") + +func TestSub(t *testing.T) { + testCases := []struct { + name string + feedLink string + gid int64 + }{ + { + name: "test1", + feedLink: "/bangumi/tv/calendar/today", + gid: 99, + }, + { + name: "test2", + feedLink: "/go-weekly", + gid: 99, + }, + { + name: "test3", + feedLink: "/go-weekly", + gid: 123, + }, + { + name: "test3", + feedLink: "/go-weekly", + gid: 321, + }, + { + name: "test3", + feedLink: "/go-weekly", + gid: 4123, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + channel, ifExisted, ifSub, err := dm.Subscribe(ctx, tc.gid, tc.feedLink) + if err != nil { + t.Fatal(err) + return + } + t.Logf("[TEST] add sub res: %+v,%+v,%+v\n", channel, ifExisted, ifSub) + res, ext, err := dm.storage.GetIfExistedSubscribe(ctx, tc.gid, tc.feedLink) + if err != nil { + t.Fatal(err) + return + } + t.Logf("[TEST] if exist: %+v,%+v", res, ext) + channels, err := dm.GetSubscribedChannelsByGroupID(ctx, 2) + if err != nil { + t.Fatal(err) + return + } + t.Logf("[TEST] 2 channels: %+v", channels) + // del + //err = dm.Unsubscribe(ctx, tc.gid, tc.feedLink) + //if err != nil { + // t.Fatal(err) + // return + //} + //res, ext, err = dm.storage.GetIfExistedSubscribe(ctx, tc.gid, tc.feedLink) + //if err != nil { + // t.Fatal(err) + // return + //} + //t.Logf("[TEST] after del: %+v,%+v", res, ext) + //if res != nil || ext { + // t.Fatal("delete failed") + //} + + }) + } +} + +func Test_SyncFeed(t *testing.T) { + feed, err := dm.Sync(context.Background()) + if err != nil { + t.Fatal(err) + return + } + rs, _ := json.Marshal(feed) + t.Logf("[Test] feed: %+v", string(rs)) +} diff --git a/plugin/rsshub/domain/storageImpl.go b/plugin/rsshub/domain/storageImpl.go new file mode 100644 index 0000000000..842411defb --- /dev/null +++ b/plugin/rsshub/domain/storageImpl.go @@ -0,0 +1,47 @@ +package domain + +import "context" + +// RepoContent RSS 推送信息存储接口 +type RepoContent interface { + // UpsertContent 添加一条文章 + UpsertContent(ctx context.Context, content *RssContent) error + // DeleteSourceContents 删除订阅源的所有文章,返回被删除的文章数 + DeleteSourceContents(ctx context.Context, channelID int64) (int64, error) + // IsContentHashIDExist hash id 对应的文章是否已存在 + IsContentHashIDExist(ctx context.Context, hashID string) (bool, error) +} + +// RepoSource RSS 订阅源存储接口 +type RepoSource interface { + // UpsertSource 添加一个订阅源 + UpsertSource(ctx context.Context, rfc *RssSource) error + // GetSources 获取所有订阅源信息 + GetSources(ctx context.Context) ([]RssSource, error) + // GetSourceByRssHubFeedLink 通过 rssHub 的 feed 链接获取订阅源信息 + GetSourceByRssHubFeedLink(ctx context.Context, url string) (*RssSource, error) + // DeleteSource 删除一个订阅源 + DeleteSource(ctx context.Context, fID int64) error +} + +// RepoSubscribe RSS 订阅存储接口 +type RepoSubscribe interface { + // CreateSubscribe 添加一个订阅 + CreateSubscribe(ctx context.Context, gid, rssSourceID int64) error + // DeleteSubscribe 删除一个订阅 + DeleteSubscribe(ctx context.Context, subscribeID int64) error + // GetSubscribeByID 获取一个订阅 + GetSubscribeByID(ctx context.Context, gid int64, subscribeID int64) (*RssSubscribe, error) + // GetSubscribes 获取全部订阅 + GetSubscribes(ctx context.Context) ([]*RssSubscribe, error) +} + +// RepoMultiQuery 多表查询接口 +type RepoMultiQuery interface { + // GetSubscribesBySource 获取一个源对应的所有订阅群组 + GetSubscribesBySource(ctx context.Context, feedPath string) ([]*RssSubscribe, error) + // GetIfExistedSubscribe 判断一个群组是否已订阅了一个源 + GetIfExistedSubscribe(ctx context.Context, gid int64, feedPath string) (*RssSubscribe, bool, error) + // GetSubscribedChannelsByGroupID 获取该群所有的订阅 + GetSubscribedChannelsByGroupID(ctx context.Context, gid int64) ([]*RssSource, error) +} diff --git a/plugin/rsshub/domain/storageRepo.go b/plugin/rsshub/domain/storageRepo.go new file mode 100644 index 0000000000..8698a8d996 --- /dev/null +++ b/plugin/rsshub/domain/storageRepo.go @@ -0,0 +1,280 @@ +package domain + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jinzhu/gorm" + "github.com/sirupsen/logrus" +) + +// RepoStorage 定义RepoStorage接口 +type RepoStorage interface { + RepoContent + RepoSource + RepoSubscribe + RepoMultiQuery + initDB() error +} + +// repoStorage db struct for rss +type repoStorage struct { + orm *gorm.DB +} + +// initDB ... +func (s *repoStorage) initDB() (err error) { + err = s.orm.AutoMigrate(&RssSource{}, &RssContent{}, &RssSubscribe{}).Error + if err != nil { + logrus.Errorf("[rsshub initDB] error: %v", err) + return err + } + return nil + // s.orm.LogMode(true) +} + +// GetSubscribesBySource Impl +func (s *repoStorage) GetSubscribesBySource(ctx context.Context, feedPath string) ([]*RssSubscribe, error) { + logrus.WithContext(ctx).Infof("[rsshub GetSubscribesBySource] feedPath: %s", feedPath) + rs := make([]*RssSubscribe, 0) + err := s.orm.Model(&RssSubscribe{}).Joins(fmt.Sprintf("%s left join %s on %s.rss_source_id=%s.id", tableNameRssSubscribe, tableNameRssSource, tableNameRssSubscribe, tableNameRssSource)). + Where("rss_source.rss_hub_feed_path = ?", feedPath).Select("rss_subscribe.*").Find(&rs).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + logrus.WithContext(ctx).Errorf("[rsshub GetSubscribesBySource] error: %v", err) + return nil, err + } + return rs, nil +} + +// GetIfExistedSubscribe Impl +func (s *repoStorage) GetIfExistedSubscribe(ctx context.Context, gid int64, feedPath string) (*RssSubscribe, bool, error) { + rs := RssSubscribe{} + + err := s.orm.Table(tableNameRssSubscribe). + Select("rss_subscribe.id, rss_subscribe.group_id, rss_subscribe.rss_source_id, rss_subscribe.mtime"). + Joins(fmt.Sprintf("INNER JOIN %s ON %s.rss_source_id=%s.id", + tableNameRssSource, tableNameRssSubscribe, tableNameRssSource)). + Where("rss_source.rss_hub_feed_path = ? AND rss_subscribe.group_id = ?", feedPath, gid).Scan(&rs).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, false, nil + } + logrus.WithContext(ctx).Errorf("[rsshub GetIfExistedSubscribe] error: %v", err) + return nil, false, err + } + if rs.ID == 0 { + return nil, false, nil + } + return &rs, true, nil +} + +// ==================== RepoSource ==================== [Start] + +// UpsertSource Impl +func (s *repoStorage) UpsertSource(ctx context.Context, source *RssSource) (err error) { + // Update columns to default value on `id` conflict + querySource := &RssSource{RssHubFeedPath: source.RssHubFeedPath} + err = s.orm.First(querySource, "rss_hub_feed_path = ?", querySource.RssHubFeedPath).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + err = s.orm.Create(source).Omit("id").Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] add source error: %v", err) + return + } + } + return + } + source.ID = querySource.ID + logrus.WithContext(ctx).Infof("[rsshub] update source: %+v", source.UpdatedParsed) + err = s.orm.Model(&source).Where(&RssSource{ID: source.ID}). + Updates(&RssSource{ + Title: source.Title, + ChannelDesc: source.ChannelDesc, + ImageURL: source.ImageURL, + Link: source.Link, + UpdatedParsed: source.UpdatedParsed, + Mtime: time.Now(), + }).Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] update source error: %v", err) + return + } + logrus.Println("[rsshub] add source success: ", source.ID) + return nil +} + +// GetSources Impl +func (s *repoStorage) GetSources(ctx context.Context) (sources []RssSource, err error) { + sources = []RssSource{} + err = s.orm.Find(&sources, "id > 0").Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("source not found") + } + logrus.WithContext(ctx).Errorf("[rsshub] get sources error: %v", err) + return + } + logrus.WithContext(ctx).Infof("[rsshub] get sources success: %d", len(sources)) + return +} + +// GetSourceByRssHubFeedLink Impl +func (s *repoStorage) GetSourceByRssHubFeedLink(ctx context.Context, rssHubFeedLink string) (source *RssSource, err error) { + source = &RssSource{RssHubFeedPath: rssHubFeedLink} + err = s.orm.Take(source, source).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + logrus.WithContext(ctx).Errorf("[rsshub] get source error: %v", err) + return + } + return +} + +// DeleteSource Impl +func (s *repoStorage) DeleteSource(ctx context.Context, fID int64) (err error) { + err = s.orm.Delete(&RssSource{}, "id = ?", fID).Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSource: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("source not found") + } + return + } + return nil +} + +// ==================== RepoSource ==================== [End] + +// ==================== RepoContent ==================== [Start] + +// UpsertContent Impl +func (s *repoStorage) UpsertContent(ctx context.Context, content *RssContent) (err error) { + // check params + if content == nil { + err = errors.New("content is nil") + return + } + // check params.RssHubFeedPath and params.HashID + if content.RssSourceID < 0 || content.HashID == "" || content.Title == "" { + err = errors.New("content.RssSourceID or content.HashID or content.Title is empty") + return + } + err = s.orm.Create(content).Omit("id").Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] storage.UpsertContent: %v", err) + return + } + return +} + +// DeleteSourceContents Impl +func (s *repoStorage) DeleteSourceContents(ctx context.Context, channelID int64) (rows int64, err error) { + err = s.orm.Delete(&RssSubscribe{}).Where(&RssSubscribe{RssSourceID: channelID}).Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSourceContents: %v", err) + return + } + return +} + +// IsContentHashIDExist Impl +func (s *repoStorage) IsContentHashIDExist(ctx context.Context, hashID string) (bool, error) { + wanted := &RssContent{HashID: hashID} + err := s.orm.Take(wanted, wanted).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + logrus.WithContext(ctx).Errorf("[rsshub] storage.IsContentHashIDExist: %v", err) + return false, err + } + return true, nil +} + +// ==================== RepoContent ==================== [End] + +// ==================== RepoSubscribe ==================== [Start] + +// CreateSubscribe Impl +func (s *repoStorage) CreateSubscribe(ctx context.Context, gid, rssSourceID int64) (err error) { + // check subscribe + if rssSourceID < 0 || gid == 0 { + err = errors.New("gid or rssSourceID is empty") + return + } + err = s.orm.Create(&RssSubscribe{GroupID: gid, RssSourceID: rssSourceID}).Omit("id").Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] storage.CreateSubscribe: %v", err) + return + } + return +} + +// DeleteSubscribe Impl +func (s *repoStorage) DeleteSubscribe(ctx context.Context, subscribeID int64) (err error) { + err = s.orm.Delete(&RssSubscribe{}, "id = ?", subscribeID).Error + if err != nil { + logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSubscribe error: %v", err) + return + } + return +} + +// GetSubscribeByID Impl +func (s *repoStorage) GetSubscribeByID(ctx context.Context, gid int64, subscribeID int64) (res *RssSubscribe, err error) { + res = &RssSubscribe{} + err = s.orm.First(res, &RssSubscribe{GroupID: gid, RssSourceID: subscribeID}).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribeByID: %v", err) + return nil, err + } + return +} + +// GetSubscribedChannelsByGroupID Impl +func (s *repoStorage) GetSubscribedChannelsByGroupID(ctx context.Context, gid int64) (res []*RssSource, err error) { + res = make([]*RssSource, 0) + err = s.orm.Model(&RssSource{}). + Joins(fmt.Sprintf("join %s on rss_source_id=%s.id", tableNameRssSubscribe, tableNameRssSource)).Where("rss_subscribe.group_id = ?", gid). + Select("rss_source.*"). + Find(&res). + Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + err = nil + return + } + logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribedChannelsByGroupID: %v", err) + return + } + return +} + +// GetSubscribes Impl +func (s *repoStorage) GetSubscribes(ctx context.Context) (res []*RssSubscribe, err error) { + res = make([]*RssSubscribe, 0) + err = s.orm.Find(&res).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + err = nil + return + } + logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribes: %v", err) + return + } + return +} + +// ==================== RepoSubscribe ==================== [End] diff --git a/plugin/rsshub/main.go b/plugin/rsshub/main.go new file mode 100644 index 0000000000..ff4cce1088 --- /dev/null +++ b/plugin/rsshub/main.go @@ -0,0 +1,152 @@ +// Package rsshub rss_hub订阅插件 +package rsshub + +import ( + "context" + "fmt" + "regexp" + + ctrl "github.com/FloatTech/zbpctrl" + "github.com/FloatTech/zbputils/control" + zbpCtxExt "github.com/FloatTech/zbputils/ctxext" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/ZeroBot-Plugin/plugin/rsshub/domain" +) + +// 初始化 repo +var ( + rssRepo domain.RssDomain + initErr error + regexpForSQL = regexp.MustCompile(`[\^<>\[\]%&\*\(\)\{\}\|\=]|(union\s+select|update\s+|delete\s+|drop\s+|truncate\s+|insert\s+|exec\s+|declare\s+)`) +) + +var ( + // 注册插件 + engine = control.Register("rsshub", &ctrl.Options[*zero.Ctx]{ + // 默认不启动 + DisableOnDefault: false, + Brief: "RssHub订阅姬", + // 详细帮助 + Help: "RssHub订阅姬desu~ \n" + + "支持的详细订阅列表文档可见:\n" + + "https://rsshub.netlify.app/ \n" + + "- 添加rsshub订阅-/bookfere/weekly \n" + + "- 删除rsshub订阅-/bookfere/weekly \n" + + "- 查看rsshub订阅列表 \n" + + "- rsshub同步 \n" + + "Tips: 定时刷新rsshub订阅信息需要配合job一起使用, 全局只需要设置一个, 无视响应状态推送, 下为例子\n" + + "记录在\"@every 10m\"触发的指令)\n" + + "rsshub同步", + // 插件数据存储路径 + PrivateDataFolder: "rsshub", + OnEnable: func(ctx *zero.Ctx) { + ctx.SendChain(message.Text("RssHub订阅姬现在启动了哦")) + }, + OnDisable: func(ctx *zero.Ctx) { + ctx.SendChain(message.Text("RssHub订阅姬现在关闭了哦")) + }, + }).ApplySingle(zbpCtxExt.DefaultSingle) +) + +// init 命令路由 +func init() { + rssRepo, initErr = domain.NewRssDomain(engine.DataFolder() + "rsshub.db") + if initErr != nil { + logrus.Errorln("RssHub订阅姬:初始化失败", initErr) + panic(initErr) + } + engine.OnFullMatch("rsshub同步", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + // 群组-频道推送视图 map[群组]推送内容数组 + groupToFeedsMap, err := rssRepo.Sync(context.Background()) + if err != nil { + logrus.Errorln("rsshub同步失败", err) + ctx.SendPrivateMessage(zero.BotConfig.SuperUsers[0], message.Text("rsshub同步失败", err)) + return + } + // 没有更新的[群组-频道推送视图]则不推送 + if len(groupToFeedsMap) == 0 { + logrus.Info("rsshub未发现更新") + return + } + sendRssUpdateMsg(ctx, groupToFeedsMap) + }) + // 添加订阅 + engine.OnPrefix("添加rsshub订阅-", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + routeStr := ctx.State["args"].(string) + input := regexpForSQL.ReplaceAllString(routeStr, "") + logrus.Debugf("添加rsshub订阅:raw(%s), replaced(%s)", routeStr, input) + rv, _, isSubExisted, err := rssRepo.Subscribe(context.Background(), ctx.Event.GroupID, input) + if err != nil { + ctx.SendChain(message.Text("RssHub订阅姬:添加失败", err.Error())) + return + } + if isSubExisted { + ctx.SendChain(message.Text("RssHub订阅姬:已存在,更新成功")) + } else { + ctx.SendChain(message.Text("RssHub订阅姬:添加成功\n", rv.Source.Title)) + } + // 添加成功,发送订阅源快照 + msg, err := newRssDetailsMsg(ctx, rv) + if len(msg) == 0 || err != nil { + ctx.SendPrivateMessage(zero.BotConfig.SuperUsers[0], message.Text("RssHub推送错误", err)) + return + } + if id := ctx.Send(msg).ID(); id == 0 { + ctx.SendChain(message.Text("ERROR: 发送订阅源快照失败,可能被风控了")) + } + }) + engine.OnPrefix("删除rsshub订阅-", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + routeStr := ctx.State["args"].(string) + input := regexpForSQL.ReplaceAllString(routeStr, "") + logrus.Debugf("删除rsshub订阅:raw(%s), replaced(%s)", routeStr, input) + err := rssRepo.Unsubscribe(context.Background(), ctx.Event.GroupID, input) + if err != nil { + ctx.SendChain(message.Text("RssHub订阅姬:删除失败 ", err.Error())) + return + } + ctx.SendChain(message.Text(fmt.Sprintf("RssHub订阅姬:删除%s成功", input))) + }) + engine.OnFullMatch("查看rsshub订阅列表", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) { + rv, err := rssRepo.GetSubscribedChannelsByGroupID(context.Background(), ctx.Event.GroupID) + if err != nil { + ctx.SendChain(message.Text("RssHub订阅姬:查询失败 ", err.Error())) + return + } + // 添加成功,发送订阅源信息 + msg, err := newRssSourcesMsg(ctx, rv) + if err != nil { + ctx.SendChain(message.Text("RssHub订阅姬:查询失败 ", err.Error())) + return + } + if len(msg) == 0 { + ctx.SendChain(message.Text("ん? 没有订阅的频道哦~")) + return + } + ctx.SendChain(msg...) + }) +} + +// sendRssUpdateMsg 发送Rss更新消息 +func sendRssUpdateMsg(ctx *zero.Ctx, groupToFeedsMap map[int64][]*domain.RssClientView) { + for groupID, views := range groupToFeedsMap { + logrus.Infof("RssHub插件在群 %d 触发推送检查", groupID) + for _, view := range views { + if view == nil || len(view.Contents) == 0 { + continue + } + msg, err := newRssDetailsMsg(ctx, view) + if len(msg) == 0 || err != nil { + ctx.SendPrivateMessage(zero.BotConfig.SuperUsers[0], message.Text(rssHubPushErrMsg, err)) + continue + } + logrus.Infof("RssHub插件在群 %d 开始推送 %s", groupID, view.Source.Title) + ctx.SendGroupMessage(groupID, message.Text(fmt.Sprintf("%s\n该RssHub频道下有更新了哦~", view.Source.Title))) + if res := ctx.SendGroupForwardMessage(groupID, msg); !res.Exists() { + ctx.SendPrivateMessage(zero.BotConfig.SuperUsers[0], message.Text(rssHubPushErrMsg)) + } + } + } +} diff --git a/plugin/rsshub/view.go b/plugin/rsshub/view.go new file mode 100644 index 0000000000..1bc2ae7c9b --- /dev/null +++ b/plugin/rsshub/view.go @@ -0,0 +1,100 @@ +package rsshub + +import ( + "fmt" + "time" + + "github.com/FloatTech/floatbox/binary" + "github.com/FloatTech/zbputils/img/text" + "github.com/sirupsen/logrus" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/ZeroBot-Plugin/plugin/rsshub/domain" +) + +const ( + rssHubPushErrMsg = "RssHub推送错误" +) + +// formatRssViewToMessagesSlice 格式化RssClientView为消息切片 +func formatRssViewToMessagesSlice(view *domain.RssClientView) ([]message.Message, error) { + // 取前20条 + cts := view.Contents + if len(cts) > 20 { + cts = cts[:20] + } + // 2n+1条消息 + fv := make([]message.Message, len(cts)*2+1) + // 订阅源头图 + toastPic, err := text.RenderToBase64(fmt.Sprintf("%s\n\n\n%s\n\n\n更新时间:%v\n\n\n", + view.Source.Title, view.Source.Link, view.Source.UpdatedParsed.Local().Format(time.DateTime)), + text.SakuraFontFile, 1200, 40) + if err != nil { + return nil, err + } + fv[0] = message.Message{message.Image("base64://" + binary.BytesToString(toastPic))} + // 元素信息 + for idx, item := range cts { + contentStr := fmt.Sprintf("%s\n\n\n", item.Title) + // Date为空时不显示 + if !item.Date.IsZero() { + contentStr += fmt.Sprintf("更新时间:\n%v\n", item.Date.Local().Format(time.DateTime)) + } + var content []byte + content, err = text.RenderToBase64(contentStr, text.SakuraFontFile, 1200, 40) + if err != nil { + logrus.WithError(err).Error("RssHub订阅姬渲染图片失败") + continue + } + itemMessagePic := message.Message{message.Image("base64://" + binary.BytesToString(content))} + fv[2*idx+1] = itemMessagePic + fv[2*idx+2] = message.Message{message.Text(item.Link)} + } + return fv, nil +} + +// newRssSourcesMsg Rss订阅源列表 +func newRssSourcesMsg(ctx *zero.Ctx, view []*domain.RssClientView) (message.Message, error) { + var msgSlice []message.Message + // 生成消息 + for _, v := range view { + if v == nil { + continue + } + item, err := formatRssViewToMessagesSlice(v) + if err != nil { + return nil, err + } + msgSlice = append(msgSlice, item...) + } + // 伪造一个发送者为RssHub订阅姬的消息节点 + msg := make(message.Message, len(msgSlice)) + for i, item := range msgSlice { + msg[i] = fakeSenderForwardNode(ctx.Event.SelfID, item...) + } + return msg, nil +} + +// newRssDetailsMsg Rss订阅源详情(包含文章信息列表) +func newRssDetailsMsg(ctx *zero.Ctx, view *domain.RssClientView) (message.Message, error) { + // 生成消息 + msgSlice, err := formatRssViewToMessagesSlice(view) + if err != nil { + return nil, err + } + // 伪造一个发送者为RssHub订阅姬的消息节点 + msg := make(message.Message, len(msgSlice)) + for i, item := range msgSlice { + msg[i] = fakeSenderForwardNode(ctx.Event.SelfID, item...) + } + return msg, nil +} + +// fakeSenderForwardNode 伪造一个发送者为RssHub订阅姬的消息节点 +func fakeSenderForwardNode(userID int64, msgs ...message.Segment) message.Segment { + return message.CustomNode( + "RssHub订阅姬", + userID, + msgs) +} diff --git a/plugin/saucenao/searcher.go b/plugin/saucenao/searcher.go index 9afb04bad4..4c129bcc67 100644 --- a/plugin/saucenao/searcher.go +++ b/plugin/saucenao/searcher.go @@ -22,7 +22,6 @@ import ( ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" - "github.com/FloatTech/zbputils/img/pool" ) const ( @@ -73,20 +72,10 @@ func init() { // 插件主体 for i := range illust.ImageUrls { f := file.BOTPATH + "/" + illust.Path(i) n := name + "_p" + strconv.Itoa(i) - var m *pool.Image if file.IsNotExist(f) { - m, err = pool.GetImage(n) - if err == nil { - imgs = append(imgs, message.Image(m.String())) - continue - } logrus.Debugln("[saucenao]开始下载", n) logrus.Debugln("[saucenao]urls:", illust.ImageUrls) err1 := illust.DownloadToCache(i) - if err1 == nil { - m.SetFile(f) - _, _ = m.Push(ctxext.SendToSelf(ctx), ctxext.GetMessage(ctx)) - } if err1 != nil { logrus.Debugln("[saucenao]下载err:", err1) } diff --git a/plugin/score/draw.go b/plugin/score/draw.go index 33cc58b451..acb9568e0a 100644 --- a/plugin/score/draw.go +++ b/plugin/score/draw.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/FloatTech/AnimeAPI/wallet" "github.com/FloatTech/floatbox/file" "github.com/FloatTech/gg" "github.com/FloatTech/imgfactory" @@ -84,8 +85,8 @@ func drawScore16(a *scdata) (image.Image, error) { return nil, err } canvas.DrawStringAnchored(hourWord, 350, 280, 0, 0) - canvas.DrawStringAnchored("ATRI币 + "+strconv.Itoa(a.inc), 350, 350, 0, 0) - canvas.DrawStringAnchored("当前ATRI币:"+strconv.Itoa(a.score), 350, 400, 0, 0) + canvas.DrawStringAnchored(wallet.GetWalletName()+" + "+strconv.Itoa(a.inc), 350, 350, 0, 0) + canvas.DrawStringAnchored("当前"+wallet.GetWalletName()+":"+strconv.Itoa(a.score), 350, 400, 0, 0) canvas.DrawStringAnchored("LEVEL: "+strconv.Itoa(getrank(a.level)), 350, 450, 0, 0) // draw Info(Time,etc.) getTime := time.Now().Format("2006-01-02 15:04:05") @@ -161,8 +162,8 @@ func drawScore15(a *scdata) (image.Image, error) { if err = canvas.LoadFontFace(text.FontFile, float64(back.Bounds().Size().X)*0.04); err != nil { return nil, err } - canvas.DrawString(a.nickname+fmt.Sprintf(" ATRI币+%d", a.inc), float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.3) - canvas.DrawString("当前ATRI币:"+strconv.FormatInt(int64(a.score), 10), float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.4) + canvas.DrawString(a.nickname+fmt.Sprintf(" %s+%d", wallet.GetWalletName(), a.inc), float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.3) + canvas.DrawString("当前"+wallet.GetWalletName()+":"+strconv.FormatInt(int64(a.score), 10), float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.4) canvas.DrawString("LEVEL:"+strconv.FormatInt(int64(a.rank), 10), float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.5) canvas.DrawRectangle(float64(back.Bounds().Size().X)*0.1, float64(back.Bounds().Size().Y)*1.55, float64(back.Bounds().Size().X)*0.6, float64(back.Bounds().Size().Y)*0.1) canvas.SetRGB255(150, 150, 150) @@ -247,8 +248,8 @@ func drawScore17(a *scdata) (image.Image, error) { if err = canvas.ParseFontFace(data, 20); err != nil { return nil, err } - canvas.DrawStringAnchored("ATRI币 + "+strconv.Itoa(a.inc), 40, float64(imgDY-90), 0, 0) - canvas.DrawStringAnchored("当前ATRI币:"+strconv.Itoa(a.score), 40, float64(imgDY-60), 0, 0) + canvas.DrawStringAnchored(wallet.GetWalletName()+" + "+strconv.Itoa(a.inc), 40, float64(imgDY-90), 0, 0) + canvas.DrawStringAnchored("当前"+wallet.GetWalletName()+":"+strconv.Itoa(a.score), 40, float64(imgDY-60), 0, 0) canvas.DrawStringAnchored("LEVEL: "+strconv.Itoa(getrank(a.level)), 40, float64(imgDY-30), 0, 0) // Draw Info(Time, etc.) @@ -285,7 +286,6 @@ func drawScore17b2(a *scdata) (img image.Image, err error) { if err != nil { return } - back, err := gg.LoadImage(a.picfile) if err != nil { return @@ -294,30 +294,27 @@ func drawScore17b2(a *scdata) (img image.Image, err error) { bx, by := float64(back.Bounds().Dx()), float64(back.Bounds().Dy()) sc := 1280 / bx - - colors := gg.TakeColor(back, 3) + var colors []color.RGBA canvas := gg.NewContext(1280, 1280*int(by)/int(bx)) - cw, ch := float64(canvas.W()), float64(canvas.H()) sch := ch * 6 / 10 var blurback, scbackimg, backshadowimg, avatarimg, avatarbackimg, avatarshadowimg, whitetext, blacktext image.Image - var wg sync.WaitGroup - wg.Add(8) + wg := &sync.WaitGroup{} + wg.Add(7) + scback := gg.NewContext(canvas.W(), canvas.H()) + + scback.ScaleAbout(sc, sc, cw/2, ch/2) + scback.DrawImageAnchored(back, canvas.W()/2, canvas.H()/2, 0.5, 0.5) + scback.Identity() + colors = gg.TakeColor(scback.Image(), 3) go func() { defer wg.Done() - scback := gg.NewContext(canvas.W(), canvas.H()) - scback.ScaleAbout(sc, sc, cw/2, ch/2) - scback.DrawImageAnchored(back, canvas.W()/2, canvas.H()/2, 0.5, 0.5) - scback.Identity() - go func() { - defer wg.Done() - blurback = imaging.Blur(scback.Image(), 20) - }() + blurback = imaging.Blur(scback.Image(), 20) scbackimg = rendercard.Fillet(scback.Image(), 12) }() @@ -477,7 +474,7 @@ func customtext(a *scdata, fontdata []byte, cw, ch, aw float64, textcolor color. return } - canvas.DrawStringAnchored("ATRI币 + "+strconv.Itoa(a.inc), ((cw-scw)-(cw/3-scw/2))/8, (ch-sch)/2+sch/4+tempfh, 0, 0.5) + canvas.DrawStringAnchored(wallet.GetWalletName()+" + "+strconv.Itoa(a.inc), ((cw-scw)-(cw/3-scw/2))/8, (ch-sch)/2+sch/4+tempfh, 0, 0.5) canvas.DrawStringAnchored("EXP + 1", ((cw-scw)-(cw/3-scw/2))/8, (ch-sch)/2+sch/4+tempfh+canvas.FontHeight(), 0, 1) err = canvas.ParseFontFace(fontdata, (ch-sch)/2/4) @@ -485,7 +482,7 @@ func customtext(a *scdata, fontdata []byte, cw, ch, aw float64, textcolor color. return } - canvas.DrawStringAnchored("你有 "+strconv.Itoa(a.score)+" 枚ATRI币", ((cw-scw)-(cw/3-scw/2))/8, (ch-sch)/2+sch/4*3, 0, 0.5) + canvas.DrawStringAnchored("你有 "+strconv.Itoa(a.score)+" 枚"+wallet.GetWalletName(), ((cw-scw)-(cw/3-scw/2))/8, (ch-sch)/2+sch/4*3, 0, 0.5) img = canvas.Image() return diff --git a/plugin/score/model.go b/plugin/score/model.go index e0e21d8a4f..95eaadb57d 100644 --- a/plugin/score/model.go +++ b/plugin/score/model.go @@ -2,6 +2,7 @@ package score import ( "os" + "sync" "time" "github.com/jinzhu/gorm" @@ -11,7 +12,10 @@ import ( var sdb *scoredb // scoredb 分数数据库 -type scoredb gorm.DB +type scoredb struct { + db *gorm.DB + scoremu sync.Mutex +} // scoretable 分数结构体 type scoretable struct { @@ -52,25 +56,31 @@ func initialize(dbpath string) *scoredb { panic(err) } gdb.AutoMigrate(&scoretable{}).AutoMigrate(&signintable{}) - return (*scoredb)(gdb) + return &scoredb{ + db: gdb, + } } // Close ... func (sdb *scoredb) Close() error { - db := (*gorm.DB)(sdb) + db := sdb.db return db.Close() } // GetScoreByUID 取得分数 func (sdb *scoredb) GetScoreByUID(uid int64) (s scoretable) { - db := (*gorm.DB)(sdb) + sdb.scoremu.Lock() + defer sdb.scoremu.Unlock() + db := sdb.db db.Model(&scoretable{}).FirstOrCreate(&s, "uid = ? ", uid) return s } // InsertOrUpdateScoreByUID 插入或更新分数 func (sdb *scoredb) InsertOrUpdateScoreByUID(uid int64, score int) (err error) { - db := (*gorm.DB)(sdb) + sdb.scoremu.Lock() + defer sdb.scoremu.Unlock() + db := sdb.db s := scoretable{ UID: uid, Score: score, @@ -91,14 +101,18 @@ func (sdb *scoredb) InsertOrUpdateScoreByUID(uid int64, score int) (err error) { // GetSignInByUID 取得签到次数 func (sdb *scoredb) GetSignInByUID(uid int64) (si signintable) { - db := (*gorm.DB)(sdb) + sdb.scoremu.Lock() + defer sdb.scoremu.Unlock() + db := sdb.db db.Model(&signintable{}).FirstOrCreate(&si, "uid = ? ", uid) return si } // InsertOrUpdateSignInCountByUID 插入或更新签到次数 func (sdb *scoredb) InsertOrUpdateSignInCountByUID(uid int64, count int) (err error) { - db := (*gorm.DB)(sdb) + sdb.scoremu.Lock() + defer sdb.scoremu.Unlock() + db := sdb.db si := signintable{ UID: uid, Count: count, @@ -118,7 +132,9 @@ func (sdb *scoredb) InsertOrUpdateSignInCountByUID(uid int64, count int) (err er } func (sdb *scoredb) GetScoreRankByTopN(n int) (st []scoretable, err error) { - db := (*gorm.DB)(sdb) + sdb.scoremu.Lock() + defer sdb.scoremu.Unlock() + db := sdb.db err = db.Model(&scoretable{}).Order("score desc").Limit(n).Find(&st).Error return } diff --git a/plugin/score/sign_in.go b/plugin/score/sign_in.go index fd5ef0142a..3c852d150c 100644 --- a/plugin/score/sign_in.go +++ b/plugin/score/sign_in.go @@ -3,6 +3,7 @@ package score import ( "encoding/base64" + "errors" "io" "math" "math/rand" @@ -22,13 +23,14 @@ import ( "github.com/FloatTech/zbputils/ctxext" "github.com/FloatTech/zbputils/img/text" "github.com/golang/freetype" + log "github.com/sirupsen/logrus" "github.com/wcharczuk/go-chart/v2" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" ) const ( - backgroundURL = "https://iw233.cn/api.php?sort=pc" + backgroundURL = "https://pic.re/image" referer = "https://weibo.com/" signinMax = 1 // SCOREMAX 分数上限定为1200 @@ -139,13 +141,11 @@ func init() { // 更新钱包 rank := getrank(level) add := 1 + rand.Intn(10) + rank*5 // 等级越高获得的钱越高 - go func() { - err = wallet.InsertWalletOf(uid, add) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - }() + err = wallet.InsertWalletOf(uid, add) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } alldata := &scdata{ drawedfile: drawedFile, picfile: picFile, @@ -158,7 +158,7 @@ func init() { } drawimage, err := styles[k](alldata) if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) + ctx.SendChain(message.Text("签到成功,但签到图生成失败,请勿重复签到:\n", err)) return } // done. @@ -192,7 +192,7 @@ func init() { } picFile := cachePath + uidStr + time.Now().Format("20060102") + ".png" if file.IsNotExist(picFile) { - ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("请先签到!")) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("签到背景加载失败")) return } trySendImage(picFile, ctx) @@ -325,7 +325,7 @@ func getrank(count int) int { func initPic(picFile string, uid int64) (avatar []byte, err error) { defer process.SleepAbout1sTo2s() - avatar, err = web.GetData("http://q4.qlogo.cn/g?b=qq&nk=" + strconv.FormatInt(uid, 10) + "&s=640") + avatar, err = web.GetData("https://q4.qlogo.cn/g?b=qq&nk=" + strconv.FormatInt(uid, 10) + "&s=640") if err != nil { return } @@ -333,14 +333,15 @@ func initPic(picFile string, uid int64) (avatar []byte, err error) { return } url, err := bilibili.GetRealURL(backgroundURL) - if err != nil { - return - } - data, err := web.RequestDataWith(web.NewDefaultClient(), url, "", referer, "", nil) - if err != nil { - return + if err == nil { + data, err := web.RequestDataWith(web.NewDefaultClient(), url, "", referer, "", nil) + if err == nil { + return avatar, os.WriteFile(picFile, data, 0644) + } } - return avatar, os.WriteFile(picFile, data, 0644) + // 获取网络图片失败,使用本地已有的图片 + log.Error("[score:get online img error]:", err) + return avatar, copyImage(picFile) } // 使用"file:"发送图片失败后,改用base64发送 @@ -371,3 +372,47 @@ func trySendImage(filePath string, ctx *zero.Ctx) { return } } + +// 从已有签到背景中,复制出一张图片 +func copyImage(picFile string) (err error) { + // 读取目录中的文件列表,并随机挑选出一张图片 + cachePath := engine.DataFolder() + "cache/" + files, err := os.ReadDir(cachePath) + if err != nil { + return err + } + + // 随机取10次图片,取到图片就break退出 + imgNum := len(files) + var validFile string + for i := 0; i < len(files) && i < 10; i++ { + imgFile := files[rand.Intn(imgNum)] + if !imgFile.IsDir() && strings.HasSuffix(imgFile.Name(), ".png") && !strings.HasSuffix(imgFile.Name(), "signin.png") { + validFile = imgFile.Name() + break + } + } + if len(validFile) == 0 { + return errors.New("copyImage: no local image") + } + selectedFile := cachePath + validFile + + // 使用 io.Copy 复制签到背景 + srcFile, err := os.Open(selectedFile) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(picFile) + if err != nil { + return err + } + defer dstFile.Close() + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return err + } + + return err +} diff --git a/plugin/setutime/setu_geter.go b/plugin/setutime/setu_geter.go index 1e8eeeeb65..f2fbf39c59 100644 --- a/plugin/setutime/setu_geter.go +++ b/plugin/setutime/setu_geter.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strconv" - "strings" "sync" "time" @@ -25,11 +24,11 @@ import ( // Pools 图片缓冲池 type imgpool struct { - db *sql.Sqlite + db sql.Sqlite dbmu sync.RWMutex path string max int - pool map[string][]*message.MessageSegment + pool map[string][]*message.Segment poolmu sync.Mutex } @@ -45,10 +44,9 @@ func (p *imgpool) List() (l []string) { } var pool = &imgpool{ - db: &sql.Sqlite{}, path: pixiv.CacheDir, max: 10, - pool: make(map[string][]*message.MessageSegment), + pool: make(map[string][]*message.Segment), } func init() { // 插件主体 @@ -64,7 +62,7 @@ func init() { // 插件主体 getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { // 如果数据库不存在则下载 - pool.db.DBPath = engine.DataFolder() + "SetuTime.db" + pool.db = sql.New(engine.DataFolder() + "SetuTime.db") _, _ = engine.GetLazyData("SetuTime.db", false) err := pool.db.Open(time.Hour) if err != nil { @@ -158,34 +156,22 @@ func (p *imgpool) push(ctx *zero.Ctx, imgtype string, illust *pixiv.Illust) { if len(illust.ImageUrls) == 0 { return } - u := illust.ImageUrls[0] - n := u[strings.LastIndex(u, "/")+1 : len(u)-4] - m, err := imagepool.GetImage(n) - var msg message.MessageSegment + var msg message.Segment f := fileutil.BOTPATH + "/" + illust.Path(0) - if err != nil { - if fileutil.IsNotExist(f) { - // 下载图片 - if err := illust.DownloadToCache(0); err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - } - m.SetFile(f) - _, _ = m.Push(ctxext.SendToSelf(ctx), ctxext.GetMessage(ctx)) - msg = message.Image("file:///" + f) - } else { - msg = message.Image(m.String()) - if ctxext.SendToSelf(ctx)(msg) == 0 { - msg = msg.Add("cache", "0") + if fileutil.IsNotExist(f) { + // 下载图片 + if err := illust.DownloadToCache(0); err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } } + msg = message.Image("file:///" + f) p.poolmu.Lock() p.pool[imgtype] = append(p.pool[imgtype], &msg) p.poolmu.Unlock() } -func (p *imgpool) pop(imgtype string) (msg *message.MessageSegment) { +func (p *imgpool) pop(imgtype string) (msg *message.Segment) { p.poolmu.Lock() defer p.poolmu.Unlock() if p.size(imgtype) == 0 { @@ -229,9 +215,9 @@ func (p *imgpool) add(ctx *zero.Ctx, imgtype string, id int64) error { if len(illust.ImageUrls) == 0 { return errors.New("nil image url") } - err = imagepool.SendImageFromPool(strconv.FormatInt(illust.Pid, 10)+"_p0", illust.Path(0), func() error { + err = imagepool.SendImageFromPool(illust.Path(0), func(string) error { return illust.DownloadToCache(0) - }, ctxext.Send(ctx), ctxext.GetMessage(ctx)) + }, ctxext.Send(ctx)) if err != nil { return err } @@ -242,5 +228,5 @@ func (p *imgpool) add(ctx *zero.Ctx, imgtype string, id int64) error { func (p *imgpool) remove(imgtype string, id int64) error { p.dbmu.Lock() defer p.dbmu.Unlock() - return p.db.Del(imgtype, fmt.Sprintf("WHERE pid=%d", id)) + return p.db.Del(imgtype, "WHERE pid = ?", id) } diff --git a/plugin/steam/store.go b/plugin/steam/store.go index 3e49422188..a204d39abe 100644 --- a/plugin/steam/store.go +++ b/plugin/steam/store.go @@ -1,7 +1,6 @@ package steam import ( - "strconv" "sync" "time" @@ -16,7 +15,7 @@ var ( database streamDB // 开启并检查数据库链接 getDB = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - database.db.DBPath = engine.DataFolder() + "steam.db" + database.db = sql.New(engine.DataFolder() + "steam.db") err := database.db.Open(time.Hour) if err != nil { ctx.SendChain(message.Text("[steam] ERROR: ", err)) @@ -71,8 +70,7 @@ func (sdb *streamDB) update(dbInfo *player) error { func (sdb *streamDB) find(steamID int64) (dbInfo player, err error) { sdb.Lock() defer sdb.Unlock() - condition := "where steam_id = " + strconv.FormatInt(steamID, 10) - err = sdb.db.Find(tableListenPlayer, &dbInfo, condition) + err = sdb.db.Find(tableListenPlayer, &dbInfo, "WHERE steam_id = ?", steamID) if err == sql.ErrNullResult { // 规避没有该用户数据的报错 err = nil } @@ -83,8 +81,7 @@ func (sdb *streamDB) find(steamID int64) (dbInfo player, err error) { func (sdb *streamDB) findWithGroupID(steamID int64, groupID string) (dbInfo player, err error) { sdb.Lock() defer sdb.Unlock() - condition := "where steam_id = " + strconv.FormatInt(steamID, 10) + " AND target LIKE '%" + groupID + "%'" - err = sdb.db.Find(tableListenPlayer, &dbInfo, condition) + err = sdb.db.Find(tableListenPlayer, &dbInfo, "WHERE steam_id = ? AND target LIKE ?", steamID, "%"+groupID+"%") if err == sql.ErrNullResult { // 规避没有该用户数据的报错 err = nil } @@ -102,5 +99,5 @@ func (sdb *streamDB) findAll() (dbInfos []*player, err error) { func (sdb *streamDB) del(steamID int64) error { sdb.Lock() defer sdb.Unlock() - return sdb.db.Del(tableListenPlayer, "where steam_id = "+strconv.FormatInt(steamID, 10)) + return sdb.db.Del(tableListenPlayer, "WHERE steam_id = ?", steamID) } diff --git a/plugin/tarot/tarot.go b/plugin/tarot/tarot.go index 442313b817..420ea440eb 100644 --- a/plugin/tarot/tarot.go +++ b/plugin/tarot/tarot.go @@ -5,26 +5,21 @@ import ( "encoding/json" "math/rand" "os" + "path" "strconv" "strings" "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" - "github.com/FloatTech/floatbox/file" - "github.com/FloatTech/floatbox/process" - "github.com/FloatTech/floatbox/web" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" - "github.com/FloatTech/zbputils/img/pool" "github.com/FloatTech/zbputils/img/text" "github.com/sirupsen/logrus" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" ) -const bed = "https://gitcode.net/shudorcl/zbp-tarot/-/raw/master/" - type cardInfo struct { Description string `json:"description"` ReverseDescription string `json:"reverseDescription"` @@ -48,6 +43,9 @@ var ( formationMap = make(map[string]formation, 10) majorArcanaName = make([]string, 0, 80) formationName = make([]string, 0, 10) + reverse = [...]string{"", "Reverse/"} + arcanaType = [...]string{"MajorArcana", "MinorArcana"} + minorArcanaType = [...]string{"Cups", "Pentacles", "Swords", "Wands"} ) func init() { @@ -61,13 +59,25 @@ func init() { PublicDataFolder: "Tarot", }).ApplySingle(ctxext.DefaultSingle) - cache := engine.DataFolder() + "cache" - _ = os.RemoveAll(cache) - err := os.MkdirAll(cache, 0755) - if err != nil { - panic(err) + for _, r := range reverse { + for _, at := range arcanaType { + if at == "MinorArcana" { + for _, mat := range minorArcanaType { + cachePath := path.Join(engine.DataFolder(), r, at, mat) + err := os.MkdirAll(cachePath, 0755) + if err != nil { + panic(err) + } + } + } else { + cachePath := path.Join(engine.DataFolder(), r, at) + err := os.MkdirAll(cachePath, 0755) + if err != nil { + panic(err) + } + } + } } - getTarot := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { data, err := engine.GetLazyData("tarots.json", true) if err != nil { @@ -108,7 +118,6 @@ func init() { n := 1 reasons := [...]string{"您抽到的是~\n", "锵锵锵,塔罗牌的预言是~\n", "诶,让我看看您抽到了~\n"} position := [...]string{"『正位』", "『逆位』"} - reverse := [...]string{"", "Reverse/"} start := 0 length := 22 if match != "" { @@ -140,31 +149,15 @@ func init() { if p == 1 { description = card.ReverseDescription } - imgurl := bed + reverse[p] + card.ImgURL - imgname := "" - if p == 1 { - imgname = reverse[p][:len(reverse[p])-1] + name - } else { - imgname = name - } - imgpath := cache + "/" + imgname + ".png" - err := pool.SendImageFromPool("pool"+imgname, imgpath, func() error { - data, err := web.RequestDataWith(web.NewTLS12Client(), imgurl, "GET", "gitcode.net", web.RandUA(), nil) - if err != nil { - return err - } - f, err := os.Create(imgpath) - if err != nil { - return err - } - defer f.Close() - return os.WriteFile(f.Name(), data, 0755) - }, ctxext.Send(ctx), ctxext.GetMessage(ctx)) + imgurl := reverse[p] + card.ImgURL + data, err := engine.GetLazyData(imgurl, true) if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) + // ctx.SendChain(message.Text("ERROR: ", err)) + logrus.Infof("[tarot]获取图片失败: %v", err) + ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description)) return } - process.SleepAbout1sTo2s() + ctx.SendChain(message.ImageBytes(data)) ctx.SendChain(message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n其释义为: ", description)) return } @@ -185,20 +178,19 @@ func init() { if p == 1 { description = card.ReverseDescription } - imgurl := bed + reverse[p] + card.ImgURL + imgurl := reverse[p] + card.ImgURL tarotmsg := message.Message{message.Text(reasons[rand.Intn(len(reasons))], position[p], "的『", name, "』\n")} - var imgmsg message.MessageSegment + var imgmsg message.Segment var err error - if p == 1 { - imgmsg, err = poolimg(ctx, imgurl, reverse[p][:len(reverse[p])-1]+name, cache) - } else { - imgmsg, err = poolimg(ctx, imgurl, name, cache) - } + data, err := engine.GetLazyData(imgurl, true) if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return + // ctx.SendChain(message.Text("ERROR: ", err)) + logrus.Infof("[tarot]获取图片失败: %v", err) + // return + } else { + imgmsg = message.ImageBytes(data) + tarotmsg = append(tarotmsg, imgmsg) } - tarotmsg = append(tarotmsg, imgmsg) tarotmsg = append(tarotmsg, message.Text("\n其释义为: ", description)) msg[i] = ctxext.FakeSenderForwardNode(ctx, tarotmsg...) } @@ -211,14 +203,17 @@ func init() { match := ctx.State["regex_matched"].([]string)[1] info, ok := infoMap[match] if ok { - imgurl := bed + info.ImgURL + imgurl := info.ImgURL var tarotmsg message.Message - imgmsg, err := poolimg(ctx, imgurl, match, cache) + data, err := engine.GetLazyData(imgurl, true) if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return + // ctx.SendChain(message.Text("ERROR: ", err)) + logrus.Infof("[tarot]获取图片失败: %v", err) + // return + } else { + imgmsg := message.ImageBytes(data) + tarotmsg = append(tarotmsg, imgmsg) } - tarotmsg = append(tarotmsg, imgmsg) tarotmsg = append(tarotmsg, message.Text("\n", match, "的含义是~\n『正位』:", info.Description, "\n『逆位』:", info.ReverseDescription)) if id := ctx.Send(tarotmsg).ID(); id == 0 { ctx.SendChain(message.Text("ERROR: 可能被风控了")) @@ -280,19 +275,18 @@ func init() { description = card.ReverseDescription } var tarotmsg message.Message - imgurl := bed + reverse[p] + card.ImgURL - var imgmsg message.MessageSegment + imgurl := reverse[p] + card.ImgURL + var imgmsg message.Segment var err error - if p == 1 { - imgmsg, err = poolimg(ctx, imgurl, reverse[p][:len(reverse[p])-1]+name, cache) - } else { - imgmsg, err = poolimg(ctx, imgurl, name, cache) - } + data, err := engine.GetLazyData(imgurl, true) if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return + // ctx.SendChain(message.Text("ERROR: ", err)) + logrus.Infof("[tarot]获取图片失败: %v", err) + // return + } else { + imgmsg = message.ImageBytes(data) + tarotmsg = append(tarotmsg, imgmsg) } - tarotmsg = append(tarotmsg, imgmsg) build.WriteString(info.Represent[0][i]) build.WriteString(":") build.WriteString(position[p]) @@ -318,37 +312,3 @@ func init() { } }) } - -func poolimg(ctx *zero.Ctx, imgurl, imgname, cache string) (msg message.MessageSegment, err error) { - imgfile := cache + "/" + imgname + ".png" - aimgfile := file.BOTPATH + "/" + imgfile - m, err := pool.GetImage("pool" + imgname) - if err == nil { - msg = message.Image(m.String()) - if ctxext.SendToSelf(ctx)(msg) == 0 { - msg = msg.Add("cache", "0") - } - return - } - if file.IsNotExist(aimgfile) { - var data []byte - data, err = web.RequestDataWith(web.NewTLS12Client(), imgurl, "GET", "gitcode.net", web.RandUA(), nil) - if err != nil { - return - } - var f *os.File - f, err = os.Create(imgfile) - if err != nil { - return - } - defer f.Close() - err = os.WriteFile(f.Name(), data, 0755) - if err != nil { - return - } - } - m.SetFile(aimgfile) - _, _ = m.Push(ctxext.SendToSelf(ctx), ctxext.GetMessage(ctx)) - msg = message.Image("file:///" + aimgfile) - return -} diff --git a/plugin/thesaurus/chat.go b/plugin/thesaurus/chat.go index 6fc9303823..268e3e8400 100644 --- a/plugin/thesaurus/chat.go +++ b/plugin/thesaurus/chat.go @@ -1,61 +1,32 @@ -// Package thesaurus 修改过的单纯回复插件 +// Package thesaurus 修改过的单纯回复插件, 仅@触发 package thesaurus import ( - "bytes" - "encoding/json" "math/rand" - "net/http" - "os" - "strconv" "strings" - "time" - "github.com/FloatTech/floatbox/binary" + "github.com/go-ego/gse" + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/AnimeAPI/kimoi" "github.com/FloatTech/floatbox/ctxext" - "github.com/FloatTech/floatbox/file" "github.com/FloatTech/floatbox/process" - "github.com/FloatTech/floatbox/web" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" - "github.com/fumiama/jieba" - "github.com/sirupsen/logrus" - zero "github.com/wdvxdr1123/ZeroBot" - "github.com/wdvxdr1123/ZeroBot/message" - "gopkg.in/yaml.v3" ) func init() { engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, - Brief: "词典匹配回复", - Help: "- 切换[kimo|傲娇|可爱|🦙]词库\n- 设置词库触发概率0.x (0= 9 { - ctx.SendChain(message.Text("ERROR: 概率越界")) - return - } - n-- // 0~7 - gid := ctx.Event.GroupID - if gid == 0 { - gid = -ctx.Event.UserID - } - d := c.GetData(gid) - err := c.SetData(gid, (d&3)|(int64(n)<<59)) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("成功!")) - }) - engine.OnRegex(`^设置🦙API地址\s*(http.*)\s*$`, zero.SuperUserPermission, zero.OnlyPrivate).SetBlock(true).Handle(func(ctx *zero.Ctx) { - alpacapiurl = ctx.State["regex_matched"].([]string)[1] - err := os.WriteFile(alpacapifile, binary.StringToBytes(alpacapiurl), 0644) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("成功!")) - }) - engine.OnRegex(`^设置🦙token\s*([0-9a-f]{112})\s*$`, zero.SuperUserPermission, zero.OnlyPrivate).SetBlock(true).Handle(func(ctx *zero.Ctx) { - alpacatoken = ctx.State["regex_matched"].([]string)[1] - err := os.WriteFile(alpacatokenfile, binary.StringToBytes(alpacatoken), 0644) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - ctx.SendChain(message.Text("成功!")) - }) go func() { - data, err := engine.GetLazyData("dict.txt", false) - if err != nil { - panic(err) - } - seg, err := jieba.LoadDictionary(bytes.NewReader(data)) + var seg gse.Segmenter + err := seg.LoadDictEmbed() if err != nil { panic(err) } @@ -144,21 +68,6 @@ func init() { if err != nil { panic(err) } - data, err = engine.GetLazyData("kimoi.json", false) - if err != nil { - panic(err) - } - kimomap := make(kimo, 256) - err = json.Unmarshal(data, &kimomap) - if err != nil { - panic(err) - } - chatList := make([]string, 0, len(kimomap)) - for k := range kimomap { - chatList = append(chatList, k) - } - logrus.Infoln("[thesaurus]加载", len(chatList), "条kimoi") - chatListD := make([]string, 0, len(sm.D)) for k := range sm.D { chatListD = append(chatListD, k) @@ -169,89 +78,47 @@ func init() { } logrus.Infoln("[thesaurus]加载", len(chatListD), "条傲娇词库", len(chatListK), "条可爱词库") - engine.OnMessage(canmatch(tKIMO), match(chatList, seg)). - SetBlock(false). - Handle(randreply(kimomap)) - engine.OnMessage(canmatch(tDERE), match(chatListD, seg)). - SetBlock(false). - Handle(randreply(sm.D)) - engine.OnMessage(canmatch(tKAWA), match(chatListK, seg)). - SetBlock(false). - Handle(randreply(sm.K)) - engine.OnMessage(canmatch(tALPACA), func(_ *zero.Ctx) bool { - return alpacapiurl != "" && alpacatoken != "" - }).SetBlock(false).Handle(func(ctx *zero.Ctx) { + engine.OnMessage(zero.OnlyToMe, canmatch(tKIMO)). + SetBlock(false).Handle(func(ctx *zero.Ctx) { msg := ctx.ExtractPlainText() - if msg != "" { - data, err := web.RequestDataWithHeaders(http.DefaultClient, alpacapiurl+"/reply", "POST", - func(r *http.Request) error { - r.Header.Set("Authorization", alpacatoken) - return nil - }, bytes.NewReader(binary.NewWriterF(func(writer *binary.Writer) { - _ = json.NewEncoder(writer).Encode(&[]alpacamsg{{ - Name: ctx.CardOrNickName(ctx.Event.UserID), - Message: msg, - }}) - }))) - if err != nil { - logrus.Warnln("[chat] 🦙 err:", err) - return - } - type reply struct { - ID int - Msg string - } - m := reply{} - err = json.Unmarshal(data, &m) - if err != nil { - logrus.Warnln("[chat] 🦙 unmarshal err:", err) - return - } - for i := 0; i < 60; i++ { - time.Sleep(time.Second * 4) - data, err := web.RequestDataWithHeaders(http.DefaultClient, alpacapiurl+"/get?id="+strconv.Itoa(m.ID), "GET", - func(r *http.Request) error { - r.Header.Set("Authorization", alpacatoken) - return nil - }, nil) - if err != nil { - continue - } - err = json.Unmarshal(data, &m) + r, err := kimoi.Chat(msg) + if err == nil { + c := 0 + for r.Confidence < 0.2 && c < 3 { + r, err = kimoi.Chat(msg) if err != nil { - logrus.Warnln("[chat] 🦙 unmarshal err:", err) return } - if len(m.Msg) > 0 { - ctx.Send(message.Text(m.Msg)) - } + c++ + } + if r.Confidence < 0.2 { return } + ctx.Block() + ctx.SendChain(message.Text(r.Reply)) } }) + engine.OnMessage(zero.OnlyToMe, canmatch(tDERE), match(chatListD, &seg)). + SetBlock(false). + Handle(randreply(sm.D)) + engine.OnMessage(zero.OnlyToMe, canmatch(tKAWA), match(chatListK, &seg)). + SetBlock(false). + Handle(randreply(sm.K)) }() } -type kimo = map[string][]string - type simai struct { D map[string][]string `yaml:"傲娇"` K map[string][]string `yaml:"可爱"` } -type alpacamsg struct { - Name string - Message string -} - const ( tKIMO = iota tDERE tKAWA - tALPACA ) -func match(l []string, seg *jieba.Segmenter) zero.Rule { +func match(l []string, seg *gse.Segmenter) zero.Rule { return func(ctx *zero.Ctx) bool { return ctxext.JiebaSimilarity(0.66, seg, func(ctx *zero.Ctx) string { return ctx.ExtractPlainText() @@ -273,12 +140,13 @@ func canmatch(typ int64) zero.Rule { gid = -ctx.Event.UserID } d := c.GetData(gid) - return d&3 == typ && rand.Int63n(10) <= d>>59 + return ctx.ExtractPlainText() != "" && d&3 == typ } } func randreply(m map[string][]string) zero.Handler { return func(ctx *zero.Ctx) { + ctx.Block() key := ctx.State["matched"].(string) val := m[key] nick := zero.BotConfig.NickName[rand.Intn(len(zero.BotConfig.NickName))] @@ -287,6 +155,9 @@ func randreply(m map[string][]string) zero.Handler { text = strings.ReplaceAll(text, "{me}", nick) id := ctx.Event.MessageID for _, t := range strings.Split(text, "{segment}") { + if t == "" { + continue + } process.SleepAbout1sTo2s() id = ctx.SendChain(message.Reply(id), message.Text(t)) } diff --git a/plugin/tiangou/tiangou.go b/plugin/tiangou/tiangou.go index df386a4238..07b650fcfb 100644 --- a/plugin/tiangou/tiangou.go +++ b/plugin/tiangou/tiangou.go @@ -19,7 +19,7 @@ type tiangou struct { Text string `db:"text"` } -var db = &sql.Sqlite{} +var db sql.Sqlite func init() { en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ @@ -31,7 +31,7 @@ func init() { en.OnFullMatch("舔狗日记", fcext.DoOnceOnSuccess( func(ctx *zero.Ctx) bool { - db.DBPath = en.DataFolder() + "tiangou.db" + db = sql.New(en.DataFolder() + "tiangou.db") _, err := en.GetLazyData("tiangou.db", true) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) diff --git a/plugin/vitsnyaru/vitsnyaru.go b/plugin/vitsnyaru/vitsnyaru.go deleted file mode 100644 index 709bc5b471..0000000000 --- a/plugin/vitsnyaru/vitsnyaru.go +++ /dev/null @@ -1,141 +0,0 @@ -// Package vitsnyaru vits猫雷 -package vitsnyaru - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - hf "github.com/FloatTech/AnimeAPI/huggingface" - ctrl "github.com/FloatTech/zbpctrl" - "github.com/FloatTech/zbputils/control" - "github.com/tidwall/gjson" - zero "github.com/wdvxdr1123/ZeroBot" - "github.com/wdvxdr1123/ZeroBot/message" -) - -const ( - vitsnyaruRepo = "innnky/vits-nyaru" -) - -func init() { // 插件主体 - engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ - DisableOnDefault: false, - Brief: "vits猫雷", - Help: "- 让猫雷说 xxx", - PrivateDataFolder: "vitsnyaru", - }) - - // 开启 - engine.OnPrefix(`让猫雷说`).SetBlock(true). - Handle(func(ctx *zero.Ctx) { - _ctx, _cancel := context.WithTimeout(context.Background(), hf.TimeoutMax*time.Second) - defer _cancel() - ch := make(chan []byte, 1) - - args := ctx.State["args"].(string) - pushURL := fmt.Sprintf(hf.HTTPSPushPath, vitsnyaruRepo) - statusURL := fmt.Sprintf(hf.HTTPSStatusPath, vitsnyaruRepo) - ctx.SendChain(message.Text("少女祈祷中...")) - var ( - pushReq hf.PushRequest - pushRes hf.PushResponse - statusReq hf.StatusRequest - statusRes hf.StatusResponse - data []byte - ) - - // 获取clean后的文本 - pushReq = hf.PushRequest{ - Action: hf.DefaultAction, - Data: []interface{}{args}, - FnIndex: 1, - } - pushRes, err := hf.Push(pushURL, pushReq) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - statusReq = hf.StatusRequest{ - Hash: pushRes.Hash, - } - - t := time.NewTicker(time.Second * 1) - defer t.Stop() - LOOP: - for { - select { - case <-t.C: - data, err = hf.Status(statusURL, statusReq) - if err != nil { - ch <- data - break LOOP - } - if gjson.ParseBytes(data).Get("status").String() == hf.CompleteStatus { - ch <- data - break LOOP - } - case <-_ctx.Done(): - ch <- data - break LOOP - } - } - - data = <-ch - err = json.Unmarshal(data, &statusRes) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - - // 用clean的文本预测语音 - pushReq = hf.PushRequest{ - Action: hf.DefaultAction, - Data: statusRes.Data.Data, - FnIndex: 2, - } - pushRes, err = hf.Push(pushURL, pushReq) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - statusReq = hf.StatusRequest{ - Hash: pushRes.Hash, - } - - LOOP2: - for { - select { - case <-t.C: - data, err = hf.Status(statusURL, statusReq) - if err != nil { - ch <- data - break LOOP2 - } - if gjson.ParseBytes(data).Get("status").String() == hf.CompleteStatus { - ch <- data - break LOOP2 - } - case <-_ctx.Done(): - ch <- data - break LOOP2 - } - } - - data = <-ch - err = json.Unmarshal(data, &statusRes) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) - return - } - - // 发送语音 - if len(statusRes.Data.Data) < 2 { - ctx.SendChain(message.Text("ERROR: 未能获取语音")) - return - } - ctx.SendChain(message.Record("base64://" + strings.TrimPrefix(statusRes.Data.Data[1].(string), "data:audio/wav;base64,"))) - }) -} diff --git a/plugin/wallet/wallet.go b/plugin/wallet/wallet.go index 9791cf824b..b44abd9ac3 100644 --- a/plugin/wallet/wallet.go +++ b/plugin/wallet/wallet.go @@ -4,10 +4,13 @@ package wallet import ( "math" "os" + "regexp" "strconv" + "strings" "time" "github.com/FloatTech/AnimeAPI/wallet" + "github.com/FloatTech/floatbox/binary" "github.com/FloatTech/floatbox/file" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" @@ -21,24 +24,38 @@ import ( func init() { en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ - DisableOnDefault: false, - Brief: "钱包", - Help: "- 查看我的钱包\n- 查看钱包排名", + DisableOnDefault: false, + Brief: "钱包", + Help: "- 查看钱包排名\n" + + "- 设置硬币名称XX\n" + + "- 管理钱包余额[+金额|-金额][@xxx]\n" + + "- 查看我的钱包|查看钱包余额[@xxx]\n" + + "- 钱包转账[金额][@xxx]\n" + + "注:仅超级用户能“管理钱包余额”\n", PrivateDataFolder: "wallet", }) cachePath := en.DataFolder() + "cache/" + coinNameFile := en.DataFolder() + "coin_name.txt" go func() { _ = os.RemoveAll(cachePath) err := os.MkdirAll(cachePath, 0755) if err != nil { panic(err) } + // 更改硬币名称 + var coinName string + if file.IsExist(coinNameFile) { + content, err := os.ReadFile(coinNameFile) + if err != nil { + panic(err) + } + coinName = binary.BytesToString(content) + } else { + // 旧版本数据 + coinName = "ATRI币" + } + wallet.SetWalletName(coinName) }() - en.OnFullMatch("查看我的钱包").SetBlock(true).Handle(func(ctx *zero.Ctx) { - uid := ctx.Event.UserID - money := wallet.GetWalletOf(uid) - ctx.SendChain(message.At(uid), message.Text("你的钱包当前有", money, "ATRI币")) - }) en.OnFullMatch("查看钱包排名", zero.OnlyGroup).Limit(ctxext.LimitByGroup).SetBlock(true). Handle(func(ctx *zero.Ctx) { @@ -62,7 +79,7 @@ func init() { return } if len(st) == 0 { - ctx.SendChain(message.Text("ERROR: 当前没人获取过ATRI币")) + ctx.SendChain(message.Text("ERROR: 当前没人获取过", wallet.GetWalletName())) return } else if len(st) > 10 { st = st[:10] @@ -98,7 +115,7 @@ func init() { } err = chart.BarChart{ Font: font, - Title: "ATRI币排名(1天只刷新1次)", + Title: wallet.GetWalletName() + "排名(1天只刷新1次)", Background: chart.Style{ Padding: chart.Box{ Top: 40, @@ -122,4 +139,119 @@ func init() { } ctx.SendChain(message.Image("file:///" + file.BOTPATH + "/" + drawedFile)) }) + en.OnPrefix("设置硬币名称", zero.OnlyToMe, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + coinName := strings.TrimSpace(ctx.State["args"].(string)) + err := os.WriteFile(coinNameFile, binary.StringToBytes(coinName), 0644) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return + } + wallet.SetWalletName(coinName) + ctx.SendChain(message.Text("记住啦~")) + }) + + en.OnPrefix(`管理钱包余额`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + param := strings.TrimSpace(ctx.State["args"].(string)) + + // 捕获修改的金额 + re := regexp.MustCompile(`^[+-]?\d+$`) + amount, err := strconv.Atoi(re.FindString(param)) + if err != nil { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("输入的金额异常")) + return + } + + // 捕获用户QQ号,只支持@事件 + var uidStr string + if len(ctx.Event.Message) > 1 && ctx.Event.Message[1].Type == "at" { + uidStr = ctx.Event.Message[1].Data["qq"] + } else { + // 没at就修改自己的钱包 + uidStr = strconv.FormatInt(ctx.Event.UserID, 10) + } + + uidInt, err := strconv.ParseInt(uidStr, 10, 64) + if err != nil { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("QQ号处理失败")) + return + } + if amount+wallet.GetWalletOf(uidInt) < 0 { + ctx.SendChain(message.Text("管理失败:对方钱包余额不足,扣款失败")) + return + } + err = wallet.InsertWalletOf(uidInt, amount) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:管理失败,钱包坏掉了:\n", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("钱包余额修改成功,已修改用户:", uidStr, "的钱包,修改金额为:", amount)) + }) + + // 保留用户习惯,兼容旧语法“查看我的钱包” + en.OnPrefixGroup([]string{`查看钱包余额`, `查看我的钱包`}).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + param := ctx.State["args"].(string) + var uidStr string + if len(ctx.Event.Message) > 1 && ctx.Event.Message[1].Type == "at" { + uidStr = ctx.Event.Message[1].Data["qq"] + } else if param == "" { + uidStr = strconv.FormatInt(ctx.Event.UserID, 10) + } + uidInt, err := strconv.ParseInt(uidStr, 10, 64) + if err != nil { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("QQ号处理失败")) + return + } + money := wallet.GetWalletOf(uidInt) + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("QQ号:", uidStr, ",的钱包有", money, wallet.GetWalletName())) + }) + + en.OnPrefix(`钱包转账`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + param := strings.TrimSpace(ctx.State["args"].(string)) + + // 捕获修改的金额,amount扣款金额恒正(要注意符号) + re := regexp.MustCompile(`^[+]?\d+$`) + amount, err := strconv.Atoi(re.FindString(param)) + if err != nil || amount <= 0 { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("输入额异常,请检查金额或at是否正常")) + return + } + + // 捕获用户QQ号,只支持@事件 + var uidStr string + if len(ctx.Event.Message) > 1 && ctx.Event.Message[1].Type == "at" { + uidStr = ctx.Event.Message[1].Data["qq"] + } else { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("获取被转方信息失败")) + return + } + + uidInt, err := strconv.ParseInt(uidStr, 10, 64) + if err != nil { + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("QQ号处理失败")) + return + } + + // 开始转账流程 + if amount > wallet.GetWalletOf(ctx.Event.UserID) { + ctx.SendChain(message.Text("[ERROR]:钱包余额不足,转账失败")) + return + } + + err = wallet.InsertWalletOf(ctx.Event.UserID, -amount) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:转账失败,扣款异常:\n", err)) + return + } + + err = wallet.InsertWalletOf(uidInt, amount) + if err != nil { + ctx.SendChain(message.Text("[ERROR]:转账失败,转账时银行被打劫:\n", err)) + return + } + ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("转账成功:成功给"), message.At(uidInt), message.Text(",转账:", amount, wallet.GetWalletName())) + }) } diff --git a/plugin/warframeapi/main.go b/plugin/warframeapi/main.go index 1fb04d4eda..a11673f250 100644 --- a/plugin/warframeapi/main.go +++ b/plugin/warframeapi/main.go @@ -148,7 +148,7 @@ func init() { // }) // eng.OnFullMatch(`wf订阅检测`).SetBlock(true).Handle(func(ctx *zero.Ctx) { // rwm.Lock() - // var msg []message.MessageSegment + // var msg []message.Segment // for i, v := range gameTimes { // nt := time.Until(v.NextTime).Seconds() // switch { @@ -321,9 +321,9 @@ func init() { } ismod := iteminfo.ModMaxRank != 0 - max := 5 - if len(sells) < max { - max = len(sells) + maxCount := 5 + if len(sells) < maxCount { + maxCount = len(sells) } sb := strings.Builder{} if ismod { @@ -332,13 +332,13 @@ func init() { } else { msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, message.Text("请输入编号选择(30s内)\n输入c直接结束会话"))) } - for i := 0; i < max; i++ { + for i := 0; i < maxCount; i++ { // msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, // message.Text(fmt.Sprintf("[%d] (Rank:%d/%d) %dP - %s\n", i, sells[i].ModRank, iteminfo.ModMaxRank, sells[i].Platinum, sells[i].User.IngameName)))) sb.WriteString(fmt.Sprintf("[%d] (Rank:%d/%d) %dP - %s\n", i, sells[i].ModRank, iteminfo.ModMaxRank, sells[i].Platinum, sells[i].User.IngameName)) } } else { - for i := 0; i < max; i++ { + for i := 0; i < maxCount; i++ { // msgs = append(msgs, ctxext.FakeSenderForwardNode(ctx, // message.Text(fmt.Sprintf("[%d] %dP -%s\n", i, sells[i].Platinum, sells[i].User.IngameName)))) sb.WriteString(fmt.Sprintf("[%d] %dP -%s\n", i, sells[i].Platinum, sells[i].User.IngameName)) diff --git a/plugin/wenxinvilg/main.go b/plugin/wenxinvilg/main.go index 701a7eb468..36d64f8215 100644 --- a/plugin/wenxinvilg/main.go +++ b/plugin/wenxinvilg/main.go @@ -31,8 +31,8 @@ const ( ) type keydb struct { - db *sql.Sqlite sync.RWMutex + db sql.Sqlite } // db内容 @@ -48,15 +48,11 @@ type apikey struct { } var ( - name = "椛椛" - limit int - vilginfo = &keydb{ - db: &sql.Sqlite{}, - } - modelinfo = &keydb{ - db: &sql.Sqlite{}, - } - dtype = [...]string{ + name = "椛椛" + limit int + vilginfo keydb + modelinfo keydb + dtype = [...]string{ "古风", "油画", "水彩画", "卡通画", "二次元", "浮世绘", "蒸汽波艺术", "low poly", "像素风格", "概念艺术", "未来主义", "赛博朋克", "写实风格", "洛丽塔风格", "巴洛克风格", "超现实主义", } ) @@ -99,7 +95,7 @@ func init() { // 插件主体 }), )) getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - vilginfo.db.DBPath = engine.DataFolder() + "ernieVilg.db" + vilginfo.db = sql.New(engine.DataFolder() + "ernieVilg.db") err := vilginfo.db.Open(time.Hour) if err != nil { ctx.SendChain(message.Text(serviceErr, err)) @@ -301,7 +297,7 @@ func init() { // 插件主体 }), )) getmodeldb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { - modelinfo.db.DBPath = engine.DataFolder() + "ernieModel.db" + modelinfo.db = sql.New(engine.DataFolder() + "ernieModel.db") err := modelinfo.db.Open(time.Hour) if err != nil { ctx.SendChain(message.Text(modelErr, err)) @@ -415,14 +411,14 @@ func init() { // 插件主体 minlen := 1 maxlen := 128 if mun != "" { - max, err := strconv.Atoi(mun) + maxNum, err := strconv.Atoi(mun) if err != nil { ctx.SendChain(message.Text(modelErr, err)) return } - minlen = max - if max > 128 { - maxlen = max + minlen = maxNum + if maxNum > 128 { + maxlen = maxNum } } keyword := ctx.State["regex_matched"].([]string)[4] @@ -521,10 +517,10 @@ func (sql *keydb) insert(gid int64, model, akey, skey string) error { } // 获取group信息 groupinfo := apikey{} // 用于暂存数据 - err = sql.db.Find("groupinfo", &groupinfo, "where ID is "+strconv.FormatInt(gid, 10)) + err = sql.db.Find("groupinfo", &groupinfo, "WHERE ID = ?", gid) if err != nil { // 如果该group没有注册过 - err = sql.db.Find("groupinfo", &groupinfo, "where APIKey is '"+akey+"' and SecretKey is '"+skey+"'") + err = sql.db.Find("groupinfo", &groupinfo, "WHERE APIKey = ? and SecretKey = ?", akey, skey) if err == nil { // 如果key存在过将当前的数据迁移过去 groupinfo.ID = gid @@ -575,7 +571,7 @@ func (sql *keydb) checkGroup(gid int64, model string) (groupinfo apikey, err err model = "文心" } // 先判断该群是否已经设置过key了 - if ok := sql.db.CanFind("groupinfo", "where ID is "+strconv.FormatInt(gid, 10)); !ok { + if ok := sql.db.CanFind("groupinfo", "WHERE ID = ?", gid); !ok { if gid > 0 { err = errors.New("该群没有设置过apikey,请前往https://wenxin.baidu.com/moduleApi/key获取key值后,发送指令:\n为本群设置" + model + "key [API Key] [Secret Key]\n或\n为自己设置" + model + "key [API Key] [Secret Key]") } else { @@ -584,7 +580,7 @@ func (sql *keydb) checkGroup(gid int64, model string) (groupinfo apikey, err err return } // 获取group信息 - err = sql.db.Find("groupinfo", &groupinfo, "where ID is "+strconv.FormatInt(gid, 10)) + err = sql.db.Find("groupinfo", &groupinfo, "WHERE ID = ?", gid) if err != nil { return } @@ -608,19 +604,16 @@ func (sql *keydb) checkGroup(gid int64, model string) (groupinfo apikey, err err err = sql.db.Insert("groupinfo", &groupinfo) if err == nil { // 更新相同key的他人次数 - condition := "where not ID is " + strconv.FormatInt(gid, 10) + - " and APIKey = '" + groupinfo.APIKey + - "' and SecretKey = '" + groupinfo.SecretKey + "'" otherinfo := apikey{} var groups []int64 // 将相同的key的ID暂存 // 无视没有找到相同的key的err - _ = sql.db.FindFor("groupinfo", &otherinfo, condition, func() error { + _ = sql.db.FindFor("groupinfo", &otherinfo, "WHERE ID <> ? AND APIKey = ? AND SecretKey = ?", func() error { groups = append(groups, otherinfo.ID) return nil - }) + }, gid, groupinfo.APIKey, groupinfo.SecretKey) if len(groups) != 0 { // 如果有相同的key就更新 for _, group := range groups { - err = sql.db.Find("groupinfo", &otherinfo, "where ID is "+strconv.FormatInt(group, 10)) + err = sql.db.Find("groupinfo", &otherinfo, "WHERE ID = ?", group) if err == nil { otherinfo.Token = groupinfo.Token otherinfo.Updatetime = groupinfo.Updatetime @@ -644,7 +637,7 @@ func (sql *keydb) update(gid int64, sub int) error { } groupinfo := apikey{} // 用于暂存数据 // 获取group信息 - err = sql.db.Find("groupinfo", &groupinfo, "where ID is "+strconv.FormatInt(gid, 10)) + err = sql.db.Find("groupinfo", &groupinfo, "WHERE ID = ?", gid) if err != nil { return err } @@ -655,19 +648,16 @@ func (sql *keydb) update(gid int64, sub int) error { return err } // 更新相同key的他人次数 - condition := "where not ID is " + strconv.FormatInt(gid, 10) + - " and APIKey = '" + groupinfo.APIKey + - "' and SecretKey = '" + groupinfo.SecretKey + "'" otherinfo := apikey{} var groups []int64 // 将相同的key的ID暂存 // 无视没有找到相同的key的err - _ = sql.db.FindFor("groupinfo", &otherinfo, condition, func() error { + _ = sql.db.FindFor("groupinfo", &otherinfo, "WHERE ID <> ? AND APIKey = ? AND SecretKey = ?", func() error { groups = append(groups, otherinfo.ID) return nil - }) + }, gid, groupinfo.APIKey, groupinfo.SecretKey) if len(groups) != 0 { // 如果有相同的key就更新 for _, group := range groups { - err = sql.db.Find("groupinfo", &otherinfo, "where ID is "+strconv.FormatInt(group, 10)) + err = sql.db.Find("groupinfo", &otherinfo, "WHERE ID = ?", group) if err == nil { otherinfo.MaxLimit = groupinfo.MaxLimit otherinfo.DayLimit = groupinfo.DayLimit diff --git a/plugin/wordcount/main.go b/plugin/wordcount/main.go index bb91222d71..127c7da8db 100644 --- a/plugin/wordcount/main.go +++ b/plugin/wordcount/main.go @@ -8,9 +8,14 @@ import ( "sort" "strconv" "strings" - "sync" "time" + "github.com/go-ego/gse" + "github.com/golang/freetype" + "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "github.com/wcharczuk/go-chart/v2" + "github.com/FloatTech/floatbox/binary" fcext "github.com/FloatTech/floatbox/ctxext" "github.com/FloatTech/floatbox/file" @@ -18,30 +23,34 @@ import ( "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" "github.com/FloatTech/zbputils/img/text" - "github.com/golang/freetype" - "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" - "github.com/wcharczuk/go-chart/v2" + zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" + "github.com/wdvxdr1123/ZeroBot/utils/helper" ) var ( re = regexp.MustCompile(`^[一-龥]+$`) stopwords []string + seg gse.Segmenter ) func init() { engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "聊天热词", - Help: "- 热词 [群号] [消息数目]|热词 123456 1000", + Help: "- 热词 [消息数目]|热词 1000", PublicDataFolder: "WordCount", }) cachePath := engine.DataFolder() + "cache/" + // 读取gse内置中文词典 + err := seg.LoadDictEmbed() + if err != nil { + panic(err) + } _ = os.RemoveAll(cachePath) _ = os.MkdirAll(cachePath, 0755) - engine.OnRegex(`^热词\s?(\d*)\s?(\d*)$`, zero.OnlyGroup, fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { + engine.OnRegex(`^热词\s?(\d*)$`, zero.OnlyGroup, fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool { _, err := engine.GetLazyData("stopwords.txt", false) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) @@ -75,17 +84,14 @@ func init() { } ctx.SendChain(message.Text("少女祈祷中...")) - gid, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) - p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64) + p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64) if p > 10000 { p = 10000 } if p == 0 { p = 1000 } - if gid == 0 { - gid = ctx.Event.GroupID - } + gid := ctx.Event.GroupID group := ctx.GetGroupInfo(gid, false) if group.MemberCount == 0 { ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "未加入", group.Name, "(", gid, "),无法获得热词呢")) @@ -98,42 +104,22 @@ func init() { return } messageMap := make(map[string]int, 256) - msghists := make(chan *gjson.Result, 256) - go func() { - h := ctx.GetLatestGroupMessageHistory(gid) - messageSeq := h.Get("messages.0.message_seq").Int() - msghists <- &h - for i := 1; i < int(p/20) && messageSeq != 0; i++ { - h := ctx.GetGroupMessageHistory(gid, messageSeq) - msghists <- &h - messageSeq = h.Get("messages.0.message_seq").Int() - } - close(msghists) - }() - var wg sync.WaitGroup - var mapmu sync.Mutex - for h := range msghists { - wg.Add(1) - go func(h *gjson.Result) { - for _, v := range h.Get("messages.#.message").Array() { - tex := strings.TrimSpace(message.ParseMessageFromString(v.Str).ExtractPlainText()) - if tex == "" { - continue - } - for _, t := range ctx.GetWordSlices(tex).Get("slices").Array() { - tex := strings.TrimSpace(t.Str) - i := sort.SearchStrings(stopwords, tex) - if re.MatchString(tex) && (i >= len(stopwords) || stopwords[i] != tex) { - mapmu.Lock() - messageMap[tex]++ - mapmu.Unlock() - } + h := ctx.GetGroupMessageHistory(gid, 0, p, false) + h.Get("messages").ForEach(func(_, msgObj gjson.Result) bool { + tex := strings.TrimSpace(message.ParseMessageFromString(msgObj.Get("raw_message").Str).ExtractPlainText()) + if tex != "" { + segments := seg.Segment(helper.StringToBytes(tex)) + words := gse.ToSlice(segments, true) + for _, word := range words { + word = strings.TrimSpace(word) + i := sort.SearchStrings(stopwords, word) + if re.MatchString(word) && (i >= len(stopwords) || stopwords[i] != word) { + messageMap[word]++ } } - wg.Done() - }(h) - } - wg.Wait() + } + return true + }) wc := rankByWordCount(messageMap) if len(wc) > 20 { diff --git a/plugin/wordle/wordle.go b/plugin/wordle/wordle.go index 3a3615846f..491f27dee4 100644 --- a/plugin/wordle/wordle.go +++ b/plugin/wordle/wordle.go @@ -235,10 +235,6 @@ func newWordleGame(target string) func(string) (bool, []byte, error) { } } record = append(record, s) - if len(record) >= cap(record) { - err = errTimesRunOut - return - } } var side = 20 var space = 10 @@ -269,6 +265,10 @@ func newWordleGame(target string) func(string) (bool, []byte, error) { } } data, err = imgfactory.ToBytes(ctx.Image()) + if len(record) >= cap(record) { + err = errTimesRunOut + return + } return } } diff --git a/shell.nix b/shell.nix index b7b1aabc2e..303d0b78d3 100644 --- a/shell.nix +++ b/shell.nix @@ -13,7 +13,7 @@ mkGoEnv ? pkgs.mkGoEnv, gomod2nix ? pkgs.gomod2nix, }: let - goEnv = mkGoEnv {pwd = ./.;}; + goEnv = mkGoEnv { pwd = ./.; go = pkgs.go_1_20; }; in pkgs.mkShell { packages = [ diff --git a/winres/winres.json b/winres/winres.json index ad66634919..329dbb5479 100644 --- a/winres/winres.json +++ b/winres/winres.json @@ -12,7 +12,7 @@ "0409": { "identity": { "name": "ZeroBot-Plugin", - "version": "1.8.1.1998" + "version": "1.9.9.2250" }, "description": "", "minimum-os": "vista", @@ -36,23 +36,23 @@ "#1": { "0000": { "fixed": { - "file_version": "1.8.1.1998", - "product_version": "v1.8.1", - "timestamp": "2024-05-30T16:47:38+08:00" + "file_version": "1.9.9.2250", + "product_version": "v1.9.9", + "timestamp": "2025-09-10T10:40:54+08:00" }, "info": { "0409": { "Comments": "OneBot plugins based on ZeroBot", "CompanyName": "FloatTech", "FileDescription": "https://github.com/FloatTech/ZeroBot-Plugin", - "FileVersion": "1.8.1.1998", + "FileVersion": "1.9.9.2250", "InternalName": "", - "LegalCopyright": "© 2020 - 2024 FloatTech. All Rights Reserved.", + "LegalCopyright": "© 2020 - 2025 FloatTech. All Rights Reserved.", "LegalTrademarks": "", "OriginalFilename": "ZBP.EXE", "PrivateBuild": "", "ProductName": "ZeroBot-Plugin", - "ProductVersion": "v1.8.1", + "ProductVersion": "v1.9.9", "SpecialBuild": "" } }