diff --git a/README.md b/README.md
index 7056ca964b..322fef207e 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
[](https://goreportcard.com/badge/github.com/FloatTech/ZeroBot-Plugin)
[](https://t.me/zerobotplugin)
- [](https://github.com/wdvxdr1123/ZeroBot)
+ [](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": ""
}
}