steamutils 1.5.52 → 1.5.53

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.
Files changed (4) hide show
  1. package/index.js +30 -3
  2. package/package.json +1 -1
  3. package/parse_html.js +336 -189
  4. package/utils.js +11 -0
package/index.js CHANGED
@@ -5,7 +5,7 @@ import moment from "moment";
5
5
  import { hex2b64, Key as RSA } from "node-bignumber";
6
6
  import SteamID from "steamid";
7
7
  import qs from "qs";
8
- import { console_log, convertLongsToNumbers, downloadImage, formatMarketCurrency, formatMarketHistoryDate, getCleanObject, getImageSize, getMarketPriceValueAsInt, isJWT, JSON_parse, JSON_stringify, removeSpaceKeys, secretAsBuffer, sleep } from "./utils.js";
8
+ import { console_log, convertLongsToNumbers, downloadImage, getCleanObject, getImageSize, isJWT, JSON_parse, JSON_stringify, removeSpaceKeys, secretAsBuffer, sleep } from "./utils.js";
9
9
  import { Header, request } from "./axios.js";
10
10
  import { getTableHasHeaders, querySelectorAll, table2json } from "./cheerio.js";
11
11
  import { getJSObjectFronXML } from "./xml2json.js";
@@ -21,7 +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
+ import { extractAssetItemsFromHovers, parseMarketHistoryListings, parseMarketListings, parseSteamProfileXmlToJson, parseUserProfile } from "./parse_html.js";
25
25
 
26
26
  const eventEmitter = (globalThis.steamUserEventEmitter = new EventEmitter());
27
27
 
@@ -397,8 +397,9 @@ export default class SteamUser {
397
397
 
398
398
  const result = await this._httpRequest(url);
399
399
  if (result instanceof ResponseError) {
400
- return result;
400
+ return null;
401
401
  }
402
+
402
403
  return SteamUser._parseUserProfile(result?.data || "");
403
404
  }
404
405
 
@@ -537,6 +538,32 @@ export default class SteamUser {
537
538
  };
538
539
  }
539
540
 
541
+ /**
542
+ * Fetches a Steam Community profile (in XML format) by SteamID, parses it,
543
+ * and returns a normalized JSON representation.
544
+ *
545
+ * @async
546
+ * @param {string} [steamId] - 64-bit SteamID. Defaults to the result of `this.getSteamIdUser()`.
547
+ * @returns {Promise<SteamProfileXml|undefined>} Promise resolving to the normalized Steam profile object, or `undefined` on failure.
548
+ *
549
+ */
550
+ async getProfileFromXml(steamId = this.getSteamIdUser()) {
551
+ const result = await this._httpRequest(`https://steamcommunity.com/profiles/${steamId}/?xml=1`);
552
+ if (result instanceof ResponseError) {
553
+ return;
554
+ }
555
+ try {
556
+ return parseSteamProfileXmlToJson(result?.data);
557
+ } catch (e) {}
558
+ }
559
+
560
+ static async getProfileFromXml(steamId) {
561
+ try {
562
+ const xml = (await axios.get(`https://steamcommunity.com/profiles/${steamId}/?xml=1`)).data;
563
+ return parseSteamProfileXmlToJson(xml);
564
+ } catch (e) {}
565
+ }
566
+
540
567
  static async getUsersSummaryByWebApiKey(webApiKey, steamIds) {
541
568
  if (!Array.isArray(steamIds)) {
542
569
  steamIds = [steamIds];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steamutils",
3
- "version": "1.5.52",
3
+ "version": "1.5.53",
4
4
  "main": "index.js",
5
5
  "dependencies": {
6
6
  "alpha-common-utils": "^1.0.6",
package/parse_html.js CHANGED
@@ -1,189 +1,336 @@
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
- }
1
+ import { StringUtils } from "alpha-common-utils/index.js";
2
+ import { formatMarketHistoryDate, getAvatarHashFromUrl } from "./utils.js";
3
+ import * as cheerio from "cheerio";
4
+ import { getJSObjectFronXML } from "./xml2json.js";
5
+
6
+ /**
7
+ * @typedef {Object} HoverItem
8
+ * @property {string|null} listingId The listing ID extracted from the selector, if available (as string).
9
+ * @property {number} appid The app (game) ID for this item.
10
+ * @property {number} contextid The context ID for this item.
11
+ * @property {number} assetid The unique asset ID.
12
+ * @property {string} unknown A string field captured from the arguments (usually "0").
13
+ * @property {string} [name_selector] The HTML selector for the item's name, if present.
14
+ * @property {string} [image_selector] The HTML selector for the item's image, if present.
15
+ * @property {string} [other_selector] The selector string if it does not match name/image pattern.
16
+ */
17
+
18
+ /**
19
+ * Extracts item information from Steam inventory trade history or similar HTML fragments.
20
+ * Scans the given HTML string for calls to `CreateItemHoverFromContainer` and extracts
21
+ * key data for each referenced item. Returns each item's info in a structured array.
22
+ *
23
+ * @param {string} html - The HTML string to parse.
24
+ * @returns {HoverItem[]} Array of hover item objects.
25
+ */
26
+ export function extractAssetItemsFromHovers(html) {
27
+ // Use your preferred HTML/space scrubber.
28
+ html = StringUtils.cleanSpace(html);
29
+
30
+ // RegExp for CreateItemHoverFromContainer arguments
31
+ const re = /CreateItemHoverFromContainer\s*\(\s*g_rgAssets\s*,\s*'([^']+)'\s*,\s*(\d+)\s*,\s*'(\d+)'\s*,\s*'(\d+)'\s*,\s*(\d+)\s*\)/g;
32
+
33
+ /** @type {Record<number, HoverItem>} */
34
+ const itemsMap = {};
35
+ let match;
36
+
37
+ while ((match = re.exec(html)) !== null) {
38
+ const [_, selector, appid, contextid, assetid, unknown] = match;
39
+
40
+ // Extract listingId as a string if present: "history_row_<listingId>_"
41
+ let listingId = null;
42
+ const lidMatch = selector.match(/history_row_(\d+)_/);
43
+ if (lidMatch) listingId = lidMatch[1]; // assign as string
44
+
45
+ const appidNum = Number(appid);
46
+ const contextidNum = Number(contextid);
47
+ const assetidNum = Number(assetid);
48
+
49
+ if (!itemsMap[assetidNum]) {
50
+ itemsMap[assetidNum] = {
51
+ listingId,
52
+ appid: appidNum,
53
+ contextid: contextidNum,
54
+ assetid: assetidNum,
55
+ unknown,
56
+ };
57
+ }
58
+
59
+ if (selector.endsWith("_name")) {
60
+ itemsMap[assetidNum].name_selector = selector;
61
+ } else if (selector.endsWith("_image")) {
62
+ itemsMap[assetidNum].image_selector = selector;
63
+ } else {
64
+ itemsMap[assetidNum].other_selector = selector;
65
+ }
66
+ }
67
+ return Object.values(itemsMap);
68
+ }
69
+
70
+ /**
71
+ * @typedef {Object} MarketHistoryListing
72
+ * @property {string} id - HTML row id attribute (e.g. "history_row_4316182845737233773_4316182845737233774").
73
+ * @property {string} listingId - Listing id (e.g. "4316182845737233773").
74
+ * @property {number} price - Price in integer form (e.g. 80270).
75
+ * @property {string} itemName - The market item's name (e.g. "UMP-45 | Mudder").
76
+ * @property {string} gameName - The game's name (e.g. "Counter-Strike 2").
77
+ * @property {string} listedOn - Listing date as string (e.g. "27 May 2024").
78
+ * @property {string} actedOn - Acted on date or empty string.
79
+ * @property {string} image - Full image URL for the item.
80
+ * @property {string} gainOrLoss - "+" for buys, "-" for sells.
81
+ * @property {string} status - Status text, usually empty string.
82
+ * @property {string} [assetId] - (optional) Associated asset id, if available.
83
+ */
84
+
85
+ /**
86
+ * Parses Steam market history listing rows into structured data objects.
87
+ *
88
+ * @param {string} html - The raw HTML string to parse.
89
+ * @param {Object} [assetByListingId={}] - Optional mapping of listingId to assetId.
90
+ * @returns {MarketHistoryListing[]} Array of market listing objects.
91
+ */
92
+ export function parseMarketHistoryListings(html, assetByListingId = {}) {
93
+ const $ = cheerio.load(html);
94
+ return $(".market_listing_row")
95
+ .toArray()
96
+ .map((el) => {
97
+ const $el = $(el);
98
+ const id = $el.attr("id");
99
+ const listingId = id?.match(/history_row_(.*?)_/)?.[1];
100
+ if (!listingId) return null;
101
+ 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()];
102
+ const price = parseInt(priceText.replace(/[₫().,]/g, "").trim(), 10) || "";
103
+ const listedOn = typeof formatMarketHistoryDate === "function" ? formatMarketHistoryDate(listedOnText.replace(/Listed:/, "").trim()) : listedOnText.replace(/Listed:/, "").trim();
104
+ const actedOn = typeof formatMarketHistoryDate === "function" ? formatMarketHistoryDate(actedOnText.replace(/Listed:/, "").trim()) : actedOnText.replace(/Listed:/, "").trim();
105
+
106
+ const result = {
107
+ id,
108
+ listingId,
109
+ price,
110
+ itemName,
111
+ gameName,
112
+ listedOn,
113
+ actedOn,
114
+ image,
115
+ gainOrLoss,
116
+ status,
117
+ };
118
+ if (assetByListingId?.[listingId]) result.assetId = assetByListingId[listingId];
119
+ return result;
120
+ })
121
+ .filter(Boolean);
122
+ }
123
+
124
+ /**
125
+ * Parses Steam Market HTML listings to an array of items.
126
+ *
127
+ * @param {string} html - HTML string containing listing rows.
128
+ * @returns {Array<{
129
+ * listingId: string,
130
+ * appId: number,
131
+ * contextId: number,
132
+ * itemId: number,
133
+ * imageUrl: string|null,
134
+ * buyerPaysPrice: string,
135
+ * receivePrice: string,
136
+ * itemName: string,
137
+ * gameName: string,
138
+ * listedDate: string
139
+ * }>}
140
+ */
141
+ export function parseMarketListings(html) {
142
+ const $ = cheerio.load(html);
143
+ const results = [];
144
+
145
+ $(".market_listing_row").each((index, element) => {
146
+ const $row = $(element);
147
+
148
+ const cancelLink = $row.find(".market_listing_cancel_button > a").attr("href");
149
+ if (!cancelLink) return;
150
+
151
+ const paramsMatch = cancelLink.match(/\(([^)]*)\)/);
152
+ if (!paramsMatch) return;
153
+
154
+ let [, listingId, appId, contextId, itemId] = paramsMatch[1].split(",").map((param) => param.trim().replace(/^['"]|['"]$/g, ""));
155
+
156
+ appId = Number(appId);
157
+ contextId = Number(contextId);
158
+ itemId = Number(itemId);
159
+
160
+ const imageUrl = $row.find(`#mylisting_${listingId}_image`).attr("src") || null;
161
+
162
+ const buyerPaysElem = $row.find('.market_listing_price span[title="This is the price the buyer pays."]');
163
+ const buyerPaysPrice = StringUtils.cleanSpace(buyerPaysElem.text().replace(/[()]/g, ""));
164
+
165
+ const receiveElem = $row.find('.market_listing_price span[title="This is how much you will receive."]');
166
+ const receivePrice = StringUtils.cleanSpace(receiveElem.text().replace(/[()]/g, ""));
167
+
168
+ const itemName = $row.find(".market_listing_item_name_link").text() || $row.find(".market_listing_item_name").text();
169
+
170
+ const gameName = $row.find(".market_listing_game_name").text();
171
+
172
+ const listedDateElem = $row.find(".market_listing_game_name + .market_listing_listed_date_combined");
173
+ const listedDate = formatMarketHistoryDate(StringUtils.cleanSpace(listedDateElem.text()));
174
+
175
+ results.push({
176
+ listingId,
177
+ appId,
178
+ contextId,
179
+ itemId,
180
+ imageUrl,
181
+ buyerPaysPrice,
182
+ receivePrice,
183
+ itemName,
184
+ gameName,
185
+ listedDate,
186
+ });
187
+ });
188
+
189
+ return results;
190
+ }
191
+
192
+ /**
193
+ * Represents a Steam Community group for a profile.
194
+ * @typedef {Object} SteamProfileGroupXml
195
+ * @property {string} groupID64
196
+ * @property {string} groupName
197
+ * @property {string} groupURL
198
+ * @property {string} headline
199
+ * @property {string} summary
200
+ * @property {string} avatarIcon
201
+ * @property {string} avatarMedium
202
+ * @property {string} avatarFull
203
+ * @property {number} memberCount
204
+ * @property {number} membersInChat
205
+ * @property {number} membersInGame
206
+ * @property {number} membersOnline
207
+ */
208
+
209
+ /**
210
+ * Array of Steam Community groups.
211
+ * @typedef {SteamProfileGroupXml[]} SteamProfileGroupsXml
212
+ */
213
+
214
+ /**
215
+ * Represents a most played game on a Steam profile.
216
+ * @typedef {Object} SteamProfileMostPlayedGameXml
217
+ * @property {string} gameName
218
+ * @property {string} gameLink
219
+ * @property {string} gameIcon
220
+ * @property {string} gameLogo
221
+ * @property {string} gameLogoSmall
222
+ * @property {number} hoursPlayed
223
+ * @property {number} hoursOnRecord
224
+ * @property {string} statsName
225
+ */
226
+
227
+ /**
228
+ * Array of most played games.
229
+ * @typedef {SteamProfileMostPlayedGameXml[]} SteamProfileMostPlayedGamesXml
230
+ */
231
+
232
+ /**
233
+ * Steam Community profile (parsed from XML, normalized).
234
+ * @typedef {Object} SteamProfileXml
235
+ * @property {string} name
236
+ * @property {string} steamId
237
+ * @property {string} onlineState
238
+ * @property {string} stateMessage
239
+ * @property {string} privacyState
240
+ * @property {number} visibilityState
241
+ * @property {string} avatarIcon
242
+ * @property {string} avatarMedium
243
+ * @property {string} avatarFull
244
+ * @property {string} avatarHash
245
+ * @property {number} vacBanned
246
+ * @property {string} tradeBanState
247
+ * @property {number} isLimitedAccount
248
+ * @property {string} customURL
249
+ * @property {string} memberSince
250
+ * @property {string} steamRating
251
+ * @property {number} hoursPlayed2Wk
252
+ * @property {string} headline
253
+ * @property {string} location
254
+ * @property {string} realname
255
+ * @property {string} summary
256
+ * @property {SteamProfileMostPlayedGamesXml} [mostPlayedGames]
257
+ * @property {SteamProfileGroupsXml} [groups]
258
+ */
259
+
260
+ /**
261
+ * Parses a Steam profile XML into a normalized JSON format.
262
+ *
263
+ * @param {string} xml - The XML string as returned by the Steam Community profile endpoint.
264
+ * @returns {SteamProfileXml|undefined} The normalized profile object or undefined on error.
265
+ *
266
+ * @example
267
+ * import { parseSteamProfileXmlToJson } from "./your-module";
268
+ * const xml = await fetch('https://steamcommunity.com/profiles/76561198386265483/?xml=1').then(r => r.text());
269
+ * const profile = parseSteamProfileXmlToJson(xml);
270
+ * console.log(profile.name);
271
+ */
272
+ export function parseSteamProfileXmlToJson(xml) {
273
+ const parsed = getJSObjectFronXML(xml);
274
+ if (!parsed || !parsed.profile) return;
275
+
276
+ // Flatten out the xml2js _structure, for direct children
277
+ const profile = {};
278
+ Object.entries(parsed.profile).forEach(([key, value]) => {
279
+ // If only text, keep as string
280
+ if (typeof value === "object" && value !== null && "_" in value && Object.keys(value).length === 1) {
281
+ profile[key] = value._;
282
+ } else {
283
+ profile[key] = value;
284
+ }
285
+ });
286
+
287
+ // Rename fields
288
+ if ("steamID" in profile) {
289
+ profile.name = profile.steamID;
290
+ delete profile.steamID;
291
+ }
292
+ if ("steamID64" in profile) {
293
+ profile.steamId = profile.steamID64;
294
+ delete profile.steamID64;
295
+ }
296
+
297
+ // Convert numbers
298
+ const numberFields = ["vacBanned", "isLimitedAccount", "visibilityState", "memberCount", "membersInChat", "membersInGame", "membersOnline", "hoursOnRecord"];
299
+ const floatFields = ["hoursPlayed2Wk", "hoursPlayed"];
300
+ for (const field of numberFields) {
301
+ if (field in profile) profile[field] = parseInt(profile[field], 10) || 0;
302
+ }
303
+ for (const field of floatFields) {
304
+ if (field in profile) profile[field] = parseFloat(profile[field]) || 0;
305
+ }
306
+
307
+ // Add avatarHash
308
+ const avatarUrl = profile.avatarFull || profile.avatarMedium || profile.avatarIcon;
309
+ profile.avatarHash = getAvatarHashFromUrl(avatarUrl);
310
+
311
+ // Ensure all simple fields are strings if empty object
312
+ for (const key in profile) {
313
+ if (typeof profile[key] === "object" && profile[key] !== null && Object.keys(profile[key]).length === 0) {
314
+ profile[key] = "";
315
+ }
316
+ }
317
+
318
+ // Move groups.group to top-level 'groups' if present
319
+ if (profile.groups && profile.groups.group) {
320
+ profile.groups = profile.groups.group;
321
+ }
322
+
323
+ // Remove _attributes from groups (if present)
324
+ if (Array.isArray(profile.groups)) {
325
+ profile.groups = profile.groups.map((group) => {
326
+ if (group && typeof group === "object" && "_attributes" in group) {
327
+ // shallow clone and remove _attributes
328
+ const { _attributes, ...groupWithoutAttributes } = group;
329
+ return groupWithoutAttributes;
330
+ }
331
+ return group;
332
+ });
333
+ }
334
+
335
+ return profile;
336
+ }
package/utils.js CHANGED
@@ -1298,3 +1298,14 @@ export function isJWT(token) {
1298
1298
  }
1299
1299
  return false;
1300
1300
  }
1301
+
1302
+ /**
1303
+ * Extracts the 40-character Steam avatar hash from a URL.
1304
+ * @param {string} avatarUrl
1305
+ * @returns {string}
1306
+ */
1307
+ export function getAvatarHashFromUrl(avatarUrl) {
1308
+ if (!avatarUrl) return "";
1309
+ const match = avatarUrl.match(/\/([a-f0-9]{40})[_\.]/i);
1310
+ return match ? match[1] : "";
1311
+ }