diff options
Diffstat (limited to 'vendor/github.com/Philipp15b/go-steam/tradeoffer')
5 files changed, 671 insertions, 0 deletions
diff --git a/vendor/github.com/Philipp15b/go-steam/tradeoffer/client.go b/vendor/github.com/Philipp15b/go-steam/tradeoffer/client.go new file mode 100644 index 00000000..7bf171c3 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/tradeoffer/client.go @@ -0,0 +1,452 @@ +package tradeoffer + +import ( + "encoding/json" + "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" + "strconv" + "time" +) + +type APIKey string + +const apiUrl = "https://api.steampowered.com/IEconService/%s/v%d" + +type Client struct { + client *http.Client + key APIKey + sessionId string +} + +func NewClient(key APIKey, sessionId, steamLogin, steamLoginSecure string) *Client { + c := &Client{ + new(http.Client), + key, + sessionId, + } + community.SetCookies(c.client, sessionId, steamLogin, steamLoginSecure) + return c +} + +func (c *Client) GetOffer(offerId uint64) (*TradeOfferResult, error) { + resp, err := c.client.Get(fmt.Sprintf(apiUrl, "GetTradeOffer", 1) + "?" + netutil.ToUrlValues(map[string]string{ + "key": string(c.key), + "tradeofferid": strconv.FormatUint(offerId, 10), + "language": "en_us", + }).Encode()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + t := new(struct { + Response *TradeOfferResult + }) + if err = json.NewDecoder(resp.Body).Decode(t); err != nil { + return nil, err + } + if t.Response == nil || t.Response.Offer == nil { + return nil, newSteamErrorf("steam returned empty offer result\n") + } + + return t.Response, nil +} + +func (c *Client) GetOffers(getSent bool, getReceived bool, getDescriptions bool, activeOnly bool, historicalOnly bool, timeHistoricalCutoff *uint32) (*TradeOffersResult, error) { + if !getSent && !getReceived { + return nil, fmt.Errorf("getSent and getReceived can't be both false\n") + } + + params := map[string]string{ + "key": string(c.key), + } + if getSent { + params["get_sent_offers"] = "1" + } + if getReceived { + params["get_received_offers"] = "1" + } + if getDescriptions { + params["get_descriptions"] = "1" + params["language"] = "en_us" + } + if activeOnly { + params["active_only"] = "1" + } + if historicalOnly { + params["historical_only"] = "1" + } + if timeHistoricalCutoff != nil { + params["time_historical_cutoff"] = strconv.FormatUint(uint64(*timeHistoricalCutoff), 10) + } + resp, err := c.client.Get(fmt.Sprintf(apiUrl, "GetTradeOffers", 1) + "?" + netutil.ToUrlValues(params).Encode()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + t := new(struct { + Response *TradeOffersResult + }) + if err = json.NewDecoder(resp.Body).Decode(t); err != nil { + return nil, err + } + if t.Response == nil { + return nil, newSteamErrorf("steam returned empty offers result\n") + } + return t.Response, nil +} + +// action() is used by Decline() and Cancel() +// Steam only return success and error fields for malformed requests, +// hence client shall use GetOffer() to check action result +// It is also possible to implement Decline/Cancel using steamcommunity, +// which have more predictable responses +func (c *Client) action(method string, version uint, offerId uint64) error { + resp, err := c.client.Do(netutil.NewPostForm(fmt.Sprintf(apiUrl, method, version), netutil.ToUrlValues(map[string]string{ + "key": string(c.key), + "tradeofferid": strconv.FormatUint(offerId, 10), + }))) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf(method+" error: status code %d", resp.StatusCode) + } + return nil +} + +func (c *Client) Decline(offerId uint64) error { + return c.action("DeclineTradeOffer", 1, offerId) +} + +func (c *Client) Cancel(offerId uint64) error { + return c.action("CancelTradeOffer", 1, offerId) +} + +// Accept received trade offer +// It is best to confirm that offer was actually accepted +// by calling GetOffer after Accept and checking offer state +func (c *Client) Accept(offerId uint64) error { + baseurl := fmt.Sprintf("https://steamcommunity.com/tradeoffer/%d/", offerId) + req := netutil.NewPostForm(baseurl+"accept", netutil.ToUrlValues(map[string]string{ + "sessionid": c.sessionId, + "serverid": "1", + "tradeofferid": strconv.FormatUint(offerId, 10), + })) + req.Header.Add("Referer", baseurl) + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + t := new(struct { + StrError string `json:"strError"` + }) + if err = json.NewDecoder(resp.Body).Decode(t); err != nil { + return err + } + if t.StrError != "" { + return newSteamErrorf("accept error: %v\n", t.StrError) + } + if resp.StatusCode != 200 { + return fmt.Errorf("accept error: status code %d", resp.StatusCode) + } + return nil +} + +type TradeItem struct { + AppId uint32 `json:"appid"` + ContextId uint64 `json:"contextid,string"` + Amount uint64 `json:"amount"` + AssetId uint64 `json:"assetid,string,omitempty"` + CurrencyId uint64 `json:"currencyid,string,omitempty"` +} + +// Sends a new trade offer to the given Steam user. You can optionally specify an access token if you've got one. +// In addition, `counteredOfferId` can be non-nil, indicating the trade offer this is a counter for. +// On success returns trade offer id +func (c *Client) Create(other steamid.SteamId, accessToken *string, myItems, theirItems []TradeItem, counteredOfferId *uint64, message string) (uint64, error) { + // Create new trade offer status + to := map[string]interface{}{ + "newversion": true, + "version": 3, + "me": map[string]interface{}{ + "assets": myItems, + "currency": make([]struct{}, 0), + "ready": false, + }, + "them": map[string]interface{}{ + "assets": theirItems, + "currency": make([]struct{}, 0), + "ready": false, + }, + } + + jto, err := json.Marshal(to) + if err != nil { + panic(err) + } + + // Create url parameters for request + data := map[string]string{ + "sessionid": c.sessionId, + "serverid": "1", + "partner": other.ToString(), + "tradeoffermessage": message, + "json_tradeoffer": string(jto), + } + + var referer string + if counteredOfferId != nil { + referer = fmt.Sprintf("https://steamcommunity.com/tradeoffer/%d/", *counteredOfferId) + data["tradeofferid_countered"] = strconv.FormatUint(*counteredOfferId, 10) + } else { + // Add token for non-friend offers + if accessToken != nil { + params := map[string]string{ + "trade_offer_access_token": *accessToken, + } + paramsJson, err := json.Marshal(params) + if err != nil { + panic(err) + } + + data["trade_offer_create_params"] = string(paramsJson) + + referer = "https://steamcommunity.com/tradeoffer/new/?partner=" + strconv.FormatUint(uint64(other.GetAccountId()), 10) + "&token=" + *accessToken + } else { + + referer = "https://steamcommunity.com/tradeoffer/new/?partner=" + strconv.FormatUint(uint64(other.GetAccountId()), 10) + } + } + + // Create request + req := netutil.NewPostForm("https://steamcommunity.com/tradeoffer/new/send", netutil.ToUrlValues(data)) + req.Header.Add("Referer", referer) + + // Send request + resp, err := c.client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + t := new(struct { + StrError string `json:"strError"` + TradeOfferId uint64 `json:"tradeofferid,string"` + }) + if err = json.NewDecoder(resp.Body).Decode(t); err != nil { + return 0, err + } + // strError code descriptions: + // 15 invalide trade access token + // 16 timeout + // 20 wrong contextid + // 25 can't send more offers until some is accepted/cancelled... + // 26 object is not in our inventory + // error code names are in internal/steamlang/enums.go EResult_name + if t.StrError != "" { + return 0, newSteamErrorf("create error: %v\n", t.StrError) + } + if resp.StatusCode != 200 { + return 0, fmt.Errorf("create error: status code %d", resp.StatusCode) + } + if t.TradeOfferId == 0 { + return 0, newSteamErrorf("create error: steam returned 0 for trade offer id") + } + return t.TradeOfferId, nil +} + +func (c *Client) GetOwnInventory(contextId uint64, appId uint32) (*inventory.Inventory, error) { + return inventory.GetOwnInventory(c.client, contextId, appId) +} + +func (c *Client) GetPartnerInventory(other steamid.SteamId, contextId uint64, appId uint32, offerId *uint64) (*inventory.Inventory, error) { + return inventory.GetFullInventory(func() (*inventory.PartialInventory, error) { + return c.getPartialPartnerInventory(other, contextId, appId, offerId, nil) + }, func(start uint) (*inventory.PartialInventory, error) { + return c.getPartialPartnerInventory(other, contextId, appId, offerId, &start) + }) +} + +func (c *Client) getPartialPartnerInventory(other steamid.SteamId, contextId uint64, appId uint32, offerId *uint64, start *uint) (*inventory.PartialInventory, error) { + data := map[string]string{ + "sessionid": c.sessionId, + "partner": other.ToString(), + "contextid": strconv.FormatUint(contextId, 10), + "appid": strconv.FormatUint(uint64(appId), 10), + } + if start != nil { + data["start"] = strconv.FormatUint(uint64(*start), 10) + } + + baseUrl := "https://steamcommunity.com/tradeoffer/%v/" + if offerId != nil { + baseUrl = fmt.Sprintf(baseUrl, *offerId) + } else { + baseUrl = fmt.Sprintf(baseUrl, "new") + } + + req, err := http.NewRequest("GET", baseUrl+"partnerinventory/?"+netutil.ToUrlValues(data).Encode(), nil) + if err != nil { + panic(err) + } + req.Header.Add("Referer", baseUrl+"?partner="+strconv.FormatUint(uint64(other.GetAccountId()), 10)) + + return inventory.DoInventoryRequest(c.client, req) +} + +// Can be used to verify accepted tradeoffer and find out received asset ids +func (c *Client) GetTradeReceipt(tradeId uint64) ([]*TradeReceiptItem, error) { + url := fmt.Sprintf("https://steamcommunity.com/trade/%d/receipt", tradeId) + resp, err := c.client.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + items, err := parseTradeReceipt(respBody) + if err != nil { + return nil, newSteamErrorf("failed to parse trade receipt: %v", err) + } + return items, nil +} + +// Get duration of escrow in days. Call this before sending a trade offer +func (c *Client) GetPartnerEscrowDuration(other steamid.SteamId, accessToken *string) (*EscrowDuration, error) { + data := map[string]string{ + "partner": strconv.FormatUint(uint64(other.GetAccountId()), 10), + } + if accessToken != nil { + data["token"] = *accessToken + } + return c.getEscrowDuration("https://steamcommunity.com/tradeoffer/new/?" + netutil.ToUrlValues(data).Encode()) +} + +// Get duration of escrow in days. Call this after receiving a trade offer +func (c *Client) GetOfferEscrowDuration(offerId uint64) (*EscrowDuration, error) { + return c.getEscrowDuration("http://steamcommunity.com/tradeoffer/" + strconv.FormatUint(offerId, 10)) +} + +func (c *Client) getEscrowDuration(queryUrl string) (*EscrowDuration, error) { + resp, err := c.client.Get(queryUrl) + if err != nil { + return nil, fmt.Errorf("failed to retrieve escrow duration: %v", err) + } + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + escrowDuration, err := parseEscrowDuration(respBody) + if err != nil { + return nil, newSteamErrorf("failed to parse escrow duration: %v", err) + } + return escrowDuration, nil +} + +func (c *Client) GetOfferWithRetry(offerId uint64, retryCount int, retryDelay time.Duration) (*TradeOfferResult, error) { + var res *TradeOfferResult + return res, withRetry( + func() (err error) { + res, err = c.GetOffer(offerId) + return err + }, retryCount, retryDelay) +} + +func (c *Client) GetOffersWithRetry(getSent bool, getReceived bool, getDescriptions bool, activeOnly bool, historicalOnly bool, timeHistoricalCutoff *uint32, retryCount int, retryDelay time.Duration) (*TradeOffersResult, error) { + var res *TradeOffersResult + return res, withRetry( + func() (err error) { + res, err = c.GetOffers(getSent, getReceived, getDescriptions, activeOnly, historicalOnly, timeHistoricalCutoff) + return err + }, retryCount, retryDelay) +} + +func (c *Client) DeclineWithRetry(offerId uint64, retryCount int, retryDelay time.Duration) error { + return withRetry( + func() error { + return c.Decline(offerId) + }, retryCount, retryDelay) +} + +func (c *Client) CancelWithRetry(offerId uint64, retryCount int, retryDelay time.Duration) error { + return withRetry( + func() error { + return c.Cancel(offerId) + }, retryCount, retryDelay) +} + +func (c *Client) AcceptWithRetry(offerId uint64, retryCount int, retryDelay time.Duration) error { + return withRetry( + func() error { + return c.Accept(offerId) + }, retryCount, retryDelay) +} + +func (c *Client) CreateWithRetry(other steamid.SteamId, accessToken *string, myItems, theirItems []TradeItem, counteredOfferId *uint64, message string, retryCount int, retryDelay time.Duration) (uint64, error) { + var res uint64 + return res, withRetry( + func() (err error) { + res, err = c.Create(other, accessToken, myItems, theirItems, counteredOfferId, message) + return err + }, retryCount, retryDelay) +} + +func (c *Client) GetOwnInventoryWithRetry(contextId uint64, appId uint32, retryCount int, retryDelay time.Duration) (*inventory.Inventory, error) { + var res *inventory.Inventory + return res, withRetry( + func() (err error) { + res, err = c.GetOwnInventory(contextId, appId) + return err + }, retryCount, retryDelay) +} + +func (c *Client) GetPartnerInventoryWithRetry(other steamid.SteamId, contextId uint64, appId uint32, offerId *uint64, retryCount int, retryDelay time.Duration) (*inventory.Inventory, error) { + var res *inventory.Inventory + return res, withRetry( + func() (err error) { + res, err = c.GetPartnerInventory(other, contextId, appId, offerId) + return err + }, retryCount, retryDelay) +} + +func (c *Client) GetTradeReceiptWithRetry(tradeId uint64, retryCount int, retryDelay time.Duration) ([]*TradeReceiptItem, error) { + var res []*TradeReceiptItem + return res, withRetry( + func() (err error) { + res, err = c.GetTradeReceipt(tradeId) + return err + }, retryCount, retryDelay) +} + +func withRetry(f func() error, retryCount int, retryDelay time.Duration) error { + if retryCount <= 0 { + panic("retry count must be more than 0") + } + i := 0 + for { + i++ + if err := f(); err != nil { + // If we got steam error do not retry + if _, ok := err.(*SteamError); ok { + return err + } + if i == retryCount { + return err + } + time.Sleep(retryDelay) + continue + } + break + } + return nil +} diff --git a/vendor/github.com/Philipp15b/go-steam/tradeoffer/error.go b/vendor/github.com/Philipp15b/go-steam/tradeoffer/error.go new file mode 100644 index 00000000..439ee69c --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/tradeoffer/error.go @@ -0,0 +1,19 @@ +package tradeoffer + +import ( + "fmt" +) + +// SteamError can be returned by Create, Accept, Decline and Cancel methods. +// It means we got response from steam, but it was in unknown format +// or request was declined. +type SteamError struct { + msg string +} + +func (e *SteamError) Error() string { + return e.msg +} +func newSteamErrorf(format string, a ...interface{}) *SteamError { + return &SteamError{fmt.Sprintf(format, a...)} +} diff --git a/vendor/github.com/Philipp15b/go-steam/tradeoffer/escrow.go b/vendor/github.com/Philipp15b/go-steam/tradeoffer/escrow.go new file mode 100644 index 00000000..07806716 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/tradeoffer/escrow.go @@ -0,0 +1,47 @@ +package tradeoffer + +import ( + "errors" + "fmt" + "regexp" + "strconv" +) + +type EscrowDuration struct { + DaysMyEscrow uint32 + DaysTheirEscrow uint32 +} + +func parseEscrowDuration(data []byte) (*EscrowDuration, error) { + // TODO: why we are using case insensitive matching? + myRegex := regexp.MustCompile("(?i)g_daysMyEscrow[\\s=]+(\\d+);") + theirRegex := regexp.MustCompile("(?i)g_daysTheirEscrow[\\s=]+(\\d+);") + + myM := myRegex.FindSubmatch(data) + theirM := theirRegex.FindSubmatch(data) + + if myM == nil || theirM == nil { + // check if access token is valid + notFriendsRegex := regexp.MustCompile(">You are not friends with this user<") + notFriendsM := notFriendsRegex.FindSubmatch(data) + if notFriendsM == nil { + return nil, errors.New("regexp does not match") + } else { + return nil, errors.New("you are not friends with this user") + } + } + + myEscrow, err := strconv.ParseUint(string(myM[1]), 10, 32) + if err != nil { + return nil, fmt.Errorf("failed to parse my duration into uint: %v", err) + } + theirEscrow, err := strconv.ParseUint(string(theirM[1]), 10, 32) + if err != nil { + return nil, fmt.Errorf("failed to parse their duration into uint: %v", err) + } + + return &EscrowDuration{ + DaysMyEscrow: uint32(myEscrow), + DaysTheirEscrow: uint32(theirEscrow), + }, nil +} diff --git a/vendor/github.com/Philipp15b/go-steam/tradeoffer/receipt.go b/vendor/github.com/Philipp15b/go-steam/tradeoffer/receipt.go new file mode 100644 index 00000000..3f4b556d --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/tradeoffer/receipt.go @@ -0,0 +1,35 @@ +package tradeoffer + +import ( + "encoding/json" + "fmt" + "github.com/Philipp15b/go-steam/economy/inventory" + "regexp" +) + +type TradeReceiptItem struct { + AssetId uint64 `json:"id,string"` + AppId uint32 + ContextId uint64 + Owner uint64 `json:",string"` + Pos uint32 + inventory.Description +} + +func parseTradeReceipt(data []byte) ([]*TradeReceiptItem, error) { + reg := regexp.MustCompile("oItem =\\s+(.+?});") + itemMatches := reg.FindAllSubmatch(data, -1) + if itemMatches == nil { + return nil, fmt.Errorf("items not found\n") + } + items := make([]*TradeReceiptItem, 0, len(itemMatches)) + for _, m := range itemMatches { + item := new(TradeReceiptItem) + err := json.Unmarshal(m[1], &item) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil +} diff --git a/vendor/github.com/Philipp15b/go-steam/tradeoffer/tradeoffer.go b/vendor/github.com/Philipp15b/go-steam/tradeoffer/tradeoffer.go new file mode 100644 index 00000000..1cf3aaa3 --- /dev/null +++ b/vendor/github.com/Philipp15b/go-steam/tradeoffer/tradeoffer.go @@ -0,0 +1,118 @@ +/* +Implements methods to interact with the official Trade Offer API. + +See: https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService +*/ +package tradeoffer + +import ( + "encoding/json" + "github.com/Philipp15b/go-steam/economy/inventory" + "github.com/Philipp15b/go-steam/steamid" +) + +type TradeOfferState uint + +const ( + TradeOfferState_Invalid TradeOfferState = 1 // Invalid + TradeOfferState_Active = 2 // This trade offer has been sent, neither party has acted on it yet. + TradeOfferState_Accepted = 3 // The trade offer was accepted by the recipient and items were exchanged. + TradeOfferState_Countered = 4 // The recipient made a counter offer + TradeOfferState_Expired = 5 // The trade offer was not accepted before the expiration date + TradeOfferState_Canceled = 6 // The sender cancelled the offer + TradeOfferState_Declined = 7 // The recipient declined the offer + TradeOfferState_InvalidItems = 8 // Some of the items in the offer are no longer available (indicated by the missing flag in the output) + TradeOfferState_CreatedNeedsConfirmation = 9 // The offer hasn't been sent yet and is awaiting email/mobile confirmation. The offer is only visible to the sender. + TradeOfferState_CanceledBySecondFactor = 10 // Either party canceled the offer via email/mobile. The offer is visible to both parties, even if the sender canceled it before it was sent. + TradeOfferState_InEscrow = 11 // The trade has been placed on hold. The items involved in the trade have all been removed from both parties' inventories and will be automatically delivered in the future. +) + +type TradeOfferConfirmationMethod uint + +const ( + TradeOfferConfirmationMethod_Invalid TradeOfferConfirmationMethod = 0 + TradeOfferConfirmationMethod_Email = 1 + TradeOfferConfirmationMethod_MobileApp = 2 +) + +type Asset struct { + AppId uint32 `json:",string"` + ContextId uint64 `json:",string"` + AssetId uint64 `json:",string"` + CurrencyId uint64 `json:",string"` + ClassId uint64 `json:",string"` + InstanceId uint64 `json:",string"` + Amount uint64 `json:",string"` + Missing bool +} + +type TradeOffer struct { + TradeOfferId uint64 `json:",string"` + TradeId uint64 `json:",string"` + OtherAccountId uint32 `json:"accountid_other"` + OtherSteamId steamid.SteamId `json:"-"` + Message string `json:"message"` + ExpirationTime uint32 `json:"expiraton_time"` + State TradeOfferState `json:"trade_offer_state"` + ToGive []*Asset `json:"items_to_give"` + ToReceive []*Asset `json:"items_to_receive"` + IsOurOffer bool `json:"is_our_offer"` + TimeCreated uint32 `json:"time_created"` + TimeUpdated uint32 `json:"time_updated"` + EscrowEndDate uint32 `json:"escrow_end_date"` + ConfirmationMethod TradeOfferConfirmationMethod `json:"confirmation_method"` +} + +func (t *TradeOffer) UnmarshalJSON(data []byte) error { + type Alias TradeOffer + aux := struct { + *Alias + }{ + Alias: (*Alias)(t), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + if t.OtherAccountId == 0 { + t.OtherSteamId = steamid.SteamId(0) + return nil + } + t.OtherSteamId = steamid.SteamId(uint64(t.OtherAccountId) + 76561197960265728) + return nil +} + +type TradeOffersResult struct { + Sent []*TradeOffer `json:"trade_offers_sent"` + Received []*TradeOffer `json:"trade_offers_received"` + Descriptions []*Description +} + +type TradeOfferResult struct { + Offer *TradeOffer + Descriptions []*Description +} +type Description struct { + AppId uint32 `json:"appid"` + ClassId uint64 `json:"classid,string"` + InstanceId uint64 `json:"instanceid,string"` + + IconUrl string `json:"icon_url"` + IconUrlLarge string `json:"icon_url_large"` + + Name string + MarketName string `json:"market_name"` + MarketHashName string `json:"market_hash_name"` + + // Colors in hex, for example `B2B2B2` + NameColor string `json:"name_color"` + BackgroundColor string `json:"background_color"` + + Type string + + Tradable bool `json:"tradable"` + Commodity bool `json:"commodity"` + MarketTradableRestriction uint32 `json:"market_tradable_restriction"` + + Descriptions inventory.DescriptionLines `json:"descriptions"` + Actions []*inventory.Action `json:"actions"` +} |