diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fcfb06..411b5ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,55 +10,129 @@ env: IMAGE_NAME: gotes3mp jobs: - build-linux: - name: Build Linux + build-linux-x64: + name: Build Linux - x64 runs-on: ubuntu-latest + permissions: + contents: write + discussions: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go 1.x - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: ^1.20 + go-version: ^1.22 stable: true id: linux-build + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Generate go files + run: | + cd src + go install google.golang.org/protobuf/cmd/protoc-gen-go + go get google.golang.org/grpc/cmd/protoc-gen-go-grpc + export PATH="$PATH:$(go env GOPATH)/bin" + go generate + cd .. + - name: Run Tests + run: go test ./src/... - name: Build-Linux - run: go build -ldflags="-X 'main.Build=${{ github.event.release.tag_name }}' -X 'main.GitCommit=$GITHUB_SHA'" -o build/goTES3MP-Linux src/*.go + run: | + cd src + CGO_ENABLED=0 go build -ldflags="-X 'main.Build=${{ github.event.release.tag_name }}' -X 'main.GitCommit=$GITHUB_SHA'" -o ../build/goTES3MP_Linux_amd64 . - name: Generate default config - run: cd build/ &&chmod +x goTES3MP-Linux && ./goTES3MP-Linux - + run: cd build/ && chmod +x goTES3MP_Linux_amd64 && ./goTES3MP_Linux_amd64 - name: GH Release - uses: softprops/action-gh-release@v0.1.5 + uses: softprops/action-gh-release@v0.1.15 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | - build/goTES3MP-Linux + build/goTES3MP_Linux_amd64 + build/config.yaml + build-linux-aarch64: + name: Build Linux - aarch64 + runs-on: self-hosted + permissions: + contents: write + discussions: write + steps: + - uses: actions/checkout@v4 + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: ^1.22 + stable: true + id: linux-build + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Generate go files + run: | + cd src + go install google.golang.org/protobuf/cmd/protoc-gen-go + go get google.golang.org/grpc/cmd/protoc-gen-go-grpc + export PATH="$PATH:$(go env GOPATH)/bin" + go generate + cd .. + - name: Run Tests + run: go test ./src/... + - name: Build-Linux + run: | + cd src + CGO_ENABLED=0 go build -ldflags="-X 'main.Build=${{ github.event.release.tag_name }}' -X 'main.GitCommit=${GITHUB_SHA}'" -o ../build/goTES3MP-Linux-aarch64 . + - name: Generate default config + run: cd build/ && chmod +x goTES3MP-Linux-aarch64 && ./goTES3MP-Linux-aarch64 + - name: GH Release + uses: softprops/action-gh-release@v0.1.15 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: | + build/goTES3MP-Linux-aarch64 build/config.yaml - build-windows: name: Build Windows runs-on: ubuntu-latest + permissions: + contents: write + discussions: write steps: - uses: actions/checkout@v3 - name: Set up Go 1.x uses: actions/setup-go@v3 with: - go-version: ^1.20 + go-version: ^1.22 stable: true id: windows-build + - name: Install Protoc + uses: arduino/setup-protoc@v3 + - name: Generate go files + run: | + cd src + go install google.golang.org/protobuf/cmd/protoc-gen-go + go get google.golang.org/grpc/cmd/protoc-gen-go-grpc + export PATH="$PATH:$(go env GOPATH)/bin" + go generate + cd .. + - name: Run Tests + run: go test ./src - name: Build-Windows - run: GOOS=windows GOARCH=amd64 go build -ldflags="-X 'main.Build=${{ github.event.release.tag_name }}' -X 'main.GitCommit=$GITHUB_SHA'" -o build/goTES3MP-Windows.exe src/*.go - + run: | + cd src + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-X 'main.Build=${{ github.event.release.tag_name }}' -X 'main.GitCommit=$GITHUB_SHA'" -o build/goTES3MP-Windows-amd64.exe . - name: GH Release uses: softprops/action-gh-release@v0.1.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - files: build/goTES3MP-Windows.exe + files: build/goTES3MP-Windows-amd64.exe add-scripts: name: Add Scripts runs-on: ubuntu-latest + permissions: + contents: write + discussions: write steps: - uses: actions/checkout@v3 @@ -77,8 +151,6 @@ jobs: build-dockerImage: runs-on: ubuntu-latest - # If running with act, Uncomment below - # container: phaze9/action-runner env: DOCKER_CONFIG: $HOME/.docker steps: @@ -86,10 +158,10 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to ghcr.io - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ${{ env.IMAGE_REGISTRY }} username: ${{ env.IMAGE_OWNER }} @@ -97,7 +169,7 @@ jobs: - name: Build Docker Image id: docker_build_x64 - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: push: true platforms: linux/amd64 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..71ad1b1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Test PR + +on: + pull_request: + branches: + - main + - dev + +jobs: + test: + name: Test PR + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ^1.22 + stable: true + + - name: Generate go files + run: go generate + + - name: Install dependencies + run: go mod download + + - name: Run Tests + run: go test -v ./src \ No newline at end of file diff --git a/.gitignore b/.gitignore index e3fc751..bddad10 100755 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ goTES3MP/logs/ build/ config.yaml goTES3MP/data.json +version.go .vscode/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a8421ac..440f998 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.20-alpine as BUILDER +FROM golang:1.22-alpine as BUILDER RUN mkdir /app WORKDIR /app @@ -9,9 +9,16 @@ ARG GITHUB_SHA COPY ["go.mod", "go.sum", "./"] COPY ["src/", "./src/"] +COPY ["tes3mp/scripts/custom/IrcBridge/IrcBridge.lua", "/app/tes3mp/scripts/custom/IrcBridge/IrcBridge.lua"] -RUN go mod download && \ - go build -ldflags="-X 'main.Build=$BUILD_VERSION' -X 'main.GitCommit=$GITHUB_SHA'" -o /app/build/goTES3MP-Linux src/*.go +RUN apk add --no-cache protoc + +RUN cd src && \ + go install google.golang.org/protobuf/cmd/protoc-gen-go && \ + go get google.golang.org/grpc/cmd/protoc-gen-go-grpc && \ + go generate && \ + go mod download && \ + go build -ldflags="-X 'main.Build=$BUILD_VERSION' -X 'main.GitCommit=$GITHUB_SHA'" -o /app/build/goTES3MP-Linux . FROM golang:1.20-alpine as RUNNER diff --git a/docs/methods.md b/docs/methods.md index 4495f9c..0763b8d 100644 --- a/docs/methods.md +++ b/docs/methods.md @@ -1,7 +1,5 @@ # GoTES3MP Methods -### Note: syncid is not used however it hasnt been fully removed from the source code and still has to be included in some places, however can be a blank string. - # "Sync" Method: [Link](../tes3mp/scripts/custom/goTES3MP\sync.lua) ```lua local messageJson = { @@ -31,7 +29,6 @@ local messageJson = method = "rawDiscord", source = "TES3MP", serverid = goTES3MP.GetServerID(), - syncid = GoTES3MPSyncID, data = { channel = discordChannel, server = discordServer, @@ -52,7 +49,6 @@ local messageJson = { method = "VPNCheck", source = "TES3MP", serverid = goTES3MP.GetServerID(), - syncid = GoTES3MPSyncID, data = { channel = discordChannel, server = discordServer, diff --git a/go.mod b/go.mod index 3ddf7a0..d465c13 100755 --- a/go.mod +++ b/go.mod @@ -1,38 +1,48 @@ module github.com/hotarublaze/gotes3mp -go 1.20 +go 1.22 require ( - github.com/bwmarrin/discordgo v0.27.1 - github.com/fatih/color v1.15.0 - github.com/fsnotify/fsnotify v1.6.0 + github.com/bwmarrin/discordgo v0.27.2-0.20230816134654-ff9176adccb6 + github.com/fatih/color v1.16.0 + github.com/fsnotify/fsnotify v1.7.0 + github.com/golang/protobuf v1.5.4 github.com/google/go-github v17.0.0+incompatible + github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.6.0 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/viper v1.15.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 github.com/tidwall/pretty v1.2.1 - golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a + google.golang.org/protobuf v1.33.0 ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.0.7 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 95f4ebb..58bb8ff 100644 --- a/go.sum +++ b/go.sum @@ -1,515 +1,121 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= -github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/bwmarrin/discordgo v0.27.2-0.20230816134654-ff9176adccb6 h1:4htHaHl1onoc5NgASwhAiD+5JTOS2Ppv5AET2tR3iGw= +github.com/bwmarrin/discordgo v0.27.2-0.20230816134654-ff9176adccb6/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= -github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4= github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= -golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/src/commands.go b/src/commands.go index f04f208..13ca181 100644 --- a/src/commands.go +++ b/src/commands.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/viper" ) -func commandStatus() { +func handleStatusCommand() { getStatus(false, true) } func commandShutdown() { @@ -20,6 +20,6 @@ func commandShutdown() { os.Exit(0) } -func commandIrcReconnect() { +func handleReloadIRCCommand() { ircReconnect() } diff --git a/src/config.go b/src/config.go index 154fad5..f95ee23 100755 --- a/src/config.go +++ b/src/config.go @@ -1,3 +1,6 @@ +//go:build !test +// +build !test + package main import ( @@ -9,7 +12,7 @@ import ( ) // LoadConfig loads json config file -func LoadConfig() (ConfigLoaded bool) { +func loadConfig() (ConfigLoaded bool) { var configPath = "./config.yaml" viper.SetConfigName("config") viper.SetConfigType("yaml") @@ -40,6 +43,7 @@ func LoadConfig() (ConfigLoaded bool) { viper.SetDefault("discord.enable", false) viper.SetDefault("discord.allowColorHexUsage", false) viper.SetDefault("discord.token", "") + viper.SetDefault("discord.guildID", "") viper.SetDefault("discord.alertsChannel", "") viper.SetDefault("discord.serverChat", "") viper.SetDefault("discord.staffRoles", []string{}) @@ -54,7 +58,7 @@ func LoadConfig() (ConfigLoaded bool) { log.Infoln("[Viper]", "Created default config") os.Exit(0) } else { - log.Errorf("[Viper]", "Fatal error reading config file: %v", err) + log.Errorf("[Viper] Fatal error reading config file: %v", err) panic(1) } } diff --git a/src/discord.go b/src/discord.go index b2ea12e..b7bb58e 100755 --- a/src/discord.go +++ b/src/discord.go @@ -1,11 +1,17 @@ package main import ( + "bytes" + "encoding/json" "math/big" + "os" "strconv" + "strings" "time" "github.com/bwmarrin/discordgo" + "github.com/google/uuid" + protocols "github.com/hotarublaze/gotes3mp/src/protocols" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) @@ -18,66 +24,245 @@ type discordRole struct { // DiscordSession : Global Discord Session var DiscordSession *discordgo.Session +var DiscordGuildID string +var defaultMemberPermissions int64 = discordgo.PermissionManageServer +var DMPermission bool = false -// InitDiscord Initialize discordgo +// InitDiscord initializes the discordgo session func InitDiscord() { - Discord, err := discordgo.New("Bot " + viper.GetString("discord.token")) + // Create a new discordgo session + discord, err := discordgo.New("Bot " + viper.GetString("discord.token")) if err != nil { - log.Errorln("error creating Discord session,", err) + log.Errorln("error creating Discord session:", err) return } - defer Discord.Close() + defer discord.Close() - DiscordSession = Discord - Discord.AddHandler(messageCreate) - Discord.AddHandler(ready) - Discord.AddHandler(UpdateDiscordStatus) + // Set the global Discord session variable + DiscordSession = discord - err = Discord.Open() - if err != nil { - log.Errorln("error opening connection,", err) + // Add event handlers + discord.AddHandler(messageCreate) + discord.AddHandler(ready) + discord.AddHandler(UpdateDiscordStatus) + discord.AddHandler(handleDiscordCommands) + + // Open the connection to Discord + if err := discord.Open(); err != nil { + log.Errorln("error opening connection:", err) } } func ready(s *discordgo.Session, event *discordgo.Ready) { + // Check if bot is in any discord servers first + if len(event.Guilds) == 0 { + log.Errorln("[Discord] Bot is not in any Discord servers") + os.Exit(1) + } + if len(event.Guilds) > 1 { + log.Warnln("[Discord] Bot is in more than 1 Discord server, this can have unintended results.") + } + // Set the playing status. err := s.UpdateGameStatus(0, "") if err != nil { log.Println(err) } else { + // Discord module is ready! log.Println(tes3mpLogMessage, "Discord Module is now running") + // Get the first guildID + // DiscordGuildID = event.Guilds[0].ID + DiscordGuildID = viper.GetString("discord.guildID") + // Load Commands + commandResponses, err = LoadDiscordCommandData() + if err != nil { + log.Errorln("Error loading Discord command data:", err) + } + } +} + +func handleDiscordCommands(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Check if the interaction is an application command + if i.Type == discordgo.InteractionApplicationCommand { + // Get the name of the command + commandName := strings.ToLower(i.ApplicationCommandData().Name) + + // Convert discord options to json we can handle easier + commandArgs, err := discordOptionsToJSON(i.ApplicationCommandData().Options) + if err != nil { + log.Errorln("Error converting Discord options to JSON:", err) + } + + commandArgs = string(commandArgs) + + // Find and execute the corresponding functionality based on the command name + _, ok := commandResponses.Commands[commandName] + if ok { + // Build a DiscordCommand packet for TES3MP + discordCommand := &protocols.BaseResponse{ + JobId: uuid.New().String(), + ServerId: viper.GetString("tes3mp.serverid"), + Method: "Command", + Source: "DiscordCommand", + Target: "TES3MP", + Data: map[string]string{ + "command": commandName, + "commandArgs": commandArgs, + "discordInteractiveToken": string(i.Interaction.Token), + }, + } + jsonresponse, err := json.Marshal(discordCommand) + checkError("DiscordChat", err) + sendresponse := bytes.NewBuffer(jsonresponse).String() + IRCSendMessage(viper.GetString("irc.systemchannel"), sendresponse) + + // Temp response for now + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Processing...", + }, + }) + } else { + // Respond with unknown command message + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Unknown command. Type `/help` to see available commands.", + }, + }) + } + } +} + +// discordOptionsToJSON converts Discord options to JSON format. +func discordOptionsToJSON(options []*discordgo.ApplicationCommandInteractionDataOption) (string, error) { + // Create a map to store the data + data := make(map[string]interface{}) + + // Iterate through each option and convert it to the appropriate type + for _, option := range options { + name := option.Name + var value interface{} + switch option.Type { + case discordgo.ApplicationCommandOptionString: + value = option.StringValue() + case discordgo.ApplicationCommandOptionInteger: + value = option.IntValue() + case discordgo.ApplicationCommandOptionBoolean: + value = option.BoolValue() + default: + value = option.StringValue() + } + + // Add the converted value to the data map + data[name] = value + } + + // Marshal the data map into JSON + jsonData, err := json.Marshal(data) + if err != nil { + return "", err } + + return string(jsonData), nil +} + +// createSlashCommand creates a new slash command for the given command name in the Discord guild. +// It maps each command argument to a discordgo.ApplicationCommandOption and sets the options for the command. +func createSlashCommand(command string) error { + // Retrieve the command details from the commandResponses map + tes3mpCommand := commandResponses.Commands[command] + + // Create a slice to hold the options + var options []*discordgo.ApplicationCommandOption + + // Map each command argument to a discordgo.ApplicationCommandOption + for _, arg := range tes3mpCommand.Args { + // Determine the type of the argument based on your requirements + optionType := discordgo.ApplicationCommandOptionString // For example, assuming all args are strings + + // Create the option + option := &discordgo.ApplicationCommandOption{ + Type: optionType, + Name: arg.Name, + Description: arg.Description, + Required: arg.Required, + } + + // Add the option to the slice + options = append(options, option) + } + + // Define the data for the slash command + commandData := &discordgo.ApplicationCommand{ + Name: tes3mpCommand.Command, + Description: tes3mpCommand.Description, + Type: discordgo.ChatApplicationCommand, + DefaultMemberPermissions: &defaultMemberPermissions, + DMPermission: &DMPermission, + Options: options, // Set the options for the command + } + + // Create the slash command in a specific guild + _, err := DiscordSession.ApplicationCommandCreate(DiscordSession.State.User.ID, DiscordGuildID, commandData) + if err != nil { + return err + } + + // Print confirmation message + log.Println("Created discord slash command:", command) + return nil } -func allowcolorhexusage(message *discordgo.Message) bool { - allowhexcolors := viper.GetBool("discord.allowcolorhexusage") - if allowhexcolors { +// allowColorHexUsage checks if the usage of color hex codes is allowed. +// It returns true if hex code usage is allowed or if the user is a staff member. +func allowColorHexUsage(msg *discordgo.Message) bool { + // Check if hex code usage is allowed + allowHexColors := viper.GetBool("discord.allowcolorhexusage") + if allowHexColors { return true } - isStaff := isStaffMember(message.Author.ID, message.GuildID) + // Check if the user is a staff member + isStaff := isStaffMember(msg.Author.ID, msg.GuildID) return isStaff } -// UpdateDiscordStatus: Update discord bot status -func UpdateDiscordStatus(s *discordgo.Session, event *discordgo.Ready) { - for { - var currentPlayers = strconv.Itoa(CurrentPlayers) - var maxPlayers = strconv.Itoa(MaxPlayers) - var status = "" +// UpdateDiscordStatus updates the status of a Discord bot +func UpdateDiscordStatus( + s *discordgo.Session, + event *discordgo.Ready, +) { + // Create a ticker that ticks every 5 seconds + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for range ticker.C { + // Convert currentPlayers and maxPlayers to strings + currentPlayers := strconv.Itoa(CurrentPlayers) + maxPlayers := strconv.Itoa(MaxPlayers) + // Initialize the status string + status := "" + + // Get the serverName from the configuration file serverName := viper.GetString("serverName") if len(serverName) > 0 { + // Add the serverName to the status if it is not empty serverName = serverName + ": " status = serverName + currentPlayers + "/" + maxPlayers status = status + " Players" } else { + // Otherwise, only include the currentPlayers and maxPlayers status = currentPlayers + "/" + maxPlayers status = status + " Players" } idleSince := 0 + + // Create the UpdateStatusData struct usd := discordgo.UpdateStatusData{ IdleSince: &idleSince, Activities: []*discordgo.Activity{{ @@ -87,17 +272,18 @@ func UpdateDiscordStatus(s *discordgo.Session, event *discordgo.Ready) { AFK: false, Status: "online", } + if s.DataReady { + // Update the status using the Discord session err := s.UpdateStatusComplex(usd) if err != nil { + // If there is an error, handle it accordingly if err == discordgo.ErrWSNotFound { log.Println("no websocket connection exists, Attempting reconnection in 5 seconds") - time.Sleep(5 * time.Second) _ = DiscordSession.Open() } log.Warnln("UpdateDiscordStatus failed to update status.", err) } - time.Sleep(5 * time.Second) } } } @@ -126,21 +312,44 @@ func getDiscordRoles(UserID string, GuildID string) []discordRole { return discordRoles } +// isStaffMember checks if a user is a staff member based on their roles in a Discord guild. func isStaffMember(UserID string, GuildID string) bool { + // Get the list of staff roles from the configuration staffRoles := viper.GetStringSlice("discord.staffroles") + + // Get the roles of the user in the specified guild discordRoles := getDiscordRoles(UserID, GuildID) + + // Get the guild information guild, err := DiscordSession.Guild(GuildID) if err != nil { + // Log a warning if there was an error fetching the guild information log.Warnln("isStaffMember", "Failed to figure out if a user is a staff member.") } + + // Check if the user is the owner of the guild if UserID == guild.OwnerID { return true } + + // Check if any of the user's roles match the staff roles for _, i := range discordRoles { _, found := FindinArray(staffRoles, i.Name) if found { return true } } + + // Return false if the user is not a staff member return false } + +func SendDiscordInteractiveMessage(interactionToken, newContent string) { + // Construct the interaction response data for editing + responseEdit := &discordgo.WebhookEdit{ + Content: &newContent, // New content for the interaction response + } + + // Update the interaction response + DiscordSession.WebhookMessageEdit(DiscordSession.State.User.ID, interactionToken, "@original", responseEdit) +} diff --git a/src/discordCommandData.go b/src/discordCommandData.go new file mode 100644 index 0000000..f17ce9c --- /dev/null +++ b/src/discordCommandData.go @@ -0,0 +1,112 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + protocols "github.com/hotarublaze/gotes3mp/src/protocols" + log "github.com/sirupsen/logrus" +) + +type CommandResponses struct { + Commands map[string]protocols.CommandData `json:"commands"` +} + +var commandResponses CommandResponses + +// AddDiscordCommand is responsible for adding a new Discord slash command to the system. It saves the command and registers it with the Discord platform. +func AddDiscordCommand(data *CommandResponses, command string, description string, args ...string) { + // If the data has no Commands map, return without doing anything + if data.Commands == nil { + return + } + + // Create CommandArg objects for each argument + commandArgs := make([]*protocols.CommandArg, len(args)) + for i, arg := range args { + commandArgs[i] = &protocols.CommandArg{ + Required: true, + Name: arg, + Description: description, + } + } + + // Add the new CommandData to the Commands map in the data + data.Commands[command] = protocols.CommandData{ + Command: command, + Description: description, + Args: commandArgs, + } + + // Create a new slash command + createSlashCommand(command) + + // Save the updated Discord command data to a file + if err := SaveDiscordCommandData(*data, "discordCommands.json"); err != nil { + // Print an error message if there was an error saving the data + log.Errorln("Error saving Discord command data:", err) + } +} + +// RemoveDiscordCommand removes the specified command from the CommandResponses map. +// +// data *CommandResponses - the map of command responses +// command string - the command to be removed +func RemoveDiscordCommand(data *CommandResponses, command string) { + delete(data.Commands, command) +} + +// SaveDiscordCommandData saves the given CommandResponses data to a file specified by the filename parameter. +// It takes a CommandResponses data and a filename string as parameters and returns an error. +func SaveDiscordCommandData(data CommandResponses, filename string) error { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("error marshalling JSON: %v", err) + } + if err := os.WriteFile(filename, jsonData, 0644); err != nil { + return fmt.Errorf("error writing JSON to file: %v", err) + } + return nil +} + +// LoadDiscordCommandData loads the Discord command data from a JSON file. +// +// It returns a CommandResponses and an error. +func LoadDiscordCommandData() (CommandResponses, error) { + fileData, err := os.ReadFile("discordCommands.json") + if err != nil { + return CommandResponses{}, fmt.Errorf("error reading file: %v", err) + } + var loadedData CommandResponses + err = json.Unmarshal(fileData, &loadedData) + if err != nil { + return CommandResponses{}, fmt.Errorf("error unmarshalling JSON: %v", err) + } + + // Count the number of registered commands + numCommands := len(loadedData.Commands) + log.Println("[Discord]", "Loaded", numCommands, "Discord commands!") + + return loadedData, nil +} + +func purgeDiscordCommands() error { + commands, err := DiscordSession.ApplicationCommands(DiscordSession.State.User.ID, DiscordGuildID) + if err != nil { + return err + } + + for _, command := range commands { + err := DiscordSession.ApplicationCommandDelete(DiscordSession.State.User.ID, DiscordGuildID, command.ID) + if err != nil { + return err + } + RemoveDiscordCommand(&commandResponses, command.Name) + log.Println("[Discord]", "Purged Discord command:", command.Name) + } + + log.Println("[Discord]", "Purged all Discord commands!") + SaveDiscordCommandData(commandResponses, "discordCommands.json") + return nil +} diff --git a/src/discordCommands.go b/src/discordCommands.go deleted file mode 100644 index 59920a9..0000000 --- a/src/discordCommands.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "log" - "regexp" - "strings" - - "github.com/bwmarrin/discordgo" - "github.com/spf13/viper" -) - -func discordCommandHandler(s *discordgo.Session, m *discordgo.MessageCreate) { - Data := make(map[string]string) - var commandStruct baseresponse - - re := regexp.MustCompile(`[^\s"']+|([^\s"']*"([^"]*)"[^\s"']*)+|'([^']*)`) - stringArr := re.FindAllString(m.Content[1:], -1) - - if viper.GetBool("debug") { - log.Println("[Debug] discordCommandHandler:stringArr:'", stringArr) - } - - commandStruct.ServerID = viper.GetViper().GetString("tes3mp.serverid") - commandStruct.Method = "Command" - commandStruct.Source = "DiscordCommand" - Data["Command"] = stringArr[0] - - if len(stringArr) > 1 { - Data["TargetPlayer"] = stringArr[1] - if len(stringArr) > 2 { - Data["CommandArgs"] = strings.Join(stringArr[2:], " ") - } - } - - Data["replyChannel"] = m.ChannelID - commandStruct.Data = Data - - if viper.GetBool("debug") { - log.Println("[Debug] discordCommandHandler:commandStruct'", Data) - } - - log.Println("Staff Member '"+m.Author.Username+"' has executed the following command:", m.Content[1:]) - - jsonresponse, err := json.Marshal(commandStruct) - checkError("discordCommandHandler", err) - sendresponse := bytes.NewBuffer(jsonresponse).String() - IRCSendMessage(viper.GetString("irc.systemchannel"), sendresponse) -} diff --git a/src/generate_version.go b/src/generate_version.go new file mode 100644 index 0000000..7b920b1 --- /dev/null +++ b/src/generate_version.go @@ -0,0 +1,43 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "fmt" + "os" + "regexp" +) + +func main() { + // Read Lua file content + luaContent, err := os.ReadFile("../tes3mp/scripts/custom/IrcBridge/IrcBridge.lua") + if err != nil { + fmt.Println("Error reading Lua file:", err) + return + } + + // Extract version using regular expression + re := regexp.MustCompile(`IrcBridge\.version\s*=\s*"([^"]+)"`) + matches := re.FindSubmatch(luaContent) + if len(matches) < 2 { + fmt.Println("Version not found in Lua file") + return + } + + version := string(matches[1]) + + // Write the version to version.go + fileContent := fmt.Sprintf(`package main + +var ircBridgeVersion = "%s" +`, version) + + // Write the generated Go code to version.go + if err := os.WriteFile("version.go", []byte(fileContent), 0644); err != nil { + fmt.Println("Error writing version:", err) + return + } + + fmt.Println("Pinning goTES3MP's IRCBridge version to", version) +} diff --git a/src/github_versionchecking.go b/src/github_versionchecking.go new file mode 100644 index 0000000..4cbd070 --- /dev/null +++ b/src/github_versionchecking.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + + color "github.com/fatih/color" + "github.com/google/go-github/github" + "github.com/hashicorp/go-version" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +func getLatestGithubRelease() (isUpdate bool, latestVersion string) { + client := github.NewClient(nil) + releases, _, _ := client.Repositories.GetLatestRelease(context.Background(), "HotaruBlaze", "goTES3MP") + latestRelease := releases.GetTagName() + + // Get the build number thats set on build + currentBuild, err := version.NewVersion(Build) + if err != nil { + log.Println(err) + } + + // Get latest github release + latestBuild, err := version.NewVersion(latestRelease) + if err != nil { + log.Println(err) + } + + if currentBuild.LessThan(latestBuild) { + return true, string("v" + latestBuild.String()) + } else { + return false, "nil" + } + +} + +// GitHub file content response structure +type GitHubContentResponse struct { + Content string `json:"content"` + Encoding string `json:"encoding"` +} + +// Fetch file content from GitHub repository +func fetchIrcBridgeVersionFromGithub() (string, bool) { + var err error + // Construct GitHub API URL + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", "hotarublaze", "gotes3mp", "tes3mp/scripts/custom/IrcBridge/IrcBridge.lua") + + // Send HTTP GET request to fetch file content + resp, err := http.Get(url) + if err != nil { + return "", false + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", false + } + + // Parse GitHub response + var contentResponse GitHubContentResponse + if err := json.Unmarshal(body, &contentResponse); err != nil { + return "", false + } + + // Decode content from base64 + content, err := decodeBase64String(contentResponse.Content) + if err != nil { + return "", false + } + + // Parse version number + githubIrcBridgeVersion, err := parseVersionNumber(content) + if err != nil { + fmt.Println(err) + return "", false + } + message, safeUpdate := compareVersions(githubIrcBridgeVersion) + + return message, safeUpdate +} + +// Decode base64 encoded string +func decodeBase64String(encodedString string) (string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(encodedString) + if err != nil { + return "", err + } + return string(decodedBytes), nil +} + +// Parse version number from file content +func parseVersionNumber(content string) (string, error) { + // Regular expression to match version number + re := regexp.MustCompile(`IrcBridge\.version\s*=\s*"([^"]+)"`) + matches := re.FindStringSubmatch(content) + if len(matches) < 2 { + return "", fmt.Errorf("version number not found") + } + return matches[1], nil +} + +// This compares the current version to the version in the github repo +// this is designed to alert the user if they can safely update the bot or not. +func compareVersions(githubVersion string) (string, bool) { + versionCheck := strings.Compare(githubVersion, ircBridgeVersion) + + switch { + // Github version is older + case versionCheck < 0: + // This shouldnt happen unless it's a custom build. + return "Github version is newer than this code was built for, this usually only happens for custom builds", false + case versionCheck > 0: + return "Update is available however requires updating tes3mp's lua files", false + default: + return "No Lua Updates required", true + } +} + +func getStatus(firstLaunch bool, showModules bool) { + + if firstLaunch { + color.HiBlack(strings.Repeat("=", 32)) + } + color.HiBlack("goTES3MP: " + Build) + color.HiBlack("Commit: " + GitCommit) + color.HiBlack("Github: " + "https://github.com/hotarublaze/goTES3MP" + "\n") + color.HiBlack("Interactive Console: " + strconv.FormatBool(viper.GetBool("enableInteractiveConsole"))) + isUpdate, UpdateVersion := getLatestGithubRelease() + + if isUpdate { + // Check if the Lua updates are safe + _, isSafeLuaUpdate := fetchIrcBridgeVersionFromGithub() + if isSafeLuaUpdate { + // If Lua updates are safe + color.HiGreen("A new version of goTES3MP is available: " + Build + " -> " + UpdateVersion + ".\nYour IRC bridge version should be compatible.") + } else { + // If Lua updates are not safe + color.HiYellow("A new version of goTES3MP is available: " + Build + " -> " + UpdateVersion + ".\nHowever, the IRCBridge version may not be compatible.") + } + } + + if firstLaunch { + color.HiBlack(strings.Repeat("=", 32)) + } +} + +func checkforUpdates() { + isUpdate, UpdateVersion := getLatestGithubRelease() + + if isUpdate { + // Check if the Lua updates are safe + _, isSafeLuaUpdate := fetchIrcBridgeVersionFromGithub() + if isSafeLuaUpdate { + // If Lua updates are safe + color.HiGreen("A new version of goTES3MP is available: " + Build + " -> " + UpdateVersion + ".\nYour IRC bridge version should be compatible.") + } else { + // If Lua updates are not safe + color.HiYellow("A new version of goTES3MP is available: " + Build + " -> " + UpdateVersion + ".\nHowever, the IRC bridge version may not be compatible.") + } + } +} diff --git a/src/irc.go b/src/irc.go index de458b3..9f61df8 100755 --- a/src/irc.go +++ b/src/irc.go @@ -1,10 +1,13 @@ package main import ( - "encoding/json" - "io/ioutil" + "io" + "os" + "strings" "time" + "github.com/golang/protobuf/jsonpb" + protocols "github.com/hotarublaze/gotes3mp/src/protocols" log "github.com/sirupsen/logrus" "github.com/spf13/viper" irc "github.com/thoj/go-ircevent" @@ -19,23 +22,26 @@ var password string var irccon *irc.Connection var connectedToIRC bool -// InitIRC : Initialize IRC +// InitIRC initializes the IRC connection using the configuration from viper func InitIRC() { + // Retrieve IRC configuration from viper ircServer = viper.GetString("irc.server") ircPort = viper.GetString("irc.port") ircNick = viper.GetString("irc.nick") - // IRC "System Channe;" + // Define IRC channels and password systemchannel = viper.GetString("irc.systemchannel") - // Add a extra channel for Talking via IRC chatchannel = viper.GetString("irc.chatchannel") - password = viper.GetString("irc.pass") + + // Initialize IRC connection irccon = irc.IRC(ircNick, ircNick) irccon.Debug = false - irccon.Log.SetOutput(ioutil.Discard) + irccon.Log.SetOutput(io.Discard) irccon.UseTLS = false irccon.Password = password + + // Handle IRC connection events irccon.AddCallback("001", func(e *irc.Event) { log.Infoln("[IRC] Connected to", ircServer+":"+ircPort, "as", ircNick) irccon.Join(systemchannel) @@ -48,25 +54,63 @@ func InitIRC() { irccon.AddCallback("PRIVMSG", func(event *irc.Event) { go func(event *irc.Event) { if event.Arguments[0] == systemchannel { - var baseMsg baseresponse - err := json.Unmarshal([]byte(event.Message()), &baseMsg) + // Parse a fuzzy metadata protocol to get the method + var metadata protocols.Metadata + metadata_unmarshaler := jsonpb.Unmarshaler{ + AllowUnknownFields: true, + } + unmarshaler := jsonpb.Unmarshaler{} + + err := metadata_unmarshaler.Unmarshal(strings.NewReader(event.Message()), &metadata) if err != nil { - checkError("[IRC:AddCallback]: PRIVMSG", err) - return + checkError("[IRC:AddCallback]: PRIVMSG 0", err) } - processRelayMessage(baseMsg) - } + switch metadata.Method { + case "RegisterDiscordSlashCommand": + { + // Now parse this as a Discord Slash Command + var dataPacket protocols.DiscordSlashCommand + err := unmarshaler.Unmarshal(strings.NewReader(event.Message()), &dataPacket) + if err != nil { + checkError("[IRC:AddCallback]: PRIVMSG 1", err) + } else { + _, err := handleIncomingComamnd(dataPacket) + if err != nil { + checkError("[IRC:AddCallback]["+metadata.Method+"] PRIVMSG 2", err) + } + } + } + default: + { + // Now parse this as a normal system message + var dataPacket protocols.BaseResponse + err := unmarshaler.Unmarshal(strings.NewReader(event.Message()), &dataPacket) + if err != nil { + checkError("[IRC:AddCallback]: PRIVMSG 1", err) + } else { + _, err := handleIncomingMessage(&dataPacket) + if err != nil { + checkError("[IRC:AddCallback]: PRIVMSG 2", err) + } + } + } + } + } }(event) }) + + // Connect to IRC server err := irccon.Connect(ircServer + ":" + ircPort) if err != nil { log.Errorln("Failed to connect to IRC") log.Errorf("Err %s", err) + // This will hang if we forget this, but it's better than ignoring sig interrupt + os.Exit(1) } + // Start IRC loop log.Println(tes3mpLogMessage, "IRC Module is now running") - connectedToIRC = true irccon.Loop() } @@ -85,30 +129,26 @@ func ircReconnect() { } log.Println(tes3mpLogMessage, "[IRC] Module Loading...") - for { + for count < 6 { time.Sleep(10 * time.Second) - if count < 6 { - currentstatus := irccon.Connected() - if !currentstatus { - connectedToIRC = false - count++ - err := irccon.Reconnect() - if err != nil { - log.Fatal(err) - } else { - connectedToIRC = true - } + currentstatus := irccon.Connected() + if !currentstatus { + connectedToIRC = false + count++ + err := irccon.Reconnect() + if err != nil { + log.Fatal(err) + } else { + connectedToIRC = true } + } - if connectedToIRC && irccon.Connected() { - log.Println(tes3mpLogMessage, "[IRC] Module online...") - return - } + if connectedToIRC && irccon.Connected() { + log.Println(tes3mpLogMessage, "[IRC] Module online...") + return } - log.Error("Unable to Reconnect to IRC within 60 seconds.") - return } - + log.Error("Unable to Reconnect to IRC within 60 seconds.") } // IRCSendMessage : Send message to IRC Channel diff --git a/src/main.go b/src/main.go index acbeece..6292787 100755 --- a/src/main.go +++ b/src/main.go @@ -1,3 +1,5 @@ +//go:generate protoc --go_out=. --go_opt=paths=source_relative ./protocols/messages.proto +//go:generate go run generate_version.go package main import ( @@ -11,7 +13,6 @@ import ( "time" - color "github.com/fatih/color" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) @@ -44,16 +45,16 @@ var MultiWrite io.Writer var reader *bufio.Reader func init() { - if len(GitCommit) == 0 { + if GitCommit == "" { GitCommit = "None" } - initLogger() - LoadConfig() - pdloadData() + initializeLogger() + loadConfig() + loadData() } func main() { - enableDebug := viper.GetBool("debug") - if enableDebug { + debugEnabled := viper.GetBool("debug") + if debugEnabled { log.Warnln("Debug mode is enabled") log.SetLevel(log.DebugLevel) } @@ -62,9 +63,10 @@ func main() { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - log.Infoln("Preforming clean shutdown...") + log.Infoln("Performing clean shutdown...") commandShutdown() }() + if viper.GetBool("webserver.enable") { go InitWebserver() } @@ -80,11 +82,12 @@ func main() { if viper.GetBool("enableInteractiveConsole") { reader = bufio.NewReader(os.Stdin) } + getStatus(true, false) for { - time.Sleep(2 * 100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) + if viper.GetBool("enableInteractiveConsole") { - // TODO: This should be tweaked so ">" is always at the bottom. fmt.Print("> ") command, err := reader.ReadString('\n') if err != nil { @@ -94,24 +97,29 @@ func main() { args := strings.Split(command, " ") switch strings.ToLower(args[0]) { + case "updates": + checkforUpdates() case "status": - commandStatus() + handleStatusCommand() case "reloadirc": - commandIrcReconnect() + handleReloadIRCCommand() case "reloaddiscord": - color.HiBlack("Attempting to reload Discord") + log.Debugln("Attempting to reload Discord") InitDiscord() + case "purgecommands": + log.Println("Purging Discord commands...") + purgeDiscordCommands() case "exit", "quit", "stop": - color.HiBlack("Shutting down...") + log.Debugln("Shutting down...") commandShutdown() default: - color.Red("[goTES3MP]: " + "Command" + ` "` + command + `" ` + "was not recognised.") + log.Warnf("Command " + command + " was not recognized.") } } } } -func initLogger() { +func initializeLogger() { dt := time.Now() ProgramDirectory := "./goTES3MP/logs/" logfileName := ProgramDirectory + "goTES3MP-" + dt.Format("02-01-2006-15_04_05") + ".log" diff --git a/src/persistentData.go b/src/persistentData.go index e1a374c..9ada681 100644 --- a/src/persistentData.go +++ b/src/persistentData.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "io/ioutil" + "io" "os" log "github.com/sirupsen/logrus" @@ -16,31 +16,31 @@ type persistantServerDataStruct struct { var persistantData persistantServerDataStruct var persistantFilePath = "./goTES3MP/data.json" -func pdloadData() { +func loadData() { if _, err := os.Stat(persistantFilePath); os.IsNotExist(err) { - pdsaveData() + saveData() } persistantDataFile, err := os.Open(persistantFilePath) if err != nil { - log.Warnln("[pdloadData]: Command removerole errored with", err) + log.Warnln("[loadData]: Command removerole errored with", err) os.Exit(1) } defer persistantDataFile.Close() - byteValue, _ := ioutil.ReadAll(persistantDataFile) + byteValue, _ := io.ReadAll(persistantDataFile) err = json.Unmarshal(byteValue, &persistantData) if err != nil { - log.Errorln("[pdloadData]", "Failed to Unmarshal Persistant Data, %v", err) + log.Errorln("[loadData]", "Failed to Unmarshal Persistant Data, %v", err) } } -func pdsaveData() { +func saveData() { pd, err := json.Marshal(&persistantData) if err != nil { log.Warnln("[pdsaveData]: Command removerole errored with", err) return } - err = ioutil.WriteFile(persistantFilePath, pd, os.ModePerm) + err = os.WriteFile(persistantFilePath, pd, os.ModePerm) if err != nil { log.Errorln("[pdsaveData]", "Failed to save file: , %v", err) } diff --git a/src/protocols/messages.proto b/src/protocols/messages.proto new file mode 100644 index 0000000..d6e60b1 --- /dev/null +++ b/src/protocols/messages.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package main; +option go_package = "github.com/hotarublaze/gotes3mp"; + +message RawDiscordStruct { + string channel = 1; + string server = 2; + string message = 3; +} + +message ServerSync { + string job_id = 1; + string server_id = 2; + string status = 3; + string method = 4; +} + +// Messages related to responses and commands +message BaseResponse { + string job_id = 1; + string server_id = 2; + string method = 3; + string source = 4; + string target = 5; + map data = 6; +} + +message CommandArg { + bool required = 1; + string name = 2; + string description = 3; +} + +message CommandData { + string command = 1; + string description = 2; + repeated CommandArg args = 3; +} + +message CommandResponse { + string job_id = 1; + string server_id = 2; + string method = 3; + string source = 4; + CommandData data = 5; +} + +message DiscordSlashCommand { + string server_id = 1; + string method = 2; + string source = 3; + string job_id = 4; + CommandData data = 5; +} + +message Metadata { + string method = 1; + string source = 2; + string server_id = 3; + string job_id = 4; + map additional_data = 5; +} \ No newline at end of file diff --git a/src/relayDiscord.go b/src/relayDiscord.go index 825a268..6e1e125 100644 --- a/src/relayDiscord.go +++ b/src/relayDiscord.go @@ -2,85 +2,73 @@ package main import ( "github.com/bwmarrin/discordgo" + "github.com/google/uuid" + protocols "github.com/hotarublaze/gotes3mp/src/protocols" + log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) -type rawDiscordStruct struct { - Channel string `json:"channel"` - Server string `json:"server"` - Message string `json:"Message"` -} +func sendRawDiscordMessage(rawDiscordStruct *protocols.RawDiscordStruct) bool { + // check if discord is actually connected first. + if DiscordSession.DataReady { + _, err := DiscordSession.ChannelMessageSend(rawDiscordStruct.Channel, rawDiscordStruct.Message) + checkError("rawDiscordMessage", err) + return true + } else { + log.Errorln("Discord not connected, Message was not sent.") + return false + } -func rawDiscordMessage(rawDiscordStruct rawDiscordStruct) bool { - _, err := DiscordSession.ChannelMessageSend(rawDiscordStruct.Channel, rawDiscordStruct.Message) - checkError("rawDiscordMessage", err) - return true } +// messageCreate is a function that handles incoming messages in a Discord server func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - if m.Author.ID == s.State.User.ID { + if m.Author.ID == s.State.User.ID || len(m.Content) == 0 { return } - if len(m.Content) > 0 { - if m.Content[:1] == viper.GetString("discord.commandprefix") && isStaffMember(m.Author.ID, m.GuildID) { - discordCommandHandler(s, m) - return - } - } + if m.ChannelID != viper.GetString("discord.serverchat") { return } - var discordresponse baseresponse - discordresponse.ServerID = viper.GetViper().GetString("tes3mp.serverid") - discordresponse.Method = "DiscordChat" - discordresponse.Source = "Discord" - discordresponse.Target = "TES3MP" - var user, message string - - if !allowcolorhexusage(m.Message) { - message = removeRGBHex(m.Content) - } else { - message = m.Content + discordresponse := protocols.BaseResponse{ + JobId: uuid.New().String(), + ServerId: viper.GetViper().GetString("tes3mp.serverid"), + Method: "DiscordChat", + Source: "Discord", + Target: "TES3MP", } - // Convert all <@ >, <# >, and similarly formatted items in Discord messages to something we can actually read. + message := m.Content + if !allowColorHexUsage(m.Message) { + message = removeRGBHex(message) + } message = convertDiscordFormattedItems(message, m.GuildID) - - // Convert <:example:868167672758693909> to :example: message = filterDiscordEmotes(message) guildMember, err := s.GuildMember(m.GuildID, m.Message.Author.ID) checkError("[RelayDiscord]: guildMember ", err) - hasNickname := guildMember.Nick - if len(hasNickname) > 0 { - user = hasNickname - } else { - user = m.Author.Username + user := guildMember.Nick + if user == "" { + user = guildMember.User.GlobalName + } + if user == "" { + user = guildMember.User.Username } - var discordData map[string]string roleName, roleColor := getUsersRole(m.Message) - if len(roleName) > 0 && len(roleColor) > 0 { - discordData = map[string]string{ - "User": user, - "Message": message, - "RoleName": roleName, - "RoleColor": roleColor, // last comma is a must - } - } else { - discordData = map[string]string{ - "User": user, - "Message": message, - "RoleName": "", - "RoleColor": "", // last comma is a must - } + discordData := map[string]string{ + "User": user, + "Message": message, + "RoleName": roleName, + "RoleColor": roleColor, } discordresponse.Data = discordData - processRelayMessage(discordresponse) + + processRelayMessage(&discordresponse) } func getUsersRole(m *discordgo.Message) (string, string) { diff --git a/src/relayManager.go b/src/relayManager.go index 2c1b8dc..37c04bd 100644 --- a/src/relayManager.go +++ b/src/relayManager.go @@ -4,103 +4,258 @@ import ( "bytes" "encoding/json" "errors" + "fmt" + "strings" + "github.com/golang/protobuf/jsonpb" + protocols "github.com/hotarublaze/gotes3mp/src/protocols" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) -type baseresponse struct { - ServerID string `json:"serverid"` - Method string `json:"method"` - Source string `json:"source"` - Target string `json:"target"` - Data map[string]string `json:"data"` +// handleIncomingMessage handles the incoming message and processes it accordingly. +// +// It takes a map[string]interface{} as the parameter and returns an interface{} and an error. +func handleIncomingMessage(data *protocols.BaseResponse) (interface{}, error) { + + method := data.Method + if len(method) == 0 { + return nil, errors.New("method is not a string") + } + + processRelayMessage(data) + return "", nil } -func processRelayMessage(s baseresponse) bool { - var isValid bool - res := &s - err := processRelayMessageSanityCheck(res) +func handleIncomingComamnd(data protocols.DiscordSlashCommand) (interface{}, error) { + method := data.Method + if len(method) == 0 { + return nil, errors.New("method is not a string") + } + + // Process the message based on the method + switch method { + // Executing Discord slash command + case "DiscordSlashCommand": + // Convert this to the correct protocol + var incomingData protocols.DiscordSlashCommand + unmarshaler := jsonpb.Unmarshaler{} + + err := unmarshaler.Unmarshal(strings.NewReader(data.String()), &incomingData) + if err != nil { + return nil, err + } + + // Print processing message if in debug mode + if viper.GetBool("debug") { + log.Println("Processing Discord Command:", method) + } + return data, nil + // Registering Discord Slash Command + case "RegisterDiscordSlashCommand": + // Print registering message if in debug mode + if viper.GetBool("debug") { + log.Println("Registering a Discord Command:", method) + } + // Process the Discord command + VerifyDiscordCommand(data) + + // Ensure commandResponses.Commands map is initialized + if commandResponses.Commands == nil { + commandResponses.Commands = make(map[string]protocols.CommandData) + } + + // Add the new command to commandResponses + newCommand := protocols.CommandData{ + Command: data.Data.Command, + Description: data.Data.Description, + Args: data.Data.Args, + } + commandResponses.Commands[data.Data.Command] = newCommand + // Create a string slice of argument names + var argStrings []string + for _, arg := range data.Data.Args { + argStrings = append(argStrings, arg.Name) // Or whatever field you want to pass as string + } + + // Call AddDiscordCommand with the converted arguments + AddDiscordCommand(&commandResponses, data.Data.Command, data.Data.Description, argStrings...) + + return "", nil + default: + return "", nil + } +} + +// processRelayMessage processes the relay message and returns a boolean. +// +// s: baseresponse object containing the relay message data +// returns: true if the relay message is processed successfully, false otherwise +func processRelayMessage(s *protocols.BaseResponse) bool { + // Convert the baseresponse to a pointer + res := s + + // Perform sanity check on the relay message + err := processRelayMessageSanityCheck(res) if err != nil { log.Errorln("processRelayMessageSanityCheck failed.") log.Errorf("Err %s", err) return false } + + // Log the length and data of the relay message if debug mode is enabled if viper.GetBool("debug") { log.Println("[Debug][processRelayMessage]:", len(res.Data)) log.Println(res.Data) } - if len(res.ServerID) > 0 { - if viper.GetBool("debug") { - log.Println("[Debug]:", "ServerID found:", res.ServerID) - } - isValid = true - } else { + // Check if ServerID is missing from the response + if len(res.ServerId) == 0 { log.Warnln("ServerID Missing from response:") + return false } - if isValid { - switch res.Method { - case "Sync": - serverSync(res.ServerID, res) - return false - case "IRC": - log.Println("TODO: Method \"IRC\" Not Implemented Yet.") + + // Process the relay message based on the method + switch res.Method { + case "Sync": + serverSync(res.ServerId, res) + case "IRC": + log.Println("TODO: Method \"IRC\" Not Implemented Yet.") + case "DiscordChat": + // Send the relay message data to Discord chat + jsonresponse, err := json.Marshal(res) + checkError("DiscordChat", err) + sendresponse := bytes.NewBuffer(jsonresponse).String() + IRCSendMessage(viper.GetString("irc.systemchannel"), sendresponse) + usrMsg := res.Data["User"] + ": " + res.Data["Message"] + logRelayedMessages("Discord", usrMsg) + case "rawDiscord": + // Process the relay message for raw Discord + var m protocols.RawDiscordStruct + m.Channel = res.Data["channel"] + m.Server = res.Data["server"] + m.Message = res.Data["message"] + + // Check if required fields are not nil + if m.Channel == "" || m.Server == "" || m.Message == "" { + log.Errorln("[ProcessRelayMessage][rawDiscord]: One or more required fields are nil") return false - case "DiscordChat": - jsonresponse, err := json.Marshal(res) - checkError("DiscordChat", err) - sendresponse := bytes.NewBuffer(jsonresponse).String() - IRCSendMessage(viper.GetString("irc.systemchannel"), sendresponse) - usrMsg := res.Data["User"] + ": " + res.Data["Message"] - logRelayedMessages("Discord", usrMsg) - case "rawDiscord": - var m rawDiscordStruct - m.Channel = res.Data["channel"] - m.Server = res.Data["server"] - m.Message = res.Data["message"] - status := rawDiscordMessage(m) + } else { + // Send the raw Discord message and log the relayed message + status := sendRawDiscordMessage(&m) logRelayedMessages("TES3MP", m.Message) return status - case "VPNCheck": - var m rawDiscordStruct - m.Channel = res.Data["channel"] - m.Server = res.Data["server"] - m.Message = res.Data["message"] - isPlayerUsingVPN := checkPlayerIP(m.Message) - if isPlayerUsingVPN { - log.Println("[VPNCheck]:", m.Message, "has been kicked.") - res.Data["kickPlayer"] = "yes" - } else { - log.Println("[VPNCheck]:", m.Message, "is not suspected to be using a VPN.") - res.Data["kickPlayer"] = "no" - } - jsonresponse, err := json.Marshal(res) - checkError("VPNCheck", err) - sendresponse := bytes.NewBuffer(jsonresponse).String() - IRCSendMessage(viper.GetString("irc.systemchannel"), sendresponse) - default: - log.Println(res.Method, " is an unknown method.") } + case "VPNCheck": + // Process the VPN check for the relay message + processVPNCheck(res) + case "DiscordSlashCommandResponse": + // Process the response for Discord slash command + discordInteractiveToken := res.Data["discordInteractiveToken"] + discordInteractiveReply := res.Data["response"] + SendDiscordInteractiveMessage(discordInteractiveToken, discordInteractiveReply) + default: + log.Println(res.Method, " is an unknown method.") } + return false } +func VerifyDiscordCommand(s protocols.DiscordSlashCommand) bool { + + err := processRelayCommandSanityCheck(&s) + if err != nil { + log.Errorln("VerifyDiscordCommand failed.") + log.Errorf("Err %s", err) + return false + } + + if len(s.ServerId) == 0 { + log.Warnln("ServerID Missing from response:") + return false + } + + return true +} + +// processVPNCheck is a function that processes VPN checks for a given response. +func processVPNCheck(res *protocols.BaseResponse) { + // Create a rawDiscordStruct from the data in the response + m := protocols.RawDiscordStruct{ + Channel: res.Data["channel"], + Server: res.Data["server"], + Message: res.Data["message"], + } + + // Perform the VPN check on the player's IP + isPlayerUsingVPN := checkPlayerIP(m.Message) + + // Set the kickPlayer field in the response data based on the result of the VPN check + res.Data["kickPlayer"] = "no" + if isPlayerUsingVPN { + log.Printf("[VPNCheck]: %s has been kicked.", m.Message) + res.Data["kickPlayer"] = "yes" + } else { + log.Printf("[VPNCheck]: %s is not suspected to be using a VPN.", m.Message) + } + + // Convert the response to JSON + jsonresponse, err := json.Marshal(res) + checkError("VPNCheck", err) + + // Send the JSON response to the IRC system channel + IRCSendMessage(viper.GetString("irc.systemchannel"), string(jsonresponse)) +} + func logRelayedMessages(server string, message string) { if server != "" && message != "" { log.Println("<" + server + "> " + message) } } -func processRelayMessageSanityCheck(Rmsg *baseresponse) error { - tempRelayMsg := Rmsg - // Tried to convert this to a switch, it didnt like it. - if tempRelayMsg.Method == "" { - return errors.New("processRelayMessage: method cannot be blank") +// processRelayMessageSanityCheck checks the sanity of the relay message. +// It ensures that the method is not blank and that data is provided. +// If any of the checks fail, it returns an error. +func processRelayMessageSanityCheck(relayMsg *protocols.BaseResponse) error { + // Check if the method is blank + if relayMsg.Method == "" { + return fmt.Errorf("method cannot be blank") } - if len(tempRelayMsg.Data) == 0 { - return errors.New("processRelayMessage: No data provided.") + + // Check if jobid is blank + if relayMsg.JobId == "" { + return fmt.Errorf("jobid cannot be blank") } + + // Check if data is provided + if len(relayMsg.Data) == 0 { + return fmt.Errorf("no data provided") + } + + // Return nil if all checks pass + return nil +} + +// processRelayMessageSanityCheck checks the sanity of the relay message. +// It ensures that the method is not blank and that data is provided. +// If any of the checks fail, it returns an error. +func processRelayCommandSanityCheck(relayMsg *protocols.DiscordSlashCommand) error { + // Check if the method is blank + if relayMsg.Method == "" { + return fmt.Errorf("method cannot be blank") + } + + // Check if jobid is blank + if relayMsg.JobId == "" { + return fmt.Errorf("jobid cannot be blank") + } + + // Check if data is provided + if relayMsg.Data.Command == "" { + return fmt.Errorf("no command provided") + } + + // Return nil if all checks pass return nil } diff --git a/src/relayManager_test.go b/src/relayManager_test.go new file mode 100644 index 0000000..2d04f33 --- /dev/null +++ b/src/relayManager_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "testing" + + protocols "github.com/hotarublaze/gotes3mp/src/protocols" +) + +func Test_relayManagerTestProcessRelayMessageSanityCheck(t *testing.T) { + + // Test case 1: Method is blank + rmsg1 := &protocols.BaseResponse{ + Method: "", + JobId: "test jobid", + Data: map[string]string{"key": "test data"}, + } + err := processRelayMessageSanityCheck(rmsg1) + if err.Error() != "method cannot be blank" { + t.Errorf("Expected error: 'processRelayMessage: method cannot be blank', got: '%s'", err.Error()) + } + + // Test case 2: Data is empty + rmsg2 := &protocols.BaseResponse{ + Method: "test method", + JobId: "test jobid", + Data: nil, + } + err = processRelayMessageSanityCheck(rmsg2) + if err.Error() != "no data provided" { + t.Errorf("Expected error: 'no data provided', got: '%s'", err.Error()) + } + + // Test case 3: Method and data are valid + rmsg3 := &protocols.BaseResponse{ + Method: "test method", + JobId: "test jobid", + Data: map[string]string{"key": "test data"}, + } + err = processRelayMessageSanityCheck(rmsg3) + if err != nil { + t.Errorf("Expected no error, got: '%s'", err.Error()) + } + + // Test case 4: Method is valid, data is empty map + rmsg4 := &protocols.BaseResponse{ + Method: "test method", + JobId: "test jobid", + Data: make(map[string]string), + } + err = processRelayMessageSanityCheck(rmsg4) + if err.Error() != "no data provided" { + t.Errorf("Expected error: 'no data provided', got: '%s'", err.Error()) + } + + // Test case 5: Method is valid, data has multiple key-value pairs + rmsg5 := &protocols.BaseResponse{ + Method: "test method", + JobId: "test jobid", + Data: map[string]string{"key1": "value1", "key2": "value2"}, + } + err = processRelayMessageSanityCheck(rmsg5) + if err != nil { + t.Errorf("Expected no error, got: '%s'", err.Error()) + } + + // Test case 6: Method is blank + rmsg6 := &protocols.BaseResponse{ + Method: "Testing", + JobId: "", + Data: map[string]string{"key": "test data"}, + } + err = processRelayMessageSanityCheck(rmsg6) + if err.Error() != "jobid cannot be blank" { + t.Errorf("Expected error: 'jobid cannot be blank', got: '%s'", err.Error()) + } +} diff --git a/src/status.go b/src/status.go index 92b0ec1..6cb4105 100755 --- a/src/status.go +++ b/src/status.go @@ -1,17 +1,9 @@ package main import ( - "context" "encoding/json" "log" - "strconv" - "strings" "time" - - color "github.com/fatih/color" - "github.com/google/go-github/github" - "github.com/hashicorp/go-version" - "github.com/spf13/viper" ) // ServerStatus struct @@ -55,46 +47,3 @@ func UpdateStatus() (s []byte) { } return jsonData } - -func getLatestGithubRelease() (isUpdate bool, latestVersion string) { - client := github.NewClient(nil) - releases, _, _ := client.Repositories.GetLatestRelease(context.Background(), "HotaruBlaze", "goTES3MP") - latestRelease := releases.GetTagName() - - // Get the build number thats set on build - currentBuild, err := version.NewVersion(Build) - if err != nil { - log.Println(err) - } - - // Get latest github release - latestBuild, err := version.NewVersion(latestRelease) - if err != nil { - log.Println(err) - } - - if currentBuild.LessThan(latestBuild) { - return true, string("v" + latestBuild.String()) - } else { - return false, "nil" - } - -} - -func getStatus(firstLaunch bool, showModules bool) { - if firstLaunch { - color.HiBlack(strings.Repeat("=", 32)) - } - color.HiBlack("goTES3MP: " + Build) - color.HiBlack("Commit: " + GitCommit) - color.HiBlack("Github: " + "https://github.com/hotarublaze/goTES3MP" + "\n") - color.HiBlack("Interactive Console: " + strconv.FormatBool(viper.GetBool("enableInteractiveConsole"))) - isUpdate, UpdateVersion := getLatestGithubRelease() - if isUpdate { - color.HiGreen("New build of goTES3MP is available: " + UpdateVersion + ", Current: " + Build) - } - - if firstLaunch { - color.HiBlack(strings.Repeat("=", 32)) - } -} diff --git a/src/tes3mpSync.go b/src/tes3mpSync.go index 4ca46e5..be86114 100644 --- a/src/tes3mpSync.go +++ b/src/tes3mpSync.go @@ -5,41 +5,24 @@ import ( "encoding/json" "strconv" + protocols "github.com/hotarublaze/gotes3mp/src/protocols" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) var ServerID string -// var registrationToken -type serverSyncresponse struct { - ServerID string `json:"serverID"` - // SyncID string // Removed for now - Status string `json:"status"` - Method string `json:"method"` -} - -// type syncresponse struct { -// Serverid string `json:"serverid"` -// // SyncID string `json:"SyncID"` -// MaxPlayers int `json:"MaxPlayers"` -// CurrentPlayerCount int `json:"CurrentPlayerCount"` -// Forced bool `json:"Forced"` -// // Players []string `json:"Players"` -// // Status string `json:"Status"` -// } - -func serverSync(id string, res *baseresponse) { +func serverSync(id string, res *protocols.BaseResponse) { // We dont have any server saved, lets attempt to register the server. if viper.GetViper().GetString("tes3mp.serverid") == "" { if id != "" { viper.GetViper().Set("tes3mp.serverid", id) } } - if viper.GetViper().GetString("tes3mp.serverid") != res.ServerID { + if viper.GetViper().GetString("tes3mp.serverid") != res.ServerId { if viper.GetViper().GetBool("debug") { log.Warnln("[DEBUG]:", - "Ignoring'"+res.ServerID+"'", + "Ignoring'"+res.ServerId+"'", ",Configured to use serverID", viper.GetViper().GetString("tes3mp.serverid"), ) @@ -47,9 +30,9 @@ func serverSync(id string, res *baseresponse) { } // var syncRes syncresponse if len(ServerID) == 0 { - ServerID = res.Data["ServerID"] + ServerID = res.Data["server_id"] } - if ServerID == res.Data["ServerID"] && res.Data["Status"] == "Ping" { + if ServerID == res.Data["server_id"] && res.Data["Status"] == "Ping" { if res.Data["CurrentPlayerCount"] != "" && res.Data["MaxPlayers"] != "" { var err error CurrentPlayers, err = strconv.Atoi(res.Data["CurrentPlayerCount"]) @@ -59,14 +42,14 @@ func serverSync(id string, res *baseresponse) { checkError("MaxPlayersSync", err) } - var pongresponse serverSyncresponse + var pongresponse protocols.ServerSync - pongresponse.ServerID = res.ServerID + pongresponse.ServerId = res.ServerId pongresponse.Status = "Pong" pongresponse.Method = "Sync" // pongresponse.SyncID = ServerSyncID - jsonresponse, err := json.Marshal(pongresponse) + jsonresponse, err := json.Marshal(&pongresponse) checkError("pongresponse", err) pongresponseMsg := bytes.NewBuffer(jsonresponse).String() diff --git a/src/utils.go b/src/utils.go index 32ea2bd..1785d44 100755 --- a/src/utils.go +++ b/src/utils.go @@ -11,7 +11,7 @@ import ( ) // AppendIfMissing : Appends string if missing from array. -func AppendIfMissing(slice []string, i string) []string { +func appendIfMissing(slice []string, i string) []string { currentSlice := slice for _, ele := range currentSlice { if ele == i { @@ -81,32 +81,30 @@ func bToMb(b uint64) uint64 { return b / 1024 / 1024 } -// convertDiscordFormattedItems : Formats Discord Formatted Items to Text Only +// convertDiscordFormattedItems formats Discord formatted items to text only. func convertDiscordFormattedItems(str string, gid string) string { - // match all discord formatted items reChannel := regexp.MustCompile(`<#(\d+)>`) reMentionUser := regexp.MustCompile(`<@(?:\!)?(\d+)>`) reMentionRole := regexp.MustCompile(`<@&(\d+)>`) + // Replace channel mentions with channel names str = reChannel.ReplaceAllStringFunc(str, func(s string) string { - // get the channels for this guild id := reChannel.FindStringSubmatch(s)[1] channels, err := DiscordSession.GuildChannels(gid) if err != nil { log.Errorln("[utils:convertDiscordFormattedItems]", "Error getting guild channels: ", err) return s } - for _, c := range channels { - if c.ID == id { - return "<%" + c.Name + ">" + for _, channel := range channels { + if channel.ID == id { + return "<%" + channel.Name + ">" } } - return s }) - reMentionUser.FindAllStringSubmatchIndex(str, -1) + + // Replace user mentions with user names str = reMentionUser.ReplaceAllStringFunc(str, func(s string) string { - // get the user for this guild id := reMentionUser.FindStringSubmatch(s)[1] member, err := DiscordSession.GuildMember(gid, id) if err != nil { @@ -119,30 +117,29 @@ func convertDiscordFormattedItems(str string, gid string) string { return "<@" + member.User.Username + ">" } }) + + // Replace role mentions with role names str = reMentionRole.ReplaceAllStringFunc(str, func(s string) string { - // get the role for this guild id := reMentionRole.FindStringSubmatch(s)[1] roles, err := DiscordSession.GuildRoles(gid) if err != nil { log.Errorln("[utils:convertDiscordFormattedItems]", "Error getting guild roles: ", err) return s } - for _, r := range roles { - if r.ID == id { - return "<%@" + r.Name + ">" + for _, role := range roles { + if role.ID == id { + return "<%@" + role.Name + ">" } } return s }) + return str } // filterDiscordEmotes : Formats Discord Emotes Correctly func filterDiscordEmotes(str string) string { - re := regexp.MustCompile(`<:(\S+):\d+>`) - filteredString := re.ReplaceAllString(str, `:$1:`) - - return filteredString + return regexp.MustCompile(`<:(\S+):\d+>`).ReplaceAllString(str, `:$1:`) } // MemoryDebugInfo : Print current memory and GC cycles, Used for monitoring for memory leaks @@ -155,18 +152,9 @@ func MemoryDebugInfo() { } -// func trimLeftChar(s string) string { -// for i := range s { -// if i > 0 { -// return s[i:] -// } -// } -// return s[:0] -// } - func checkError(str string, err error) bool { if err != nil { - log.Errorf(str, err) + log.Errorf("%s - %s", str, err) return true } return false diff --git a/src/utils_test.go b/src/utils_test.go new file mode 100644 index 0000000..ec5fa62 --- /dev/null +++ b/src/utils_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_utils_appendIfMissing(t *testing.T) { + slice := []string{"apple", "banana", "cherry"} + + // Append a missing element + newSlice := appendIfMissing(slice, "durian") + expectedSlice := []string{"apple", "banana", "cherry", "durian"} + assert.Equal(t, expectedSlice, newSlice) + + // Don't append an existing element + newSlice = appendIfMissing(slice, "banana") + assert.Equal(t, slice, newSlice) +} + +func Test_utils_ToHexInt(t *testing.T) { + n := big.NewInt(123456) + + hex := toHexInt(n) + expectedHex := "01E240" + assert.Equal(t, expectedHex, hex) +} + +func Test_utils_FindinArray(t *testing.T) { + slice := []string{"apple", "banana", "cherry"} + + // Find an existing element + index, found := FindinArray(slice, "banana") + assert.True(t, found) + assert.Equal(t, 1, index) + + // Try to find a missing element + index, found = FindinArray(slice, "durian") + assert.False(t, found) + assert.Equal(t, -1, index) +} + +func Test_utils_RemoveRGBHex(t *testing.T) { + message := "The color is #FF0000" + expectedMessage := "The color is " + newMessage := removeRGBHex(message) + assert.Equal(t, expectedMessage, newMessage) + + // Test with multiple RGB Hex values + message = "The colors are #FF0000, #00FF00, and #0000FF" + expectedMessage = "The colors are , , and " + newMessage = removeRGBHex(message) + assert.Equal(t, expectedMessage, newMessage) +} + +func Test_utils_FilterDiscordEmotes(t *testing.T) { + message := "This is a <:emote_name:123456> emote" + expectedMessage := "This is a :emote_name: emote" + newMessage := filterDiscordEmotes(message) + assert.Equal(t, expectedMessage, newMessage) + + // Test with multiple emotes + message = "These are <:emote1:123> and <:emote2:456> emotes" + expectedMessage = "These are :emote1: and :emote2: emotes" + newMessage = filterDiscordEmotes(message) + assert.Equal(t, expectedMessage, newMessage) +} diff --git a/src/vpnChecker.go b/src/vpnChecker.go index 3ec4e30..b41bee7 100644 --- a/src/vpnChecker.go +++ b/src/vpnChecker.go @@ -2,7 +2,8 @@ package main import ( "encoding/json" - "io/ioutil" + "fmt" + "io" "net/http" log "github.com/sirupsen/logrus" @@ -12,7 +13,7 @@ import ( var ipAddressArray []string -type IPHubresponseStruct struct { +type IPHubResponseStruct struct { IP string `json:"ip"` CountryCode string `json:"countryCode"` CountryName string `json:"countryName"` @@ -58,7 +59,7 @@ func checkPlayerIP(ipAddress string) bool { } // If no api keys are set, print out a warning and skip the checks. - if len(viper.GetString("vpn.iphub_apikey")) == 0 && len(viper.GetString("vpn.iphub_apikey")) == 0 { + if len(viper.GetString("vpn.iphub_apikey")) == 0 && len(viper.GetString("vpn.ipqualityscore_apikey")) == 0 { log.Warnln("[vpnChecker]: ", "vpnChecker was triggered, however no api keys are currently set. Allowing player to join.") return false } @@ -80,8 +81,8 @@ func checkPlayerIP(ipAddress string) bool { } func ipHubRequest(ipAddress string) bool { - var webReq = "http://v2.api.iphub.info/ip/" + ipAddress - req, err := http.NewRequest("GET", webReq, nil) + url := fmt.Sprintf("http://v2.api.iphub.info/ip/%s", ipAddress) + req, err := http.NewRequest("GET", url, nil) if err != nil { checkError("ipHubRequest:1", err) } @@ -91,54 +92,60 @@ func ipHubRequest(ipAddress string) bool { if err != nil { checkError("ipHubRequest:2", err) } + defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { checkError("ipHubRequest:3", err) } - var IPresponse IPHubresponseStruct - err = json.Unmarshal(body, &IPresponse) + var IPResponse IPHubResponseStruct + err = json.Unmarshal(body, &IPResponse) if err != nil { checkError("ipHubRequest:4", err) } - defer resp.Body.Close() - if IPresponse.Block == 1 { - ipAddressArray = AppendIfMissing(ipAddressArray, ipAddress) + if IPResponse.Block == 1 { + ipAddressArray = appendIfMissing(ipAddressArray, ipAddress) return true } return false } func ipqualityscoreRequest(ipAddress string) bool { - var webReq = "https://ipqualityscore.com/api/json/ip/" + viper.GetString("vpn.ipqualityscore_apikey") + "/" + ipAddress + apiKey := viper.GetString("vpn.ipqualityscore_apikey") + webReq := fmt.Sprintf("https://ipqualityscore.com/api/json/ip/%s/%s", apiKey, ipAddress) + req, err := http.NewRequest("GET", webReq, nil) if err != nil { checkError("ipqualityscoreRequest:1", err) + return false } resp, err := http.DefaultClient.Do(req) if err != nil { checkError("ipqualityscoreRequest:2", err) + return false } + defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { checkError("ipqualityscoreRequest:3", err) + return false } var IPresponse ipqualityscoreresponseStruct err = json.Unmarshal(body, &IPresponse) if err != nil { checkError("ipqualityscoreRequest:4", err) + return false } - defer resp.Body.Close() - // fraud_score if IPresponse.FraudScore >= 80 { - ipAddressArray = AppendIfMissing(ipAddressArray, ipAddress) + ipAddressArray = appendIfMissing(ipAddressArray, ipAddress) return true } + return false } diff --git a/src/webserver.go b/src/webserver.go index 412d868..8788f12 100755 --- a/src/webserver.go +++ b/src/webserver.go @@ -25,11 +25,11 @@ func InitWebserver() { // status : Print current ServerStatus struct as json func status(w http.ResponseWriter, r *http.Request) { s := UpdateStatus() - status := pretty.Pretty(s) if s == nil { log.Errorln("UpdateStatus returned nil") - fmt.Fprintf(w, string("An Error Occurred while getting /status")) - } else { - fmt.Fprintf(w, string(status)) + fmt.Fprint(w, "An Error Occurred while getting /status") + return } + status := pretty.Pretty(s) + fmt.Fprint(w, status) } diff --git a/src/webserver_test.go b/src/webserver_test.go new file mode 100644 index 0000000..a858a6c --- /dev/null +++ b/src/webserver_test.go @@ -0,0 +1,25 @@ +// webserver_test.go + +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test_webserver_statusHandler(t *testing.T) { + // Create a request to the /status endpoint + req := httptest.NewRequest("GET", "/status", nil) + + // Create a ResponseRecorder to capture the response + rr := httptest.NewRecorder() + + // Call the status handler function with the ResponseRecorder and Request + status(rr, req) + + // Check the response status code + if rr.Code != http.StatusOK { + t.Errorf("Expected status code %d; got %d", http.StatusOK, rr.Code) + } +} diff --git a/tes3mp/lib/lua/irc.lua b/tes3mp/lib/lua/irc.lua index c60dc02..4036d6f 100644 --- a/tes3mp/lib/lua/irc.lua +++ b/tes3mp/lib/lua/irc.lua @@ -1,4 +1,9 @@ -local socket = require "socket-lanes" +local socket = nil +if package.config:sub(1,1) == "/" then + socket = require "socket-lanes" +else + socket = require "socket" +end local error = error local setmetatable = setmetatable diff --git a/tes3mp/scripts/custom/IrcBridge/IrcBridge.lua b/tes3mp/scripts/custom/IrcBridge/IrcBridge.lua index ed9be93..c18dfcc 100644 --- a/tes3mp/scripts/custom/IrcBridge/IrcBridge.lua +++ b/tes3mp/scripts/custom/IrcBridge/IrcBridge.lua @@ -7,19 +7,17 @@ require("color") local irc = require("irc") -local cjson = require("cjson") -local goTES3MPSync = require("custom.goTES3MP.sync") -local goTES3MPUtils = require("custom.goTES3MP.utils") -local goTES3MPCommands = require("custom.goTES3MP.commands") local goTES3MPConfig = require("custom.goTES3MP.config") -local goTES3MPVPNChecker = require("custom.goTES3MP.vpnChecker") +local goTES3MPSync = require("custom.goTES3MP.sync") +local goTES3MPModules = nil local IrcBridge = {} -IrcBridge.version = "v5.0.0-goTES3MP" +IrcBridge.version = "v0.4.1-goTES3MP" IrcBridge.scriptName = "IrcBridge" IrcBridge.debugMode = false +IrcBridge.maxMessageLength = 2048 local config = goTES3MPConfig.GetConfig() @@ -62,26 +60,32 @@ IrcBridge.RecvMessage = function() print("IRCDebug: " .. message) end - local response = goTES3MPUtils.isJsonValidDecode(message) - - IrcBridge.switch(response.method) { - ["Sync"] = function() - goTES3MPSync.GotSync(response.ServerID, response.SyncID) - end, - ["Command"] = function() - goTES3MPCommands.processCommand(response.data["TargetPlayer"],response.data["Command"],response.data["CommandArgs"], response.data["replyChannel"]) - end, - ["DiscordChat"] = function() - IrcBridge.chatMessage(response) - end, - ["VPNCheck"] = function() - goTES3MPVPNChecker.kickPlayer(response.data["playerpid"], response.data["kickPlayer"]) - end, - default = function() - print("Error: "..tableHelper.getSimplePrintableTable(response)) - print("Unknown method (" .. response.method .. ") was received.") - end, - } + local response = goTES3MPModules.utils.isJsonValidDecode(message) + if response ~= nil then + IrcBridge.switch(response.method) { + ["Sync"] = function() + goTES3MPSync.gotSync(response.ServerID, response.SyncID) + end, + ["Command"] = function() + local command = response.data.command + local commandArgs = goTES3MPModules.utils.isJsonValidDecode(response.data.commandArgs) + tes3mp.LogMessage(enumerations.log.INFO, "[GoTES3MP:Command] Executing command \"" .. command .. "\" with args {" .. tableHelper.getSimplePrintableTable(commandArgs).."}") + commandArgs["discordInteractiveToken"] = response.data["discordInteractiveToken"] + + goTES3MPModules.commands.processCommand(command, commandArgs) + end, + ["DiscordChat"] = function() + IrcBridge.chatMessage(response) + end, + ["VPNCheck"] = function() + goTES3MPModules.vpnChecker.kickPlayer(response.data["playerpid"], response.data["kickPlayer"]) + end, + default = function() + print("Error: "..tableHelper.getSimplePrintableTable(response)) + print("Unknown method (" .. response.method .. ") was received.") + end, + } + end end lastMessage = message end @@ -112,6 +116,10 @@ IrcBridge.chatMessage = function(response) end IrcBridge.SendSystemMessage = function(message) + if string.len(message) > IrcBridge.maxMessageLength then + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:IRCBridge] SendSystemMessage was skipped due to message length exceeding limit.") + return + end if message ~= lastMessage then s:sendChat(systemchannel, message) lastMessage = message @@ -141,6 +149,7 @@ customEventHooks.registerValidator( function() IRCTimerId = tes3mp.CreateTimer("OnIRCUpdate", time.seconds(1)) tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:IRCBridge] ".. IrcBridge.version.. " Loaded") + goTES3MPModules = goTES3MP.GetModules() tes3mp.StartTimer(IRCTimerId) end ) diff --git a/tes3mp/scripts/custom/IrcBridge/irc.lua b/tes3mp/scripts/custom/IrcBridge/irc.lua index ea9e019..37f1541 100644 --- a/tes3mp/scripts/custom/IrcBridge/irc.lua +++ b/tes3mp/scripts/custom/IrcBridge/irc.lua @@ -1,4 +1,9 @@ -local socket = require "socket-lanes" +local socket = nil +if package.config:sub(1,1) == "/" then + socket = require "socket-lanes" +else + socket = require "socket" +end local error = error local setmetatable = setmetatable @@ -7,7 +12,6 @@ local unpack = unpack local pairs = pairs local assert = assert local require = require -local tonumber = tonumber local type = type local pcall = pcall diff --git a/tes3mp/scripts/custom/IrcBridge/irc/set.lua b/tes3mp/scripts/custom/IrcBridge/irc/set.lua index 13952c3..13e99bd 100644 --- a/tes3mp/scripts/custom/IrcBridge/irc/set.lua +++ b/tes3mp/scripts/custom/IrcBridge/irc/set.lua @@ -1,5 +1,9 @@ -local select = require "socket".select - +local socket = nil +if package.config:sub(1,1) == "/" then + socket = require "socket-lanes" +else + socket = require "socket" +end local setmetatable = setmetatable local insert = table.insert local remove = table.remove diff --git a/tes3mp/scripts/custom/goTES3MP/commands.lua b/tes3mp/scripts/custom/goTES3MP/commands.lua index 8841664..37001a3 100644 --- a/tes3mp/scripts/custom/goTES3MP/commands.lua +++ b/tes3mp/scripts/custom/goTES3MP/commands.lua @@ -1,115 +1,135 @@ local commands = {} -local goTES3MPUtils = require("custom.goTES3MP.utils") -local goTES3MPgetJournal = require("custom.goTES3MP.getJournal") -local goTES3MPgetPlayers = require("custom.goTES3MP.getPlayers") - -commands.kickPlayer = function(player, discordReplyChannel) - targetPid = commands.getPlayerPID(player) - if targetPid ~= nil then - tes3mp.SendMessage( - targetPid, - color.Red .. "[SYSTEM]" .. " " .. "You have been kicked by an Administrator.", - false - ) - tes3mp.Kick(targetPid) - commands.Sendresponse(discordReplyChannel) - end -end +local goTES3MPModules = goTES3MP.GetModules() -commands.runConsole = function(player, commandArgs, discordReplyChannel) - targetPid = commands.getPlayerPID(player) - if targetPid ~= nil then - logicHandler.RunConsoleCommandOnPlayer(targetPid, commandArgs) - commands.Sendresponse(discordReplyChannel) - end -end +---@class CommandHandler +---@field description string +---@field handler fun(player: string, commandArgs: string[], discordReplyChannel: string) +---@type table +local commandHandlers = { + ["kickplayer"] = { + description = "Kicks the specified player from the tes3mp server.", + handler = function(commandArgs) + local username = commandArgs["username"] + local targetPid = commands.getPlayerPID(username) -commands.resetKills = function(discordReplyChannel) - for refId, killCount in pairs(WorldInstance.data.kills) do - WorldInstance.data.kills[refId] = 0 - end - WorldInstance:QuicksaveToDrive() - if tableHelper.getCount(Players) > 0 then - for pid, player in pairs(Players) do - WorldInstance:LoadKills(pid, true) - tes3mp.SendMessage(pid, "All the kill counts for creatures and NPCs have been reset.\n", false) - end - end - tes3mp.LogMessage(enumerations.log.INFO, "All the kill counts for creatures and NPCs have been reset.") - commands.Sendresponse(discordReplyChannel) -end -commands.processCommand = function(player, command, commandArgs, discordReplyChannel) - if player ~= nil then - if string.byte(player:sub(1,1)) == 34 then - pLength = string.len(player) - player = player:sub(2, pLength - 1) + if targetPid ~= nil then + tes3mp.SendMessage(targetPid, color.Red .. "[SYSTEM] " .. "You have been kicked by an Administrator.", false) + tes3mp.Kick(targetPid) + commands.sendDiscordSlashResponse(username.." has been kicked", commandArgs) + else + commands.sendDiscordSlashResponse(username.." does not exist", commandArgs) + end + end, + args = { + {name = "username", description = "The name of the player to kick.", required = true} + } + }, + ["runconsole"] = { + description = "Run a console command on a specific Player.", + handler = function(commandArgs) + local username = commandArgs["username"] + local consoleCommand = commandArgs["command"] + + local targetPid = commands.getPlayerPID(username) + if targetPid ~= nil then + logicHandler.RunConsoleCommandOnPlayer(targetPid, consoleCommand) + commands.sendDiscordSlashResponse("Console command has been sent to the user", commandArgs) + else + commands.sendDiscordSlashResponse("Player does not exist", commandArgs) + end + end, + args = { + {name = "username", description = "The name of the player.", required = true}, + {name = "command", description = "The console command to run.", required = true} + } + }, + ["resetkills"] = { + description = "Reset player kills.", + handler = function(commandArgs) + for refId, killCount in pairs(WorldInstance.data.kills) do + WorldInstance.data.kills[refId] = 0 + end + WorldInstance:QuicksaveToDrive() + if tableHelper.getCount(Players) > 0 then + for pid, player in pairs(Players) do + WorldInstance:LoadKills(pid, true) + tes3mp.SendMessage(pid, "All the kill counts for creatures and NPCs have been reset.\n", false) + end + end + tes3mp.LogMessage(enumerations.log.INFO, "All the kill counts for creatures and NPCs have been reset.") + commands.sendDiscordSlashResponse("All the kill counts for creatures and NPCs have been reset", commandArgs) end - if commandArgs ~= nil then - tes3mp.LogMessage( - enumerations.log.INFO, - "[Discord]: " .. - "Running " .. - command .. ' on player "' .. player .. '"' .. ' with Arguements "' .. commandArgs .. '"' - ) - else - tes3mp.LogMessage( - enumerations.log.INFO, - "[Discord]: " .. "Running " .. command .. ' on player "' .. player .. '"' - ) + }, + ["players"] = { + description = "List Players", + handler = function(commandArgs) + if goTES3MPModules["getPlayers"] ~= nil then + local playerList = goTES3MPModules.getPlayers.getPlayers() + commands.sendDiscordSlashResponse(playerList, commandArgs) + else + commands.sendDiscordSlashResponse("Module not found or loaded", commandArgs) + end end + }, + ["getjournal"] = { + description = "get a player's Journal Entry", + handler = function(commandArgs) + local username = commandArgs["username"] + local questid = commandArgs["questid"] + if goTES3MPModules["getJournal"] ~= nil then + local questList = goTES3MPModules.getJournal.GetJournalEntries(username, questid) + commands.sendDiscordSlashResponse(questList, commandArgs) + else + commands.sendDiscordSlashResponse("Module not found or loaded", commandArgs) + end + end, + args = { + {name = "username", description = "The name of the player.", required = true}, + {name = "questid", description = "the id of the quest", required = true} + } + }, +} + +--- Process the command and call the appropriate handler. +---@param command string +---@param commandArgs string[] +---@param discordReplyChannel string +commands.processCommand = function(command, commandArgs, discordReplyChannel) + local command = string.lower(command) + local commandHandlerData = commandHandlers[command] + + if commandHandlerData then + local handler = commandHandlerData.handler + handler(commandArgs) else - tes3mp.LogMessage(enumerations.log.INFO, "[Discord]: " .. "Running " .. command) + tes3mp.LogMessage(enumerations.log.WARN, "[Discord]: Unrecognized command: " .. command) end +end - if command == "kickplayer" then - commands.kickPlayer(player, discordReplyChannel) - end - if command == "players" then - goTES3MPgetPlayers.getPlayers(discordReplyChannel) - end - if command == "runconsole" then - commands.runConsole(player, commandArgs, discordReplyChannel) - end - if command == "resetkills" then - commands.resetKills(discordReplyChannel) - end - if command == "getJournal" then - goTES3MPgetJournal.GetJournalEntrys(player, commandArgs, discordReplyChannel) - end - if command == "help" then - local commandList = "" - commandList = commandList .. "```" .. "\n" - commandList = - commandList .. "!kickplayer (Player): Kicks the specified player from the tes3mp server." .. "\n" - commandList = - commandList .. - "!runconsole (Player) (Command): Run a console command on a specific Player." .. "\n" - commandList = commandList .. "!resetkills: Reset player kills." .. "\n" - commandList = commandList .. "!players: List Players" .. "\n" - commandList = commandList .. "!getJournal (Player) (QuestID): Get a players Journal Entry" .. "\n" - commandList = commandList .. "```" .. "\n" - - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - commandList - ) +--- Send a response to the Discord channel. +---@param discordReplyChannel string +commands.sendDiscordSlashResponse = function(responseText, commandArgs) + local messageJson = { + job_id = goTES3MPModules.utils.generate_uuid(), + server_id = goTES3MP.GetServerID(), + method = "DiscordSlashCommandResponse", + source = "TES3MP", + data = commandArgs + } + messageJson["data"]["response"] = responseText -- Assuming `response` is the response text + + local encodedMessage = goTES3MPModules.utils.isJsonValidEncode(messageJson) + if encodedMessage ~= nil then + IrcBridge.SendSystemMessage(encodedMessage) end end -commands.Sendresponse = function(discordReplyChannel) - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - "**Command Executed**" - ) -end --- Running this before a player connects, will cause a tes3mp crash --- tes3mp.GetLastPlayerId() crashes if a player hasnt connected since server start. commands.getPlayerPID = function(str) + if tableHelper.getCount(Players) == 0 then + return nil + end + local lastPid = tes3mp.GetLastPlayerId() if str ~= nil then for playerIndex = 0, lastPid do @@ -123,4 +143,61 @@ commands.getPlayerPID = function(str) return nil end +commands.pushSlashCommands = function(pid, cmd) + local commandData = tableHelper.shallowCopy(commandHandlers[cmd[2]]) + + -- Remove the "handler" field if present + commandData.handler = nil + -- Include the "command" field for each command + commandData.command = cmd[2] + + local messageJson = { + job_id = goTES3MPModules.utils.generate_uuid(), + method = "RegisterDiscordSlashCommand", + source = "TES3MP", + server_id = goTES3MP.GetServerID(), + data = commandData + } + + local response = goTES3MPModules.utils.isJsonValidEncode(messageJson) + if response ~= nil then + IrcBridge.SendSystemMessage(response) + end +end + +commands.pushAllSlashCommands = function(pid, cmd) + for cmdName, _ in pairs(commandHandlers) do + local commandData = tableHelper.shallowCopy(commandHandlers[cmdName]) + + -- Remove the "handler" field if present + commandData.handler = nil + -- Include the "command" field for each command + commandData.command = cmdName + + local messageJson = { + job_id = goTES3MPModules.utils.generate_uuid(), + method = "RegisterDiscordSlashCommand", + source = "TES3MP", + server_id = goTES3MP.GetServerID(), + data = commandData + } + + local response = goTES3MPModules.utils.isJsonValidEncode(messageJson) + if response ~= nil then + IrcBridge.SendSystemMessage(response) + end + end +end + +customCommandHooks.registerCommand("pushSlashCommand", function(pid, cmd) + commands.pushSlashCommands(pid, cmd) +end) +customCommandHooks.setRankRequirement("pushSlashCommand", 3) + +customCommandHooks.registerCommand("pushAllSlashCommands", function(pid, cmd) + commands.pushAllSlashCommands(pid, cmd) +end) +customCommandHooks.setRankRequirement("pushAllSlashCommands", 3) + + return commands \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/config.lua b/tes3mp/scripts/custom/goTES3MP/config.lua index 14de10a..cefbdc7 100644 --- a/tes3mp/scripts/custom/goTES3MP/config.lua +++ b/tes3mp/scripts/custom/goTES3MP/config.lua @@ -1,5 +1,4 @@ local cjson = require("cjson") -local goTES3MPUtils = require("custom.goTES3MP.utils") local goTES3MPConfig = {} local config = {} @@ -7,25 +6,28 @@ local configFile = "custom/goTES3MPConfig.json" local defaultConfig = { goTES3MP = { - serverid = "", - configVersion = 1, - defaultDiscordServer = "", - defaultDiscordChannel = "", - defaultDiscordNotifications = "", + serverid = "", -- Server ID + configVersion = 1, -- Configuration version + defaultDiscordServer = "", -- Default Discord server + defaultDiscordChannel = "", -- Default Discord channel + defaultDiscordNotifications = "", -- Default Discord notifications + userModules = {}, -- User modules }, IRCBridge = { - nick = "", - server = "", - port = "6667", - password = "", - nspasswd = "", - systemchannel = "#", - nickfilter = "", - discordColor = "#825eed", - ircColor = "#5D9BEE" + nick = "", -- IRC nickname + server = "", -- IRC server + port = "6667", -- IRC port + password = "", -- IRC password + nspasswd = "", -- NS password + systemchannel = "#", -- IRC system channel + nickfilter = "", -- Nick filter + discordColor = "#825eed", -- Discord color + ircColor = "#5D9BEE" -- IRC color } } +--- Retrieves the configuration. +---@return table - The configuration goTES3MPConfig.GetConfig = function() if next(config) == nil then config = goTES3MPConfig.LoadConfig() @@ -34,13 +36,18 @@ goTES3MPConfig.GetConfig = function() return config end - +--- Saves the configuration to the specified file. +--- @param config table - The configuration to save. goTES3MPConfig.SaveConfig = function(config) if config ~= nil then jsonInterface.quicksave(configFile, config) end end +--- Loads the configuration from the file. +--- If the file doesn't exist, a default configuration is generated. +--- If a migration is possible, the configuration is migrated. +--- @return table - The loaded configuration. goTES3MPConfig.LoadConfig = function() config = jsonInterface.load(configFile) @@ -67,6 +74,9 @@ goTES3MPConfig.LoadConfig = function() return config end +--- Migrates the configuration from the deprecated DataManager format. +--- @param config table - The current configuration. +--- @return table - The migrated configuration. goTES3MPConfig.MigrateFromDataManager = function(config) -- Config file does not already exist, Lets see if we can migrate local dataManagerIRCConfig = jsonInterface.load("custom/__config_IrcBridge.json") @@ -104,6 +114,9 @@ goTES3MPConfig.MigrateFromDataManager = function(config) end end +--- Migrates the `goTES3MPData` from the deprecated DataManager format. +--- @param dataManagerGoTES3MPData table - The `goTES3MPData` from the deprecated DataManager format. +--- @return table - The migrated `goTES3MPData`. goTES3MPConfig.goTES3MPDataMigration = function(currentConfig) -- Before a config version was added, we dont need to check the version currently. if currentConfig.configVersion == nil or currentConfig.discordchannel ~= nil or currentConfig.discordalerts ~= nil or currentConfig.discordserver ~= nil then diff --git a/tes3mp/scripts/custom/goTES3MP/getJournal.lua b/tes3mp/scripts/custom/goTES3MP/getJournal.lua deleted file mode 100644 index a31f290..0000000 --- a/tes3mp/scripts/custom/goTES3MP/getJournal.lua +++ /dev/null @@ -1,54 +0,0 @@ -local getJournal = {} -local cjson = require("cjson") -local goTES3MPUtils = require("custom.goTES3MP.utils") - -getJournal.GetJournalEntrys = function(playerName, questID, discordReplyChannel) - local questIndexs = {} - local player = logicHandler.GetPlayerByName(playerName) - if player ~= nil then - for _, quest in pairs(player.data.journal) do - if string.lower(quest.quest) == string.lower(questID) then - table.insert(questIndexs, quest.index) - end - end - if #questIndexs == 0 then - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - "**Quest ID is invalid or player does not have this Quest.**" - ) - return - end - - questIndexs = goTES3MPUtils.alphanumsort(questIndexs) - questList = - "**" .. playerName .. "'s Journal entry's for " .. '"' .. string.lower(questID) .. '"' .. "**" .. "\n" - questList = questList .. "```" .. "\n" - - for i, index in pairs(questIndexs) do - if i == #questIndexs then - questList = questList .. index .. "\n" - else - questList = questList .. index .. "," - end - end - questList = questList .. "```" - - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - questList - ) - else - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - "**" .. "Player does not Exist." .. "**" - ) - end -end - -return getJournal diff --git a/tes3mp/scripts/custom/goTES3MP/getPlayers.lua b/tes3mp/scripts/custom/goTES3MP/getPlayers.lua deleted file mode 100644 index 4f0b326..0000000 --- a/tes3mp/scripts/custom/goTES3MP/getPlayers.lua +++ /dev/null @@ -1,26 +0,0 @@ -local getPlayers = {} -local cjson = require("cjson") -local goTES3MPUtils = require("custom.goTES3MP.utils") - -getPlayers.getPlayers = function(discordReplyChannel) - local playerList = "" - - if tableHelper.getCount(Players) > 0 then - for pid, player in pairs(Players) do - if player ~= nil and player:IsLoggedIn()then - playerList = playerList .. player.name .. "\n" - end - end - end - - playerList = "```" .."\n".. playerList .."\n".. "```" - - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - discordReplyChannel, - goTES3MP.GetDefaultDiscordServer(), - playerList - ) -end - -return getPlayers \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/main.lua b/tes3mp/scripts/custom/goTES3MP/main.lua index 0bffd05..0069f82 100644 --- a/tes3mp/scripts/custom/goTES3MP/main.lua +++ b/tes3mp/scripts/custom/goTES3MP/main.lua @@ -1,50 +1,152 @@ +-- The cjson library is required to parse JSON data. local cjson = require("cjson") --- GoTES3MPSyncID = "" + +-- WaitingForSync is a flag indicating whether the server is waiting for synchronization. WaitingForSync = false + +-- The goTES3MP table is used to define functions related to the goTES3MP module. local goTES3MP = {} -local TES3MPOnline = false --- Modules -local goTES3MPUtils = require("custom.goTES3MP.utils") -local goTES3MPSync = require("custom.goTES3MP.sync") -local goTES3MPChat = require("custom.goTES3MP.chat") -local goTES3MPVPNCheck = require("custom.goTES3MP.vpnChecker") +-- The goTES3MPModules variable is used to store the modules obtained from goTES3MP.GetModules(). +goTES3MPModules = nil + +-- TES3MPOnline is a flag indicating whether the TES3MP server is online. +local TES3MPOnline = false + +-- The goTES3MPConfig module is required to access the goTES3MP configuration. local goTES3MPConfig = require("custom.goTES3MP.config") +-- The goTES3MPConfig module is required to access the goTES3MP configuration. +local goTES3MPUtils = require("custom.goTES3MP.utils") local config = goTES3MPConfig.GetConfig() +-- Helper function to get a list of Lua module names from a folder. +---@param folderPath string The path of the folder to search for Lua modules. +---@return table A table containing the Lua module names found. +local function getLuaModulesFromFolder(folderPath) + local luaFiles = {} + + local command + if package.config:sub(1,1) == '\\' then + -- Windows + command = 'dir /B "' .. folderPath .. '"' + else + -- Unix-like systems + command = 'ls -1 "' .. folderPath .. '"' + end + + local fileHandle = io.popen(command) + local commandOutput = fileHandle:read("*a") -- Read the entire output + + if fileHandle:close() then + for filename in commandOutput:gmatch("[^\r\n]+") do + local moduleName = filename:match("(.+)%.lua$") + if moduleName then + table.insert(luaFiles, moduleName) + end + end + else + print("Failed to execute command: " .. command) + end + + return luaFiles +end + +-- Function to load the modules. +--- @return table The loaded goTES3MP modules. +goTES3MP.LoadModules = function() + if goTES3MPModules ~= nil then + return goTES3MPModules + end + + goTES3MPModules = {} + + -- Required Modules + local requiredModules = { + "utils", + "sync", + "commands" + } + + tes3mp.LogMessage(enumerations.log.INFO, "[GoTES3MP:Module] Loading Modules...") + -- Load required modules + for _, moduleName in ipairs(requiredModules) do + if moduleName ~= "main" then + tes3mp.LogMessage(enumerations.log.INFO, "[GoTES3MP:Module] Loading Required Module: \""..moduleName.."\"") + goTES3MPModules[moduleName] = require("custom.goTES3MP." .. moduleName) + end + end + + -- Ensure userModulesConfig exists in config + if config["goTES3MP"]["userModules"] == nil then + config["goTES3MP"]["userModules"] = {} + goTES3MPConfig.SaveConfig(config) + end + + local userModulesConfig = config["goTES3MP"]["userModules"] + + tes3mp.LogMessage(enumerations.log.INFO, "[GoTES3MP:Module] Loading user Modules...") + -- Load user-controllable modules + for _, moduleName in ipairs(getLuaModulesFromFolder("server/scripts/custom/goTES3MP/userModules")) do + local moduleValue = userModulesConfig[moduleName] + if moduleValue == true then + tes3mp.LogMessage(enumerations.log.INFO, "[GoTES3MP:Module] Loading userModule: \""..moduleName.."\"") + goTES3MPModules[moduleName] = require("custom.goTES3MP.userModules." .. moduleName) + end + userModulesConfig[moduleName] = moduleValue or false + end + + -- Write Config + goTES3MPConfig.SaveConfig(config) + return goTES3MPModules +end + +-- Function to get the server ID. +--- @return string The server ID. goTES3MP.GetServerID = function() - if config.goTES3MP.serverid == "" then - config.goTES3MP.serverid = goTES3MPUtils.randomString(16) - DataManager.saveData("goTES3MP", goTES3MP.config) + if config.goTES3MP.server_id == "" then + config.goTES3MP.server_id = goTES3MPModules.utils.randomString(16) + goTES3MPConfig.SaveConfig(config) end - return config.goTES3MP.serverid + return tostring(config.goTES3MP.serverid) end --- goTES3MP.GetSyncID = function() --- if GoTES3MPSyncID == "" then --- GoTES3MPSyncID = goTES3MPUtils.randomString(16) --- end --- return GoTES3MPSyncID --- end +-- Function to get the loaded modules. +--- @return table The loaded goTES3MP modules. +goTES3MP.GetModules = function() + if goTES3MPModules ~= nil then + return goTES3MPModules + end + goTES3MPModules = goTES3MP.LoadModules() + return goTES3MPModules +end + +-- Function to get the default Discord channel. +--- @return string The default Discord channel. goTES3MP.GetDefaultDiscordChannel = function() - return config.goTES3MP.defaultDiscordChannel + return tostring(config.goTES3MP.defaultDiscordChannel) end +-- Function to get the default Discord notifications channel. +--- @return string The default Discord notifications channel. goTES3MP.GetDefaultDiscordNotificationsChannel = function() - return config.goTES3MP.defaultDiscordNotifications + return tostring(config.goTES3MP.defaultDiscordNotifications) end +-- Function to get the default Discord server. +---@return string The default Discord server. goTES3MP.GetDefaultDiscordServer = function() - return config.goTES3MP.defaultDiscordServer + return tostring(config.goTES3MP.defaultDiscordServer) end + customEventHooks.registerValidator( "OnServerInit", function() goTES3MPConfig.LoadConfig() + goTES3MPModules = goTES3MP.LoadModules() goTES3MP.GetServerID() tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP]: Loaded") end @@ -72,7 +174,7 @@ customEventHooks.registerHandler("OnServerExit", function(eventStatus, pid) end) customCommandHooks.registerCommand("forceSync", function(pid) - goTES3MPSync.SendSync(true) + goTES3MPModules.sync.sendSync(true) end) return goTES3MP \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/sync.lua b/tes3mp/scripts/custom/goTES3MP/sync.lua index 91151e9..f1f3611 100644 --- a/tes3mp/scripts/custom/goTES3MP/sync.lua +++ b/tes3mp/scripts/custom/goTES3MP/sync.lua @@ -1,65 +1,89 @@ -local Sync = {} -local goTES3MPUtils = require("custom.goTES3MP.utils") -SyncTimerID = nil +-- The goTES3MPModules variable is used to store the modules obtained from goTES3MP.GetModules(). +local goTES3MPModules = nil --- SyncTimer: In seconds -local SyncTimer = 30 +-- The goTES3MPSync table is used to define functions related to synchronization. +local goTES3MPSync = {} -local cjson = require("cjson") +-- The syncTimerID variable is used to store the ID of the synchronization timer. +local syncTimerID = nil --- Ping GoTES3MP with stats -Sync.SendSync = function(forceResync) - local ServerID = goTES3MP.GetServerID() - if goTES3MP.GetServerID() ~= "" then +-- The syncTimer variable is used to define the duration (in seconds) between synchronization updates. +local syncTimer = 30 + +-- Sends a synchronization message to the server. +---@param forceResync boolean indicator whether to force a resynchronization. +goTES3MPSync.sendSync = function(forceResync) + local serverID = goTES3MP.GetServerID() + if serverID ~= "" then + local maxPlayers = tostring(tes3mp.GetMaxPlayers()) + local currentPlayerCount = tostring(logicHandler.GetConnectedPlayerCount()) + + -- Construct the synchronization message as a JSON object. local messageJson = { - ServerID = ServerID, + job_id = goTES3MPModules.utils.generate_uuid(), + server_id = serverID, method = "Sync", source = "TES3MP", data = { - MaxPlayers = tostring(tes3mp.GetMaxPlayers()), - CurrentPlayerCount = tostring(logicHandler.GetConnectedPlayerCount()), + MaxPlayers = maxPlayers, + CurrentPlayerCount = currentPlayerCount, Forced = tostring(forceResync), - Status = "Ping", + Status = "Ping" } } - local response = goTES3MPUtils.isJsonValidEncode(messageJson) + + -- Encode the synchronization message into a JSON string. + local response = goTES3MPModules.utils.isJsonValidEncode(messageJson) + + -- Send the encoded synchronization message via IrcBridge. if response ~= nil then IrcBridge.SendSystemMessage(response) end WaitingForSync = true end - tes3mp.RestartTimer(SyncTimerID, time.seconds(SyncTimer)) + + -- Restart the synchronization timer. + tes3mp.RestartTimer(syncTimerID, time.seconds(syncTimer)) end -Sync.GotSync = function(ServerID, recievedSyncID) - if ServerID == goTES3MP.GetServerID() then +-- Callback function called when a sync message is received. +---@param serverID string - The server ID of the received sync message. +---@param receivedSyncID string - The ID of the received sync message. +goTES3MPSync.gotSync = function(serverID, receivedSyncID) + if serverID == goTES3MP.GetServerID() then WaitingForSync = false end end -customEventHooks.registerValidator( - "OnServerInit", - function() - if SyncTimerID == nil then - SyncTimerID = tes3mp.CreateTimer("OnSyncUpdate", time.seconds(SyncTimer)) - tes3mp.StartTimer(SyncTimerID) - Sync.SendSync(false) - end +-- Validator function registered for the "OnServerInit" event. +-- Initializes the synchronization timer and sends the initial sync message. +customEventHooks.registerValidator("OnServerInit", function() + if syncTimerID == nil then + -- Obtain the modules from goTES3MP.GetModules(). + goTES3MPModules = goTES3MP.GetModules() + + -- Create and start the synchronization timer. + syncTimerID = tes3mp.CreateTimer("OnSyncUpdate", time.seconds(syncTimer)) + tes3mp.StartTimer(syncTimerID) + + -- Send the initial sync message. + goTES3MPSync.sendSync(false) end -) +end) -customEventHooks.registerValidator( - "OnServerExit", - function() - if SyncTimerID ~= nil then - tes3mp.StopTimer(SyncTimerID) - end +-- Validator function registered for the "OnServerExit" event. +-- Stops the synchronization timer. +customEventHooks.registerValidator("OnServerExit", function() + if syncTimerID ~= nil then + tes3mp.StopTimer(syncTimerID) end -) +end) +-- Callback function called by the synchronization timer. function OnSyncUpdate() - Sync.SendSync(false) + goTES3MPSync.sendSync(false) end -return Sync \ No newline at end of file +-- Return the goTES3MPSync table. +return goTES3MPSync \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/chat.lua b/tes3mp/scripts/custom/goTES3MP/userModules/chat.lua similarity index 61% rename from tes3mp/scripts/custom/goTES3MP/chat.lua rename to tes3mp/scripts/custom/goTES3MP/userModules/chat.lua index 9f10385..45fecde 100644 --- a/tes3mp/scripts/custom/goTES3MP/chat.lua +++ b/tes3mp/scripts/custom/goTES3MP/userModules/chat.lua @@ -1,7 +1,8 @@ local cjson = require("cjson") -local goTES3MPUtils = require("custom.goTES3MP.utils") local goTES3MPChat = {} +local goTES3MPModules = goTES3MP.GetModules() +local serverID = "" local discordServer = "" local discordChannel = "" @@ -13,33 +14,38 @@ customEventHooks.registerValidator( -- Get the default configs from goTES3MP discordServer = goTES3MP.GetDefaultDiscordServer() discordChannel = goTES3MP.GetDefaultDiscordChannel() + serverID = goTES3MP.GetServerID() tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:chat] Loaded") end ) +-- Handle player authentication event customEventHooks.registerHandler( "OnPlayerAuthentified", function(eventStatus, pid) - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - goTES3MP.GetDefaultDiscordChannel(), - goTES3MP.GetDefaultDiscordServer(), + goTES3MPModules.utils.sendDiscordMessage( + serverID, + discordChannel, + discordServer, "**" .. "[TES3MP] " .. tes3mp.GetName(pid) .. " has connected" .. "**" ) end ) +-- Handle player disconnection event customEventHooks.registerValidator( "OnPlayerDisconnect", function(eventStatus, pid) - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - goTES3MP.GetDefaultDiscordChannel(), - goTES3MP.GetDefaultDiscordServer(), + goTES3MPModules.utils.sendDiscordMessage( + serverID, + discordChannel, + discordServer, "**" .. "[TES3MP] " .. tes3mp.GetName(pid) .. " has disconnected" .. "**" ) - end) + end +) +-- Handle player chat message event customEventHooks.registerValidator( "OnPlayerSendMessage", function(eventStatus, pid, message) @@ -50,10 +56,10 @@ customEventHooks.registerValidator( if message:sub(1, 1) == "/" then return else - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - goTES3MP.GetDefaultDiscordChannel(), - goTES3MP.GetDefaultDiscordServer(), + goTES3MPModules.utils.sendDiscordMessage( + serverID, + discordChannel, + discordServer, tes3mp.GetName(pid) .. ": " .. message ) end @@ -61,26 +67,34 @@ customEventHooks.registerValidator( end ) +-- Handle player death event customEventHooks.registerValidator( "OnPlayerDeath", function(eventStatus, pid) local playerName = Players[pid].name - local deathReason = "committed suicide" + local deathReason = "" if tes3mp.DoesPlayerHavePlayerKiller(pid) then local killerPid = tes3mp.GetPlayerKillerPid(pid) - if pid ~= killerPid then deathReason = "was killed by player " .. Players[killerPid].name end + if pid ~= killerPid and Players[killerPid] ~= nil then + deathReason = "was killed by player " .. Players[killerPid].name + elseif pid == killerPid then + deathReason = "committed suicide" + end else local killerName = tes3mp.GetPlayerKillerName(pid) - if killerName ~= "" then deathReason = "was killed by " .. killerName end + if killerName ~= nil then + deathReason = "was killed by " .. killerName + else + deathReason = "was killed by an unknown entity" + end end - deathReason = playerName .. " " .. deathReason - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - goTES3MP.GetDefaultDiscordChannel(), - goTES3MP.GetDefaultDiscordServer(), + goTES3MPModules.utils.sendDiscordMessage( + serverID, + discordChannel, + discordServer, "***" .. deathReason .. "***" ) end diff --git a/tes3mp/scripts/custom/goTES3MP/userModules/crashGrabber.lua b/tes3mp/scripts/custom/goTES3MP/userModules/crashGrabber.lua new file mode 100644 index 0000000..7100df1 --- /dev/null +++ b/tes3mp/scripts/custom/goTES3MP/userModules/crashGrabber.lua @@ -0,0 +1,144 @@ +local crashGrabber = {} +local cjson = require("cjson") +local goTES3MPModules = goTES3MP.GetModules() +local discordChannel = "" + +local LogFolder = ".config/openmw" + +customEventHooks.registerValidator( + "OnServerPostInit", + function() + discordServer = goTES3MP.GetDefaultDiscordServer() + serverID = goTES3MP.GetServerID() + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:crashGrabber]: Loaded") + + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:crashGrabber]: Checking if restart was due to a script error...") + crashType, crashReason = crashGrabber.getPreviousError() + + if crashReason then + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:crashGrabber]: Previous crash was due to\n - "..crashReason) + goTES3MPModules.utils.sendDiscordMessage( + serverID, + discordChannel, + discordServer, + crashGrabber.generateCrashMessage(crashType, crashReason) + ) + else + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:crashGrabber]: No previous error found.") + end + end +) + +--- Generate a crash message +---@param crashReason string The reason for the crash +---@return string The formatted crash message +function crashGrabber.generateCrashMessage(crashType, crashReason) + return string.format("### %s\n```\n%s\n```", crashType, crashReason) +end + +--- Get the second newest file in the log folder +---@param LogFolder string The path to the log folder +---@return string The name of the second newest log file +crashGrabber.getSecondNewestFile = function(LogFolder) + local newestFile = nil + local secondNewestFile = nil + local newestTimestamp = 0 + local secondNewestTimestamp = 0 + + local command + if package.config:sub(1,1) == '\\' then + -- Windows + command = 'dir /B /O-D "' .. LogFolder .. '"' + else + -- Unix-like systems + command = 'ls -1t "' .. LogFolder .. '"' + end + + local fileHandle = io.popen(command) + local commandOutput = fileHandle:read("*a") -- Read the entire output + + if fileHandle:close() then + for file in commandOutput:gmatch("[^\r\n]+") do + local filename = file:match("tes3mp%-server%-%d%d%d%d%-%d%d%-%d%d%-%d%d_%d%d_%d%d%.log") + if filename then + local timestamp = os.time { year = filename:sub(15, 18), month = filename:sub(20, 21), day = filename:sub(23, 24), hour = filename:sub(26, 27), min = filename:sub(29, 30), sec = filename:sub(32, 33) } + + if timestamp > newestTimestamp then + secondNewestFile = newestFile + secondNewestTimestamp = newestTimestamp + newestFile = filename + newestTimestamp = timestamp + elseif timestamp > secondNewestTimestamp then + secondNewestFile = filename + secondNewestTimestamp = timestamp + end + end + end + else + print("Failed to execute command: " .. command) + end + + return secondNewestFile +end + +--- Read errors from a log file +---@param file userdata The file handle of the log file +---@return table An array of captured errors +crashGrabber.readErrorsFromLog = function(file) + local capturedErrors = {} + local capturedLines = {} + local numLinesToCapture = 5 + local lastLine = nil + local pattern = "%[(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d)%] %[(ERR)%]: .-" + + for line in file:lines() do + local timestamp, severity = line:match(pattern) + if timestamp and severity == "ERR" then + table.insert(capturedErrors, {severity = severity, line = line}) + end + + table.insert(capturedLines, line) + if #capturedLines > numLinesToCapture then + table.remove(capturedLines, 1) + end + end + + return capturedErrors, lastLine +end + +--- Get the previous error from the log files +--- @return string|nil The error type, and the corresponding information or nil if no error is found +crashGrabber.getPreviousError = function() + local errorLog = crashGrabber.getSecondNewestFile(LogFolder) + + local file = assert(io.open(LogFolder.."/"..errorLog, "r")) + local capturedErrors, lastLines = crashGrabber.readErrorsFromLog(file) + file:close() + + local filePathsFound = {} + local hasScriptError = false + + for _, errorData in ipairs(capturedErrors) do + local filePath = errorData.line:match("%.%/%a+/.+") + if filePath then + table.insert(filePathsFound, {severity = errorData.severity, line = errorData.line, filePath = filePath}) + end + + local matchedScriptError = errorData.line:match("%[ERR%]: %[Script%]: Error state: false") + if matchedScriptError then + hasScriptError = true + end + end + + if #filePathsFound > 0 then + local errorFilePath = filePathsFound[1].filePath + return "Script Error", errorFilePath + else + if lastLines == nil then + return "crashGrabber was unable to find or access the previous error reason!" + end + if not hasScriptError then + return "Server did not crash natually, Last log is below", table.concat(lastLines, "\n") + end + end +end \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/userModules/getJournal.lua b/tes3mp/scripts/custom/goTES3MP/userModules/getJournal.lua new file mode 100644 index 0000000..3fa1a07 --- /dev/null +++ b/tes3mp/scripts/custom/goTES3MP/userModules/getJournal.lua @@ -0,0 +1,58 @@ +local cjson = require("cjson") +local goTES3MPUtils = require("custom.goTES3MP.utils") + +local serverID = goTES3MP.GetServerID() +local discordServer = goTES3MP.GetDefaultDiscordServer() + +local getJournal = {} + +--- Get the journal entries for a player and send them as a message to a Discord channel +---@param playerName string The name of the player +---@param questID string The ID of the quest +---@param discordReplyChannel string The Discord channel to send the journal entries message to +---@return string The response message +getJournal.GetJournalEntries = function(playerName, questID) + local questIndexs = {} + + -- Get the player by name + local player = logicHandler.GetPlayerByName(playerName) + if player then + -- Iterate over each quest in the player's journal + for _, quest in pairs(player.data.journal) do + -- Check if the quest ID matches the provided quest ID (case-insensitive) + if string.lower(quest.quest) == string.lower(questID) then + table.insert(questIndexs, quest.index) + end + end + + -- Check if no matching quest entries were found + if #questIndexs == 0 then + return "Quest ID is invalid or player does not have this Quest." + end + + -- Sort the quest indices in alphanumeric order + questIndexs = goTES3MPUtils.alphanumsort(questIndexs) + + local questList = {} + questList[#questList + 1] = "**" .. playerName .. "'s Journal entries for " .. '"' .. string.lower(questID) .. '"' .. "**\n" + questList[#questList + 1] = "```" + + -- Concatenate the quest indices into a string + for i, index in pairs(questIndexs) do + questList[#questList + 1] = index + if i < #questIndexs then + questList[#questList + 1] = "," + end + end + + questList[#questList + 1] = "```" + + -- Send the quest list as a message to the Discord channel + return table.concat(questList) + else + -- Send a message to the Discord channel indicating that the player does not exist + return "Player does not exist." + end +end + +return getJournal \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/userModules/getPlayers.lua b/tes3mp/scripts/custom/goTES3MP/userModules/getPlayers.lua new file mode 100644 index 0000000..6809c84 --- /dev/null +++ b/tes3mp/scripts/custom/goTES3MP/userModules/getPlayers.lua @@ -0,0 +1,35 @@ +local getPlayers = {} +local goTES3MPModules = goTES3MP.GetModules() + + +--- Retrieve the list of players and send it as a message to a Discord channel +---@param discordReplyChannel string The Discord channel to send the player list message to +---@return string The player list message +getPlayers.getPlayers = function() + local playerList = "" + + -- Check if there are any players online + if tableHelper.getCount(Players) > 0 then + -- Iterate over each player + for pid, player in pairs(Players) do + -- Check if the player is logged in + if player ~= nil and player:IsLoggedIn() then + -- Add the player's name to the player list + playerList = playerList .. player.name .. "\n" + end + end + end + + -- Check if playerList is empty or has no players + if playerList == "" then + local noPlayersMessage = "**No players are currently online.**" + return noPlayersMessage + + else + -- Format the playerList with triple backticks and send it as a message to Discord + playerList = "```" .."\n".. playerList .."\n".. "```" + return playerList + end +end + +return getPlayers \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/userModules/vpnChecker.lua b/tes3mp/scripts/custom/goTES3MP/userModules/vpnChecker.lua new file mode 100644 index 0000000..2db6f52 --- /dev/null +++ b/tes3mp/scripts/custom/goTES3MP/userModules/vpnChecker.lua @@ -0,0 +1,120 @@ +local goTES3MPVPNChecker = {} +local cjson = require("cjson") +local goTES3MPModules = goTES3MP.GetModules() + +local discordChannel = "" +local discordServer = "" + +local vpnWhitelist = {} + +--- Load the VPN whitelist from a JSON file +---@return table Load the VPN whitelist +goTES3MPVPNChecker.LoadConfig = function() + vpnWhitelist = jsonInterface.load("custom/goTES3MP_VPNWhitelist.json") + + if vpnWhitelist == nil then + vpnWhitelist = {} + goTES3MPVPNChecker.SaveConfig(vpnWhitelist) + end + return vpnWhitelist +end + +--- Save the VPN whitelist to a JSON file +---@param vpnWhitelist table - Save the VPN whitelist to file. +goTES3MPVPNChecker.SaveConfig = function(vpnWhitelist) + if vpnWhitelist ~= nil then + jsonInterface.quicksave("custom/goTES3MP_VPNWhitelist.json", vpnWhitelist) + end +end + +--- Handle the whitelist-related commands +---@param pid number The player ID +---@param cmd table The command parameters +goTES3MPVPNChecker.whitelistController = function(pid, cmd) + if cmd[2] == "add" then + local username = string.lower(tableHelper.concatenateFromIndex(cmd, 3)) + vpnWhitelist[string.lower(username)] = true + goTES3MPVPNChecker.SaveConfig(vpnWhitelist) + tes3mp.SendMessage(pid, color.RebeccaPurple .."[VPN Whitelist] " .. color.Default .. "Player \""..tableHelper.concatenateFromIndex(cmd, 3).."\" was added to the whitelist\n",false) + end + + if cmd[2] == "remove" then + local username = string.lower(tableHelper.concatenateFromIndex(cmd, 3)) + vpnWhitelist[string.lower(username)] = false + goTES3MPVPNChecker.SaveConfig(vpnWhitelist) + tes3mp.SendMessage(pid, color.RebeccaPurple .."[VPN Whitelist] " .. color.Default .. "Player \""..tableHelper.concatenateFromIndex(cmd, 3).."\" was removed from the whitelist\n",false) + end + +end + +--- Kick a player who is detected using a VPN and send messages +---@param pid number - The player ID to be kicked +---@param shouldKickPlayer string - Whether to kick the player or not ("yes" or "no") +goTES3MPVPNChecker.kickPlayer = function(pid, shouldKickPlayer) + local pid = pid + local shouldKickPlayer = shouldKickPlayer + + if shouldKickPlayer == "yes" then + if tes3mp.GetName(pid) ~= nil then + + playerName = tes3mp.GetName(pid) + tes3mp.SendMessage(pid, playerName .. " was kicked for trying to use a VPN.\n", true, false) + tes3mp.Kick(pid) + + goTES3MPModules.utils.sendDiscordMessage( + goTES3MP.GetServerID(), + goTES3MP.GetDefaultDiscordChannel(), + goTES3MP.GetDefaultDiscordServer(), + "**"..playerName.." was kicked for trying to connect with a VPN.".."**" + ) + end + end +end + +customEventHooks.registerValidator( + "OnServerPostInit", + function() + -- Get the default configs from goTES3MP + discordServer = goTES3MP.GetDefaultDiscordServer() + discordChannel = goTES3MP.GetDefaultDiscordChannel() + vpnWhitelist = goTES3MPVPNChecker.LoadConfig() + tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:VPNChecker] Loaded") + end +) + +-- Send IP to goTES3MP +customEventHooks.registerHandler( + "OnPlayerConnect", + function(eventStatus, pid) + local playerName = string.lower(tes3mp.GetName(pid)) + + -- If player is whitelisted, dont run the check. + if vpnWhitelist[playerName] then + return + end + + local IP = tes3mp.GetIP(pid) + local messageJson = { + job_id = goTES3MPModules.utils.generate_uuid(), + server_id = goTES3MP.GetServerID(), + method = "VPNCheck", + source = "TES3MP", + data = { + channel = discordChannel, + server = discordServer, + message = IP, + playerpid = tostring(pid) + } + } + + local response = goTES3MPModules.utils.isJsonValidEncode(messageJson) + if response ~= nil then + IrcBridge.SendSystemMessage(response) + end + end +) + +customCommandHooks.registerCommand("whitelist", goTES3MPVPNChecker.whitelistController) +customCommandHooks.setRankRequirement("whitelist", 1) + +return goTES3MPVPNChecker \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/utils.lua b/tes3mp/scripts/custom/goTES3MP/utils.lua index 5dcd7ee..943f3fc 100644 --- a/tes3mp/scripts/custom/goTES3MP/utils.lua +++ b/tes3mp/scripts/custom/goTES3MP/utils.lua @@ -1,3 +1,4 @@ +-- Utility functions related to various operations in GoTES3MP. local goTES3MPUtils = {} local cjson = require("cjson") @@ -7,12 +8,18 @@ local charset = {} do -- [0-9a-zA-Z] for c = 97, 122 do table.insert(charset, string.char(c)) end end +-- Generates a random string of the specified length using alphanumeric characters. +---@param length number The length of the random string to generate. +---@return string The generated random string. goTES3MPUtils.randomString = function(length) if not length or length <= 0 then return '' end math.randomseed(os.clock()^5) return goTES3MPUtils.randomString(length - 1) .. charset[math.random(1, #charset)] end +-- Validates and decodes a JSON string into a Lua table. +-- @param json_str The JSON string to decode into a Lua table. +-- @return The decoded Lua table if successful, or nil if an error occurs. goTES3MPUtils.isJsonValidEncode = function(json_table) local success, result = pcall(cjson.encode, json_table); if success then @@ -22,6 +29,9 @@ goTES3MPUtils.isJsonValidEncode = function(json_table) end end +-- Validates and decodes a JSON string into a Lua table. +-- @param json_str The JSON string to decode into a Lua table. +-- @return The decoded Lua table if successful, or nil if an error occurs. goTES3MPUtils.isJsonValidDecode = function(json_str) local success, result = pcall(cjson.decode, json_str); if success then @@ -31,27 +41,39 @@ goTES3MPUtils.isJsonValidDecode = function(json_str) end end +--- Sends a message to Discord. +---@param ServerID string The ID of the server. +---@param channel string The channel to send the message to. +---@param server string server The target discord server id. +---@param message string The message to send to Discord. goTES3MPUtils.sendDiscordMessage = function(ServerID, channel, server, message) + -- Max character limit Discord allows as a single message + if string.len(message) > 2000 then + tes3mp.LogMessage(enumerations.log.WARN, "[goTES3MPUtils:sendDiscordMessage] message is over the 2000 character limit imposed by discord. Skipping") + return + end local messageJson = { + job_id = goTES3MPUtils.generate_uuid(), method = "rawDiscord", source = "TES3MP", - serverid = ServerID, - syncid = GoTES3MPSyncID, + server_id = ServerID, data = { channel = channel, server = server, - message =message + message = message } } local response = goTES3MPUtils.isJsonValidEncode(messageJson) - if response ~= nil then IrcBridge.SendSystemMessage(response) else - tes3mp.LogMessage(enumerations.log.WARN, "[goTES3MPUtils:sendDiscordMessage] failed to send message to discord.s") + tes3mp.LogMessage(enumerations.log.WARN, "[goTES3MPUtils:sendDiscordMessage] failed to send message to discord.") end end +-- Sorts a table in an alphanumeric order. +---@param o table The table to sort. +---@return table The sorted table. goTES3MPUtils.alphanumsort = function(o) function padnum(d) return ("%03d%s"):format(#d, d) @@ -62,4 +84,14 @@ goTES3MPUtils.alphanumsort = function(o) return o end +goTES3MPUtils.generate_uuid = function() + -- Set a random seed based on os.clock() + math.randomseed(os.clock()^5) + + return ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'):gsub('[xy]', function(c) + local v = c == 'x' and math.random(0, 15) or math.random(8, 11) + return ('%x'):format(v) + end) +end + return goTES3MPUtils \ No newline at end of file diff --git a/tes3mp/scripts/custom/goTES3MP/vpnChecker.lua b/tes3mp/scripts/custom/goTES3MP/vpnChecker.lua deleted file mode 100644 index ac30f8e..0000000 --- a/tes3mp/scripts/custom/goTES3MP/vpnChecker.lua +++ /dev/null @@ -1,74 +0,0 @@ -local goTES3MPVPNChecker = {} -local cjson = require("cjson") -local goTES3MPUtils = require("custom.goTES3MP.utils") - -local discordChannel = "" -local discordServer = "" - -local vpnWhitelist = { - -- ["exampleUser"] = true, -} - -customEventHooks.registerValidator( - "OnServerPostInit", - function() - -- Get the default configs from goTES3MP - discordServer = goTES3MP.GetDefaultDiscordServer() - discordChannel = goTES3MP.GetDefaultDiscordChannel() - tes3mp.LogMessage(enumerations.log.INFO, "[goTES3MP:VPNChecker] Loaded") - end -) - --- Send IP to goTES3MP -customEventHooks.registerHandler( - "OnPlayerConnect", - function(eventStatus, pid) - local playerName = string.lower(tes3mp.GetName(pid)) - - -- If player is whitelisted, dont run the check. - if vpnWhitelist[playerName] then - return - end - - local IP = tes3mp.GetIP(pid) - local messageJson = { - method = "VPNCheck", - source = "TES3MP", - serverid = goTES3MP.GetServerID(), - syncid = GoTES3MPSyncID, - data = { - channel = discordChannel, - server = discordServer, - message = IP, - playerpid = tostring(pid) - } - } - - local response = goTES3MPUtils.isJsonValidEncode(messageJson) - if response ~= nil then - IrcBridge.SendSystemMessage(response) - end - end -) - -goTES3MPVPNChecker.kickPlayer = function(pid, shouldKickPlayer) - local pid = pid - local shouldKickPlayer = shouldKickPlayer - - if shouldKickPlayer == "yes" then - if tes3mp.GetName(pid) ~= nil then - - playerName = tes3mp.GetName(pid) - tes3mp.SendMessage(pid, playerName .. " was kicked for trying to use a VPN.\n", true, false) - tes3mp.Kick(pid) - - goTES3MPUtils.sendDiscordMessage( - goTES3MP.GetServerID(), - goTES3MP.GetDefaultDiscordChannel(), - goTES3MP.GetDefaultDiscordServer(), - "**"..playerName.." was kicked for trying to connect with a VPN.".."**" - ) - end - end -end -return goTES3MPVPNChecker \ No newline at end of file