├── LICENSE ├── README.md ├── api ├── item.go ├── public_stash_tab_subscription.go ├── public_stash_tabs.go ├── public_stash_tabs_test.go ├── stash.go └── testdata │ └── public-stash-tabs.json └── examples └── poe-indexing-101 └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Christopher Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poe-go 2 | 3 | The goal of this library is to provide an entry-level guide to writing tools for PoE in the Go programming language. 4 | 5 | It should be simple enough for anyone moderately computer savvy to follow and have their own stash tab indexer running in no time. :moneybag: 6 | 7 | ## Getting Started 8 | 9 | ### Prerequisites 10 | 11 | Before doing anything else, you'll need to install [Go](https://golang.org/dl/) and [Git](https://git-scm.com/downloads). Both provide installers that you can just spam-click "next" through. 12 | 13 | Once both Go and Git are installed, open up "Git Bash" and verify that the "go" and "git" commands are available by typing `go version` and `git version`. 14 | 15 | ![Go and Git](https://i.imgur.com/Z8jVV7X.png) 16 | 17 | ### Creating the Project 18 | 19 | Go has relatively rigid project organization, so the project directory that we'll be working in needs to be made in your user directory's _go/src_ directory. 20 | 21 | Create a new project directory: `mkdir -p ~/go/src/poe-indexing-101` 22 | 23 | Make it the current directory: `cd ~/go/src/poe-indexing-101` 24 | 25 | ![Project Directory](https://i.imgur.com/tf173RC.png) 26 | 27 | Create a new file named _main.go_ by copying the contents of [examples/poe-indexing-101/main.go](examples/poe-indexing-101/main.go): 28 | 29 | ![main.go](https://i.imgur.com/qJb7AoU.png) 30 | 31 | Finally, we'll build and run your first indexer. 32 | 33 | Install dependencies (This will download and install this library.): `go get -v .` 34 | 35 | Run _main.go_: `go run main.go` 36 | 37 | ![go run](https://i.imgur.com/rXHqln8.png) 38 | 39 | Nice! :thumbsup: 40 | 41 | ### Where to Go from Here 42 | 43 | As-is, this example isn't incredibly useful. You probably want to modify the `processStash` function in _main.go_: 44 | 45 | ```go 46 | func processStash(stash *api.Stash) { 47 | for _, item := range stash.Items { 48 | if item.Type == "Ancient Reliquary Key" { 49 | log.Printf("Ancient Reliquary Key: account = %v, league = %v, note = %v, tab = %v", stash.AccountName, item.League, item.Note, stash.Label) 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | You may want to filter by league, show the account's last character name, parse buyouts, play sounds, compose ready-to-whisper messages, etc. 56 | 57 | You can refer to [api/item.go](api/item.go) and [api/stash.go](api/stash.go) to see what data is available for you to use. 58 | 59 | And if you're new to Go, you should probably read up a bit on [how to write Go code](https://golang.org/doc/code.html). 60 | 61 | ## Versioning 62 | 63 | This library is currently unversioned. There will never be any major breaking changes to the library, but stash and item fields may occasionally change to maintain accuracy and compatibility with the PoE API. 64 | 65 | ## License 66 | 67 | This source is released under the MIT license (see the LICENSE file). 68 | -------------------------------------------------------------------------------- /api/item.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Socket struct { 4 | GroupId int `json:"group"` 5 | Attribute string `json:"attr"` 6 | } 7 | 8 | type ItemProperty struct { 9 | Name string `json:"name"` 10 | Values []interface{} `json:"values"` 11 | DisplayMode int `json:"displayMode"` 12 | } 13 | 14 | type FrameType int 15 | 16 | const ( 17 | NormalItemFrameType FrameType = iota 18 | MagicItemFrameType 19 | RareItemFrameType 20 | UniqueItemFrameType 21 | GemFrameType 22 | CurrencyFrameType 23 | DivinationCardFrameType 24 | QuestItemFrameType 25 | ProphecyFrameType 26 | RelicFrameType 27 | ) 28 | 29 | type Item struct { 30 | // Names for some items may include markup. For example: <><><>Roth's Reach 31 | Name string `json:"name"` 32 | Type string `json:"typeLine"` 33 | 34 | Properties []ItemProperty `json:"properties"` 35 | Requirements []ItemProperty `json:"requirements"` 36 | 37 | Sockets []Socket `json:"sockets"` 38 | 39 | ExplicitMods []string `json:"explicitMods"` 40 | ImplicitMods []string `json:"implicitMods"` 41 | UtilityMods []string `json:"utilityMods"` 42 | EnchantMods []string `json:"enchantMods"` 43 | CraftedMods []string `json:"craftedMods"` 44 | CosmeticMods []string `json:"cosmeticMods"` 45 | 46 | Note string `json:"note"` 47 | 48 | IsVerified bool `json:"verified"` 49 | Width int `json:"w"` 50 | Height int `json:"h"` 51 | ItemLevel int `json:"ilvl"` 52 | Icon string `json:"icon"` 53 | League string `json:"league"` 54 | Id string `json:"id"` 55 | IsIdentified bool `json:"identified"` 56 | IsCorrupted bool `json:"corrupted"` 57 | IsLockedToCharacter bool `json:"lockedToCharacter"` 58 | IsSupport bool `json:"support"` 59 | DescriptionText string `json:"descrText"` 60 | SecondDescriptionText string `json:"secDescrText"` 61 | FlavorText []string `json:"flavourText"` 62 | ArtFilename string `json:"artFilename"` 63 | FrameType FrameType `json:"frameType"` 64 | StackSize int `json:"stackSize"` 65 | MaxStackSize int `json:"maxStackSize"` 66 | X int `json:"x"` 67 | Y int `json:"y"` 68 | InventoryId string `json:"inventoryId"` 69 | SocketedItems []Item `json:"socketedItems"` 70 | IsRelic bool `json:"isRelic"` 71 | TalismanTier int `json:"talismanTier"` 72 | ProphecyText string `json:"prophecyText"` 73 | ProphecyDifficultyText string `json:"prophecyDiffText"` 74 | } 75 | -------------------------------------------------------------------------------- /api/public_stash_tab_subscription.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type PublicStashTabSubscriptionResult struct { 11 | ChangeId string 12 | PublicStashTabs *PublicStashTabs 13 | Error error 14 | } 15 | 16 | type PublicStashTabSubscription struct { 17 | Channel chan PublicStashTabSubscriptionResult 18 | closeChannel chan bool 19 | host string 20 | } 21 | 22 | // Opens a subscription that begins with the given change id. To subscribe from the beginning, pass 23 | // an empty string. 24 | func OpenPublicStashTabSubscription(firstChangeId string) *PublicStashTabSubscription { 25 | return OpenPublicStashTabSubscriptionForHost("www.pathofexile.com", firstChangeId) 26 | } 27 | 28 | // Opens a subscription for an alternative host. Can be used for beta or foreign servers. 29 | func OpenPublicStashTabSubscriptionForHost(host, firstChangeId string) *PublicStashTabSubscription { 30 | ret := &PublicStashTabSubscription{ 31 | Channel: make(chan PublicStashTabSubscriptionResult), 32 | closeChannel: make(chan bool), 33 | host: host, 34 | } 35 | go ret.run(firstChangeId) 36 | return ret 37 | } 38 | 39 | func (s *PublicStashTabSubscription) Close() { 40 | s.closeChannel <- true 41 | } 42 | 43 | func (s *PublicStashTabSubscription) run(firstChangeId string) { 44 | defer close(s.Channel) 45 | 46 | nextChangeId := firstChangeId 47 | 48 | const requestInterval = time.Second 49 | var lastRequestTime time.Time 50 | 51 | for { 52 | waitTime := requestInterval - time.Now().Sub(lastRequestTime) 53 | if waitTime > 0 { 54 | time.Sleep(waitTime) 55 | } 56 | 57 | select { 58 | case <-s.closeChannel: 59 | return 60 | default: 61 | lastRequestTime = time.Now() 62 | response, err := http.Get("https://" + s.host + "/api/public-stash-tabs?id=" + url.QueryEscape(nextChangeId)) 63 | if err != nil { 64 | s.Channel <- PublicStashTabSubscriptionResult{ 65 | ChangeId: nextChangeId, 66 | Error: err, 67 | } 68 | continue 69 | } 70 | 71 | tabs := new(PublicStashTabs) 72 | decoder := json.NewDecoder(response.Body) 73 | err = decoder.Decode(tabs) 74 | if err != nil { 75 | s.Channel <- PublicStashTabSubscriptionResult{ 76 | ChangeId: nextChangeId, 77 | Error: err, 78 | } 79 | continue 80 | } 81 | 82 | if len(tabs.Stashes) > 0 { 83 | s.Channel <- PublicStashTabSubscriptionResult{ 84 | ChangeId: nextChangeId, 85 | PublicStashTabs: tabs, 86 | } 87 | } 88 | 89 | nextChangeId = tabs.NextChangeId 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /api/public_stash_tabs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type PublicStashTabs struct { 4 | NextChangeId string `json:"next_change_id"` 5 | Stashes []Stash `json:"stashes"` 6 | } 7 | -------------------------------------------------------------------------------- /api/public_stash_tabs_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var sampleJSON []byte 12 | 13 | func init() { 14 | b, err := ioutil.ReadFile("testdata/public-stash-tabs.json") 15 | if err != nil { 16 | panic(err) 17 | } 18 | sampleJSON = b 19 | } 20 | 21 | func TestPublicStashTabs(t *testing.T) { 22 | var tabs PublicStashTabs 23 | err := json.Unmarshal(sampleJSON, &tabs) 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, "2300-4136-3306-4292-1278", tabs.NextChangeId) 27 | assert.NotEmpty(t, tabs.Stashes) 28 | } 29 | 30 | func BenchmarkPublicStashTabs(b *testing.B) { 31 | var tabs PublicStashTabs 32 | err := json.Unmarshal(sampleJSON, &tabs) 33 | assert.NoError(b, err) 34 | 35 | assert.Equal(b, "2300-4136-3306-4292-1278", tabs.NextChangeId) 36 | assert.NotEmpty(b, tabs.Stashes) 37 | } 38 | -------------------------------------------------------------------------------- /api/stash.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Stash struct { 4 | AccountName string `json:"accountName"` 5 | LastCharacterName string `json:"lastCharacterName"` 6 | Id string `json:"id"` 7 | Label string `json:"stash"` 8 | Type string `json:"stashType"` 9 | Items []Item `json:"items"` 10 | IsPublic bool `json:"public"` 11 | } 12 | -------------------------------------------------------------------------------- /examples/poe-indexing-101/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | 11 | "github.com/ccbrown/poe-go/api" 12 | ) 13 | 14 | // To begin receiving newly updated items immediately, we need to get a recent change id. poe.ninja 15 | // can provide us with that. 16 | func getRecentChangeId() (string, error) { 17 | resp, err := http.Get("http://api.poe.ninja/api/Data/GetStats") 18 | if err != nil { 19 | return "", err 20 | } 21 | defer resp.Body.Close() 22 | 23 | body, err := ioutil.ReadAll(resp.Body) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | var stats struct { 29 | // There are a few more fields in the response, but we only care about this. 30 | NextChangeId string `json:"nextChangeId"` 31 | } 32 | if err := json.Unmarshal(body, &stats); err != nil { 33 | return "", err 34 | } 35 | 36 | return stats.NextChangeId, nil 37 | } 38 | 39 | // This is where we look through stashes for items of interest. For this example, we'll just log 40 | // reliquary key activity. You might want to parse buyouts, play sounds, compose messages that you 41 | // can send in whispers, etc. 42 | func processStash(stash *api.Stash) { 43 | for _, item := range stash.Items { 44 | if item.Type == "Ancient Reliquary Key" { 45 | log.Printf("Ancient Reliquary Key: account = %v, league = %v, note = %v, tab = %v", stash.AccountName, item.League, item.Note, stash.Label) 46 | } 47 | } 48 | } 49 | 50 | func main() { 51 | log.Printf("requesting a recent change id from poe.ninja...") 52 | recentChangeId, err := getRecentChangeId() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | log.Printf("starting with change id %v", recentChangeId) 57 | 58 | subscription := api.OpenPublicStashTabSubscription(recentChangeId) 59 | 60 | // If we get an interrupt signal (e.g. from control+c on the terminal), handle it gracefully. 61 | go func() { 62 | ch := make(chan os.Signal) 63 | signal.Notify(ch, os.Interrupt) 64 | <-ch 65 | log.Printf("shutting down") 66 | subscription.Close() 67 | }() 68 | 69 | // Loop forever over results. 70 | for result := range subscription.Channel { 71 | if result.Error != nil { 72 | log.Printf("error: %v", result.Error.Error()) 73 | continue 74 | } 75 | for _, stash := range result.PublicStashTabs.Stashes { 76 | processStash(&stash) 77 | } 78 | } 79 | } 80 | --------------------------------------------------------------------------------