From fbb59b3ea071deb3330093a1bb5da137e18f33cc Mon Sep 17 00:00:00 2001 From: CMorooney Date: Sat, 4 Jan 2025 11:23:47 -0500 Subject: [PATCH] creates syncSteamAchievements calls in steam_api and steamSync, adds models to support. adds palette command. need to acually inject into template --- src/apis/steam_api.ts | 48 +++++++++++++++++++++++++++++++++- src/main.ts | 25 +++++++++++++++++- src/models/steam_game.model.ts | 37 +++++++++++++++++++++++++- src/utils/steamSync.ts | 43 ++++++++++++++++++------------ 4 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/apis/steam_api.ts b/src/apis/steam_api.ts index a37da4c..f8ffd40 100644 --- a/src/apis/steam_api.ts +++ b/src/apis/steam_api.ts @@ -3,7 +3,14 @@ import type { Nullable } from '../main'; import { requestUrl } from 'obsidian'; import { extract } from 'fuzzball'; -import { SteamResponse, OwnedSteamGames, SteamGame, SteamWishlistedGame } from '@models/steam_game.model'; +import { + SteamResponse, + OwnedSteamGames, + SteamGame, + SteamWishlistedGame, + SteamAchievementInfo, + SteamUserAchievement, +} from '@models/steam_game.model'; export class SteamAPI { constructor(private readonly key: string, private readonly steamId: string) {} @@ -89,6 +96,7 @@ export class SteamAPI { }); const games = res?.json?.response?.games; + if (games) { const ret: { [key: string]: { playtime_forever: Nullable; playtime_2weeks: Nullable } } = {}; for (const game of games) { @@ -106,4 +114,42 @@ export class SteamAPI { throw error; } } + + async getPlayerAchievmentsForGame(gameId: string): Promise { + try { + const userAchievementsUrl = new URL('http://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/'); + userAchievementsUrl.searchParams.append('key', this.key); + userAchievementsUrl.searchParams.append('format', 'json'); + userAchievementsUrl.searchParams.append('steamid', this.steamId); + userAchievementsUrl.searchParams.append('appid', gameId); + + const achievementDataUrl = new URL('https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2'); + achievementDataUrl.searchParams.append('key', this.key); + achievementDataUrl.searchParams.append('format', 'json'); + achievementDataUrl.searchParams.append('steamid', this.steamId); + achievementDataUrl.searchParams.append('appid', gameId); + + // this has readable content we'll need to match + const gameResult = await requestUrl({ url: achievementDataUrl.href, method: 'GET' }); + // this has the user achievement data but nothing readable + const userResult = await requestUrl({ url: userAchievementsUrl.href, method: 'GET' }); + + const matches: SteamAchievementInfo[] = []; + + if ((userResult?.json?.playerstats?.achievements?.length ?? 0) > 0) { + userResult?.json?.playerstats?.achievements + .filter(a => a.achieved) + .forEach(async (a: SteamUserAchievement) => { + const match = gameResult.json.game.availableGameStats.achievements.first(gs => gs.name === a.name); + if (match) { + matches.push(match); + } + }); + } + return matches; + } catch (error) { + console.warn('[Game Search][Steam API][getPlayerAchievementsForGame]' + error); + throw error; + } + } } diff --git a/src/main.ts b/src/main.ts index d4d27bd..c80f2ab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,7 @@ import { useTemplaterPluginInFile, executeInlineScriptsTemplates, } from '@utils/template'; -import { syncSteamWishlist, syncOwnedSteamGames, syncPlaytimes } from '@utils/steamSync'; +import { syncAchievements, syncSteamWishlist, syncOwnedSteamGames, syncPlaytimes } from '@utils/steamSync'; export type Nullable = T | undefined | null; @@ -78,6 +78,12 @@ export default class GameSearchPlugin extends Plugin { callback: () => this.syncSteamPlaytime(true), }); + this.addCommand({ + id: 'sync steam achievements', + name: 'Sync Steam Achievements', + callback: () => this.syncSteamAchievements(true), + }); + // This adds a settings tab so the user can configure various aspects of the plugin this.addSettingTab(new GameSearchSettingTab(this.app, this)); } @@ -239,6 +245,23 @@ export default class GameSearchPlugin extends Plugin { }).open(); } + async syncSteamAchievements(alertUninitializedApi: boolean): Promise { + // always check to see if steamApi needs to be initialized on sync, + // it's possible the user has entered API credentials at any point in time. + if (this.steamApi === undefined && this.settings.steamApiKey && this.settings.steamUserId) { + console.info('[Game Search][Steam Sync]: initializing steam api'); + this.steamApi = new SteamAPI(this.settings.steamApiKey, this.settings.steamUserId); + } + if (this.steamApi !== undefined) { + new Notice('syncing steam achievements'); + await syncAchievements(this.app.vault, this.app.fileManager, this.steamApi, this.settings); + new Notice('steam achievements sync complete'); + } else if (alertUninitializedApi) { + console.warn('[Game Search][SteamSync]: steam api not initialized'); + this.showNotice('Steam Api not initialized. Did you enter your steam API key and user Id in plugin settings?'); + } + } + async syncSteamPlaytime(alertUninitializedApi: boolean): Promise { // always check to see if steamApi needs to be initialized on sync, // it's possible the user has entered API credentials at any point in time. diff --git a/src/models/steam_game.model.ts b/src/models/steam_game.model.ts index 098897a..5b4c2c3 100644 --- a/src/models/steam_game.model.ts +++ b/src/models/steam_game.model.ts @@ -43,10 +43,45 @@ export interface SteamWishlistedGame { win: number; } -interface SteamWishlistedGameSub { +export interface SteamWishlistedGameSub { packageid: number; bundleid?: number; discount_block: string; discount_pct?: string; price: string; } + +export interface SteamUserAchievement { + name: string; + achieved: number; +} + +export interface SteamAchievementInfo { + name: string; + defaultvalue: number; + displayName: string; + hidden: number; + description: string; + icon: string; + icongray: string; +} + +export interface SteamGameStats { + achievements: SteamAchievementInfo[]; +} + +export interface SteamAchievementDataResponse { + game: { + gameName: string; + gameVersion: string; + availableGameStats: SteamGameStats; + }; +} + +export interface SteamUserStateForGameResponse { + playerstats: { + steamID: string; + gameName: string; + achievements: SteamUserAchievement[]; + }; +} diff --git a/src/utils/steamSync.ts b/src/utils/steamSync.ts index e23f6ea..2bd5bb0 100644 --- a/src/utils/steamSync.ts +++ b/src/utils/steamSync.ts @@ -165,14 +165,6 @@ export async function syncOwnedSteamGames( } } -async function parseFileMetadata(fileManager: FileManager, file: TFile): Promise { - return new Promise(accept => { - fileManager.processFrontMatter(file, (data: any) => { - accept(data); - }); - }); -} - export async function syncPlaytimes( vault: Vault, fileManager: FileManager, @@ -192,14 +184,6 @@ export async function syncPlaytimes( } }; - doForChild(async file => { - const noteMetadata = await parseFileMetadata(fileManager, file); - const steamId: Nullable = noteMetadata.steamId; - if (steamId) { - ids.push(steamId); - } - }); - const playerStats = await steamApi.getPlayerStatsForGames(ids); if (playerStats) { doForChild(async file => { @@ -213,3 +197,30 @@ export async function syncPlaytimes( }); } } + +export async function syncAchievements( + vault: Vault, + fileManager: FileManager, + steamApi: SteamAPI, + settings: any, +): Promise { + const folderPath = normalizePath(settings.folder); + const folder = vault.getFolderByPath(folderPath); + + const doForChild = async (func: (file: TFile) => Promise) => { + for (const f of folder.children) { + const file = f as TFile; + if (!!file && file.name.includes('.md')) { + await func(file); + } + } + }; + + doForChild(async file => { + fileManager.processFrontMatter(file, async data => { + if (data.steamId) { + await steamApi.getPlayerAchievmentsForGame(data.steamId); + } + }); + }); +}