commit cd137e4b1a1cb4856250cabe23f08900cf921f44 Author: Nils Stinnesbeck Date: Wed Jul 31 23:09:11 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b54764c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# environment variables are saved in here +setenv.sh diff --git a/accounts/accounts.go b/accounts/accounts.go new file mode 100644 index 0000000..7537a6e --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,72 @@ +// Package accounts holds functions that interact with the accounts +package accounts + +import ( + "errors" + "regexp" + + "git.stinnesbeck.com/nils/artifacts/api" +) + +// Account is a struct that holds info on an account +type Account struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` +} + +// custom Error codes +var ( + ErrUsernameTooShort = errors.New("username is too short, it needs to be at least 6 chars long") + ErrUsernameTooLong = errors.New("username is too long, it needs to be at max 32 chars long") + ErrUsernameRegex = errors.New(`username must pass this regex: ^[a-zA-Z0-9_-]+$`) + ErrPasswordTooShort = errors.New("password is too short, it needs to be at least 5 chars long") + ErrPasswordTooLong = errors.New("password is too long, it needs to be at max 50 chars long") + ErrPasswordRegex = errors.New(`password must pass this regex: ^[^\s]+$`) +) + +// Create can be used to create an account +func Create(username, password, email string) error { + // >= 6 characters<= 32 characters + userRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + passRegex := regexp.MustCompile(`^[^\s]+$`) + + // username checks + switch { + case len(username) < 6: + return ErrUsernameTooShort + case len(username) > 32: + return ErrUsernameTooLong + case !userRegex.MatchString(username): + return ErrUsernameRegex + } + + // password checks + switch { + case len(password) < 5: + return ErrPasswordTooShort + case len(password) > 50: + return ErrPasswordTooLong + case !passRegex.MatchString(password): + return ErrPasswordRegex + } + + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 456: "Username already used.", + 457: "Email already used.", + } + + newAccount := Account{ + Username: username, + Password: password, + Email: email, + } + + _, err := api.Post[string]("/accounts/create", newAccount, responseCodes) + if err != nil { + return err + } + + return nil +} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..c73753e --- /dev/null +++ b/api/api.go @@ -0,0 +1,175 @@ +// Package api holds functions to interact with the artifacts API +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "slices" +) + +// custom error codes +var ( + ErrTokenNotProvided = errors.New("make sure to provide a token via the api.SetToken() function, or set it via env variable ARTIFACTS_TOKEN") +) + +const ( + baseURL = "https://api.artifactsmmo.com" +) + +var token string // the token must be provided for the client to work + +type apiResponse struct { + Data json.RawMessage `json:"data"` + Total int `json:"total"` + Page int `json:"page"` + Size int `json:"size"` + Pages int `json:"pages"` +} + +// SetToken can be used to set the token, which must be provided before calling any other function. +// It can be provided using this function or via environment variable 'ARTIFACTS_TOKEN'. +func SetToken(t string) { + token = t +} + +func meta[T any](endpoint string, method string, body any, responseCodes map[int]string) (*T, error) { + // make it possible to set a token via env variable + if token == "" && os.Getenv("ARTIFACTS_TOKEN") != "" { + SetToken(os.Getenv("ARTIFACTS_TOKEN")) + } + + // check for token + if token == "" { + return nil, ErrTokenNotProvided + } + + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, err + } + + // create a new request to retrieve characters + request, err := http.NewRequest(method, baseURL+endpoint, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, err + } + + // add auth header to the request + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + request.Header.Set("Content-Type", "application/json") + + // execute this request + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, err + } + + // check all additional status codes first + for code, message := range responseCodes { + if response.StatusCode != code { + continue + } + + // exit the function if a statuscode was found and return it's message + return nil, fmt.Errorf("%d: %s", code, message) + } + + // basic error handling + if response.StatusCode != 200 { + return nil, fmt.Errorf("%s", response.Status) + } + + // read in the body of the response + b, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + // close the body when we are finished + defer response.Body.Close() + + // convert it to a native go type + var ar apiResponse + if err := json.Unmarshal(b, &ar); err != nil { + return nil, err + } + + if ar.Page < ar.Pages { + // parse endpoint + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + urlValues := u.Query() + urlValues.Set("page", fmt.Sprintf("%d", ar.Page+1)) + endpoint = u.Path + + nextPage, err := meta[T](fmt.Sprintf("%s?%s", endpoint, urlValues.Encode()), method, body, responseCodes) + if err != nil { + return nil, err + } + + b1, err := ar.Data.MarshalJSON() + if err != nil { + return nil, err + } + + b2, err := json.Marshal(nextPage) + if err != nil { + return nil, err + } + + bCombined := slices.Concat(b1[0:len(b1)-1], []byte(","), b2[1:]) + + // copy combined dataset into ar.Data + ar.Data.UnmarshalJSON(bCombined) + + // combine both things + } + + d, err := ar.Data.MarshalJSON() + if err != nil { + return nil, err + } + + // no error occurred + var r T + if err := json.Unmarshal(d, &r); err != nil { + return nil, err + } + + return &r, nil +} + +// Post is used to call an endpoint using a POST request, responseCodes contains all possible +// error codes that the API can respond with other than 200 OK +func Post[T any](endpoint string, body any, responseCodes map[int]string) (output T, err error) { + // use meta function to get a response + response, err := meta[T](endpoint, http.MethodPost, body, responseCodes) + if err != nil { + return output, err + } + + // return the data alongside any error that might have occurred + return *response, err +} + +// Get is used to call an endpoint using a GET request, responseCodes contains all possible +// error codes that the API can respond with other than 200 OK +func Get[T any](endpoint string, responseCodes map[int]string) (output T, err error) { + // use meta function to get a response + response, err := meta[T](endpoint, http.MethodGet, nil, responseCodes) + if err != nil { + return output, err + } + + // return the data alongside any error that might have occurred + return *response, err +} diff --git a/characters/characters.go b/characters/characters.go new file mode 100644 index 0000000..bfde0db --- /dev/null +++ b/characters/characters.go @@ -0,0 +1,61 @@ +// Package characters holds functions that interact with the characters +package characters + +import ( + "fmt" + + "git.stinnesbeck.com/nils/artifacts/api" +) + +// GetMy retrieves all characters from your account +func GetMy() ([]Character, error) { + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 404: "Characters not found", + } + + // use Get function to retrieve characters + return api.Get[[]Character]("/my/characters/", responseCodes) +} + +// GetAll tries to retrieve all characters on the server +func GetAll() ([]Character, error) { + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 404: "Characters not found", + } + + // use Get function to retrieve characters + return api.Get[[]Character]("/characters/", responseCodes) +} + +// Get tries to retrieve a character by name +func Get(name string) (Character, error) { + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 404: "Character not found", + } + + // use Get function to retrieve character + return api.Get[Character](fmt.Sprintf("/characters/%s", name), responseCodes) +} + +// Create will try to create a new character +func Create(name string, skin Skin) (Character, error) { + if len(name) > 12 { + return Character{}, fmt.Errorf("make sure to provide a name that is >=3 and <=12 characters long") + } + + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 494: "Name already used", + 495: "Maximum characters reached on your account", + } + + newChar := Character{ + Name: name, + Skin: skin, + } + + return api.Post[Character]("/characters/create", newChar, responseCodes) +} diff --git a/characters/move.go b/characters/move.go new file mode 100644 index 0000000..a83a867 --- /dev/null +++ b/characters/move.go @@ -0,0 +1,34 @@ +package characters + +import ( + "fmt" + + "git.stinnesbeck.com/nils/artifacts/api" +) + +// Move will try to move a character to a map tile located at x,y +func Move(name string, x, y int) (CharacterMovement, error) { + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 404: "Map not found.", + 486: "Character is locked. Action is already in progress.", + 490: "Character already at destination.", + 498: "Character not found.", + 499: "Character in cooldown.", + } + + var movement = struct { + X int `json:"x"` + Y int `json:"y"` + }{ + X: x, + Y: y, + } + + return api.Post[CharacterMovement](fmt.Sprintf("/my/%s/action/move", name), movement, responseCodes) +} + +// Move will try to move a character to a map tile located at x,y +func (c *Character) Move(x, y int) (CharacterMovement, error) { + return Move(c.Name, x, y) +} diff --git a/characters/move_test.go b/characters/move_test.go new file mode 100644 index 0000000..6bc0807 --- /dev/null +++ b/characters/move_test.go @@ -0,0 +1,13 @@ +package characters_test + +import ( + "testing" + + "git.stinnesbeck.com/nils/artifacts/characters" +) + +func TestMove(t *testing.T) { + if _, err := characters.Move("Nils", 0, 1); err != nil { + t.Fatal(err) + } +} diff --git a/characters/types.go b/characters/types.go new file mode 100644 index 0000000..e192e4f --- /dev/null +++ b/characters/types.go @@ -0,0 +1,153 @@ +package characters + +import "time" + +// Character is a struct that holds info on a character +type Character struct { + Name string `json:"name"` + Skin Skin `json:"skin"` + Level int `json:"level"` + Xp int `json:"xp"` + MaxXp int `json:"max_xp"` + TotalXp int `json:"total_xp"` + Gold int `json:"gold"` + Speed int `json:"speed"` + MiningLevel int `json:"mining_level"` + MiningXp int `json:"mining_xp"` + MiningMaxXp int `json:"mining_max_xp"` + WoodcuttingLevel int `json:"woodcutting_level"` + WoodcuttingXp int `json:"woodcutting_xp"` + WoodcuttingMaxXp int `json:"woodcutting_max_xp"` + FishingLevel int `json:"fishing_level"` + FishingXp int `json:"fishing_xp"` + FishingMaxXp int `json:"fishing_max_xp"` + WeaponcraftingLevel int `json:"weaponcrafting_level"` + WeaponcraftingXp int `json:"weaponcrafting_xp"` + WeaponcraftingMaxXp int `json:"weaponcrafting_max_xp"` + GearcraftingLevel int `json:"gearcrafting_level"` + GearcraftingXp int `json:"gearcrafting_xp"` + GearcraftingMaxXp int `json:"gearcrafting_max_xp"` + JewelrycraftingLevel int `json:"jewelrycrafting_level"` + JewelrycraftingXp int `json:"jewelrycrafting_xp"` + JewelrycraftingMaxXp int `json:"jewelrycrafting_max_xp"` + CookingLevel int `json:"cooking_level"` + CookingXp int `json:"cooking_xp"` + CookingMaxXp int `json:"cooking_max_xp"` + Hp int `json:"hp"` + Haste int `json:"haste"` + CriticalStrike int `json:"critical_strike"` + Stamina int `json:"stamina"` + AttackFire int `json:"attack_fire"` + AttackEarth int `json:"attack_earth"` + AttackWater int `json:"attack_water"` + AttackAir int `json:"attack_air"` + DmgFire int `json:"dmg_fire"` + DmgEarth int `json:"dmg_earth"` + DmgWater int `json:"dmg_water"` + DmgAir int `json:"dmg_air"` + ResFire int `json:"res_fire"` + ResEarth int `json:"res_earth"` + ResWater int `json:"res_water"` + ResAir int `json:"res_air"` + X int `json:"x"` + Y int `json:"y"` + Cooldown int `json:"cooldown"` + CooldownExpiration time.Time `json:"cooldown_expiration"` + WeaponSlot string `json:"weapon_slot"` + ShieldSlot string `json:"shield_slot"` + HelmetSlot string `json:"helmet_slot"` + BodyArmorSlot string `json:"body_armor_slot"` + LegArmorSlot string `json:"leg_armor_slot"` + BootsSlot string `json:"boots_slot"` + Ring1Slot string `json:"ring1_slot"` + Ring2Slot string `json:"ring2_slot"` + AmuletSlot string `json:"amulet_slot"` + Artifact1Slot string `json:"artifact1_slot"` + Artifact2Slot string `json:"artifact2_slot"` + Artifact3Slot string `json:"artifact3_slot"` + Consumable1Slot string `json:"consumable1_slot"` + Consumable1SlotQuantity int `json:"consumable1_slot_quantity"` + Consumable2Slot string `json:"consumable2_slot"` + Consumable2SlotQuantity int `json:"consumable2_slot_quantity"` + InventorySlot1 string `json:"inventory_slot1"` + InventorySlot1Quantity int `json:"inventory_slot1_quantity"` + InventorySlot2 string `json:"inventory_slot2"` + InventorySlot2Quantity int `json:"inventory_slot2_quantity"` + InventorySlot3 string `json:"inventory_slot3"` + InventorySlot3Quantity int `json:"inventory_slot3_quantity"` + InventorySlot4 string `json:"inventory_slot4"` + InventorySlot4Quantity int `json:"inventory_slot4_quantity"` + InventorySlot5 string `json:"inventory_slot5"` + InventorySlot5Quantity int `json:"inventory_slot5_quantity"` + InventorySlot6 string `json:"inventory_slot6"` + InventorySlot6Quantity int `json:"inventory_slot6_quantity"` + InventorySlot7 string `json:"inventory_slot7"` + InventorySlot7Quantity int `json:"inventory_slot7_quantity"` + InventorySlot8 string `json:"inventory_slot8"` + InventorySlot8Quantity int `json:"inventory_slot8_quantity"` + InventorySlot9 string `json:"inventory_slot9"` + InventorySlot9Quantity int `json:"inventory_slot9_quantity"` + InventorySlot10 string `json:"inventory_slot10"` + InventorySlot10Quantity int `json:"inventory_slot10_quantity"` + InventorySlot11 string `json:"inventory_slot11"` + InventorySlot11Quantity int `json:"inventory_slot11_quantity"` + InventorySlot12 string `json:"inventory_slot12"` + InventorySlot12Quantity int `json:"inventory_slot12_quantity"` + InventorySlot13 string `json:"inventory_slot13"` + InventorySlot13Quantity int `json:"inventory_slot13_quantity"` + InventorySlot14 string `json:"inventory_slot14"` + InventorySlot14Quantity int `json:"inventory_slot14_quantity"` + InventorySlot15 string `json:"inventory_slot15"` + InventorySlot15Quantity int `json:"inventory_slot15_quantity"` + InventorySlot16 string `json:"inventory_slot16"` + InventorySlot16Quantity int `json:"inventory_slot16_quantity"` + InventorySlot17 string `json:"inventory_slot17"` + InventorySlot17Quantity int `json:"inventory_slot17_quantity"` + InventorySlot18 string `json:"inventory_slot18"` + InventorySlot18Quantity int `json:"inventory_slot18_quantity"` + InventorySlot19 string `json:"inventory_slot19"` + InventorySlot19Quantity int `json:"inventory_slot19_quantity"` + InventorySlot20 string `json:"inventory_slot20"` + InventorySlot20Quantity int `json:"inventory_slot20_quantity"` + InventoryMaxItems int `json:"inventory_max_items"` + Task string `json:"task"` + TaskType string `json:"task_type"` + TaskProgress int `json:"task_progress"` + TaskTotal int `json:"task_total"` +} + +// Skin is a custom type used for the allowed skins +type Skin string + +// allowed skins +const ( + Man1 Skin = "men1" + Man2 Skin = "men2" + Man3 Skin = "men3" + Woman1 Skin = "women1" + Woman2 Skin = "women2" + Woman3 Skin = "women3" +) + +// CharacterMovement is a struct that holds info on the movement of a character +type CharacterMovement struct { + Cooldown Cooldown + Destination Destination + Character Character +} + +// Cooldown is a struct that holds info on the remaining time for a given action +type Cooldown struct { + TotalSeconds int `json:"totalSeconds"` + RemainingSeconds int `json:"remainingSeconds"` + Expiration time.Time `json:"expiration"` + Reason string `json:"reason"` +} + +// Destination is a struct that holds info on a map tile +type Destination struct { + Name string `json:"name"` + X int `json:"x"` + Y int `json:"y"` + Content any `json:"content"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..96698b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.stinnesbeck.com/nils/artifacts + +go 1.22.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/maps/maps.go b/maps/maps.go new file mode 100644 index 0000000..65a7564 --- /dev/null +++ b/maps/maps.go @@ -0,0 +1,14 @@ +// Package maps holds functions that interact with the maps +package maps + +import "git.stinnesbeck.com/nils/artifacts/api" + +// GetAll will try to retrieve all map tiles +func GetAll() ([]Map, error) { + // these are the possible response codes, other than 200 OK + responseCodes := map[int]string{ + 404: "Maps not found.", + } + + return api.Get[[]Map]("/maps/", responseCodes) +} diff --git a/maps/types.go b/maps/types.go new file mode 100644 index 0000000..a92b1e0 --- /dev/null +++ b/maps/types.go @@ -0,0 +1,16 @@ +package maps + +// Content is a struct that holds info on the type of a map tile +type Content struct { + Type string `json:"type"` + Code string `json:"code"` +} + +// Map is a struct that holds info on a map tile +type Map struct { + Name string `json:"name"` + Skin string `json:"skin"` + X int `json:"x"` + Y int `json:"y"` + Content Content `json:"content"` +} diff --git a/status/status.go b/status/status.go new file mode 100644 index 0000000..5d1569a --- /dev/null +++ b/status/status.go @@ -0,0 +1,29 @@ +// Package status holds functions that interact with the status of the server +package status + +import ( + "time" + + "git.stinnesbeck.com/nils/artifacts/api" +) + +// Announcements is used to display messages and when they were created +type Announcements struct { + Message string `json:"message"` + CreatedAt time.Time `json:"created_at"` +} + +// ServerStatus is a struct that holds info on a server +type ServerStatus struct { + Status string `json:"status"` + Version string `json:"version"` + CharactersOnline int `json:"characters_online"` + Announcements []Announcements `json:"announcements"` + LastWipe string `json:"last_wipe"` + NextWipe string `json:"next_wipe"` +} + +// Get tries to retrieve the status of the server +func Get() (ServerStatus, error) { + return api.Get[ServerStatus]("/", nil) +}