feat(core): Implement Steam library sync with metadata extraction and image processing (#278)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added AWS queue infrastructure and SQS handler for processing Steam
game libraries and images.
- Introduced event-driven handling for new credentials and game
additions, including image uploads to S3.
- Added client functions to fetch Steam user libraries, friends lists,
app info, and related images.
- Added new database columns and schema updates to track game
acquisition, playtime, and family sharing.
  - Added utility function for chunking arrays.
- Added new event notifications for library queue processing and game
creation.
  - Added new lookup functions for categories and teams by slug.
- Introduced a new Team API with endpoints to list and fetch teams by
slug.
  - Added a new Steam library page displaying game images.

- **Enhancements**
  - Improved game creation with event notifications and upsert logic.
  - Enhanced category and team retrieval with new lookup functions.
  - Renamed and refined image categories for clearer classification.
  - Expanded dependencies for image processing and AWS SDK integration.
- Improved image processing utilities with caching, ranking, and
metadata extraction.
  - Refined Steam client utilities for concurrency and error handling.

- **Bug Fixes**
- Fixed event publishing timing and removed deprecated credential
retrieval methods.

- **Chores**
- Updated infrastructure configurations with increased timeouts, memory,
and resource linking.
- Added new dependencies for image processing, caching, and AWS SDK
clients.
  - Refined internal code structure and imports for clarity.
  - Removed Steam provider and related UI components from the frontend.
- Disabled authentication providers and Steam-related routes in the
frontend.
  - Updated API fetch handler to accept environment bindings.

- **Refactor**
- Simplified query result handling and renamed functions for better
clarity.
- Removed outdated event handler in favor of consolidated event
subscriber.
- Consolidated and simplified database relationships and permission
queries.

- **Tests**
  - No explicit test changes included in this release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wanjohi
2025-05-17 00:51:18 +03:00
committed by GitHub
parent cc2065299d
commit e1a903a7c9
82 changed files with 7819 additions and 1002 deletions

View File

@@ -0,0 +1,117 @@
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Steam } from "@nestri/core/steam/index";
import { Client } from "@nestri/core/client/index";
import { Images } from "@nestri/core/images/index";
import { Friend } from "@nestri/core/friend/index";
import { BaseGame } from "@nestri/core/base-game/index";
import { Credentials } from "@nestri/core/credentials/index";
import { EAuthTokenPlatformType, LoginSession } from "steam-session";
import { PutObjectCommand, S3Client, HeadObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
export const handler = bus.subscriber(
[Credentials.Events.New, BaseGame.Events.New],
async (event) => {
console.log(event.type, event.properties, event.metadata);
switch (event.type) {
case "new_credentials.added": {
const input = event.properties
const credentials = await Credentials.fromSteamID(input.steamID)
if (credentials) {
const session = new LoginSession(EAuthTokenPlatformType.MobileApp);
session.refreshToken = credentials.refreshToken;
const cookies = await session.getWebCookies();
const friends = await Client.getFriendsList(cookies);
const putFriends = friends.map(async (user) => {
const wasAdded =
await Steam.create({
id: user.steamID.toString(),
name: user.name,
realName: user.realName,
avatarHash: user.avatarHash,
steamMemberSince: user.memberSince,
profileUrl: user.customURL?.trim() || null,
limitations: {
isLimited: user.isLimitedAccount,
isVacBanned: user.vacBanned,
tradeBanState: user.tradeBanState.toLowerCase() as any,
privacyState: user.privacyState as any,
visibilityState: Number(user.visibilityState)
}
})
if (!wasAdded) {
console.log(`Steam user ${user.steamID.toString()} already exists`)
}
await Friend.add({ friendSteamID: user.steamID.toString(), steamID: input.steamID })
})
const settled = await Promise.allSettled(putFriends);
settled
.filter(result => result.status === 'rejected')
.forEach(result => console.warn('[putFriends] failed:', (result as PromiseRejectedResult).reason))
}
break;
}
case "new_game.added": {
const input = event.properties
// Get images and save to s3
const images = await Client.getImages(input.appID);
(await Promise.allSettled(
images.map(async (image) => {
// Put the images into the db
await Images.create({
type: image.type,
imageHash: image.hash,
baseGameID: input.appID,
position: image.position,
fileSize: image.fileSize,
sourceUrl: image.sourceUrl,
dimensions: image.dimensions,
extractedColor: image.averageColor,
});
try {
//Check whether the image already exists
await s3.send(
new HeadObjectCommand({
Bucket: Resource.Storage.name,
Key: `images/${image.hash}`,
})
);
} catch (e) {
// Save to s3 because it doesn't already exist
await s3.send(
new PutObjectCommand({
Bucket: Resource.Storage.name,
Key: `images/${image.hash}`,
Body: image.buffer,
...(image.format && { ContentType: `image/${image.format}` }),
Metadata: {
type: image.type,
appID: input.appID,
}
})
)
}
})
))
.filter(i => i.status === "rejected")
.forEach(r => console.warn("[createImages] failed:", (r as PromiseRejectedResult).reason));
break;
}
}
},
);