diff --git a/README.md b/README.md
index 9b9f2bc154..865e31da6b 100644
--- a/README.md
+++ b/README.md
@@ -950,6 +950,20 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 符号说明: C5是中央C,后面不写数字,默认接5,Cb6<1,b代表降调,#代表升调,6比5高八度,<1代表音长×2,<3代表音长×8,<-1代表音长×0.5,<-3代表音长×0.125,R是休止符
+
+ Minecraft服务器监控&订阅
+
+`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver"`
+
+- [x] mc服务器状态 [服务器IP/URI]
+- [x] mc服务器添加订阅 [服务器IP/URI]
+- [x] mc服务器取消订阅 [服务器IP/URI]
+- [x] mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)
+ - 使用job插件设置定时, 对话例子如下::
+ - 记录在"@every 1m"触发的指令
+ - (机器人回答:您的下一条指令将被记录,在@@every 1m时触发)
+ - mc服务器订阅拉取
+
摸鱼
diff --git a/go.mod b/go.mod
index 9090b9611c..cfc2691dba 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
github.com/FloatTech/zbputils v1.7.2-0.20250222055844-5d403aa9cecf
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
github.com/antchfx/htmlquery v1.3.4
github.com/corona10/goimagehash v1.1.0
github.com/davidscholberg/go-durationfmt v0.0.0-20170122144659-64843a2083d3
@@ -30,6 +31,7 @@ require (
github.com/fumiama/terasu v0.0.0-20241027183601-987ab91031ce
github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+ github.com/google/uuid v1.6.0
github.com/jinzhu/gorm v1.9.16
github.com/jozsefsallai/gophersauce v1.0.1
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5
@@ -63,7 +65,6 @@ require (
github.com/gabriel-vasile/mimetype v1.0.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/google/uuid v1.6.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/jfreymuth/vorbis v1.0.0 // indirect
@@ -85,8 +86,8 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
- golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
- golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
+ golang.org/x/exp/shiny v0.0.0-20250210185358-939b2ce775ac // indirect
+ golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/net v0.33.0 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index dbdcc3adc7..f7e3583764 100644
--- a/go.sum
+++ b/go.sum
@@ -24,6 +24,8 @@ github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 h1:S/ferNiehVjNaBMN
github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 h1:bBmmB7he0iVN4m5mcehfheeRUEer/Avo4ujnxI3uCqs=
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5/go.mod h1:0UcFaCkhp6vZw6l5Dpq0Dp673CoF9GdvA8lTfst0GiU=
+github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
+github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5UaBEQLjcvn7aAU01nqU/NUyOBEU+ew=
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4=
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA=
@@ -208,16 +210,18 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp/shiny v0.0.0-20250210185358-939b2ce775ac h1:v0JK6d+F5Wcwvfz5i1UMwk2jaCEC0jkGM1xYmr6n3VQ=
+golang.org/x/exp/shiny v0.0.0-20250210185358-939b2ce775ac/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
-golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
diff --git a/main.go b/main.go
index 987f782cf9..9a71aad96c 100644
--- a/main.go
+++ b/main.go
@@ -62,90 +62,91 @@ import (
// vvvvvvvvvvvvvv //
// vvvv //
- _ "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/aiwife" // 随机老婆
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi" // 颜文字抽象转写
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" // 牛牛大作战
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame
- _ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API
+ _ "github.com/FloatTech/ZeroBot-Plugin/custom" // 自定义插件合集
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ahsai" // ahsai tts
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi" // 颜文字抽象转写
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver" // Minecraft服务器监控&订阅
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" // 牛牛大作战
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API
// _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wtf" // 鬼东西
diff --git a/plugin/minecraftobserver/minecraftobserver.go b/plugin/minecraftobserver/minecraftobserver.go
new file mode 100644
index 0000000000..c0d6d393ee
--- /dev/null
+++ b/plugin/minecraftobserver/minecraftobserver.go
@@ -0,0 +1,300 @@
+// Package minecraftobserver 通过mc服务器地址获取服务器状态信息并绘制图片发送到QQ群
+package minecraftobserver
+
+import (
+ "fmt"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/FloatTech/zbputils/control"
+ zbpCtxExt "github.com/FloatTech/zbputils/ctxext"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+ "strings"
+ "time"
+)
+
+const (
+ name = "minecraftobserver"
+)
+
+var (
+ // 注册插件
+ engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
+ // 默认不启动
+ DisableOnDefault: false,
+ Brief: "Minecraft服务器状态查询/订阅",
+ // 详细帮助
+ Help: "- mc服务器状态 [服务器IP/URI]\n" +
+ "- mc服务器添加订阅 [服务器IP/URI]\n" +
+ "- mc服务器取消订阅 [服务器IP/URI]\n" +
+ "- mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)" +
+ "-----------------------\n" +
+ "使用job插件设置定时, 例:" +
+ "记录在\"@every 1m\"触发的指令\n" +
+ "(机器人回答:您的下一条指令将被记录,在@@every 1m时触发)" +
+ "mc服务器订阅拉取",
+ // 插件数据存储路径
+ PrivateDataFolder: name,
+ }).ApplySingle(zbpCtxExt.DefaultSingle)
+)
+
+func init() {
+ // 状态查询
+ engine.OnRegex("^[mM][cC]服务器状态 (.+)$").SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ // 关键词查找
+ addr := ctx.State["regex_matched"].([]string)[1]
+ resp, err := getMinecraftServerStatus(addr)
+ if err != nil {
+ ctx.Send(message.Text("服务器状态获取失败... 错误信息: ", err))
+ return
+ }
+ status := resp.genServerSubscribeSchema(addr, 0)
+ textMsg, iconBase64 := status.generateServerStatusMsg()
+ var msg message.Message
+ if iconBase64 != "" {
+ msg = append(msg, message.Image(iconBase64))
+ }
+ msg = append(msg, message.Text(textMsg))
+ if id := ctx.Send(msg); id.ID() == 0 {
+ //logrus.Errorln(logPrefix + "Send failed")
+ return
+ }
+ })
+ // 添加订阅
+ engine.OnRegex(`^[mM][cC]服务器添加订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ // 关键词查找
+ addr := ctx.State["regex_matched"].([]string)[1]
+ status, err := getMinecraftServerStatus(addr)
+ if err != nil {
+ ctx.Send(message.Text("服务器信息初始化失败,请检查服务器是否可用!\n错误信息: ", err))
+ return
+ }
+ targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
+ err = dbInstance.newSubscribe(addr, targetID, targetType)
+ if err != nil {
+ ctx.Send(message.Text("订阅添加失败... 错误信息: ", err))
+ return
+ }
+ // 插入数据库(首条,需要更新状态)
+ err = dbInstance.updateServerStatus(status.genServerSubscribeSchema(addr, 0))
+ if err != nil {
+ ctx.Send(message.Text("服务器状态更新失败... 错误信息: ", err))
+ return
+ }
+ if sid := ctx.Send(message.Text(fmt.Sprintf("服务器 %s 订阅添加成功", addr))); sid.ID() == 0 {
+ //logrus.Errorln(logPrefix + "Send failed")
+ return
+ }
+ // 成功后立即发送一次状态
+ textMsg, iconBase64 := status.genServerSubscribeSchema(addr, 0).generateServerStatusMsg()
+ var msg message.Message
+ if iconBase64 != "" {
+ msg = append(msg, message.Image(iconBase64))
+ }
+ msg = append(msg, message.Text(textMsg))
+ if id := ctx.Send(msg); id.ID() == 0 {
+ //logrus.Errorln(logPrefix + "Send failed")
+ return
+ }
+ })
+ // 删除
+ engine.OnRegex(`^[mM][cC]服务器取消订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ addr := ctx.State["regex_matched"].([]string)[1]
+ // 通过群组id和服务器地址获取服务器状态
+ targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
+ err := dbInstance.deleteSubscribe(addr, targetID, targetType)
+ if err != nil {
+ ctx.Send(message.Text("取消订阅失败...", fmt.Sprintf("错误信息: %v", err)))
+ return
+ }
+ ctx.Send(message.Text("取消订阅成功"))
+ })
+ // 查看当前渠道的所有订阅
+ engine.OnRegex(`^[mM][cC]服务器订阅列表$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ subList, err := dbInstance.getSubscribesByTarget(warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID))
+ if err != nil {
+ ctx.Send(message.Text("获取订阅列表失败... 错误信息: ", err))
+ return
+ }
+ if len(subList) == 0 {
+ ctx.Send(message.Text("当前没有订阅哦"))
+ return
+ }
+ stringBuilder := strings.Builder{}
+ stringBuilder.WriteString("[订阅列表]\n")
+ for _, v := range subList {
+ stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", v.ServerAddr))
+ }
+ if sid := ctx.Send(message.Text(stringBuilder.String())); sid.ID() == 0 {
+ //logrus.Errorln(logPrefix + "Send failed")
+ return
+ }
+ })
+ // 查看全局订阅情况(仅限管理员私聊可用)
+ engine.OnRegex(`^[mM][cC]服务器全局订阅列表$`, zero.OnlyPrivate, zero.SuperUserPermission, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ subList, err := dbInstance.getAllSubscribes()
+ if err != nil {
+ ctx.Send(message.Text("获取全局订阅列表失败... 错误信息: ", err))
+ return
+ }
+ if len(subList) == 0 {
+ ctx.Send(message.Text("当前一个订阅都没有哦"))
+ return
+ }
+ userID := ctx.Event.UserID
+ userName := ctx.CardOrNickName(userID)
+ msg := make(message.Message, 0)
+
+ // 按照群组or用户分组来定
+ groupSubMap := make(map[int64][]serverSubscribe)
+ userSubMap := make(map[int64][]serverSubscribe)
+ for _, v := range subList {
+ switch v.TargetType {
+ case targetTypeGroup:
+ groupSubMap[v.TargetID] = append(groupSubMap[v.TargetID], v)
+ case targetTypeUser:
+ userSubMap[v.TargetID] = append(userSubMap[v.TargetID], v)
+ default:
+ }
+ }
+
+ // 群
+ for k, v := range groupSubMap {
+ stringBuilder := strings.Builder{}
+ stringBuilder.WriteString(fmt.Sprintf("[群 %d]存在以下订阅:\n", k))
+ for _, sub := range v {
+ stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
+ }
+ msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
+ }
+ // 个人
+ for k, v := range userSubMap {
+ stringBuilder := strings.Builder{}
+ stringBuilder.WriteString(fmt.Sprintf("[用户 %d]存在以下订阅:\n", k))
+ for _, sub := range v {
+ stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
+ }
+ msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
+ }
+ // 合并发送
+ ctx.SendPrivateForwardMessage(ctx.Event.UserID, msg)
+ })
+ // 状态变更通知,全局触发,逐个服务器检查,检查到变更则逐个发送通知
+ engine.OnRegex(`^[mM][cC]服务器订阅拉取$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ serverList, err := dbInstance.getAllSubscribes()
+ if err != nil {
+ su := zero.BotConfig.SuperUsers[0]
+ // 如果订阅列表获取失败,通知管理员
+ ctx.SendPrivateMessage(su, message.Text(logPrefix, "获取订阅列表失败..."))
+ return
+ }
+ //logrus.Debugln(logPrefix+"global get ", len(serverList), " subscribe(s)")
+ serverMap := make(map[string][]serverSubscribe)
+ for _, v := range serverList {
+ serverMap[v.ServerAddr] = append(serverMap[v.ServerAddr], v)
+ }
+ changedCount := 0
+ for subAddr, oneServerSubList := range serverMap {
+ // 查询当前存储的状态
+ storedStatus, sErr := dbInstance.getServerStatus(subAddr)
+ if sErr != nil {
+ //logrus.Errorln(logPrefix+fmt.Sprintf("getServerStatus ServerAddr(%s) error: ", subAddr), sErr)
+ continue
+ }
+ isChanged, changedNotifyMsg, sErr := singleServerScan(storedStatus)
+ if sErr != nil {
+ //logrus.Errorln(logPrefix+"singleServerScan error: ", sErr)
+ continue
+ }
+ if !isChanged {
+ continue
+ }
+ changedCount++
+ // 发送变化信息
+ for _, subInfo := range oneServerSubList {
+ time.Sleep(100 * time.Millisecond)
+ if subInfo.TargetType == targetTypeUser {
+ ctx.SendPrivateMessage(subInfo.TargetID, changedNotifyMsg)
+ } else if subInfo.TargetType == targetTypeGroup {
+ m, ok := control.Lookup(name)
+ if !ok {
+ continue
+ }
+ if !m.IsEnabledIn(subInfo.TargetID) {
+ continue
+ }
+ ctx.SendGroupMessage(subInfo.TargetID, changedNotifyMsg)
+ }
+ }
+ }
+ })
+}
+
+// singleServerScan 单个服务器状态扫描
+func singleServerScan(oldSubStatus *serverStatus) (changed bool, notifyMsg message.Message, err error) {
+ notifyMsg = make(message.Message, 0)
+ newSubStatus := &serverStatus{}
+ // 获取服务器状态 & 检查是否需要更新
+ rawServerStatus, err := getMinecraftServerStatus(oldSubStatus.ServerAddr)
+ if err != nil {
+ //logrus.Warnln(logPrefix+"getMinecraftServerStatus error: ", err)
+ err = nil
+ // 计数器没有超限,增加计数器并跳过
+ if cnt, ts := addPingServerUnreachableCounter(oldSubStatus.ServerAddr, time.Now()); cnt < pingServerUnreachableCounterThreshold &&
+ time.Now().Sub(ts) < pingServerUnreachableCounterTimeThreshold {
+ //logrus.Warnln(logPrefix+"server ", oldSubStatus.ServerAddr, " unreachable, counter: ", cnt, " ts:", ts)
+ return
+ }
+ // 不可达计数器已经超限,则更新服务器状态
+ // 深拷贝,设置PingDelay为不可达
+ newSubStatus = oldSubStatus.deepCopy()
+ newSubStatus.PingDelay = pingDelayUnreachable
+ } else {
+ newSubStatus = rawServerStatus.genServerSubscribeSchema(oldSubStatus.ServerAddr, oldSubStatus.ID)
+ }
+ if newSubStatus == nil {
+ //logrus.Errorln(logPrefix + "newSubStatus is nil")
+ return
+ }
+ // 检查是否有订阅信息变化
+ if oldSubStatus.isServerStatusSpecChanged(newSubStatus) {
+ //logrus.Warnf(logPrefix+"server subscribe spec changed: (%+v) -> (%+v)", oldSubStatus, newSubStatus)
+ changed = true
+ // 更新数据库
+ err = dbInstance.updateServerStatus(newSubStatus)
+ if err != nil {
+ //logrus.Errorln(logPrefix+"updateServerSubscribeStatus error: ", err)
+ return
+ }
+ // 纯文本信息
+ notifyMsg = append(notifyMsg, message.Text(formatSubStatusChangeText(oldSubStatus, newSubStatus)))
+ // 如果有图标变更
+ if oldSubStatus.FaviconMD5 != newSubStatus.FaviconMD5 {
+ // 有图标变更
+ notifyMsg = append(notifyMsg, message.Text("\n-----[图标变更]-----\n"))
+ // 旧图标
+ notifyMsg = append(notifyMsg, message.Text("[旧]\n"))
+ if oldSubStatus.FaviconRaw != "" {
+ notifyMsg = append(notifyMsg, message.Image(oldSubStatus.FaviconRaw.toBase64String()))
+ } else {
+ notifyMsg = append(notifyMsg, message.Text("(空)\n"))
+ }
+ // 新图标
+ notifyMsg = append(notifyMsg, message.Text("[新]\n"))
+ if newSubStatus.FaviconRaw != "" {
+ notifyMsg = append(notifyMsg, message.Image(newSubStatus.FaviconRaw.toBase64String()))
+ } else {
+ notifyMsg = append(notifyMsg, message.Text("(空)\n"))
+ }
+ }
+ notifyMsg = append(notifyMsg, message.Text("\n-------最新状态-------\n"))
+ // 服务状态
+ textMsg, iconBase64 := newSubStatus.generateServerStatusMsg()
+ if iconBase64 != "" {
+ notifyMsg = append(notifyMsg, message.Image(iconBase64))
+ }
+ notifyMsg = append(notifyMsg, message.Text(textMsg))
+ }
+ // 逻辑到达这里,说明状态已经变更 or 无变更且服务器可达,重置不可达计数器
+ resetPingServerUnreachableCounter(oldSubStatus.ServerAddr)
+ return
+}
diff --git a/plugin/minecraftobserver/minecraftobserver_test.go b/plugin/minecraftobserver/minecraftobserver_test.go
new file mode 100644
index 0000000000..aa7babc968
--- /dev/null
+++ b/plugin/minecraftobserver/minecraftobserver_test.go
@@ -0,0 +1,127 @@
+package minecraftobserver
+
+import (
+ "fmt"
+ "github.com/wdvxdr1123/ZeroBot/message"
+ "testing"
+)
+
+func Test_singleServerScan(t *testing.T) {
+ initErr := initializeDB("data/minecraftobserver/" + dbPath)
+ if initErr != nil {
+ t.Fatalf("initializeDB() error = %v", initErr)
+ }
+ if dbInstance == nil {
+ t.Fatalf("initializeDB() got = %v, want not nil", dbInstance)
+ }
+ t.Run("状态变更", func(t *testing.T) {
+ cleanTestData(t)
+ newSS1 := &serverStatus{
+ ServerAddr: "cn.nekoland.top",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "",
+ }
+ err := dbInstance.updateServerStatus(newSS1)
+ if err != nil {
+ t.Fatalf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1)
+ if err != nil {
+ t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err)
+ }
+ changed, msg, err := singleServerScan(newSS1)
+ if err != nil {
+ t.Fatalf("singleServerScan() error = %v", err)
+ }
+ if !changed {
+ t.Fatalf("singleServerScan() got = %v, want true", changed)
+ }
+ if len(msg) == 0 {
+ t.Fatalf("singleServerScan() got = %v, want not empty", msg)
+ }
+ fmt.Printf("msg: %v\n", msg)
+ })
+
+ t.Run("可达 -> 不可达", func(t *testing.T) {
+ cleanTestData(t)
+ newSS1 := &serverStatus{
+ ServerAddr: "dx.123213213123123.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "",
+ PingDelay: 123,
+ }
+ err := dbInstance.updateServerStatus(newSS1)
+ if err != nil {
+ t.Fatalf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.123213213123123.net", 123456, 1)
+ if err != nil {
+ t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err)
+ }
+ var msg message.Message
+ changed, _, err := singleServerScan(newSS1)
+ if err != nil {
+ t.Fatalf("singleServerScan() error = %v", err)
+ }
+ if changed {
+ t.Fatalf("singleServerScan() got = %v, want false", changed)
+ }
+ // 第二次
+ changed, _, err = singleServerScan(newSS1)
+ if err != nil {
+ t.Fatalf("singleServerScan() error = %v", err)
+ }
+ if changed {
+ t.Fatalf("singleServerScan() got = %v, want false", changed)
+ }
+ // 第三次
+ changed, msg, err = singleServerScan(newSS1)
+ if err != nil {
+ t.Fatalf("singleServerScan() error = %v", err)
+ }
+ if !changed {
+ t.Fatalf("singleServerScan() got = %v, want true", changed)
+ }
+ if len(msg) == 0 {
+ t.Fatalf("singleServerScan() got = %v, want not empty", msg)
+ }
+ fmt.Printf("msg: %v\n", msg)
+
+ })
+
+ t.Run("不可达 -> 可达", func(t *testing.T) {
+ cleanTestData(t)
+ newSS1 := &serverStatus{
+ ServerAddr: "cn.nekoland.top",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "",
+ PingDelay: pingDelayUnreachable,
+ }
+ err := dbInstance.updateServerStatus(newSS1)
+ if err != nil {
+ t.Fatalf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1)
+ if err != nil {
+ t.Fatalf("newSubscribe() error = %v", err)
+ }
+ changed, msg, err := singleServerScan(newSS1)
+ if err != nil {
+ t.Fatalf("singleServerScan() error = %v", err)
+ }
+ if !changed {
+ t.Fatalf("singleServerScan() got = %v, want true", changed)
+ }
+ if len(msg) == 0 {
+ t.Fatalf("singleServerScan() got = %v, want not empty", msg)
+ }
+ fmt.Printf("msg: %v\n", msg)
+ })
+
+}
diff --git a/plugin/minecraftobserver/model.go b/plugin/minecraftobserver/model.go
new file mode 100644
index 0000000000..763d5278d9
--- /dev/null
+++ b/plugin/minecraftobserver/model.go
@@ -0,0 +1,252 @@
+package minecraftobserver
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "github.com/Tnze/go-mc/chat"
+ "github.com/google/uuid"
+ "github.com/wdvxdr1123/ZeroBot/utils/helper"
+ "strings"
+ "time"
+)
+
+// ====================
+// DB Schema
+
+// serverStatus 服务器状态
+type serverStatus struct {
+ // ID 主键
+ ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"`
+ // 服务器地址
+ ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_server_addr"`
+ // 服务器描述
+ Description string `json:"description" gorm:"column:description;default:null;type:CLOB"`
+ // 在线玩家
+ Players string `json:"players" gorm:"column:players;default:''"`
+ // 版本
+ Version string `json:"version" gorm:"column:version;default:''"`
+ // FaviconMD5 Favicon MD5
+ FaviconMD5 string `json:"favicon_md5" gorm:"column:favicon_md5;default:''"`
+ // FaviconRaw 原始数据
+ FaviconRaw icon `json:"favicon_raw" gorm:"column:favicon_raw;default:null;type:CLOB"`
+ // 延迟,不可达时为-1
+ PingDelay int64 `json:"ping_delay" gorm:"column:ping_delay;default:-1"`
+ // 更新时间
+ LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"`
+}
+
+// serverSubscribe 订阅信息
+type serverSubscribe struct {
+ // ID 主键
+ ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"`
+ // 服务器地址
+ ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_ait"`
+ // 推送目标id
+ TargetID int64 `json:"target_id" gorm:"column:target_id;default:0;unique_index:udx_ait"`
+ // 类型 1:群组 2:个人
+ TargetType int64 `json:"target_type" gorm:"column:target_type;default:0;unique_index:udx_ait"`
+ // 更新时间
+ LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"`
+}
+
+const (
+ // pingDelayUnreachable 不可达
+ pingDelayUnreachable = -1
+)
+
+// isServerStatusSpecChanged 检查是否有状态变化
+func (ss *serverStatus) isServerStatusSpecChanged(newStatus *serverStatus) (res bool) {
+ res = false
+ if ss == nil || newStatus == nil {
+ res = false
+ return
+ }
+ // 描述变化、版本变化、Favicon变化
+ if ss.Description != newStatus.Description || ss.Version != newStatus.Version || ss.FaviconMD5 != newStatus.FaviconMD5 {
+ res = true
+ return
+ }
+ // 状态由不可达变为可达 or 反之
+ if (ss.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable) ||
+ (ss.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable) {
+ res = true
+ return
+ }
+ return
+}
+
+// deepCopy 深拷贝
+func (ss *serverStatus) deepCopy() (dst *serverStatus) {
+ if ss == nil {
+ return
+ }
+ dst = &serverStatus{}
+ *dst = *ss
+ return
+}
+
+// generateServerStatusMsg 生成服务器状态消息
+func (ss *serverStatus) generateServerStatusMsg() (msg string, iconBase64 string) {
+ var msgBuilder strings.Builder
+ if ss == nil {
+ return
+ }
+ msgBuilder.WriteString(ss.Description)
+ msgBuilder.WriteString("\n")
+ msgBuilder.WriteString("服务器地址:")
+ msgBuilder.WriteString(ss.ServerAddr)
+ msgBuilder.WriteString("\n")
+ // 版本
+ msgBuilder.WriteString("版本:")
+ msgBuilder.WriteString(ss.Version)
+ msgBuilder.WriteString("\n")
+ // Ping
+ if ss.PingDelay < 0 {
+ msgBuilder.WriteString("Ping延迟:超时\n")
+ } else {
+ msgBuilder.WriteString("Ping延迟:")
+ msgBuilder.WriteString(fmt.Sprintf("%d 毫秒\n", ss.PingDelay))
+ msgBuilder.WriteString("在线人数:")
+ msgBuilder.WriteString(ss.Players)
+ }
+ // 图标
+ if ss.FaviconRaw != "" && ss.FaviconRaw.checkPNG() {
+ iconBase64 = ss.FaviconRaw.toBase64String()
+ }
+ msg = msgBuilder.String()
+ return
+}
+
+// DB Schema End
+
+// ====================
+// Ping & List Response DTO
+
+// serverPingAndListResp 服务器状态数据传输对象 From mc server response
+type serverPingAndListResp struct {
+ Description chat.Message
+ Players struct {
+ Max int
+ Online int
+ Sample []struct {
+ ID uuid.UUID
+ Name string
+ }
+ }
+ Version struct {
+ Name string
+ Protocol int
+ }
+ Favicon icon
+ Delay time.Duration
+}
+
+// icon should be a PNG image that is Base64 encoded
+// (without newlines: \n, new lines no longer work since 1.13)
+// and prepended with "data:image/png;base64,".
+type icon string
+
+//func (i icon) toImage() (icon image.Image, err error) {
+// const prefix = "data:image/png;base64,"
+// if !strings.HasPrefix(string(i), prefix) {
+// return nil, errors.Errorf("server icon should prepended with %s", prefix)
+// }
+// base64png := strings.TrimPrefix(string(i), prefix)
+// r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64png))
+// icon, err = png.Decode(r)
+// return
+//}
+
+// checkPNG 检查是否为PNG
+func (i icon) checkPNG() bool {
+ const prefix = "data:image/png;base64,"
+ return strings.HasPrefix(string(i), prefix)
+}
+
+// toBase64String 转换为base64字符串
+func (i icon) toBase64String() string {
+ return "base64://" + strings.TrimPrefix(string(i), "data:image/png;base64,")
+}
+
+// genServerSubscribeSchema 将DTO转换为DB Schema
+func (dto *serverPingAndListResp) genServerSubscribeSchema(addr string, id int64) *serverStatus {
+ if dto == nil {
+ return nil
+ }
+ faviconMD5 := md5.Sum(helper.StringToBytes(string(dto.Favicon)))
+ return &serverStatus{
+ ID: id,
+ ServerAddr: addr,
+ Description: dto.Description.ClearString(),
+ Version: dto.Version.Name,
+ Players: fmt.Sprintf("%d/%d", dto.Players.Online, dto.Players.Max),
+ FaviconMD5: hex.EncodeToString(faviconMD5[:]),
+ FaviconRaw: dto.Favicon,
+ PingDelay: dto.Delay.Milliseconds(),
+ LastUpdate: time.Now().Unix(),
+ }
+}
+
+// Ping & List Response DTO End
+// ====================
+
+// ====================
+// Biz Model
+const (
+ logPrefix = "[minecraft observer] "
+)
+
+// warpTargetIDAndType 转换消息信息到订阅的目标ID和类型
+func warpTargetIDAndType(groupID, userID int64) (int64, int64) {
+ // 订阅
+ var targetID int64
+ var targetType int64
+ if groupID == 0 {
+ targetType = targetTypeUser
+ targetID = userID
+ } else {
+ targetType = targetTypeGroup
+ targetID = groupID
+ }
+ return targetID, targetType
+}
+
+// formatSubStatusChangeText 格式化状态变更文本
+func formatSubStatusChangeText(oldStatus, newStatus *serverStatus) string {
+ var msgBuilder strings.Builder
+ if oldStatus == nil || newStatus == nil {
+ return ""
+ }
+ // 变更通知
+ msgBuilder.WriteString("[Minecraft服务器状态变更通知]\n")
+ // 地址
+ msgBuilder.WriteString(fmt.Sprintf("服务器地址: %v\n", oldStatus.ServerAddr))
+ // 描述
+ if oldStatus.Description != newStatus.Description {
+ msgBuilder.WriteString("\n-----[描述变更]-----\n")
+ msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Description))
+ msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Description))
+ }
+ // 版本
+ if oldStatus.Version != newStatus.Version {
+ msgBuilder.WriteString("\n-----[版本变更]-----\n")
+ msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Version))
+ msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Version))
+ }
+ // 状态由不可达变为可达,反之
+ if oldStatus.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable {
+ msgBuilder.WriteString("\n-----[Ping延迟]-----\n")
+ msgBuilder.WriteString(fmt.Sprintf("[旧]\n超时\n"))
+ msgBuilder.WriteString(fmt.Sprintf("[新]\n%v毫秒\n", newStatus.PingDelay))
+ }
+ if oldStatus.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable {
+ msgBuilder.WriteString("\n-----[Ping延迟]-----\n")
+ msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v毫秒\n", oldStatus.PingDelay))
+ msgBuilder.WriteString(fmt.Sprintf("[新]\n超时\n"))
+ }
+ return msgBuilder.String()
+}
+
+// Biz Model End
+// ====================
diff --git a/plugin/minecraftobserver/ping.go b/plugin/minecraftobserver/ping.go
new file mode 100644
index 0000000000..e4691c5ecd
--- /dev/null
+++ b/plugin/minecraftobserver/ping.go
@@ -0,0 +1,62 @@
+package minecraftobserver
+
+import (
+ "encoding/json"
+ "github.com/RomiChan/syncx"
+ "github.com/Tnze/go-mc/bot"
+ "time"
+)
+
+var (
+ // pingServerUnreachableCounter Ping服务器不可达计数器,防止bot本体网络抖动导致误报
+ pingServerUnreachableCounter = syncx.Map[string, pingServerUnreachableCounterDef]{}
+ // 计数器阈值
+ pingServerUnreachableCounterThreshold = int64(3)
+ // 时间阈值
+ pingServerUnreachableCounterTimeThreshold = time.Minute * 30
+)
+
+type pingServerUnreachableCounterDef struct {
+ count int64
+ firstUnreachableTime time.Time
+}
+
+func addPingServerUnreachableCounter(addr string, ts time.Time) (int64, time.Time) {
+ key := addr
+ get, ok := pingServerUnreachableCounter.Load(key)
+ if !ok {
+ pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{
+ count: 1,
+ firstUnreachableTime: ts,
+ })
+ return 1, ts
+ }
+ // 存在则更新,时间戳不变
+ pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{
+ count: get.count + 1,
+ firstUnreachableTime: get.firstUnreachableTime,
+ })
+ return get.count + 1, get.firstUnreachableTime
+}
+
+func resetPingServerUnreachableCounter(addr string) {
+ key := addr
+ pingServerUnreachableCounter.Delete(key)
+}
+
+// getMinecraftServerStatus 获取Minecraft服务器状态
+func getMinecraftServerStatus(addr string) (*serverPingAndListResp, error) {
+ var s serverPingAndListResp
+ resp, delay, err := bot.PingAndListTimeout(addr, time.Second*5)
+ if err != nil {
+ //logrus.Errorln(logPrefix+"PingAndList error: ", err)
+ return nil, err
+ }
+ err = json.Unmarshal(resp, &s)
+ if err != nil {
+ //logrus.Errorln(logPrefix+"Parse json response fail: ", err)
+ return nil, err
+ }
+ s.Delay = delay
+ return &s, nil
+}
diff --git a/plugin/minecraftobserver/ping_test.go b/plugin/minecraftobserver/ping_test.go
new file mode 100644
index 0000000000..8f44078935
--- /dev/null
+++ b/plugin/minecraftobserver/ping_test.go
@@ -0,0 +1,27 @@
+package minecraftobserver
+
+import (
+ "fmt"
+ "testing"
+)
+
+func Test_PingListInfo(t *testing.T) {
+ t.Run("normal", func(t *testing.T) {
+ resp, err := getMinecraftServerStatus("cn.nekoland.top")
+ if err != nil {
+ t.Fatalf("getMinecraftServerStatus() error = %v", err)
+ }
+ msg, iconBase64 := resp.genServerSubscribeSchema("cn.nekoland.top", 123456).generateServerStatusMsg()
+ fmt.Printf("msg: %v\n", msg)
+ fmt.Printf("iconBase64: %v\n", iconBase64)
+ })
+ t.Run("不可达", func(t *testing.T) {
+ ss, err := getMinecraftServerStatus("dx.123213213123123.net")
+ if err == nil {
+ t.Fatalf("getMinecraftServerStatus() error = %v", err)
+ }
+ if ss != nil {
+ t.Fatalf("getMinecraftServerStatus() got = %v, want nil", ss)
+ }
+ })
+}
diff --git a/plugin/minecraftobserver/store.go b/plugin/minecraftobserver/store.go
new file mode 100644
index 0000000000..ff22fabd08
--- /dev/null
+++ b/plugin/minecraftobserver/store.go
@@ -0,0 +1,220 @@
+package minecraftobserver
+
+import (
+ "errors"
+ fcext "github.com/FloatTech/floatbox/ctxext"
+ "github.com/jinzhu/gorm"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+ "os"
+ "sync"
+ "time"
+)
+
+const (
+ dbPath = "minecraft_observer"
+
+ targetTypeGroup = 1
+ targetTypeUser = 2
+)
+
+var (
+ // 数据库连接失败
+ errDBConn = errors.New("数据库连接失败")
+ // 参数错误
+ errParam = errors.New("参数错误")
+)
+
+type db struct {
+ sdb *gorm.DB
+ statusLock sync.RWMutex
+ subscribeLock sync.RWMutex
+}
+
+// initializeDB 初始化数据库
+func initializeDB(dbpath string) error {
+ if _, err := os.Stat(dbpath); err != nil || os.IsNotExist(err) {
+ // 生成文件
+ f, err := os.Create(dbpath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ }
+ gdb, err := gorm.Open("sqlite3", dbpath)
+ if err != nil {
+ //logrus.Errorln(logPrefix+"initializeDB ERROR: ", err)
+ return err
+ }
+ gdb.AutoMigrate(&serverStatus{}, &serverSubscribe{})
+ dbInstance = &db{
+ sdb: gdb,
+ statusLock: sync.RWMutex{},
+ subscribeLock: sync.RWMutex{},
+ }
+ return nil
+}
+
+var (
+ // dbInstance 数据库实例
+ dbInstance *db
+ // 开启并检查数据库链接
+ getDB = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
+ var err error
+ err = initializeDB(engine.DataFolder() + dbPath)
+ if err != nil {
+ //logrus.Errorln(logPrefix+"initializeDB ERROR: ", err)
+ ctx.SendChain(message.Text("[mc-ob] ERROR: ", err))
+ return false
+ }
+ return true
+ })
+)
+
+// 通过群组id和服务器地址获取状态
+func (d *db) getServerStatus(addr string) (*serverStatus, error) {
+ if d == nil {
+ return nil, errDBConn
+ }
+ if addr == "" {
+ return nil, errParam
+ }
+ var ss serverStatus
+ if err := d.sdb.Model(&ss).Where("server_addr = ?", addr).First(&ss).Error; err != nil {
+ //logrus.Errorln(logPrefix+"getServerStatus ERROR: ", err)
+ return nil, err
+ }
+ return &ss, nil
+}
+
+// 更新服务器状态
+func (d *db) updateServerStatus(ss *serverStatus) (err error) {
+ if d == nil {
+ return errDBConn
+ }
+ d.statusLock.Lock()
+ defer d.statusLock.Unlock()
+ if ss == nil || ss.ServerAddr == "" {
+ return errParam
+ }
+ ss.LastUpdate = time.Now().Unix()
+ ss2 := ss.deepCopy()
+ if err = d.sdb.Where(&serverStatus{ServerAddr: ss.ServerAddr}).Assign(ss2).FirstOrCreate(ss).Debug().Error; err != nil {
+ //logrus.Errorln(logPrefix, fmt.Sprintf("updateServerStatus %v ERROR: %v", ss, err))
+ return
+ }
+ return
+}
+
+func (d *db) delServerStatus(addr string) (err error) {
+ if d == nil {
+ return errDBConn
+ }
+ if addr == "" {
+ return errParam
+ }
+ d.statusLock.Lock()
+ defer d.statusLock.Unlock()
+ if err = d.sdb.Where("server_addr = ?", addr).Delete(&serverStatus{}).Error; err != nil {
+ //logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
+ return
+ }
+ return
+}
+
+// 新增订阅
+func (d *db) newSubscribe(addr string, targetID, targetType int64) (err error) {
+ if d == nil {
+ return errDBConn
+ }
+ if targetID == 0 || (targetType != 1 && targetType != 2) {
+ //logrus.Errorln(logPrefix+"newSubscribe ERROR: 参数错误 ", targetID, " ", targetType)
+ return errParam
+ }
+ d.subscribeLock.Lock()
+ defer d.subscribeLock.Unlock()
+ // 如果已经存在,需要报错
+ existedRec := &serverSubscribe{}
+ err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(existedRec).Error
+ if err != nil && !gorm.IsRecordNotFoundError(err) {
+ //logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err)
+ return
+ }
+ if existedRec.ID != 0 {
+ return errors.New("已经存在的订阅")
+ }
+ ss := &serverSubscribe{
+ ServerAddr: addr,
+ TargetID: targetID,
+ TargetType: targetType,
+ LastUpdate: time.Now().Unix(),
+ }
+ if err = d.sdb.Model(&ss).Create(ss).Error; err != nil {
+ //logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err)
+ return
+ }
+ return
+}
+
+// 删除订阅
+func (d *db) deleteSubscribe(addr string, targetID int64, targetType int64) (err error) {
+ if d == nil {
+ return errDBConn
+ }
+ if addr == "" || targetID == 0 || targetType == 0 {
+ return errParam
+ }
+ d.subscribeLock.Lock()
+ defer d.subscribeLock.Unlock()
+ // 检查是否存在
+ if err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(&serverSubscribe{}).Error; err != nil {
+ if gorm.IsRecordNotFoundError(err) {
+ return errors.New("未找到订阅")
+ }
+ //logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
+ return
+ }
+
+ if err = d.sdb.Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).Delete(&serverSubscribe{}).Error; err != nil {
+ //logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
+ return
+ }
+
+ // 扫描是否还有订阅,如果没有则删除服务器状态
+ var cnt int
+ err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ?", addr).Count(&cnt).Error
+ if err != nil {
+ //logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
+ return
+ }
+ if cnt == 0 {
+ _ = d.delServerStatus(addr)
+ }
+ return
+}
+
+// 获取所有订阅
+func (d *db) getAllSubscribes() (subs []serverSubscribe, err error) {
+ if d == nil {
+ return nil, errDBConn
+ }
+ subs = []serverSubscribe{}
+ if err = d.sdb.Find(&subs).Error; err != nil {
+ //logrus.Errorln(logPrefix+"getAllSubscribes ERROR: ", err)
+ return
+ }
+ return
+}
+
+// 获取渠道对应的订阅列表
+func (d *db) getSubscribesByTarget(targetID, targetType int64) (subs []serverSubscribe, err error) {
+ if d == nil {
+ return nil, errDBConn
+ }
+ subs = []serverSubscribe{}
+ if err = d.sdb.Model(&serverSubscribe{}).Where("target_id = ? and target_type = ?", targetID, targetType).Find(&subs).Error; err != nil {
+ //logrus.Errorln(logPrefix+"getSubscribesByTarget ERROR: ", err)
+ return
+ }
+ return
+}
diff --git a/plugin/minecraftobserver/store_test.go b/plugin/minecraftobserver/store_test.go
new file mode 100644
index 0000000000..d96c82d132
--- /dev/null
+++ b/plugin/minecraftobserver/store_test.go
@@ -0,0 +1,317 @@
+package minecraftobserver
+
+import (
+ "errors"
+ "fmt"
+ "github.com/jinzhu/gorm"
+ "testing"
+)
+
+func cleanTestData(t *testing.T) {
+ err := dbInstance.sdb.Delete(&serverStatus{}).Where("id > 0").Error
+ if err != nil {
+ t.Fatalf("cleanTestData() error = %v", err)
+ }
+ err = dbInstance.sdb.Delete(&serverSubscribe{}).Where("id > 0").Error
+ if err != nil {
+ t.Fatalf("cleanTestData() error = %v", err)
+ }
+}
+
+func Test_DAO(t *testing.T) {
+ initErr := initializeDB("data/minecraftobserver/" + dbPath)
+ if initErr != nil {
+ t.Fatalf("initializeDB() error = %v", initErr)
+ }
+ if dbInstance == nil {
+ t.Fatalf("initializeDB() got = %v, want not nil", dbInstance)
+ }
+ t.Run("insert", func(t *testing.T) {
+ cleanTestData(t)
+ newSS1 := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ }
+ newSS2 := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.8",
+ FaviconMD5: "1234567",
+ }
+ err := dbInstance.updateServerStatus(newSS1)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.updateServerStatus(newSS2)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+
+ // check insert
+ queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net")
+ if err != nil {
+ t.Fatalf("getServerStatus() error = %v", err)
+ }
+ if queryResult == nil {
+ t.Fatalf("getServerStatus() got = %v, want not nil", queryResult)
+ }
+ if queryResult.Version != "1.16.8" {
+ t.Fatalf("getServerStatus() got = %v, want 1.16.8", queryResult.Version)
+ }
+
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeUser)
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ // check insert
+ res, err := dbInstance.getAllSubscribes()
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ if len(res) != 2 {
+ t.Fatalf("getAllServer() got = %v, want 2", len(res))
+ }
+ // 检查是否符合预期
+ if res[0].ServerAddr != "dx.zhaomc.net" {
+ t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[0].ServerAddr)
+ }
+ if res[0].TargetType != targetTypeGroup {
+ t.Fatalf("getAllServer() got = %v, want %v", res[0].TargetType, targetTypeGroup)
+ }
+ if res[1].ServerAddr != "dx.zhaomc.net" {
+ t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[1].ServerAddr)
+ }
+ if res[1].TargetType != targetTypeUser {
+ t.Fatalf("getAllServer() got = %v, want %v", res[1].TargetType, targetTypeUser)
+ }
+
+ // 顺带验证一下 byTarget
+ res2, err := dbInstance.getSubscribesByTarget(123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("getSubscribesByTarget() error = %v", err)
+ }
+ if len(res2) != 1 {
+ t.Fatalf("getSubscribesByTarget() got = %v, want 1", len(res2))
+ }
+
+ })
+ // 重复添加订阅
+ t.Run("insert dup", func(t *testing.T) {
+ cleanTestData(t)
+ newSS := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ }
+ err := dbInstance.updateServerStatus(newSS)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err == nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ fmt.Printf("insert dup error: %+v", err)
+ })
+
+ t.Run("update", func(t *testing.T) {
+ cleanTestData(t)
+ newSS := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ }
+ err := dbInstance.updateServerStatus(newSS)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.updateServerStatus(&serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "更新测试",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ })
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ // check update
+ queryResult2, err := dbInstance.getServerStatus("dx.zhaomc.net")
+ if err != nil {
+ t.Errorf("getAllServer() error = %v", err)
+ }
+ if queryResult2.Description != "更新测试" {
+ t.Errorf("getAllServer() got = %v, want 更新测试", queryResult2.Description)
+ }
+ })
+ t.Run("delete status", func(t *testing.T) {
+ cleanTestData(t)
+ newSS := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ }
+ err := dbInstance.updateServerStatus(newSS)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ // check insert
+ queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net")
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ if queryResult == nil {
+ t.Fatalf("getAllServer() got = %v, want not nil", queryResult)
+ }
+ err = dbInstance.delServerStatus("dx.zhaomc.net")
+ if err != nil {
+ t.Fatalf("deleteServerStatus() error = %v", err)
+ }
+ // check delete
+ _, err = dbInstance.getServerStatus("dx.zhaomc.net")
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+
+ })
+
+ // 删除订阅
+ t.Run("delete subscribe", func(t *testing.T) {
+ cleanTestData(t)
+ newSS := &serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ }
+ err := dbInstance.updateServerStatus(newSS)
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("deleteSubscribe() error = %v", err)
+ }
+ // check delete
+ _, err = dbInstance.getServerStatus("dx.zhaomc.net")
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ })
+
+ // 重复删除订阅
+ t.Run("delete subscribe dup", func(t *testing.T) {
+ cleanTestData(t)
+ err := dbInstance.updateServerStatus(&serverStatus{
+ ServerAddr: "dx.zhaomc.net",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ })
+ if err != nil {
+ t.Errorf("upsertServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("newSubscribe() error = %v", err)
+ }
+
+ err = dbInstance.newSubscribe("dx.zhaomc.net123", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("newSubscribe() error = %v", err)
+ }
+ err = dbInstance.updateServerStatus(&serverStatus{
+ ServerAddr: "dx.zhaomc.net123",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ })
+ if err != nil {
+ t.Fatalf("updateServerStatus() error = %v", err)
+ }
+ err = dbInstance.newSubscribe("dx.zhaomc.net4567", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("newSubscribe() error = %v", err)
+ }
+ err = dbInstance.updateServerStatus(&serverStatus{
+ ServerAddr: "dx.zhaomc.net4567",
+ Description: "测试服务器",
+ Players: "1/20",
+ Version: "1.16.5",
+ FaviconMD5: "1234567",
+ })
+ if err != nil {
+ t.Fatalf("updateServerStatus() error = %v", err)
+ }
+
+ // 检查是不是3个
+ allSub, err := dbInstance.getAllSubscribes()
+ if err != nil {
+ t.Fatalf("getAllSubscribes() error = %v", err)
+ }
+ if len(allSub) != 3 {
+ t.Fatalf("getAllSubscribes() got = %v, want 3", len(allSub))
+ }
+ err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err != nil {
+ t.Fatalf("deleteSubscribe() error = %v", err)
+ }
+ err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
+ if err == nil {
+ t.Fatalf("deleteSubscribe() error = %v", err)
+ }
+ fmt.Println("delete dup error: ", err)
+
+ // 检查其他的没有被删
+ allSub, err = dbInstance.getAllSubscribes()
+ if err != nil {
+ t.Fatalf("getAllSubscribes() error = %v", err)
+ }
+ // 检查是否符合预期
+ if len(allSub) != 2 {
+ t.Fatalf("getAllSubscribes() got = %v, want 2", len(allSub))
+ }
+ // 状态
+ _, err = dbInstance.getServerStatus("dx.zhaomc.net")
+ if !gorm.IsRecordNotFoundError(err) {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ status1, err := dbInstance.getServerStatus("dx.zhaomc.net123")
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ status2, err := dbInstance.getServerStatus("dx.zhaomc.net4567")
+ if err != nil {
+ t.Fatalf("getAllServer() error = %v", err)
+ }
+ if status1 == nil || status2 == nil {
+ t.Fatalf("getAllServer() want not nil")
+ }
+
+ })
+}