steamutils 1.5.52 → 1.5.54

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