diff --git a/README.md b/README.md index 7056ca964b..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.8.0-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) @@ -192,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 @@ -255,6 +267,8 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w] - [x] 翻牌 - [x] 赞我 + + - [x] 群签到 - [x] [开启 | 关闭]入群验证 @@ -276,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 +
定时指令触发器 @@ -384,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 @@ -605,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解密 [文本] +
今日早报 @@ -976,6 +1027,14 @@ print("run[CQ:image,file="+j["img"]+"]") - (机器人回答:您的下一条指令将被记录,在@@every 1m时触发) - mc服务器订阅拉取
+
+ Movies猫眼电影查询 + +`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies"` + +- [x] 今日电影 +- [x] 预售电影 +
摸鱼 @@ -1018,6 +1077,10 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 酷我点歌[xxx] - [x] 酷狗点歌[xxx] + + - [x] qq点歌[xxx] + + - [x] 咪咕点歌[xxx]
@@ -1221,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"触发的指令) +
在线代码运行 @@ -1456,7 +1530,7 @@ print("run[CQ:image,file="+j["img"]+"]") `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/word_count"` - - [x] 热词 [群号] [消息数目]|热词 123456 1000 + - [x] 热词 [消息数目]|热词 1000
@@ -1572,14 +1646,20 @@ print("run[CQ:image,file="+j["img"]+"]") - [x] 设置AI聊天温度80 - [x] 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI] - [x] 设置AI聊天(不)支持系统提示词 - - [x] 设置AI聊天接口地址https://xxx + - [x] 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions - [x] 设置AI聊天密钥xxx - - [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
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/data b/data index ca3652920a..328d7638e6 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit ca3652920a2be63314adc32dc4b78ff7f2be4aff +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 ed9ab6dccd..36b302f6df 100644 --- a/flake.lock +++ b/flake.lock @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1741396135, - "narHash": "sha256-wqmdLr7h4Bk8gyKutgaApJKOM8JVvywI5P48NuqJ9Jg=", + "lastModified": 1742209644, + "narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=", "owner": "nix-community", "repo": "gomod2nix", - "rev": "0983848bf2a7ccbfe24d874065adb8fd0f23729b", + "rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884", "type": "github" }, "original": { @@ -42,6 +42,22 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1745391562, + "narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-with-go_1_20": { "locked": { "lastModified": 1710843028, "narHash": "sha256-CMbK45c4nSkGvayiEHFkGFH+doGPbgo3AWfecd2t1Fk=", @@ -61,7 +77,8 @@ "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 d069177aae..21b6eec41c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,8 @@ { description = "基于 ZeroBot 的 OneBot 插件"; - # pin nixpkgs to preserve dropped go_1_20 - inputs.nixpkgs.url = "github:NixOS/nixpkgs/33c51330782cb486764eb598d5907b43dc87b4c2"; + 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"; inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; @@ -11,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. @@ -26,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; @@ -43,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 88555b7db3..d73abcea98 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ go 1.20 require ( github.com/Baidu-AIP/golang-sdk v1.1.1 - github.com/FloatTech/AnimeAPI v1.7.1-0.20250217140215-4856397458c9 - github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024 + 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.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.20250330125231-d8be1c9d3b9c + 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/Tnze/go-mc v1.20.2 @@ -22,7 +22,7 @@ require ( 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-20250330125128-71ec2f7c085e + 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 @@ -30,6 +30,7 @@ require ( 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 @@ -37,6 +38,7 @@ require ( 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 @@ -44,7 +46,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/tidwall/gjson v1.18.0 github.com/wcharczuk/go-chart/v2 v2.1.2 - github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250330133859-27c25d9412b5 + github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20 gitlab.com/gomidi/midi/v2 v2.1.7 golang.org/x/image v0.24.0 golang.org/x/sys v0.30.0 @@ -53,8 +55,10 @@ require ( ) 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/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 @@ -69,10 +73,14 @@ require ( 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/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 @@ -85,6 +93,7 @@ require ( 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/shiny v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect diff --git a/go.sum b/go.sum index 57121051eb..d337514073 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ 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.20250217140215-4856397458c9 h1:tI9GgG8fdMK2WazFiEbMXAXjwMCckIfDaXbig9B6DdA= -github.com/FloatTech/AnimeAPI v1.7.1-0.20250217140215-4856397458c9/go.mod h1:XXG1eBJf+eeWacQx5azsQKL5Gg7jDYTFyyZGIa/56js= -github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024 h1:mrvWpiwfRklt9AyiQjKgDGJjf4YL6FZ3yC+ydbkuF2o= -github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024/go.mod h1:+P3hs+Cvl10/Aj3SNE96TuBvKAXCe+XD1pKphTZyiwk= +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= @@ -17,9 +17,11 @@ github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562 h1:snfw7FNFym1eNnLrQ 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.20250330125231-d8be1c9d3b9c h1:nIybmanPvQknseVOJ+s4/m3q7EZxtqMoTy3wiiZts6E= -github.com/FloatTech/zbputils v1.7.2-0.20250330125231-d8be1c9d3b9c/go.mod h1:ArZ0fMAcmPEIXOqDmfzbSx+oYH8sssApQnbCu694iS8= +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= @@ -31,6 +33,8 @@ github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0 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/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= @@ -59,8 +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-20250330125128-71ec2f7c085e h1:L/Z5N6UfpuqNIiPUrjSzmrnWj3mjd3auwl/2ctpGXNY= -github.com/fumiama/deepinfra v0.0.0-20250330125128-71ec2f7c085e/go.mod h1:wW05PQSn8mo1mZIoa6LBUE+3xIBjkoONvnfPTV5ZOhY= +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= @@ -92,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= @@ -106,6 +112,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= @@ -122,10 +129,12 @@ 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/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= @@ -148,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= @@ -177,6 +195,7 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -192,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/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.20250330133859-27c25d9412b5 h1:HsMcBsVpYuQv+W8pjX5WdwYROrFQP9c5Pbf4x4adDus= -github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250330133859-27c25d9412b5/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M= +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= @@ -235,6 +257,7 @@ 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -260,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= @@ -285,6 +309,7 @@ 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= diff --git a/gomod2nix.toml b/gomod2nix.toml index 1d8db9d68d..12c63f8e72 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -5,11 +5,11 @@ schema = 3 version = "v1.1.1" hash = "sha256-hKshA0K92bKuK92mmtM0osVmqLJcSbeobeWSDpQoRCo=" [mod."github.com/FloatTech/AnimeAPI"] - version = "v1.7.1-0.20250217140215-4856397458c9" - hash = "sha256-7TkWoVslfzO/aTx+F7UwttrtBGGMMqe4GHN0aF4JUd0=" + version = "v1.7.1-0.20250901143505-180d33844860" + hash = "sha256-k1MlgaBGwpaqoVk+8WYfXoVLfzqyEQW5LQaJgBKlhUA=" [mod."github.com/FloatTech/floatbox"] - version = "v0.0.0-20241106130736-5aea0a935024" - hash = "sha256-hSKmkzpNZwXRo0qm4G+1lXkNzWMwV9leYlYLQuzWx3M=" + version = "v0.0.0-20250513111443-adba80e84e80" + hash = "sha256-Zt9zkUa3qqldrSttAq66YLPZPxrnkOR2MaU7oapIWEE=" [mod."github.com/FloatTech/gg"] version = "v1.1.3" hash = "sha256-7K/R2mKjUHVnoJ3b1wDObJ5Un2Htj59Y97G1Ja1tuPo=" @@ -29,8 +29,8 @@ schema = 3 version = "v1.7.0" hash = "sha256-HDDnE0oktWJH1tkxuQwUUbeJhmVwY5fyc/vR72D2mkU=" [mod."github.com/FloatTech/zbputils"] - version = "v1.7.2-0.20250330125231-d8be1c9d3b9c" - hash = "sha256-v2ueCLEwy6oqF8HoxodDHcNOeDPzKn+7fiYSC2L2/e4=" + 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=" @@ -77,8 +77,8 @@ schema = 3 version = "v1.3.0" hash = "sha256-/sN7X8dKXQgv8J+EDzVUB+o+AY9gBC8e1C6sYhaTy1k=" [mod."github.com/fumiama/deepinfra"] - version = "v0.0.0-20250330125128-71ec2f7c085e" - hash = "sha256-O7Om4mIcBB2zdVxaeKh7qHbiG83gVLP+f8vxSF17kCw=" + version = "v0.0.0-20250910022828-8cde75e137f4" + hash = "sha256-1CV8t3R91maqJztHg7whECqvS4+sxWcSvq+EyO4PyZ8=" [mod."github.com/fumiama/go-base16384"] version = "v1.7.0" hash = "sha256-vTAsBBYe2ISzb2Nba5E96unodZSkhMcqo6hbwR01nz8=" @@ -112,6 +112,9 @@ schema = 3 [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=" @@ -217,12 +220,15 @@ 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.2" hash = "sha256-GXWWea/u6BezTsPPrWhTYiTetPP/YW6P+Sj4YdocPaM=" [mod."github.com/wdvxdr1123/ZeroBot"] - version = "v1.8.2-0.20250330133859-27c25d9412b5" - hash = "sha256-gT3uFTg5E0Th3r1M1vLzr0QtOjbMusqEjD/ckoBdDFc=" + 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=" diff --git a/kanban/banner/banner.go b/kanban/banner/banner.go index 576141cd31..cf6bd49f1c 100644 --- a/kanban/banner/banner.go +++ b/kanban/banner/banner.go @@ -3,13 +3,13 @@ package banner // Version ... -var Version = "v1.9.6" +var Version = "v1.9.9" // Copyright ... var Copyright = "© 2020 - 2025 FloatTech" // Banner ... var Banner = "* OneBot + ZeroBot + Golang\n" + - "* Version " + Version + " - 2025-03-30 23:46:38 +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 34e7130c8a..01b5511e27 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,8 @@ import ( _ "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" // 群管 @@ -65,6 +67,7 @@ import ( _ "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识别 @@ -81,6 +84,7 @@ import ( _ "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" // 嘉心糖发病 @@ -110,6 +114,7 @@ import ( _ "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" // 点歌 @@ -127,6 +132,7 @@ import ( _ "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" // 分数 diff --git a/plugin/aichat/cfg.go b/plugin/aichat/cfg.go index 545e3e2c7e..f92875320e 100644 --- a/plugin/aichat/cfg.go +++ b/plugin/aichat/cfg.go @@ -1,6 +1,8 @@ package aichat import ( + "fmt" + "strconv" "strings" ctrl "github.com/FloatTech/zbpctrl" @@ -12,17 +14,22 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" ) -var cfg = newconfig() +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 { @@ -94,3 +101,98 @@ func newextrasetbool(ptr *bool) func(ctx *zero.Ctx) { 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 index 25bd4dce9f..fc78af58c6 100644 --- a/plugin/aichat/main.go +++ b/plugin/aichat/main.go @@ -1,22 +1,27 @@ -// Package aichat OpenAI聊天 +// 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 ( @@ -29,22 +34,60 @@ var ( "- 设置AI聊天温度80\n" + "- 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]\n" + "- 设置AI聊天(不)支持系统提示词\n" + - "- 设置AI聊天接口地址https://xxx\n" + + "- 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions\n" + "- 设置AI聊天密钥xxx\n" + - "- 设置AI聊天模型名xxx\n" + + "- 设置AI聊天模型名Qwen/Qwen3-8B\n" + "- 查看AI聊天系统提示词\n" + "- 重置AI聊天系统提示词\n" + "- 设置AI聊天系统提示词xxx\n" + "- 设置AI聊天分隔符(留空则清除)\n" + - "- 设置AI聊天(不)响应AT", + "- 设置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, +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() { @@ -74,31 +117,25 @@ func init() { return } - if temp <= 0 { - temp = 70 // default setting - } - if temp > 100 { - temp = 100 - } + 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, - float32(temp)/100, 0.9, 4096, + temperature, topp, maxn, ) case 1: mod = model.NewOLLaMA( cfg.ModelName, cfg.Separator, - float32(temp)/100, 0.9, 4096, + temperature, topp, maxn, ) case 2: mod = model.NewGenAI( cfg.ModelName, - float32(temp)/100, 0.9, 4096, + temperature, topp, maxn, ) default: logrus.Warnln("[aichat] unsupported AI type", cfg.Type) @@ -125,10 +162,20 @@ func init() { if t == "" { continue } - if id != nil { - id = ctx.SendChain(message.Reply(id), message.Text(t)) + 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 { - id = ctx.SendChain(message.Text(t)) + if id != nil { + id = ctx.SendChain(message.Reply(id), message.Text(t)) + } else { + id = ctx.SendChain(message.Text(t)) + } } process.SleepAbout1sTo2s() } @@ -255,4 +302,240 @@ func init() { 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/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/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/bilibili/bilibili_parse.go b/plugin/bilibili/bilibili_parse.go index 09607630c5..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 @@ -189,3 +243,47 @@ func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.Se } 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/card2msg.go b/plugin/bilibili/card2msg.go index 3e6c59918e..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" ) @@ -303,7 +305,10 @@ func liveCard2msg(card bz.RoomCard) (msg []message.Segment) { // videoCard2msg 视频卡片转消息 func videoCard2msg(card bz.Card) (msg []message.Segment, err error) { - var mCard bz.MemberCard + var ( + mCard bz.MemberCard + onlineTotal bz.OnlineTotal + ) msg = make([]message.Segment, 0, 16) mCard, err = bz.GetMemberCard(card.Owner.Mid) msg = append(msg, message.Text("标题: ", card.Title, "\n")) @@ -313,16 +318,25 @@ func videoCard2msg(card bz.Card) (msg []message.Segment, err error) { } } else { if err != nil { - 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/chatcount/chatcount.go b/plugin/chatcount/chatcount.go index a4e1bb41a8..caf88ebeed 100644 --- a/plugin/chatcount/chatcount.go +++ b/plugin/chatcount/chatcount.go @@ -43,8 +43,15 @@ func init() { }) 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, ctx.Event.UserID) + 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). 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/gif/context.go b/plugin/gif/context.go index 1b5f541738..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.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/main/` + name) + 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.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/main/` + name) + 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/kfccrazythursday/kfccrazythursday.go b/plugin/kfccrazythursday/kfccrazythursday.go index a7c2cd265c..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 = "http://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/manager/manager.go b/plugin/manager/manager.go index f1a6f788e8..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" + @@ -405,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). 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 592438359e..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,12 +47,37 @@ 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.Segment { headers := http.Header{ diff --git a/plugin/niuniu/main.go b/plugin/niuniu/main.go index 40028efb91..d49b5a062f 100644 --- a/plugin/niuniu/main.go +++ b/plugin/niuniu/main.go @@ -2,9 +2,11 @@ package niuniu import ( + "errors" "fmt" "math/rand" "strconv" + "strings" "time" "github.com/FloatTech/AnimeAPI/niu" @@ -18,12 +20,6 @@ import ( "github.com/wdvxdr1123/ZeroBot/message" ) -type lastLength struct { - TimeLimit time.Time - Count int - Length float64 -} - var ( en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, @@ -47,8 +43,8 @@ var ( }) dajiaoLimiter = rate.NewManager[string](time.Second*90, 1) jjLimiter = rate.NewManager[string](time.Second*150, 1) - jjCount = syncx.Map[string, *lastLength]{} - register = syncx.Map[string, *lastLength]{} + jjCount = syncx.Map[string, *niu.PKRecord]{} + register = syncx.Map[string, *niu.PKRecord]{} ) func init() { @@ -65,7 +61,7 @@ func init() { 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+1, info.UserID, info.Money, wallet.GetWalletName(), info.Length) + 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 { @@ -81,7 +77,7 @@ func init() { for { select { case <-timer.C: - ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消")) + ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消")) return case r := <-recv: answer = r.Event.Message.String() @@ -90,7 +86,6 @@ func init() { ctx.SendChain(message.Text("ERROR: ", err)) return } - n-- msg, err := niu.Auction(gid, uid, n) if err != nil { ctx.SendChain(message.Text("ERROR:", err)) @@ -104,11 +99,21 @@ func init() { 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 err != nil { + 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) { @@ -135,29 +140,33 @@ func init() { cost int scope string description string - count int }{ - 1: {"伟哥", 300, "打胶", "可以让你打胶每次都增长", 5}, - 2: {"媚药", 300, "打胶", "可以让你打胶每次都减少", 5}, - 3: {"击剑神器", 500, "jj", "可以让你每次击剑都立于不败之地", 2}, - 4: {"击剑神稽", 500, "jj", "可以让你每次击剑都失败", 2}, + 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 := range propMap { + for id := 1; id <= len(propMap); id++ { product := propMap[id] - productInfo := fmt.Sprintf("商品%d\n商品名: %s\n商品价格: %dATRI币\n商品作用域: %s\n商品描述: %s\n使用次数:%d", - id, product.name, product.cost, product.scope, product.description, product.count) + 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 } - - ctx.SendChain(message.Text("输入对应序号进行购买商品")) - recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(\d+)$`)).Repeat() + 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 := "" @@ -165,17 +174,17 @@ func init() { for { select { case <-timer.C: - ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消")) + 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 - } - if err = niu.Store(gid, uid, n); err != nil { + // 解析输入的商品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 } @@ -196,16 +205,16 @@ func init() { } if time.Since(last.TimeLimit) > time.Hour { - ctx.SendChain(message.Text("时间已经过期了,牛牛已被收回!")) + ctx.SendChain(message.Text("时间已经过期了,牛牛已被收回!")) jjCount.Delete(fmt.Sprintf("%d_%d", gid, uid)) return } if last.Count < 4 { - ctx.SendChain(message.Text("你还没有被厥够4次呢,不能赎牛牛")) + ctx.SendChain(message.Text("你还没有被厥够4次呢,不能赎牛牛")) return } - ctx.SendChain(message.Text("再次确认一下哦,这次赎牛牛,牛牛长度将会变成", last.Length, "cm\n还需要嘛【是|否】")) + 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) @@ -222,11 +231,11 @@ func init() { return } - if err := niu.Redeem(gid, uid, last.Length); err == nil { + 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))) @@ -332,7 +341,7 @@ func init() { } uid := ctx.Event.UserID gid := ctx.Event.GroupID - msg, length, err := niu.JJ(gid, uid, adduser, patternParsed[0].Text()[1]) + 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)) @@ -341,22 +350,27 @@ func init() { ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg)) j := fmt.Sprintf("%d_%d", gid, adduser) count, ok := jjCount.Load(j) - var c lastLength - // 按照最后一次被jj时的时间计算,超过60分钟则重置 + var c niu.PKRecord + // 按照最后一次被 jj 时的时间计算,超过60分钟则重置 if !ok { - c = lastLength{ + // 第一次被 jj + c = niu.PKRecord{ + NiuID: niuID, TimeLimit: time.Now(), Count: 1, Length: length, } } else { - c = lastLength{ + c = niu.PKRecord{ + NiuID: niuID, TimeLimit: time.Now(), Count: count.Count + 1, Length: count.Length, } + // 超时了,重置 if time.Since(c.TimeLimit) > time.Hour { - c = lastLength{ + c = niu.PKRecord{ + NiuID: niuID, TimeLimit: time.Now(), Count: 1, Length: length, @@ -372,6 +386,9 @@ func init() { ))) if c.Count >= 4 { + if c.Count == 6 { + return + } id := ctx.SendPrivateMessage(adduser, message.Text(fmt.Sprintf("你在%d群里已经被厥冒烟了,快去群里赎回你原本的牛牛!\n发送:`赎牛牛`即可!", gid))) if id == 0 { @@ -386,8 +403,8 @@ func init() { key := fmt.Sprintf("%d_%d", gid, uid) data, ok := register.Load(key) switch { - case !ok || time.Since(data.TimeLimit) > time.Hour*12: - data = &lastLength{ + case !ok || time.Since(data.TimeLimit) > time.Hour*24: + data = &niu.PKRecord{ TimeLimit: time.Now(), Count: 1, } @@ -396,6 +413,7 @@ func init() { ctx.SendChain(message.Text("你的钱不够你注销牛牛了,这次注销需要", data.Count*50, wallet.GetWalletName())) return } + data.Count++ } register.Store(key, data) msg, err := niu.Cancel(gid, uid) 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/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/thesaurus/chat.go b/plugin/thesaurus/chat.go index 47fc4d0c08..268e3e8400 100644 --- a/plugin/thesaurus/chat.go +++ b/plugin/thesaurus/chat.go @@ -2,11 +2,10 @@ package thesaurus import ( - "bytes" "math/rand" "strings" - "github.com/fumiama/jieba" + "github.com/go-ego/gse" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -55,11 +54,8 @@ func init() { 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) } @@ -102,10 +98,10 @@ func init() { ctx.SendChain(message.Text(r.Reply)) } }) - engine.OnMessage(zero.OnlyToMe, canmatch(tDERE), match(chatListD, seg)). + engine.OnMessage(zero.OnlyToMe, canmatch(tDERE), match(chatListD, &seg)). SetBlock(false). Handle(randreply(sm.D)) - engine.OnMessage(zero.OnlyToMe, canmatch(tKAWA), match(chatListK, seg)). + engine.OnMessage(zero.OnlyToMe, canmatch(tKAWA), match(chatListK, &seg)). SetBlock(false). Handle(randreply(sm.K)) }() @@ -122,7 +118,7 @@ const ( tKAWA ) -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() 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/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 07387d7c77..329dbb5479 100644 --- a/winres/winres.json +++ b/winres/winres.json @@ -12,7 +12,7 @@ "0409": { "identity": { "name": "ZeroBot-Plugin", - "version": "1.9.6.2202" + "version": "1.9.9.2250" }, "description": "", "minimum-os": "vista", @@ -36,23 +36,23 @@ "#1": { "0000": { "fixed": { - "file_version": "1.9.6.2202", - "product_version": "v1.9.6", - "timestamp": "2025-03-30T23:46:55+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.9.6.2202", + "FileVersion": "1.9.9.2250", "InternalName": "", "LegalCopyright": "© 2020 - 2025 FloatTech. All Rights Reserved.", "LegalTrademarks": "", "OriginalFilename": "ZBP.EXE", "PrivateBuild": "", "ProductName": "ZeroBot-Plugin", - "ProductVersion": "v1.9.6", + "ProductVersion": "v1.9.9", "SpecialBuild": "" } }