diff --git a/registry.json b/registry.json index 77e227a..9f253b4 100644 --- a/registry.json +++ b/registry.json @@ -122,6 +122,21 @@ "script": "sha256:d58629b152ad3471e27dfb4cbe33b84d74f426a7ff330754aacdc1ec89f72f6c", "metadata": "sha256:d3ff5230a6ebe6ff3f655546d834e7ed59f741febdc540dd2f89894b6e5baa7a" } + }, + { + "id": "steam-playwright", + "company": "valve", + "version": "1.0.0", + "name": "Steam", + "description": "Exports your Steam profile, game library with playtime stats, and friend list using the Steam Web API.", + "files": { + "script": "valve/steam-playwright.js", + "metadata": "valve/steam-playwright.json" + }, + "checksums": { + "script": "sha256:3c4b768fee81c9347ae6f8da714ce7fbabd2bb16918992a2e3e34db20c49acfc", + "metadata": "sha256:f2cabb1d26193df8c12207ebdcd6e668c05cfc14a112264a73ac3f88fb5e2bec" + } } ] } diff --git a/valve/schemas/steam.friends.json b/valve/schemas/steam.friends.json new file mode 100644 index 0000000..0dbcba2 --- /dev/null +++ b/valve/schemas/steam.friends.json @@ -0,0 +1,20 @@ +{ + "name": "Steam Friends", + "scope": "steam.friends", + "description": "Steam friend list with persona names, avatars, and friendship dates", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "steamId": { "type": "string", "description": "Friend's 64-bit Steam ID" }, + "personaName": { "type": ["string", "null"], "description": "Friend's display name" }, + "avatarUrl": { "type": ["string", "null"], "format": "uri", "description": "URL to the friend's avatar image" }, + "profileUrl": { "type": ["string", "null"], "format": "uri", "description": "URL to the friend's Steam Community profile" }, + "friendSince": { "type": ["string", "null"], "format": "date-time", "description": "When the friendship was established" }, + "relationship": { "type": "string", "description": "Relationship type (usually 'friend')" } + }, + "required": ["steamId"] + } + } +} diff --git a/valve/schemas/steam.games.json b/valve/schemas/steam.games.json new file mode 100644 index 0000000..05d064d --- /dev/null +++ b/valve/schemas/steam.games.json @@ -0,0 +1,42 @@ +{ + "name": "Steam Games", + "scope": "steam.games", + "description": "Owned games with playtime statistics and recently played games", + "schema": { + "type": "object", + "properties": { + "owned": { + "type": "array", + "description": "All games owned by the user, sorted by playtime descending", + "items": { + "type": "object", + "properties": { + "appId": { "type": "integer", "description": "Steam application ID" }, + "name": { "type": "string", "description": "Game title" }, + "playtimeMinutes": { "type": "integer", "description": "Total playtime in minutes" }, + "playtimeHours": { "type": "number", "description": "Total playtime in hours (rounded to 1 decimal)" }, + "playtimeTwoWeeksMinutes": { "type": "integer", "description": "Playtime in the last two weeks in minutes" }, + "iconUrl": { "type": ["string", "null"], "format": "uri", "description": "URL to the game's icon image" }, + "lastPlayed": { "type": ["string", "null"], "format": "date-time", "description": "When the game was last played" } + }, + "required": ["appId", "name", "playtimeMinutes"] + } + }, + "recentlyPlayed": { + "type": "array", + "description": "Games played in the last two weeks", + "items": { + "type": "object", + "properties": { + "appId": { "type": "integer", "description": "Steam application ID" }, + "name": { "type": "string", "description": "Game title" }, + "playtimeTwoWeeksMinutes": { "type": "integer", "description": "Playtime in last two weeks in minutes" }, + "playtimeForeverMinutes": { "type": "integer", "description": "Total playtime in minutes" } + }, + "required": ["appId", "name"] + } + } + }, + "required": ["owned", "recentlyPlayed"] + } +} diff --git a/valve/schemas/steam.profile.json b/valve/schemas/steam.profile.json new file mode 100644 index 0000000..fc901e6 --- /dev/null +++ b/valve/schemas/steam.profile.json @@ -0,0 +1,24 @@ +{ + "name": "Steam Profile", + "scope": "steam.profile", + "description": "Steam user profile including persona name, avatar, level, location, and account metadata", + "schema": { + "type": "object", + "properties": { + "steamId": { "type": "string", "description": "64-bit Steam ID" }, + "personaName": { "type": "string", "description": "Display name on Steam" }, + "profileUrl": { "type": "string", "format": "uri", "description": "URL to the user's Steam Community profile" }, + "avatarUrl": { "type": "string", "format": "uri", "description": "URL to the user's full-size avatar image" }, + "realName": { "type": ["string", "null"], "description": "Real name if set by the user" }, + "country": { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2 country code" }, + "state": { "type": ["string", "null"], "description": "State/region code" }, + "cityId": { "type": ["integer", "null"], "description": "Steam internal city identifier" }, + "steamLevel": { "type": ["integer", "null"], "description": "Steam profile level" }, + "accountCreated": { "type": ["string", "null"], "format": "date-time", "description": "When the Steam account was created" }, + "lastLogoff": { "type": ["string", "null"], "format": "date-time", "description": "Last time the user was seen online" }, + "personaState": { "type": "integer", "description": "Online status (0=offline, 1=online, 2=busy, 3=away, 4=snooze, 5=looking to trade, 6=looking to play)" }, + "communityVisibilityState": { "type": "integer", "description": "Profile visibility (1=private, 3=public)" } + }, + "required": ["steamId", "personaName", "profileUrl"] + } +} diff --git a/valve/steam-playwright.js b/valve/steam-playwright.js new file mode 100644 index 0000000..e80c6f0 --- /dev/null +++ b/valve/steam-playwright.js @@ -0,0 +1,174 @@ + +(async () => { + const STEAM_API = 'https://api.steampowered.com'; + + // ─── Auth: API key + Steam ID ───────────────────────────── + let apiKey = process.env.STEAM_API_KEY; + let steamId = process.env.STEAM_ID; + + if (!apiKey || !steamId) { + const creds = await page.requestInput({ + message: 'Enter your Steam Web API key and Steam ID.\n\nGet your API key at: https://steamcommunity.com/dev/apikey\nFind your Steam ID at: https://steamid.io', + schema: { + type: 'object', + properties: { + apiKey: { type: 'string', title: 'Steam Web API Key' }, + steamId: { type: 'string', title: 'Steam ID (76561...)' }, + }, + required: ['apiKey', 'steamId'], + }, + }); + apiKey = creds.apiKey; + steamId = creds.steamId; + } + + await page.closeBrowser(); + + // ─── Helpers ────────────────────────────────────────────── + async function steamGet(iface, method, version, params) { + const qs = new URLSearchParams({ key: apiKey, steamid: steamId, ...params }); + const url = STEAM_API + '/' + iface + '/' + method + '/' + version + '/?' + qs.toString(); + const resp = await page.httpFetch(url); + if (!resp.ok) return { _error: 'HTTP ' + resp.status }; + return resp.json; + } + + // ─── Step 1: Profile ───────────────────────────────────── + await page.setProgress({ phase: { step: 1, total: 4, label: 'Fetching profile' }, message: 'Getting player summary...' }); + + const profileResp = await steamGet('ISteamUser', 'GetPlayerSummaries', 'v2', { steamids: steamId }); + if (profileResp._error) { + await page.setData('error', 'Failed to fetch profile: ' + profileResp._error + '. Check your API key and Steam ID.'); + return; + } + + const players = (profileResp.response && profileResp.response.players) || []; + if (players.length === 0) { + await page.setData('error', 'No player found for Steam ID ' + steamId + '. Check your Steam ID.'); + return; + } + + const player = players[0]; + + const levelResp = await steamGet('IPlayerService', 'GetSteamLevel', 'v1', {}); + const steamLevel = (levelResp.response && levelResp.response.player_level) || null; + + const profile = { + steamId: player.steamid, + personaName: player.personaname, + profileUrl: player.profileurl, + avatarUrl: player.avatarfull || player.avatar, + realName: player.realname || null, + country: player.loccountrycode || null, + state: player.locstatecode || null, + cityId: player.loccityid || null, + steamLevel: steamLevel, + accountCreated: player.timecreated ? new Date(player.timecreated * 1000).toISOString() : null, + lastLogoff: player.lastlogoff ? new Date(player.lastlogoff * 1000).toISOString() : null, + personaState: player.personastate, + communityVisibilityState: player.communityvisibilitystate, + }; + + // ─── Step 2: Owned games ───────────────────────────────── + await page.setProgress({ phase: { step: 2, total: 4, label: 'Fetching games' }, message: 'Getting owned games...' }); + + const gamesResp = await steamGet('IPlayerService', 'GetOwnedGames', 'v1', { + include_appinfo: '1', + include_played_free_games: '1', + }); + + let games = []; + if (!gamesResp._error && gamesResp.response && gamesResp.response.games) { + games = gamesResp.response.games.map(function (g) { + return { + appId: g.appid, + name: g.name, + playtimeMinutes: g.playtime_forever || 0, + playtimeHours: Math.round((g.playtime_forever || 0) / 60 * 10) / 10, + playtimeTwoWeeksMinutes: g.playtime_2weeks || 0, + iconUrl: g.img_icon_url + ? 'https://media.steampowered.com/steamcommunity/public/images/apps/' + g.appid + '/' + g.img_icon_url + '.jpg' + : null, + lastPlayed: g.rtime_last_played ? new Date(g.rtime_last_played * 1000).toISOString() : null, + }; + }); + games.sort(function (a, b) { return b.playtimeMinutes - a.playtimeMinutes; }); + } + + const recentResp = await steamGet('IPlayerService', 'GetRecentlyPlayedGames', 'v1', {}); + let recentGames = []; + if (!recentResp._error && recentResp.response && recentResp.response.games) { + recentGames = recentResp.response.games.map(function (g) { + return { + appId: g.appid, + name: g.name, + playtimeTwoWeeksMinutes: g.playtime_2weeks || 0, + playtimeForeverMinutes: g.playtime_forever || 0, + }; + }); + } + + // ─── Step 3: Friends ───────────────────────────────────── + await page.setProgress({ phase: { step: 3, total: 4, label: 'Fetching friends' }, message: 'Getting friend list...' }); + + let friends = []; + const friendsResp = await steamGet('ISteamUser', 'GetFriendList', 'v1', { relationship: 'friend' }); + if (!friendsResp._error && friendsResp.friendslist && friendsResp.friendslist.friends) { + const friendIds = friendsResp.friendslist.friends; + + // Batch resolve friend names (100 at a time) + var friendProfiles = {}; + for (var i = 0; i < friendIds.length; i += 100) { + var batch = friendIds.slice(i, i + 100); + var ids = batch.map(function (f) { return f.steamid; }).join(','); + var batchResp = await page.httpFetch( + STEAM_API + '/ISteamUser/GetPlayerSummaries/v2/?key=' + apiKey + '&steamids=' + ids + ); + if (batchResp.ok && batchResp.json && batchResp.json.response) { + var batchPlayers = batchResp.json.response.players || []; + for (var p = 0; p < batchPlayers.length; p++) { + friendProfiles[batchPlayers[p].steamid] = batchPlayers[p]; + } + } + if (i + 100 < friendIds.length) await page.sleep(300); + } + + friends = friendIds.map(function (f) { + var fp = friendProfiles[f.steamid]; + return { + steamId: f.steamid, + personaName: fp ? fp.personaname : null, + avatarUrl: fp ? (fp.avatarfull || fp.avatar) : null, + profileUrl: fp ? fp.profileurl : null, + friendSince: f.friend_since ? new Date(f.friend_since * 1000).toISOString() : null, + relationship: f.relationship, + }; + }); + } + + // ─── Step 4: Build result ──────────────────────────────── + await page.setProgress({ phase: { step: 4, total: 4, label: 'Building result' }, message: 'Packaging data...' }); + + var totalPlaytimeHours = Math.round(games.reduce(function (sum, g) { return sum + g.playtimeMinutes; }, 0) / 60); + + var topGames = games.slice(0, 5).map(function (g) { return g.name + ' (' + g.playtimeHours + 'h)'; }).join(', '); + + var result = { + 'steam.profile': profile, + 'steam.games': { owned: games, recentlyPlayed: recentGames }, + 'steam.friends': friends, + exportSummary: { + count: games.length + friends.length, + label: 'Steam data items', + details: games.length + ' games (' + totalPlaytimeHours + 'h total), ' + + friends.length + ' friends, ' + + recentGames.length + ' recently played. ' + + (topGames ? 'Top: ' + topGames : ''), + }, + timestamp: new Date().toISOString(), + version: '1.0.0', + platform: 'steam', + }; + + await page.setData('result', result); +})(); diff --git a/valve/steam-playwright.json b/valve/steam-playwright.json new file mode 100644 index 0000000..caf7ddc --- /dev/null +++ b/valve/steam-playwright.json @@ -0,0 +1,32 @@ +{ + "id": "steam-playwright", + "version": "1.0.0", + "name": "Steam", + "company": "valve", + "iconURL": "icons/steam.svg", + "description": "Exports your Steam profile, game library with playtime stats, and friend list using the Steam Web API.", + "connectURL": "https://steamcommunity.com/dev/apikey", + "connectSelector": "input#domain", + "exportFrequency": "weekly", + "runtime": "playwright", + "scopes": [ + { + "scope": "steam.profile", + "label": "Your Profile", + "description": "Steam profile including persona name, avatar, level, country, and account age" + }, + { + "scope": "steam.games", + "label": "Your Games", + "description": "Owned games with playtime statistics, plus recently played games" + }, + { + "scope": "steam.friends", + "label": "Your Friends", + "description": "Friend list with persona names, avatars, and friendship dates" + } + ], + "vectorize_config": { + "documents": "steam.games.owned[].name" + } +}