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") + } + + }) +}