steamutils 1.5.51 → 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.
@@ -1,6 +1,7 @@
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";
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = path.dirname(__filename);
@@ -11,10 +12,14 @@ export class SteamProto {
11
12
  }
12
13
 
13
14
  toProto() {
14
- const root = new Protobuf.Root().loadSync(path.join(`${__dirname}/protos/`, this._proto.filename), {
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], {
15
20
  keepCase: true,
16
21
  });
17
- return root[this._proto.name];
22
+ return root.lookupType(this._proto.name);
18
23
  }
19
24
 
20
25
  protoEncode(obj) {
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];
@@ -7291,6 +7318,39 @@ export default class SteamUser {
7291
7318
  };
7292
7319
  }
7293
7320
 
7321
+ /**
7322
+ * @typedef {Object} FriendGameplayInfoEntry
7323
+ * @property {string} steamId - The SteamID64 of the friend.
7324
+ * @property {number} minutes_played - Number of minutes played recently or currently.
7325
+ * @property {number} minutes_played_forever - Total minutes this friend has played forever.
7326
+ */
7327
+
7328
+ /**
7329
+ * @typedef {Object} YourInfo
7330
+ * @property {number} minutes_played - Number of minutes you have played recently or currently.
7331
+ * @property {number} minutes_played_forever - Total minutes you have played forever.
7332
+ * @property {boolean} in_wishlist - Whether the app is in your wishlist.
7333
+ * @property {boolean} owned - Whether you own the app.
7334
+ */
7335
+
7336
+ /**
7337
+ * @typedef {Object} FriendsGameplayInfoResponse
7338
+ * @property {FriendGameplayInfoEntry[]} in_game - Friends currently playing the game.
7339
+ * @property {FriendGameplayInfoEntry[]} played_recently - Friends who played the game recently.
7340
+ * @property {FriendGameplayInfoEntry[]} played_ever - Friends who have ever played the game.
7341
+ * @property {FriendGameplayInfoEntry[]} owns - Friends who own the game.
7342
+ * @property {YourInfo} your_info - Information about your own playtime, wishlist and ownership for this app.
7343
+ */
7344
+
7345
+ /**
7346
+ * Retrieves Steam friends' gameplay stats for a specific app.
7347
+ *
7348
+ * @param {string} accessToken - Steam user access token.
7349
+ * @param {number|string} appId - The App ID of the game to query.
7350
+ * @returns {Promise<FriendsGameplayInfoResponse|undefined>}
7351
+ * Resolves with gameplay info: lists of friends (by status) and your stats for the app,
7352
+ * or `undefined` if the result is empty or on unauthorized access.
7353
+ */
7294
7354
  async getFriendsGameplayInfo(accessToken, appId) {
7295
7355
  if (!appId) {
7296
7356
  return;
@@ -7311,7 +7371,7 @@ export default class SteamUser {
7311
7371
  responseType: "arraybuffer",
7312
7372
  });
7313
7373
  if (result instanceof ResponseError) {
7314
- return result;
7374
+ return;
7315
7375
  }
7316
7376
 
7317
7377
  if (!result || result.status === 401) {
@@ -7339,45 +7399,6 @@ export default class SteamUser {
7339
7399
  owns: formatList(owns),
7340
7400
  your_info: _.omit(your_info, "steamid"),
7341
7401
  };
7342
-
7343
- const example = [
7344
- {
7345
- public_data: {
7346
- visibility_state: 3,
7347
- privacy_state: 0,
7348
- profile_state: 1,
7349
- ban_expires_time: 0,
7350
- account_flags: 0,
7351
- persona_name: "LOL haha",
7352
- profile_url: "",
7353
- content_country_restricted: false,
7354
- steamId: "76561199243542939",
7355
- avatarHash: "0000000000000000000000000000000000000000",
7356
- },
7357
- private_data: {
7358
- persona_state: 0,
7359
- persona_state_flags: 0,
7360
- time_created: 1644675779,
7361
- game_id: 0,
7362
- game_server_ip_address: 0,
7363
- game_server_port: 0,
7364
- game_extra_info: "",
7365
- account_name: "",
7366
- lobby_steam_id: 0,
7367
- rich_presence_kv: "",
7368
- watching_broadcast_accountid: 0,
7369
- watching_broadcast_appid: 0,
7370
- watching_broadcast_viewers: 0,
7371
- watching_broadcast_title: "",
7372
- last_logoff_time: 0,
7373
- last_seen_online: 0,
7374
- game_os_type: 0,
7375
- game_device_type: 0,
7376
- game_device_name: "",
7377
- game_is_private: false,
7378
- },
7379
- },
7380
- ];
7381
7402
  }
7382
7403
 
7383
7404
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "steamutils",
3
- "version": "1.5.51",
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,6 +1,7 @@
1
1
  import { StringUtils } from "alpha-common-utils/index.js";
2
- import { formatMarketHistoryDate } from "./utils.js";
2
+ import { formatMarketHistoryDate, getAvatarHashFromUrl } from "./utils.js";
3
3
  import * as cheerio from "cheerio";
4
+ import { getJSObjectFronXML } from "./xml2json.js";
4
5
 
5
6
  /**
6
7
  * @typedef {Object} HoverItem
@@ -187,3 +188,149 @@ export function parseMarketListings(html) {
187
188
 
188
189
  return results;
189
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
+ }