├── version.txt ├── plugins ├── clearlag.json ├── towny.json ├── fawe.json ├── dynmap.json ├── chunky.json ├── townychat.json ├── luckperms.json ├── clearlagg.json ├── worldguard.json ├── vault.json ├── worldedit.json ├── fastasyncworldedit.json ├── essentialsxspawn.json ├── essentialsxchat.json ├── floodgate.json ├── geyser.json ├── essentialsx.json ├── townyadvanced.json └── example.jsonc ├── .prettierrc ├── media └── pap.gif ├── scripts ├── gen_man.sh ├── publish.sh └── test_plugins.sh ├── internal ├── cmd │ ├── plugincmds │ │ ├── plugincmds.go │ │ ├── generatecmds │ │ │ ├── bukkit.go │ │ │ ├── modrinth.go │ │ │ ├── spigotmc.go │ │ │ └── generatecmds.go │ │ ├── uninstall.go │ │ ├── install.go │ │ └── info.go │ ├── propcmds │ │ ├── propcmds.go │ │ ├── reset.go │ │ ├── get.go │ │ └── set.go │ ├── update.go │ ├── downloadcmds │ │ ├── downloadcmds.go │ │ ├── forge.go │ │ ├── fabric.go │ │ ├── purpur.go │ │ ├── official.go │ │ └── paper.go │ ├── geyser.go │ ├── cmd.go │ ├── eula.go │ └── script.go ├── plugins │ ├── jenkins │ │ ├── structs.go │ │ └── jenkins.go │ ├── sources │ │ ├── paplug │ │ │ ├── paplug.go │ │ │ └── structs.go │ │ ├── bukkit │ │ │ ├── structs.go │ │ │ └── bukkit.go │ │ ├── modrinth │ │ │ ├── structs.go │ │ │ └── modrinth.go │ │ ├── sources.go │ │ └── spigotmc │ │ │ ├── structs.go │ │ │ └── spigot.go │ ├── download.go │ ├── deps.go │ ├── info.go │ ├── plugins.go │ └── get.go ├── time │ └── time.go ├── log │ ├── log_test.go │ ├── color │ │ ├── init_windows.go │ │ └── color.go │ └── log.go ├── net │ ├── net_test.go │ └── net.go ├── jarfiles │ ├── fabric │ │ ├── struct.go │ │ ├── api.go │ │ └── fabric.go │ ├── forge │ │ ├── forge.go │ │ ├── struct.go │ │ ├── version.go │ │ ├── forge_test.go │ │ └── api.go │ ├── purpur │ │ ├── structs.go │ │ ├── purpur.go │ │ ├── purpur_test.go │ │ └── api.go │ ├── jarfiles_test.go │ ├── official │ │ ├── structs.go │ │ ├── official.go │ │ ├── official_test.go │ │ └── api.go │ ├── paper │ │ ├── structs.go │ │ ├── paper_test.go │ │ ├── paper.go │ │ └── api.go │ └── jarfiles.go ├── global │ └── global.go ├── exec │ └── exec.go ├── fs │ ├── fs.go │ └── unzip.go ├── properties │ └── properties.go └── update │ └── update.go ├── Makefile ├── .golangci.yaml ├── .gitignore ├── go.mod ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ ├── plugintestall.yml │ ├── plugintest.yml │ ├── codeql.yml │ └── tests.yml ├── .goreleaser.yaml ├── LICENSE ├── go.sum ├── assets └── default.server.properties ├── CONTRIBUTING.md ├── PLUGINS.md ├── README.md └── main.go /version.txt: -------------------------------------------------------------------------------- 1 | 0.16.0 -------------------------------------------------------------------------------- /plugins/clearlag.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "clearlagg" 3 | } -------------------------------------------------------------------------------- /plugins/towny.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "townyadvanced" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false 4 | } -------------------------------------------------------------------------------- /plugins/fawe.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "fastasyncworldedit" 3 | } 4 | -------------------------------------------------------------------------------- /media/pap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talwat/pap/HEAD/media/pap.gif -------------------------------------------------------------------------------- /scripts/gen_man.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | help2man -h "help" -v "version" -o pap.1 ./pap -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | GOPROXY=proxy.golang.org go list -m "github.com/talwat/pap@v$1" -------------------------------------------------------------------------------- /internal/cmd/plugincmds/plugincmds.go: -------------------------------------------------------------------------------- 1 | // The commands for the plugin manager. 2 | // This includes basic logic for getting all the information to install the plugins and so on. 3 | package plugincmds 4 | -------------------------------------------------------------------------------- /internal/cmd/propcmds/propcmds.go: -------------------------------------------------------------------------------- 1 | // All the commands for the properties command. 2 | // Most of these are very basic, just doing simple input validation and then calling the properties package. 3 | package propcmds 4 | -------------------------------------------------------------------------------- /internal/cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/update" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func UpdateCommand(cCtx *cli.Context) error { 9 | update.Update() 10 | 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = $(HOME)/.local 2 | 3 | build: 4 | mkdir -vp build 5 | go build -o build 6 | 7 | install: build/pap* 8 | mkdir -p $(PREFIX)/bin 9 | mv build/pap* $(PREFIX)/bin 10 | chmod +x $(PREFIX)/bin/pap* 11 | 12 | clean: 13 | rm -rvf build -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/downloadcmds.go: -------------------------------------------------------------------------------- 1 | // These commands just get the url, download the jarfile, and checksum it. 2 | // Because not everyone uses SHA256 for checksumming, 3 | // it's safe to ignore gosec warnings about insecure hash algorithms. 4 | package downloadcmds 5 | -------------------------------------------------------------------------------- /internal/cmd/propcmds/reset.go: -------------------------------------------------------------------------------- 1 | package propcmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/properties" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func ResetPropertiesCommand(cCtx *cli.Context) error { 9 | properties.ResetProperties() 10 | 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable: 3 | - exhaustivestruct 4 | - tagliatelle 5 | - exhaustruct 6 | - nosnakecase 7 | - godox 8 | - musttag 9 | - gci 10 | - ifshort 11 | 12 | enable-all: true 13 | 14 | issues: 15 | exclude: 16 | - unused-parameter -------------------------------------------------------------------------------- /internal/plugins/jenkins/structs.go: -------------------------------------------------------------------------------- 1 | package jenkins 2 | 3 | type Artifact struct { 4 | FileName string `json:"fileName"` 5 | DisplayName string `json:"displayPath"` 6 | RelativePath string `json:"relativePath"` 7 | } 8 | 9 | type Build struct { 10 | Artifacts []Artifact `json:"artifacts"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/time/time.go: -------------------------------------------------------------------------------- 1 | // Time related utilities. 2 | package time 3 | 4 | import "time" 5 | 6 | const MinecraftTimeFormat = "Mon Jan 02 15:04:05 MST 2006" 7 | 8 | // Gets the time now in minecraft's strange format. 9 | func MinecraftDateNow() string { 10 | return time.Now().Format(MinecraftTimeFormat) 11 | } 12 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/generatecmds/bukkit.go: -------------------------------------------------------------------------------- 1 | package generatecmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/plugins/sources/bukkit" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func GenerateBukkit(cCtx *cli.Context) error { 9 | args := cCtx.Args().Slice() 10 | 11 | Generate(bukkit.GetPluginInfo, args) 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/talwat/pap/internal/log" 7 | ) 8 | 9 | func TestLog(t *testing.T) { 10 | t.Parallel() 11 | 12 | log.Log("test") 13 | log.Warn("test") 14 | log.Success("test") 15 | log.Error(nil, "test") 16 | log.RawLog("test\n") 17 | log.OutputLog("test") 18 | } 19 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/generatecmds/modrinth.go: -------------------------------------------------------------------------------- 1 | package generatecmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/plugins/sources/modrinth" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func GenerateModrinth(cCtx *cli.Context) error { 9 | args := cCtx.Args().Slice() 10 | 11 | Generate(modrinth.GetPluginInfo, args) 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/generatecmds/spigotmc.go: -------------------------------------------------------------------------------- 1 | package generatecmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/plugins/sources/spigotmc" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func GenerateSpigotMC(cCtx *cli.Context) error { 9 | args := cCtx.Args().Slice() 10 | 11 | Generate(spigotmc.GetPluginInfo, args) 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /scripts/test_plugins.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | changes=$(git diff --name-only HEAD^) 4 | filtered=() 5 | 6 | for i in $changes 7 | do 8 | if [[ $i == plugins/*.json ]] && [[ -f $i ]]; then 9 | filtered+=("$i") 10 | fi 11 | done 12 | 13 | if [[ ${#filtered[@]} == 0 ]]; then 14 | echo "nochanges" 15 | exit 0 16 | fi 17 | 18 | echo "${filtered[@]}" 19 | exit 0 -------------------------------------------------------------------------------- /internal/cmd/geyser.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // This should be purged after the 1.0 release including the entry in the main.go file. 4 | 5 | import ( 6 | "github.com/talwat/pap/internal/log" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func GeyserCommand(cCtx *cli.Context) error { 11 | log.RawError("this command has been replaced by: pap plugin install --optional geyser") 12 | 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Custom 21 | test/ 22 | dist/ 23 | build/ 24 | *.jar 25 | 26 | .DS_Store -------------------------------------------------------------------------------- /internal/plugins/sources/paplug/paplug.go: -------------------------------------------------------------------------------- 1 | // The pap plugin manager format 2 | package paplug 3 | 4 | // Check if a plugin exists in a list of plugins. 5 | func PluginExists(plugin PluginInfo, plugins []PluginInfo) bool { 6 | for _, pluginToCheck := range plugins { 7 | // Just check the name which should normally be unique. 8 | if pluginToCheck.Name == plugin.Name { 9 | return true 10 | } 11 | } 12 | 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /internal/log/color/init_windows.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | // Windows is weird, but this is supposed to enable color on the old fashioned CMD. 4 | 5 | import ( 6 | "os" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | func init() { 12 | stdout := windows.Handle(os.Stdout.Fd()) 13 | var originalMode uint32 14 | 15 | windows.GetConsoleMode(stdout, &originalMode) 16 | windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) 17 | } 18 | -------------------------------------------------------------------------------- /internal/cmd/propcmds/get.go: -------------------------------------------------------------------------------- 1 | package propcmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/log" 5 | "github.com/talwat/pap/internal/properties" 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func GetPropertyCommand(cCtx *cli.Context) error { 10 | prop := cCtx.Args().Get(0) 11 | 12 | if prop == "" { 13 | log.RawError("property name is required") 14 | } 15 | 16 | val := properties.GetProperty(prop) 17 | log.OutputLog("%s", val) 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/net/net_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/talwat/pap/internal/net" 7 | ) 8 | 9 | func TestGet(t *testing.T) { 10 | t.Parallel() 11 | 12 | type Todo struct { 13 | ID int 14 | } 15 | 16 | var todo Todo 17 | 18 | net.Get("https://jsonplaceholder.typicode.com/todos/1", "todo not found", &todo) 19 | 20 | if todo.ID != 1 { 21 | t.Errorf(`Get("https://jsonplaceholder.typicode.com/todos/1", &todo) = %d; want 1`, todo.ID) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/talwat/pap 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/talwat/gobar v1.0.2 7 | github.com/urfave/cli/v2 v2.25.1 8 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb 9 | golang.org/x/sys v0.7.0 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 14 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 15 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 16 | golang.org/x/term v0.7.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /internal/jarfiles/fabric/struct.go: -------------------------------------------------------------------------------- 1 | package fabric 2 | 3 | type MinecraftVersion struct { 4 | Version string 5 | Stable bool 6 | } 7 | 8 | type MinecraftVersions struct { 9 | Game []MinecraftVersion 10 | } 11 | 12 | type LoaderVersion struct { 13 | Separator string 14 | Version string 15 | Build uint16 16 | Stable bool 17 | } 18 | 19 | type InstallerVersion struct { 20 | Stable bool 21 | Version string 22 | } 23 | 24 | type Versions interface { 25 | ~[]struct{ Stable bool } 26 | } 27 | -------------------------------------------------------------------------------- /internal/plugins/sources/bukkit/structs.go: -------------------------------------------------------------------------------- 1 | package bukkit 2 | 3 | // https://api.curseforge.com/servermods/projects?search= 4 | // A bukkit project. 5 | // Called PluginInfo for the sake of consistency. 6 | type PluginInfo struct { 7 | Slug string 8 | ID uint32 9 | 10 | ResolvedFiles []File 11 | } 12 | 13 | // https://api.curseforge.com/servermods/files?projectIds= 14 | // A bukkit file. 15 | type File struct { 16 | DownloadURL string 17 | FileName string 18 | ReleaseType string 19 | } 20 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Package containing all CLI commands. 2 | // Usually, most unimportant logic before or after a command is here. 3 | package cmd 4 | 5 | import ( 6 | "regexp" 7 | 8 | "github.com/talwat/pap/internal/log" 9 | ) 10 | 11 | func ValidateOption(value string, pattern string, name string) { 12 | match, err := regexp.MatchString(pattern, value) 13 | log.Error(err, "an error occurred while verifying %s", name) 14 | 15 | if !match { 16 | log.RawError("%s '%s' is not valid", name, value) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: "1.20" 20 | 21 | - uses: actions/checkout@v3 22 | 23 | - uses: golangci/golangci-lint-action@v3 24 | with: 25 | version: latest 26 | -------------------------------------------------------------------------------- /internal/cmd/propcmds/set.go: -------------------------------------------------------------------------------- 1 | package propcmds 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/talwat/pap/internal/log" 7 | "github.com/talwat/pap/internal/properties" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func SetPropertyCommand(cCtx *cli.Context) error { 12 | args := cCtx.Args() 13 | prop := args.Get(0) 14 | val := args.Tail() 15 | 16 | if prop == "" { 17 | log.RawError("property name is required") 18 | } else if len(val) == 0 { 19 | log.RawError("value is required") 20 | } 21 | 22 | properties.SetProperty(prop, strings.Join(val, " ")) 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - run: git fetch --force --tags 20 | - uses: actions/setup-go@v3 21 | with: 22 | go-version: '>=1.18' 23 | cache: true 24 | 25 | - uses: goreleaser/goreleaser-action@v2 26 | with: 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /internal/jarfiles/forge/forge.go: -------------------------------------------------------------------------------- 1 | package forge 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/log" 5 | ) 6 | 7 | func GetURL(mverInput, iverInput string, useLatest bool) string { 8 | var minecraft MinecraftVersion 9 | 10 | var installer InstallerVersion 11 | 12 | if iverInput != "" { 13 | minecraft, installer = getSpecificInstaller(mverInput, iverInput) 14 | } else { 15 | minecraft, installer = getInstaller(mverInput, useLatest) 16 | } 17 | 18 | log.Log("using minecraft version %s", minecraft.String()) 19 | log.Log("using %s forge installer version %s", installer.Type, installer.Version) 20 | 21 | url := BuildURL(&minecraft, &installer) 22 | 23 | return url 24 | } 25 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/uninstall.go: -------------------------------------------------------------------------------- 1 | package plugincmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/log" 5 | "github.com/talwat/pap/internal/plugins" 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func UninstallCommand(cCtx *cli.Context) error { 10 | args := cCtx.Args().Slice() 11 | 12 | if len(args) == 0 { 13 | log.RawError("you must specify plugins to uninstall") 14 | } 15 | 16 | log.Log("fetching plugins...") 17 | 18 | info := plugins.GetManyPluginInfo(args, false, false, false) 19 | 20 | plugins.PluginList(info, "uninstalling") 21 | plugins.PluginDoMany(info, plugins.PluginUninstall) 22 | 23 | log.Success("successfully uninstalled all plugins") 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/jarfiles/purpur/structs.go: -------------------------------------------------------------------------------- 1 | package purpur 2 | 3 | // Note: Uneeded values have been omitted from the original API responses. 4 | 5 | type Errorable struct { 6 | Error string 7 | } 8 | type Versions struct { 9 | Versions []string 10 | } 11 | 12 | type Version struct { 13 | Builds BuildsList 14 | Version string 15 | 16 | Errorable 17 | } 18 | 19 | type BuildsList struct { 20 | All []string 21 | Latest string 22 | } 23 | 24 | type Commit struct { 25 | Description string 26 | Hash string 27 | Timestamp uint64 28 | } 29 | 30 | type BuildMetadata struct { 31 | Timestamp int 32 | Commits []Commit 33 | } 34 | 35 | type Build struct { 36 | Build string 37 | MD5 string 38 | 39 | Errorable 40 | BuildMetadata 41 | } 42 | -------------------------------------------------------------------------------- /internal/cmd/eula.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // The eula/sign command. 4 | // The internal naming of this is a bit confusing, but eula and sign both refer to the same command. 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/talwat/pap/internal/fs" 10 | "github.com/talwat/pap/internal/log" 11 | "github.com/talwat/pap/internal/time" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | func EulaCommand(cCtx *cli.Context) error { 16 | fs.WriteFile("eula.txt", fmt.Sprintf( 17 | `#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA). 18 | #%s 19 | #Signed by pap 20 | eula=true`, 21 | time.MinecraftDateNow(), 22 | ), fs.ReadWritePerm) 23 | log.Success("signed eula") 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go get -u ./... 7 | builds: 8 | - binary: pap 9 | goos: 10 | - darwin 11 | - windows 12 | - linux 13 | goarch: 14 | - amd64 15 | - arm64 16 | - arm 17 | - "386" 18 | goarm: 19 | - "6" 20 | - "7" 21 | env: 22 | - CGO_ENABLED=0 23 | 24 | archives: 25 | - format: binary 26 | 27 | checksum: 28 | name_template: "checksums.txt" 29 | 30 | snapshot: 31 | name_template: "{{ incpatch .Version }}-next" 32 | 33 | changelog: 34 | sort: asc 35 | filters: 36 | exclude: 37 | - "^docs:" 38 | - "^test:" 39 | -------------------------------------------------------------------------------- /internal/jarfiles/jarfiles_test.go: -------------------------------------------------------------------------------- 1 | package jarfiles_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/talwat/pap/internal/jarfiles" 8 | ) 9 | 10 | func TestFormatErrorMessage(t *testing.T) { 11 | t.Parallel() 12 | 13 | msg := jarfiles.FormatErrorMessage("This is a test message.") 14 | 15 | if msg != "this is a test message" { 16 | t.Errorf(`FormatErrorMessage("This is a test message.") = "%s"; want "this is a test message"`, msg) 17 | } 18 | } 19 | 20 | func TestAPIError(t *testing.T) { 21 | t.Parallel() 22 | 23 | jarfiles.APIError("", 0) 24 | } 25 | 26 | func TestVerifyJarfile(t *testing.T) { 27 | t.Parallel() 28 | 29 | sum := "54d626e08c1c802b305dad30b7e54a82f102390cc92c7d4db112048935236e9c" 30 | calculated, _ := hex.DecodeString(sum) 31 | jarfiles.VerifyJarfile(calculated, sum) 32 | } 33 | -------------------------------------------------------------------------------- /plugins/dynmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynmap", 3 | "version": "latest", 4 | "license": "Apache-2.0", 5 | "description": "Dynamic web maps for Minecraft servers.", 6 | "authors": ["mikeprimm"], 7 | "site": "https://github.com/webbukkit/dynmap", 8 | "downloads": [ 9 | { 10 | "type": "url", 11 | "url": "https://dev.bukkit.org/projects/dynmap/files/latest", 12 | "filename": "Dynmap.jar" 13 | } 14 | ], 15 | "install": { 16 | "type": "simple" 17 | }, 18 | "uninstall": { 19 | "files": [ 20 | { 21 | "type": "main", 22 | "path": "Dynmap.jar" 23 | }, 24 | { 25 | "type": "config", 26 | "path": "Dynmap" 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugins/chunky.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chunky", 3 | "version": "latest", 4 | "description": "Pre-generates chunks, quickly and efficiently.", 5 | "authors": ["pop4959"], 6 | "license": "GPL-3.0-only", 7 | "site": "https://github.com/pop4959/Chunky", 8 | "downloads": [ 9 | { 10 | "type": "url", 11 | "filename": "Chunky.jar", 12 | "url": "https://dev.bukkit.org/projects/chunky-pregenerator/files/latest" 13 | } 14 | ], 15 | "install": { 16 | "type": "simple" 17 | }, 18 | "uninstall": { 19 | "files": [ 20 | { 21 | "type": "main", 22 | "path": "Chunky.jar" 23 | }, 24 | { 25 | "type": "config", 26 | "path": "Chunky" 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /plugins/townychat.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "townychat", 3 | "version": "0.103", 4 | "license": "CC-BY-NC-ND-3.0", 5 | "description": "TownyChat plugin which hooks into Towny.", 6 | "authors": ["LlmDl", "phrstbrn", "Shade", "ElgarL"], 7 | "site": "https://townyadvanced.github.io/", 8 | "downloads": [ 9 | { 10 | "type": "url", 11 | "url": "https://github.com/TownyAdvanced/TownyChat/releases/download/{version}/TownyChat-{version}.jar", 12 | "filename": "TownyChat.jar" 13 | } 14 | ], 15 | "dependencies": ["townyadvanced"], 16 | "install": { 17 | "type": "simple" 18 | }, 19 | "uninstall": { 20 | "files": [ 21 | { 22 | "type": "main", 23 | "path": "TownyChat.jar" 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/jarfiles/official/structs.go: -------------------------------------------------------------------------------- 1 | package official 2 | 3 | // Note: Uneeded values have been omitted from the original API responses. 4 | 5 | type Versions struct { 6 | Latest Latest 7 | Versions []Version 8 | } 9 | 10 | type Latest struct { 11 | Release string 12 | Snapshot string 13 | } 14 | 15 | type Version struct { 16 | ID string 17 | VersionType string `json:"type"` 18 | URL string 19 | ReleaseTime string `json:"releaseTime"` 20 | } 21 | 22 | // Note: Values have been omitted from this struct from the original mojang API response. 23 | type Package struct { 24 | ID string 25 | Time string 26 | ReleaseTime string `json:"releaseTime"` 27 | Downloads Downloads 28 | } 29 | 30 | type Downloads struct { 31 | Server Download 32 | } 33 | 34 | type Download struct { 35 | SHA1 string 36 | URL string 37 | } 38 | -------------------------------------------------------------------------------- /plugins/luckperms.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luckperms", 3 | "version": "latest", 4 | "license": "MIT", 5 | "description": "A permissions plugin for Minecraft servers.", 6 | "authors": ["Luck"], 7 | "site": "https://luckperms.net/", 8 | "downloads": [ 9 | { 10 | "type": "jenkins", 11 | "job": "https://ci.lucko.me/job/LuckPerms", 12 | "artifact": "LuckPerms-Bukkit-.*.jar", 13 | "filename": "LuckPerms.jar" 14 | } 15 | ], 16 | "install": { 17 | "type": "simple" 18 | }, 19 | "uninstall": { 20 | "files": [ 21 | { 22 | "type": "main", 23 | "path": "LuckPerms.jar" 24 | }, 25 | { 26 | "type": "config", 27 | "path": "LuckPerms" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/jarfiles/paper/structs.go: -------------------------------------------------------------------------------- 1 | package paper 2 | 3 | // Note: Uneeded values have been omitted from the original API responses. 4 | 5 | // Structs that are set directly to the API response and may have an 'error' attribute. 6 | type Errorable struct { 7 | Error string 8 | } 9 | 10 | type Builds struct { 11 | Builds []Build 12 | Errorable 13 | } 14 | 15 | type BuildMetadata struct { 16 | Time string 17 | Changes []Change 18 | } 19 | 20 | type Build struct { 21 | Build uint16 22 | Channel string 23 | Downloads Downloads 24 | 25 | Errorable 26 | BuildMetadata 27 | } 28 | 29 | type Change struct { 30 | Commit string 31 | Summary string 32 | } 33 | 34 | type Downloads struct { 35 | Application Application 36 | } 37 | 38 | type Application struct { 39 | Name string 40 | Sha256 string `json:"sha256"` 41 | } 42 | 43 | type Versions struct { 44 | Versions []string 45 | } 46 | -------------------------------------------------------------------------------- /plugins/clearlagg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clearlagg", 3 | "version": "latest", 4 | "description": "Diagnose, or use one of clearlag's many utilities to reduce lag.", 5 | "license": "proprietary", 6 | "authors": [ 7 | "bob7l" 8 | ], 9 | "site": "https://dev.bukkit.org/projects/clearlagg/", 10 | "downloads": [ 11 | { 12 | "type": "url", 13 | "filename": "Clearlag.jar", 14 | "url": "https://dev.bukkit.org/projects/clearlagg/files/latest" 15 | } 16 | ], 17 | "install": { 18 | "type": "simple" 19 | }, 20 | "uninstall": { 21 | "files": [ 22 | { 23 | "type": "main", 24 | "path": "Clearlag.jar" 25 | }, 26 | { 27 | "type": "config", 28 | "path": "Clearlag" 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /internal/log/color/color.go: -------------------------------------------------------------------------------- 1 | // Just a bunch of constants for colors. 2 | package color 3 | 4 | const ( 5 | Reset = "\x1B[0m" 6 | Bold = "\x1B[1m" 7 | Dim = "\x1B[2m" 8 | Italic = "\x1B[3m" 9 | URL = "\x1B[4m" 10 | Blink = "\x1B[5m" 11 | Blink2 = "\x1B[6m" 12 | Selected = "\x1B[7m" 13 | Hidden = "\x1B[8m" 14 | Strikethrough = "\x1B[9m" 15 | 16 | Black = "\x1B[30m" 17 | Red = "\x1B[31m" 18 | Green = "\x1B[32m" 19 | Yellow = "\x1B[33m" 20 | Blue = "\x1B[34m" 21 | Magenta = "\x1B[35m" 22 | Cyan = "\x1B[36m" 23 | White = "\x1B[37m" 24 | 25 | BrightBlack = "\x1B[30;1m" 26 | BrightRed = "\x1B[31;1m" 27 | BrightGreen = "\x1B[32;1m" 28 | BrightYellow = "\x1B[33;1m" 29 | BrightBlue = "\x1B[34;1m" 30 | BrightMagenta = "\x1B[35;1m" 31 | BrightCyan = "\x1B[36;1m" 32 | BrightWhite = "\x1B[37;1m" 33 | ) 34 | -------------------------------------------------------------------------------- /internal/jarfiles/official/official.go: -------------------------------------------------------------------------------- 1 | // Interact with official mojang downloads api and verification of downloaded files. 2 | package official 3 | 4 | import ( 5 | "os" 6 | "time" 7 | 8 | "github.com/talwat/pap/internal/log" 9 | "github.com/talwat/pap/internal/log/color" 10 | ) 11 | 12 | func GetURL(versionInput string) (string, Package) { 13 | pkg := GetPackage(versionInput) 14 | 15 | if pkg.Downloads.Server.URL == "" { 16 | log.Log("%serror%s: the server URL could not be found", color.Red, color.Reset) 17 | log.Log("%serror%s: this may be because server versions below 1.2.5 are not available", color.Red, color.Reset) 18 | os.Exit(1) 19 | } 20 | 21 | time, err := time.Parse("2006-01-02T15:04:05-07:00", pkg.ReleaseTime) 22 | log.Error(err, "an error occurred while parsing date supplied by mojang api") 23 | 24 | log.Log("using %s (%s)", pkg.ID, time.Format("2006-01-02")) 25 | 26 | return pkg.Downloads.Server.URL, pkg 27 | } 28 | -------------------------------------------------------------------------------- /plugins/worldguard.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worldguard", 3 | "version": "latest", 4 | "license": "GPL-3.0", 5 | "description": "Protect your Minecraft server and lets players claim areas.", 6 | "authors": ["sk89q", "me4502", "wizjany_", "zmlaoeu"], 7 | "site": "https://enginehub.org/worldguard", 8 | "dependencies": ["worldedit"], 9 | "downloads": [ 10 | { 11 | "type": "url", 12 | "url": "https://dev.bukkit.org/projects/worldguard/files/latest", 13 | "filename": "WorldGuard.jar" 14 | } 15 | ], 16 | "install": { 17 | "type": "simple" 18 | }, 19 | "uninstall": { 20 | "files": [ 21 | { 22 | "type": "main", 23 | "path": "WorldGuard.jar" 24 | }, 25 | { 26 | "type": "config", 27 | "path": "WorldGuard" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plugins/vault.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vault", 3 | "version": "latest", 4 | "license": "LGPL-3.0", 5 | "description": "Vault is a Chat, Permissions & Economy API to allow plugins to more easily hook into these systems without needing to hook each individual system themselves.", 6 | "authors": ["cereal", "Sleaker", "mung3r"], 7 | "site": "https://github.com/MilkBowl/Vault", 8 | "downloads": [ 9 | { 10 | "type": "url", 11 | "url": "https://github.com/MilkBowl/Vault/releases/latest/download/Vault.jar", 12 | "filename": "Vault.jar" 13 | } 14 | ], 15 | "install": { 16 | "type": "simple" 17 | }, 18 | "uninstall": { 19 | "files": [ 20 | { 21 | "type": "main", 22 | "path": "Vault.jar" 23 | }, 24 | { 25 | "type": "config", 26 | "path": "Vault" 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/forge.go: -------------------------------------------------------------------------------- 1 | package downloadcmds 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/fs" 7 | "github.com/talwat/pap/internal/global" 8 | "github.com/talwat/pap/internal/jarfiles" 9 | "github.com/talwat/pap/internal/jarfiles/forge" 10 | "github.com/talwat/pap/internal/log" 11 | "github.com/talwat/pap/internal/net" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | func DownloadForgeCommand(cCtx *cli.Context) error { 16 | url := forge.GetURL( 17 | global.MinecraftVersionInput, 18 | global.ForgeInstallerVersion, 19 | global.ForgeUseLatestInstaller, 20 | ) 21 | 22 | net.Download( 23 | url, 24 | "resolved forge installer jarfile not found", 25 | fmt.Sprintf("forge-%s-%s-installer.jar", global.MinecraftVersionInput, global.ForgeInstallerVersion), 26 | "forge server installer jarfile", 27 | nil, 28 | fs.ReadWritePerm, 29 | ) 30 | 31 | log.Success("done downloading") 32 | 33 | jarfiles.UnsupportedMessage() 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /plugins/worldedit.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worldedit", 3 | "version": "latest", 4 | "license": "GPL-3.0", 5 | "description": "Minecraft map editor and mod.", 6 | "authors": [ 7 | "sk89q", 8 | "Forge_User_84940863", 9 | "me4502", 10 | "octylFractal", 11 | "wizjany_", 12 | "zmlaoeu" 13 | ], 14 | "site": "https://enginehub.org/worldedit", 15 | "downloads": [ 16 | { 17 | "type": "url", 18 | "url": "https://dev.bukkit.org/projects/worldedit/files/latest", 19 | "filename": "WorldEdit.jar" 20 | } 21 | ], 22 | "install": { 23 | "type": "simple" 24 | }, 25 | "uninstall": { 26 | "files": [ 27 | { 28 | "type": "main", 29 | "path": "WorldEdit.jar" 30 | }, 31 | { 32 | "type": "config", 33 | "path": "WorldEdit" 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/fabric.go: -------------------------------------------------------------------------------- 1 | package downloadcmds 2 | 3 | // Fabric unfortunately does not have checksums available. 4 | // If I am wrong about this, feel free to open an issue. 5 | 6 | import ( 7 | "github.com/talwat/pap/internal/fs" 8 | "github.com/talwat/pap/internal/global" 9 | "github.com/talwat/pap/internal/jarfiles" 10 | "github.com/talwat/pap/internal/jarfiles/fabric" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/net" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func DownloadFabricCommand(cCtx *cli.Context) error { 17 | url := fabric.GetURL( 18 | global.MinecraftVersionInput, 19 | global.FabricLoaderVersion, 20 | global.FabricInstallerVersion, 21 | ) 22 | 23 | net.Download( 24 | url, 25 | "resolved fabric jarfile not found", 26 | "fabric.jar", 27 | "fabric server jarfile", 28 | nil, 29 | fs.ReadWritePerm, 30 | ) 31 | 32 | log.Success("done downloading") 33 | 34 | jarfiles.UnsupportedMessage() 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/plugintestall.yml: -------------------------------------------------------------------------------- 1 | name: plugintestall 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: read 9 | 10 | jobs: 11 | plugintestall: 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest, ubuntu-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version: '1.20' 21 | 22 | - uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: "Build pap" 27 | run: | 28 | make 29 | make install PREFIX=./ 30 | 31 | - name: "Test all plugins (unix)" 32 | if: matrix.os != 'windows-latest' 33 | run: "./bin/pap -y -d plugin install $(find plugins/*.json)" 34 | 35 | - name: "Test all plugins (windows)" 36 | if: matrix.os == 'windows-latest' 37 | run: "./bin/pap -y -d plugin install $((Get-ChildItem plugins/*.json).FullName)" -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/purpur.go: -------------------------------------------------------------------------------- 1 | package downloadcmds 2 | 3 | //nolint:gosec // Not being used for security, only checksumming. Why does purpur use MD5? 4 | import ( 5 | "crypto/md5" 6 | 7 | "github.com/talwat/pap/internal/fs" 8 | "github.com/talwat/pap/internal/global" 9 | "github.com/talwat/pap/internal/jarfiles" 10 | "github.com/talwat/pap/internal/jarfiles/purpur" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/net" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func DownloadPurpurCommand(cCtx *cli.Context) error { 17 | url, build := purpur.GetURL(global.MinecraftVersionInput, global.JarBuildInput) 18 | 19 | //nolint:gosec // Not being used for security, only checksumming. Why does purpur use MD5? 20 | checksum := net.Download( 21 | url, 22 | "resolved purpur jarfile not found", 23 | "purpur.jar", 24 | "purpur jarfile", 25 | md5.New(), 26 | fs.ReadWritePerm, 27 | ) 28 | 29 | log.Success("done downloading") 30 | jarfiles.VerifyJarfile(checksum, build.MD5) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /plugins/fastasyncworldedit.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastasyncworldedit", 3 | "version": "latest", 4 | "license": "GPL-3.0", 5 | "description": "Blazingly fast world manipulation for builders, large networks and developers.", 6 | "authors": [ 7 | "Empire92", 8 | "MattBDev", 9 | "IronApollo", 10 | "dordsor21", 11 | "NotMyFault" 12 | ], 13 | "site": "https://www.spigotmc.org/resources/13932/", 14 | "downloads": [ 15 | { 16 | "type": "jenkins", 17 | "job": "https://ci.athion.net/job/FastAsyncWorldEdit/", 18 | "artifact": "FastAsyncWorldEdit-Bukkit-.*.jar", 19 | "filename": "FastAsyncWorldEdit.jar" 20 | } 21 | ], 22 | "install": { 23 | "type": "simple" 24 | }, 25 | "uninstall": { 26 | "files": [ 27 | { 28 | "type": "main", 29 | "path": "FastAsyncWorldEdit.jar" 30 | }, 31 | { 32 | "type": "config", 33 | "path": "FastAsyncWorldEdit" 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/essentialsxspawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essentialsxspawn", 3 | "version": "2.19.7", 4 | "license": "GPL-3.0", 5 | "description": "Provides spawn control features for Essentials. Requires Permissions.", 6 | "authors": [ 7 | "Zenexer", 8 | "ementalo", 9 | "Aelux", 10 | "Brettflan", 11 | "KimKandor", 12 | "snowleo", 13 | "ceulemans", 14 | "Xeology", 15 | "KHobbits", 16 | "SupaHam", 17 | "mdcfe", 18 | "DoNotSpamPls", 19 | "JRoy" 20 | ], 21 | "site": "https://essentialsx.net/", 22 | "dependencies": ["essentialsx"], 23 | "downloads": [ 24 | { 25 | "type": "url", 26 | "url": "https://github.com/EssentialsX/Essentials/releases/download/{version}/EssentialsXSpawn-{version}.jar", 27 | "filename": "EssentialsXSpawn.jar" 28 | } 29 | ], 30 | "install": { 31 | "type": "simple" 32 | }, 33 | "uninstall": { 34 | "files": [ 35 | { 36 | "type": "main", 37 | "path": "EssentialsXSpawn.jar" 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/jarfiles/purpur/purpur.go: -------------------------------------------------------------------------------- 1 | package purpur 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/talwat/pap/internal/log" 9 | ) 10 | 11 | func formatURL(version string, build Build) string { 12 | return fmt.Sprintf( 13 | "https://api.purpurmc.org/v2/purpur/%s/%s/download", 14 | version, 15 | build.Build, 16 | ) 17 | } 18 | 19 | // Returns URL to build download, and the build information. 20 | func GetURL(versionInput string, buildID string) (string, Build) { 21 | version := GetVersion(versionInput) 22 | build := GetBuild(version, buildID) 23 | 24 | log.Log("using purpur version %s", version.Version) 25 | 26 | time := time.UnixMilli(int64(build.Timestamp)) 27 | formattedTime := time.Format("2006-01-02") 28 | 29 | log.Debug("raw timestamp: %d", build.Timestamp) 30 | log.Debug("unix time: %s", time) 31 | log.Debug("formatted time: %s", formattedTime) 32 | 33 | log.Log("using purpur build %s (%s), commits:", build.Build, formattedTime) 34 | 35 | for _, commit := range build.Commits { 36 | log.RawLog(" (%s) %s\n", commit.Hash, strings.Split(commit.Description, "\n")[0]) 37 | } 38 | 39 | return formatURL(version.Version, build), build 40 | } 41 | -------------------------------------------------------------------------------- /internal/plugins/sources/modrinth/structs.go: -------------------------------------------------------------------------------- 1 | package modrinth 2 | 3 | // https://api.modrinth.com/v2/project/ 4 | 5 | // The license of a project. 6 | type License struct { 7 | ID string 8 | } 9 | 10 | // A list of links that could be used as the website. 11 | type Websites struct { 12 | IssuesURL string `json:"issues_url"` 13 | SourceURL string `json:"source_url"` 14 | WikiURL string `json:"wiki_url"` 15 | DiscordURL string `json:"discord_url"` 16 | } 17 | 18 | // Non-important metadata for a modrinth plugin. 19 | type Metadata struct { 20 | Description string 21 | License License 22 | Websites 23 | } 24 | 25 | // The modrinth plugin itself. 26 | // Uses a slug instead of a name. 27 | type PluginInfo struct { 28 | Slug string 29 | 30 | ResolvedVersion Version 31 | Versions []string 32 | Metadata 33 | } 34 | 35 | // https://api.modrinth.com/v2/version/ 36 | 37 | // A file in a version. 38 | type File struct { 39 | URL string 40 | Filename string 41 | } 42 | 43 | // A version, which has a number and a list of files to download. 44 | type Version struct { 45 | VersionNumber string `json:"version_number"` 46 | Files []File 47 | } 48 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/install.go: -------------------------------------------------------------------------------- 1 | package plugincmds 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/fs" 5 | "github.com/talwat/pap/internal/log" 6 | "github.com/talwat/pap/internal/plugins" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func InstallCommand(cCtx *cli.Context) error { 11 | args := cCtx.Args().Slice() 12 | 13 | if len(args) == 0 { 14 | log.RawError("you must specify plugins to install") 15 | } 16 | 17 | log.Debug("making plugins directory...") 18 | fs.MakeDirectory("plugins") 19 | 20 | log.Log("fetching plugins...") 21 | 22 | pluginsToInstall := plugins.GetManyPluginInfo(args, false, false, true) 23 | dependencies := plugins.ResolveDependencies(pluginsToInstall) 24 | 25 | // Append dependencies 26 | pluginsToInstall = append(pluginsToInstall, dependencies...) 27 | 28 | plugins.PluginList(pluginsToInstall, "installing") 29 | 30 | plugins.PluginDoMany(pluginsToInstall, plugins.PluginDownload) 31 | plugins.PluginDoMany(pluginsToInstall, plugins.PluginInstall) 32 | 33 | log.Success("successfully installed all plugins") 34 | 35 | // Display notes 36 | for _, plugin := range pluginsToInstall { 37 | plugins.DisplayAdditionalInfo(plugin) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /plugins/essentialsxchat.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essentialsxchat", 3 | "version": "2.19.7", 4 | "license": "GPL-3.0", 5 | "description": "Provides chat control features for Essentials. Requires Permissions.", 6 | "authors": [ 7 | "Zenexer", 8 | "ementalo", 9 | "Aelux", 10 | "Brettflan", 11 | "KimKandor", 12 | "snowleo", 13 | "ceulemans", 14 | "Xeology", 15 | "KHobbits", 16 | "md_5", 17 | "Okamosy", 18 | "Iaccidentally", 19 | "mdcfe", 20 | "JRoy", 21 | "triagonal" 22 | ], 23 | "site": "https://essentialsx.net", 24 | "dependencies": ["essentialsx"], 25 | "downloads": [ 26 | { 27 | "type": "url", 28 | "url": "https://github.com/EssentialsX/Essentials/releases/download/{version}/EssentialsXChat-{version}.jar", 29 | "filename": "EssentialsXChat.jar" 30 | } 31 | ], 32 | "install": { 33 | "type": "simple" 34 | }, 35 | "uninstall": { 36 | "files": [ 37 | { 38 | "type": "main", 39 | "path": "EssentialsXChat.jar" 40 | } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/global/global.go: -------------------------------------------------------------------------------- 1 | // Global variables, mainly values set by command line flags which are needed by the whole application. 2 | package global 3 | 4 | //nolint:gochecknoglobals 5 | var ( 6 | Version = "0.16.0" 7 | 8 | // Global options. 9 | 10 | AssumeDefaultInput = false 11 | Debug = false 12 | 13 | // Downloading Server Jarfiles. 14 | 15 | MinecraftVersionInput = "latest" 16 | JarBuildInput = "latest" 17 | PaperExperimentalBuildInput = false 18 | UseSnapshotInput = false 19 | FabricExperimentalMinecraftVersion = false 20 | FabricExperimentalLoaderVersion = false 21 | FabricLoaderVersion = "latest" 22 | FabricInstallerVersion = "latest" 23 | ForgeInstallerVersion = "latest" 24 | ForgeUseLatestInstaller = false 25 | 26 | // Script. 27 | 28 | MemoryInput = "2G" 29 | AikarsInput = false 30 | UseStdoutInput = false 31 | JarInput = "" 32 | GUIInput = false 33 | 34 | // Plugin. 35 | 36 | NoDepsInput = false 37 | InstallOptionalDepsInput = false 38 | PluginExperimentalInput = false 39 | 40 | // Update. 41 | 42 | ReinstallInput = false 43 | ) 44 | -------------------------------------------------------------------------------- /internal/plugins/download.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/talwat/pap/internal/fs" 9 | "github.com/talwat/pap/internal/log" 10 | "github.com/talwat/pap/internal/net" 11 | "github.com/talwat/pap/internal/plugins/jenkins" 12 | "github.com/talwat/pap/internal/plugins/sources/paplug" 13 | ) 14 | 15 | // Downloads a plugin. 16 | func PluginDownload(plugin paplug.PluginInfo) { 17 | for _, download := range plugin.Downloads { 18 | var url string 19 | 20 | if download.Type == "url" { 21 | url = download.URL 22 | } else if download.Type == "jenkins" { 23 | url = jenkins.GetJenkinsURL(download) 24 | } 25 | 26 | url = SubstituteProps(plugin, url) 27 | path := filepath.Join("plugins", download.Filename) 28 | 29 | net.Download( 30 | url, 31 | fmt.Sprintf("%s not found, please report this to https://github.com/talwat/pap/issues", url), 32 | path, 33 | download.Filename, 34 | nil, 35 | fs.ReadWritePerm, 36 | ) 37 | 38 | if strings.HasSuffix(path, ".zip") { 39 | log.Debug("unzipping %s...", path) 40 | fs.Unzip(path, "plugins/") 41 | 42 | log.Debug("cleaning up...") 43 | fs.DeletePath(path) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/official.go: -------------------------------------------------------------------------------- 1 | package downloadcmds 2 | 3 | //nolint:gosec // Not being used for security, only checksumming. No clue why mojang still uses SHA1. 4 | import ( 5 | "crypto/sha1" 6 | 7 | "github.com/talwat/pap/internal/fs" 8 | "github.com/talwat/pap/internal/global" 9 | "github.com/talwat/pap/internal/jarfiles" 10 | "github.com/talwat/pap/internal/jarfiles/official" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/net" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func DownloadOfficialCommand(cCtx *cli.Context) error { 17 | log.Warn("the official jarfile is much slower and less efficient than paper") 18 | log.Continue("are you sure you would like to continue?") 19 | 20 | url, pkg := official.GetURL(global.MinecraftVersionInput) 21 | 22 | //nolint:gosec // Not being used for security, only checksumming. No clue why mojang still uses SHA1. 23 | checksum := net.Download( 24 | url, 25 | "resolved official jarfile not found", 26 | "server.jar", 27 | "official server jarfile", 28 | sha1.New(), 29 | fs.ReadWritePerm, 30 | ) 31 | 32 | log.Success("done downloading") 33 | jarfiles.VerifyJarfile(checksum, pkg.Downloads.Server.SHA1) 34 | 35 | jarfiles.UnsupportedMessage() 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/jarfiles/fabric/api.go: -------------------------------------------------------------------------------- 1 | package fabric 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/log" 5 | "github.com/talwat/pap/internal/net" 6 | ) 7 | 8 | func GetMinecraftVersions() MinecraftVersions { 9 | log.Log("getting version list...") 10 | 11 | var versions MinecraftVersions 12 | 13 | net.Get( 14 | "https://meta.fabricmc.net/v2/versions", 15 | "minecraft version information not found, please report this to https://github.com/talwat/pap/issues", 16 | &versions, 17 | ) 18 | 19 | return versions 20 | } 21 | 22 | func GetLoaderVersions() []LoaderVersion { 23 | log.Log("getting loader list...") 24 | 25 | var versions []LoaderVersion 26 | 27 | net.Get( 28 | "https://meta.fabricmc.net/v2/versions/loader", 29 | "loader version information not found, please report this to https://github.com/talwat/pap/issues", 30 | &versions, 31 | ) 32 | 33 | return versions 34 | } 35 | 36 | func GetInstallerVersions() []InstallerVersion { 37 | log.Log("getting installer list...") 38 | 39 | var versions []InstallerVersion 40 | 41 | net.Get( 42 | "https://meta.fabricmc.net/v2/versions/installer/", 43 | "installer version information not found, please report this to https://github.com/talwat/pap/issues", 44 | &versions, 45 | ) 46 | 47 | return versions 48 | } 49 | -------------------------------------------------------------------------------- /plugins/floodgate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floodgate", 3 | "version": "latest", 4 | "license": "MIT", 5 | "description": "Hybrid mode plugin to allow for connections from Geyser to join online mode servers.", 6 | "authors": ["GeyserMC"], 7 | "site": "https://geysermc.org/", 8 | "dependencies": ["geyser"], 9 | "downloads": [ 10 | { 11 | "type": "jenkins", 12 | "job": "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/", 13 | "artifact": "floodgate-spigot.jar", 14 | "filename": "Floodgate.jar" 15 | } 16 | ], 17 | "install": { 18 | "type": "simple" 19 | }, 20 | "uninstall": { 21 | "files": [ 22 | { 23 | "type": "main", 24 | "path": "Floodgate.jar" 25 | }, 26 | { 27 | "type": "config", 28 | "path": "floodgate" 29 | } 30 | ] 31 | }, 32 | "note": [ 33 | "floodgate and geyser do not support key signing yet for chat messages", 34 | "this feature was introduced in 1.19.1, so you do not have to disable it if your version is below that", 35 | "to disable it run: pap properties set enforce-secure-profile false" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /plugins/geyser.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geyser", 3 | "version": "latest", 4 | "license": "MIT", 5 | "description": "A bridge/proxy allowing you to connect to Minecraft: Java Edition servers with Minecraft: Bedrock Edition.", 6 | "authors": ["GeyserMC"], 7 | "site": "https://geysermc.org/", 8 | "optionalDependencies": ["floodgate"], 9 | "downloads": [ 10 | { 11 | "type": "jenkins", 12 | "job": "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/", 13 | "artifact": "Geyser-Spigot.jar", 14 | "filename": "Geyser.jar" 15 | } 16 | ], 17 | "install": { 18 | "type": "simple" 19 | }, 20 | "uninstall": { 21 | "files": [ 22 | { 23 | "type": "main", 24 | "path": "Geyser.jar" 25 | }, 26 | { 27 | "type": "config", 28 | "path": "Geyser-Spigot" 29 | } 30 | ] 31 | }, 32 | "note": [ 33 | "floodgate and geyser do not support key signing yet for chat messages", 34 | "this feature was introduced in 1.19.1, so you do not have to disable it if your version is below that", 35 | "to disable it run: pap properties set enforce-secure-profile false" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /internal/jarfiles/purpur/purpur_test.go: -------------------------------------------------------------------------------- 1 | package purpur_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/talwat/pap/internal/jarfiles/purpur" 7 | ) 8 | 9 | func TestLatestBuild(t *testing.T) { 10 | t.Parallel() 11 | 12 | version := purpur.GetSpecificVersion("1.14.2") 13 | 14 | if version.Version != "1.14.2" { 15 | t.Errorf(`GetSpecificVersion("1.14.2") = %s; want 1.14.2`, version.Version) 16 | } 17 | 18 | build := purpur.GetLatestBuild(version) 19 | 20 | if build.Build != "126" { 21 | t.Errorf(`GetLatetstBuild(version) = %s; want 126`, build.Build) 22 | } 23 | } 24 | 25 | func TestSpecificBuild(t *testing.T) { 26 | t.Parallel() 27 | 28 | version := purpur.GetSpecificVersion("1.14.2") 29 | 30 | if version.Version != "1.14.2" { 31 | t.Errorf(`GetSpecificVersion("1.14.2") = %s; want 1.14.2`, version.Version) 32 | } 33 | 34 | build := purpur.GetSpecificBuild(version, "124") 35 | 36 | if build.Build != "124" { 37 | t.Errorf(`GetSpecificBuild("1.14.2", "124") = %s; want 124`, build.Build) 38 | } 39 | } 40 | 41 | func TestGetURL(t *testing.T) { 42 | t.Parallel() 43 | 44 | url, build := purpur.GetURL("1.14.2", "124") 45 | 46 | expected := "https://api.purpurmc.org/v2/purpur/1.14.2/124/download" 47 | 48 | if url != expected || build.Build != "124" { 49 | t.Errorf(`GetURL("1.14.2", "124") = "%s", %s; want "%s", 124`, url, build.Build, expected) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/plugins/jenkins/jenkins.go: -------------------------------------------------------------------------------- 1 | package jenkins 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | "github.com/talwat/pap/internal/plugins/sources/paplug" 10 | ) 11 | 12 | // Returns a download URL from a jenkins job. 13 | func GetJenkinsURL(download paplug.Download) string { 14 | var jenkinsBuild Build 15 | 16 | log.Debug("getting jenkins build information...") 17 | 18 | url := fmt.Sprintf("%s/lastSuccessfulBuild/api/json", download.Job) 19 | 20 | net.Get( 21 | url, 22 | "jenkins build not found, please report this to https://github.com/talwat/pap/issues", 23 | &jenkinsBuild, 24 | ) 25 | 26 | log.Debug("finding correct artifact...") 27 | 28 | for _, artifact := range jenkinsBuild.Artifacts { 29 | log.Debug("checking if %s matches %s...", artifact.FileName, download.Artifact) 30 | 31 | matched, err := regexp.MatchString(download.Artifact, artifact.FileName) 32 | log.Error(err, "an error occurred while checking if %s is the correct artifact", artifact.FileName) 33 | 34 | if matched { 35 | log.Debug("using %s", artifact.FileName) 36 | 37 | return fmt.Sprintf("%s/lastSuccessfulBuild/artifact/%s", download.Job, artifact.RelativePath) 38 | } 39 | } 40 | 41 | log.RawError("no artifacts matched, please report this to https://github.com/talwat/pap/issues") 42 | 43 | return "" 44 | } 45 | -------------------------------------------------------------------------------- /plugins/essentialsx.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essentialsx", 3 | "version": "2.19.7", 4 | "license": "GPL-3.0", 5 | "description": "The modern Essentials suite for Spigot and Paper.", 6 | "authors": [ 7 | "Zenexer", 8 | "ementalo", 9 | "Aelux", 10 | "Brettflan", 11 | "KimKandor", 12 | "snowleo", 13 | "ceulemans", 14 | "Xeology", 15 | "KHobbits", 16 | "md_5", 17 | "Iaccidentally", 18 | "drtshock", 19 | "vemacs", 20 | "SupaHam", 21 | "mdcfe", 22 | "JRoy", 23 | "pop4959" 24 | ], 25 | "site": "https://essentialsx.net", 26 | "dependencies": ["vault"], 27 | "optionalDependencies": ["essentialsxchat", "essentialsxspawn"], 28 | "downloads": [ 29 | { 30 | "type": "url", 31 | "url": "https://github.com/EssentialsX/Essentials/releases/download/{version}/EssentialsX-{version}.jar", 32 | "filename": "EssentialsX.jar" 33 | } 34 | ], 35 | "install": { 36 | "type": "simple" 37 | }, 38 | "uninstall": { 39 | "files": [ 40 | { 41 | "type": "main", 42 | "path": "EssentialsX.jar" 43 | }, 44 | { 45 | "type": "config", 46 | "path": "EssentialsX" 47 | } 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/jarfiles/paper/paper_test.go: -------------------------------------------------------------------------------- 1 | package paper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/talwat/pap/internal/jarfiles/paper" 7 | ) 8 | 9 | func TestLatestBuild(t *testing.T) { 10 | t.Parallel() 11 | 12 | build := paper.GetLatestBuild("1.12") 13 | 14 | if build.Build != 1169 { 15 | t.Errorf(`GetLatetstBuild("1.12") = %d; want 1169`, build.Build) 16 | } 17 | 18 | build = paper.GetLatestBuild("1.13") 19 | 20 | if build.Build != 173 { 21 | t.Errorf(`GetLatestBuild("1.12") = %d; want 173`, build.Build) 22 | } 23 | } 24 | 25 | func TestSpecificBuild(t *testing.T) { 26 | t.Parallel() 27 | 28 | build := paper.GetSpecificBuild("1.12", "1160") 29 | 30 | if build.Build != 1160 { 31 | t.Errorf(`GetSpecificBuild("1.12", "1160") = %d; want 1160`, build.Build) 32 | } 33 | } 34 | 35 | func TestGetBuild(t *testing.T) { 36 | t.Parallel() 37 | 38 | build := paper.GetBuild("1.12", "1160") 39 | 40 | if build.Build != 1160 { 41 | t.Errorf(`GetSpecificBuild("1.12", "1160") = %d; want 1160`, build.Build) 42 | } 43 | } 44 | 45 | func TestGetURL(t *testing.T) { 46 | t.Parallel() 47 | 48 | url, build := paper.GetURL("1.12", "1160") 49 | 50 | expected := "https://api.papermc.io/v2/projects/paper/versions/1.12/builds/1160/downloads/paper-1.12-1160.jar" 51 | 52 | if url != expected || build.Build != 1160 { 53 | t.Errorf(`GetURL("1.12", "1160") = "%s", %d; want "%s", 1160`, url, build.Build, expected) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/cmd/plugincmds/info.go: -------------------------------------------------------------------------------- 1 | package plugincmds 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/plugins" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func DisplayLineString(output *string, key string, value string) { 13 | if value == "" { 14 | return 15 | } 16 | 17 | *output += fmt.Sprintf("%s: %s\n", key, value) 18 | } 19 | 20 | func DisplayLineArray(output *string, key string, value []string) { 21 | if len(value) == 0 { 22 | return 23 | } 24 | 25 | *output += fmt.Sprintf("%s: %s\n", key, strings.Join(value, ", ")) 26 | } 27 | 28 | func InfoCommand(cCtx *cli.Context) error { 29 | args := cCtx.Args() 30 | name := args.Get(0) 31 | 32 | plugin := plugins.GetPluginInfo(name) 33 | output := "" 34 | 35 | // Not using a map because golang doesn't preserve order (annoyingly). 36 | 37 | DisplayLineString(&output, "name", plugin.Name) 38 | DisplayLineString(&output, "version", plugin.Version) 39 | DisplayLineString(&output, "site", plugin.Site) 40 | DisplayLineString(&output, "description", plugin.Description) 41 | DisplayLineString(&output, "license", plugin.License) 42 | 43 | DisplayLineArray(&output, "authors", plugin.Authors) 44 | DisplayLineArray(&output, "dependencies", plugin.Dependencies) 45 | DisplayLineArray(&output, "optional dependencies", plugin.OptionalDependencies) 46 | 47 | log.OutputLog(strings.TrimSpace(output)) 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/plugins/sources/sources.go: -------------------------------------------------------------------------------- 1 | // Utilities for various external sources of plugins (modrinth, spigotmc, and bukkit) 2 | package sources 3 | 4 | import ( 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/talwat/pap/internal/log" 9 | "github.com/talwat/pap/internal/plugins/sources/paplug" 10 | ) 11 | 12 | const Undefined = "unknown" 13 | 14 | // Gets info for many plugins & converts them to the standard pap format. 15 | func GetManyPluginInfo(names []string, getInfo func(name string) paplug.PluginInfo) []paplug.PluginInfo { 16 | infos := []paplug.PluginInfo{} 17 | 18 | for _, name := range names { 19 | log.Debug("getting info for %s...", name) 20 | infos = append(infos, getInfo(name)) 21 | } 22 | 23 | return infos 24 | } 25 | 26 | // Format a plugin name well enough so that it can be safely handled. 27 | func FormatName(name string) string { 28 | re := regexp.MustCompile("[[:^ascii:]]") 29 | newName := re.ReplaceAllLiteralString(name, "") 30 | newName = strings.ToLower(newName) 31 | 32 | log.Debug("stripped name of ascii characters: %s -> %s", name, newName) 33 | 34 | return newName 35 | } 36 | 37 | // Parses the description of a plugin to include a period (.) at the end. 38 | func FormatDesc(desc string) string { 39 | if strings.HasSuffix(desc, ".") { 40 | return desc 41 | } 42 | 43 | log.Debug("description does not have a trailing period (.), appending a period...") 44 | 45 | newDesc := desc 46 | newDesc += "." 47 | 48 | return newDesc 49 | } 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/talwat/gobar v1.0.2 h1:Uso3J+DmMrOnvbRKlY7tyynhFu1eOOIfFmc4TNfqWJs= 6 | github.com/talwat/gobar v1.0.2/go.mod h1:q0DIOURCR185OU2xwxTnee7KQehke7Sz58q6dhGNamo= 7 | github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= 8 | github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 9 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 10 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 11 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb h1:rhjz/8Mbfa8xROFiH+MQphmAmgqRM0bOMnytznhWEXk= 12 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 13 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 14 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= 16 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 17 | -------------------------------------------------------------------------------- /internal/jarfiles/official/official_test.go: -------------------------------------------------------------------------------- 1 | package official_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/talwat/pap/internal/jarfiles/official" 7 | ) 8 | 9 | func TestGetURL(t *testing.T) { 10 | t.Parallel() 11 | 12 | url, pkg := official.GetURL("1.12") 13 | 14 | expected := "https://launcher.mojang.com/v1/objects/8494e844e911ea0d63878f64da9dcc21f53a3463/server.jar" 15 | expectedSha1 := "8494e844e911ea0d63878f64da9dcc21f53a3463" 16 | 17 | if url != expected { 18 | t.Errorf(`GetURL("1.12") = "%s"; want "%s"`, url, expected) 19 | } else if pkg.Downloads.Server.SHA1 != expectedSha1 { 20 | t.Errorf(`GetURL("1.12").sha1 = "%s"; want "%s"`, pkg.Downloads.Server.SHA1, expectedSha1) 21 | } 22 | } 23 | 24 | func TestGetPackage(t *testing.T) { 25 | t.Parallel() 26 | 27 | pkg := official.GetPackage("1.12") 28 | 29 | if expected := "1.12"; pkg.ID != expected { 30 | t.Errorf(`GetPackage("1.12") = "%s"; want "%s"`, pkg.ID, expected) 31 | } 32 | } 33 | 34 | func TestLocateVersion(t *testing.T) { 35 | t.Parallel() 36 | 37 | versions := official.GetVersionManifest() 38 | 39 | last := versions.Versions[len(versions.Versions)-1].ID 40 | 41 | if last != "rd-132211" { 42 | t.Errorf(`GetVersionManifest() = [..., %s]; want [..., %s]`, last, "rd-132211") 43 | } 44 | 45 | versionInfo := official.FindVersion(versions, "1.14") 46 | 47 | if versionInfo.ID != "1.14" { 48 | t.Errorf(`FindVersion(versions, "1.14") = "%s"; want "%s"`, versionInfo.ID, "1.14") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/plugintest.yml: -------------------------------------------------------------------------------- 1 | name: plugintest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | plugintest: 14 | strategy: 15 | matrix: 16 | os: [macos-latest, windows-latest, ubuntu-latest] 17 | 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: "Get diff" 25 | shell: bash 26 | run: echo "CHANGES=$(./scripts/test_plugins.sh)" >> $GITHUB_ENV 27 | 28 | - uses: actions/setup-go@v3 29 | if: ${{ env.CHANGES != 'nochanges' }} 30 | with: 31 | go-version: '1.20' 32 | 33 | - name: "Build pap" 34 | if: ${{ env.CHANGES != 'nochanges' }} 35 | run: | 36 | make 37 | make install PREFIX=./ 38 | 39 | - name: "Test changed plugins: ${{ env.CHANGES }} (windows)" 40 | if: ${{ (env.CHANGES != 'nochanges') && (matrix.os == 'windows-latest') }} 41 | run: | 42 | echo $env:CHANGES 43 | $ChangesFinal = $env:CHANGES.Split(" ") 44 | ./bin/pap -y -d plugin install @ChangesFinal 45 | 46 | - name: "Test changed plugins: ${{ env.CHANGES }} (unix)" 47 | if: ${{ (env.CHANGES != 'nochanges') && (matrix.os != 'windows-latest') }} 48 | run: | 49 | echo $CHANGES 50 | ./bin/pap -y -d plugin install $CHANGES -------------------------------------------------------------------------------- /internal/cmd/plugincmds/generatecmds/generatecmds.go: -------------------------------------------------------------------------------- 1 | // pap plugin generate definitions. 2 | // Pretty simple, most of them just call the Generate() function and that's it. 3 | package generatecmds 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/talwat/pap/internal/fs" 10 | "github.com/talwat/pap/internal/global" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/plugins/sources" 13 | "github.com/talwat/pap/internal/plugins/sources/paplug" 14 | ) 15 | 16 | // Generates and writes the converted plugin. 17 | func Generate(getPluginInfo func(plugin string) paplug.PluginInfo, plugins []string) { 18 | if len(plugins) == 0 { 19 | log.RawError("you must specify plugins to generate") 20 | } 21 | 22 | log.Log("getting plugins to write...") 23 | 24 | pluginsToWrite := sources.GetManyPluginInfo(plugins, getPluginInfo) 25 | 26 | for _, plugin := range pluginsToWrite { 27 | WritePlugin(plugin) 28 | } 29 | 30 | log.Success("all plugins generated successfully!") 31 | } 32 | 33 | func WritePlugin(plugin paplug.PluginInfo) { 34 | unmarshaled, err := json.MarshalIndent(plugin, "", " ") 35 | log.Error(err, "an error occurred while converting plugin back into json") 36 | 37 | if global.UseStdoutInput { 38 | log.OutputLog(string(unmarshaled)) 39 | } else { 40 | log.Log("writing %s...", plugin.Name) 41 | fs.WriteFileByte(fmt.Sprintf("%s.json", plugin.Name), unmarshaled, fs.ReadWritePerm) 42 | } 43 | 44 | log.Success("generated %s!", plugin.Name) 45 | } 46 | -------------------------------------------------------------------------------- /assets/default.server.properties: -------------------------------------------------------------------------------- 1 | #Minecraft server properties 2 | #Time 3 | enable-jmx-monitoring=false 4 | rcon.port=25575 5 | level-seed= 6 | gamemode=survival 7 | enable-command-block=false 8 | enable-query=false 9 | generator-settings={} 10 | enforce-secure-profile=true 11 | level-name=world 12 | motd=A Minecraft Server 13 | query.port=25565 14 | pvp=true 15 | generate-structures=true 16 | max-chained-neighbor-updates=1000000 17 | difficulty=easy 18 | network-compression-threshold=256 19 | max-tick-time=60000 20 | require-resource-pack=false 21 | use-native-transport=true 22 | max-players=20 23 | online-mode=true 24 | enable-status=true 25 | allow-flight=false 26 | initial-disabled-packs= 27 | broadcast-rcon-to-ops=true 28 | view-distance=10 29 | server-ip= 30 | resource-pack-prompt= 31 | allow-nether=true 32 | server-port=25565 33 | enable-rcon=false 34 | sync-chunk-writes=true 35 | op-permission-level=4 36 | prevent-proxy-connections=false 37 | hide-online-players=false 38 | resource-pack= 39 | entity-broadcast-range-percentage=100 40 | simulation-distance=10 41 | rcon.password= 42 | player-idle-timeout=0 43 | force-gamemode=false 44 | rate-limit=0 45 | hardcore=false 46 | white-list=false 47 | broadcast-console-to-ops=true 48 | spawn-npcs=true 49 | spawn-animals=true 50 | function-permission-level=2 51 | initial-enabled-packs=vanilla 52 | level-type=minecraft\:normal 53 | text-filtering-config= 54 | spawn-monsters=true 55 | enforce-whitelist=false 56 | spawn-protection=16 57 | resource-pack-sha1= 58 | max-world-size=29999984 -------------------------------------------------------------------------------- /internal/cmd/downloadcmds/paper.go: -------------------------------------------------------------------------------- 1 | package downloadcmds 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "github.com/talwat/pap/internal/cmd" 7 | "github.com/talwat/pap/internal/fs" 8 | "github.com/talwat/pap/internal/global" 9 | "github.com/talwat/pap/internal/jarfiles" 10 | "github.com/talwat/pap/internal/jarfiles/paper" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/net" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | // This will probably have some kind of issue eventually with a valid version being mistaken as invalid. 17 | // But it does help of quickly giving the user feedback that they made a typo in the version number. 18 | func validatePaperOptions() { 19 | const latest = "latest" 20 | 21 | if global.MinecraftVersionInput != latest { 22 | cmd.ValidateOption(global.MinecraftVersionInput, `^\d\.\d{1,2}(\.\d)?(-pre\d|-SNAPSHOT\d)?$`, "version") 23 | } 24 | 25 | if global.JarBuildInput != latest { 26 | cmd.ValidateOption(global.JarBuildInput, `^\d+$`, "build") 27 | } 28 | } 29 | 30 | func DownloadPaperCommand(cCtx *cli.Context) error { 31 | validatePaperOptions() 32 | 33 | url, build := paper.GetURL(global.MinecraftVersionInput, global.JarBuildInput) 34 | 35 | checksum := net.Download( 36 | url, 37 | "resolved paper jarfile not found", 38 | "paper.jar", 39 | "paper jarfile", 40 | sha256.New(), 41 | fs.ReadWritePerm, 42 | ) 43 | 44 | log.Success("done downloading") 45 | jarfiles.VerifyJarfile(checksum, build.Downloads.Application.Sha256) 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/jarfiles/jarfiles.go: -------------------------------------------------------------------------------- 1 | // Useful methods for downloading jarfiles 2 | package jarfiles 3 | 4 | import ( 5 | "encoding/hex" 6 | "strings" 7 | 8 | "github.com/talwat/pap/internal/log" 9 | ) 10 | 11 | const Latest = "latest" 12 | 13 | func UnsupportedMessage() { 14 | log.Warn("because you are using a jarfile which is not by papermc, please do not use 'pap script' with --aikars") 15 | log.Warn("additionally, plugins from the plugin manager will not work properly") 16 | } 17 | 18 | // Verifies a jarfile by using it's calculated checksum from the net.Download function. 19 | // If compares the hex encoded checksum with `proper`. 20 | func VerifyJarfile(calculated []byte, proper string) { 21 | log.Log("verifying downloaded jarfile...") 22 | 23 | if checksum := hex.EncodeToString(calculated); checksum == proper { 24 | log.Debug("%s == %s", checksum, proper) 25 | log.Success("checksums match!") 26 | } else { 27 | log.RawError( 28 | "checksums (calculated: %s, proper: %s) don't match!", 29 | checksum, 30 | proper, 31 | ) 32 | } 33 | } 34 | 35 | // An API error. If the `err` string isn't empty (undefined), then it will spit out an error. 36 | func APIError(err string, statusCode int) { 37 | if err != "" { 38 | log.RawError("api returned an error with status code %d: %s", statusCode, FormatErrorMessage(err)) 39 | } 40 | } 41 | 42 | // Format API errors to comply with pap's log guidelines 43 | // https://github.com/talwat/pap/blob/main/CONTRIBUTING.md 44 | func FormatErrorMessage(msg string) string { 45 | return strings.ToLower(strings.TrimSuffix(msg, ".")) 46 | } 47 | -------------------------------------------------------------------------------- /plugins/townyadvanced.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "townyadvanced", 3 | "version": "0.99.0.0", 4 | "license": "CC-BY-NC-ND-3.0", 5 | "description": "Resident-Town-Nation hierarchy combined with a grid based protection system, many features and an expansive API.", 6 | "authors": ["LlmDl", "Warrior", "Shade", "ElgarL"], 7 | "site": "https://townyadvanced.github.io/", 8 | "downloads": [ 9 | { 10 | "type": "url", 11 | "url": "https://github.com/TownyAdvanced/Towny/releases/download/{version}/Towny.Advanced.{version}.zip", 12 | "filename": "Towny.Advanced.zip" 13 | } 14 | ], 15 | "dependencies": ["townychat"], 16 | "install": { 17 | "type": "custom", 18 | "commands": { 19 | "windows": [ 20 | "Move-Item -Force -Path 'TownyAdvanced {version}/Towny-{version}.jar' -Destination ./", 21 | "Remove-Item -Recurse -Force 'TownyAdvanced {version}'", 22 | "Rename-Item -Force -Path Towny-{version}.jar -NewName Towny.jar" 23 | ], 24 | "unix": [ 25 | "mv -v 'TownyAdvanced {version}'/Towny-{version}.jar Towny.jar", 26 | "rm -rvf 'TownyAdvanced {version}'" 27 | ] 28 | } 29 | }, 30 | "uninstall": { 31 | "files": [ 32 | { 33 | "type": "main", 34 | "path": "Towny.jar" 35 | }, 36 | { 37 | "type": "config", 38 | "path": "Towny" 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/jarfiles/forge/struct.go: -------------------------------------------------------------------------------- 1 | package forge 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Forge Minecraft versions use a faux semantic versioning scheme (most of the time). 8 | // Major, minor and patch correspond to similar ones in semantic versioning: X.x.p. 9 | // Prerelease versions contain a _preX suffix, such as 1.7.10_pre4. 10 | type MinecraftVersion struct { 11 | Major int 12 | Minor int 13 | Patch int 14 | 15 | IsPrerelease bool 16 | PrereleaseVersion int 17 | } 18 | 19 | type ByVersion []MinecraftVersion 20 | 21 | func (a ByVersion) Len() int { return len(a) } 22 | 23 | //nolint:varnamelen 24 | func (a ByVersion) Less(i, j int) bool { 25 | switch { 26 | case a[i].Major < a[j].Major: 27 | return true 28 | case a[i].Minor < a[j].Minor: 29 | return true 30 | case a[i].Patch == a[j].Patch: 31 | return !a[i].IsPrerelease 32 | case a[i].Patch < a[j].Patch: 33 | if a[i].Minor > a[j].Minor { 34 | return false 35 | } 36 | 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | func (a ByVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 43 | 44 | func (minecraft *MinecraftVersion) String() string { 45 | builder := fmt.Sprintf("%d.%d", minecraft.Major, minecraft.Minor) 46 | 47 | if minecraft.Patch != 0 { 48 | builder += fmt.Sprintf(".%d", minecraft.Patch) 49 | } 50 | 51 | if minecraft.IsPrerelease { 52 | builder += fmt.Sprintf("_pre%d", minecraft.PrereleaseVersion) 53 | } 54 | 55 | return builder 56 | } 57 | 58 | type InstallerVersion struct { 59 | Version string 60 | Type string 61 | } 62 | 63 | type PromotionsSlim struct { 64 | Promos map[string]string `json:"promos"` 65 | } 66 | -------------------------------------------------------------------------------- /internal/jarfiles/paper/paper.go: -------------------------------------------------------------------------------- 1 | // Interact with papermc downloads api and verification of downloaded files. 2 | package paper 3 | 4 | // This is the only file which is accessed from other packages. 5 | // If you would like to add compatibility for other jarfile types, you only need a GetURL() function. 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/talwat/pap/internal/log" 12 | ) 13 | 14 | func formatURL(version string, build Build) string { 15 | return fmt.Sprintf( 16 | "https://api.papermc.io/v2/projects/paper/versions/%s/builds/%d/downloads/paper-%s-%d.jar", 17 | version, 18 | build.Build, 19 | version, 20 | build.Build, 21 | ) 22 | } 23 | 24 | // Returns URL to build download, and the build information. 25 | func GetURL(versionInput string, buildID string) (string, Build) { 26 | version := GetVersion(versionInput) 27 | build := GetBuild(version, buildID) 28 | 29 | log.Log("using paper version %s", version) 30 | 31 | // Format time 32 | log.Debug("raw time: %s", build.Time) 33 | 34 | time, err := time.Parse(time.RFC3339, build.Time) 35 | log.Error(err, "an error occurred while parsing date supplied by papermc api") 36 | 37 | formattedTime := time.Format("2006-01-02") 38 | 39 | log.Debug("formatted time: %s", formattedTime) 40 | 41 | // Log final info 42 | log.Log("using paper build %d (%s), changes:", build.Build, formattedTime) 43 | 44 | if len(build.Changes) == 0 { 45 | log.RawLog(" no changes") 46 | } else { 47 | for _, change := range build.Changes { 48 | log.RawLog(" (%s) %s\n", change.Commit, change.Summary) 49 | } 50 | } 51 | 52 | return formatURL(version, build), build 53 | } 54 | -------------------------------------------------------------------------------- /internal/plugins/sources/spigotmc/structs.go: -------------------------------------------------------------------------------- 1 | package spigotmc 2 | 3 | // https://api.spiget.org/v2/authors/ 4 | // The author of a plugin, only the name is needed. 5 | type ResolvedAuthor struct { 6 | Name string 7 | } 8 | 9 | // https://api.spiget.org/v2/resources//versions/ 10 | // The latest version of a plugin. 11 | type ResolvedLatestVersion struct { 12 | Name string 13 | } 14 | 15 | // Resolved information, like the author and latest version. 16 | // Resolved meaning it is in a separate endpoint. 17 | type Resolved struct { 18 | Author ResolvedAuthor 19 | LatestVersion ResolvedLatestVersion 20 | } 21 | 22 | // Main struct 23 | 24 | // The author of a plugin, used if contributors is undefined. 25 | type Author struct { 26 | ID int 27 | } 28 | 29 | // A file provided by the plugin. 30 | type File struct { 31 | FileType string `json:"type"` 32 | URL string 33 | } 34 | 35 | // Websites that are used to get a plugins website. 36 | type Links struct { 37 | SourceCodeLink string 38 | DonationLink string 39 | } 40 | 41 | // A version with an ID, pretty basic. 42 | type Version struct { 43 | ID uint32 44 | } 45 | 46 | // Non-important metadata for a plugin. 47 | type Metadata struct { 48 | Contributors string 49 | Tag string 50 | Likes int 51 | Author Author 52 | 53 | Links 54 | } 55 | 56 | // Information used to check if a plugin is able to be downloaded. 57 | type DownloadInfo struct { 58 | Premium bool 59 | File File 60 | } 61 | 62 | // The plugin information itself. 63 | type PluginInfo struct { 64 | Name string 65 | Version Version 66 | ID uint32 67 | 68 | Metadata 69 | DownloadInfo 70 | Resolved Resolved 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Some of these rules will be caught automatically by golangci-lint, but others will not. 4 | 5 | ## Variable & Function names 6 | 7 | Please use descriptive names, but not names that are extremely long. 8 | 9 | Single letter variables are only okay if it's extremely obvious like `i` as the index, otherwise please use 10 | longer names, abbreviations like `pkg` are okay. 11 | 12 | ## Functions 13 | 14 | Try to seperate different parts of a function into their own seperate functions if they get too long or are utilized somewhere else. 15 | 16 | Or, if you have several pieces of code that do a similar thing, put them into their own file. 17 | 18 | For example, functions which get information from the PaperMC api are in `paper/api.go`. 19 | 20 | ## Adding support for other jarfile types 21 | 22 | If you wish to do this, make sure you include a `GetURL` function, and beyond that it's up to you. 23 | 24 | Please try and include a few unit tests aswell. 25 | 26 | Additionally, you must get the jarfile and all information directly from **official sources**. 27 | 28 | ## Logging 29 | 30 | Log major steps in an action. You can use the functions in the `log` package to do this. 31 | 32 | It's a bad idea to have long periods of time with no logs, because this gives off the impression that nothing is happening, 33 | and it's better to be transparent about what the program is doing when. 34 | 35 | Logs should not end with a `.` and must always begin with `pap:` which is automatically done by the `log` package. 36 | 37 | Logs should also be preferably all lowercase. 38 | 39 | The only exeption is for `...`. 40 | 41 | ## Styling 42 | 43 | Just use [gofumpt](https://github.com/mvdan/gofumpt). 44 | 45 | ## Commits 46 | 47 | Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for all commits. 48 | -------------------------------------------------------------------------------- /internal/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "os/exec" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/talwat/pap/internal/global" 11 | "github.com/talwat/pap/internal/log" 12 | ) 13 | 14 | // Check if a binary exists on the system, for example, `ls`. 15 | func CommandExists(cmd string) bool { 16 | _, err := exec.LookPath(cmd) 17 | 18 | return !errors.Is(err, exec.ErrNotFound) 19 | } 20 | 21 | // Runs a command and uses a bunch of dots after the log to display progress. 22 | // Whenever the command outputs something to stdout or stderr, it will output a '.'. 23 | // So it would look something like: 'pap: running command go build...........'. 24 | func Run(workDir string, cmd string) int { 25 | log.NoNewline("running command %s", cmd) 26 | 27 | var cmdObj *exec.Cmd 28 | 29 | if runtime.GOOS == "windows" { 30 | log.NewlineBeforeDebug("using powershell") 31 | 32 | cmdObj = exec.Command("powershell", "-command", cmd) 33 | } else { 34 | log.NewlineBeforeDebug("using sh") 35 | 36 | cmdObj = exec.Command("sh", "-c", cmd) 37 | } 38 | 39 | log.Debug("using working directory %s", workDir) 40 | cmdObj.Dir = workDir 41 | 42 | cmdReader, err := cmdObj.StdoutPipe() 43 | cmdObj.Stderr = cmdObj.Stdout 44 | 45 | log.NewlineBeforeError(err, "an error occurred while creating stdout pipe") 46 | 47 | err = cmdObj.Start() 48 | log.NewlineBeforeError(err, "an error occurred while starting command") 49 | 50 | output := "" 51 | scanner := bufio.NewScanner(cmdReader) 52 | 53 | for scanner.Scan() { 54 | output += scanner.Text() 55 | 56 | if global.Debug { 57 | log.RawLog(" %s\n", output) 58 | } else { 59 | log.RawLog(".") 60 | } 61 | } 62 | 63 | output = strings.TrimSpace(output) 64 | err = cmdObj.Wait() 65 | 66 | log.NewlineBeforeError(err, "an error occurred while running command. output:\n%s", output) 67 | 68 | return cmdObj.ProcessState.ExitCode() 69 | } 70 | -------------------------------------------------------------------------------- /internal/jarfiles/official/api.go: -------------------------------------------------------------------------------- 1 | package official 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/global" 7 | "github.com/talwat/pap/internal/jarfiles" 8 | "github.com/talwat/pap/internal/log" 9 | "github.com/talwat/pap/internal/net" 10 | ) 11 | 12 | // Finds the version in a list of versions. 13 | func FindVersion(versions Versions, version string) Version { 14 | for i := range versions.Versions { 15 | if versions.Versions[i].ID == version { 16 | return versions.Versions[i] 17 | } 18 | } 19 | 20 | log.RawError("version %s does not exist", version) 21 | 22 | //nolint:exhaustruct // The process will be terminated by log.RawError before this ever gets run. 23 | return Version{} 24 | } 25 | 26 | func GetVersionManifest() Versions { 27 | log.Log("getting version manifest...") 28 | 29 | var versions Versions 30 | 31 | net.Get( 32 | "https://launchermeta.mojang.com/mc/game/version_manifest.json", 33 | "version manifest not found, please report this to https://github.com/talwat/pap/issues", 34 | &versions, 35 | ) 36 | 37 | return versions 38 | } 39 | 40 | func GetSpecificPackage(version string) Package { 41 | versions := GetVersionManifest() 42 | 43 | log.Log("locating version %s...", version) 44 | versionInfo := FindVersion(versions, version) 45 | 46 | log.Log("getting package for %s...", version) 47 | 48 | var pkg Package 49 | 50 | net.Get(versionInfo.URL, fmt.Sprintf("package %s not found", version), &pkg) 51 | 52 | return pkg 53 | } 54 | 55 | func GetLatestPackage() Package { 56 | versions := GetVersionManifest() 57 | 58 | var version string 59 | 60 | if global.UseSnapshotInput { 61 | version = versions.Latest.Snapshot 62 | } else { 63 | version = versions.Latest.Release 64 | } 65 | 66 | log.Log("locating version %s...", version) 67 | versionInfo := FindVersion(versions, version) 68 | 69 | log.Log("getting package for %s...", version) 70 | 71 | var pkg Package 72 | 73 | net.Get(versionInfo.URL, "latest package not found", &pkg) 74 | 75 | return pkg 76 | } 77 | 78 | func GetPackage(versionInput string) Package { 79 | if versionInput == jarfiles.Latest { 80 | return GetLatestPackage() 81 | } 82 | 83 | return GetSpecificPackage(versionInput) 84 | } 85 | -------------------------------------------------------------------------------- /internal/plugins/deps.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "github.com/talwat/pap/internal/global" 5 | "github.com/talwat/pap/internal/log" 6 | "github.com/talwat/pap/internal/plugins/sources/paplug" 7 | ) 8 | 9 | // Recursive function. 10 | // Gets a plugins dependencies and then calls itself to get that dependencies own dependencies. 11 | // This happens until it's done. 12 | // 13 | // dest is where to write the plugins to. 14 | func getDependencyLevel(deps []string, dest *[]paplug.PluginInfo, installed []paplug.PluginInfo, isOptional bool) { 15 | depsInfo := GetManyPluginInfo(deps, !isOptional, isOptional, true) 16 | 17 | for _, dep := range depsInfo { 18 | log.Debug("checking if %s already marked for installation...", dep.Name) 19 | 20 | if paplug.PluginExists(dep, installed) { 21 | return 22 | } 23 | 24 | *dest = append(*dest, dep) 25 | 26 | log.Debug("checking if %s has subdependencies...", dep.Name) 27 | 28 | if len(dep.Dependencies) != 0 { 29 | getDependencyLevel(dep.Dependencies, dest, installed, isOptional) 30 | } 31 | } 32 | } 33 | 34 | // Gets the dependencies for one plugin. 35 | // This avoids using dependencies that are already specified for installation. 36 | func getDependencies(deps []string, installed []paplug.PluginInfo, isOptional bool) []paplug.PluginInfo { 37 | finalDeps := []paplug.PluginInfo{} 38 | 39 | getDependencyLevel(deps, &finalDeps, installed, isOptional) 40 | 41 | return finalDeps 42 | } 43 | 44 | // Resolves all of the dependencies for a list of plugins. 45 | func ResolveDependencies(plugins []paplug.PluginInfo) []paplug.PluginInfo { 46 | deps := []paplug.PluginInfo{} 47 | 48 | if global.NoDepsInput { 49 | log.Debug("nodeps on, not resolving dependencies") 50 | 51 | return deps 52 | } 53 | 54 | log.Log("resolving dependencies...") 55 | 56 | for _, plugin := range plugins { 57 | log.Debug("resolving for %s...", plugin.Name) 58 | 59 | deps = append(deps, getDependencies(plugin.Dependencies, plugins, false)...) 60 | 61 | if !global.InstallOptionalDepsInput { 62 | continue 63 | } 64 | 65 | log.Debug("appending optional dependencies: %s...", plugin.OptionalDependencies) 66 | 67 | deps = append(deps, getDependencies(plugin.OptionalDependencies, deps, true)...) 68 | } 69 | 70 | return deps 71 | } 72 | -------------------------------------------------------------------------------- /internal/jarfiles/fabric/fabric.go: -------------------------------------------------------------------------------- 1 | package fabric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/global" 7 | "github.com/talwat/pap/internal/jarfiles" 8 | "github.com/talwat/pap/internal/log" 9 | ) 10 | 11 | func getLatestVersion(versions MinecraftVersions) MinecraftVersion { 12 | latest := versions.Game[0] 13 | 14 | if global.UseSnapshotInput { 15 | log.Debug("using experimental/snapshot minecraft version (%s) regardless", latest.Version) 16 | 17 | return latest 18 | } 19 | 20 | for _, version := range versions.Game { 21 | if version.Stable { 22 | return version 23 | } 24 | } 25 | 26 | return latest 27 | } 28 | 29 | func getLatestLoader(versions []LoaderVersion) LoaderVersion { 30 | latest := versions[0] 31 | 32 | if global.FabricExperimentalLoaderVersion { 33 | log.Debug("using experimental fabric loader version (%s) regardless", latest.Version) 34 | 35 | return latest 36 | } 37 | 38 | for _, version := range versions { 39 | if version.Stable { 40 | return version 41 | } 42 | } 43 | 44 | return latest 45 | } 46 | 47 | func getLatestInstaller(versions []InstallerVersion) InstallerVersion { 48 | latest := versions[0] 49 | 50 | for _, version := range versions { 51 | if version.Stable { 52 | return version 53 | } 54 | } 55 | 56 | return latest 57 | } 58 | 59 | func GetURL(versionInput string, loaderInput string, installerInput string) string { 60 | version := versionInput 61 | loader := loaderInput 62 | installer := installerInput 63 | 64 | // A bit of repetitive code, but I am not willing to use generics to do this. 65 | if version == jarfiles.Latest { 66 | versions := GetMinecraftVersions() 67 | version = getLatestVersion(versions).Version 68 | } 69 | 70 | if loader == jarfiles.Latest { 71 | versions := GetLoaderVersions() 72 | loader = getLatestLoader(versions).Version 73 | } 74 | 75 | if installer == jarfiles.Latest { 76 | versions := GetInstallerVersions() 77 | installer = getLatestInstaller(versions).Version 78 | } 79 | 80 | log.Log("using minecraft version %s", version) 81 | log.Log("using fabric loader version %s", loader) 82 | log.Log("using fabric installer version %s", installer) 83 | 84 | return fmt.Sprintf("https://meta.fabricmc.net/v2/versions/loader/%s/%s/%s/server/jar", version, loader, installer) 85 | } 86 | -------------------------------------------------------------------------------- /internal/plugins/info.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/talwat/pap/internal/global" 8 | "github.com/talwat/pap/internal/log" 9 | "github.com/talwat/pap/internal/log/color" 10 | "github.com/talwat/pap/internal/plugins/sources/paplug" 11 | ) 12 | 13 | func DisplayNote(plugin paplug.PluginInfo) { 14 | if len(plugin.Note) == 0 { 15 | return 16 | } 17 | 18 | log.Log("%simportant note%s from %s:", color.BrightBlue, color.Reset, plugin.Name) 19 | 20 | for _, line := range plugin.Note { 21 | log.RawLog(" %s\n", line) 22 | } 23 | } 24 | 25 | func DisplayOptionalDependencies(plugin paplug.PluginInfo) { 26 | if len(plugin.OptionalDependencies) == 0 || global.InstallOptionalDepsInput { 27 | return 28 | } 29 | 30 | log.Log("%soptional dependencies%s from %s:", color.BrightBlue, color.Reset, plugin.Name) 31 | 32 | for _, dep := range plugin.OptionalDependencies { 33 | log.RawLog(" %s\n", dep) 34 | } 35 | } 36 | 37 | func DisplayAdditionalInfo(plugin paplug.PluginInfo) { 38 | if len(plugin.Note) == 0 && len(plugin.OptionalDependencies) == 0 { 39 | return 40 | } 41 | 42 | log.RawLog("\n") 43 | log.Log("additional information for %s%s%s", color.BrightBlue, plugin.Name, color.Reset) 44 | 45 | DisplayNote(plugin) 46 | DisplayOptionalDependencies(plugin) 47 | } 48 | 49 | func displayPluginLine(plugin paplug.PluginInfo) { 50 | pluginLine := fmt.Sprintf(" %s %s", plugin.Name, plugin.Version) 51 | 52 | switch { 53 | case plugin.Path != "": 54 | pluginLine += fmt.Sprintf(" (%s)", plugin.Path) 55 | case plugin.URL != "": 56 | pluginLine += fmt.Sprintf(" (%s)", plugin.URL) 57 | case plugin.Source != "": 58 | pluginLine += fmt.Sprintf(" (%s)", plugin.Source) 59 | } 60 | 61 | if plugin.IsDependency { 62 | pluginLine += " [dependency]" 63 | } 64 | 65 | if plugin.IsOptionalDependency { 66 | pluginLine += " [optional dependency]" 67 | } 68 | 69 | log.RawLog("%s\n", pluginLine) 70 | } 71 | 72 | // List out plugins. 73 | func PluginList(plugins []paplug.PluginInfo, operation string) { 74 | if len(plugins) == 0 { 75 | log.Log("there are no plugins to install, exiting...") 76 | 77 | os.Exit(0) 78 | } 79 | 80 | log.Log("%s %d plugin(s):", operation, len(plugins)) 81 | 82 | for _, plugin := range plugins { 83 | displayPluginLine(plugin) 84 | } 85 | 86 | log.Continue("would you like to continue?") 87 | } 88 | -------------------------------------------------------------------------------- /internal/fs/fs.go: -------------------------------------------------------------------------------- 1 | // Filesystem Management. 2 | // Basically just the fs parts of the os package but with debug logging and error handling. 3 | package fs 4 | 5 | import ( 6 | "io/fs" 7 | "os" 8 | 9 | "github.com/talwat/pap/internal/log" 10 | ) 11 | 12 | const ( 13 | ExecutePerm fs.FileMode = 0o700 14 | ReadWritePerm fs.FileMode = 0o600 15 | ) 16 | 17 | func WriteFile(name string, text string, perms fs.FileMode) { 18 | WriteFileByte(name, []byte(text), perms) 19 | } 20 | 21 | func WriteFileByte(name string, text []byte, perms fs.FileMode) { 22 | log.Debug("writing to %s", name) 23 | 24 | err := os.WriteFile(name, text, perms) 25 | log.Error(err, "an error occurred while writing %s", name) 26 | } 27 | 28 | func ReadFile(name string) []byte { 29 | log.Debug("reading %s", name) 30 | 31 | raw, err := os.ReadFile(name) 32 | log.Error(err, "an error occurred while reading %s", name) 33 | 34 | return raw 35 | } 36 | 37 | func FileExists(filename string) bool { 38 | log.Debug("checking if %s exists", filename) 39 | 40 | if _, err := os.Stat(filename); os.IsNotExist(err) { 41 | return false 42 | } else if err != nil { 43 | log.Error(err, "an error occurred while checking if %s exists", filename) 44 | } 45 | 46 | return true 47 | } 48 | 49 | func MakeDirectory(path string) { 50 | log.Debug("making directory at %s", path) 51 | 52 | err := os.MkdirAll(path, os.ModePerm) 53 | log.Error(err, "an error occurred while creating %s", path) 54 | } 55 | 56 | func DeletePath(path string) { 57 | log.Debug("deleting %s", path) 58 | 59 | err := os.RemoveAll(path) 60 | log.Error(err, "an error occurred while deleting %s", path) 61 | } 62 | 63 | func MoveFile(oldpath string, newpath string) { 64 | log.Debug("moving %s to %s", oldpath, newpath) 65 | 66 | err := os.Rename(oldpath, newpath) 67 | log.Error(err, "an error occurred while moving %s to %s", oldpath, newpath) 68 | } 69 | 70 | func CreateFile(filename string, perms fs.FileMode) *os.File { 71 | log.Debug("opening %s...", filename) 72 | 73 | file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, perms) 74 | log.Error(err, "an error occurred while opening %s", filename) 75 | 76 | return file 77 | } 78 | 79 | func OpenFile(filename string) *os.File { 80 | log.Debug("opening %s...", filename) 81 | 82 | file, err := os.Open(filename) 83 | log.Error(err, "an error occurred while opening %s", filename) 84 | 85 | return file 86 | } 87 | -------------------------------------------------------------------------------- /internal/plugins/sources/paplug/structs.go: -------------------------------------------------------------------------------- 1 | package paplug 2 | 3 | // If type is "jenkins". 4 | type JenkinsDownload struct { 5 | Job string `json:"job,omitempty"` 6 | Artifact string `json:"artifact,omitempty"` 7 | } 8 | 9 | // If type is "url". 10 | type URLDownload struct { 11 | URL string `json:"url,omitempty"` 12 | } 13 | 14 | type Download struct { 15 | Type string `json:"type"` 16 | Filename string `json:"filename"` 17 | 18 | JenkinsDownload 19 | URLDownload 20 | } 21 | 22 | type Commands struct { 23 | Windows []string `json:"windows"` 24 | Unix []string `json:"unix"` 25 | } 26 | 27 | type File struct { 28 | Type string `json:"type"` 29 | Path string `json:"path"` 30 | } 31 | 32 | type Install struct { 33 | Type string `json:"type"` 34 | Commands *Commands `json:"commands,omitempty"` 35 | } 36 | 37 | type Uninstall struct { 38 | Files []File `json:"files"` 39 | } 40 | 41 | // Defined in pap, not in the json files themselves. 42 | type DefinedLater struct { 43 | Path string `json:"path,omitempty"` 44 | URL string `json:"url,omitempty"` 45 | Source string `json:"source,omitempty"` 46 | IsDependency bool `json:"isDependency,omitempty"` 47 | IsOptionalDependency bool `json:"isOptionalDependency,omitempty"` 48 | } 49 | 50 | // Metadata that isn't used for core operations in pap. 51 | type Metadata struct { 52 | Description string `json:"description"` 53 | Authors []string `json:"authors"` 54 | Note []string `json:"note,omitempty"` 55 | 56 | LessImportantMetadata 57 | } 58 | 59 | // Metadata that is not very important. 60 | type LessImportantMetadata struct { 61 | License string `json:"license"` 62 | Site string `json:"site,omitempty"` 63 | } 64 | 65 | // Operation steps (installing and uninstalling). 66 | type Steps struct { 67 | Install Install `json:"install"` 68 | Uninstall Uninstall `json:"uninstall"` 69 | } 70 | 71 | // All dependencies including optional dependencies. 72 | type AllDependencies struct { 73 | Dependencies []string `json:"dependencies,omitempty"` 74 | OptionalDependencies []string `json:"optionalDependencies,omitempty"` 75 | } 76 | 77 | type PluginInfo struct { 78 | Name string `json:"name"` 79 | Version string `json:"version"` 80 | Metadata 81 | AllDependencies 82 | 83 | Downloads []Download `json:"downloads"` 84 | Steps 85 | DefinedLater 86 | 87 | Alias string `json:"alias,omitempty"` 88 | } 89 | -------------------------------------------------------------------------------- /internal/plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/talwat/pap/internal/exec" 10 | "github.com/talwat/pap/internal/fs" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/plugins/sources/paplug" 13 | ) 14 | 15 | // Substitutes parts of a string like {version} with their proper counterpart. 16 | func SubstituteProps(plugin paplug.PluginInfo, str string) string { 17 | toReplace := map[string]string{ 18 | "version": plugin.Version, 19 | "name": plugin.Name, 20 | } 21 | 22 | final := str 23 | 24 | for key, value := range toReplace { 25 | log.Debug("substituting %s with %s", key, value) 26 | final = strings.ReplaceAll(final, fmt.Sprintf("{%s}", key), value) 27 | } 28 | 29 | return final 30 | } 31 | 32 | func CheckIfInstalled(plugin paplug.PluginInfo) bool { 33 | for _, file := range plugin.Uninstall.Files { 34 | if file.Type != "main" || !fs.FileExists(filepath.Join("plugins", file.Path)) { 35 | continue 36 | } 37 | 38 | log.Warn("skipping %s: it may already be installed. if not, try reinstalling the plugin", plugin.Name) 39 | 40 | return true 41 | } 42 | 43 | return false 44 | } 45 | 46 | func PluginInstall(plugin paplug.PluginInfo) { 47 | name := plugin.Name 48 | 49 | log.Log("installing %s...", name) 50 | 51 | if plugin.Install.Type == "simple" { 52 | log.Success("successfully installed %s (simple)", name) 53 | 54 | return 55 | } 56 | 57 | log.Log("running commands for %s...", name) 58 | 59 | var cmds []string 60 | 61 | if runtime.GOOS == "windows" { 62 | log.Debug("using windows commands...") 63 | 64 | cmds = plugin.Install.Commands.Windows 65 | } else { 66 | log.Debug("using unix commands...") 67 | 68 | cmds = plugin.Install.Commands.Unix 69 | } 70 | 71 | for _, cmd := range cmds { 72 | exec.Run("plugins", SubstituteProps(plugin, cmd)) 73 | log.RawLog("\n") 74 | } 75 | } 76 | 77 | func PluginUninstall(plugin paplug.PluginInfo) { 78 | name := plugin.Name 79 | 80 | log.Log("uninstalling %s...", name) 81 | 82 | for _, file := range plugin.Uninstall.Files { 83 | path := filepath.Join("plugins", SubstituteProps(plugin, file.Path)) 84 | 85 | if file.Type == "" { 86 | file.Type = "other" 87 | } 88 | 89 | log.Log("deleting %s at %s", file.Type, path) 90 | fs.DeletePath(path) 91 | } 92 | } 93 | 94 | func PluginDoMany(plugins []paplug.PluginInfo, operation func(plugin paplug.PluginInfo)) { 95 | for _, plugin := range plugins { 96 | operation(plugin) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/jarfiles/forge/version.go: -------------------------------------------------------------------------------- 1 | package forge 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/talwat/pap/internal/log" 10 | "golang.org/x/exp/maps" 11 | ) 12 | 13 | var ( 14 | preRegex = regexp.MustCompile(`_pre[0-9]`) 15 | typeRegex = regexp.MustCompile(`-[^"]*`) 16 | ) 17 | 18 | // Avoid using a magic number. 19 | const majorAndMinorVersion int = 2 20 | 21 | func cleanMinecraftVersionString(version string, minecraft *MinecraftVersion) string { 22 | preVersion := preRegex.FindString(version) 23 | if preVersion != "" { 24 | minecraft.IsPrerelease = true 25 | preVersion = strings.Replace(preVersion, "_pre", "", 1) 26 | 27 | var err error 28 | minecraft.PrereleaseVersion, err = strconv.Atoi(preVersion) 29 | log.Error(err, "failed to parse prerelease version number") 30 | 31 | version = preRegex.ReplaceAllString(version, "") 32 | } 33 | 34 | version = typeRegex.ReplaceAllString(version, "") 35 | 36 | log.Debug("cleaned version string: %s", version) 37 | 38 | return version 39 | } 40 | 41 | func parseMinecraftVersion(ver string) MinecraftVersion { 42 | var minecraft MinecraftVersion 43 | 44 | cleanVersion := cleanMinecraftVersionString(ver, &minecraft) 45 | splitVersion := strings.Split(cleanVersion, ".") 46 | 47 | var err error 48 | minecraft.Major, err = strconv.Atoi(splitVersion[0]) 49 | log.Error(err, "failed to parse major version") 50 | 51 | minecraft.Minor, err = strconv.Atoi(splitVersion[1]) 52 | log.Error(err, "failed to parse minor version") 53 | 54 | if len(splitVersion) > majorAndMinorVersion { 55 | minecraft.Patch, err = strconv.Atoi(splitVersion[2]) 56 | log.Error(err, "failed to parse minor version") 57 | } 58 | 59 | log.Debug("parsed minecraft version: %+v", minecraft) 60 | 61 | return minecraft 62 | } 63 | 64 | func getLatestMinecraftVersion(promotions *PromotionsSlim) MinecraftVersion { 65 | promoKeys := maps.Keys(promotions.Promos) 66 | 67 | keymap := make(map[string]bool, len(promoKeys)) 68 | 69 | for _, val := range promoKeys { 70 | s := strings.Split(val, "-")[0] 71 | _, exists := keymap[s] 72 | 73 | if !exists { 74 | keymap[s] = true 75 | } 76 | } 77 | 78 | keys := maps.Keys(keymap) 79 | minecraftVersions := make([]MinecraftVersion, len(keys)) 80 | 81 | for i := 0; i < len(keys); i++ { 82 | minecraftVersions[i] = parseMinecraftVersion(keys[i]) 83 | } 84 | 85 | sort.Sort(ByVersion(minecraftVersions)) 86 | 87 | last := minecraftVersions[len(minecraftVersions)-1] 88 | 89 | log.Debug("latest minecraft version: %+v", last) 90 | 91 | return last 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | pull_request: 17 | schedule: 18 | - cron: '17 21 * * 0' 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'go' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 34 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v2 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | 49 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 50 | # queries: security-extended,security-and-quality 51 | 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # If the Autobuild fails above, remove it and uncomment the following three lines. 62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 63 | 64 | # - run: | 65 | # echo "Run, Build Application using script" 66 | # ./location_of_script_within_repo/buildscript.sh 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v2 70 | with: 71 | category: "/language:${{matrix.language}}" 72 | -------------------------------------------------------------------------------- /plugins/example.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // The name of the plugin, should be exactly the same as the filename 3 | "name": "example", 4 | 5 | // Can also be "latest" 6 | "version": "0.1", 7 | 8 | // The description, it should end with a period 9 | "description": "Example Description.", 10 | 11 | // SPDX Identifier. Can also be "proprietary" or "unknown" 12 | "license": "GPL-3.0", 13 | 14 | // List of authors 15 | "authors": ["Person", "Another person"], 16 | 17 | // The site or repository (optional) 18 | "site": "https://www.example.com", 19 | 20 | // The dependencies (optional) 21 | "dependencies": ["example"], 22 | 23 | // Optional dependencies (optional) 24 | "optionalDependencies": ["vault"], 25 | 26 | // The files to download 27 | "downloads": [ 28 | { 29 | // This can either be "jenkins" or "url" 30 | // Jenkins: Get the latest successful build 31 | // URL: Get from a predefined URL 32 | "type": "jenkins", 33 | 34 | // The job to get from 35 | "job": "https://ci.athion.net/job/Example", 36 | 37 | // The artifact in jenkins to download 38 | // This is a regex, however, if the version is not "latest" then {version} will be substitued with the version attribute 39 | "artifact": "jarfile-bukkit-.*", 40 | 41 | // The filename to save the plugin as 42 | "filename": "plugin.jar" 43 | }, 44 | { 45 | // This can either be "jenkins" or "url" 46 | // Jenkins: Get the latest successful build 47 | // URL: Get from a predefined URL 48 | "type": "url", 49 | 50 | // The url 51 | "url": "https://www.example.com/plugin-{version}.jar", 52 | 53 | // The filename to save the plugin as 54 | "filename": "plugin.jar" 55 | } 56 | ], 57 | 58 | // How to install the plugin 59 | "install": { 60 | // Can either be "simple" (Just downloads a jarfile) 61 | // Or "custom" (Custom commands to install the jarfile) 62 | "type": "simple", 63 | 64 | // The commands to run if "custom" 65 | "commands": { 66 | // Commands to run on windows 67 | // Uses powershell 68 | "windows": ["Rename-Item -Path plugin.jar -NewName something.jar"], 69 | 70 | // Commands to run on unix-like operating systems (Linux, macOS, BSD, etc...) 71 | // Uses sh 72 | "unix": ["mv plugin.jar something.jar"] 73 | } 74 | }, 75 | 76 | // How to uninstall the plugin 77 | "uninstall": { 78 | // Files to delete 79 | "files": [ 80 | { 81 | // The type of file being deleted, this can be "main", "config", "data" or "other" 82 | "type": "main", 83 | 84 | // The path, relative to the plugins directory 85 | "path": "myPlugin.jar" 86 | } 87 | ] 88 | }, 89 | 90 | // Defines an alias (optional) 91 | // This overrides everything else, and just redirects to another package. This can also be a URL. 92 | "alias": "exampleimproved" 93 | } 94 | -------------------------------------------------------------------------------- /internal/net/net.go: -------------------------------------------------------------------------------- 1 | // Networking 2 | package net 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "hash" 9 | "io" 10 | "io/fs" 11 | "net/http" 12 | 13 | "github.com/talwat/gobar" 14 | papfs "github.com/talwat/pap/internal/fs" 15 | "github.com/talwat/pap/internal/global" 16 | "github.com/talwat/pap/internal/log" 17 | ) 18 | 19 | // Makes and executes an HTTP request with proper headers identifying pap. 20 | func DoRequest(url string, notFoundMsg string) *http.Response { 21 | log.Debug("making a new request to %s", url) 22 | 23 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 24 | log.Error(err, "an error occurred while making request") 25 | 26 | userAgent := fmt.Sprintf("talwat/pap/%s", global.Version) 27 | req.Header.Set("User-Agent", userAgent) 28 | 29 | log.Debug("using user-agent %s", userAgent) 30 | log.Debug("doing request to %s", url) 31 | 32 | resp, err := http.DefaultClient.Do(req) 33 | log.Error(err, "an error occurred while sending request") 34 | 35 | if resp.StatusCode == http.StatusNotFound { 36 | log.RawError("404: %s (%s)", notFoundMsg, url) 37 | } 38 | 39 | log.Debug("status code %d", resp.StatusCode) 40 | 41 | return resp 42 | } 43 | 44 | // Like get, but just returns plaintext. 45 | func GetPlainText(url string, notFoundMsg string) (string, int) { 46 | resp := DoRequest(url, notFoundMsg) 47 | 48 | log.Debug("reading response body...") 49 | 50 | raw, err := io.ReadAll(resp.Body) 51 | log.Error(err, "an error occurred while reading request body") 52 | 53 | defer resp.Body.Close() 54 | 55 | return string(raw), resp.StatusCode 56 | } 57 | 58 | // Saves the decoded JSON data to the value of content. 59 | func Get(url string, notFoundMsg string, content interface{}) int { 60 | resp := DoRequest(url, notFoundMsg) 61 | 62 | log.Debug("decoding json response") 63 | 64 | err := json.NewDecoder(resp.Body).Decode(&content) 65 | log.Error(err, "an error occurred while decoding response") 66 | 67 | defer resp.Body.Close() 68 | 69 | return resp.StatusCode 70 | } 71 | 72 | // Downloads a file. If the file is too small, a progress bar won't be displayed. 73 | // Set hash to nil in order to prevent calculating the hash. 74 | func Download( 75 | url string, 76 | notFoundMsg string, 77 | filename string, 78 | filedesc string, 79 | hash hash.Hash, 80 | perms fs.FileMode, 81 | ) []byte { 82 | resp := DoRequest(url, notFoundMsg) 83 | defer resp.Body.Close() 84 | 85 | file := papfs.CreateFile(filename, perms) 86 | defer file.Close() 87 | 88 | log.Debug("content length: %d", resp.ContentLength) 89 | 90 | writers := []io.Writer{file} 91 | 92 | if hash != nil { 93 | writers = append(writers, hash) 94 | } 95 | 96 | if resp.ContentLength != -1 { 97 | bar := gobar.NewBar( 98 | 0, 99 | resp.ContentLength, 100 | fmt.Sprintf("pap: downloading %s", filedesc), 101 | ) 102 | 103 | writers = append(writers, bar) 104 | } else { 105 | log.Log("downloading %s", filedesc) 106 | } 107 | 108 | _, err := io.Copy(io.MultiWriter(writers...), resp.Body) 109 | log.Error(err, "An error occurred while writing %s", filedesc) 110 | 111 | if hash == nil { 112 | return nil 113 | } 114 | 115 | return hash.Sum(nil) 116 | } 117 | -------------------------------------------------------------------------------- /internal/jarfiles/purpur/api.go: -------------------------------------------------------------------------------- 1 | package purpur 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/jarfiles" 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | ) 10 | 11 | // Gets the latest version's info. 12 | func GetLatestVersion() Version { 13 | log.Log("getting versions...") 14 | 15 | var versions Versions 16 | 17 | net.Get( 18 | "https://api.purpurmc.org/v2/purpur", 19 | "version list not found, please report this to https://github.com/talwat/pap/issues", 20 | &versions, 21 | ) 22 | 23 | log.Log("getting latest version info...") 24 | 25 | var version Version 26 | 27 | versionID := versions.Versions[len(versions.Versions)-1] 28 | log.Debug("latest version: %s", versionID) 29 | 30 | net.Get( 31 | fmt.Sprintf( 32 | "https://api.purpurmc.org/v2/purpur/%s", 33 | versionID, 34 | ), 35 | fmt.Sprintf( 36 | "version information for %s not found, please report this to https://github.com/talwat/pap/issues", 37 | versionID, 38 | ), 39 | &version, 40 | ) 41 | 42 | return version 43 | } 44 | 45 | // Gets a specific version's info. 46 | func GetSpecificVersion(versionID string) Version { 47 | log.Log("getting info for %s...", versionID) 48 | 49 | var version Version 50 | 51 | statusCode := net.Get( 52 | fmt.Sprintf( 53 | "https://api.purpurmc.org/v2/purpur/%s", 54 | versionID, 55 | ), 56 | fmt.Sprintf("version information for %s not found", versionID), 57 | &version, 58 | ) 59 | 60 | jarfiles.APIError(version.Error, statusCode) 61 | 62 | return version 63 | } 64 | 65 | // Gets the latest build using the provided version information. 66 | func GetLatestBuild(version Version) Build { 67 | log.Log("getting latest build info...") 68 | 69 | buildID := version.Builds.Latest 70 | log.Debug("latest build: %s", buildID) 71 | 72 | var build Build 73 | 74 | net.Get( 75 | fmt.Sprintf( 76 | "https://api.purpurmc.org/v2/purpur/%s/%s", 77 | version.Version, 78 | buildID, 79 | ), 80 | fmt.Sprintf("build information for %s not found", buildID), 81 | &build, 82 | ) 83 | 84 | return build 85 | } 86 | 87 | // Gets a specific build using the provided version. 88 | func GetSpecificBuild(version Version, buildID string) Build { 89 | log.Log("getting build info for %s...", buildID) 90 | 91 | var build Build 92 | statusCode := net.Get( 93 | fmt.Sprintf( 94 | "https://api.purpurmc.org/v2/purpur/%s/%s", 95 | version.Version, 96 | buildID, 97 | ), 98 | fmt.Sprintf("build information for %s not found", buildID), 99 | &build, 100 | ) 101 | 102 | jarfiles.APIError(build.Error, statusCode) 103 | 104 | return build 105 | } 106 | 107 | // Gets a build. It will be the latest one depending on what the input is. 108 | func GetBuild(version Version, buildInput string) Build { 109 | if buildInput == jarfiles.Latest { 110 | return GetLatestBuild(version) 111 | } 112 | 113 | return GetSpecificBuild(version, buildInput) 114 | } 115 | 116 | // Gets a version. It will be the latest one depending on what the input is. 117 | func GetVersion(versionInput string) Version { 118 | if versionInput == jarfiles.Latest { 119 | return GetLatestVersion() 120 | } 121 | 122 | return GetSpecificVersion(versionInput) 123 | } 124 | -------------------------------------------------------------------------------- /internal/jarfiles/paper/api.go: -------------------------------------------------------------------------------- 1 | package paper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/global" 7 | "github.com/talwat/pap/internal/jarfiles" 8 | "github.com/talwat/pap/internal/log" 9 | "github.com/talwat/pap/internal/net" 10 | ) 11 | 12 | // Gets the latest minecraft version in the list of versions. 13 | func GetLatestVersion() string { 14 | var versions Versions 15 | 16 | log.Log("getting latest version information") 17 | net.Get( 18 | "https://api.papermc.io/v2/projects/paper", 19 | "version information not found, please report this to https://github.com/talwat/pap/issues", 20 | &versions, 21 | ) 22 | 23 | version := versions.Versions[len(versions.Versions)-1] 24 | log.Debug("latest version: %s", version) 25 | 26 | return version 27 | } 28 | 29 | // Gets the latest build in a version. 30 | func GetLatestBuild(version string) Build { 31 | var builds Builds 32 | 33 | log.Log("getting latest build information") 34 | 35 | url := fmt.Sprintf("https://api.papermc.io/v2/projects/paper/versions/%s/builds", version) 36 | statusCode := net.Get(url, fmt.Sprintf("build information for %s not found", version), &builds) 37 | 38 | jarfiles.APIError(builds.Error, statusCode) 39 | 40 | // latest build, can be experimental or stable 41 | latest := builds.Builds[len(builds.Builds)-1] 42 | 43 | if global.PaperExperimentalBuildInput { 44 | log.Debug("using latest build (%d) regardless", latest.Build) 45 | 46 | return latest 47 | } 48 | 49 | // Iterate through builds.Builds backwards 50 | for i := len(builds.Builds) - 1; i >= 0; i-- { 51 | if builds.Builds[i].Channel == "default" { // "default" usually means stable 52 | return builds.Builds[i] // Stable build found, return it 53 | } 54 | } 55 | 56 | log.Continue("no stable build found, would you like to use the latest experimental build?") 57 | 58 | return latest 59 | } 60 | 61 | // Gets a specific build in a version. 62 | func GetSpecificBuild(version string, buildID string) Build { 63 | log.Log("getting build information for %s", buildID) 64 | 65 | var build Build 66 | 67 | url := fmt.Sprintf("https://api.papermc.io/v2/projects/paper/versions/%s/builds/%s", version, buildID) 68 | statusCode := net.Get(url, fmt.Sprintf("build %s of version %s not found", buildID, version), &build) 69 | 70 | jarfiles.APIError(build.Error, statusCode) 71 | 72 | return build 73 | } 74 | 75 | // Gets either the latest build or a specific one depending on buildID. 76 | func GetBuild(version string, buildID string) Build { 77 | var build Build 78 | 79 | if buildID == jarfiles.Latest { 80 | build = GetLatestBuild(version) 81 | } else { 82 | build = GetSpecificBuild(version, buildID) 83 | } 84 | 85 | if build.Channel == "experimental" && !global.PaperExperimentalBuildInput { 86 | log.Continue( 87 | "build %d has been flagged as experimental, are you sure you would like to download it?", 88 | build.Build, 89 | ) 90 | } 91 | 92 | return build 93 | } 94 | 95 | // Gets a specific version or the latest one depending on the input. 96 | // Additionally, if using a specific one, it will not get the latest release of a specific minor release. 97 | // For example if you put in 1.12, you will get 1.12 and not 1.12.2. 98 | func GetVersion(version string) string { 99 | if version == jarfiles.Latest { 100 | return GetLatestVersion() 101 | } 102 | 103 | return version 104 | } 105 | -------------------------------------------------------------------------------- /internal/plugins/sources/modrinth/modrinth.go: -------------------------------------------------------------------------------- 1 | package modrinth 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | "github.com/talwat/pap/internal/plugins/sources" 10 | "github.com/talwat/pap/internal/plugins/sources/paplug" 11 | ) 12 | 13 | // Gets a website for a modrinth plugin. 14 | func getWebsite(plugin PluginInfo) string { 15 | switch { 16 | case plugin.SourceURL != "": 17 | log.Debug("source url isn't empty, using it (%s)", plugin.SourceURL) 18 | 19 | return plugin.SourceURL 20 | case plugin.WikiURL != "": 21 | log.Debug("wiki url isn't empty, using it (%s)", plugin.WikiURL) 22 | 23 | return plugin.WikiURL 24 | case plugin.IssuesURL != "": 25 | log.Debug("issues url isn't empty, using it (%s)", plugin.IssuesURL) 26 | 27 | return plugin.IssuesURL 28 | case plugin.DiscordURL != "": 29 | log.Debug("discord url isn't empty, using it (%s)", plugin.DiscordURL) 30 | 31 | return plugin.DiscordURL 32 | default: 33 | url := fmt.Sprintf("https://modrinth.com/mod/%s", plugin.Slug) 34 | log.Debug("no links defined, falling back to modrinth page (%s)", url) 35 | 36 | return fmt.Sprintf("https://modrinth.com/mod/%s", plugin.Slug) 37 | } 38 | } 39 | 40 | // Converts a modrinth plugin into the paplug format. 41 | func ConvertToPlugin(modrinthPlugin PluginInfo) paplug.PluginInfo { 42 | plugin := paplug.PluginInfo{} 43 | 44 | plugin.Name = sources.FormatName(modrinthPlugin.Slug) 45 | plugin.Description = modrinthPlugin.Description 46 | plugin.License = modrinthPlugin.License.ID 47 | plugin.Site = getWebsite(modrinthPlugin) 48 | 49 | if !strings.HasSuffix(plugin.Description, ".") { 50 | plugin.Description += "." 51 | } 52 | 53 | plugin.Install.Type = "simple" 54 | 55 | // Unknown vars 56 | plugin.Authors = []string{} 57 | plugin.Note = []string{} 58 | plugin.Dependencies = []string{} 59 | plugin.OptionalDependencies = []string{} 60 | 61 | plugin.Version = modrinthPlugin.ResolvedVersion.VersionNumber 62 | 63 | for _, file := range modrinthPlugin.ResolvedVersion.Files { 64 | download := paplug.Download{} 65 | 66 | download.Type = "url" 67 | download.URL = file.URL 68 | download.Filename = file.Filename 69 | plugin.Downloads = append(plugin.Downloads, download) 70 | 71 | uninstallFile := paplug.File{} 72 | 73 | uninstallFile.Path = file.Filename 74 | uninstallFile.Type = "main" 75 | 76 | plugin.Uninstall.Files = append(plugin.Uninstall.Files, uninstallFile) 77 | } 78 | 79 | return plugin 80 | } 81 | 82 | // Gets a raw modrinth plugin. 83 | func Get(name string) PluginInfo { 84 | var modrinthPlugin PluginInfo 85 | 86 | net.Get( 87 | fmt.Sprintf("https://api.modrinth.com/v2/project/%s", name), 88 | fmt.Sprintf("modrinth plugin %s not found", name), 89 | &modrinthPlugin, 90 | ) 91 | 92 | // This may take a beta version. 93 | // But to get only a stable one it would require sending a GET request for potentially every version. 94 | // Which is very slow. 95 | version := modrinthPlugin.Versions[len(modrinthPlugin.Versions)-1] 96 | 97 | net.Get( 98 | fmt.Sprintf("https://api.modrinth.com/v2/version/%s", version), 99 | fmt.Sprintf("version %s not found", version), 100 | &modrinthPlugin.ResolvedVersion, 101 | ) 102 | 103 | return modrinthPlugin 104 | } 105 | 106 | // Gets & converts to the standard pap format. 107 | func GetPluginInfo(name string) paplug.PluginInfo { 108 | return ConvertToPlugin(Get(name)) 109 | } 110 | -------------------------------------------------------------------------------- /internal/plugins/sources/bukkit/bukkit.go: -------------------------------------------------------------------------------- 1 | package bukkit 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/global" 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | "github.com/talwat/pap/internal/plugins/sources" 10 | "github.com/talwat/pap/internal/plugins/sources/paplug" 11 | ) 12 | 13 | // Gets the latest stable build. 14 | func getLatestBuild(project PluginInfo) File { 15 | latest := project.ResolvedFiles[len(project.ResolvedFiles)-1] 16 | 17 | if global.PluginExperimentalInput { 18 | log.Debug("using latest file (%s) regardless", latest.FileName) 19 | 20 | return latest 21 | } 22 | 23 | // Iterate through project.ResolvedFiles backwards 24 | for i := len(project.ResolvedFiles) - 1; i >= 0; i-- { 25 | if project.ResolvedFiles[i].ReleaseType == "release" { // "release" usually means stable 26 | return project.ResolvedFiles[i] // Stable build found, return it 27 | } 28 | } 29 | 30 | log.Continue("warning: no stable build found, would you like to use the latest experimental file?") 31 | 32 | return latest 33 | } 34 | 35 | // Converts a bukkit project to a paplug plugin. 36 | func ConvertToPlugin(bukkitProject PluginInfo) paplug.PluginInfo { 37 | plugin := paplug.PluginInfo{} 38 | plugin.Name = sources.FormatName(bukkitProject.Slug) 39 | plugin.Site = fmt.Sprintf("https://dev.bukkit.org/projects/%s", plugin.Name) 40 | 41 | plugin.Install.Type = "simple" 42 | 43 | // Unknown vars 44 | plugin.Note = []string{} 45 | plugin.Dependencies = []string{} 46 | plugin.OptionalDependencies = []string{} 47 | plugin.Version = sources.Undefined 48 | plugin.Description = sources.Undefined 49 | plugin.License = sources.Undefined 50 | plugin.Description = sources.Undefined 51 | 52 | // File & Download 53 | latestFile := getLatestBuild(bukkitProject) 54 | 55 | // File 56 | file := paplug.File{} 57 | file.Path = latestFile.FileName 58 | file.Type = "main" 59 | 60 | plugin.Uninstall.Files = append(plugin.Uninstall.Files, file) 61 | 62 | // Download 63 | download := paplug.Download{} 64 | download.URL = latestFile.DownloadURL 65 | download.Type = "url" 66 | download.Filename = latestFile.FileName 67 | 68 | plugin.Downloads = append(plugin.Downloads, download) 69 | 70 | return plugin 71 | } 72 | 73 | // Gets a project from a list and tries to match the slug. 74 | // This helps get more accurate results. 75 | func getProject(name string, projects []PluginInfo) PluginInfo { 76 | for _, project := range projects { 77 | if project.Slug == name { 78 | return project 79 | } 80 | } 81 | 82 | log.Warn("there are no plugins that match %s exactly, using first result", name) 83 | 84 | return projects[0] 85 | } 86 | 87 | // Gets the raw bukkit project. 88 | func Get(name string) PluginInfo { 89 | var projects []PluginInfo 90 | 91 | net.Get( 92 | fmt.Sprintf("https://api.curseforge.com/servermods/projects?search=%s", name), 93 | fmt.Sprintf("bukkitdev search %s not found", name), 94 | &projects, 95 | ) 96 | 97 | if len(projects) == 0 { 98 | log.RawError("bukkitdev plugin %s not found", name) 99 | } 100 | 101 | project := getProject(name, projects) 102 | 103 | net.Get( 104 | fmt.Sprintf("https://api.curseforge.com/servermods/files?projectIds=%d", project.ID), 105 | fmt.Sprintf("bukkitdev versions for %s not found", project.Slug), 106 | &project.ResolvedFiles, 107 | ) 108 | 109 | return project 110 | } 111 | 112 | // Gets & converts to the standard pap format. 113 | func GetPluginInfo(name string) paplug.PluginInfo { 114 | return ConvertToPlugin(Get(name)) 115 | } 116 | -------------------------------------------------------------------------------- /internal/properties/properties.go: -------------------------------------------------------------------------------- 1 | // Management of the server.properties file 2 | package properties 3 | 4 | import ( 5 | "bufio" 6 | "fmt" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/talwat/pap/internal/fs" 11 | "github.com/talwat/pap/internal/log" 12 | "github.com/talwat/pap/internal/net" 13 | "github.com/talwat/pap/internal/time" 14 | ) 15 | 16 | func WritePropertiesFile(filename string, props map[string]interface{}) { 17 | log.Debug("writing properties file...") 18 | 19 | keys := make([]string, 0) 20 | final := fmt.Sprintf("#Minecraft server properties\n#%s\n", time.MinecraftDateNow()) 21 | 22 | for k := range props { 23 | keys = append(keys, k) 24 | } 25 | 26 | sort.Strings(keys) 27 | 28 | for _, k := range keys { 29 | final += fmt.Sprintf("%s=%s\n", k, props[k]) 30 | } 31 | 32 | fs.WriteFile(filename, final, fs.ReadWritePerm) 33 | } 34 | 35 | func parsePropertiesLine(line string, conf map[string]interface{}) { 36 | equalIdx := strings.Index(line, "=") 37 | 38 | // If "=" is -1 (which means "=" isn't in the line), skip. 39 | if equalIdx == -1 { 40 | log.Debug("'%s' does not include an = sign, skipping...", line) 41 | 42 | return 43 | } 44 | 45 | // Set the key to everything before "=" using the equal index. 46 | key := strings.TrimSpace(line[:equalIdx]) 47 | 48 | // If the key is empty, skip. 49 | if len(key) == 0 { 50 | log.Log("the key is empty, skiping...") 51 | 52 | return 53 | } 54 | 55 | val := "" 56 | 57 | // Check if there is anything after "=" in the line. 58 | if len(line) > equalIdx { 59 | // If there is, set it as the value. 60 | val = strings.TrimSpace(line[equalIdx+1:]) 61 | } 62 | 63 | // Save the value to the key in the conf map. 64 | conf[key] = val 65 | 66 | log.Debug("parsed line %s. parsed line: %s=%s", line, key, val) 67 | } 68 | 69 | func ReadPropertiesFile(filename string) map[string]interface{} { 70 | log.Debug("reading properties file...") 71 | 72 | props := map[string]interface{}{} 73 | file := fs.OpenFile(filename) 74 | 75 | defer file.Close() 76 | 77 | scanner := bufio.NewScanner(file) 78 | for scanner.Scan() { 79 | line := scanner.Text() 80 | parsePropertiesLine(line, props) 81 | } 82 | 83 | err := scanner.Err() 84 | log.Error(err, "an error occurred while parsing the properties file") 85 | 86 | return props 87 | } 88 | 89 | func SetProperty(prop string, val string) { 90 | log.Log("reading server properties...") 91 | 92 | props := ReadPropertiesFile("server.properties") 93 | 94 | log.Log("editing server properties...") 95 | 96 | props[prop] = val 97 | 98 | log.Log("writing server properties...") 99 | WritePropertiesFile("server.properties", props) 100 | 101 | log.Success("successfully set %s to %s", prop, val) 102 | } 103 | 104 | func ResetProperties() { 105 | log.Log("this command is expected to be used with the latest minecraft version") 106 | log.Log("if you are using an older version, please manually delete the properties file and run the server") 107 | log.Continue("are you sure you would like to reset your server.properties file?") 108 | net.Download( 109 | "https://raw.githubusercontent.com/talwat/pap/main/assets/default.server.properties", 110 | "server properties file not found, please report this to https://github.com/talwat/pap/issues", 111 | "server.properties", 112 | "server properties file", 113 | nil, 114 | fs.ReadWritePerm, 115 | ) 116 | log.Success("successfully reset server properties file") 117 | } 118 | 119 | func GetProperty(prop string) interface{} { 120 | props := ReadPropertiesFile("server.properties") 121 | 122 | val := props[prop] 123 | 124 | if val == nil { 125 | log.RawError("property %s does not exist", prop) 126 | } 127 | 128 | return val 129 | } 130 | -------------------------------------------------------------------------------- /internal/fs/unzip.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/talwat/pap/internal/exec" 12 | "github.com/talwat/pap/internal/log" 13 | ) 14 | 15 | const ( 16 | Success = 1 17 | Fail = 2 18 | NotFound = 3 19 | ) 20 | 21 | // Try using a specific command to unzip a file. 22 | func tryUnzipCommand(program string, src string, dest string, cmd string, params ...interface{}) { 23 | log.Log("using %s to unzip %s to %s...", program, src, dest) 24 | 25 | exitCode := exec.Run(".", fmt.Sprintf(cmd, params...)) 26 | 27 | log.RawLog("\n") 28 | 29 | if exitCode != 0 { 30 | log.RawError("%s failed with exit code: %d", program, exitCode) 31 | } 32 | } 33 | 34 | // Unzip using commands provided by the OS. 35 | func commandUnzip(src string, dest string) int { 36 | switch { 37 | case exec.CommandExists("unzip"): 38 | tryUnzipCommand("unzip", src, dest, "unzip -o %s -d %s", src, dest) 39 | 40 | return Success 41 | case exec.CommandExists("7z"): 42 | tryUnzipCommand("7z", src, dest, "7z %s -vd %s", src, dest) 43 | 44 | return Success 45 | case exec.CommandExists("bsdtar"): 46 | tryUnzipCommand("bstdar", src, dest, "bsdtar -xvf %s -C %s", src, dest) 47 | 48 | return Success 49 | default: 50 | log.Log("using golang method to unzip %s...", src) 51 | 52 | return NotFound 53 | } 54 | } 55 | 56 | // Full golang implementation, refactoring and editing is needed. 57 | // This function is avoided if possible, but kept just in case the user doesn't have basic utilities. 58 | // I mean seriously, who doesn't have unzip? 59 | // 60 | //nolint:goerr113,funlen,wrapcheck // I have no idea how to shorten this mess. 61 | func unsafeUnzip(src string, dest string) { 62 | zipReader, err := zip.OpenReader(src) 63 | log.Error(err, "an error occurred while opening zip reader") 64 | 65 | defer func() { 66 | err := zipReader.Close() 67 | log.Error(err, "a critical error occurred while closing zip reader") 68 | }() 69 | 70 | // Closure to address file descriptors issue with all the deferred .Close() methods 71 | extractAndWriteFile := func(zipfile *zip.File) error { 72 | readCloser, err := zipfile.Open() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | defer func() { 78 | err := readCloser.Close() 79 | log.Error(err, "a critical error occurred while closing the readcloser") 80 | }() 81 | 82 | //nolint:gosec // Checked by very next statement 83 | path := filepath.Join(dest, zipfile.Name) 84 | 85 | // Check for ZipSlip (Directory traversal) 86 | if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { 87 | return fmt.Errorf("illegal file path: %s", path) 88 | } 89 | 90 | // If it's a directory that is specified, make the directory and then exit 91 | if zipfile.FileInfo().IsDir() { 92 | MakeDirectory(path) 93 | 94 | return nil 95 | } 96 | 97 | MakeDirectory(filepath.Dir(path)) 98 | 99 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipfile.Mode()) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | defer func() { 105 | err := file.Close() 106 | 107 | log.Error(err, "a critical error occurred while closing the file") 108 | }() 109 | 110 | //nolint:gosec // This file is trusted, so we don't need to worry about a decompression bomb. 111 | _, err = io.Copy(file, readCloser) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | } 118 | 119 | for _, f := range zipReader.File { 120 | err := extractAndWriteFile(f) 121 | log.Error(err, "an error occurred while extracting zip file") 122 | } 123 | } 124 | 125 | func Unzip(src string, dest string) { 126 | MakeDirectory(dest) 127 | 128 | status := commandUnzip(src, dest) 129 | 130 | if status == Success { 131 | return 132 | } 133 | 134 | unsafeUnzip(src, dest) 135 | } 136 | -------------------------------------------------------------------------------- /internal/jarfiles/forge/forge_test.go: -------------------------------------------------------------------------------- 1 | package forge_test 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/talwat/pap/internal/jarfiles/forge" 8 | ) 9 | 10 | //nolint:lll 11 | func TestURL(t *testing.T) { 12 | t.Parallel() 13 | 14 | installer := forge.GetURL("1.6.4", "", false) 15 | want := "https://maven.minecraftforge.net/net/minecraftforge/forge/1.6.4-9.11.1.1345/forge-1.6.4-9.11.1.1345-installer.jar" 16 | 17 | if installer != want { 18 | t.Errorf(`GetURL("1.6.4", "", false) = %s; want %s`, installer, want) 19 | } 20 | 21 | installer = forge.GetURL("1.9", "", true) 22 | want = "https://maven.minecraftforge.net/net/minecraftforge/forge/1.9-12.16.1.1938-1.9.0/forge-1.9-12.16.1.1938-1.9.0-installer.jar" 23 | 24 | if installer != want { 25 | t.Errorf(`GetURL("1.9", "", true) = %s; want %s`, installer, want) 26 | } 27 | 28 | installer = forge.GetURL("1.7.10_pre4", "", true) 29 | want = "https://maven.minecraftforge.net/net/minecraftforge/forge/1.7.10_pre4-10.12.2.1149-prerelease/forge-1.7.10_pre4-10.12.2.1149-prerelease-installer.jar" 30 | 31 | if installer != want { 32 | t.Errorf(`GetURL("1.7.10_pre4", "", true) = %s; want %s`, installer, want) 33 | } 34 | 35 | installer = forge.GetURL("1.19.3", "44.1.0", false) 36 | want = "https://maven.minecraftforge.net/net/minecraftforge/forge/1.19.3-44.1.0/forge-1.19.3-44.1.0-installer.jar" 37 | 38 | if installer != want { 39 | t.Errorf(`forge.GetURL("1.19.3", "44.1.0", false) = %s; want %s`, installer, want) 40 | } 41 | } 42 | 43 | func TestSort(t *testing.T) { 44 | t.Parallel() 45 | 46 | version1 := forge.MinecraftVersion{ 47 | Major: 1, 48 | Minor: 16, 49 | Patch: 5, 50 | } 51 | version2 := forge.MinecraftVersion{ 52 | Major: 1, 53 | Minor: 6, 54 | Patch: 4, 55 | } 56 | version3 := forge.MinecraftVersion{ 57 | Major: 1, 58 | Minor: 19, 59 | Patch: 3, 60 | } 61 | version4 := forge.MinecraftVersion{ 62 | Major: 1, 63 | Minor: 7, 64 | Patch: 10, 65 | IsPrerelease: true, 66 | PrereleaseVersion: 4, 67 | } 68 | versions := []forge.MinecraftVersion{version1, version2, version3, version4} 69 | 70 | sort.Sort(forge.ByVersion(versions)) 71 | 72 | correctVersion := 3 73 | 74 | if versions[correctVersion].Minor != version3.Minor { 75 | t.Errorf(`sort.Sort(forge.ByVersion(vs))[3].Minor = %d; want %d`, versions[correctVersion].Minor, 19) 76 | } 77 | } 78 | 79 | //nolint:lll 80 | func TestBuildURL(t *testing.T) { 81 | t.Parallel() 82 | 83 | minecraft := forge.MinecraftVersion{ 84 | Major: 1, 85 | Minor: 9, 86 | Patch: 0, 87 | } 88 | 89 | installer := forge.InstallerVersion{ 90 | Version: "12.16.1.1938", 91 | } 92 | 93 | url := forge.BuildURL(&minecraft, &installer) 94 | want := "https://maven.minecraftforge.net/net/minecraftforge/forge/1.9-12.16.1.1938-1.9.0/forge-1.9-12.16.1.1938-1.9.0-installer.jar" 95 | 96 | if url != want { 97 | t.Errorf(`forge.BuildUrl(&minecraft, &installer) = %s; want %s`, url, want) 98 | } 99 | 100 | minecraft = forge.MinecraftVersion{ 101 | Major: 1, 102 | Minor: 16, 103 | Patch: 5, 104 | } 105 | 106 | installer = forge.InstallerVersion{ 107 | Version: "36.2.39", 108 | } 109 | 110 | url = forge.BuildURL(&minecraft, &installer) 111 | want = "https://maven.minecraftforge.net/net/minecraftforge/forge/1.16.5-36.2.39/forge-1.16.5-36.2.39-installer.jar" 112 | 113 | if url != want { 114 | t.Errorf(`forge.BuildUrl(&minecraft, &installer) = %s; want %s`, url, want) 115 | } 116 | 117 | minecraft = forge.MinecraftVersion{ 118 | Major: 1, 119 | Minor: 7, 120 | Patch: 10, 121 | IsPrerelease: true, 122 | PrereleaseVersion: 4, 123 | } 124 | 125 | installer = forge.InstallerVersion{ 126 | Version: "10.12.2.1149", 127 | } 128 | 129 | url = forge.BuildURL(&minecraft, &installer) 130 | want = "https://maven.minecraftforge.net/net/minecraftforge/forge/1.7.10_pre4-10.12.2.1149-prerelease/forge-1.7.10_pre4-10.12.2.1149-prerelease-installer.jar" 131 | 132 | if url != want { 133 | t.Errorf(`forge.BuildUrl(&minecraft, &installer) = %s; want %s`, url, want) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /internal/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/talwat/pap/internal/fs" 12 | "github.com/talwat/pap/internal/global" 13 | "github.com/talwat/pap/internal/log" 14 | "github.com/talwat/pap/internal/net" 15 | ) 16 | 17 | func parseVersion(rawVersion string) []string { 18 | noExtra := strings.Split(rawVersion, "-")[0] 19 | 20 | return strings.Split(noExtra, ".") 21 | } 22 | 23 | func checkIfNewUpdate() string { 24 | log.Log("checking for a new update...") 25 | 26 | rawLatest, statusCode := net.GetPlainText( 27 | "https://raw.githubusercontent.com/talwat/pap/main/version.txt", 28 | "latest version information not found, please report this to https://github.com/talwat/pap/issues", 29 | ) 30 | 31 | if statusCode != http.StatusOK { 32 | log.RawError("http request to get latest version returned %d", statusCode) 33 | } 34 | 35 | log.Debug("raw latest version: %s", rawLatest) 36 | 37 | latest := parseVersion(rawLatest) 38 | log.Debug("parsed latest version: %s", latest) 39 | 40 | current := parseVersion(global.Version) 41 | log.Debug("parsed current version: %s", current) 42 | 43 | latestLen := len(latest) 44 | currentLen := len(current) 45 | 46 | if latestLen != currentLen { 47 | log.RawError( 48 | //nolint:lll 49 | "latest (%s) and current (%s) version are different lengths, please report this issue to https://github.com/talwat/pap/issues", 50 | latest, 51 | current, 52 | ) 53 | } 54 | 55 | for idx := range latest { 56 | switch { 57 | case latest[idx] < current[idx]: 58 | log.Debug("%s > %s, assuming you are using a development version", current[idx], latest[idx]) 59 | 60 | log.Log("pap is newer than the current latest version (you are a developer?)") 61 | os.Exit(0) 62 | case latest[idx] > current[idx]: 63 | log.Debug("%s > %s, out of date!", latest[idx], current[idx]) 64 | log.Log("out of date! current version is %s, latest is %s", global.Version, rawLatest) 65 | 66 | return rawLatest 67 | default: 68 | continue 69 | } 70 | } 71 | 72 | if global.ReinstallInput { 73 | log.Warn("pap is up to date, but --reinstall is set, so continuing") 74 | 75 | return rawLatest 76 | } 77 | 78 | log.Log("pap is up to date") 79 | os.Exit(0) 80 | 81 | return "" 82 | } 83 | 84 | func getExePath() string { 85 | exe, err := os.Executable() 86 | log.Error(err, "an error occurred while finding location of currently installed executable") 87 | log.Debug("executable path: %s", exe) 88 | 89 | evaluatedExe, err := filepath.EvalSymlinks(exe) 90 | log.Error(err, "an error occurred while locating location of original executable, perhaps a broken symlink") 91 | log.Debug("evaluated exe: %s", evaluatedExe) 92 | 93 | if runtime.GOOS == "windows" { 94 | log.Debug("running on windows, skipping path check") 95 | 96 | return evaluatedExe 97 | } 98 | 99 | home, err := os.UserHomeDir() 100 | homePath := filepath.Join(home, "/.local/bin/pap") 101 | 102 | log.Error(err, "an error occurred while getting the user's home directory") 103 | log.Debug("pap local: %s", homePath) 104 | 105 | if evaluatedExe != "/usr/bin" && evaluatedExe != homePath { 106 | log.Warn("it seems like you installed pap in a location not specified by the install guide (%s)", evaluatedExe) 107 | log.Warn("if this is expected, you can ignore this.") 108 | log.Continue("would like to continue?") 109 | } 110 | 111 | return evaluatedExe 112 | } 113 | 114 | func Update() { 115 | latest := checkIfNewUpdate() 116 | 117 | log.Log("finding exe...") 118 | 119 | exe := getExePath() 120 | url := fmt.Sprintf( 121 | "https://github.com/talwat/pap/releases/download/v%s/pap_%s_%s_%s", 122 | latest, 123 | latest, 124 | runtime.GOOS, 125 | runtime.GOARCH, 126 | ) 127 | 128 | tmpPath := fmt.Sprintf("/tmp/pap-update-%s", latest) 129 | 130 | net.Download( 131 | url, 132 | "release not found, your OS or architecture may not be supported", 133 | tmpPath, 134 | fmt.Sprintf("pap %s", latest), 135 | nil, 136 | fs.ExecutePerm, 137 | ) 138 | 139 | log.Log("installing pap...") 140 | fs.MoveFile(tmpPath, exe) 141 | 142 | log.Success("done! updated pap to %s", latest) 143 | } 144 | -------------------------------------------------------------------------------- /internal/jarfiles/forge/api.go: -------------------------------------------------------------------------------- 1 | package forge 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/talwat/pap/internal/jarfiles" 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | ) 10 | 11 | func BuildURL(minecraft *MinecraftVersion, installer *InstallerVersion) string { 12 | var returnURL string 13 | 14 | returnURL += "https://maven.minecraftforge.net/net/minecraftforge/forge" 15 | returnURL += fmt.Sprintf("/%s-%s", minecraft.String(), installer.Version) 16 | 17 | if minecraft.Minor == 8 || minecraft.Minor == 9 { 18 | returnURL += fmt.Sprintf("-%d.%d.%d", minecraft.Major, minecraft.Minor, minecraft.Patch) 19 | } else if minecraft.IsPrerelease { 20 | returnURL += "-prerelease" 21 | } 22 | 23 | returnURL += fmt.Sprintf("/forge-%s-%s-", minecraft.String(), installer.Version) 24 | 25 | // Forge versioning scheme changes with these two specific versions: 26 | // Versions 1.3 -> 1.7 and 1.10 -> latest use 27 | // MinecraftVersion-InstallerVersion/forge-MinecraftVersion-InstallerVersion-installer.jar 28 | // 1.19.4 45.0.57 1.19.4 45.0.57 29 | // 30 | // While 1.8 and 1.9 use a different scheme 31 | // MinecraftVersion-InstallerVersion-VersionTriple/forge-MinecraftVersion-InstallerVersion-VersionTriple-installer.jar 32 | // 1.9 12.16.1.1938 1.9.0 1.9 12.16.1.1938 1.9.0 33 | 34 | if minecraft.Minor == 8 || minecraft.Minor == 9 { 35 | returnURL += fmt.Sprintf("%d.%d.%d-", minecraft.Major, minecraft.Minor, minecraft.Patch) 36 | } else if minecraft.IsPrerelease { 37 | returnURL += "prerelease-" 38 | } 39 | 40 | returnURL += "installer.jar" 41 | 42 | return returnURL 43 | } 44 | 45 | func getPromotions() PromotionsSlim { 46 | var promotions PromotionsSlim 47 | 48 | log.Log("getting promotions...") 49 | 50 | net.Get( 51 | "https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json", 52 | "could not retrieve promotions", 53 | &promotions, 54 | ) 55 | 56 | return promotions 57 | } 58 | 59 | func getInstaller(version string, useLatestInstaller bool) (MinecraftVersion, InstallerVersion) { 60 | if useLatestInstaller { 61 | log.Debug("using latest installer version") 62 | 63 | return getSpecificInstaller(version, jarfiles.Latest) 64 | } 65 | 66 | promos := getPromotions() 67 | 68 | var minecraft MinecraftVersion 69 | 70 | if version == jarfiles.Latest { 71 | log.Debug("using latest minecraft version") 72 | 73 | minecraft = getLatestMinecraftVersion(&promos) 74 | } else { 75 | minecraft = parseMinecraftVersion(version) 76 | } 77 | 78 | installer := getVersion(&promos, &minecraft, "recommended") 79 | 80 | if (installer == InstallerVersion{}) { 81 | log.Continue("no recommended installer found for version %s. use the latest version?", minecraft.String()) 82 | } 83 | 84 | installer = getVersion(&promos, &minecraft, jarfiles.Latest) 85 | 86 | if (installer == InstallerVersion{}) { 87 | log.RawError("could not get a valid installer version") 88 | } 89 | 90 | return minecraft, installer 91 | } 92 | 93 | func getSpecificInstaller(version string, installer string) (MinecraftVersion, InstallerVersion) { 94 | promos := getPromotions() 95 | 96 | var minecraft MinecraftVersion 97 | 98 | if version == jarfiles.Latest { 99 | log.Debug("using latest minecraft version") 100 | 101 | minecraft = getLatestMinecraftVersion(&promos) 102 | } else { 103 | minecraft = parseMinecraftVersion(version) 104 | } 105 | 106 | if installer == jarfiles.Latest { 107 | log.Debug("using latest installer version") 108 | 109 | return minecraft, getVersion(&promos, &minecraft, "latest") 110 | } 111 | 112 | return minecraft, InstallerVersion{ 113 | Version: installer, 114 | } 115 | } 116 | 117 | // `golangci-lint` complains about *MinecraftVersion because this function only uses the string value 118 | // of the version. `interfacer` says we can use fmt.Stringer here, but that may lead to confusion. 119 | 120 | //nolint:interfacer 121 | func getVersion(promos *PromotionsSlim, minecraft *MinecraftVersion, installerType string) InstallerVersion { 122 | promo, found := promos.Promos[fmt.Sprintf("%s-%s", minecraft.String(), installerType)] 123 | 124 | if found { 125 | return InstallerVersion{ 126 | Version: promo, 127 | Type: installerType, 128 | } 129 | } 130 | 131 | return InstallerVersion{} 132 | } 133 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | // Logging and user input. 2 | package log 3 | 4 | import ( 5 | "bufio" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/talwat/pap/internal/global" 11 | "github.com/talwat/pap/internal/log/color" 12 | ) 13 | 14 | // Checks if err != nil and if so exits. 15 | func Error(err error, msg string, params ...interface{}) { 16 | if err != nil { 17 | RawError("%s: %s", fmt.Sprintf(msg, params...), err) 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | // Like Error but spits out a newline before. 23 | func NewlineBeforeError(err error, msg string, params ...interface{}) { 24 | if err != nil { 25 | RawLog("\n") 26 | RawError("%s: %s", fmt.Sprintf(msg, params...), err) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | // Basic log message including the pap: prefix. 32 | func Log(msg string, params ...interface{}) { 33 | RawLog("pap: %s\n", fmt.Sprintf(msg, params...)) 34 | } 35 | 36 | // Like Log but without a newline at the end. 37 | func NoNewline(msg string, params ...interface{}) { 38 | RawLog("pap: %s", fmt.Sprintf(msg, params...)) 39 | } 40 | 41 | // Basically just fmt.Fprintf. 42 | // Use this function in case pap decides to add a log file or whatever. 43 | func RawLog(msg string, params ...interface{}) { 44 | fmt.Fprintf(os.Stderr, msg, params...) 45 | } 46 | 47 | // Like RawLog, but prints to stdout instead. 48 | // 49 | // Note: This function outputs a trailing newline. 50 | func OutputLog(msg string, params ...interface{}) { 51 | fmt.Fprintf(os.Stdout, "%s\n", fmt.Sprintf(msg, params...)) 52 | } 53 | 54 | // Prints out an error message and exits regardless. 55 | func RawError(msg string, params ...interface{}) { 56 | Log("%serror%s: %s", color.BrightRed, color.Reset, fmt.Sprintf(msg, params...)) 57 | os.Exit(1) 58 | } 59 | 60 | // Prints out a warning. 61 | func Warn(msg string, params ...interface{}) { 62 | Log("%swarning%s: %s", color.Yellow, color.Reset, fmt.Sprintf(msg, params...)) 63 | } 64 | 65 | // Prints out a debug message. 66 | // Debug info should be internal info that makes it easier to debug pap. 67 | // Usually the information outputted here is completely useless to the end user. 68 | func Debug(msg string, params ...interface{}) { 69 | if global.Debug { 70 | Log("%sdebug%s: %s", color.Magenta, color.Reset, fmt.Sprintf(msg, params...)) 71 | } 72 | } 73 | 74 | // Like Debug, but with a newline before. 75 | func NewlineBeforeDebug(msg string, params ...interface{}) { 76 | if global.Debug { 77 | RawLog("\n") 78 | Log("%sdebug%s: %s", color.Magenta, color.Reset, fmt.Sprintf(msg, params...)) 79 | } 80 | } 81 | 82 | // Prints out a success message. 83 | // Whenever a "major" operation finishes, you can use this. 84 | func Success(msg string, params ...interface{}) { 85 | Log("%ssuccess%s: %s", color.Green, color.Reset, fmt.Sprintf(msg, params...)) 86 | } 87 | 88 | // Scans standard input until it reaches a newline. 89 | func RawScan() string { 90 | reader := bufio.NewReader(os.Stdin) 91 | text, err := reader.ReadString('\n') 92 | Error(err, "an error occurred while reading input") 93 | 94 | return strings.TrimSpace(text) 95 | } 96 | 97 | // Scan but it also handles the -y flag and has some nice looking logs. 98 | func Scan(defaultVal string, prompt string, params ...interface{}) string { 99 | NoNewline("%s (default %s): ", fmt.Sprintf(prompt, params...), defaultVal) 100 | 101 | if global.AssumeDefaultInput { 102 | RawLog("\n") 103 | Log("continuing with value %s because assume-default is turned on", defaultVal) 104 | 105 | return defaultVal 106 | } 107 | 108 | input := RawScan() 109 | 110 | if input == "" { 111 | return defaultVal 112 | } 113 | 114 | return input 115 | } 116 | 117 | // A yes or no prompt. 118 | // Different from Continue because it won't exit if you say no. 119 | func YesOrNo(defaultVal string, prompt string, params ...interface{}) bool { 120 | NoNewline("%s [y/n]: ", fmt.Sprintf(prompt, params...)) 121 | 122 | if global.AssumeDefaultInput { 123 | RawLog("\n") 124 | Log("choosing [%s] because assume-default is turned on", defaultVal) 125 | 126 | return true 127 | } 128 | 129 | input := strings.ToLower(RawScan()) 130 | 131 | if input == "" { 132 | input = defaultVal 133 | } 134 | 135 | return input == "y" 136 | } 137 | 138 | // A continue prompt. If the user puts out anything that isn't "y" (case insensitive), then it will exit. 139 | func Continue(prompt string, params ...interface{}) { 140 | NoNewline("%s [y/n]: ", fmt.Sprintf(prompt, params...)) 141 | 142 | if global.AssumeDefaultInput { 143 | RawLog("\n") 144 | Log("continuing because assume-default is turned on") 145 | 146 | return 147 | } 148 | 149 | input := strings.ToLower(RawScan()) 150 | 151 | if input != "y" { 152 | Log("aborting...") 153 | os.Exit(1) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /internal/cmd/script.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // The entire `script` command is defined here. 4 | // It could if deemed useful be split up into several files, 5 | // And put in it's own directory which is inside `internal`. 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/talwat/pap/internal/fs" 14 | "github.com/talwat/pap/internal/global" 15 | "github.com/talwat/pap/internal/log" 16 | "github.com/urfave/cli/v2" 17 | ) 18 | 19 | // Aikars defines aikars flags. 20 | // See https://docs.papermc.io/paper/aikars-flags for more info. 21 | // 22 | //nolint:gochecknoglobals 23 | var Aikars = []string{ 24 | "-XX:+UseG1GC", 25 | "-XX:+ParallelRefProcEnabled", 26 | "-XX:MaxGCPauseMillis=200", 27 | "-XX:+UnlockExperimentalVMOptions", 28 | "-XX:+DisableExplicitGC", 29 | "-XX:+AlwaysPreTouch", 30 | "-XX:G1HeapWastePercent=5", 31 | "-XX:G1MixedGCCountTarget=4", 32 | "-XX:G1MixedGCLiveThresholdPercent=90", 33 | "-XX:G1RSetUpdatingPauseTimePercent=5", 34 | "-XX:SurvivorRatio=32", 35 | "-XX:+PerfDisableSharedMem", 36 | "-XX:MaxTenuringThreshold=1", 37 | "-Dusing.aikars.flags=https://mcflags.emc.gs", 38 | "-Daikars.new.flags=true", 39 | } 40 | 41 | // LargeMemFlags are used if allocated memory is bigger than 12 GB. 42 | // 43 | //nolint:gochecknoglobals 44 | var LargeMemFlags = []string{ 45 | "-XX:G1NewSizePercent=40", 46 | "-XX:G1MaxNewSizePercent=50", 47 | "-XX:G1HeapRegionSize=16M", 48 | "-XX:G1ReservePercent=15", 49 | "-XX:InitiatingHeapOccupancyPercent=20", 50 | } 51 | 52 | // SmallMemFlags are used if allocated memory is smaller than 12 GB. 53 | // 54 | //nolint:gochecknoglobals 55 | var SmallMemFlags = []string{ 56 | "-XX:G1NewSizePercent=30", 57 | "-XX:G1MaxNewSizePercent=40", 58 | "-XX:G1HeapRegionSize=8M", 59 | "-XX:G1ReservePercent=20", 60 | "-XX:InitiatingHeapOccupancyPercent=15", 61 | } 62 | 63 | func memInputToMegabytes(memInput string) int { 64 | switch { 65 | case strings.HasSuffix(global.MemoryInput, "G"): // Memory is specified in gigabytes (G) 66 | log.Debug("using gigabytes as memory unit") 67 | 68 | gigabytes, err := strconv.Atoi(strings.TrimSuffix(memInput, "G")) 69 | log.Error(err, "invalid memory amount") 70 | 71 | // How many megabytes are in one gigabyte 72 | const MBInGB = 1000 73 | 74 | megabytes := gigabytes * MBInGB 75 | log.Debug("memory amount in megabytes: %d", megabytes) 76 | 77 | return megabytes 78 | case strings.HasSuffix(global.MemoryInput, "M"): // Memory is specified in megabytes (M) 79 | log.Debug("using megabytes as memory unit") 80 | 81 | megabytes, err := strconv.Atoi(strings.TrimSuffix(memInput, "M")) 82 | log.Error(err, "invalid memory amount") 83 | 84 | log.Debug("memory amount in megabytes: %d", megabytes) 85 | 86 | return megabytes 87 | default: 88 | log.RawError("memory value does not end with M (megabytes) or G (gigabytes)") 89 | 90 | return 0 91 | } 92 | } 93 | 94 | func generateAikars() []string { 95 | flagsToUse := Aikars 96 | 97 | // Specified RAM in megabytes 98 | ram := memInputToMegabytes(global.MemoryInput) 99 | 100 | // What is considered a lot of ram 101 | const largeRAM = 12000 102 | 103 | if ram > largeRAM { 104 | log.Debug("there is more than 12G of ram, using large ram flags") 105 | 106 | flagsToUse = append(flagsToUse, LargeMemFlags...) 107 | } else { 108 | log.Debug("there is less than 12G of ram, using small ram flags") 109 | 110 | flagsToUse = append(flagsToUse, SmallMemFlags...) 111 | } 112 | 113 | return flagsToUse 114 | } 115 | 116 | func generateCommand() string { 117 | // Base includes the base flags 118 | base := []string{ 119 | "-Xms" + global.MemoryInput, 120 | "-Xmx" + global.MemoryInput, 121 | } 122 | 123 | flagsToUse := base 124 | 125 | if global.AikarsInput { 126 | flagsToUse = append(flagsToUse, generateAikars()...) 127 | } 128 | 129 | flagsToUse = append(flagsToUse, fmt.Sprintf("-jar %s", global.JarInput)) 130 | 131 | if !global.GUIInput { 132 | flagsToUse = append(flagsToUse, "--nogui") 133 | } 134 | 135 | return fmt.Sprintf("java %s", strings.Join(flagsToUse, " ")) 136 | } 137 | 138 | func output(name string, text string) { 139 | if global.UseStdoutInput { 140 | log.OutputLog(text) 141 | } else { 142 | fs.WriteFile(name, text, fs.ExecutePerm) 143 | } 144 | 145 | log.Success("generated shell script!") 146 | } 147 | 148 | func ScriptCommand(cCtx *cli.Context) error { 149 | if global.JarInput == "" { 150 | log.RawError("the --jar option is required for the script command") 151 | } 152 | 153 | command := generateCommand() 154 | 155 | if runtime.GOOS == "windows" { 156 | output("run.bat", fmt.Sprintf("@ECHO OFF\n%s\npause", command)) 157 | } else { 158 | output("run.sh", fmt.Sprintf("#!/bin/sh\n%s", command)) 159 | } 160 | 161 | log.Log("go to aikars flags (https://docs.papermc.io/paper/aikars-flags) for more information on optimizing flags and tuning java") //nolint:lll 162 | 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /internal/plugins/sources/spigotmc/spigot.go: -------------------------------------------------------------------------------- 1 | package spigotmc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/talwat/pap/internal/log" 8 | "github.com/talwat/pap/internal/net" 9 | "github.com/talwat/pap/internal/plugins/sources" 10 | "github.com/talwat/pap/internal/plugins/sources/paplug" 11 | ) 12 | 13 | // Gets a plugins website by checking it's links. 14 | func getWebsite(plugin PluginInfo) string { 15 | switch { 16 | case plugin.SourceCodeLink != "": 17 | log.Debug("source code link isn't empty, using it (%s)", plugin.SourceCodeLink) 18 | 19 | return plugin.SourceCodeLink 20 | case plugin.DonationLink != "": 21 | log.Debug("donation link isn't empty, using it (%s)", plugin.DonationLink) 22 | 23 | return plugin.DonationLink 24 | default: 25 | url := fmt.Sprintf("https://www.spigotmc.org/resources/%d", plugin.ID) 26 | log.Debug("both donation link and plugin link are empty, falling back to the spigotmc page (%s)", url) 27 | 28 | return url 29 | } 30 | } 31 | 32 | // Gets a plugins author(s). 33 | func getAuthors(plugin PluginInfo) []string { 34 | if plugin.Contributors == "" { 35 | log.Debug("contributors is empty, using authors information (%s)", plugin.Resolved.Author.Name) 36 | 37 | return []string{plugin.Resolved.Author.Name} 38 | } 39 | 40 | log.Debug("contributors is not empty, splitting it by ', ' (%s)", plugin.Contributors) 41 | 42 | return strings.Split(plugin.Contributors, ", ") 43 | } 44 | 45 | // Gets a plugins license by checking if it has a source code link. 46 | func getLicense(plugin PluginInfo) string { 47 | if plugin.SourceCodeLink != "" { 48 | log.Debug("source code link is not empty, using unknown license") 49 | 50 | return sources.Undefined 51 | } 52 | 53 | log.Debug("source code link is empty, assuming app is proprietary") 54 | 55 | return "proprietary" 56 | } 57 | 58 | // Converts a spigotmc file into a paplug download. 59 | // Path is the parsed filename for the plugin jarfile. 60 | // NOTE: Any plugin that has an "external" download source cannot be downloaded. 61 | func ConvertDownload(plugin PluginInfo, path string) paplug.Download { 62 | download := paplug.Download{} 63 | download.Type = "url" 64 | download.Filename = path 65 | 66 | if !plugin.Premium && plugin.File.FileType == ".jar" { 67 | log.Debug("%s has a direct download and isn't premium, adding download", plugin.Name) 68 | download.URL = fmt.Sprintf("https://api.spiget.org/v2/resources/%d/download", plugin.ID) 69 | } else { 70 | log.Debug("%s is either premium or doesn't have a .jar filetype", plugin.Name) 71 | download.URL = sources.Undefined 72 | log.Warn( 73 | "%s does not support downloading. if you are downloading %s as a plugin, you will get an error", 74 | plugin.Name, 75 | plugin.Name, 76 | ) 77 | } 78 | 79 | return download 80 | } 81 | 82 | // Converts a spigotmc plugin into the paplug format. 83 | func ConvertToPlugin(spigotPlugin PluginInfo) paplug.PluginInfo { 84 | plugin := paplug.PluginInfo{} 85 | 86 | plugin.Name = sources.FormatName(spigotPlugin.Name) 87 | plugin.Description = sources.FormatDesc(spigotPlugin.Tag) 88 | plugin.Site = getWebsite(spigotPlugin) 89 | plugin.Authors = getAuthors(spigotPlugin) 90 | plugin.License = getLicense(spigotPlugin) 91 | 92 | plugin.Install.Type = "simple" 93 | 94 | plugin.Version = spigotPlugin.Resolved.LatestVersion.Name 95 | 96 | // Unknown vars 97 | plugin.Note = []string{} 98 | plugin.Dependencies = []string{} 99 | plugin.OptionalDependencies = []string{} 100 | 101 | // File & Download 102 | path := fmt.Sprintf("%s.jar", plugin.Name) 103 | 104 | log.Debug("plugin jarfile path: %s", path) 105 | 106 | // File 107 | log.Debug("adding uninstall file...") 108 | 109 | file := paplug.File{} 110 | file.Path = path 111 | file.Type = "main" 112 | 113 | plugin.Uninstall.Files = append(plugin.Uninstall.Files, file) 114 | 115 | // Download 116 | log.Debug("adding download...") 117 | 118 | plugin.Downloads = append(plugin.Downloads, ConvertDownload(spigotPlugin, path)) 119 | 120 | return plugin 121 | } 122 | 123 | // Gets a raw spigotmc plugin. 124 | func Get(name string) PluginInfo { 125 | var plugins []PluginInfo 126 | 127 | net.Get( 128 | //nolint:lll 129 | fmt.Sprintf("https://api.spiget.org/v2/search/resources/%s?field=name&size=1&page=0&sort=-likes&fields=file,contributors,likes,name,tag,sourceCodeLink,donationLink,premium,id,version,author", name), 130 | fmt.Sprintf("spigot plugin %s not found", name), 131 | &plugins, 132 | ) 133 | 134 | // Gets the first plugin because it's sorted by likes, so it should be fine. 135 | // Also spigotmc doesn't really have slugs. 136 | plugin := plugins[0] 137 | 138 | if plugin.Contributors == "" { 139 | net.Get( 140 | fmt.Sprintf("https://api.spiget.org/v2/authors/%d?fields=name", plugin.Author.ID), 141 | fmt.Sprintf("spigot author %d not found", plugin.Author.ID), 142 | &plugin.Resolved.Author, 143 | ) 144 | } 145 | 146 | version := plugin.Version.ID 147 | 148 | net.Get( 149 | fmt.Sprintf("https://api.spiget.org/v2/resources/%d/versions/%d?fields=name", plugin.ID, version), 150 | fmt.Sprintf("spigot version %d not found", version), 151 | &plugin.Resolved.LatestVersion, 152 | ) 153 | 154 | return plugin 155 | } 156 | 157 | // Gets & converts to the standard pap format. 158 | func GetPluginInfo(name string) paplug.PluginInfo { 159 | return ConvertToPlugin(Get(name)) 160 | } 161 | -------------------------------------------------------------------------------- /internal/plugins/get.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/talwat/pap/internal/fs" 9 | "github.com/talwat/pap/internal/log" 10 | "github.com/talwat/pap/internal/net" 11 | "github.com/talwat/pap/internal/plugins/sources/bukkit" 12 | "github.com/talwat/pap/internal/plugins/sources/modrinth" 13 | "github.com/talwat/pap/internal/plugins/sources/paplug" 14 | "github.com/talwat/pap/internal/plugins/sources/spigotmc" 15 | ) 16 | 17 | // Trim plugin name of a list of prefixes. 18 | func trimPluginName(name string, prefixes []string) string { 19 | trimmedName := name 20 | 21 | for _, prefix := range prefixes { 22 | trimmedName = strings.TrimPrefix(trimmedName, prefix) 23 | } 24 | 25 | return trimmedName 26 | } 27 | 28 | // Get plugin info & strip out the prefix (modrinth:, spigotmc:, etc...). 29 | func getPluginFromSource( 30 | name string, 31 | source string, 32 | prefixes []string, 33 | getPluginInfo func(name string) paplug.PluginInfo, 34 | ) paplug.PluginInfo { 35 | pluginName := trimPluginName(name, prefixes) 36 | 37 | info := getPluginInfo(pluginName) 38 | info.Source = source 39 | 40 | return info 41 | } 42 | 43 | // Get using a URL, eg: 'https://example.com/plugin.json'. 44 | func getURL(name string, info *paplug.PluginInfo) { 45 | log.Debug("using url (%s)", name) 46 | net.Get(name, fmt.Sprintf("plugin at %s not found", name), &info) 47 | 48 | info.URL = name 49 | } 50 | 51 | // Get using a local file, eg: 'plugins/plugin.json'. 52 | func getLocal(name string, info *paplug.PluginInfo) { 53 | log.Debug("using local json file (%s)", name) 54 | raw := fs.ReadFile(name) 55 | 56 | log.Debug("unmarshaling %s...", name) 57 | 58 | err := json.Unmarshal(raw, &info) 59 | log.Error(err, "an error occurred while parsing %s", name) 60 | 61 | info.Path = name 62 | } 63 | 64 | // Get from modrinth, eg: 'modrinth:plugin'. 65 | func getModrinth(name string, info *paplug.PluginInfo) { 66 | log.Debug("using modrinth (%s)", name) 67 | *info = getPluginFromSource( 68 | name, 69 | "modrinth", 70 | []string{"modrinth:"}, 71 | modrinth.GetPluginInfo, 72 | ) 73 | } 74 | 75 | // Get from spigotmc, eg: 'spigotmc:plugin'. 76 | func getSpigotmc(name string, info *paplug.PluginInfo) { 77 | log.Debug("using spigotmc (%s)", name) 78 | *info = getPluginFromSource( 79 | strings.ReplaceAll(name, "_", " "), 80 | "spigotmc", 81 | []string{"spigot:", "spigotmc:"}, 82 | spigotmc.GetPluginInfo, 83 | ) 84 | } 85 | 86 | // Get from bukkitdev, eg: 'bukkitdev:plugin'. 87 | func getBukkitdev(name string, info *paplug.PluginInfo) { 88 | log.Debug("using bukkitdev (%s)", name) 89 | *info = getPluginFromSource( 90 | name, 91 | "bukkit", 92 | []string{"bukkit:", "bukkitdev:"}, 93 | bukkit.GetPluginInfo, 94 | ) 95 | } 96 | 97 | // Get from the repositories, eg: 'plugin'. 98 | func getRepos(name string, info *paplug.PluginInfo) { 99 | log.Debug("using repos (%s)", name) 100 | net.Get( 101 | fmt.Sprintf( 102 | "https://raw.githubusercontent.com/talwat/pap/main/plugins/%s.json", 103 | name, 104 | ), 105 | fmt.Sprintf("plugin %s not found", name), 106 | &info, 107 | ) 108 | } 109 | 110 | // This function will call itself in case of an alias. 111 | func GetPluginInfo(name string) paplug.PluginInfo { 112 | var info paplug.PluginInfo 113 | 114 | switch { 115 | // If it's a url using http then use this: 116 | case strings.HasPrefix(name, "https://") || strings.HasPrefix(name, "http://"): 117 | getURL(name, &info) 118 | 119 | // If it's file which ends in .json try reading it locally: 120 | case strings.HasSuffix(name, ".json"): 121 | getLocal(name, &info) 122 | 123 | // If it's a modrinth plugin try getting it from modrinth: 124 | case strings.HasPrefix(name, "modrinth:"): 125 | getModrinth(name, &info) 126 | 127 | // If it's a spigot plugin try getting it from spigotmc: 128 | case strings.HasPrefix(name, "spigot:"), 129 | strings.HasPrefix(name, "spigotmc:"): 130 | getSpigotmc(name, &info) 131 | 132 | // If it's a bukkit plugin try getting it from bukkit: 133 | case strings.HasPrefix(name, "bukkit:"), 134 | strings.HasPrefix(name, "bukkitdev:"): 135 | getBukkitdev(name, &info) 136 | 137 | // If it's none of the options above try getting it from the repos: 138 | default: 139 | getRepos(name, &info) 140 | } 141 | 142 | if info.Alias != "" { 143 | log.Warn("%s is an alias to %s", name, info.Alias) 144 | 145 | return GetPluginInfo(info.Alias) 146 | } 147 | 148 | return info 149 | } 150 | 151 | // Gets the information for a list of plugins. 152 | // The plugins it gets the information for can also be dependencies or optional dependencies. 153 | // checkInstalled will also verify if they are installed before returning information for them. 154 | func GetManyPluginInfo( 155 | plugins []string, 156 | isDependencies bool, 157 | isOptionalDependencies bool, 158 | checkInstalled bool, 159 | ) []paplug.PluginInfo { 160 | pluginsInfo := []paplug.PluginInfo{} 161 | 162 | for _, plugin := range plugins { 163 | if strings.HasPrefix(plugin, "-") { 164 | log.Warn("you need to put the flag (%s) before", plugin) 165 | 166 | continue 167 | } 168 | 169 | info := GetPluginInfo(plugin) 170 | 171 | if paplug.PluginExists(info, pluginsInfo) { 172 | continue 173 | } 174 | 175 | if checkInstalled { 176 | if CheckIfInstalled(info) { 177 | continue 178 | } 179 | } 180 | 181 | if isDependencies { 182 | info.IsDependency = true 183 | } 184 | 185 | if isOptionalDependencies { 186 | info.IsOptionalDependency = true 187 | } 188 | 189 | pluginsInfo = append(pluginsInfo, info) 190 | } 191 | 192 | return pluginsInfo 193 | } 194 | -------------------------------------------------------------------------------- /PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## Table of contents 4 | 5 | - [Plugins](#plugins) 6 | - [Table of contents](#table-of-contents) 7 | - [A plugin is out of date](#a-plugin-is-out-of-date) 8 | - [A plugin isn't working](#a-plugin-isnt-working) 9 | - [Creating a plugin](#creating-a-plugin) 10 | - [Getting a starting point](#getting-a-starting-point) 11 | - [Fields](#fields) 12 | - [`name`](#name) 13 | - [`version`](#version) 14 | - [`description`](#description) 15 | - [`license`](#license) 16 | - [`authors`](#authors) 17 | - [`site` _(optional)_](#site-optional) 18 | - [`dependencies` _(optional)_](#dependencies-optional) 19 | - [`optionalDependencies` _(optional)_](#optionaldependencies-optional) 20 | - [`downloads`](#downloads) 21 | - [Jenkins](#jenkins) 22 | - [URL](#url) 23 | - [Full example](#full-example) 24 | - [`install`](#install) 25 | - [`uninstall`](#uninstall) 26 | - [`note` _(optional)_](#note-optional) 27 | - [Testing your plugin](#testing-your-plugin) 28 | 29 | ## A plugin is out of date 30 | 31 | If you notice a plugin is out of date, usually you can update it by simply changing the `version` property to whatever the current version is. 32 | 33 | ## A plugin isn't working 34 | 35 | If a plugin is broken, please create an [issue](https://github.com/talwat/pap/issues). 36 | 37 | ## Creating a plugin 38 | 39 | Creating a plugin is very easy, and most of the information asked can be found in `plugin.yml`. 40 | 41 | Every plugin has a json file in the `plugins` directory. This file tells pap how to install it, uninstall it, and so on. 42 | 43 | You can see [`plugins/example.jsonc`](plugins/example.jsonc) for a commented example on how to create a plugin and each field needed. 44 | 45 | Or, continue here for more detailed explanations. 46 | 47 | ### Getting a starting point 48 | 49 | You can vastly speed up the proccess by generating a plugin if it's on [modrinth](https://modrinth.com/) or [spigotmc](https://spigotmc.org/). Just run: 50 | 51 | ```sh 52 | pap plugin generate modrinth 53 | ``` 54 | 55 | or if your plugin is on spigotmc: 56 | 57 | ```sh 58 | pap plugin generate spigotmc 59 | ``` 60 | 61 | ### Fields 62 | 63 | Each plugin has some fields that give metadata & important information, this section lists them. 64 | 65 | #### `name` 66 | 67 | The name of the plugin. This should be all lowercase, without spaces. 68 | 69 | The name of the plugin should also be identical to the json file itself. 70 | 71 | #### `version` 72 | 73 | The version of the plugin. Use `latest` if you either: 74 | 75 | - Use jenkins to automatically build and distribute your plugin. 76 | - Have a static link to the latest version of your plugin that doesn't change. 77 | 78 | If you don't meet ethier of those requirements, you can pick a version that matches your versioning scheme, and then access it in a URL by using `{version}`. 79 | 80 | For example: 81 | 82 | ```json 83 | { 84 | "version": "0.1" 85 | } 86 | ``` 87 | 88 | #### `description` 89 | 90 | A short description of your plugin, it should end with a period (`.`) 91 | 92 | #### `license` 93 | 94 | The license your plugin uses. 95 | 96 | If you actually have a license, use it's [SPDX Identifier](https://spdx.org/licenses/). 97 | 98 | If the plugin is proprietary, use `proprietary`. 99 | 100 | If you have no idea what license it uses, just use `unknown`. 101 | 102 | #### `authors` 103 | 104 | A list of authors that created the plugin. 105 | 106 | This can be usernames, real names, etc... 107 | 108 | #### `site` _(optional)_ 109 | 110 | The site of the plugin, this is optional. 111 | 112 | If you don't have a website, this can also be your repository. 113 | 114 | You should include the protocol (usually `https://`) 115 | 116 | For example: 117 | 118 | ```json 119 | { 120 | "site": "https://www.example.com" 121 | } 122 | ``` 123 | 124 | #### `dependencies` _(optional)_ 125 | 126 | The dependencies of your plugin. This is only optional if you don't have any. 127 | 128 | If your plugin has a dependency that isn't in pap yet, you can either: 129 | 130 | 1. Implement that plugin yourself 131 | 2. Or open an [issue](https://github.com/talwat/pap/issues) if it's a very common dependency 132 | 133 | This is a list of strings. 134 | 135 | For example: 136 | 137 | ```json 138 | { 139 | "dependencies": ["exampledependency"] 140 | } 141 | ``` 142 | 143 | #### `optionalDependencies` _(optional)_ 144 | 145 | An optional dependency of your plugin. This enhances functionality or adds new features. 146 | 147 | For example: 148 | 149 | ```json 150 | { 151 | "optionalDependencies": ["vault"] 152 | } 153 | ``` 154 | 155 | #### `downloads` 156 | 157 | This is **the** most important part. 158 | 159 | This is a list of downloads, so you can have multiple. 160 | 161 | Downloads have two types: `jenkins` and `urls`. 162 | 163 | As mentioned in the [`version`](#version) field, you can use jenkins of a fixed url. 164 | 165 | This is defined in the `type` attribute, so it can be `jenkins` or `url`. 166 | 167 | The `filename` attribute defines what name to save the downloaded file as, so it's predictable. 168 | 169 | ##### Jenkins 170 | 171 | If you are using jenkins, you can define your job in the `job` property. 172 | 173 | Additionally, you can select an artifact with the `artifact` property, which is a regex. 174 | 175 | Please only use basic regex's, because more complex ones hurt compatibility. 176 | 177 | For example: 178 | 179 | ```json 180 | { 181 | "type": "jenkins", 182 | "job": "https://ci.athion.net/job/Example", 183 | "artifact": "jarfile-bukkit-v.*", 184 | "filename": "plugin.jar" 185 | } 186 | ``` 187 | 188 | ##### URL 189 | 190 | If you are using the `url` method, just define the `url` property as your URL. 191 | 192 | You can use `{version}` in the URL which will be substituted with whatever the `version` property is set to. 193 | 194 | For example: 195 | 196 | ```json 197 | { 198 | "type": "url", 199 | "url": "https://www.example.com/plugin-{version}.jar", 200 | "filename": "plugin.jar" 201 | } 202 | ``` 203 | 204 | ##### Full example 205 | 206 | ```json 207 | { 208 | "downloads": [ 209 | { 210 | "type": "jenkins", 211 | "job": "https://ci.athion.net/job/Example", 212 | "artifact": "jarfile-bukkit-.*", 213 | "filename": "plugin.jar" 214 | }, 215 | { 216 | "type": "url", 217 | "url": "https://www.example.com/plugin-{version}.jar", 218 | "filename": "plugin.jar" 219 | } 220 | ] 221 | } 222 | ``` 223 | 224 | #### `install` 225 | 226 | If your plugin just downloads a jarfile like most, you can get away with setting the `type` attribute to `simple` which downloads the jarfile and exits. 227 | 228 | For example: 229 | 230 | ```json 231 | { 232 | "install": { 233 | "type": "simple" 234 | } 235 | } 236 | ``` 237 | 238 | If you need to unzip a file or run some commands, you can use `complex` for the `type` attribute. 239 | 240 | This allows you to define commands to run on windows and unix like operating systems. 241 | 242 | On unix, `sh` is used for the shell. On windows, it's `powershell`. 243 | 244 | For example (non functional): 245 | 246 | ```json 247 | { 248 | "install": { 249 | "type": "complex", 250 | "commands": { 251 | "windows": ["move my_plugin/*.jar .", "rmdir my_plugin"], 252 | "unix": ["mv my_plugin/*.jar .", "rm -rf my_plugin"] 253 | } 254 | } 255 | } 256 | ``` 257 | 258 | #### `uninstall` 259 | 260 | How to uninstall your plugin. 261 | 262 | You do this by defining some files/directories to delete. 263 | 264 | Each file has a `path` which is relative to the `plugins` directory, and a `type` which can be `main`, `config`, or `data`. It can also be `other`. 265 | 266 | For example: 267 | 268 | ```json 269 | { 270 | "uninstall": { 271 | "files": [ 272 | { 273 | "type": "main", 274 | "path": "myPlugin.jar" 275 | }, 276 | { 277 | "type": "config", 278 | "path": "myPlugin" 279 | } 280 | ] 281 | } 282 | } 283 | ``` 284 | 285 | #### `note` _(optional)_ 286 | 287 | The `note` attribute will be displayed at the end of the command, and is useful for displaying key information. 288 | 289 | For example, if your plugin needs a specific property to be turned off/on, mention it here. 290 | 291 | It is an array, and each item will be displayed on a seperate line. 292 | 293 | Example: 294 | 295 | ```json 296 | { 297 | "note": [ 298 | "you need to disable pvp for this plugin to work correctly", 299 | "or else bad things will happen" 300 | ] 301 | } 302 | ``` 303 | 304 | ### Testing your plugin 305 | 306 | Now that your plugin file is complete, it's time to test. 307 | 308 | First, make a `test` directory so you don't accidentally put downloaded files in the wrong place. 309 | 310 | This directory is on the `.gitignore` so you don't have to worry about any binary files being commited. 311 | 312 | ```sh 313 | mkdir test 314 | cd test 315 | ``` 316 | 317 | Then, you can run the plugin install command but with a path instead: 318 | 319 | ```sh 320 | pap plugin install ../plugins/myplugin.json 321 | ``` 322 | 323 | And then create a PR. 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pap 2 | 3 | [![codebeat badge](https://codebeat.co/badges/95ce3938-9084-418c-b8fe-8093f6292d28)](https://codebeat.co/projects/github-com-talwat-pap-main) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/talwat/pap)](https://goreportcard.com/report/github.com/talwat/pap) 5 | [![AUR version](https://img.shields.io/aur/version/pap)](https://aur.archlinux.org/packages/pap) 6 | [![License](https://img.shields.io/github/license/talwat/pap)](https://github.com/talwat/pap/blob/main/LICENSE) 7 | ![Go version](https://img.shields.io/github/go-mod/go-version/talwat/pap) 8 | 9 | A swiss army knife for minecraft servers. 10 | 11 | ## pap is close to 1.0 🎉 12 | 13 | pap is now feature complete (for now) and just needs testing & code reviewing. 14 | 15 | If you want, try installing pap and messing around with it. 16 | 17 | If you actually manage to break it, [open an issue](https://github.com/talwat/pap/issues). 18 | 19 | Or, make a PR. 20 | 21 | ## Table of contents 22 | 23 | - [pap](#pap) 24 | - [pap is close to 1.0 🎉](#pap-is-close-to-10-) 25 | - [Table of contents](#table-of-contents) 26 | - [Examples](#examples) 27 | - [Demo](#demo) 28 | - [Download the latest papermc jarfile](#download-the-latest-papermc-jarfile) 29 | - [Sign the EULA](#sign-the-eula) 30 | - [Generate a script to run the jarfile](#generate-a-script-to-run-the-jarfile) 31 | - [Turn off pvp](#turn-off-pvp) 32 | - [Install worldedit](#install-worldedit) 33 | - [Install a plugin from bukkit, spigot, or modrinth](#install-a-plugin-from-bukkit-spigot-or-modrinth) 34 | - [Why though?](#why-though) 35 | - [Install](#install) 36 | - [Build Dependencies](#build-dependencies) 37 | - [Arch linux](#arch-linux) 38 | - [Ubuntu](#ubuntu) 39 | - [Unix](#unix) 40 | - [Unix - From Releases](#unix---from-releases) 41 | - [Unix - System wide from releases](#unix---system-wide-from-releases) 42 | - [Unix - Local from releases](#unix---local-from-releases) 43 | - [Unix - From Source](#unix---from-source) 44 | - [Unix - System wide from source](#unix---system-wide-from-source) 45 | - [Unix - Local from source](#unix---local-from-source) 46 | - [Windows](#windows) 47 | - [Windows - From Releases _(recommended)_](#windows---from-releases-recommended) 48 | - [Windows - From Source](#windows---from-source) 49 | - [Common issues](#common-issues) 50 | - [Local installation not found](#local-installation-not-found) 51 | - [Bash](#bash) 52 | - [Zsh](#zsh) 53 | - [Fish](#fish) 54 | - [Updating pap](#updating-pap) 55 | - [Uninstalling pap](#uninstalling-pap) 56 | - [Contributing](#contributing) 57 | - [Dependencies](#dependencies) 58 | - [Packaging](#packaging) 59 | 60 | ## Examples 61 | 62 | ### Demo 63 | 64 | _Click on the gif for a higher resolution version._ 65 | 66 | [![Demo of pap](/media/pap.gif)](https://asciinema.org/a/574226) 67 | 68 | ### Download the latest papermc jarfile 69 | 70 | ```sh 71 | pap download paper 72 | ``` 73 | 74 | ### Sign the EULA 75 | 76 | ```sh 77 | pap sign 78 | ``` 79 | 80 | ### Generate a script to run the jarfile 81 | 82 | ```sh 83 | pap script --jar server.jar 84 | ``` 85 | 86 | ### Turn off pvp 87 | 88 | ```sh 89 | pap properties set pvp false 90 | ``` 91 | 92 | ### Install worldedit 93 | 94 | ```sh 95 | pap plugin install worldedit 96 | ``` 97 | 98 | ### Install a plugin from bukkit, spigot, or modrinth 99 | 100 | ```sh 101 | pap plugin install bukkit:holographic-displays 102 | pap plugin install spigot:death_signs 103 | pap plugin install modrinth:chunky 104 | ``` 105 | 106 | ## Why though? 107 | 108 | pap has a few purposes: 109 | 110 | - To simplify some of the common tasks you need to do when creating or managing a server (such as when you download/update the server jar.) 111 | - To easily and automatically verify the jars you download to avoid bad issues down the line. 112 | - To provide an easy CLI to do common tasks like changing server.properties and signing EULA, for usage in scripts. 113 | - To quickly install plugins directly from their sources. 114 | 115 | ## Install 116 | 117 | ### Build Dependencies 118 | 119 | If you are obtaining pap from source, you will need these dependencies: 120 | 121 | - [Go](https://go.dev/) 1.18 or later 122 | - [Git](https://git-scm.com/) 123 | - [Make](https://en.wikipedia.org/wiki/Make_(software)) 124 | 125 | ### Arch linux 126 | 127 | > **Info** 128 | > The AUR build might not have the latest version of pap, but it may be more stable. 129 | 130 | If you wish, pap can be installed from the AUR: 131 | 132 | ```sh 133 | yay -S pap 134 | ``` 135 | 136 | ### Ubuntu 137 | 138 | > **Info** 139 | > Pacstall might not have the latest version of pap, but it may be more stable. 140 | 141 | Thank you so much to [Henryws](https://github.com/Henryws) for keeping pap up to date on pacstall. 142 | 143 | If you have [pacstall](https://github.com/pacstall/pacstall), you can run: 144 | 145 | ```bash 146 | pacstall -I pap 147 | ``` 148 | 149 | ### Unix 150 | 151 | #### Unix - From Releases 152 | 153 | You can go to the [latest release](https://github.com/talwat/pap/releases/latest) 154 | and download the fitting binary for your system from there. 155 | 156 | pap is available on most architectures and operating systems, so you will rarely need to compile it from source. 157 | 158 | ##### Unix - System wide from releases 159 | 160 | ```sh 161 | sudo install -Dm755 pap* /usr/bin/pap 162 | ``` 163 | 164 | ##### Unix - Local from releases 165 | 166 | > **Warning** 167 | > You may see an error that pap wasn't found, if you see this you may not have `~/.local/bin/` in your PATH. 168 | > See [common issues](#local-installation-not-found) on how to add it. 169 | 170 | ```sh 171 | install -Dm755 pap* ~/.local/bin/pap 172 | ``` 173 | 174 | #### Unix - From Source 175 | 176 | > **Warning** 177 | > `pap update` downloads and installs a binary, it does not compile it from source. 178 | > If you need to compile pap from source, don't use `pap update`. 179 | 180 | First, clone pap: 181 | 182 | ```sh 183 | git clone https://github.com/talwat/pap 184 | cd pap 185 | ``` 186 | 187 | Switch to the latest tag _(optional)_: 188 | 189 | ```sh 190 | git tag # get all tags 191 | git checkout 192 | ``` 193 | 194 | And then build: 195 | 196 | ```sh 197 | make 198 | ``` 199 | 200 | Finally, move it into your binary directory: 201 | 202 | ##### Unix - System wide from source 203 | 204 | ```sh 205 | sudo make install PREFIX=/usr 206 | ``` 207 | 208 | ##### Unix - Local from source 209 | 210 | > **Warning** 211 | > You may see an error that pap wasn't found, if you see this you may not have `~/.local/bin/` in your PATH. 212 | > See [common issues](#local-installation-not-found) on how to add it. 213 | 214 | ```sh 215 | make install 216 | ``` 217 | 218 | ### Windows 219 | 220 | pap **does** work on windows, but windows has a ~~bad~~ different way to CLI apps. 221 | 222 | #### Windows - From Releases _(recommended)_ 223 | 224 | If you want to download from releases, download the fitting windows exe and [put it into path](https://stackoverflow.com/questions/4822400/register-an-exe-so-you-can-run-it-from-any-command-line-in-windows#:~:text=Go%20to%20%22My%20computer%20%2D%3E,exe%20's%20directory%20into%20path.). 225 | 226 | #### Windows - From Source 227 | 228 | First, clone pap: 229 | 230 | ```sh 231 | git clone https://github.com/talwat/pap 232 | cd pap 233 | ``` 234 | 235 | Switch to the latest tag _(optional)_: 236 | 237 | ```sh 238 | git tag # get all tags 239 | git checkout 240 | ``` 241 | 242 | And then build: 243 | 244 | ```sh 245 | # Use `make` if you have it, otherwise: 246 | 247 | mkdir -vp build 248 | go build -o build 249 | ``` 250 | 251 | Finally, [put it into path](https://stackoverflow.com/questions/4822400/register-an-exe-so-you-can-run-it-from-any-command-line-in-windows#:~:text=Go%20to%20%22My%20computer%20%2D%3E,exe%20's%20directory%20into%20path.). 252 | 253 | ### Common issues 254 | 255 | #### Local installation not found 256 | 257 | Usually this is because `~/.local/bin` is not in PATH. 258 | 259 | You can add `~/.local/bin` to PATH through your shell: 260 | 261 | ##### Bash 262 | 263 | ```sh 264 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc 265 | ``` 266 | 267 | ##### Zsh 268 | 269 | ```sh 270 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc 271 | ``` 272 | 273 | ##### Fish 274 | 275 | Look at the [fish docs](https://fishshell.com/docs/current/tutorial.html#path) for more detailed instructions. 276 | 277 | ```sh 278 | fish_add_path $HOME/.local/bin 279 | ``` 280 | 281 | ## Updating pap 282 | 283 | > **Note** 284 | > This will only work if you are running version 0.11.0 or higher. If not, just reinstall using the install guide. 285 | 286 | If you used a release and followed the install guide, you should be able to simply run: 287 | 288 | ```sh 289 | sudo pap update 290 | ``` 291 | 292 | or if you did a local install: 293 | 294 | ```sh 295 | pap update 296 | ``` 297 | 298 | ## Uninstalling pap 299 | 300 | Simply delete the binary file you installed. pap does not create any files that you do not explicitly 301 | tell it to. 302 | 303 | So, if you did a system wide install do: 304 | 305 | ```sh 306 | sudo rm /usr/bin/pap 307 | ``` 308 | 309 | or if you did a local install: 310 | 311 | ```sh 312 | rm ~/.local/bin/pap 313 | ``` 314 | 315 | ## Contributing 316 | 317 | Anyone is welcome to contribute, and if someone can port pap to various package managers, it would be greatly appreciated. 318 | 319 | If you want more info about how to contribute, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). 320 | 321 | If you would like to add a plugin to the repository, take a look at [PLUGINS.md](PLUGINS.md). 322 | 323 | If you like pap, feel free to [star it on github](https://github.com/talwat/pap), or [vote for it on the AUR](https://aur.archlinux.org/packages/pap). 324 | 325 | ## Dependencies 326 | 327 | - [urfave/cli](https://github.com/urfave/cli) 328 | 329 | ## Packaging 330 | 331 | If you would like to package & submit pap to a repository, [open an issue](https://github.com/talwat/pap/issues). 332 | 333 | [![Packaging status](https://repology.org/badge/vertical-allrepos/pap.svg)](https://repology.org/project/pap/versions) 334 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: testing 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | unit-testing: 14 | strategy: 15 | matrix: 16 | go: ['1.18', '1.20'] 17 | os: [ubuntu-latest] 18 | 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - uses: actions/checkout@v3 26 | 27 | - run: "go test ./..." 28 | 29 | testing: 30 | strategy: 31 | matrix: 32 | go: ['1.18', '1.20'] 33 | os: [macos-latest, windows-latest, ubuntu-latest] 34 | 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - uses: actions/setup-go@v3 39 | with: 40 | go-version: ${{ matrix.go }} 41 | 42 | - uses: actions/checkout@v3 43 | 44 | - name: "Build pap" 45 | run: | 46 | make 47 | make install PREFIX=./ 48 | tree || find . 49 | ls 50 | ls bin 51 | 52 | - name: "Run help" 53 | run: "./bin/pap -y -d help" 54 | 55 | - name: "Test plugin manager" 56 | run: | 57 | ./bin/pap -y -d plugin install townyadvanced fawe 58 | ls plugins 59 | ./bin/pap -y -d plugin uninstall townyadvanced fawe 60 | ls plugins 61 | ./bin/pap -y -d plugin install townyadvanced townychat 62 | ls plugins 63 | ./bin/pap -y -d plugin install geyser 64 | ls plugins 65 | ./bin/pap -y -d plugin install --optional geyser 66 | ls plugins 67 | ./bin/pap -y -d plugin install modrinth:essentialsx 68 | ls plugins 69 | ./bin/pap -y -d plugin install spigot:death_signs 70 | ls plugins 71 | ./bin/pap -y -d plugin install bukkit:holographic-displays 72 | ls plugins 73 | ./bin/pap -y -d plugin install --experimental bukkit:worldguard 74 | ls plugins 75 | 76 | - name: "Test plugin manager generator" 77 | run: | 78 | ./bin/pap -y -d plugin generate --stdout spigotmc essentialsx 79 | ./bin/pap -y -d plugin generate --stdout modrinth essentialsx 80 | ./bin/pap -y -d plugin generate --stdout bukkit holographic-displays 81 | 82 | ./bin/pap -y -d plugin generate spigotmc essentialsx 83 | tree || find . 84 | cat essentialsx.json 85 | ./bin/pap -y -d plugin generate modrinth essentialsx 86 | tree || find . 87 | cat essentialsx.json 88 | ./bin/pap -y -d plugin generate bukkit holographic-displays 89 | tree || find . 90 | cat holographic-displays.json 91 | 92 | - run: "./bin/pap -y -d sign" 93 | - name: Read Signed EULA 94 | run: "cat eula.txt" 95 | 96 | - run: "./bin/pap -y -d script --jar server.jar" 97 | - run: "./bin/pap -y -d script --jar server.jar --stdout" 98 | - name: Read Generated Script (windows) 99 | if: matrix.os == 'windows-latest' 100 | run: "cat run.bat" 101 | - name: Read Generated Script (unix) 102 | if: matrix.os != 'windows-latest' 103 | run: "cat run.sh" 104 | 105 | - run: "./bin/pap -y -d script --jar server.jar --mem 2G" 106 | - run: "./bin/pap -y -d script --jar server.jar --mem 2G --stdout" 107 | - name: Read Generated Script (windows) 108 | if: matrix.os == 'windows-latest' 109 | run: "cat run.bat" 110 | - name: Read Generated Script (unix) 111 | if: matrix.os != 'windows-latest' 112 | run: "cat run.sh" 113 | 114 | - run: "./bin/pap -y -d script --jar server.jar --mem 13G" 115 | - run: "./bin/pap -y -d script --jar server.jar --mem 13G --stdout" 116 | - name: Read Generated Script (windows) 117 | if: matrix.os == 'windows-latest' 118 | run: "cat run.bat" 119 | - name: Read Generated Script (unix) 120 | if: matrix.os != 'windows-latest' 121 | run: "cat run.sh" 122 | 123 | - run: "./bin/pap -y -d script --jar server.jar --mem 13G --gui" 124 | - run: "./bin/pap -y -d script --jar server.jar --mem 13G --gui --stdout" 125 | - name: Read Generated Script (windows) 126 | if: matrix.os == 'windows-latest' 127 | run: "cat run.bat" 128 | - name: Read Generated Script (unix) 129 | if: matrix.os != 'windows-latest' 130 | run: "cat run.sh" 131 | 132 | - run: "./bin/pap -y -d script --jar server.jar --mem 2000M" 133 | - run: "./bin/pap -y -d script --jar server.jar --mem 2000M --stdout" 134 | - name: Read Generated Script (windows) 135 | if: matrix.os == 'windows-latest' 136 | run: "cat run.bat" 137 | - name: Read Generated Script (unix) 138 | if: matrix.os != 'windows-latest' 139 | run: "cat run.sh" 140 | 141 | - run: "./bin/pap -y -d script --jar server.jar --mem 2000M --gui" 142 | - run: "./bin/pap -y -d script --jar server.jar --mem 2000M --gui --stdout" 143 | - name: Read Generated Script (windows) 144 | if: matrix.os == 'windows-latest' 145 | run: "cat run.bat" 146 | - name: Read Generated Script (unix) 147 | if: matrix.os != 'windows-latest' 148 | run: "cat run.sh" 149 | 150 | - run: "./bin/pap -y -d script --jar wow.jar" 151 | - run: "./bin/pap -y -d script --jar wow.jar --stdout" 152 | - name: Read Generated Script (windows) 153 | if: matrix.os == 'windows-latest' 154 | run: "cat run.bat" 155 | - name: Read Generated Script (unix) 156 | if: matrix.os != 'windows-latest' 157 | run: "cat run.sh" 158 | 159 | - run: "./bin/pap -y -d script --jar wow.jar --gui" 160 | - run: "./bin/pap -y -d script --jar wow.jar --gui --stdout" 161 | - name: Read Generated Script (windows) 162 | if: matrix.os == 'windows-latest' 163 | run: "cat run.bat" 164 | - name: Read Generated Script (unix) 165 | if: matrix.os != 'windows-latest' 166 | run: "cat run.sh" 167 | 168 | - run: "./bin/pap -y -d script --gui --jar wow.jar" 169 | - run: "./bin/pap -y -d script --gui --jar wow.jar --stdout" 170 | - name: Read Generated Script (windows) 171 | if: matrix.os == 'windows-latest' 172 | run: "cat run.bat" 173 | - name: Read Generated Script (unix) 174 | if: matrix.os != 'windows-latest' 175 | run: "cat run.sh" 176 | 177 | - run: "./bin/pap -y -d script --jar wow.jar --mem 4G --gui" 178 | - run: "./bin/pap -y -d script --jar wow.jar --mem 4G --gui --stdout" 179 | - name: Read Generated Script (windows) 180 | if: matrix.os == 'windows-latest' 181 | run: "cat run.bat" 182 | - name: Read Generated Script (unix) 183 | if: matrix.os != 'windows-latest' 184 | run: "cat run.sh" 185 | 186 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2G" 187 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2G --stdout" 188 | - name: Read Generated Script (windows) 189 | if: matrix.os == 'windows-latest' 190 | run: "cat run.bat" 191 | - name: Read Generated Script (unix) 192 | if: matrix.os != 'windows-latest' 193 | run: "cat run.sh" 194 | 195 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 13G" 196 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 13G --stdout" 197 | - name: Read Generated Script (windows) 198 | if: matrix.os == 'windows-latest' 199 | run: "cat run.bat" 200 | - name: Read Generated Script (unix) 201 | if: matrix.os != 'windows-latest' 202 | run: "cat run.sh" 203 | 204 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 13G --gui" 205 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 13G --gui --stdout" 206 | - name: Read Generated Script (windows) 207 | if: matrix.os == 'windows-latest' 208 | run: "cat run.bat" 209 | - name: Read Generated Script (unix) 210 | if: matrix.os != 'windows-latest' 211 | run: "cat run.sh" 212 | 213 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2000M" 214 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2000M --stdout" 215 | - name: Read Generated Script (windows) 216 | if: matrix.os == 'windows-latest' 217 | run: "cat run.bat" 218 | - name: Read Generated Script (unix) 219 | if: matrix.os != 'windows-latest' 220 | run: "cat run.sh" 221 | 222 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2000M --gui" 223 | - run: "./bin/pap -y -d script --jar server.jar --aikars --mem 2000M --gui --stdout" 224 | - name: Read Generated Script (windows) 225 | if: matrix.os == 'windows-latest' 226 | run: "cat run.bat" 227 | - name: Read Generated Script (unix) 228 | if: matrix.os != 'windows-latest' 229 | run: "cat run.sh" 230 | 231 | - run: "./bin/pap -y -d script --aikars --jar wow.jar" 232 | - run: "./bin/pap -y -d script --aikars --jar wow.jar --stdout" 233 | - name: Read Generated Script (windows) 234 | if: matrix.os == 'windows-latest' 235 | run: "cat run.bat" 236 | - name: Read Generated Script (unix) 237 | if: matrix.os != 'windows-latest' 238 | run: "cat run.sh" 239 | 240 | - run: "./bin/pap -y -d script --aikars --jar wow.jar --gui" 241 | - run: "./bin/pap -y -d script --aikars --jar wow.jar --gui --stdout" 242 | - name: Read Generated Script (windows) 243 | if: matrix.os == 'windows-latest' 244 | run: "cat run.bat" 245 | - name: Read Generated Script (unix) 246 | if: matrix.os != 'windows-latest' 247 | run: "cat run.sh" 248 | 249 | - run: "./bin/pap -y -d script --aikars --gui --jar wow.jar" 250 | - run: "./bin/pap -y -d script --aikars --gui --jar wow.jar --stdout" 251 | - name: Read Generated Script (windows) 252 | if: matrix.os == 'windows-latest' 253 | run: "cat run.bat" 254 | - name: Read Generated Script (unix) 255 | if: matrix.os != 'windows-latest' 256 | run: "cat run.sh" 257 | 258 | - run: "./bin/pap -y -d script --aikars --jar wow.jar --mem 4G --gui" 259 | - run: "./bin/pap -y -d script --aikars --jar wow.jar --mem 4G --gui --stdout" 260 | - name: Read Generated Script (windows) 261 | if: matrix.os == 'windows-latest' 262 | run: "cat run.bat" 263 | - name: Read Generated Script (unix) 264 | if: matrix.os != 'windows-latest' 265 | run: "cat run.sh" 266 | 267 | - run: "./bin/pap -y -d version" 268 | - name: Read Generated Script (windows) 269 | if: matrix.os == 'windows-latest' 270 | run: "cat run.bat" 271 | - name: Read Generated Script (unix) 272 | if: matrix.os != 'windows-latest' 273 | run: "cat run.sh" 274 | 275 | - name: "Reset server.properties" 276 | run: "./bin/pap -y -d properties reset" 277 | - name: Read created server.properties 278 | run: "cat server.properties" 279 | 280 | - name: "Get server property" 281 | run: "./bin/pap -y -d properties get gamemode" 282 | 283 | - name: "Set server property" 284 | run: "./bin/pap -y -d properties set gamemode creative" 285 | - name: Read modified server.properties 286 | run: "cat server.properties" 287 | 288 | - name: "Get modified server property" 289 | run: "./bin/pap -y -d properties get gamemode" 290 | 291 | - name: "Test paper downloading" 292 | run: | 293 | ./bin/pap -y -d download paper --experimental 294 | ./bin/pap -y -d download paper 295 | ./bin/pap -y -d download paper --version 1.19.2 296 | ./bin/pap -y -d download paper --version 1.19.2 --build 300 297 | ./bin/pap -y -d download paper --version 1.12.2 --build 1230 298 | ./bin/pap -y -d download paper --version 1.12.2 299 | ./bin/pap -y -d download paper --version latest --build latest --experimental 300 | ./bin/pap -y -d download paper --version 1.19.3 --build 319 --experimental 301 | ./bin/pap -y -d download paper --version 1.19.3 --build 319 302 | ./bin/pap -y -d download paper --version 1.12.2 --build latest 303 | 304 | - name: "Test purpur downloading" 305 | run: | 306 | ./bin/pap -y -d download purpur 307 | ./bin/pap -y -d download purpur --version 1.19.2 308 | ./bin/pap -y -d download purpur --version 1.14.2 --build 124 309 | ./bin/pap -y -d download purpur --version 1.14.2 310 | ./bin/pap -y -d download purpur --version latest 311 | ./bin/pap -y -d download purpur --version 1.19.3 --build 1881 312 | ./bin/pap -y -d download purpur --version 1.14.2 --build latest 313 | 314 | - name: "Test official downloading" 315 | run: | 316 | ./bin/pap -y -d download official --snapshot 317 | ./bin/pap -y -d download official 318 | ./bin/pap -y -d download official --version 1.19.2 319 | ./bin/pap -y -d download official --version 1.12.2 320 | ./bin/pap -y -d download official --version latest --snapshot 321 | ./bin/pap -y -d download official --version 1.19.3 --snapshot 322 | ./bin/pap -y -d download official --version 18w07c 323 | ./bin/pap -y -d download official --version 1.2.5 324 | ./bin/pap -y -d download official --version 1.13-pre6 325 | 326 | - name: "Test fabric downloading" 327 | run: | 328 | ./bin/pap -y -d download fabric --snapshot 329 | ./bin/pap -y -d download fabric 330 | ./bin/pap -y -d download fabric --loader 0.13.3 331 | ./bin/pap -y -d download fabric --installer 0.8.0 332 | ./bin/pap -y -d download fabric --version 1.19.2 333 | ./bin/pap -y -d download fabric --version latest --snapshot 334 | ./bin/pap -y -d download fabric --version 1.19.3 --snapshot 335 | ./bin/pap -y -d download fabric --version 20w22a 336 | ./bin/pap -y -d download fabric --version 1.16-pre8 337 | 338 | - name: "Test forge downloading" 339 | run: | 340 | ./bin/pap -y -d download forge 341 | ./bin/pap -y -d download forge --latest 342 | ./bin/pap -y -d download forge --version 1.6.4 343 | ./bin/pap -y -d download forge --version 1.9 --latest 344 | ./bin/pap -y -d download forge --version 1.7.10_pre4 --latest 345 | ./bin/pap -y -d download forge --version 1.19.3 --installer 44.1.0 346 | 347 | - name: "Test self updater" 348 | continue-on-error: true 349 | run: | 350 | ./bin/pap -y -d update 351 | ./bin/pap -y -d update -r 352 | ./bin/pap help 353 | ./bin/pap version 354 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // pap :) 2 | package main 3 | 4 | import ( 5 | "os" 6 | 7 | "github.com/talwat/pap/internal/cmd" 8 | "github.com/talwat/pap/internal/cmd/downloadcmds" 9 | "github.com/talwat/pap/internal/cmd/plugincmds" 10 | "github.com/talwat/pap/internal/cmd/plugincmds/generatecmds" 11 | "github.com/talwat/pap/internal/cmd/propcmds" 12 | "github.com/talwat/pap/internal/global" 13 | "github.com/talwat/pap/internal/log" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | //nolint:funlen,exhaustruct,maintidx // Ignoring these issues because this file only serves to define commands. 18 | func main() { 19 | app := &cli.App{ 20 | Name: "pap", 21 | Usage: "a swiss army knife for minecraft servers", 22 | Version: global.Version, 23 | Authors: []*cli.Author{ 24 | { 25 | Name: "talwat", 26 | }, 27 | }, 28 | HideHelp: true, 29 | HideVersion: true, 30 | //nolint:lll 31 | CustomAppHelpTemplate: `NAME: 32 | {{template "helpNameTemplate" .}} 33 | 34 | USAGE: 35 | {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} 36 | 37 | VERSION: 38 | {{.Version}}{{if .Description}} 39 | 40 | DESCRIPTION: 41 | {{template "descriptionTemplate" .}}{{end}} 42 | {{- if len .Authors}} 43 | 44 | AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}} 45 | 46 | COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}} 47 | 48 | GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}} 49 | 50 | GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}} 51 | 52 | COPYRIGHT: 53 | {{template "copyrightTemplate" .}}{{end}} 54 | `, 55 | CommandNotFound: func(ctx *cli.Context, command string) { 56 | log.RawError("command not found: %s", command) 57 | }, 58 | OnUsageError: func(ctx *cli.Context, err error, isSubcommand bool) error { 59 | log.RawError("%s", err) 60 | 61 | return nil 62 | }, 63 | Flags: []cli.Flag{ 64 | &cli.BoolFlag{ 65 | Name: "assume-default", 66 | Value: false, 67 | Usage: "assume the default answer in all prompts", 68 | Aliases: []string{"y"}, 69 | Destination: &global.AssumeDefaultInput, 70 | }, 71 | &cli.BoolFlag{ 72 | Name: "debug", 73 | Value: false, 74 | Usage: "print extra information for debugging or troubleshooting", 75 | Aliases: []string{"d"}, 76 | Destination: &global.Debug, 77 | }, 78 | }, 79 | Commands: []*cli.Command{ 80 | { 81 | Name: "download", 82 | Aliases: []string{"d"}, 83 | Usage: "download a jarfile", 84 | ArgsUsage: "[type]", 85 | Flags: []cli.Flag{ 86 | &cli.StringFlag{ 87 | Name: "minecraft-version", 88 | Value: "latest", 89 | Usage: "the minecraft version to download", 90 | Aliases: []string{"version", "v"}, 91 | Destination: &global.MinecraftVersionInput, 92 | }, 93 | }, 94 | Subcommands: []*cli.Command{ 95 | { 96 | Name: "paper", 97 | Aliases: []string{"pa"}, 98 | Usage: "download a paper jarfile", 99 | Action: downloadcmds.DownloadPaperCommand, 100 | Flags: []cli.Flag{ 101 | &cli.StringFlag{ 102 | Name: "paper-build", 103 | Value: "latest", 104 | Usage: "the papermc build to download", 105 | Aliases: []string{"build", "b"}, 106 | Destination: &global.JarBuildInput, 107 | }, 108 | &cli.BoolFlag{ 109 | Name: "paper-experimental", 110 | Value: false, 111 | Usage: "takes the latest build regardless", 112 | Aliases: []string{"experimental", "e"}, 113 | Destination: &global.PaperExperimentalBuildInput, 114 | }, 115 | &cli.StringFlag{ 116 | Name: "minecraft-version", 117 | Value: "latest", 118 | Usage: "the minecraft version to download", 119 | Aliases: []string{"version", "v"}, 120 | Destination: &global.MinecraftVersionInput, 121 | }, 122 | }, 123 | }, 124 | { 125 | Name: "fabric", 126 | Aliases: []string{"fa"}, 127 | Usage: "download a fabric jarfile", 128 | Action: downloadcmds.DownloadFabricCommand, 129 | Flags: []cli.Flag{ 130 | &cli.StringFlag{ 131 | Name: "fabric-loader", 132 | Value: "latest", 133 | Usage: "the fabric loader version to use", 134 | Aliases: []string{"loader", "l"}, 135 | Destination: &global.FabricLoaderVersion, 136 | }, 137 | &cli.StringFlag{ 138 | Name: "fabric-installer", 139 | Value: "latest", 140 | Usage: "the fabric installer version to use", 141 | Aliases: []string{"installer", "i"}, 142 | Destination: &global.FabricInstallerVersion, 143 | }, 144 | &cli.BoolFlag{ 145 | Name: "fabric-loader-experimental", 146 | Value: false, 147 | Usage: "takes the latest loader version regardless", 148 | Aliases: []string{"experimental", "e"}, 149 | Destination: &global.FabricExperimentalLoaderVersion, 150 | }, 151 | &cli.BoolFlag{ 152 | Name: "minecraft-snapshot", 153 | Value: false, 154 | Usage: "takes the latest snapshot instead of the latest release", 155 | Aliases: []string{"snapshot", "s"}, 156 | Destination: &global.UseSnapshotInput, 157 | }, 158 | &cli.StringFlag{ 159 | Name: "minecraft-version", 160 | Value: "latest", 161 | Usage: "the minecraft version to download", 162 | Aliases: []string{"version", "v"}, 163 | Destination: &global.MinecraftVersionInput, 164 | }, 165 | }, 166 | }, 167 | { 168 | Name: "purpur", 169 | Aliases: []string{"pu"}, 170 | Usage: "download a purpur jarfile", 171 | Action: downloadcmds.DownloadPurpurCommand, 172 | Flags: []cli.Flag{ 173 | &cli.StringFlag{ 174 | Name: "purpur-build", 175 | Value: "latest", 176 | Usage: "the papermc build to download", 177 | Aliases: []string{"build", "b"}, 178 | Destination: &global.JarBuildInput, 179 | }, 180 | &cli.StringFlag{ 181 | Name: "minecraft-version", 182 | Value: "latest", 183 | Usage: "the minecraft version to download", 184 | Aliases: []string{"version", "v"}, 185 | Destination: &global.MinecraftVersionInput, 186 | }, 187 | }, 188 | }, 189 | { 190 | Name: "official", 191 | Aliases: []string{"o"}, 192 | Usage: "download an official mojang jarfile", 193 | Action: downloadcmds.DownloadOfficialCommand, 194 | Flags: []cli.Flag{ 195 | &cli.BoolFlag{ 196 | Name: "minecraft-snapshot", 197 | Value: false, 198 | Usage: "takes the latest snapshot instead of the latest release", 199 | Aliases: []string{"snapshot", "s"}, 200 | Destination: &global.UseSnapshotInput, 201 | }, 202 | &cli.StringFlag{ 203 | Name: "minecraft-version", 204 | Value: "latest", 205 | Usage: "the minecraft version to download", 206 | Aliases: []string{"version", "v"}, 207 | Destination: &global.MinecraftVersionInput, 208 | }, 209 | }, 210 | }, 211 | { 212 | Name: "forge", 213 | Aliases: []string{"fo"}, 214 | Usage: "download a forge jarfile", 215 | Action: downloadcmds.DownloadForgeCommand, 216 | Flags: []cli.Flag{ 217 | &cli.BoolFlag{ 218 | Name: "download-latest", 219 | Value: false, 220 | Usage: "download the latest forge installer instead of a recommended version", 221 | Aliases: []string{"latest", "l"}, 222 | Destination: &global.ForgeUseLatestInstaller, 223 | }, 224 | &cli.StringFlag{ 225 | Name: "minecraft-version", 226 | Value: "latest", 227 | Usage: "the minecraft version to download", 228 | Aliases: []string{"version", "v"}, 229 | Destination: &global.MinecraftVersionInput, 230 | }, 231 | &cli.StringFlag{ 232 | Name: "installer-version", 233 | Value: "latest", 234 | Usage: "the forge installer version to download", 235 | Aliases: []string{"installer", "i"}, 236 | Destination: &global.ForgeInstallerVersion, 237 | }, 238 | }, 239 | }, 240 | }, 241 | }, 242 | { 243 | Name: "geyser", 244 | Aliases: []string{"g"}, 245 | Usage: "downloads geyser", 246 | Action: cmd.GeyserCommand, 247 | }, 248 | { 249 | Name: "plugin", 250 | Aliases: []string{"pl"}, 251 | Usage: "manages plugins", 252 | ArgsUsage: "[install|uninstall] [plugin]", 253 | Subcommands: []*cli.Command{ 254 | { 255 | Name: "install", 256 | Aliases: []string{"i"}, 257 | Usage: "installs a plugin", 258 | Action: plugincmds.InstallCommand, 259 | Flags: []cli.Flag{ 260 | &cli.BoolFlag{ 261 | Name: "no-deps", 262 | Value: false, 263 | Usage: "whether to install and calculate dependencies", 264 | Aliases: []string{"no-dependencies"}, 265 | Destination: &global.NoDepsInput, 266 | }, 267 | &cli.BoolFlag{ 268 | Name: "install-optional-deps", 269 | Value: false, 270 | Usage: "whether to install optional dependencies", 271 | Aliases: []string{"optional"}, 272 | Destination: &global.InstallOptionalDepsInput, 273 | }, 274 | &cli.BoolFlag{ 275 | Name: "plugin-experimental", 276 | Value: false, 277 | Usage: "takes the latest version regardless", 278 | Aliases: []string{"experimental"}, 279 | Destination: &global.PluginExperimentalInput, 280 | }, 281 | }, 282 | }, 283 | { 284 | Name: "uninstall", 285 | Aliases: []string{"u", "remove", "r"}, 286 | Usage: "get property", 287 | Action: plugincmds.UninstallCommand, 288 | }, 289 | { 290 | Name: "info", 291 | Aliases: []string{"in"}, 292 | Usage: "get information about a plugin", 293 | Action: plugincmds.InfoCommand, 294 | }, 295 | { 296 | Name: "generate", 297 | Aliases: []string{"gen", "g"}, 298 | Usage: "generate a plugin json file using a 3rd party plugin library", 299 | Flags: []cli.Flag{ 300 | &cli.BoolFlag{ 301 | Name: "use-stdout", 302 | Aliases: []string{"stdout", "s"}, 303 | Usage: "output to stdout instead of writing it to the disk", 304 | Destination: &global.UseStdoutInput, 305 | }, 306 | }, 307 | Subcommands: []*cli.Command{ 308 | { 309 | Name: "modrinth", 310 | Aliases: []string{"m"}, 311 | Usage: "generate a plugin json file using modrinth", 312 | Action: generatecmds.GenerateModrinth, 313 | }, 314 | { 315 | Name: "spigotmc", 316 | Aliases: []string{"s"}, 317 | Usage: "generate a plugin json file using spigotmc", 318 | Action: generatecmds.GenerateSpigotMC, 319 | }, 320 | { 321 | Name: "bukkit", 322 | Aliases: []string{"b"}, 323 | Usage: "generate a plugin json file using bukkit", 324 | Action: generatecmds.GenerateBukkit, 325 | }, 326 | }, 327 | }, 328 | }, 329 | }, 330 | { 331 | Name: "script", 332 | Aliases: []string{"sc"}, 333 | Usage: "generate a script to run the jarfile", 334 | Action: cmd.ScriptCommand, 335 | Flags: []cli.Flag{ 336 | &cli.StringFlag{ 337 | Name: "mem", 338 | Value: "2G", 339 | Usage: "the value for -Xms and -Xmx in the run command", 340 | Aliases: []string{"memory", "m"}, 341 | Destination: &global.MemoryInput, 342 | }, 343 | &cli.BoolFlag{ 344 | Name: "aikars", 345 | Value: false, 346 | Usage: "whether to use aikars flags: https://docs.papermc.io/paper/aikars-flags", 347 | Aliases: []string{"a"}, 348 | Destination: &global.AikarsInput, 349 | }, 350 | &cli.StringFlag{ 351 | Name: "jar", 352 | Usage: "the name for the server jarfile", 353 | Aliases: []string{"j"}, 354 | Destination: &global.JarInput, 355 | }, 356 | &cli.BoolFlag{ 357 | Name: "use-gui", 358 | Aliases: []string{"gui"}, 359 | Usage: "whether to use the GUI or not", 360 | Destination: &global.GUIInput, 361 | }, 362 | &cli.BoolFlag{ 363 | Name: "use-stdout", 364 | Aliases: []string{"stdout", "s"}, 365 | Usage: "output to stdout instead of writing it to the disk", 366 | Destination: &global.UseStdoutInput, 367 | }, 368 | }, 369 | }, 370 | { 371 | Name: "update", 372 | Aliases: []string{"u"}, 373 | Usage: "updates pap if there is a new version available", 374 | Action: cmd.UpdateCommand, 375 | Flags: []cli.Flag{ 376 | &cli.BoolFlag{ 377 | Name: "reinstall", 378 | Aliases: []string{"r"}, 379 | Usage: "reinstalls even if pap is up to date", 380 | Destination: &global.ReinstallInput, 381 | }, 382 | }, 383 | }, 384 | { 385 | Name: "sign", 386 | Aliases: []string{"si", "eula", "e"}, 387 | Usage: "sign the EULA", 388 | Action: cmd.EulaCommand, 389 | }, 390 | { 391 | Name: "help", 392 | Aliases: []string{"h"}, 393 | Usage: "show help", 394 | Action: cli.ShowAppHelp, 395 | }, 396 | { 397 | Name: "version", 398 | Aliases: []string{"v"}, 399 | Usage: "show version", 400 | Action: func(cCtx *cli.Context) error { 401 | cli.ShowVersion(cCtx) 402 | 403 | return nil 404 | }, 405 | }, 406 | { 407 | Name: "properties", 408 | Aliases: []string{"pr"}, 409 | Usage: "manages the server.properties file", 410 | ArgsUsage: "[set|get] [property] [value]", 411 | Subcommands: []*cli.Command{ 412 | { 413 | Name: "set", 414 | Aliases: []string{"s"}, 415 | Usage: "set property", 416 | Action: propcmds.SetPropertyCommand, 417 | }, 418 | { 419 | Name: "get", 420 | Aliases: []string{"g"}, 421 | Usage: "get property", 422 | Action: propcmds.GetPropertyCommand, 423 | }, 424 | { 425 | Name: "reset", 426 | Aliases: []string{"r"}, 427 | Usage: "downloads the default server.properties", 428 | Action: propcmds.ResetPropertiesCommand, 429 | }, 430 | }, 431 | }, 432 | }, 433 | } 434 | 435 | if app.Run(os.Args) != nil { 436 | os.Exit(1) 437 | } 438 | } 439 | --------------------------------------------------------------------------------