✨ Steam presence service
This commit is contained in:
250
DysonNetwork.Pass/Account/Presences/SteamPresenceService.cs
Normal file
250
DysonNetwork.Pass/Account/Presences/SteamPresenceService.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using SteamWebAPI2.Interfaces;
|
||||
using SteamWebAPI2.Utilities;
|
||||
|
||||
namespace DysonNetwork.Pass.Account.Presences;
|
||||
|
||||
public class SteamPresenceService(
|
||||
AppDatabase db,
|
||||
AccountEventService accountEventService,
|
||||
ILogger<SteamPresenceService> logger,
|
||||
IConfiguration configuration
|
||||
) : IPresenceService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ServiceId => "steam";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdatePresencesAsync(IEnumerable<Guid> userIds)
|
||||
{
|
||||
var userIdList = userIds.ToList();
|
||||
var steamConnections = await db.AccountConnections
|
||||
.Where(c => userIdList.Contains(c.AccountId) && c.Provider == "steam")
|
||||
.Include(c => c.Account)
|
||||
.ToListAsync();
|
||||
|
||||
if (steamConnections.Count == 0)
|
||||
return;
|
||||
|
||||
// Get Steam API key from configuration
|
||||
var apiKey = configuration["Oidc:Steam:ApiKey"];
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
logger.LogWarning("Steam API key not configured, skipping presence update for {Count} users", steamConnections.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create Steam Web API client
|
||||
var webInterfaceFactory = new SteamWebInterfaceFactory(apiKey);
|
||||
var steamUserInterface = webInterfaceFactory.CreateSteamWebInterface<SteamUser>();
|
||||
|
||||
// Collect all Steam IDs for batch request
|
||||
var steamIds = steamConnections
|
||||
.Select(c => ulong.Parse(c.ProvidedIdentifier))
|
||||
.ToList();
|
||||
|
||||
// Make batch API call (Steam supports up to 100 IDs per request)
|
||||
var playerSummariesResponse = await steamUserInterface.GetPlayerSummariesAsync(steamIds);
|
||||
var playerSummaries = playerSummariesResponse?.Data != null
|
||||
? (IEnumerable<dynamic>)playerSummariesResponse.Data
|
||||
: new List<dynamic>();
|
||||
|
||||
// Create a lookup dictionary for quick access
|
||||
var playerSummaryLookup = playerSummaries
|
||||
.ToDictionary(ps => ((dynamic)ps).SteamId.ToString(), ps => ps);
|
||||
|
||||
// Process each connection
|
||||
foreach (var connection in steamConnections)
|
||||
{
|
||||
if (playerSummaryLookup.TryGetValue(connection.ProvidedIdentifier, out var playerSummaryData))
|
||||
{
|
||||
await UpdateSteamPresenceFromDataAsync(connection.Account, playerSummaryData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No data for this user, remove any existing presence
|
||||
await RemoveSteamPresenceAsync(connection.Account.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update Steam presence for {Count} users", steamConnections.Count);
|
||||
|
||||
// On batch error, fall back to individual calls to avoid losing all presence data
|
||||
foreach (var connection in steamConnections)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UpdateSteamPresenceAsync(connection.Account, connection.ProvidedIdentifier);
|
||||
}
|
||||
catch (Exception individualEx)
|
||||
{
|
||||
logger.LogError(individualEx, "Failed to update Steam presence for user {UserId}", connection.Account.Id);
|
||||
await RemoveSteamPresenceAsync(connection.Account.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Steam presence activity for a specific user using pre-fetched player summary data
|
||||
/// </summary>
|
||||
private async Task UpdateSteamPresenceFromDataAsync(SnAccount account, dynamic playerSummaryData)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(playerSummaryData.PlayingGameId) && !string.IsNullOrEmpty(playerSummaryData.PlayingGameName))
|
||||
{
|
||||
// User is playing a game
|
||||
var presenceActivity = ParsePlayerSummaryToPresenceActivity(account.Id, playerSummaryData);
|
||||
|
||||
// Try to update existing activity first
|
||||
var updatedActivity = await accountEventService.UpdateActivityByManualId(
|
||||
"steam",
|
||||
account.Id,
|
||||
UpdateActivityWithPresenceData,
|
||||
5
|
||||
);
|
||||
|
||||
// If update failed (no existing activity), create a new one
|
||||
if (updatedActivity == null)
|
||||
await accountEventService.SetActivity(presenceActivity, 5);
|
||||
|
||||
// Local function to avoid capturing external variables in lambda
|
||||
void UpdateActivityWithPresenceData(SnPresenceActivity activity)
|
||||
{
|
||||
activity.Type = PresenceType.Gaming;
|
||||
activity.Title = presenceActivity.Title;
|
||||
activity.Subtitle = presenceActivity.Subtitle;
|
||||
activity.Caption = presenceActivity.Caption;
|
||||
activity.LargeImage = presenceActivity.LargeImage;
|
||||
activity.SmallImage = presenceActivity.SmallImage;
|
||||
activity.TitleUrl = presenceActivity.TitleUrl;
|
||||
activity.SubtitleUrl = presenceActivity.SubtitleUrl;
|
||||
activity.Meta = presenceActivity.Meta;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// User is not playing a game, remove any existing Steam presence
|
||||
await RemoveSteamPresenceAsync(account.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Steam presence activity for a specific user (fallback individual API call)
|
||||
/// </summary>
|
||||
private async Task UpdateSteamPresenceAsync(SnAccount account, string steamId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get Steam API key from configuration
|
||||
var apiKey = configuration["Oidc:Steam:ApiKey"];
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
logger.LogWarning("Steam API key not configured, skipping presence update for user {UserId}", account.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Steam Web API client
|
||||
var webInterfaceFactory = new SteamWebInterfaceFactory(apiKey);
|
||||
var steamUserInterface = webInterfaceFactory.CreateSteamWebInterface<SteamUser>();
|
||||
|
||||
// Get player summary
|
||||
var playerSummaryResponse = await steamUserInterface.GetPlayerSummaryAsync(ulong.Parse(steamId));
|
||||
var playerSummaryData = playerSummaryResponse.Data;
|
||||
|
||||
if (!string.IsNullOrEmpty(playerSummaryData.PlayingGameId) && !string.IsNullOrEmpty(playerSummaryData.PlayingGameName))
|
||||
{
|
||||
// User is playing a game
|
||||
var presenceActivity = ParsePlayerSummaryToPresenceActivity(account.Id, playerSummaryData);
|
||||
|
||||
// Try to update existing activity first
|
||||
var updatedActivity = await accountEventService.UpdateActivityByManualId(
|
||||
"steam",
|
||||
account.Id,
|
||||
UpdateActivityWithPresenceData,
|
||||
5
|
||||
);
|
||||
|
||||
// If update failed (no existing activity), create a new one
|
||||
if (updatedActivity == null)
|
||||
await accountEventService.SetActivity(presenceActivity, 5);
|
||||
|
||||
// Local function to avoid capturing external variables in lambda
|
||||
void UpdateActivityWithPresenceData(SnPresenceActivity activity)
|
||||
{
|
||||
activity.Type = PresenceType.Gaming;
|
||||
activity.Title = presenceActivity.Title;
|
||||
activity.Subtitle = presenceActivity.Subtitle;
|
||||
activity.Caption = presenceActivity.Caption;
|
||||
activity.LargeImage = presenceActivity.LargeImage;
|
||||
activity.SmallImage = presenceActivity.SmallImage;
|
||||
activity.TitleUrl = presenceActivity.TitleUrl;
|
||||
activity.SubtitleUrl = presenceActivity.SubtitleUrl;
|
||||
activity.Meta = presenceActivity.Meta;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// User is not playing a game, remove any existing Steam presence
|
||||
await RemoveSteamPresenceAsync(account.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// On error, remove the presence to avoid stale data
|
||||
await RemoveSteamPresenceAsync(account.Id);
|
||||
|
||||
logger.LogError(ex, "Failed to update Steam presence for user {UserId}", account.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Steam presence activity for a user
|
||||
/// </summary>
|
||||
private async Task RemoveSteamPresenceAsync(Guid accountId)
|
||||
{
|
||||
await accountEventService.UpdateActivityByManualId(
|
||||
"steam",
|
||||
accountId,
|
||||
activity =>
|
||||
{
|
||||
// Mark it for immediate expiration
|
||||
activity.LeaseExpiresAt = SystemClock.Instance.GetCurrentInstant();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static SnPresenceActivity ParsePlayerSummaryToPresenceActivity(Guid accountId, dynamic playerSummary)
|
||||
{
|
||||
var gameName = playerSummary.PlayingGameName ?? "Unknown Game";
|
||||
var gameId = playerSummary.PlayingGameId?.ToString() ?? "";
|
||||
|
||||
// Build metadata
|
||||
var meta = new Dictionary<string, object>
|
||||
{
|
||||
["game_id"] = gameId,
|
||||
["steam_profile_url"] = $"https://steamcommunity.com/profiles/{playerSummary.SteamId}",
|
||||
["updated_at"] = SystemClock.Instance.GetCurrentInstant()
|
||||
};
|
||||
|
||||
return new SnPresenceActivity
|
||||
{
|
||||
AccountId = accountId,
|
||||
Type = PresenceType.Gaming,
|
||||
ManualId = "steam",
|
||||
Title = gameName,
|
||||
Subtitle = "Playing on Steam",
|
||||
Caption = null, // Could be game details if available
|
||||
LargeImage = null, // Could fetch game icon from Steam API if needed
|
||||
SmallImage = null,
|
||||
TitleUrl = $"https://store.steampowered.com/app/{gameId}",
|
||||
SubtitleUrl = $"https://steamcommunity.com/profiles/{playerSummary.SteamId}",
|
||||
Meta = meta
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,9 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<LotteryService>();
|
||||
|
||||
services.AddScoped<SpotifyPresenceService>();
|
||||
services.AddScoped<SteamPresenceService>();
|
||||
services.AddScoped<IPresenceService, SpotifyPresenceService>();
|
||||
services.AddScoped<IPresenceService, SteamPresenceService>();
|
||||
|
||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||
services.AddScoped<OidcProviderService>();
|
||||
|
||||
Reference in New Issue
Block a user