├── .gitattributes ├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.go ├── build.bat ├── customize └── customize.go ├── data ├── config.go ├── config_test.go ├── database.go ├── database_test.go ├── files.go ├── log.go └── rcon.go ├── go.mod ├── go.sum ├── log └── log.go ├── ping ├── ping.go └── ping_test.go ├── syntax └── input.go └── whitelist ├── query.go └── whitelist.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | 4 | cqcfg.exe 5 | .DS_Store 6 | 7 | app.dll 8 | app.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 miaoscraft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SiS 2 | [![Build status](https://ci.appveyor.com/api/projects/status/5m4fip2k59kurfcn?svg=true)](https://ci.appveyor.com/project/Tnze/sis) 3 | 基于酷Q的MC服务器综合管理插件,主要提供白名单管理和状态查询(ping)功能。 4 | 5 | 本插件已在酷Q社区发布~ 6 | https://cqp.cc/t/44736 7 | 8 | 用法与配置请查看[Wiki](https://github.com/miaoscraft/SiS/wiki) 9 | 10 | ## 功能 11 | 本插件适用于包括原版服务端在内的各种MC服务端。 12 | 1. 白名单 13 | 群员可以自助获得白名单,游戏名经Mojang服务器验证后通过RCON发送到游戏服务器进行添加。 14 | 玩家主动或被动退群时,将被自动从白名单中移除。 15 | 2. Ping 16 | 可在群内ping游戏服务器,查看延迟以及在线玩家。 17 | ~~我们经常拿来判断是自己网络不好还是服务器崩了~~。 18 | 3. 自定义指令 19 | 在配置文件中预先写好指令,然后在Q群内调用。 20 | 拥有简单明了的权限系统用于保证命令不被恶意执行。 21 | 22 | ## 鸣谢 23 | 感谢他们⬇️对SiS的付出 24 | 25 | [Tnze](https://github.com/Tnze)(开发者) 26 | [fcc](https://github.com/Amazefcc233)(测试,提示语优化,文案,社区发布,装可爱) 27 | [柏喵](https://github.com/MscBaiMeow)(提示语优化,服主) 28 | [Miaoscraft](https://miaoscraft.cn)(感谢相遇) 29 | 30 | ## 依赖 31 | 感谢下列项目,没有它们SiS不将诞生 32 | 33 | - Go语言 https://golang.org 34 | - MC协议文档 https://wiki.vg 35 | - go-mc库 https://github.com/Tnze/go-mc 36 | - 酷Q插件SDK https://github.com/Tnze/CoolQ-Golang-SDK 37 | - SQLite驱动 https://github.com/mattn/go-sqlite3 38 | - MySQL驱动 https://github.com/go-sql-driver/mysql 39 | - Toml配置文件 https://github.com/BurntSushi/toml -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Tnze/CoolQ-Golang-SDK/cqp" 6 | "github.com/google/uuid" 7 | "github.com/miaoscraft/SiS/customize" 8 | "github.com/miaoscraft/SiS/data" 9 | "github.com/miaoscraft/SiS/log" 10 | "github.com/miaoscraft/SiS/syntax" 11 | "github.com/miaoscraft/SiS/whitelist" 12 | "net/http" 13 | "net/url" 14 | "regexp" 15 | ) 16 | 17 | //go:generate cqcfg -c . 18 | // cqp: 名称: SiS 19 | // cqp: 版本: 1.3.4:1 20 | // cqp: 作者: Tnze 21 | // cqp: 简介: Minecraft服务器综合管理器 22 | func main() { /*空*/ } 23 | 24 | func init() { 25 | cqp.AppID = "cn.miaoscraft.sis" 26 | cqp.Enable = onStart 27 | cqp.Disable = onStop 28 | cqp.Exit = onStop 29 | 30 | cqp.GroupMsg = onGroupMsg 31 | cqp.GroupMemberDecrease = onGroupMemberDecrease 32 | cqp.GroupRequest = onGroupRequest 33 | 34 | customize.Logger = log.NewLogger("Cstm") 35 | whitelist.Logger = log.NewLogger("MyID") 36 | data.Logger = log.NewLogger("Data") 37 | } 38 | 39 | var Logger = log.NewLogger("Main") 40 | 41 | // 插件生命周期开始 42 | func onStart() int32 { 43 | // 连接数据源 44 | err := data.Init(cqp.GetAppDir()) 45 | if err != nil { 46 | Logger.Errorf("初始化数据源失败: %v", err) 47 | } 48 | 49 | // 将登录账号载入命令解析器(用于识别@) 50 | syntax.CmdPrefix = fmt.Sprintf("[CQ:at,qq=%d]", cqp.GetLoginQQ()) 51 | 52 | return 0 53 | } 54 | 55 | // 插件生命周期结束 56 | func onStop() int32 { 57 | err := data.Close() 58 | if err != nil { 59 | Logger.Errorf("释放数据源失败: %v", err) 60 | } 61 | return 0 62 | } 63 | 64 | // 群消息事件 65 | func onGroupMsg(subType, msgID int32, fromGroup, fromQQ int64, fromAnonymous, msg string, font int32) int32 { 66 | if fromQQ == 80000000 { // 忽略匿名 67 | return Ignore 68 | } 69 | 70 | ret := func(resp string) { 71 | cqp.SendGroupMsg(fromGroup, resp) 72 | } 73 | 74 | switch fromGroup { 75 | case data.Config.AdminID: 76 | // 当前版本,管理群和游戏群收到的命令不做区分 77 | fallthrough 78 | case data.Config.GroupID: 79 | if syntax.GroupMsg(fromQQ, msg, ret) { 80 | return Intercept 81 | } 82 | } 83 | return Ignore 84 | } 85 | 86 | // 群成员减少事件 87 | func onGroupMemberDecrease(subType, sendTime int32, fromGroup, fromQQ, beingOperateQQ int64) int32 { 88 | retValue := Ignore 89 | ret := func(resp string) { 90 | cqp.SendGroupMsg(fromGroup, resp) 91 | retValue = Intercept 92 | } 93 | // 尝试删白名单 94 | if fromGroup == data.Config.GroupID { 95 | whitelist.RemoveWhitelist(beingOperateQQ, ret) 96 | } 97 | return retValue 98 | } 99 | 100 | // 入群请求事件 101 | func onGroupRequest(subType, sendTime int32, fromGroup, fromQQ int64, msg, respFlag string) int32 { 102 | if !data.Config.DealWithGroupRequest.Enable || // 功能未启用 103 | fromGroup != data.Config.GroupID || // 不是要管理的群 104 | subType != 1 { // 不是他人要申请入群 105 | return Ignore 106 | } 107 | Logger := log.NewLogger("DwGR") 108 | for _, name := range regexp.MustCompile(`[0-9A-Za-z_]{3,16}`).FindAllString(msg, 3) { 109 | name, id, err := whitelist.GetUUID(name) 110 | if err != nil { 111 | Logger.Infof("处理%d的入群请求,检查游戏名失败: %v", fromQQ, err) 112 | continue 113 | } 114 | 115 | if ok, err := checkRequest(name, id); err != nil { 116 | Logger.Errorf("服务器检查出错: %v", err) 117 | return Ignore 118 | } else if !ok { 119 | Logger.Infof("服务器拒绝%d作为%s入群: %v", fromQQ, name, err) 120 | if data.Config.DealWithGroupRequest.CanReject { 121 | cqp.SetGroupAddRequest(respFlag, subType, Deny, "") 122 | return Intercept 123 | } 124 | return Ignore 125 | } 126 | Logger.Infof("允许%d作为%s入群", fromQQ, name) 127 | cqp.SetGroupAddRequest(respFlag, subType, Allow, "") 128 | 129 | ret := func(resp string) { cqp.SendGroupMsg(fromGroup, resp) } 130 | whitelist.MyID(fromQQ, name, ret) 131 | return Intercept 132 | } 133 | return Ignore 134 | } 135 | 136 | func checkRequest(name string, id uuid.UUID) (bool, error) { 137 | if data.Config.DealWithGroupRequest.CheckURL == "" { 138 | return true, nil 139 | } 140 | resp, err := http.PostForm( 141 | data.Config.DealWithGroupRequest.CheckURL, 142 | url.Values{ 143 | "Token": []string{data.Config.DealWithGroupRequest.Token}, 144 | "Name": []string{name}, 145 | "UUID": []string{id.String()}, 146 | }, 147 | ) 148 | if err != nil { 149 | return false, fmt.Errorf("请求出错: %v", err) 150 | } 151 | defer resp.Body.Close() 152 | if resp.StatusCode == http.StatusNoContent { 153 | return true, nil 154 | } 155 | return false, nil 156 | } 157 | 158 | const ( 159 | Ignore int32 = 0 //忽略消息 160 | Intercept = 1 //拦截消息 161 | 162 | Allow = 1 // 允许进群 163 | Deny = 2 // 拒绝进群 164 | ) 165 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: SET DevDir=D:\CoolQ Pro\dev\cn.miaoscraft.sis 4 | 5 | echo Setting proxy 6 | SET GOPROXY=https://goproxy.cn 7 | 8 | echo Checking go installation... 9 | go version > nul 10 | IF ERRORLEVEL 1 ( 11 | echo Please install go first... 12 | goto RETURN 13 | ) 14 | 15 | echo Checking gcc installation... 16 | gcc --version > nul 17 | IF ERRORLEVEL 1 ( 18 | echo Please install gcc first... 19 | goto RETURN 20 | ) 21 | 22 | echo Checking cqcfg installation... 23 | cqcfg -v 24 | IF ERRORLEVEL 1 ( 25 | echo Install cqcfg... 26 | go get github.com/Tnze/CoolQ-Golang-SDK/tools/cqcfg@master 27 | IF ERRORLEVEL 1 ( 28 | echo Install cqcfg fail 29 | goto RETURN 30 | ) 31 | ) 32 | 33 | echo Generating app.json ... 34 | go generate 35 | IF ERRORLEVEL 1 ( 36 | echo Generate app.json fail 37 | goto RETURN 38 | ) 39 | echo. 40 | 41 | echo Setting env vars.. 42 | SET CGO_LDFLAGS=-Wl,--kill-at 43 | SET CGO_ENABLED=1 44 | SET GOOS=windows 45 | SET GOARCH=386 46 | 47 | echo Building app.dll ... 48 | go build -ldflags "-s -w" -buildmode=c-shared -ldflags "-extldflags ""-static""" -o app.dll 49 | IF ERRORLEVEL 1 (pause) ELSE (echo Build success!) 50 | 51 | if defined DevDir ( 52 | echo Copy app.dll amd app.json ... 53 | for %%f in (app.dll,app.json) do move %%f "%DevDir%\%%f" > nul 54 | IF ERRORLEVEL 1 pause 55 | ) 56 | 57 | exit /B 58 | 59 | :RETURN 60 | if not defined NOPAUSE pause 61 | exit /B 62 | -------------------------------------------------------------------------------- /customize/customize.go: -------------------------------------------------------------------------------- 1 | // Package customize 提供自定义指令的实现 2 | package customize 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/miaoscraft/SiS/data" 10 | ) 11 | 12 | // 检查命令是否匹配一个自定义命令,若是的话则丢到RCON执行 13 | // args长度必须大于0 14 | func Exec(args []string, fromQQ int64, ret func(string)) bool { 15 | cmds, ok := data.Config.Cmd[args[0]] 16 | if !ok { 17 | return false 18 | } 19 | 20 | // 获取权限 21 | level, err := data.GetLevel(fromQQ) 22 | if err != nil { 23 | Logger.Errorf("获取权限出错: %v", err) 24 | ret("当前没有办法验证权限呢") 25 | return false 26 | } 27 | // 权限确认 28 | if cmds.Level <= level { 29 | Logger.Infof("成员%d以等级%d执行指令%q", fromQQ, level, cmds.Command) 30 | 31 | rconCmd := cmds.Command 32 | if cmds.AllowArgs { 33 | rconCmd += " " + strings.Join(args[1:], " ") 34 | } 35 | 36 | // 执行指令 37 | var subret func(string) 38 | if !cmds.Silent { 39 | subret = ret 40 | } 41 | err := data.RCONCmd(rconCmd, subret) 42 | if err != nil { 43 | Logger.Errorf("执行命令出错: %v", err) 44 | ret("服务器被玩坏啦?!") 45 | } 46 | return true 47 | 48 | } else { 49 | //权限不足 50 | ret("你不能够执行这个命令哦~") 51 | return false 52 | } 53 | } 54 | 55 | func Auth(args []string, fromQQ int64, ret func(string)) bool { 56 | // args: ["auth", "@Member" | "QQ-num", "level"] 57 | if len(args) < 2 || args[0] != "auth" { 58 | return false 59 | } 60 | 61 | // 解析目标QQ 62 | var target int64 63 | if _, err := fmt.Sscanf(args[1], "[CQ:at,qq=%d]", &target); err == nil { 64 | } else if target, err = strconv.ParseInt(args[1], 10, 64); err == nil { 65 | } else { 66 | return false 67 | } 68 | 69 | if len(args) < 3 { // auth查询 70 | return getAuth(fromQQ, target, ret) 71 | } // auth设置 72 | 73 | // 解析权限等级 74 | level, err := strconv.ParseInt(args[2], 10, 64) 75 | if err != nil { 76 | return false 77 | } 78 | return setAuth(fromQQ, target, level, ret) 79 | } 80 | 81 | func getAuth(from, target int64, ret func(string)) bool { 82 | cmds, _ := data.Config.Cmd["auth"] 83 | // 查询是否有auth查询权限 84 | level, err := data.GetLevel(from) 85 | if err != nil { 86 | Logger.Errorf("获取权限出错: %v", err) 87 | ret("当前没有办法验证权限呢") 88 | return false 89 | } 90 | // 检查权限 91 | if cmds.Level <= level { 92 | level, err := data.GetLevel(target) 93 | if err != nil { 94 | Logger.Errorf("查询权限出错: %v", err) 95 | ret("查询时出现了问题(つД`)ノ") 96 | } else { 97 | ret(fmt.Sprintf("( ̄▽ ̄)~*%d", level)) 98 | } 99 | } else { 100 | //权限不足 101 | ret("你不能够执行这个命令哦~") 102 | return false 103 | } 104 | 105 | return true 106 | } 107 | 108 | func setAuth(from, targetQQ, targetLevel int64, ret func(string)) bool { 109 | // 确认是否是超级管理员 110 | for _, v := range data.Config.Administrators { 111 | if v == from { 112 | // 该用户属于最高管理员 113 | Logger.Infof("将%d的权限设置为%d", targetQQ, targetLevel) 114 | 115 | if err := data.SetLevel(targetQQ, targetLevel); err != nil { 116 | Logger.Errorf("设置权限出错: %v", err) 117 | ret("这里出现了问题(つД`)ノ") 118 | } else { 119 | ret("成功了( ̀⌄ ́)") 120 | } 121 | return true 122 | } 123 | } 124 | return false 125 | } 126 | 127 | var Logger interface { 128 | Error(str string) 129 | Errorf(format string, args ...interface{}) 130 | 131 | Waring(str string) 132 | Waringf(format string, args ...interface{}) 133 | 134 | Info(str string) 135 | Infof(format string, args ...interface{}) 136 | 137 | Debug(str string) 138 | Debugf(format string, args ...interface{}) 139 | } 140 | -------------------------------------------------------------------------------- /data/config.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | "text/template" 8 | "time" 9 | 10 | "github.com/BurntSushi/toml" 11 | ) 12 | 13 | // AppDir 当前插件数据目录 14 | var AppDir string 15 | 16 | // Init 初始化插件的数据源,包括读取配置文件、建立数据库连接 17 | func Init(dir string) error { 18 | AppDir = dir 19 | // 初始化默认文件 20 | err := initFiles() 21 | if err != nil { 22 | return fmt.Errorf("创建文件时出错: %v", err) 23 | } 24 | // 读取配置文件 25 | err = readConfig() 26 | if err != nil { 27 | return fmt.Errorf("读配置文件出错: %v", err) 28 | } 29 | 30 | // 连接数据库 31 | err = openDB(Config.Database.Driver, Config.Database.Source) 32 | if err != nil { 33 | return fmt.Errorf("打开数据库出错: %v", err) 34 | } 35 | 36 | // 连接MC服务器 37 | err = openRCON(Config.RCON.Address, Config.RCON.Password) 38 | if err != nil { 39 | return fmt.Errorf("连接RCON出错: %v", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // Close 关闭所有打开的资源 46 | func Close() error { 47 | err := closeDB() 48 | if err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | var Config struct { 55 | // 游戏群 56 | GroupID int64 57 | // 管理群 58 | AdminID int64 59 | // 管理员 60 | Administrators []int64 61 | // 处理进群请求 62 | DealWithGroupRequest struct { 63 | Enable bool 64 | CanReject bool 65 | CheckURL string 66 | Token string 67 | } 68 | 69 | // 数据库配置 70 | Database struct { 71 | Driver string 72 | Source string 73 | } 74 | 75 | // MC服务器远程控制台 76 | RCON struct { 77 | Address string 78 | Password string 79 | } 80 | // Ping工具配置 81 | Ping struct { 82 | DefaultServer string 83 | Timeout duration 84 | } 85 | 86 | // 自定义命令 87 | Cmd map[string]struct { 88 | Level int64 // 所需权限 89 | Command string // 指令本身 90 | Silent bool // 是否不回显 91 | AllowArgs bool // 是否允许使用参数 92 | } 93 | } 94 | 95 | type duration struct { 96 | time.Duration 97 | } 98 | 99 | func (d *duration) UnmarshalText(text []byte) error { 100 | var err error 101 | d.Duration, err = time.ParseDuration(string(text)) 102 | return err 103 | } 104 | 105 | func readConfig() error { 106 | md, err := toml.DecodeFile(filepath.Join(AppDir, "conf.toml"), &Config) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | // 检查配置文件是否有多余数据,抛警告⚠️ 112 | if uk := md.Undecoded(); len(uk) > 0 { 113 | Logger.Waringf("配置文件中有未知数据: %q", uk) 114 | } 115 | 116 | // 替换文件路径Database中Source的文件路径 117 | Config.Database.Source, err = rendingDBSource(Config.Database.Source) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | return nil 123 | } 124 | 125 | func rendingDBSource(raw string) (string, error) { 126 | var sb strings.Builder 127 | temp, err := template. 128 | New("DBSource"). 129 | Funcs(template.FuncMap{ 130 | "join": filepath.Join, 131 | }). 132 | Parse(raw) 133 | if err != nil { 134 | return "", fmt.Errorf("解析模版失败: %v", err) 135 | } 136 | 137 | err = temp.Execute(&sb, struct{ AppDir string }{AppDir}) 138 | if err != nil { 139 | return "", fmt.Errorf("渲染模版失败: %v", err) 140 | } 141 | 142 | return sb.String(), nil 143 | } 144 | -------------------------------------------------------------------------------- /data/config_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "testing" 4 | 5 | // 又是一个很烂的单元测试 6 | func TestReadConfig(t *testing.T) { 7 | if err := initFiles(); err != nil { 8 | t.Fatal(err) 9 | } 10 | 11 | if err := readConfig(); err != nil { 12 | t.Fatal(err) 13 | } 14 | t.Log(Config) 15 | } 16 | -------------------------------------------------------------------------------- /data/database.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/go-sql-driver/mysql" 8 | "github.com/google/uuid" 9 | _ "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | // 启动数据库 15 | func openDB(driver, source string) (err error) { 16 | db, err = sql.Open(driver, source) 17 | if err != nil { 18 | return fmt.Errorf("打开数据库失败: %v", err) 19 | } 20 | 21 | err = initDB() 22 | if err != nil { 23 | return fmt.Errorf("初始化数据库失败: %v", err) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // 关闭数据库 30 | func closeDB() error { 31 | if db != nil { 32 | return db.Close() 33 | } 34 | return nil 35 | } 36 | 37 | // 初始化数据库 38 | func initDB() error { 39 | // "QQ->UUID", "UUID->QQ", "QQ->Level", 40 | _, err := db.Exec(` 41 | CREATE TABLE IF NOT EXISTS users( 42 | QQ INTEGER PRIMARY KEY , 43 | UUID BLOB NOT NULL 44 | ); 45 | `) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | _, err = db.Exec(` 51 | CREATE TABLE IF NOT EXISTS auths( 52 | QQ INTEGER PRIMARY KEY , 53 | Level INT DEFAULT 0 54 | ); 55 | `) 56 | if err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | // SetWhitelist 尝试向数据库写入白名单数据,当ID未被占用时返回自己的QQ,当ID被占用则返回占用者的QQ 63 | // 若原本该账号占有一个UUID,则会返回当时的UUID 64 | func SetWhitelist(QQ int64, ID uuid.UUID, onOldID func(oldID uuid.UUID) error, onSuccess func() error) (owner int64, err error) { 65 | var tx *sql.Tx 66 | tx, err = db.Begin() 67 | if err != nil { 68 | err = fmt.Errorf("数据库开始事务失败: %v", err) 69 | return 70 | } 71 | 72 | // 在函数结束时根据err判断是否应该Rollback或者Commit 73 | defer func() { 74 | if err != nil { 75 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 76 | err = fmt.Errorf("%v,且无法回滚数据: %v", err, rollbackErr) 77 | } 78 | } else { 79 | if err = tx.Commit(); err != nil { 80 | err = fmt.Errorf("数据提交失败: %v", err) 81 | } 82 | } 83 | }() 84 | 85 | // 检查UUID是否被他人占用 86 | err = tx.QueryRow("SELECT QQ FROM users WHERE UUID=?", ID[:]).Scan(&owner) 87 | switch err { 88 | case sql.ErrNoRows: 89 | // 没有被占用 90 | owner = QQ 91 | err = nil 92 | case nil: 93 | // 被占用了 94 | if owner != QQ { 95 | return 96 | } 97 | default: 98 | // 查询出错 99 | err = fmt.Errorf("数据库查询是否有占用者失败: %v", err) 100 | return 101 | } 102 | 103 | // 查询是否有旧白名单 104 | var oldID uuid.UUID 105 | err = tx.QueryRow("SELECT UUID FROM users WHERE QQ=?", QQ).Scan(&oldID) 106 | 107 | switch err { 108 | case nil: // 有旧的UUID 109 | // 消除旧账号白名单 110 | if err = onOldID(oldID); err != nil { 111 | return 112 | } 113 | 114 | // 更新UUID 115 | if _, err = tx.Exec("UPDATE users SET UUID=? WHERE QQ=?", ID[:], QQ); err != nil { 116 | err = fmt.Errorf("数据库更新UUID失败: %v", err) 117 | return 118 | } 119 | case sql.ErrNoRows: // 没有旧UUID 120 | if _, err = tx.Exec("INSERT INTO users (QQ, UUID) VALUES (?,?)", QQ, ID[:]); err != nil { 121 | err = fmt.Errorf("数据库插入UUID失败: %v", err) 122 | return 123 | } 124 | err = nil 125 | 126 | default: //查询出错 127 | err = fmt.Errorf("查询旧UUID失败: %v", err) 128 | return 129 | } 130 | 131 | // 更新玩家UUID 132 | if err = onSuccess(); err != nil { 133 | return 134 | } 135 | return 136 | } 137 | 138 | // UnsetWhitelist 从数据库获取玩家绑定的ID,返回UUID并删除记录 139 | func UnsetWhitelist(QQ int64, onHas func(ID uuid.UUID) error) error { 140 | tx, err := db.Begin() 141 | if err != nil { 142 | return fmt.Errorf("数据库开始事务失败: %v", err) 143 | } 144 | 145 | // 在函数结束时根据err判断是否应该Rollback或者Commit 146 | defer func() { 147 | if err != nil { 148 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 149 | err = fmt.Errorf("数据库操作失败: %v,且无法回滚数据: %v", err, rollbackErr) 150 | } 151 | } else { 152 | if err = tx.Commit(); err != nil { 153 | err = fmt.Errorf("数据提交失败: %v", err) 154 | } 155 | } 156 | }() 157 | 158 | var oldID uuid.UUID 159 | err = tx.QueryRow("SELECT UUID FROM users WHERE QQ=?", QQ).Scan(&oldID) 160 | if err == sql.ErrNoRows { 161 | return nil // 没有数据 162 | } else if err != nil { 163 | return fmt.Errorf("数据库查询UUID失败: %v", err) 164 | } 165 | 166 | if err := onHas(oldID); err != nil { 167 | return err 168 | } 169 | 170 | if _, err := tx.Exec("DELETE FROM users WHERE QQ=?", QQ); err != nil { 171 | return fmt.Errorf("数据库删除UUID失败: %v", err) 172 | } 173 | 174 | return nil 175 | } 176 | 177 | // GetWhitelistByQQ 从数据库读取玩家绑定的ID,若没有绑定ID则返回uuid.Nil 178 | func GetWhitelistByQQ(QQ int64) (id uuid.UUID, err error) { 179 | err = db.QueryRow("SELECT UUID FROM users WHERE QQ=?", QQ).Scan(&id) 180 | if err == sql.ErrNoRows { 181 | return uuid.Nil, nil 182 | } 183 | if err != nil { 184 | return uuid.Nil, err 185 | } 186 | 187 | return 188 | } 189 | 190 | // GetWhitelistByUUID 从数据库读取绑定ID的玩家,若ID没有被绑定则则返回0 191 | func GetWhitelistByUUID(ID uuid.UUID) (qq int64, err error) { 192 | err = db.QueryRow("SELECT QQ FROM users WHERE UUID=?", ID[:]).Scan(&qq) 193 | if err == sql.ErrNoRows { 194 | return qq, nil 195 | } 196 | 197 | return 198 | } 199 | 200 | // GetLevel 获取某人的权限等级 201 | func GetLevel(QQ int64) (level int64, err error) { 202 | err = db.QueryRow("SELECT Level FROM auths WHERE QQ=?", QQ).Scan(&level) 203 | if err == sql.ErrNoRows { 204 | level = 0 205 | err = nil 206 | } else if err != nil { 207 | err = fmt.Errorf("查询Level失败: %v", err) 208 | } 209 | 210 | return 211 | } 212 | 213 | // SetLevel 设置某人的权限等级 214 | func SetLevel(QQ, level int64) (err error) { 215 | var tx *sql.Tx 216 | tx, err = db.Begin() 217 | if err != nil { 218 | err = fmt.Errorf("数据库开始事务失败: %v", err) 219 | return 220 | } 221 | 222 | // 查询是否有记录 223 | var rows *sql.Rows 224 | rows, err = tx.Query("SELECT Level FROM auths WHERE QQ=?", QQ) 225 | if err != nil { 226 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 227 | return fmt.Errorf("数据库操作失败: %v,且无法回滚数据: %v", err, rollbackErr) 228 | } 229 | return fmt.Errorf("数据库查询等级失败: %v", err) 230 | } 231 | 232 | // 根据数据存在性判断采用INSERT还是UPDATE 233 | if rows.Next() { 234 | if err = rows.Close(); err != nil { 235 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 236 | return fmt.Errorf("关闭rows失败: %v,且无法回滚数据: %v", err, rollbackErr) 237 | } 238 | return fmt.Errorf("关闭rows失败: %v", err) 239 | } 240 | _, err = tx.Exec("UPDATE auths SET Level=? WHERE QQ=?", level, QQ) 241 | } else { 242 | _, err = tx.Exec("INSERT INTO auths (QQ, Level) VALUES (?,?)", QQ, level) 243 | } 244 | if err != nil { 245 | if rollbackErr := tx.Rollback(); rollbackErr != nil { 246 | return fmt.Errorf("数据库操作失败: %v,且无法回滚数据: %v", err, rollbackErr) 247 | } 248 | return fmt.Errorf("数据库操作失败: %v", err) 249 | } 250 | 251 | err = tx.Commit() 252 | if err != nil { 253 | return fmt.Errorf("数据库提交数据失败: %v", err) 254 | } 255 | return 256 | } 257 | -------------------------------------------------------------------------------- /data/database_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // 两个不是很完善的测试 4 | 5 | import ( 6 | "github.com/google/uuid" 7 | "testing" 8 | ) 9 | 10 | func TestOpenDatabase_sqlite3(t *testing.T) { 11 | err := openDB("sqlite3", "data.db") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | //defer os.Remove("data.db") 16 | defer func() { 17 | err := closeDB() 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | }() 22 | 23 | owner, err := SetWhitelist(3261340757, uuid.MustParse("58f6356eb30c48118bfcd72a9ee99e74"), 24 | func(id uuid.UUID) error { 25 | t.Log("old id:", id) 26 | return nil 27 | }, 28 | func() error { 29 | t.Log("success") 30 | return nil 31 | }) 32 | t.Log("owner:", owner) 33 | 34 | //err = UnsetWhitelist(3261340757, func(id uuid.UUID) error { 35 | // t.Log(id) 36 | // return nil 37 | //}) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | err = closeDB() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | 48 | func TestGetLevel(t *testing.T) { 49 | err := openDB("sqlite3", "data.db") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | //defer os.Remove("data.db") 54 | defer func() { 55 | err := closeDB() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | }() 60 | 61 | if level, err := GetLevel(3261340757); err != nil { 62 | t.Fatal(err) 63 | } else { 64 | t.Log(level) 65 | } 66 | 67 | if err := SetLevel(3261340757, 11); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if level, err := GetLevel(3261340757); err != nil { 72 | t.Fatal(err) 73 | } else { 74 | t.Log(level) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /data/files.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // 创建一些需要但不存在的文件 11 | func initFiles() error { 12 | load := func(f *os.File, content string) error { 13 | defer f.Close() 14 | 15 | _, err := io.Copy(f, strings.NewReader(content)) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return nil 21 | } 22 | for fileName, fileContent := range defaultFiles { 23 | f, err := os.OpenFile(filepath.Join(AppDir, fileName), os.O_CREATE|os.O_EXCL|os.O_RDWR, 0666) 24 | if os.IsExist(err) { 25 | continue 26 | } else if err != nil { 27 | return err 28 | } 29 | 30 | err = load(f, fileContent) 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | var defaultFiles = map[string]string{ 39 | "conf.toml": `# SiS配置文件,在Wiki中有详细的配置说明:https://github.com/miaoscraft/SiS/wiki 40 | 41 | GroupID = 123456789 # 游戏群群号 42 | # AdminID = 123456789 # 管理群群号(可选) 43 | # Administrators = [ 12345678910, 23456789101, 34567891011] # 设置管理员们(他们可以设置任何人的Level) 44 | 45 | [DealWithGroupRequest]# 处理入群请求。详细见:https://github.com/miaoscraft/SiS/wiki/%E8%87%AA%E5%8A%A8%E5%A4%84%E7%90%86%E5%85%A5%E7%BE%A4%E8%AF%B7%E6%B1%82 46 | Enable = true # 启用 47 | CanReject = false # 是否允许机器人拒绝请求 48 | CheckURL = "" 49 | Token = "" 50 | 51 | [Database] 52 | Driver = "sqlite3" # 数据库类型(仅支持mysql和sqlite3) 53 | Source = "{{ join .AppDir \"data.db\"}}" # SQLite写法, 详细用法见https://github.com/mattn/go-sqlite3#dsn-examples 54 | # Source = "用户:密码@tcp(地址:端口)/库名" # MySQL写法, 详细用法见https://github.com/go-sql-driver/mysql#dsn-data-source-name 55 | 56 | [Ping] # Ping工具配置 57 | DefaultServer = "play.miaoscraft.cn" # 默认目标服务器[:端口],端口是可选的,默认为25565 58 | Timeout = "60s" # 最长ping时间,为0时禁用。例如:"300ms", "1.5h" 或 "2h45m"。可用的单位有 纳秒"ns", 微妙"us" (或 "µs"), 毫秒"ms", 秒"s", 分钟"m", 小时"h". 59 | 60 | [RCON] # RCON配置 61 | Address = "127.0.0.1:25575" #服务器地址:端口,必须写上端口 62 | Password = "rcon_password" #服务器RCON密码,server.properties文件里的rcon.password 63 | 64 | # 自定义命令配置 65 | [Cmd.tps] # 命令名 66 | Level = 0 # 执行该命令所需等级 67 | Command = "tps" # 执行时实际发送的命令 68 | # Silent = true # 禁用命令回显 69 | # AllowArgs = true # 允许执行时附加参数(试验性功能,请自行考察安全性后慎重开启) 70 | 71 | [Cmd."帮助"] # 中文命令需要引号,命令不可包含空格 72 | Level = 0 73 | Command = "help" 74 | `, 75 | } 76 | -------------------------------------------------------------------------------- /data/log.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | var Logger interface { 4 | Error(str string) 5 | Errorf(format string, args ...interface{}) 6 | 7 | Waring(str string) 8 | Waringf(format string, args ...interface{}) 9 | 10 | Info(str string) 11 | Infof(format string, args ...interface{}) 12 | 13 | Debug(str string) 14 | Debugf(format string, args ...interface{}) 15 | } 16 | -------------------------------------------------------------------------------- /data/rcon.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "errors" 5 | "github.com/Tnze/go-mc/chat" 6 | mcnet "github.com/Tnze/go-mc/net" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var rconDialer func() (mcnet.RCONClientConn, error) 12 | 13 | func openRCON(address, password string) error { 14 | rconDialer = func() (mcnet.RCONClientConn, error) { 15 | return mcnet.DialRCON(address, password) 16 | } 17 | return nil 18 | } 19 | 20 | // RCONCmd 执行RCON命令,每次都创建一个连接。 21 | // 当ret不为nil时,通过回调方式返回RCON执行结果,ret可能被调用多次。 22 | func RCONCmd(cmd string, ret func(string)) error { 23 | var r *mcnet.RCONConn 24 | 25 | if rconDialer == nil { 26 | return errors.New("RCON未设置") 27 | } 28 | conn, err := rconDialer() 29 | if err != nil { 30 | return err 31 | } 32 | r = conn.(*mcnet.RCONConn) 33 | 34 | err = r.Cmd(cmd) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | go func() { 40 | defer r.Close() 41 | if ret == nil { 42 | return 43 | } 44 | tip := time.AfterFunc(time.Second, func() { 45 | ret("正在努力发送指令噢,请稍后~") 46 | }) 47 | for { 48 | _ = r.SetDeadline(time.Now().Add(time.Second * 10)) 49 | resp, err := r.Resp() 50 | if err != nil { 51 | Logger.Debugf("停止转发RCON返回值: %v", err) 52 | return 53 | } 54 | tip.Stop() // 不再发送提示 55 | 56 | Logger.Debugf("RCON返回: %q", resp) 57 | // 过滤掉末尾换行符、空格和零字符,过滤§格式字符串 58 | resp = chat.Message{Text: strings.TrimRight(resp, " \000\n")}.ClearString() 59 | ret(resp) 60 | } 61 | }() 62 | 63 | return nil 64 | } 65 | 66 | // AddWhitelist 从游戏服务器添加白名单 67 | func AddWhitelist(name string) error { 68 | return RCONCmd("whitelist add "+name, nil) 69 | } 70 | 71 | // RemoveWhitelist 从游戏服务器删除白名单 72 | func RemoveWhitelist(name string) error { 73 | return RCONCmd("whitelist remove "+name, nil) 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/miaoscraft/SiS 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/Tnze/CoolQ-Golang-SDK v1.2.2-0.20200731122902-0c31e2b5c843 8 | github.com/Tnze/go-mc v1.16.5-pre.0.20210228071452-951bedbb131d 9 | github.com/go-mc/mcping v1.2.1 10 | github.com/go-sql-driver/mysql v1.4.1 11 | github.com/google/uuid v1.2.0 12 | github.com/mattn/go-sqlite3 v1.13.0 13 | google.golang.org/appengine v1.6.5 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Tnze/CoolQ-Golang-SDK v1.2.2-0.20200731122902-0c31e2b5c843 h1:eae1v+VBAwtPyLfj7i43QxfNuOYNZK7XJo6LFABum5s= 4 | github.com/Tnze/CoolQ-Golang-SDK v1.2.2-0.20200731122902-0c31e2b5c843/go.mod h1:e0wTTiIAcKLmr4JHTUHOo0dLzctmhNE1m3HhNHHGQLA= 5 | github.com/Tnze/go-mc v1.16.5-pre.0.20210225122206-f8b3501b6045/go.mod h1:qRHkOeNlteN0X8li2fN/JMsm0PZFGOSVmKvXtBkqQIQ= 6 | github.com/Tnze/go-mc v1.16.5-pre.0.20210228071452-951bedbb131d h1:At3e8yq2QwJ3KTxHM9oJ5j7Ugd6bcmbCPJc5sp28uvM= 7 | github.com/Tnze/go-mc v1.16.5-pre.0.20210228071452-951bedbb131d/go.mod h1:pDvu0CPg7n2G3LBLofeT9Mk9KDbyQ0H86PbCxkDm7nE= 8 | github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc= 9 | github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= 10 | github.com/go-mc/mcping v1.2.0 h1:S6PXVMyhCC+KWkFjoCNJS/ySuwZFvtAEU+Ms4Knzir0= 11 | github.com/go-mc/mcping v1.2.0/go.mod h1:Afl6dxAHG6XmHec1ASRlbWiwNcUEQozV/BI+85+JG+8= 12 | github.com/go-mc/mcping v1.2.1 h1:v1gUAboUKSdcpEHuydZOO9WY1kn2Oo/czU/FVGSmDAM= 13 | github.com/go-mc/mcping v1.2.1/go.mod h1:Afl6dxAHG6XmHec1ASRlbWiwNcUEQozV/BI+85+JG+8= 14 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 15 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 16 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 19 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 21 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 22 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 23 | github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= 24 | github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 32 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 33 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 34 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 35 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 36 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Tnze/CoolQ-Golang-SDK/cqp" 6 | ) 7 | 8 | type Logger struct { 9 | Name string 10 | } 11 | 12 | func NewLogger(name string) *Logger { return &Logger{name} } 13 | 14 | func (l *Logger) Error(str string) { cqp.AddLog(cqp.Error, l.Name, str) } 15 | func (l *Logger) Errorf(format string, args ...interface{}) { l.Error(fmt.Sprintf(format, args...)) } 16 | 17 | func (l *Logger) Waring(str string) { cqp.AddLog(cqp.Warning, l.Name, str) } 18 | func (l *Logger) Waringf(format string, args ...interface{}) { l.Waring(fmt.Sprintf(format, args...)) } 19 | 20 | func (l *Logger) Info(str string) { cqp.AddLog(cqp.Info, l.Name, str) } 21 | func (l *Logger) Infof(format string, args ...interface{}) { l.Info(fmt.Sprintf(format, args...)) } 22 | 23 | func (l *Logger) Debug(str string) { cqp.AddLog(cqp.Debug, l.Name, str) } 24 | func (l *Logger) Debugf(format string, args ...interface{}) { l.Debug(fmt.Sprintf(format, args...)) } 25 | -------------------------------------------------------------------------------- /ping/ping.go: -------------------------------------------------------------------------------- 1 | // Package ping 提供内置指令"ping"的实现 2 | package ping 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "text/template" 12 | "time" 13 | 14 | "github.com/Tnze/CoolQ-Golang-SDK/cqp/util" 15 | "github.com/Tnze/go-mc/bot" 16 | "github.com/Tnze/go-mc/chat" 17 | "github.com/google/uuid" 18 | "github.com/miaoscraft/SiS/data" 19 | ) 20 | 21 | func Ping(args []string, ret func(msg string)) bool { 22 | var ( 23 | resp []byte 24 | delay time.Duration 25 | err error 26 | ) 27 | 28 | addresses := getAddr(args) 29 | statuses := make([]status, len(addresses)) 30 | for i, addr := range addresses { 31 | if d := data.Config.Ping.Timeout.Duration; d > 0 { 32 | //启用Timeout 33 | resp, delay, err = bot.PingAndListTimeout(addr, d) 34 | } else { 35 | //禁用Timeout 36 | resp, delay, err = bot.PingAndList(addr) 37 | } 38 | if err != nil { 39 | statuses[i].Error = err 40 | continue 41 | } 42 | 43 | err = json.Unmarshal(resp, &statuses[i]) 44 | if err != nil { 45 | statuses[i].Error = err 46 | continue 47 | } 48 | 49 | // 延迟用手动填进去 50 | statuses[i].Delay = delay 51 | statuses[i].Address = addr 52 | } 53 | ret(render(statuses)) 54 | return true 55 | } 56 | 57 | // 从[]string获取服务器地址和端口 58 | // 支持的格式有: 59 | // [ "ping" "play.miaoscraft.cn" ] 60 | // [ "ping" "play.miaoscraft.cn:25565" ] 61 | // [ "ping" "play.miaoscraft.cn" "25565" ] 62 | func getAddr(args []string) (addrs []string) { 63 | args = args[1:] //去除第一个元素"ping" 64 | switch len(args) { 65 | default: // len >= 2 66 | return []string{net.JoinHostPort(args[0], args[1])} 67 | case 0: // 默认值 68 | args = append(args, data.Config.Ping.DefaultServer) 69 | fallthrough 70 | case 1: 71 | var addrErr *net.AddrError 72 | const missingPort = "missing port in address" 73 | addr := args[0] 74 | if _, _, err := net.SplitHostPort(addr); errors.As(err, &addrErr) && addrErr.Err == missingPort { 75 | _, addrsSRV, err := net.LookupSRV("minecraft", "tcp", addr) 76 | if err == nil && len(addrsSRV) > 0 { 77 | for _, addrSRV := range addrsSRV { 78 | addrs = append(addrs, net.JoinHostPort(addrSRV.Target, strconv.Itoa(int(addrSRV.Port)))) 79 | } 80 | return 81 | } 82 | return []string{net.JoinHostPort(addr, "25565")} 83 | } else { 84 | return []string{addr} 85 | } 86 | } 87 | } 88 | 89 | type status struct { 90 | Description chat.Message 91 | Players struct { 92 | Max int 93 | Online int 94 | Sample []struct { 95 | ID uuid.UUID 96 | Name string 97 | } 98 | } 99 | Version struct { 100 | Name string 101 | Protocol int 102 | } 103 | //favicon ignored 104 | 105 | Address string `json:"-"` 106 | Delay time.Duration `json:"-"` 107 | Error error `json:"-"` 108 | } 109 | 110 | var tmp = template.Must(template. 111 | New("PingRet"). 112 | Funcs(CQCodeUtil). 113 | Parse(`喵哈喽~{{ $list := .}} 114 | {{ with index . 0 }}服务器版本: [{{ .Version.Protocol }}] {{ .Version.Name | escape }} 115 | 每日消息: {{ .Description.ClearString | escape }} 116 | {{ range $index, $elem := $list }}延迟[{{ $index }}]: {{if .Error}}请求失败:{{ .Error }}{{ else }}{{ .Delay }}{{ end }} - {{ .Address }} 117 | {{ end }}在线人数: {{ .Players.Online -}}/{{- .Players.Max }} 118 | 玩家列表: 119 | {{ range .Players.Sample }}- [{{ .Name | escape }}] 120 | {{ end }}{{ end }}にゃ~`)) 121 | 122 | var CQCodeUtil = template.FuncMap{ 123 | "escape": util.Escape, 124 | } 125 | 126 | func render(statuses []status) string { 127 | var sb strings.Builder 128 | err := tmp.Execute(&sb, statuses) 129 | if err != nil { 130 | return fmt.Sprintf("似乎在渲染文字模版时出现了棘手的问题: %v", err) 131 | } 132 | cleanStr, _ := chat.TransCtrlSeq(sb.String(), false) 133 | return cleanStr 134 | } 135 | -------------------------------------------------------------------------------- /ping/ping_test.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPing(t *testing.T) { 8 | var args = []string{"ping", "play.miaoscraft.cn"} 9 | ret := func(resp string) { 10 | t.Log(resp) 11 | } 12 | Ping(args, ret) 13 | } 14 | -------------------------------------------------------------------------------- /syntax/input.go: -------------------------------------------------------------------------------- 1 | // Package syntax 实现SiS机器人支持的语法的解析和执行 2 | package syntax 3 | 4 | import ( 5 | "github.com/miaoscraft/SiS/customize" 6 | "github.com/miaoscraft/SiS/ping" 7 | "github.com/miaoscraft/SiS/whitelist" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | // 指令前缀,通常为cq码[CQ:at,qq=<机器人qq>] 14 | CmdPrefix string 15 | ) 16 | 17 | var expMyID = regexp.MustCompile(`^\s*(?i)MyID\s*[==]\s*([0-9A-Za-z_]{3,16})\s*$`) 18 | 19 | // GroupMsg 处理从游戏群接收到的消息,若为合法命令则进行相应的处理。并发安全 20 | // 返回值指示是否拦截本消息 21 | func GroupMsg(from int64, msg string, ret func(msg string)) bool { 22 | // 识别MyID指令 23 | if match := expMyID.FindStringSubmatch(msg); len(match) == 2 { 24 | whitelist.MyID(from, match[1], ret) 25 | return true 26 | } 27 | 28 | // 识别@指令 29 | if strings.HasPrefix(msg, CmdPrefix) { 30 | cmd := msg[len(CmdPrefix):] 31 | args := strings.Fields(cmd) 32 | if len(args) < 1 { // 如果没有首单词则不处理 33 | return false 34 | } 35 | 36 | switch args[0] { 37 | case "ping": // ping指令 38 | return ping.Ping(args, ret) 39 | 40 | case "auth": // auth指令 41 | return customize.Auth(args, from, ret) 42 | 43 | case "info": // 白名单查询指令 44 | return whitelist.Info(args, from, ret) 45 | 46 | default: // 自定义指令 47 | return customize.Exec(args, from, ret) 48 | } 49 | } 50 | 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /whitelist/query.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "github.com/miaoscraft/SiS/data" 7 | "regexp" 8 | "strconv" 9 | ) 10 | 11 | var expQQ = regexp.MustCompile(`^(?:([0-9]+)|\[CQ:at,qq=([0-9]+)])$`) // 匹配一个QQ或At 12 | var expName = regexp.MustCompile(`^([0-9A-Za-z_]{3,16})$`) // 匹配一个玩家名 13 | 14 | func Info(args []string, fromQQ int64, ret func(string)) bool { 15 | // 找出当前想查询的人的QQ 16 | switch len(args) { 17 | case 1: 18 | qqInfo(fromQQ, ret) 19 | return true 20 | case 2: 21 | if sms := expQQ.FindStringSubmatch(args[1]); len(sms) == 3 { // 匹配一个QQ或At 22 | for _, sm := range sms[1:3] { // [3]sms中后两项有一项为空,另一项为QQ 23 | qq, err := strconv.ParseInt(sm, 10, 64) 24 | if err != nil { 25 | continue 26 | } 27 | qqInfo(qq, ret) 28 | return true 29 | } 30 | } else if sm := expName.FindStringSubmatch(args[1]); len(sm) == 2 { // 匹配一个玩家名 31 | nameInfo(sm[1], ret) 32 | return true 33 | } 34 | return false 35 | default: 36 | return false 37 | } 38 | } 39 | 40 | func qqInfo(targetQQ int64, ret func(string)) { 41 | // 查询本人的绑定 42 | ID, err := data.GetWhitelistByQQ(targetQQ) 43 | if err != nil { 44 | Logger.Errorf("读取玩家绑定的ID出错: %v", err) 45 | ret("数据库查询失败惹(つД`)ノ") 46 | return 47 | } 48 | if ID == uuid.Nil { 49 | ret("这个还没有绑定白名单呢") 50 | return 51 | } 52 | 53 | // 根据UUID找到名字 54 | name, err := getName(ID) 55 | if err != nil { 56 | ret("游戏名查询失败惹(つД`)ノ") 57 | return 58 | } 59 | ret(name) 60 | } 61 | 62 | func nameInfo(targetName string, ret func(string)) { 63 | name, id, err := GetUUID(targetName) 64 | if err != nil { 65 | Logger.Errorf("查询UUID失败: %v", err) 66 | ret("查无此人") 67 | return 68 | } 69 | 70 | qq, err := data.GetWhitelistByUUID(id) 71 | if err != nil { 72 | Logger.Errorf("数据库查询QQ失败: %v", err) 73 | ret("数据库出问题了(つД`)ノ") 74 | return 75 | } 76 | 77 | if qq == 0 { 78 | ret(fmt.Sprintf("没人绑定%s哟~", name)) 79 | } else { 80 | ret(fmt.Sprintf("啊呐占用%s的是%d哟", name, qq)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /whitelist/whitelist.go: -------------------------------------------------------------------------------- 1 | package whitelist 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/google/uuid" 11 | "github.com/miaoscraft/SiS/data" 12 | ) 13 | 14 | func MyID(qq int64, name string, ret func(msg string)) { 15 | // 查询玩家名字和ID 16 | Name, id, err := GetUUID(name) 17 | if err != nil { 18 | Logger.Errorf("向Mojang查询玩家UUID失败: %v", err) 19 | ret(fmt.Sprintf("捡到个纸团\n( ^ ω ^) \n≡⊃§⊂≡ \n打开看一眼\n( ^ ω ^)\n⊃|" + name + "|⊂\n不认识这个id呢\n( ^ ω ^) \n≡⊃§⊂≡\n \n§\n ¶\n ∩( ^ ω ^)")) 20 | return 21 | } 22 | 23 | onOldID := func(oldID uuid.UUID) error { 24 | // 删除用户的旧白名单 25 | oldName, err := getName(oldID) 26 | if err != nil { 27 | return fmt.Errorf("向Mojang查询玩家Name失败: %v", err) 28 | } 29 | 30 | // 删除旧白名单 31 | err = data.RemoveWhitelist(oldName) 32 | if err != nil { 33 | return fmt.Errorf("删除白名单失败: %v", err) 34 | } 35 | 36 | return nil 37 | } 38 | 39 | onSuccess := func() error { 40 | // 添加白名单 41 | err = data.AddWhitelist(Name) 42 | if err != nil { 43 | return fmt.Errorf("添加白名单失败: %v", err) 44 | } 45 | Logger.Infof("添加白名单%q成功", Name) 46 | return nil 47 | } 48 | 49 | // 在数据库中记录 50 | owner, err := data.SetWhitelist(qq, id, onOldID, onSuccess) 51 | if err != nil { 52 | Logger.Errorf("设置白名单失败: %v", err) 53 | ret("白名单貌似没有成功加上欸,怎么办ʕ •ᴥ•ʔ") 54 | return 55 | } 56 | 57 | // 若owner是当前处理的用户则说明绑定成功,否则就是失败 58 | if owner != qq { 59 | if len(Name) < 3 { 60 | ret(fmt.Sprintf("白名单%s现在在[CQ:at,qq=%d]手上", Name, owner)) 61 | } else { 62 | ret(fmt.Sprintf("{\\__/}\n( • . •)\n/ >%s\n你要这个吗?\n\n{\\__/}\n( • - •)\n%s< \\\n这是[CQ:at,qq=%d]的", Name, Name[len(Name)-3:], owner)) 63 | } 64 | return 65 | } 66 | ret(fmt.Sprintf("{\\__/}\n( • . •)\n/ >%s\n呐,你的白名单", Name)) 67 | } 68 | 69 | func RemoveWhitelist(qq int64, ret func(msg string)) { 70 | onHas := func(ID uuid.UUID) error { 71 | name, err := getName(ID) 72 | if err != nil { 73 | return fmt.Errorf("查询QQ%d游戏名失败: %v", qq, err) 74 | } 75 | 76 | err = data.RemoveWhitelist(name) 77 | if err != nil { 78 | return fmt.Errorf("删除%s白名单失败: %v", name, err) 79 | } 80 | 81 | Logger.Infof("删除白名单%q成功", name) 82 | ret(name + ",你白名单(号)没了") 83 | return nil 84 | } 85 | // 删除数据库中的数据 86 | err := data.UnsetWhitelist(qq, onHas) 87 | if err != nil { 88 | Logger.Errorf("删除白名单失败: %v", err) 89 | ret("我的系统又出问题了(つД`)ノ") 90 | return 91 | } 92 | } 93 | 94 | // GetUUID 查询玩家的UUID 95 | func GetUUID(name string) (string, uuid.UUID, error) { 96 | var id uuid.UUID 97 | 98 | // 发送请求 99 | data, status, err := get("https://api.mojang.com/users/profiles/minecraft/" + name) 100 | if err != nil { 101 | return "", id, err 102 | } 103 | defer data.Close() 104 | 105 | // 检查返回码 106 | if status != 200 { 107 | err = fmt.Errorf("服务器状态码非200: %v", status) 108 | } 109 | 110 | // 解析json返回值 111 | err = json.NewDecoder(data).Decode(&struct { 112 | Name *string 113 | ID *uuid.UUID 114 | }{&name, &id}) 115 | if err != nil { 116 | return name, id, err 117 | } 118 | 119 | return name, id, err 120 | } 121 | 122 | // getName 查询玩家的Name 123 | func getName(UUID uuid.UUID) (string, error) { 124 | data, status, err := get("https://sessionserver.mojang.com/session/minecraft/profile/" + hex.EncodeToString(UUID[:])) 125 | if err != nil { 126 | return "", err 127 | } 128 | defer data.Close() 129 | 130 | // 检查返回码 131 | if status != 200 { 132 | err = fmt.Errorf("服务器状态码非200: %v", status) 133 | } 134 | 135 | var resp struct{ Name string } 136 | // 解析json返回值 137 | err = json.NewDecoder(data).Decode(&resp) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | return resp.Name, nil 143 | } 144 | 145 | // 发送GET请求 146 | func get(url string) (io.ReadCloser, int, error) { 147 | request, err := http.NewRequest("GET", url, nil) 148 | if err != nil { 149 | return nil, 0, err 150 | } 151 | 152 | // Golang默认的User-agent被屏蔽了 153 | request.Header.Set("User-agent", "SiS") 154 | 155 | // 发送Get请求 156 | resp, err := new(http.Client).Do(request) 157 | if err != nil { 158 | return nil, 0, err 159 | } 160 | 161 | return resp.Body, resp.StatusCode, nil 162 | } 163 | 164 | var Logger interface { 165 | Error(str string) 166 | Errorf(format string, args ...interface{}) 167 | 168 | Waring(str string) 169 | Waringf(format string, args ...interface{}) 170 | 171 | Info(str string) 172 | Infof(format string, args ...interface{}) 173 | 174 | Debug(str string) 175 | Debugf(format string, args ...interface{}) 176 | } 177 | --------------------------------------------------------------------------------