steamutils 1.5.43 → 1.5.44

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.
package/_steamproto.js CHANGED
@@ -1,56 +1,56 @@
1
- import path from "path";
2
- import { fileURLToPath } from "url";
3
- import Protobuf from "protobufjs";
4
- import gpf from "google-proto-files";
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
-
9
- export class SteamProto {
10
- constructor(proto) {
11
- this._proto = proto;
12
- }
13
-
14
- toProto() {
15
- const localProto = path.join(`${__dirname}/protos/`, this._proto.filename);
16
-
17
- const descriptorPath = gpf.getProtoPath("protobuf/descriptor.proto");
18
-
19
- const root = new Protobuf.Root().loadSync([descriptorPath, localProto], {
20
- keepCase: true,
21
- });
22
- return root.lookupType(this._proto.name);
23
- }
24
-
25
- protoEncode(obj) {
26
- const protobuf = this.toProto();
27
- return protobuf.encode(protobuf.create(obj)).finish();
28
- }
29
-
30
- protoEncodeBase64(obj) {
31
- return this.protoEncode(obj).toString("base64");
32
- }
33
-
34
- protoDecode(obj) {
35
- if (isBase64(obj)) {
36
- obj = Buffer.from(obj, "base64");
37
- }
38
-
39
- const protobuf = this.toProto();
40
- try {
41
- return protobuf.toObject(protobuf.decode(obj), { defaults: true });
42
- } catch (e) {
43
- console.error(`[${this._proto.name}] protoDecode 1`, typeof obj, obj);
44
- console.error(`[${this._proto.name}] protoDecode 2`, e);
45
- return null;
46
- }
47
- }
48
- }
49
-
50
- function isBase64(str) {
51
- if (typeof str !== "string") return false;
52
- // remove whitespace and check
53
- str = str.trim();
54
- // base64 should only contain A-Z, a-z, 0-9, +, /, and possibly = at the end
55
- return /^[A-Za-z0-9+/]+={0,2}$/.test(str) && str.length % 4 === 0;
56
- }
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import Protobuf from "protobufjs";
4
+ import gpf from "google-proto-files";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export class SteamProto {
10
+ constructor(proto) {
11
+ this._proto = proto;
12
+ }
13
+
14
+ toProto() {
15
+ const localProto = path.join(`${__dirname}/protos/`, this._proto.filename);
16
+
17
+ const descriptorPath = gpf.getProtoPath("protobuf/descriptor.proto");
18
+
19
+ const root = new Protobuf.Root().loadSync([descriptorPath, localProto], {
20
+ keepCase: true,
21
+ });
22
+ return root.lookupType(this._proto.name);
23
+ }
24
+
25
+ protoEncode(obj) {
26
+ const protobuf = this.toProto();
27
+ return protobuf.encode(protobuf.create(obj)).finish();
28
+ }
29
+
30
+ protoEncodeBase64(obj) {
31
+ return this.protoEncode(obj).toString("base64");
32
+ }
33
+
34
+ protoDecode(obj) {
35
+ if (isBase64(obj)) {
36
+ obj = Buffer.from(obj, "base64");
37
+ }
38
+
39
+ const protobuf = this.toProto();
40
+ try {
41
+ return protobuf.toObject(protobuf.decode(obj), { defaults: true });
42
+ } catch (e) {
43
+ console.error(`[${this._proto.name}] protoDecode 1`, typeof obj, obj);
44
+ console.error(`[${this._proto.name}] protoDecode 2`, e);
45
+ return null;
46
+ }
47
+ }
48
+ }
49
+
50
+ function isBase64(str) {
51
+ if (typeof str !== "string") return false;
52
+ // remove whitespace and check
53
+ str = str.trim();
54
+ // base64 should only contain A-Z, a-z, 0-9, +, /, and possibly = at the end
55
+ return /^[A-Za-z0-9+/]+={0,2}$/.test(str) && str.length % 4 === 0;
56
+ }
@@ -1,7 +1,6 @@
1
1
  import path from "path";
2
2
  import { fileURLToPath } from "url";
3
3
  import Protobuf from "protobufjs";
4
- import gpf from "google-proto-files";
5
4
 
6
5
  const __filename = fileURLToPath(import.meta.url);
7
6
  const __dirname = path.dirname(__filename);
@@ -12,14 +11,10 @@ export class SteamProto {
12
11
  }
13
12
 
14
13
  toProto() {
15
- const localProto = path.join(`${__dirname}/protos/`, this._proto.filename);
16
-
17
- const descriptorPath = gpf.getProtoPath("protobuf/descriptor.proto");
18
-
19
- const root = new Protobuf.Root().loadSync([descriptorPath, localProto], {
14
+ const root = new Protobuf.Root().loadSync(path.join(`${__dirname}/protos/`, this._proto.filename), {
20
15
  keepCase: true,
21
16
  });
22
- return root.lookupType(this._proto.name);
17
+ return root[this._proto.name];
23
18
  }
24
19
 
25
20
  protoEncode(obj) {
package/index.js CHANGED
@@ -21,6 +21,7 @@ import { AppID_CSGO, E1GameBanOnRecord, E1VACBanOnRecord, EActivityType, ECommen
21
21
  import SteamTotp from "steam-totp";
22
22
  import { SteamProto, SteamProtoType } from "./steamproto.js";
23
23
  import EventEmitter from "node:events";
24
+ import { extractAssetItemsFromHovers, parseMarketHistoryListings, parseMarketListings } from "./parse_html.js";
24
25
 
25
26
  const eventEmitter = (globalThis.steamUserEventEmitter = new EventEmitter());
26
27
 
@@ -6299,43 +6300,77 @@ export default class SteamUser {
6299
6300
  return null;
6300
6301
  }
6301
6302
 
6302
- const $ = cheerio.load(data.results_html);
6303
- const list = [];
6304
- $(".market_listing_row").each(function () {
6305
- try {
6306
- const $row = $(this);
6307
- const [sElementPrefix, listingid, appid, contextid, itemid] = $row
6308
- .find(".market_listing_cancel_button > a")
6309
- .attr("href")
6310
- .split("(")[1]
6311
- .split(")")[0]
6312
- .split(",")
6313
- .map((r) => r.trim().replaceAll(`'`, "").replaceAll(`"`, ""));
6314
- const image = $row.find(`#mylisting_${listingid}_image`).attr("src");
6315
- const buyer_pays_price = StringUtils.cleanSpace($row.find(`.market_listing_price span[title="This is the price the buyer pays."]`).text().replaceAll(`(`, "").replaceAll(`)`, ""));
6316
- const receive_price = StringUtils.cleanSpace($row.find(`.market_listing_price span[title="This is how much you will receive."]`).text().replaceAll(`(`, "").replaceAll(`)`, ""));
6317
- const item_name = $row.find(".market_listing_item_name_link").text() || $row.find(".market_listing_item_name").text();
6318
- const game_name = $row.find(".market_listing_game_name").text();
6319
- const date_combined = formatMarketHistoryDate(StringUtils.cleanSpace($row.find(".market_listing_game_name + .market_listing_listed_date_combined").text()));
6320
- list.push({
6321
- listingid,
6322
- appid,
6323
- contextid,
6324
- itemid,
6325
- buyer_pays_price,
6326
- receive_price,
6327
- item_name,
6328
- game_name,
6329
- date_combined,
6330
- image,
6331
- });
6332
- } catch (e) {}
6333
- });
6303
+ const list = parseMarketListings(data.results_html);
6334
6304
  const assets = Object.values(data.assets["730"]?.["2"] || {});
6335
6305
  return { list, assets, success: true };
6336
6306
  }
6337
6307
 
6338
- async getMyMarketHistory({ start = 0, count = 100 } = {}) {
6308
+ /**
6309
+ * @typedef {Object} MarketHistoryResponse
6310
+ * @property {boolean} success Whether the request was successful.
6311
+ * @property {number} pagesize Number of items per page.
6312
+ * @property {number} total_count Total count of market entries available.
6313
+ * @property {number} start Starting index of the results.
6314
+ * @property {Array<Asset>} assets Related inventory assets for the transactions.
6315
+ * @property {string} hovers Raw JavaScript code for web asset hovers.
6316
+ * @property {string} results_html Raw HTML snippet of market history.
6317
+ * @property {Array<MarketHistoryListing>} list Simplified, parsed list of market actions (buys/sells).
6318
+ * // Optionally, you may add below for pre-parsed hover info
6319
+ * // @property {Array<HoverItem>} [hoverItems] Parsed item hover info (if extracted from hovers).
6320
+ */
6321
+
6322
+ /**
6323
+ * @typedef {Object} Asset
6324
+ * @property {number} currency
6325
+ * @property {number} appid
6326
+ * @property {string} contextid
6327
+ * @property {string} id
6328
+ * @property {string} classid
6329
+ * @property {string} instanceid
6330
+ * @property {string} amount
6331
+ * @property {number} status
6332
+ * @property {string} original_amount
6333
+ * @property {string} unowned_id
6334
+ * @property {string} unowned_contextid
6335
+ * @property {string} background_color
6336
+ * @property {string} icon_url
6337
+ * @property {Array<AssetDescription>} descriptions
6338
+ * @property {number} tradable
6339
+ * @property {Array<AssetAction>} [actions]
6340
+ * @property {string} name
6341
+ * @property {string} name_color
6342
+ * @property {string} type
6343
+ * @property {string} market_name
6344
+ * @property {string} market_hash_name
6345
+ * @property {Array<AssetAction>} [market_actions]
6346
+ * @property {number} commodity
6347
+ * @property {number} market_tradable_restriction
6348
+ * @property {number} market_marketable_restriction
6349
+ * @property {number} marketable
6350
+ * @property {string} app_icon
6351
+ * @property {number} owner
6352
+ */
6353
+
6354
+ /**
6355
+ * @typedef {Object} AssetDescription
6356
+ * @property {string} type Description style or block type (e.g. "html").
6357
+ * @property {string} value The description's content.
6358
+ * @property {string} name Field/tag name.
6359
+ * @property {string} [color] (Optional) Hex color string for this description entry.
6360
+ */
6361
+
6362
+ /**
6363
+ * @typedef {Object} AssetAction
6364
+ * @property {string} link URL or steam protocol link.
6365
+ * @property {string} name Action button text/description.
6366
+ */
6367
+
6368
+ /**
6369
+ * Retrieves the user's Steam Market transaction history, including assets, listing info, and hover selectors.
6370
+ *
6371
+ * @returns {MarketHistoryResponse} The market history data structure.
6372
+ */
6373
+ async getMarketHistory({ start = 0, count = 100 } = {}) {
6339
6374
  const result = await this._httpRequestAjax({
6340
6375
  url: `market/myhistory/render/?query=&start=${start}&count=${count}`,
6341
6376
  });
@@ -6349,56 +6384,10 @@ export default class SteamUser {
6349
6384
  return null;
6350
6385
  }
6351
6386
 
6352
- const hovers = StringUtils.cleanSpace(data.hovers);
6387
+ const hovers = extractAssetItemsFromHovers(data.hovers);
6353
6388
  const assetById = data.assets?.[730]?.[2] || {};
6354
- const assestByListingId = {};
6355
- hovers.split("CreateItemHoverFromContainer").forEach(function (text) {
6356
- text = text.trim();
6357
- if (!text.startsWith("(")) {
6358
- return;
6359
- }
6360
- const texts = text.split(",");
6361
- const listingId = texts[1]?.substringBetweenOrNull("history_row_", "_");
6362
- const assestId = texts[4]?.trim()?.removeSurrounding("'");
6363
- if (!listingId || !assestId || !assetById[assestId]) {
6364
- return;
6365
- }
6366
- assestByListingId[listingId] = assestId;
6367
- });
6368
-
6369
- const $ = cheerio.load(data.results_html);
6370
- const list = [...$(".market_listing_row")]
6371
- .map(function (el) {
6372
- el = $(el);
6373
- const id = el.attr("id");
6374
- const listingid = id.substringBetweenOrNull("history_row_", "_");
6375
- if (!listingid) {
6376
- return;
6377
- }
6378
- const gainOrLoss = StringUtils.cleanSpace(el.find(".market_listing_gainorloss").text());
6379
- const image = el.find(`.market_listing_item_img`).attr("src");
6380
- const price = parseInt(el.find(`.market_table_value .market_listing_price`).text().replaceAll(`(`, "").replaceAll(`)`, "").replaceAll(`₫`, "").replaceAll(`.`, "").replaceAll(`,`, "").trim()) || "";
6381
- const item_name = el.find(".market_listing_item_name").text();
6382
- const game_name = el.find(".market_listing_game_name").text();
6383
- const listedOn = formatMarketHistoryDate(StringUtils.cleanSpace(el.find(".market_listing_listed_date + .market_listing_listed_date").text().replaceAll(`Listed:`, "")));
6384
- const actedOn = formatMarketHistoryDate(StringUtils.cleanSpace(el.find(".market_listing_whoactedwith + .market_listing_listed_date").text().replaceAll(`Listed:`, "")));
6385
- const status = StringUtils.cleanSpace(el.find(".market_listing_whoactedwith").text());
6386
-
6387
- return {
6388
- id,
6389
- listingid,
6390
- price,
6391
- item_name,
6392
- game_name,
6393
- listedOn,
6394
- actedOn,
6395
- image,
6396
- gainOrLoss,
6397
- status,
6398
- ...(!!assestByListingId[listingid] && { assetId: assestByListingId[listingid] }),
6399
- };
6400
- })
6401
- .filter(Boolean);
6389
+ const assestByListingId = hovers.reduce((map, item) => (item.listingId && item.assetid && (map[item.listingId] = item.assetid), map), {});
6390
+ const list = parseMarketHistoryListings(data.results_html, assestByListingId);
6402
6391
  const assets = Object.values(assetById);
6403
6392
  return { ...data, list, assets, success: true };
6404
6393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steamutils",
3
- "version": "1.5.43",
3
+ "version": "1.5.44",
4
4
  "main": "index.js",
5
5
  "dependencies": {
6
6
  "alpha-common-utils": "^1.0.6",
package/parse_html.js ADDED
@@ -0,0 +1,189 @@
1
+ import { StringUtils } from "alpha-common-utils/index.js";
2
+ import { formatMarketHistoryDate } from "./utils.js";
3
+ import * as cheerio from "cheerio";
4
+
5
+ /**
6
+ * @typedef {Object} HoverItem
7
+ * @property {string|null} listingId The listing ID extracted from the selector, if available (as string).
8
+ * @property {number} appid The app (game) ID for this item.
9
+ * @property {number} contextid The context ID for this item.
10
+ * @property {number} assetid The unique asset ID.
11
+ * @property {string} unknown A string field captured from the arguments (usually "0").
12
+ * @property {string} [name_selector] The HTML selector for the item's name, if present.
13
+ * @property {string} [image_selector] The HTML selector for the item's image, if present.
14
+ * @property {string} [other_selector] The selector string if it does not match name/image pattern.
15
+ */
16
+
17
+ /**
18
+ * Extracts item information from Steam inventory trade history or similar HTML fragments.
19
+ * Scans the given HTML string for calls to `CreateItemHoverFromContainer` and extracts
20
+ * key data for each referenced item. Returns each item's info in a structured array.
21
+ *
22
+ * @param {string} html - The HTML string to parse.
23
+ * @returns {HoverItem[]} Array of hover item objects.
24
+ */
25
+ export function extractAssetItemsFromHovers(html) {
26
+ // Use your preferred HTML/space scrubber.
27
+ html = StringUtils.cleanSpace(html);
28
+
29
+ // RegExp for CreateItemHoverFromContainer arguments
30
+ const re = /CreateItemHoverFromContainer\s*\(\s*g_rgAssets\s*,\s*'([^']+)'\s*,\s*(\d+)\s*,\s*'(\d+)'\s*,\s*'(\d+)'\s*,\s*(\d+)\s*\)/g;
31
+
32
+ /** @type {Record<number, HoverItem>} */
33
+ const itemsMap = {};
34
+ let match;
35
+
36
+ while ((match = re.exec(html)) !== null) {
37
+ const [_, selector, appid, contextid, assetid, unknown] = match;
38
+
39
+ // Extract listingId as a string if present: "history_row_<listingId>_"
40
+ let listingId = null;
41
+ const lidMatch = selector.match(/history_row_(\d+)_/);
42
+ if (lidMatch) listingId = lidMatch[1]; // assign as string
43
+
44
+ const appidNum = Number(appid);
45
+ const contextidNum = Number(contextid);
46
+ const assetidNum = Number(assetid);
47
+
48
+ if (!itemsMap[assetidNum]) {
49
+ itemsMap[assetidNum] = {
50
+ listingId,
51
+ appid: appidNum,
52
+ contextid: contextidNum,
53
+ assetid: assetidNum,
54
+ unknown,
55
+ };
56
+ }
57
+
58
+ if (selector.endsWith("_name")) {
59
+ itemsMap[assetidNum].name_selector = selector;
60
+ } else if (selector.endsWith("_image")) {
61
+ itemsMap[assetidNum].image_selector = selector;
62
+ } else {
63
+ itemsMap[assetidNum].other_selector = selector;
64
+ }
65
+ }
66
+ return Object.values(itemsMap);
67
+ }
68
+
69
+ /**
70
+ * @typedef {Object} MarketHistoryListing
71
+ * @property {string} id - HTML row id attribute (e.g. "history_row_4316182845737233773_4316182845737233774").
72
+ * @property {string} listingId - Listing id (e.g. "4316182845737233773").
73
+ * @property {number} price - Price in integer form (e.g. 80270).
74
+ * @property {string} itemName - The market item's name (e.g. "UMP-45 | Mudder").
75
+ * @property {string} gameName - The game's name (e.g. "Counter-Strike 2").
76
+ * @property {string} listedOn - Listing date as string (e.g. "27 May 2024").
77
+ * @property {string} actedOn - Acted on date or empty string.
78
+ * @property {string} image - Full image URL for the item.
79
+ * @property {string} gainOrLoss - "+" for buys, "-" for sells.
80
+ * @property {string} status - Status text, usually empty string.
81
+ * @property {string} [assetId] - (optional) Associated asset id, if available.
82
+ */
83
+
84
+ /**
85
+ * Parses Steam market history listing rows into structured data objects.
86
+ *
87
+ * @param {string} html - The raw HTML string to parse.
88
+ * @param {Object} [assetByListingId={}] - Optional mapping of listingId to assetId.
89
+ * @returns {MarketHistoryListing[]} Array of market listing objects.
90
+ */
91
+ export function parseMarketHistoryListings(html, assetByListingId = {}) {
92
+ const $ = cheerio.load(html);
93
+ return $(".market_listing_row")
94
+ .toArray()
95
+ .map((el) => {
96
+ const $el = $(el);
97
+ const id = $el.attr("id");
98
+ const listingId = id?.match(/history_row_(.*?)_/)?.[1];
99
+ if (!listingId) return null;
100
+ const [gainOrLoss, image, priceText, itemName, gameName, listedOnText, actedOnText, status] = [$el.find(".market_listing_gainorloss").text().trim(), $el.find(".market_listing_item_img").attr("src"), $el.find(".market_table_value .market_listing_price").text(), $el.find(".market_listing_item_name").text().trim(), $el.find(".market_listing_game_name").text().trim(), $el.find(".market_listing_listed_date + .market_listing_listed_date").text(), $el.find(".market_listing_whoactedwith + .market_listing_listed_date").text(), $el.find(".market_listing_whoactedwith").text().trim()];
101
+ const price = parseInt(priceText.replace(/[₫().,]/g, "").trim(), 10) || "";
102
+ const listedOn = typeof formatMarketHistoryDate === "function" ? formatMarketHistoryDate(listedOnText.replace(/Listed:/, "").trim()) : listedOnText.replace(/Listed:/, "").trim();
103
+ const actedOn = typeof formatMarketHistoryDate === "function" ? formatMarketHistoryDate(actedOnText.replace(/Listed:/, "").trim()) : actedOnText.replace(/Listed:/, "").trim();
104
+
105
+ const result = {
106
+ id,
107
+ listingId,
108
+ price,
109
+ itemName,
110
+ gameName,
111
+ listedOn,
112
+ actedOn,
113
+ image,
114
+ gainOrLoss,
115
+ status,
116
+ };
117
+ if (assetByListingId?.[listingId]) result.assetId = assetByListingId[listingId];
118
+ return result;
119
+ })
120
+ .filter(Boolean);
121
+ }
122
+
123
+ /**
124
+ * Parses Steam Market HTML listings to an array of items.
125
+ *
126
+ * @param {string} html - HTML string containing listing rows.
127
+ * @returns {Array<{
128
+ * listingId: string,
129
+ * appId: number,
130
+ * contextId: number,
131
+ * itemId: number,
132
+ * imageUrl: string|null,
133
+ * buyerPaysPrice: string,
134
+ * receivePrice: string,
135
+ * itemName: string,
136
+ * gameName: string,
137
+ * listedDate: string
138
+ * }>}
139
+ */
140
+ export function parseMarketListings(html) {
141
+ const $ = cheerio.load(html);
142
+ const results = [];
143
+
144
+ $(".market_listing_row").each((index, element) => {
145
+ const $row = $(element);
146
+
147
+ const cancelLink = $row.find(".market_listing_cancel_button > a").attr("href");
148
+ if (!cancelLink) return;
149
+
150
+ const paramsMatch = cancelLink.match(/\(([^)]*)\)/);
151
+ if (!paramsMatch) return;
152
+
153
+ let [listingId, appId, contextId, itemId] = paramsMatch[1].split(",").map((param) => param.trim().replace(/^['"]|['"]$/g, ""));
154
+
155
+ appId = Number(appId);
156
+ contextId = Number(contextId);
157
+ itemId = Number(itemId);
158
+
159
+ const imageUrl = $row.find(`#mylisting_${listingId}_image`).attr("src") || null;
160
+
161
+ const buyerPaysElem = $row.find('.market_listing_price span[title="This is the price the buyer pays."]');
162
+ const buyerPaysPrice = StringUtils.cleanSpace(buyerPaysElem.text().replace(/[()]/g, ""));
163
+
164
+ const receiveElem = $row.find('.market_listing_price span[title="This is how much you will receive."]');
165
+ const receivePrice = StringUtils.cleanSpace(receiveElem.text().replace(/[()]/g, ""));
166
+
167
+ const itemName = $row.find(".market_listing_item_name_link").text() || $row.find(".market_listing_item_name").text();
168
+
169
+ const gameName = $row.find(".market_listing_game_name").text();
170
+
171
+ const listedDateElem = $row.find(".market_listing_game_name + .market_listing_listed_date_combined");
172
+ const listedDate = formatMarketHistoryDate(StringUtils.cleanSpace(listedDateElem.text()));
173
+
174
+ results.push({
175
+ listingId,
176
+ appId,
177
+ contextId,
178
+ itemId,
179
+ imageUrl,
180
+ buyerPaysPrice,
181
+ receivePrice,
182
+ itemName,
183
+ gameName,
184
+ listedDate,
185
+ });
186
+ });
187
+
188
+ return results;
189
+ }