-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add gamereplays tab with active/upcoming tournament tracking #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| namespace GenHub.Core.Constants; | ||
|
|
||
| /// <summary> | ||
| /// Constants for GameReplays integration. | ||
| /// </summary> | ||
| public static class GameReplaysConstants | ||
| { | ||
| /// <summary> | ||
| /// Base URL for GameReplays website. | ||
| /// </summary> | ||
| public const string BaseUrl = "https://www.gamereplays.org"; | ||
|
|
||
| /// <summary> | ||
| /// Tournament board topic ID. | ||
| /// </summary> | ||
| public const string TournamentBoardTopicId = "1066376"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 authorization endpoint. | ||
| /// </summary> | ||
| public const string OAuthAuthorizationEndpoint = "https://www.gamereplays.org/oauth/authorize"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 token endpoint. | ||
| /// </summary> | ||
| public const string OAuthTokenEndpoint = "https://www.gamereplays.org/oauth/token"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 resource endpoint. | ||
| /// </summary> | ||
| public const string OAuthResourceEndpoint = "https://www.gamereplays.org/oauth/resource"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 client ID. | ||
| /// TODO: Replace with actual client ID from GameReplays. | ||
| /// </summary> | ||
| public const string OAuthClientId = "genhub_client"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 client secret. | ||
| /// TODO: Replace with actual client secret from GameReplays. | ||
| /// </summary> | ||
| public const string OAuthClientSecret = "genhub_secret"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 redirect URI for callback handling. | ||
| /// Uses custom URI scheme for desktop application. | ||
| /// </summary> | ||
| public const string OAuthRedirectUri = "genhub://oauth/callback"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 scope for user profile access. | ||
| /// </summary> | ||
| public const string OAuthScope = "user_profile"; | ||
|
|
||
| /// <summary> | ||
| /// OAuth state validity period in minutes. | ||
| /// </summary> | ||
| public const int OAuthStateValidityMinutes = 10; | ||
|
|
||
| /// <summary> | ||
| /// Cache duration for tournament data in minutes. | ||
| /// </summary> | ||
| public const int CacheDurationMinutes = 5; | ||
|
|
||
| /// <summary> | ||
| /// Default request timeout in seconds. | ||
| /// </summary> | ||
| public const int DefaultRequestTimeoutSeconds = 30; | ||
|
|
||
| /// <summary> | ||
| /// Maximum retry attempts for failed requests. | ||
| /// </summary> | ||
| public const int MaxRetryAttempts = 3; | ||
|
|
||
| /// <summary> | ||
| /// Delay between retry attempts in milliseconds. | ||
| /// </summary> | ||
| public const int RetryDelayMs = 1000; | ||
|
|
||
| /// <summary> | ||
| /// Forum ID for Zero Hour General Discussion. | ||
| /// </summary> | ||
| public const string ZeroHourGeneralDiscussionForumId = "35"; | ||
|
|
||
| /// <summary> | ||
| /// UI colors for tournament statuses. | ||
| /// </summary> | ||
| public static class Colors | ||
|
undead2146 marked this conversation as resolved.
|
||
| { | ||
| /// <summary>Signups are currently open.</summary> | ||
| public const string SignupsOpen = "#FF9800"; // Orange | ||
|
|
||
| /// <summary>Tournament is upcoming.</summary> | ||
| public const string Upcoming = "#F44336"; // Red | ||
|
|
||
| /// <summary>Tournament is currently active.</summary> | ||
| public const string Active = "#4CAF50"; // Green | ||
|
|
||
| /// <summary>Tournament has finished.</summary> | ||
| public const string Finished = "#9C27B0"; // Purple | ||
|
|
||
| /// <summary>Default status color.</summary> | ||
| public const string Default = "#757575"; // Gray | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||
| using GenHub.Core.Interfaces.GameReplays; | ||||||||||
| using GenHub.Core.Services.GameReplays; | ||||||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||||||
|
|
||||||||||
| namespace GenHub.Infrastructure.DependencyInjection; | ||||||||||
|
|
||||||||||
| /// <summary> | ||||||||||
| /// Dependency injection module for GameReplays services. | ||||||||||
| /// Registers all GameReplays-related services with appropriate lifetimes. | ||||||||||
| /// </summary> | ||||||||||
| public static class GameReplaysModule | ||||||||||
| { | ||||||||||
| /// <summary> | ||||||||||
| /// Registers GameReplays services with service collection. | ||||||||||
| /// </summary> | ||||||||||
| /// <param name="services">The service collection to register services with.</param> | ||||||||||
| public static void RegisterGameReplaysServices(this IServiceCollection services) | ||||||||||
| { | ||||||||||
| // Register HTTP client as transient (creates new instance each time) | ||||||||||
| services.AddTransient<IGameReplaysHttpClient, GameReplaysHttpClient>(); | ||||||||||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. registering Prompt To Fix With AIThis is a comment left during a code review.
Path: GenHub/GenHub.Core/Infrastructure/DependencyInjection/GameReplaysModule.cs
Line: 19:20
Comment:
registering `HttpClient` as transient can cause socket exhaustion - use `IHttpClientFactory` instead with `AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>()`
How can I resolve this? If you propose a fix, please make it concise.
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. transient
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: GenHub/GenHub.Core/Infrastructure/DependencyInjection/GameReplaysModule.cs
Line: 19:20
Comment:
transient `HttpClient` registration creates new instances on each request, leading to socket exhaustion under load
```suggestion
// Use IHttpClientFactory to manage HttpClient lifecycle properly
services.AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>();
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||
|
|
||||||||||
| // Register parser as transient (stateless, can be created each time) | ||||||||||
| services.AddTransient<IGameReplaysParser, GameReplaysParser>(); | ||||||||||
|
|
||||||||||
| // Register auth service as singleton (maintains OAuth state across app) | ||||||||||
| services.AddSingleton<IGameReplaysAuthService, GameReplaysAuthService>(); | ||||||||||
|
|
||||||||||
| // Register comment service as transient | ||||||||||
| services.AddTransient<IGameReplaysCommentService, GameReplaysCommentService>(); | ||||||||||
|
|
||||||||||
| // Register main service as scoped (one instance per request/scope) | ||||||||||
| services.AddScoped<IGameReplaysService, GameReplaysService>(); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| using GenHub.Core.Models.GameReplays; | ||
| using GenHub.Core.Models.Results; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace GenHub.Core.Interfaces.GameReplays; | ||
|
|
||
| /// <summary> | ||
| /// OAuth 2.0 authentication service for GameReplays. | ||
| /// Handles login flow, token management, and user session. | ||
| /// </summary> | ||
| public interface IGameReplaysAuthService | ||
| { | ||
| /// <summary> | ||
| /// Gets the authorization URL for OAuth 2.0 login. | ||
| /// </summary> | ||
| /// <returns>Result containing the authorization URL.</returns> | ||
| OperationResult<string> GetAuthorizationUrl(); | ||
|
|
||
| /// <summary> | ||
| /// Exchanges authorization code for access token. | ||
| /// </summary> | ||
| /// <param name="code">The authorization code from callback.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing the OAuth token response.</returns> | ||
| Task<OperationResult<OAuthTokenResponse>> ExchangeCodeForTokenAsync( | ||
| string code, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Refreshes the access token using refresh token. | ||
| /// </summary> | ||
| /// <param name="refreshToken">The refresh token.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing the new OAuth token response.</returns> | ||
| Task<OperationResult<OAuthTokenResponse>> RefreshTokenAsync( | ||
| string refreshToken, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Gets user info using the access token. | ||
| /// </summary> | ||
| /// <param name="accessToken">The access token.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing the user info.</returns> | ||
| Task<OperationResult<OAuthUserInfo>> GetUserInfoAsync( | ||
| string accessToken, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Validates the OAuth state parameter for CSRF protection. | ||
| /// </summary> | ||
| /// <param name="state">The state parameter from callback.</param> | ||
| /// <returns>True if state is valid, false otherwise.</returns> | ||
| bool ValidateState(string state); | ||
|
|
||
| /// <summary> | ||
| /// Generates a new OAuth state parameter. | ||
| /// </summary> | ||
| /// <returns>The generated state string.</returns> | ||
| string GenerateState(); | ||
|
|
||
| /// <summary> | ||
| /// Saves the OAuth token response to secure storage. | ||
| /// </summary> | ||
| /// <param name="tokenResponse">The token response to save.</param> | ||
| /// <returns>Result indicating success or failure.</returns> | ||
| Task<OperationResult<bool>> SaveTokenAsync(OAuthTokenResponse tokenResponse); | ||
|
|
||
| /// <summary> | ||
| /// Loads the OAuth token response from secure storage. | ||
| /// </summary> | ||
| /// <returns>Result containing the token response or null if not found.</returns> | ||
| Task<OperationResult<OAuthTokenResponse?>> LoadTokenAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Clears the saved OAuth token from secure storage. | ||
| /// </summary> | ||
| /// <returns>Result indicating success or failure.</returns> | ||
| Task<OperationResult<bool>> ClearTokenAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Checks if the current token is expired or needs refresh. | ||
| /// </summary> | ||
| /// <returns>True if token is expired, false otherwise.</returns> | ||
| bool IsTokenExpired(); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current access token. | ||
| /// </summary> | ||
| /// <returns>The access token or null if not authenticated.</returns> | ||
| string? GetAccessToken(); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current refresh token. | ||
| /// </summary> | ||
| /// <returns>The refresh token or null if not available.</returns> | ||
| string? GetRefreshToken(); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current authenticated user info. | ||
| /// </summary> | ||
| /// <returns>The user info or null if not authenticated.</returns> | ||
| OAuthUserInfo? GetCurrentUser(); | ||
|
|
||
| /// <summary> | ||
| /// Sets the current user info. | ||
| /// </summary> | ||
| /// <param name="userInfo">The user info to set.</param> | ||
| void SetCurrentUser(OAuthUserInfo userInfo); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| using GenHub.Core.Models.GameReplays; | ||
| using GenHub.Core.Models.Results; | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace GenHub.Core.Interfaces.GameReplays; | ||
|
|
||
| /// <summary> | ||
| /// Service for posting comments to GameReplays forum topics. | ||
| /// </summary> | ||
| public interface IGameReplaysCommentService | ||
| { | ||
| /// <summary> | ||
| /// Posts a comment to a forum topic. | ||
| /// </summary> | ||
| /// <param name="topicId">The tournament topic ID.</param> | ||
| /// <param name="comment">The comment content.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result indicating success or failure.</returns> | ||
| Task<OperationResult<bool>> PostCommentAsync( | ||
| string topicId, | ||
| string comment, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Gets comments for a forum topic. | ||
| /// TODO: Implement when GameReplays provides comments API. | ||
| /// </summary> | ||
| /// <param name="topicId">The tournament topic ID.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing comment collection.</returns> | ||
| Task<OperationResult<IEnumerable<Comment>>> GetCommentsAsync( | ||
| string topicId, | ||
| CancellationToken cancellationToken = default); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| using GenHub.Core.Models.GameReplays; | ||
| using GenHub.Core.Models.Results; | ||
| using System.Collections.Generic; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace GenHub.Core.Interfaces.GameReplays; | ||
|
|
||
| /// <summary> | ||
| /// HTTP client interface for GameReplays API and web scraping. | ||
| /// Handles cookie management and HTTP requests. | ||
| /// </summary> | ||
| public interface IGameReplaysHttpClient | ||
| { | ||
| /// <summary> | ||
| /// Gets HTML content from a URL. | ||
| /// </summary> | ||
| /// <param name="url">The URL to fetch.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing HTML content.</returns> | ||
| Task<OperationResult<string>> GetHtmlAsync( | ||
| string url, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Posts form data to a URL. | ||
| /// </summary> | ||
| /// <param name="url">The URL to post to.</param> | ||
| /// <param name="formData">The form data to post.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing response content.</returns> | ||
| Task<OperationResult<string>> PostFormAsync( | ||
| string url, | ||
| Dictionary<string, string> formData, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Gets JSON content from an API endpoint. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type to deserialize to.</typeparam> | ||
| /// <param name="url">The API endpoint URL.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing deserialized data.</returns> | ||
| Task<OperationResult<T>> GetJsonAsync<T>( | ||
| string url, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Posts JSON data to an API endpoint. | ||
| /// </summary> | ||
| /// <typeparam name="TRequest">The request type.</typeparam> | ||
| /// <typeparam name="TResponse">The response type.</typeparam> | ||
| /// <param name="url">The API endpoint URL.</param> | ||
| /// <param name="data">The data to post.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Result containing deserialized response.</returns> | ||
| Task<OperationResult<TResponse>> PostJsonAsync<TRequest, TResponse>( | ||
| string url, | ||
| TRequest data, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Sets authentication cookie for requests. | ||
| /// </summary> | ||
| /// <param name="cookieValue">The cookie value.</param> | ||
| void SetAuthCookie(string cookieValue); | ||
|
|
||
| /// <summary> | ||
| /// Clears authentication cookie. | ||
| /// </summary> | ||
| void ClearAuthCookie(); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current authentication cookie value. | ||
| /// </summary> | ||
| /// <returns>The cookie value or null if not set.</returns> | ||
| string? GetAuthCookie(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hardcoded OAuth credentials (
genhub_clientandgenhub_secret) are a security risk - these placeholder values should be replaced with actual credentials from environment variables or secure configuration before this code reaches productionPrompt To Fix With AI