Skip to content

Commit eeb243f

Browse files
committed
Fix Steam attribution and flag untracked announcers
The game RSS feed exposes posters by display name, but SteamFeed matched against the account's SteamID64/vanity identifier, so no post ever matched and zero Steam posts were saved. - SteamFeed: resolve each account's current persona name from its profile XML and match feed authors against that (trimmed, case-insensitive) - Steam.afterIndex: once per game, ntfy for any recent announcement author that matches no tracked account's persona (finder-style discovery hint), deduped via the permanent cache - indexer.js: generic afterIndex hook so the cross-account check stays out of the type-agnostic indexing loop
1 parent bd32e7f commit eeb243f

3 files changed

Lines changed: 138 additions & 4 deletions

File tree

indexer.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,17 @@ const indexService = function indexService ( serviceConfig, serviceOptions, game
8181
}
8282

8383
return Promise.all( indexerPromises )
84-
.then( () => {
84+
.then( async () => {
85+
const IndexerClass = Indexers[ serviceConfig.indexerType ];
86+
87+
if ( IndexerClass && typeof IndexerClass.afterIndex === 'function' ) {
88+
try {
89+
await IndexerClass.afterIndex( serviceConfig, serviceOptions, gameIdentifier, load );
90+
} catch ( afterIndexError ) {
91+
console.error( afterIndexError );
92+
}
93+
}
94+
8595
console.timeEnd( `${ gameIdentifier }-${ serviceConfig.indexerType }` );
8696
} );
8797
};
@@ -161,8 +171,8 @@ const indexGame = function indexGame ( game ) {
161171

162172
// eslint-disable-next-line no-process-env
163173
if ( process.env.LIMIT_SERVICE && service !== process.env.LIMIT_SERVICE ) {
164-
continue;
165-
}
174+
continue;
175+
}
166176

167177
servicesIndexers.push( pFinally( indexService( serviceConfig[ service ], configuredServices[ service ] || {}, identifier ) ) );
168178
}

modules/indexers/Steam.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
const RSS = require( './RSS.js' );
12
const SteamFeed = require( './SteamFeed' );
3+
const cache = require( '../cache.js' );
4+
const ntfy = require( '../ntfy.js' );
5+
6+
const ATTRIBUTION_WINDOW_SECONDS = 30 * 24 * 60 * 60;
27

38
class Steam {
49
constructor ( userIdentifier, providerConfig, load ) {
@@ -15,6 +20,87 @@ class Steam {
1520
return [];
1621
}
1722
}
23+
24+
// Runs once per game after every tracked account has been indexed. The game
25+
// feed is shared across all of a game's Steam accounts, so attribution is a
26+
// cross-account question: an announcement author we can't match to ANY
27+
// tracked persona means there's a studio/dev account we should be tracking
28+
// (much like finder discovering new accounts).
29+
static async afterIndex ( serviceConfig, serviceOptions, gameIdentifier, load ) {
30+
const appId = serviceOptions.allowedSections && serviceOptions.allowedSections[ 0 ];
31+
32+
if ( !appId ) {
33+
return;
34+
}
35+
36+
const endpoint = `https://steamcommunity.com/games/${ appId }/rss/`;
37+
38+
let items = false;
39+
40+
try {
41+
items = await new RSS( appId, { endpoint }, load ).loadRecentPosts();
42+
} catch ( feedError ) {
43+
console.error( feedError );
44+
}
45+
46+
if ( !items || items.length === 0 ) {
47+
return;
48+
}
49+
50+
const cutoff = Math.floor( Date.now() / 1000 ) - ATTRIBUTION_WINDOW_SECONDS;
51+
const recentAuthors = new Set();
52+
53+
for ( let i = 0; i < items.length; i = i + 1 ) {
54+
if ( items[ i ].author && ( !items[ i ].timestamp || items[ i ].timestamp >= cutoff ) ) {
55+
recentAuthors.add( items[ i ].author.trim() );
56+
}
57+
}
58+
59+
if ( recentAuthors.size === 0 ) {
60+
return;
61+
}
62+
63+
const trackedPersonas = new Set();
64+
65+
await Promise.all( serviceConfig.developers.map( async ( developer ) => {
66+
const persona = await SteamFeed.resolvePersonaName( developer.identifier, load );
67+
68+
if ( persona ) {
69+
trackedPersonas.add( persona.toLowerCase() );
70+
}
71+
} ) );
72+
73+
for ( const author of recentAuthors ) {
74+
if ( trackedPersonas.has( author.toLowerCase() ) ) {
75+
continue;
76+
}
77+
78+
// Notify only once per (game, author) so the 60s run loop doesn't spam.
79+
const marker = `steam-unattributed-${ appId }-${ author }`;
80+
let alreadyNotified = false;
81+
82+
try {
83+
alreadyNotified = await cache.get( marker );
84+
} catch ( cacheError ) {
85+
console.error( cacheError );
86+
}
87+
88+
if ( alreadyNotified ) {
89+
continue;
90+
}
91+
92+
ntfy( {
93+
message: `"${ author }" posts announcements for ${ gameIdentifier } (app ${ appId }) but matches no tracked account. Consider adding them as a developer/studio account.`,
94+
title: 'Untracked Steam announcer',
95+
} );
96+
97+
try {
98+
await cache.store( marker, 'notified', true );
99+
} catch ( storeError ) {
100+
console.error( storeError );
101+
}
102+
}
103+
}
18104
}
19105

20106
module.exports = Steam;

modules/indexers/SteamFeed.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ class SteamFeed extends RSS {
1313
this.section = providerConfig.allowedSections[ 0 ];
1414
}
1515

16+
// The game RSS feed exposes posters by display name (e.g. "Mal"), but our
17+
// account identifier is a SteamID64 or vanity name. Resolve the account's
18+
// current persona name from its profile XML so we can match feed authors.
19+
static async resolvePersonaName ( userIdentifier, load ) {
20+
const profileUrl = /^\d+$/.test( userIdentifier )
21+
? `https://steamcommunity.com/profiles/${ userIdentifier }/?xml=1`
22+
: `https://steamcommunity.com/id/${ userIdentifier }/?xml=1`;
23+
24+
let profileXml = false;
25+
26+
try {
27+
profileXml = await load.get( profileUrl );
28+
} catch ( profileLoadError ) {
29+
console.error( `[SteamFeed] failed to load profile ${ profileUrl }: ${ profileLoadError.message }` );
30+
}
31+
32+
if ( !profileXml ) {
33+
return false;
34+
}
35+
36+
const match = profileXml.match( /<steamID>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/steamID>/ );
37+
38+
return match ? match[ 1 ].trim() : false;
39+
}
40+
1641
async loadRecentPosts () {
1742
let posts = false;
1843

@@ -22,10 +47,23 @@ class SteamFeed extends RSS {
2247
console.error( postLoadError );
2348
}
2449

50+
if ( !posts ) {
51+
return [];
52+
}
53+
54+
const personaName = await SteamFeed.resolvePersonaName( this.userId, this.load );
55+
56+
if ( !personaName ) {
57+
console.warn( `[SteamFeed] could not resolve persona for ${ this.userId }, skipping ${ this.section }` );
58+
59+
return [];
60+
}
61+
62+
const normalizedPersona = personaName.toLowerCase();
2563
const validPosts = [];
2664

2765
for ( let i = 0; i < posts.length; i = i + 1 ) {
28-
if ( posts[ i ].author !== this.userId ) {
66+
if ( !posts[ i ].author || posts[ i ].author.trim().toLowerCase() !== normalizedPersona ) {
2967
continue;
3068
}
3169

0 commit comments

Comments
 (0)