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.
- package/index.js +30 -3
- package/package.json +1 -1
- package/parse_html.js +346 -189
- 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,
|
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
|
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
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
|
-
|
7
|
-
|
8
|
-
* @
|
9
|
-
* @property {
|
10
|
-
* @property {number}
|
11
|
-
* @property {
|
12
|
-
* @property {
|
13
|
-
* @property {string}
|
14
|
-
* @property {string} [
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
*
|
21
|
-
*
|
22
|
-
*
|
23
|
-
*
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
const
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
itemsMap[assetidNum].
|
62
|
-
} else {
|
63
|
-
itemsMap[assetidNum].
|
64
|
-
}
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
* @
|
73
|
-
* @property {
|
74
|
-
* @property {string}
|
75
|
-
* @property {
|
76
|
-
* @property {string}
|
77
|
-
* @property {string}
|
78
|
-
* @property {string}
|
79
|
-
* @property {string}
|
80
|
-
* @property {string}
|
81
|
-
* @property {string}
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
*
|
88
|
-
*
|
89
|
-
* @
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
const
|
99
|
-
|
100
|
-
const
|
101
|
-
|
102
|
-
const
|
103
|
-
const
|
104
|
-
|
105
|
-
const
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
}
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
*
|
127
|
-
*
|
128
|
-
*
|
129
|
-
*
|
130
|
-
*
|
131
|
-
*
|
132
|
-
*
|
133
|
-
*
|
134
|
-
*
|
135
|
-
*
|
136
|
-
*
|
137
|
-
*
|
138
|
-
*
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
const
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
appId =
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
const
|
162
|
-
|
163
|
-
|
164
|
-
const
|
165
|
-
|
166
|
-
|
167
|
-
const
|
168
|
-
|
169
|
-
const
|
170
|
-
|
171
|
-
const
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
+
}
|