diff options
Diffstat (limited to 'vendor/github.com/Philipp15b/go-steam/trade')
6 files changed, 624 insertions, 0 deletions
diff --git a/vendor/github.com/Philipp15b/go-steam/trade/actions.go b/vendor/github.com/Philipp15b/go-steam/trade/actions.go new file mode 100644 index 00000000..e9301940 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/actions.go @@ -0,0 +1,84 @@ +package trade + +import ( + "github.com/Philipp15b/go-steam/economy/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) GetTheirInventory(contextId uint64, appId uint32) (*inventory.Inventory, error) { + return inventory.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.api.GetOwnInventory(contextId, appId) +} + +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()) +} diff --git a/vendor/github.com/Philipp15b/go-steam/trade/doc.go b/vendor/github.com/Philipp15b/go-steam/trade/doc.go new file mode 100644 index 00000000..549ad47d --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/doc.go @@ -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, steamLoginSecure, 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 diff --git a/vendor/github.com/Philipp15b/go-steam/trade/trade.go b/vendor/github.com/Philipp15b/go-steam/trade/trade.go new file mode 100644 index 00000000..4889f407 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/trade.go @@ -0,0 +1,122 @@ +package trade + +import ( + "errors" + "time" + + "github.com/Philipp15b/go-steam/steamid" + "github.com/Philipp15b/go-steam/trade/tradeapi" +) + +const pollTimeout = time.Second + +type Trade struct { + ThemId steamid.SteamId + + MeReady, ThemReady bool + + lastPoll time.Time + queuedEvents []interface{} + api *tradeapi.Trade +} + +func New(sessionId, steamLogin, steamLoginSecure string, other steamid.SteamId) *Trade { + return &Trade{ + other, + false, false, + time.Unix(0, 0), + nil, + tradeapi.New(sessionId, steamLogin, steamLoginSecure, other), + } +} + +func (t *Trade) Version() uint { + return t.api.Version +} + +// Returns all queued events and removes them from the queue without performing a HTTP request, like Poll() would. +func (t *Trade) Events() []interface{} { + qe := t.queuedEvents + t.queuedEvents = nil + return qe +} + +func (t *Trade) onStatus(status *tradeapi.Status) error { + if !status.Success { + return errors.New("trade: returned status not successful! error message: " + status.Error) + } + + if status.NewVersion { + t.api.Version = status.Version + + t.MeReady = status.Me.Ready == true + t.ThemReady = status.Them.Ready == true + } + + switch status.TradeStatus { + case tradeapi.TradeStatus_Complete: + t.addEvent(&TradeEndedEvent{TradeEndReason_Complete}) + case tradeapi.TradeStatus_Cancelled: + t.addEvent(&TradeEndedEvent{TradeEndReason_Cancelled}) + case tradeapi.TradeStatus_Timeout: + t.addEvent(&TradeEndedEvent{TradeEndReason_Timeout}) + case tradeapi.TradeStatus_Failed: + t.addEvent(&TradeEndedEvent{TradeEndReason_Failed}) + case tradeapi.TradeStatus_Open: + // nothing + default: + // ignore too + } + + t.updateEvents(status.Events) + return nil +} + +func (t *Trade) updateEvents(events tradeapi.EventList) { + if len(events) == 0 { + return + } + + var lastLogPos uint + for i, event := range events { + if i < t.api.LogPos { + continue + } + if event.SteamId != t.ThemId { + continue + } + + if lastLogPos < i { + lastLogPos = i + } + + switch event.Action { + case tradeapi.Action_AddItem: + t.addEvent(&ItemAddedEvent{newItem(event)}) + case tradeapi.Action_RemoveItem: + t.addEvent(&ItemRemovedEvent{newItem(event)}) + case tradeapi.Action_Ready: + t.ThemReady = true + t.addEvent(new(ReadyEvent)) + case tradeapi.Action_Unready: + t.ThemReady = false + t.addEvent(new(UnreadyEvent)) + case tradeapi.Action_SetCurrency: + t.addEvent(&SetCurrencyEvent{ + newCurrency(event), + event.OldAmount, + event.NewAmount, + }) + case tradeapi.Action_ChatMessage: + t.addEvent(&ChatEvent{ + event.Text, + }) + } + } + + t.api.LogPos = uint(lastLogPos) + 1 +} + +func (t *Trade) addEvent(event interface{}) { + t.queuedEvents = append(t.queuedEvents, event) +} diff --git a/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/status.go b/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/status.go new file mode 100644 index 00000000..0a5278de --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/status.go @@ -0,0 +1,111 @@ +package tradeapi + +import ( + "encoding/json" + "github.com/Philipp15b/go-steam/jsont" + "github.com/Philipp15b/go-steam/steamid" + "strconv" +) + +type Status struct { + Success bool + Error string + NewVersion bool `json:"newversion"` + TradeStatus TradeStatus `json:"trade_status"` + Version uint + LogPos int + Me User + Them User + Events EventList +} + +type TradeStatus uint + +const ( + TradeStatus_Open TradeStatus = 0 + TradeStatus_Complete = 1 + TradeStatus_Empty = 2 // when both parties trade no items + TradeStatus_Cancelled = 3 + TradeStatus_Timeout = 4 // the partner timed out + TradeStatus_Failed = 5 +) + +type EventList map[uint]*Event + +// The EventList can either be an array or an object of id -> event +func (e *EventList) UnmarshalJSON(data []byte) error { + // initialize the map if it's nil + if *e == nil { + *e = make(EventList) + } + + o := make(map[string]*Event) + err := json.Unmarshal(data, &o) + // it's an object + if err == nil { + for is, event := range o { + i, err := strconv.ParseUint(is, 10, 32) + if err != nil { + panic(err) + } + (*e)[uint(i)] = event + } + return nil + } + + // it's an array + var a []*Event + err = json.Unmarshal(data, &a) + if err != nil { + return err + } + for i, event := range a { + (*e)[uint(i)] = event + } + return nil +} + +type Event struct { + SteamId steamid.SteamId `json:",string"` + Action Action `json:",string"` + Timestamp uint64 + + AppId uint32 + ContextId uint64 `json:",string"` + AssetId uint64 `json:",string"` + + Text string // only used for chat messages + + // The following is used for SetCurrency + CurrencyId uint64 `json:",string"` + OldAmount uint64 `json:"old_amount,string"` + NewAmount uint64 `json:"amount,string"` +} + +type Action uint + +const ( + Action_AddItem Action = 0 + Action_RemoveItem = 1 + Action_Ready = 2 + Action_Unready = 3 + Action_Accept = 4 + Action_SetCurrency = 6 + Action_ChatMessage = 7 +) + +type User struct { + Ready jsont.UintBool + Confirmed jsont.UintBool + SecSinceTouch int `json:"sec_since_touch"` + ConnectionPending bool `json:"connection_pending"` + Assets interface{} + Currency interface{} // either []*Currency or empty string +} + +type Currency struct { + AppId uint64 `json:",string"` + ContextId uint64 `json:",string"` + CurrencyId uint64 `json:",string"` + Amount uint64 `json:",string"` +} diff --git a/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/trade.go b/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/trade.go new file mode 100644 index 00000000..5e9f2a98 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/tradeapi/trade.go @@ -0,0 +1,200 @@ +/* +Wrapper around the HTTP trading API for type safety 'n' stuff. +*/ +package tradeapi + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/Philipp15b/go-steam/community" + "github.com/Philipp15b/go-steam/economy/inventory" + "github.com/Philipp15b/go-steam/netutil" + "github.com/Philipp15b/go-steam/steamid" + "io/ioutil" + "net/http" + "regexp" + "strconv" + "time" +) + +const tradeUrl = "https://steamcommunity.com/trade/%d/" + +type Trade struct { + client *http.Client + other steamid.SteamId + + LogPos uint // not automatically updated + Version uint // Incremented for each item change by Steam; not automatically updated. + + // the `sessionid` cookie is sent as a parameter/POST data for CSRF protection. + sessionId string + baseUrl string +} + +// Creates a new Trade based on the given cookies `sessionid`, `steamLogin`, `steamLoginSecure` and the trade partner's Steam ID. +func New(sessionId, steamLogin, steamLoginSecure string, other steamid.SteamId) *Trade { + client := new(http.Client) + client.Timeout = 10 * time.Second + + t := &Trade{ + client: client, + other: other, + sessionId: sessionId, + baseUrl: fmt.Sprintf(tradeUrl, other), + Version: 1, + } + community.SetCookies(t.client, sessionId, steamLogin, steamLoginSecure) + return t +} + +type Main struct { + PartnerOnProbation bool +} + +var onProbationRegex = regexp.MustCompile(`var g_bTradePartnerProbation = (\w+);`) + +// Fetches the main HTML page and parses it. Thread-safe. +func (t *Trade) GetMain() (*Main, error) { + resp, err := t.client.Get(t.baseUrl) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + match := onProbationRegex.FindSubmatch(body) + if len(match) == 0 { + return nil, errors.New("tradeapi.GetMain: Could not find probation info") + } + + return &Main{ + string(match[1]) == "true", + }, nil +} + +// Ajax POSTs to an API endpoint that should return a status +func (t *Trade) postWithStatus(url string, data map[string]string) (*Status, error) { + status := new(Status) + + req := netutil.NewPostForm(url, netutil.ToUrlValues(data)) + // Tales of Madness and Pain, Episode 1: If you forget this, Steam will return an error + // saying "missing required parameter", even though they are all there. IT WAS JUST THE HEADER, ARGH! + req.Header.Add("Referer", t.baseUrl) + + resp, err := t.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(status) + if err != nil { + return nil, err + } + return status, nil +} + +func (t *Trade) GetStatus() (*Status, error) { + return t.postWithStatus(t.baseUrl+"tradestatus/", map[string]string{ + "sessionid": t.sessionId, + "logpos": strconv.FormatUint(uint64(t.LogPos), 10), + "version": strconv.FormatUint(uint64(t.Version), 10), + }) +} + +// Thread-safe. +func (t *Trade) GetForeignInventory(contextId uint64, appId uint32, start *uint) (*inventory.PartialInventory, error) { + data := map[string]string{ + "sessionid": t.sessionId, + "steamid": fmt.Sprintf("%d", t.other), + "contextid": strconv.FormatUint(contextId, 10), + "appid": strconv.FormatUint(uint64(appId), 10), + } + if start != nil { + data["start"] = strconv.FormatUint(uint64(*start), 10) + } + + req, err := http.NewRequest("GET", t.baseUrl+"foreigninventory?"+netutil.ToUrlValues(data).Encode(), nil) + if err != nil { + panic(err) + } + req.Header.Add("Referer", t.baseUrl) + + return inventory.DoInventoryRequest(t.client, req) +} + +// Thread-safe. +func (t *Trade) GetOwnInventory(contextId uint64, appId uint32) (*inventory.Inventory, error) { + return inventory.GetOwnInventory(t.client, contextId, appId) +} + +func (t *Trade) Chat(message string) (*Status, error) { + return t.postWithStatus(t.baseUrl+"chat", map[string]string{ + "sessionid": t.sessionId, + "logpos": strconv.FormatUint(uint64(t.LogPos), 10), + "version": strconv.FormatUint(uint64(t.Version), 10), + "message": message, + }) +} + +func (t *Trade) AddItem(slot uint, itemId, contextId uint64, appId uint32) (*Status, error) { + return t.postWithStatus(t.baseUrl+"additem", map[string]string{ + "sessionid": t.sessionId, + "slot": strconv.FormatUint(uint64(slot), 10), + "itemid": strconv.FormatUint(itemId, 10), + "contextid": strconv.FormatUint(contextId, 10), + "appid": strconv.FormatUint(uint64(appId), 10), + }) +} + +func (t *Trade) RemoveItem(slot uint, itemId, contextId uint64, appId uint32) (*Status, error) { + return t.postWithStatus(t.baseUrl+"removeitem", map[string]string{ + "sessionid": t.sessionId, + "slot": strconv.FormatUint(uint64(slot), 10), + "itemid": strconv.FormatUint(itemId, 10), + "contextid": strconv.FormatUint(contextId, 10), + "appid": strconv.FormatUint(uint64(appId), 10), + }) +} + +func (t *Trade) SetCurrency(amount uint, currencyId, contextId uint64, appId uint32) (*Status, error) { + return t.postWithStatus(t.baseUrl+"setcurrency", map[string]string{ + "sessionid": t.sessionId, + "amount": strconv.FormatUint(uint64(amount), 10), + "currencyid": strconv.FormatUint(uint64(currencyId), 10), + "contextid": strconv.FormatUint(contextId, 10), + "appid": strconv.FormatUint(uint64(appId), 10), + }) +} + +func (t *Trade) SetReady(ready bool) (*Status, error) { + return t.postWithStatus(t.baseUrl+"toggleready", map[string]string{ + "sessionid": t.sessionId, + "version": strconv.FormatUint(uint64(t.Version), 10), + "ready": fmt.Sprint(ready), + }) +} + +func (t *Trade) Confirm() (*Status, error) { + return t.postWithStatus(t.baseUrl+"confirm", map[string]string{ + "sessionid": t.sessionId, + "version": strconv.FormatUint(uint64(t.Version), 10), + }) +} + +func (t *Trade) Cancel() (*Status, error) { + return t.postWithStatus(t.baseUrl+"cancel", map[string]string{ + "sessionid": t.sessionId, + }) +} + +func isSuccess(v interface{}) bool { + if m, ok := v.(map[string]interface{}); ok { + return m["success"] == true + } + return false +} diff --git a/vendor/github.com/Philipp15b/go-steam/trade/types.go b/vendor/github.com/Philipp15b/go-steam/trade/types.go new file mode 100644 index 00000000..e6f9a537 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/trade/types.go @@ -0,0 +1,67 @@ +package trade + +import ( + "github.com/Philipp15b/go-steam/trade/tradeapi" +) + +type TradeEndedEvent struct { + Reason TradeEndReason +} + +type TradeEndReason uint + +const ( + TradeEndReason_Complete TradeEndReason = 1 + TradeEndReason_Cancelled = 2 + TradeEndReason_Timeout = 3 + TradeEndReason_Failed = 4 +) + +func newItem(event *tradeapi.Event) *Item { + return &Item{ + event.AppId, + event.ContextId, + event.AssetId, + } +} + +type Item struct { + AppId uint32 + ContextId uint64 + AssetId uint64 +} + +type ItemAddedEvent struct { + Item *Item +} + +type ItemRemovedEvent struct { + Item *Item +} + +type ReadyEvent struct{} +type UnreadyEvent struct{} + +func newCurrency(event *tradeapi.Event) *Currency { + return &Currency{ + event.AppId, + event.ContextId, + event.CurrencyId, + } +} + +type Currency struct { + AppId uint32 + ContextId uint64 + CurrencyId uint64 +} + +type SetCurrencyEvent struct { + Currency *Currency + OldAmount uint64 + NewAmount uint64 +} + +type ChatEvent struct { + Message string +} |