Modularize project structure by splitting responsibilities into distinct files and namespaces. Add helper methods for authentication and JSON operations.

This commit is contained in:
Andre Beging
2025-12-11 16:41:22 +01:00
parent 63bcdf003d
commit 67260ae450
12 changed files with 333 additions and 211 deletions

View File

@@ -0,0 +1,27 @@
namespace FsTool.Tasks
{
public partial class RegionTasks
{
/// <summary>
/// Basic region store information including cooperation status.
/// </summary>
/// <param name="Id">The store identifier.</param>
/// <param name="Name">The store name.</param>
/// <param name="CooperationStatus">The cooperation status of the store.</param>
public record Store(int Id, string Name, CooperationStatus CooperationStatus);
/// <summary>
/// Describes the cooperation state between the store and the organization.
/// </summary>
public enum CooperationStatus
{
NoStatus = 0,
NoContact = 1,
Negotiating = 2,
DoNotWant = 4,
Cooperating = 5,
DonatingToTafel = 6,
NoExisting = 7,
}
}
}

47
Tasks/RegionTasks.cs Normal file
View File

@@ -0,0 +1,47 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using FsTool;
using FsTool.Helpers;
namespace FsTool.Tasks
{
public partial class RegionTasks
{
/// <summary>
/// Retrieves all stores within the specified region, ensuring the request is authenticated.
/// </summary>
/// <param name="httpClient">The HTTP client used to perform the request.</param>
/// <param name="regionId">The region identifier to query.</param>
/// <returns>A list of stores, or an empty list when the call fails or returns no data.</returns>
public static async Task<List<Store>> GetStoresInRegionAsync(HttpClient httpClient, int regionId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.RegionStores, regionId);
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
await Console.Error.WriteLineAsync($"Region stores retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
return [];
}
Console.WriteLine($"Stores in region {regionId}:");
Console.WriteLine(responseBody);
var root = JsonNode.Parse(responseBody);
if (root == null) return [];
// Deserialize JsonNode to List<Pickup>
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null // <── WICHTIG
};
return root["stores"].Deserialize<List<Store>>(opts) ?? [];
}
}
}

View File

@@ -0,0 +1,53 @@
namespace FsTool.Tasks
{
public static partial class StoreTasks
{
/// <summary>
/// Represents a pickup date and the occupied slots for that date.
/// </summary>
/// <param name="Date">The pickup date string.</param>
/// <param name="OccupiedSlots">The slots already assigned for the date.</param>
public record Pickup(string Date, List<Slot> OccupiedSlots);
/// <summary>
/// Describes a booked slot and the profile occupying it.
/// </summary>
/// <param name="IsConfirmed">Indicates whether the slot is confirmed.</param>
/// <param name="Profile">The profile assigned to the slot.</param>
public record Slot(bool IsConfirmed, Profile Profile);
/// <summary>
/// Minimal profile information for a store member.
/// </summary>
/// <param name="Id">The Foodsharing profile identifier.</param>
/// <param name="Name">The profile display name.</param>
public record Profile(int Id, string Name);
/// <summary>
/// Detailed store member information including verification and team status flags.
/// </summary>
/// <param name="Id">The member identifier.</param>
/// <param name="Name">The member name.</param>
/// <param name="Team_Active">The member's active status within the team.</param>
/// <param name="Verified">The verification state of the member.</param>
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
/// <summary>
/// Indicates whether a team member is active or only available as a jumper.
/// </summary>
public enum TeamActiveStatus
{
Jumper = 2,
Active = 1,
}
/// <summary>
/// Specifies whether a member has completed verification.
/// </summary>
public enum VerifiedStatus
{
Unverified = 0,
Verified = 1,
}
}
}

106
Tasks/StoreTasks.cs Normal file
View File

@@ -0,0 +1,106 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using FsTool;
using FsTool.Helpers;
namespace FsTool.Tasks
{
public static partial class StoreTasks
{
/// <summary>
/// Retrieves the members of the specified store, requiring authentication before the request.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier to query.</param>
/// <returns>A list of store members, or an empty list when the call fails.</returns>
public static async Task<List<Member>> GetStoreMembersAsync(HttpClient httpClient, int storeId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.StoreMembers, storeId);
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
await Console.Error.WriteLineAsync($"Store members retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
return [];
}
var root = JsonNode.Parse(responseBody);
if (root == null) return [];
// Deserialize JsonNode to List<Pickup>
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null // <── WICHTIG
};
return root.Deserialize<List<Member>>(opts) ?? [];
}
/// <summary>
/// Retrieves all pickups for the specified store with authentication handled automatically.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier to query.</param>
/// <returns>A list of pickups, or an empty list when no data is available.</returns>
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.StorePickups, storeId);
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
await Console.Error.WriteLineAsync($"Pickup retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
}
var root = JsonNode.Parse(responseBody);
if (root == null) return [];
// Deserialize JsonNode to List<Pickup>
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null // <── WICHTIG
};
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
}
/// <summary>
/// Marks a pickup slot as confirmed for the specified store, date, and user.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier containing the pickup.</param>
/// <param name="pickupDate">The date of the pickup to patch.</param>
/// <param name="fsId">The Foodsharing user identifier associated with the slot.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task PatchPickupAsync(HttpClient httpClient, int storeId, string pickupDate, int fsId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.StorePickupsSlot, storeId, pickupDate, fsId);
var payload = new JsonObject
{
["isConfirmed"] = true
};
var response = await httpClient.PatchAsync(uri, JsonContent.Create(payload));
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
await Console.Error.WriteLineAsync($"Pickup patch failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
else
Console.WriteLine($"Pickup patch succeeded {fsId} on {pickupDate}");
}
}
}

92
Tasks/UserTasks.cs Normal file
View File

@@ -0,0 +1,92 @@
using System;
using System.Linq;
using System.Net.Http.Json;
using FsTool;
using FsTool.Helpers;
namespace FsTool.Tasks
{
public static class UserTasks
{
/// <summary>
/// Performs a login request using two-factor authentication and returns the CSRF token from the response.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <returns>The CSRF token when login succeeds; otherwise, <c>null</c>.</returns>
public static async Task<string?> CallLoginEndpointAsync(HttpClient httpClient)
{
string? csrfToken = null;
// prompt for 2FA code
Console.Write("Enter 2FA code: ");
var authCode = Console.ReadLine()?.Trim();
// validate input
if (string.IsNullOrWhiteSpace(authCode))
{
Console.WriteLine("A valid 2FA code is required.");
return null;
}
var payload = new
{
email = "foodsharing@beging.de",
password = "z+hc@Ox9Zu4~MXzkB:Z@O.-S1AvsT&mc!oyQA?NK1jckl1Dzi2^-)+.H.AJKBLoi",
code = authCode,
remember_me = true
};
var response = await httpClient.PostAsJsonAsync(Endpoints.UserLogin, payload);
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
await Console.Error.WriteLineAsync($"Login failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
return null;
}
// Read headers as dictionary
var headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value));
if (headers.TryGetValue("Set-Cookie", out var setCookieHeader))
{
// Split cookies by comma and semicolon
var cookies = setCookieHeader.Split([";"], StringSplitOptions.RemoveEmptyEntries);
var csrfTokenEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("FS_CSRF_TOKEN="));
csrfToken = csrfTokenEntry?.Split('=')[1];
if (string.IsNullOrWhiteSpace(csrfToken)) return null;
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
var expireString = expiryEntry?.Split('=')[1];
if (DateTime.TryParse(expireString, out var expiration))
await AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration);
}
return csrfToken;
}
/// <summary>
/// Retrieves information about the current authenticated user.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public static async Task GetCurrentUserAsync(HttpClient httpClient)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var response = await httpClient.GetAsync(Endpoints.UserCurrent);
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
await Console.Error.WriteLineAsync($"Get current user failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
return;
}
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine("Current User Info:");
Console.WriteLine(content);
}
}
}