steampie 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- steampie/__init__.py +10 -0
- steampie/client.py +617 -0
- steampie/confirmation.py +130 -0
- steampie/exceptions.py +47 -0
- steampie/guard.py +68 -0
- steampie/login.py +238 -0
- steampie/models.py +112 -0
- steampie/utils.py +159 -0
- steampie-0.2.0.dist-info/LICENSE +22 -0
- steampie-0.2.0.dist-info/METADATA +96 -0
- steampie-0.2.0.dist-info/RECORD +13 -0
- steampie-0.2.0.dist-info/WHEEL +5 -0
- steampie-0.2.0.dist-info/top_level.txt +1 -0
steampie/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# flake8: noqa
|
|
2
|
+
__title__ = "steampie"
|
|
3
|
+
__author__ = "offish"
|
|
4
|
+
__version__ = "0.2.0"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from .client import SteamClient, SteamGuardLoginClient
|
|
8
|
+
from .exceptions import *
|
|
9
|
+
from .models import Asset, Currency, GameOptions, TradeOfferState
|
|
10
|
+
from .utils import *
|
steampie/client.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import urllib.parse as urlparse
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .confirmation import ConfirmationExecutor
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
ApiException,
|
|
11
|
+
InvalidCredentials,
|
|
12
|
+
SevenDaysHoldException,
|
|
13
|
+
SteampieException,
|
|
14
|
+
TooManyRequests,
|
|
15
|
+
)
|
|
16
|
+
from .guard import load_steam_guard
|
|
17
|
+
from .login import LoginExecutor
|
|
18
|
+
from .models import Asset, GameOptions, PredefinedOptions, SteamUrl, TradeOfferState
|
|
19
|
+
from .utils import (
|
|
20
|
+
account_id_to_steam_id,
|
|
21
|
+
get_description_key,
|
|
22
|
+
get_key_value_from_url,
|
|
23
|
+
login_required,
|
|
24
|
+
merge_items_with_descriptions_from_inventory,
|
|
25
|
+
merge_items_with_descriptions_from_offer,
|
|
26
|
+
merge_items_with_descriptions_from_offers,
|
|
27
|
+
ping_proxy,
|
|
28
|
+
steam_id_to_account_id,
|
|
29
|
+
text_between,
|
|
30
|
+
texts_between,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SteamClient:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: str,
|
|
38
|
+
username: str,
|
|
39
|
+
password: str,
|
|
40
|
+
shared_secret: str,
|
|
41
|
+
identity_secret: str,
|
|
42
|
+
login_cookies: dict | None = None,
|
|
43
|
+
proxies: dict | None = None,
|
|
44
|
+
steam_id: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._api_key = api_key
|
|
47
|
+
self._session = requests.Session()
|
|
48
|
+
|
|
49
|
+
if proxies:
|
|
50
|
+
self.set_proxies(proxies)
|
|
51
|
+
|
|
52
|
+
self.was_login_executed = False
|
|
53
|
+
self.username = username
|
|
54
|
+
self._password = password
|
|
55
|
+
self._shared_secret = shared_secret
|
|
56
|
+
self._identity_secret = identity_secret
|
|
57
|
+
self._access_token = None
|
|
58
|
+
|
|
59
|
+
# None or str at this point
|
|
60
|
+
self._steam_id = steam_id
|
|
61
|
+
|
|
62
|
+
if login_cookies:
|
|
63
|
+
self.set_login_cookies(login_cookies)
|
|
64
|
+
|
|
65
|
+
def set_proxies(self, proxies: dict) -> dict:
|
|
66
|
+
if not isinstance(proxies, dict):
|
|
67
|
+
raise TypeError(
|
|
68
|
+
"Proxy must be a dict. Example: "
|
|
69
|
+
+ "{'http': 'http://login:password@host:port', "
|
|
70
|
+
+ "'https': 'http://login:password@host:port'}",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if ping_proxy(proxies):
|
|
74
|
+
self._session.proxies.update(proxies)
|
|
75
|
+
|
|
76
|
+
return proxies
|
|
77
|
+
|
|
78
|
+
def set_login_cookies(self, cookies: dict) -> None:
|
|
79
|
+
self._session.cookies.update(cookies)
|
|
80
|
+
self.was_login_executed = True
|
|
81
|
+
|
|
82
|
+
@login_required
|
|
83
|
+
def get_steam_id(self) -> str:
|
|
84
|
+
url = SteamUrl.COMMUNITY_URL
|
|
85
|
+
response = self._session.get(url)
|
|
86
|
+
|
|
87
|
+
if steam_id := re.search(r'g_steamID = "(\d+)";', response.text):
|
|
88
|
+
return steam_id.group(1)
|
|
89
|
+
|
|
90
|
+
raise ValueError(f"Invalid steam_id: {steam_id}")
|
|
91
|
+
|
|
92
|
+
def login(self) -> None:
|
|
93
|
+
# session is alive, no need to login again
|
|
94
|
+
if self.was_login_executed and self.is_session_alive():
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
self._session.cookies.set("steamRememberLogin", "true")
|
|
98
|
+
LoginExecutor(
|
|
99
|
+
self.username,
|
|
100
|
+
self._password,
|
|
101
|
+
self._shared_secret,
|
|
102
|
+
self._session,
|
|
103
|
+
).login()
|
|
104
|
+
|
|
105
|
+
self.was_login_executed = True
|
|
106
|
+
self._access_token = self._set_access_token()
|
|
107
|
+
|
|
108
|
+
if self._steam_id is None:
|
|
109
|
+
self._steam_id = self.get_steam_id()
|
|
110
|
+
|
|
111
|
+
def _set_access_token(self) -> str:
|
|
112
|
+
steam_login_secure_cookies = [
|
|
113
|
+
cookie
|
|
114
|
+
for cookie in self._session.cookies
|
|
115
|
+
if cookie.name == "steamLoginSecure"
|
|
116
|
+
]
|
|
117
|
+
cookie_value = steam_login_secure_cookies[0].value
|
|
118
|
+
decoded_cookie_value = urlparse.unquote(cookie_value)
|
|
119
|
+
access_token_parts = decoded_cookie_value.split("||")
|
|
120
|
+
|
|
121
|
+
if len(access_token_parts) < 2:
|
|
122
|
+
print(decoded_cookie_value)
|
|
123
|
+
raise ValueError("Access token not found in steamLoginSecure cookie")
|
|
124
|
+
|
|
125
|
+
access_token = access_token_parts[1]
|
|
126
|
+
return access_token
|
|
127
|
+
|
|
128
|
+
@login_required
|
|
129
|
+
def logout(self) -> None:
|
|
130
|
+
url = f"{SteamUrl.COMMUNITY_URL}/login/logout/"
|
|
131
|
+
data = {"sessionid": self._get_session_id()}
|
|
132
|
+
self._session.post(url, data=data)
|
|
133
|
+
|
|
134
|
+
if self.is_session_alive():
|
|
135
|
+
raise SteampieException("Logout unsuccessful")
|
|
136
|
+
|
|
137
|
+
self.was_login_executed = False
|
|
138
|
+
|
|
139
|
+
def __enter__(self):
|
|
140
|
+
self.login()
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
144
|
+
del exc_type, exc_val, exc_tb
|
|
145
|
+
self.logout()
|
|
146
|
+
|
|
147
|
+
@login_required
|
|
148
|
+
def is_session_alive(self) -> bool:
|
|
149
|
+
steam_login = self.username
|
|
150
|
+
main_page_response = self._session.get(SteamUrl.COMMUNITY_URL)
|
|
151
|
+
return steam_login.lower() in main_page_response.text.lower()
|
|
152
|
+
|
|
153
|
+
def api_call(
|
|
154
|
+
self,
|
|
155
|
+
method: str,
|
|
156
|
+
interface: str,
|
|
157
|
+
api_method: str,
|
|
158
|
+
version: str,
|
|
159
|
+
params: dict | None = None,
|
|
160
|
+
) -> requests.Response:
|
|
161
|
+
url = f"{SteamUrl.API_URL}/{interface}/{api_method}/{version}"
|
|
162
|
+
response = None
|
|
163
|
+
|
|
164
|
+
if method == "GET":
|
|
165
|
+
response = self._session.get(url, params=params)
|
|
166
|
+
else:
|
|
167
|
+
response = self._session.post(url, data=params)
|
|
168
|
+
|
|
169
|
+
if self.is_invalid_api_key(response):
|
|
170
|
+
raise InvalidCredentials("Invalid API key")
|
|
171
|
+
|
|
172
|
+
return response
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def is_invalid_api_key(response: requests.Response) -> bool:
|
|
176
|
+
msg = "Access is denied. Retrying will not help. Please verify your <pre>key=</pre> parameter"
|
|
177
|
+
return msg in response.text
|
|
178
|
+
|
|
179
|
+
@login_required
|
|
180
|
+
def get_my_inventory(
|
|
181
|
+
self, game: GameOptions | str, merge: bool = True, count: int = 5000
|
|
182
|
+
) -> dict:
|
|
183
|
+
return self.get_partner_inventory(self.steam_id, game, merge, count)
|
|
184
|
+
|
|
185
|
+
@login_required
|
|
186
|
+
def get_partner_inventory(
|
|
187
|
+
self,
|
|
188
|
+
partner_steam_id: str,
|
|
189
|
+
game: GameOptions | str,
|
|
190
|
+
context_id: str = "2",
|
|
191
|
+
merge: bool = True,
|
|
192
|
+
count: int = 5000,
|
|
193
|
+
) -> dict:
|
|
194
|
+
app_id = game
|
|
195
|
+
context_id = context_id
|
|
196
|
+
|
|
197
|
+
if isinstance(game, PredefinedOptions):
|
|
198
|
+
app_id = game.app_id
|
|
199
|
+
context_id = game.context_id
|
|
200
|
+
|
|
201
|
+
if not isinstance(app_id, str):
|
|
202
|
+
raise SteampieException("Invalid app_id type")
|
|
203
|
+
|
|
204
|
+
url = f"{SteamUrl.COMMUNITY_URL}/inventory/{partner_steam_id}/{app_id}/{context_id}"
|
|
205
|
+
params = {"l": "english", "count": count}
|
|
206
|
+
|
|
207
|
+
full_response = self._session.get(url, params=params)
|
|
208
|
+
response_dict = full_response.json()
|
|
209
|
+
|
|
210
|
+
if full_response.status_code == 429:
|
|
211
|
+
raise TooManyRequests("Too many requests, try again later.")
|
|
212
|
+
|
|
213
|
+
if response_dict is None or response_dict.get("success") != 1:
|
|
214
|
+
raise ApiException("Success value should be 1.")
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
merge_items_with_descriptions_from_inventory(response_dict, game)
|
|
218
|
+
if merge
|
|
219
|
+
else response_dict
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _get_session_id(self) -> str | None:
|
|
223
|
+
response = self._session.cookies.get_dict(domain="steamcommunity.com", path="/")
|
|
224
|
+
return response.get("sessionid")
|
|
225
|
+
|
|
226
|
+
def get_trade_offers_summary(self) -> dict:
|
|
227
|
+
params = {"key": self._api_key}
|
|
228
|
+
response = self.api_call(
|
|
229
|
+
"GET", "IEconService", "GetTradeOffersSummary", "v1", params
|
|
230
|
+
)
|
|
231
|
+
return response.json()
|
|
232
|
+
|
|
233
|
+
def _set_token_params(self, params: dict, use_webtoken: bool) -> dict:
|
|
234
|
+
if use_webtoken:
|
|
235
|
+
params["access_token"] = self._access_token
|
|
236
|
+
else:
|
|
237
|
+
params["key"] = self._api_key
|
|
238
|
+
|
|
239
|
+
return params
|
|
240
|
+
|
|
241
|
+
def get_trade_offers(
|
|
242
|
+
self,
|
|
243
|
+
merge: bool = True,
|
|
244
|
+
get_sent_offers: bool = True,
|
|
245
|
+
get_received_offers: bool = True,
|
|
246
|
+
use_webtoken: bool = False,
|
|
247
|
+
max_retry: int = 5,
|
|
248
|
+
) -> dict:
|
|
249
|
+
params = {
|
|
250
|
+
"get_sent_offers": int(get_sent_offers),
|
|
251
|
+
"get_received_offers": int(get_received_offers),
|
|
252
|
+
"get_descriptions": 1,
|
|
253
|
+
"language": "english",
|
|
254
|
+
"active_only": 1,
|
|
255
|
+
"historical_only": 0,
|
|
256
|
+
"time_historical_cutoff": "",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
params = self._set_token_params(params, use_webtoken)
|
|
260
|
+
response = self._try_to_get_trade_offers(params, max_retry)
|
|
261
|
+
|
|
262
|
+
if response is None:
|
|
263
|
+
raise ApiException("Cannot get proper json from get_trade_offers method")
|
|
264
|
+
|
|
265
|
+
response_with_active_offers = self._filter_non_active_offers(response)
|
|
266
|
+
|
|
267
|
+
if not merge:
|
|
268
|
+
return response_with_active_offers
|
|
269
|
+
|
|
270
|
+
return merge_items_with_descriptions_from_offers(response_with_active_offers)
|
|
271
|
+
|
|
272
|
+
def _try_to_get_trade_offers(self, params: dict, max_retry: int) -> dict | None:
|
|
273
|
+
response = None
|
|
274
|
+
|
|
275
|
+
for _ in range(max_retry):
|
|
276
|
+
try:
|
|
277
|
+
response = self.api_call(
|
|
278
|
+
"GET", "IEconService", "GetTradeOffers", "v1", params
|
|
279
|
+
).json()
|
|
280
|
+
break
|
|
281
|
+
except json.decoder.JSONDecodeError:
|
|
282
|
+
time.sleep(2)
|
|
283
|
+
|
|
284
|
+
return response
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _filter_non_active_offers(offers_response):
|
|
288
|
+
offers_received = offers_response["response"].get("trade_offers_received", [])
|
|
289
|
+
offers_sent = offers_response["response"].get("trade_offers_sent", [])
|
|
290
|
+
|
|
291
|
+
offers_response["response"]["trade_offers_received"] = list(
|
|
292
|
+
filter(
|
|
293
|
+
lambda offer: offer["trade_offer_state"] == TradeOfferState.Active,
|
|
294
|
+
offers_received,
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
offers_response["response"]["trade_offers_sent"] = list(
|
|
298
|
+
filter(
|
|
299
|
+
lambda offer: offer["trade_offer_state"] == TradeOfferState.Active,
|
|
300
|
+
offers_sent,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return offers_response
|
|
305
|
+
|
|
306
|
+
def get_trade_offer(
|
|
307
|
+
self, trade_offer_id: str, merge: bool = True, use_webtoken: bool = False
|
|
308
|
+
) -> dict:
|
|
309
|
+
params = {"tradeofferid": trade_offer_id, "language": "english"}
|
|
310
|
+
params = self._set_token_params(params, use_webtoken)
|
|
311
|
+
|
|
312
|
+
response = self.api_call(
|
|
313
|
+
"GET", "IEconService", "GetTradeOffer", "v1", params
|
|
314
|
+
).json()
|
|
315
|
+
|
|
316
|
+
if merge and "descriptions" in response["response"]:
|
|
317
|
+
descriptions = {
|
|
318
|
+
get_description_key(offer): offer
|
|
319
|
+
for offer in response["response"]["descriptions"]
|
|
320
|
+
}
|
|
321
|
+
offer = response["response"]["offer"]
|
|
322
|
+
response["response"]["offer"] = merge_items_with_descriptions_from_offer(
|
|
323
|
+
offer, descriptions
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return response
|
|
327
|
+
|
|
328
|
+
def get_trade_history(
|
|
329
|
+
self,
|
|
330
|
+
max_trades: int = 100,
|
|
331
|
+
start_after_time=None,
|
|
332
|
+
start_after_tradeid=None,
|
|
333
|
+
get_descriptions: bool = True,
|
|
334
|
+
navigating_back: bool = True,
|
|
335
|
+
include_failed: bool = True,
|
|
336
|
+
include_total: bool = True,
|
|
337
|
+
) -> dict:
|
|
338
|
+
params = {
|
|
339
|
+
"key": self._api_key,
|
|
340
|
+
"max_trades": max_trades,
|
|
341
|
+
"start_after_time": start_after_time,
|
|
342
|
+
"start_after_tradeid": start_after_tradeid,
|
|
343
|
+
"get_descriptions": get_descriptions,
|
|
344
|
+
"navigating_back": navigating_back,
|
|
345
|
+
"include_failed": include_failed,
|
|
346
|
+
"include_total": include_total,
|
|
347
|
+
}
|
|
348
|
+
response = self.api_call("GET", "IEconService", "GetTradeHistory", "v1", params)
|
|
349
|
+
return response.json()
|
|
350
|
+
|
|
351
|
+
@login_required
|
|
352
|
+
def get_trade_receipt(self, trade_id: str):
|
|
353
|
+
html = self._session.get(
|
|
354
|
+
f"https://steamcommunity.com/trade/{trade_id}/receipt"
|
|
355
|
+
).content.decode()
|
|
356
|
+
return [
|
|
357
|
+
json.loads(item) for item in texts_between(html, "oItem = ", ";\r\n\toItem")
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
@login_required
|
|
361
|
+
def accept_trade_offer(self, trade_offer_id: str) -> dict:
|
|
362
|
+
trade = self.get_trade_offer(trade_offer_id, use_webtoken=True)
|
|
363
|
+
trade_offer_state = TradeOfferState(
|
|
364
|
+
trade["response"]["offer"]["trade_offer_state"]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if trade_offer_state is not TradeOfferState.Active:
|
|
368
|
+
raise ApiException(
|
|
369
|
+
f"Invalid trade offer state: {trade_offer_state.name} ({trade_offer_state.value})"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
partner = self._fetch_trade_partner_id(trade_offer_id)
|
|
373
|
+
session_id = self._get_session_id()
|
|
374
|
+
accept_url = f"{SteamUrl.COMMUNITY_URL}/tradeoffer/{trade_offer_id}/accept"
|
|
375
|
+
params = {
|
|
376
|
+
"sessionid": session_id,
|
|
377
|
+
"tradeofferid": trade_offer_id,
|
|
378
|
+
"serverid": "1",
|
|
379
|
+
"partner": partner,
|
|
380
|
+
"captcha": "",
|
|
381
|
+
}
|
|
382
|
+
headers = {"Referer": self._get_trade_offer_url(trade_offer_id)}
|
|
383
|
+
|
|
384
|
+
response = self._session.post(accept_url, data=params, headers=headers).json()
|
|
385
|
+
|
|
386
|
+
if response.get("needs_mobile_confirmation", False):
|
|
387
|
+
return self._confirm_transaction(trade_offer_id)
|
|
388
|
+
|
|
389
|
+
return response
|
|
390
|
+
|
|
391
|
+
def _fetch_trade_partner_id(self, trade_offer_id: str) -> str:
|
|
392
|
+
url = self._get_trade_offer_url(trade_offer_id)
|
|
393
|
+
offer_response_text = self._session.get(url).text
|
|
394
|
+
|
|
395
|
+
if (
|
|
396
|
+
"You have logged in from a new device. In order to protect the items"
|
|
397
|
+
in offer_response_text
|
|
398
|
+
):
|
|
399
|
+
raise SevenDaysHoldException(
|
|
400
|
+
"Account has logged in a new device and can't trade for 7 days"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return text_between(
|
|
404
|
+
offer_response_text, "var g_ulTradePartnerSteamID = '", "';"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _confirm_transaction(self, trade_offer_id: str) -> dict:
|
|
408
|
+
confirmation_executor = ConfirmationExecutor(
|
|
409
|
+
self._identity_secret,
|
|
410
|
+
self.steam_id,
|
|
411
|
+
self._session,
|
|
412
|
+
)
|
|
413
|
+
return confirmation_executor.send_trade_allow_request(trade_offer_id)
|
|
414
|
+
|
|
415
|
+
def decline_trade_offer(self, trade_offer_id: str) -> dict:
|
|
416
|
+
url = f"https://steamcommunity.com/tradeoffer/{trade_offer_id}/decline"
|
|
417
|
+
response = self._session.post(url, data={"sessionid": self._get_session_id()})
|
|
418
|
+
return response.json()
|
|
419
|
+
|
|
420
|
+
def cancel_trade_offer(self, trade_offer_id: str) -> dict:
|
|
421
|
+
url = f"https://steamcommunity.com/tradeoffer/{trade_offer_id}/cancel"
|
|
422
|
+
response = self._session.post(url, data={"sessionid": self._get_session_id()})
|
|
423
|
+
return response.json()
|
|
424
|
+
|
|
425
|
+
@login_required
|
|
426
|
+
def make_offer(
|
|
427
|
+
self,
|
|
428
|
+
items_from_me: list[Asset],
|
|
429
|
+
items_from_them: list[Asset],
|
|
430
|
+
partner_steam_id: str,
|
|
431
|
+
message: str = "",
|
|
432
|
+
) -> dict:
|
|
433
|
+
offer = self._create_offer_dict(items_from_me, items_from_them)
|
|
434
|
+
session_id = self._get_session_id()
|
|
435
|
+
url = f"{SteamUrl.COMMUNITY_URL}/tradeoffer/new/send"
|
|
436
|
+
server_id = 1
|
|
437
|
+
params = {
|
|
438
|
+
"sessionid": session_id,
|
|
439
|
+
"serverid": server_id,
|
|
440
|
+
"partner": partner_steam_id,
|
|
441
|
+
"tradeoffermessage": message,
|
|
442
|
+
"json_tradeoffer": json.dumps(offer),
|
|
443
|
+
"captcha": "",
|
|
444
|
+
"trade_offer_create_params": "{}",
|
|
445
|
+
}
|
|
446
|
+
partner_account_id = steam_id_to_account_id(partner_steam_id)
|
|
447
|
+
headers = {
|
|
448
|
+
"Referer": f"{SteamUrl.COMMUNITY_URL}/tradeoffer/new/?partner={partner_account_id}",
|
|
449
|
+
"Origin": SteamUrl.COMMUNITY_URL,
|
|
450
|
+
}
|
|
451
|
+
response = self._session.post(url, data=params, headers=headers).json()
|
|
452
|
+
|
|
453
|
+
if response.get("needs_mobile_confirmation"):
|
|
454
|
+
response.update(self._confirm_transaction(response["tradeofferid"]))
|
|
455
|
+
|
|
456
|
+
return response
|
|
457
|
+
|
|
458
|
+
def get_profile(self, steam_id: str) -> dict:
|
|
459
|
+
params = {"steamids": steam_id, "key": self._api_key}
|
|
460
|
+
response = self.api_call(
|
|
461
|
+
"GET", "ISteamUser", "GetPlayerSummaries", "v0002", params
|
|
462
|
+
)
|
|
463
|
+
data = response.json()
|
|
464
|
+
return data["response"]["players"][0]
|
|
465
|
+
|
|
466
|
+
def get_friend_list(self, steam_id: str, relationship_filter: str = "all") -> dict:
|
|
467
|
+
params = {
|
|
468
|
+
"key": self._api_key,
|
|
469
|
+
"steamid": steam_id,
|
|
470
|
+
"relationship": relationship_filter,
|
|
471
|
+
}
|
|
472
|
+
resp = self.api_call("GET", "ISteamUser", "GetFriendList", "v1", params)
|
|
473
|
+
data = resp.json()
|
|
474
|
+
return data["friendslist"]["friends"]
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def _create_offer_dict(
|
|
478
|
+
items_from_me: list[Asset], items_from_them: list[Asset]
|
|
479
|
+
) -> dict:
|
|
480
|
+
return {
|
|
481
|
+
"newversion": True,
|
|
482
|
+
"version": 4,
|
|
483
|
+
"me": {
|
|
484
|
+
"assets": [asset.to_dict() for asset in items_from_me],
|
|
485
|
+
"currency": [],
|
|
486
|
+
"ready": False,
|
|
487
|
+
},
|
|
488
|
+
"them": {
|
|
489
|
+
"assets": [asset.to_dict() for asset in items_from_them],
|
|
490
|
+
"currency": [],
|
|
491
|
+
"ready": False,
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
@login_required
|
|
496
|
+
def get_escrow_duration(self, trade_offer_url: str) -> int:
|
|
497
|
+
headers = {
|
|
498
|
+
"Referer": f"{SteamUrl.COMMUNITY_URL}{urlparse.urlparse(trade_offer_url).path}",
|
|
499
|
+
"Origin": SteamUrl.COMMUNITY_URL,
|
|
500
|
+
}
|
|
501
|
+
response = self._session.get(trade_offer_url, headers=headers).text
|
|
502
|
+
|
|
503
|
+
my_escrow_duration = int(text_between(response, "var g_daysMyEscrow = ", ";"))
|
|
504
|
+
their_escrow_duration = int(
|
|
505
|
+
text_between(response, "var g_daysTheirEscrow = ", ";")
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return max(my_escrow_duration, their_escrow_duration)
|
|
509
|
+
|
|
510
|
+
@login_required
|
|
511
|
+
def make_offer_with_url(
|
|
512
|
+
self,
|
|
513
|
+
items_from_me: list[Asset],
|
|
514
|
+
items_from_them: list[Asset],
|
|
515
|
+
trade_offer_url: str,
|
|
516
|
+
message: str = "",
|
|
517
|
+
case_sensitive: bool = True,
|
|
518
|
+
confirm_trade: bool = True,
|
|
519
|
+
) -> dict:
|
|
520
|
+
token = get_key_value_from_url(trade_offer_url, "token", case_sensitive)
|
|
521
|
+
partner_account_id = get_key_value_from_url(
|
|
522
|
+
trade_offer_url, "partner", case_sensitive
|
|
523
|
+
)
|
|
524
|
+
partner_steam_id = account_id_to_steam_id(partner_account_id)
|
|
525
|
+
offer = self._create_offer_dict(items_from_me, items_from_them)
|
|
526
|
+
session_id = self._get_session_id()
|
|
527
|
+
url = f"{SteamUrl.COMMUNITY_URL}/tradeoffer/new/send"
|
|
528
|
+
server_id = 1
|
|
529
|
+
trade_offer_create_params = {"trade_offer_access_token": token}
|
|
530
|
+
params = {
|
|
531
|
+
"sessionid": session_id,
|
|
532
|
+
"serverid": server_id,
|
|
533
|
+
"partner": partner_steam_id,
|
|
534
|
+
"tradeoffermessage": message,
|
|
535
|
+
"json_tradeoffer": json.dumps(offer),
|
|
536
|
+
"captcha": "",
|
|
537
|
+
"trade_offer_create_params": json.dumps(trade_offer_create_params),
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
headers = {
|
|
541
|
+
"Referer": f"{SteamUrl.COMMUNITY_URL}{urlparse.urlparse(trade_offer_url).path}",
|
|
542
|
+
"Origin": SteamUrl.COMMUNITY_URL,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
response = self._session.post(url, data=params, headers=headers).json()
|
|
546
|
+
|
|
547
|
+
if confirm_trade and response.get("needs_mobile_confirmation"):
|
|
548
|
+
response.update(self._confirm_transaction(response["tradeofferid"]))
|
|
549
|
+
|
|
550
|
+
return response
|
|
551
|
+
|
|
552
|
+
def _friend_ajax_request(self, method: str, steam_id: str, accept: int = 0) -> dict:
|
|
553
|
+
assert method.lower() in ["add", "remove"]
|
|
554
|
+
|
|
555
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
|
|
556
|
+
url = f"{SteamUrl.COMMUNITY_URL}/actions/AddFriendAjax"
|
|
557
|
+
data = {
|
|
558
|
+
"sessionID": self._get_session_id(),
|
|
559
|
+
"steamid": steam_id,
|
|
560
|
+
"accept_invite": accept,
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if method == "remove":
|
|
564
|
+
url = f"{SteamUrl.COMMUNITY_URL}/actions/RemoveFriendAjax"
|
|
565
|
+
del data["accept_invite"]
|
|
566
|
+
|
|
567
|
+
response = self._session.post(url, data=data, headers=headers)
|
|
568
|
+
|
|
569
|
+
if response.status_code != 200:
|
|
570
|
+
raise ApiException(
|
|
571
|
+
f"Request failed with status code {response.status_code}"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
return response.json()
|
|
575
|
+
|
|
576
|
+
@login_required
|
|
577
|
+
def remove_friend(self, steam_id: str) -> dict:
|
|
578
|
+
return self._friend_ajax_request("remove", steam_id)
|
|
579
|
+
|
|
580
|
+
@login_required
|
|
581
|
+
def add_friend(self, steam_id: str) -> dict:
|
|
582
|
+
return self._friend_ajax_request("add", steam_id)
|
|
583
|
+
|
|
584
|
+
@login_required
|
|
585
|
+
def accept_friend(self, steam_id: str) -> dict:
|
|
586
|
+
return self._friend_ajax_request("add", steam_id, accept=1)
|
|
587
|
+
|
|
588
|
+
@staticmethod
|
|
589
|
+
def _get_trade_offer_url(trade_offer_id: str) -> str:
|
|
590
|
+
return f"{SteamUrl.COMMUNITY_URL}/tradeoffer/{trade_offer_id}"
|
|
591
|
+
|
|
592
|
+
@property
|
|
593
|
+
def steam_id(self) -> str:
|
|
594
|
+
return self._steam_id
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class SteamGuardLoginClient(SteamClient):
|
|
598
|
+
def __init__(
|
|
599
|
+
self,
|
|
600
|
+
api_key: str,
|
|
601
|
+
password: str,
|
|
602
|
+
steam_guard_file: str,
|
|
603
|
+
) -> None:
|
|
604
|
+
steam_guard = load_steam_guard(steam_guard_file)
|
|
605
|
+
username = steam_guard["account_name"]
|
|
606
|
+
shared_secret = steam_guard["shared_secret"]
|
|
607
|
+
identity_secret = steam_guard["identity_secret"]
|
|
608
|
+
steam_id = str(steam_guard["Session"]["SteamID"])
|
|
609
|
+
|
|
610
|
+
super().__init__(
|
|
611
|
+
api_key=api_key,
|
|
612
|
+
username=username,
|
|
613
|
+
password=password,
|
|
614
|
+
shared_secret=shared_secret,
|
|
615
|
+
identity_secret=identity_secret,
|
|
616
|
+
steam_id=steam_id,
|
|
617
|
+
)
|