initial commit

This commit is contained in:
Nils Stinnesbeck 2024-07-31 23:09:11 +02:00
commit cd137e4b1a
Signed by: nils
GPG Key ID: C52E07422A136076
12 changed files with 572 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# environment variables are saved in here
setenv.sh

72
accounts/accounts.go Normal file
View File

@ -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
}

175
api/api.go Normal file
View File

@ -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
}

61
characters/characters.go Normal file
View File

@ -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)
}

34
characters/move.go Normal file
View File

@ -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)
}

13
characters/move_test.go Normal file
View File

@ -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)
}
}

153
characters/types.go Normal file
View File

@ -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"`
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.stinnesbeck.com/nils/artifacts
go 1.22.1

0
go.sum Normal file
View File

14
maps/maps.go Normal file
View File

@ -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)
}

16
maps/types.go Normal file
View File

@ -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"`
}

29
status/status.go Normal file
View File

@ -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)
}