Skip to content

Commit

Permalink
Add full support for Steam Trading
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipp15b committed Jul 15, 2014
1 parent 3695e24 commit 87f4ff5
Show file tree
Hide file tree
Showing 10 changed files with 898 additions and 0 deletions.
19 changes: 19 additions & 0 deletions jsont/jsont.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Includes helper types for working with JSON data
package jsont

import (
"encoding/json"
)

// A boolean value that can be unmarshaled from a number in JSON.
type UintBool bool

func (u *UintBool) UnmarshalJSON(data []byte) error {
var n uint
err := json.Unmarshal(data, &n)
if err != nil {
return err
}
*u = n != 0
return nil
}
115 changes: 115 additions & 0 deletions trade/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package trade

import (
"errors"
"github.com/Philipp15b/go-steam/trade/inventory"
"github.com/Philipp15b/go-steam/trade/tradeapi"
"time"
)

type Slot uint

func (t *Trade) action(status *tradeapi.Status, err error) error {
if err != nil {
return err
}
t.onStatus(status)
return nil
}

// Returns the next batch of events to process. These can be queued from calls to methods
// like `AddItem` or, if there are no queued events, from a new HTTP request to Steam's API (blocking!).
// If the latter is the case, this method may also sleep before the request
// to conform to the polling interval of the official Steam client.
func (t *Trade) Poll() ([]interface{}, error) {
if t.queuedEvents != nil {
return t.Events(), nil
}

if d := time.Since(t.lastPoll); d < pollTimeout {
time.Sleep(pollTimeout - d)
}
t.lastPoll = time.Now()

err := t.action(t.api.GetStatus())
if err != nil {
return nil, err
}

return t.Events(), nil
}

func (t *Trade) getFullInventory(getFirst func() (*inventory.PartialInventory, error), getNext func(start uint) (*inventory.PartialInventory, error)) (*inventory.Inventory, error) {
first, err := getFirst()
if err != nil {
return nil, err
}
if !first.Success {
return nil, errors.New("getFullInventory API call failed.")
}

result := &first.Inventory
var next *inventory.PartialInventory
for latest := first; latest.More; latest = next {
next, err := getNext(uint(latest.MoreStart))
if err != nil {
return nil, err
}
if !next.Success {
return nil, errors.New("getFullInventory API call failed.")
}

result = inventory.Merge(result, &next.Inventory)
}

return result, nil
}

func (t *Trade) GetTheirInventory(contextId uint64, appId uint32) (*inventory.Inventory, error) {
return t.getFullInventory(func() (*inventory.PartialInventory, error) {
return t.api.GetForeignInventory(contextId, appId, nil)
}, func(start uint) (*inventory.PartialInventory, error) {
return t.api.GetForeignInventory(contextId, appId, &start)
})
}

func (t *Trade) GetOwnInventory(contextId uint64, appId uint32) (*inventory.Inventory, error) {
return t.getFullInventory(func() (*inventory.PartialInventory, error) {
return t.api.GetOwnInventory(contextId, appId, nil)
}, func(start uint) (*inventory.PartialInventory, error) {
return t.api.GetOwnInventory(contextId, appId, &start)
})
}

func (t *Trade) GetMain() (*tradeapi.Main, error) {
return t.api.GetMain()
}

func (t *Trade) AddItem(slot Slot, item *Item) error {
return t.action(t.api.AddItem(uint(slot), item.AssetId, item.ContextId, item.AppId))
}

func (t *Trade) RemoveItem(slot Slot, item *Item) error {
return t.action(t.api.RemoveItem(uint(slot), item.AssetId, item.ContextId, item.AppId))
}

func (t *Trade) Chat(message string) error {
return t.action(t.api.Chat(message))
}

func (t *Trade) SetCurrency(amount uint, currency *Currency) error {
return t.action(t.api.SetCurrency(amount, currency.CurrencyId, currency.ContextId, currency.AppId))
}

func (t *Trade) SetReady(ready bool) error {
return t.action(t.api.SetReady(ready))
}

// This may only be called after a successful `SetReady(true)`.
func (t *Trade) Confirm() error {
return t.action(t.api.Confirm())
}

func (t *Trade) Cancel() error {
return t.action(t.api.Cancel())
}
40 changes: 40 additions & 0 deletions trade/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Allows automation of Steam Trading.
Usage
Like go-steam, this package is event-based. Call Poll() until the trade has ended, that is until the TradeEndedEvent is emitted.
// After receiving the steam.TradeSessionStartEvent
t := trade.New(sessionIdCookie, steamLoginCookie, event.Other)
for {
eventList, err := t.Poll()
if err != nil {
// error handling here
continue
}
for _, event := range eventList {
switch e := event.(type) {
case *trade.ChatEvent:
// respond to any chat message
t.Chat("Trading is awesome!")
case *trade.TradeEndedEvent:
return
// other event handlers here
}
}
}
You can either log into steamcommunity.com and use the values of the `sessionId` and `steamLogin` cookies,
or use go-steam and after logging in with client.Web.LogOn() and receiving the WebLoggedOnEvent use the `SessionId`
and `SteamLogin` fields of steam.Web for the respective cookies.
It is important that there is no delay between the Poll() calls greater than the timeout of the Steam client
(currently five seconds before the trade partner sees a warning) or the trade will be closed automatically by Steam.
Notes
All method calls to Steam APIs are blocking. This packages' and its subpackages' types are not thread-safe and no calls to any method of the same
trade instance may be done concurrently except when otherwise noted.
*/
package trade
110 changes: 110 additions & 0 deletions trade/inventory/inventory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package inventory

import (
"bytes"
"encoding/json"
"github.com/Philipp15b/go-steam/jsont"
)

type Inventory struct {
Items Items `json:"rgInventory"`
Currencies Currencies `json:"rgCurrency"`
Descriptions Descriptions `json:"rgDescriptions"`
AppInfo *AppInfo `json:"rgAppInfo"`
}

type Items map[string]*Item

func (i *Items) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Item)(i))
}

type Currencies map[string]*Currency

func (c *Currencies) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Currency)(c))
}

type Descriptions map[string]*Description

func (d *Descriptions) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
return json.Unmarshal(data, (*map[string]*Description)(d))
}

type Item struct {
Id uint64 `json:",string"`
ClassId uint64 `json:",string"`
InstanceId uint64 `json:",string"`
Amount uint64 `json:",string"`
Pos uint32
}

type Currency struct {
Id uint64 `json:",string"`
ClassId uint64 `json:",string"`
IsCurrency bool `json:"is_currency"`
Pos uint32
}

type Description struct {
AppId uint32 `json:",string"`
ClassId uint64 `json:",string"`
InstanceId uint64 `json:",string"`

IconUrl string `json:"icon_url"`
IconDragUrl string `json:"icon_drag_url"`

Name string
MarketName string `json:"market_name"`

// Colors in hex, for example `B2B2B2`
NameColor string `json:"name_color"`
BackgroundColor string `json:"background_color"`

Type string

Tradable jsont.UintBool
Marketable jsont.UintBool
Commodity jsont.UintBool

Descriptions DescriptionLines
Actions []*Action
// Application-specific data, like "def_index" and "quality" for TF2
AppData map[string]string
}

type DescriptionLines []*DescriptionLine

func (d *DescriptionLines) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte(`""`)) {
return nil
}
return json.Unmarshal(data, (*[]*DescriptionLine)(d))
}

type DescriptionLine struct {
Value string
Type *string // Is `html` for HTML descriptions
Label *string
}

type Action struct {
Name string
Link string
}

type AppInfo struct {
AppId uint32
Name string
Icon string
Link string
}
47 changes: 47 additions & 0 deletions trade/inventory/partial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package inventory

import (
"bytes"
"encoding/json"
)

// A partial inventory as sent by the Steam API.
type PartialInventory struct {
Success bool
Inventory
More bool
MoreStart MoreStart `json:"more_start"`
}

type MoreStart uint

func (m *MoreStart) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("false")) {
return nil
}
return json.Unmarshal(data, (*uint)(m))
}

// Merges the given Inventory into a single Inventory.
// The given slice must have at least one element. The first element of the slice is used
// and modified.
func Merge(p ...*Inventory) *Inventory {
inv := p[0]
for idx, i := range p {
if idx == 0 {
continue
}

for key, value := range i.Items {
inv.Items[key] = value
}
for key, value := range i.Descriptions {
inv.Descriptions[key] = value
}
for key, value := range i.Currencies {
inv.Currencies[key] = value
}
}

return inv
}
Loading

0 comments on commit 87f4ff5

Please sign in to comment.