aboutsummaryrefslogtreecommitdiff
path: root/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2024-11-11 01:12:29 +0800
committerYuqian Yang <crupest@crupest.life>2024-12-19 21:42:01 +0800
commitf9aa02ec1a4c24e80a206857d4f68198bb027bb4 (patch)
tree5994f0a62733b13f9f330e3515260ae20dc4a0bd /dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos
parent7b4d49e4bbdff6ddf1f8f7e937130e700024d5e9 (diff)
downloadcrupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.tar.gz
crupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.tar.bz2
crupest-f9aa02ec1a4c24e80a206857d4f68198bb027bb4.zip
HALF WORK: 2024.12.19
Re-organize file structure.
Diffstat (limited to 'dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos')
-rw-r--r--dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj15
-rw-r--r--dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs14
-rw-r--r--dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs163
-rw-r--r--dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs21
-rw-r--r--dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs32
5 files changed, 245 insertions, 0 deletions
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj
new file mode 100644
index 0000000..86460e3
--- /dev/null
+++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/CrupestApi.Todos.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <ItemGroup>
+ <ProjectReference Include="..\CrupestApi.Commons\CrupestApi.Commons.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <TargetType>library</TargetType>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <SelfContained>false</SelfContained>
+ </PropertyGroup>
+
+</Project>
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs
new file mode 100644
index 0000000..e8160d2
--- /dev/null
+++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosConfiguration.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CrupestApi.Todos;
+
+public class TodosConfiguration
+{
+ [Required]
+ public string Username { get; set; } = default!;
+ [Required]
+ public int ProjectNumber { get; set; } = default!;
+ [Required]
+ public string Token { get; set; } = default!;
+ public int Count { get; set; }
+} \ No newline at end of file
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs
new file mode 100644
index 0000000..5839086
--- /dev/null
+++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosService.cs
@@ -0,0 +1,163 @@
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using Microsoft.Extensions.Options;
+
+namespace CrupestApi.Todos;
+
+public class TodosItem
+{
+ public string Status { get; set; } = default!;
+ public string Title { get; set; } = default!;
+ public bool Closed { get; set; }
+ public string Color { get; set; } = default!;
+}
+
+public class TodosService
+{
+ private readonly IOptionsSnapshot<TodosConfiguration> _options;
+ private readonly ILogger<TodosService> _logger;
+
+ public TodosService(IOptionsSnapshot<TodosConfiguration> options, ILogger<TodosService> logger)
+ {
+ _options = options;
+ _logger = logger;
+ }
+
+ private static string CreateGraphQLQuery(TodosConfiguration todoConfiguration)
+ {
+ return $$"""
+{
+ user(login: "{{todoConfiguration.Username}}") {
+ projectV2(number: {{todoConfiguration.ProjectNumber}}) {
+ items(last: {{todoConfiguration.Count}}) {
+ nodes {
+ fieldValueByName(name: "Status") {
+ ... on ProjectV2ItemFieldSingleSelectValue {
+ name
+ }
+ }
+ content {
+ __typename
+ ... on Issue {
+ title
+ closed
+ }
+ ... on PullRequest {
+ title
+ closed
+ }
+ ... on DraftIssue {
+ title
+ }
+ }
+ }
+ }
+ }
+ }
+}
+""";
+ }
+
+
+ public async Task<List<TodosItem>> GetTodosAsync()
+ {
+ var todoOptions = _options.Value;
+ if (todoOptions is null)
+ {
+ throw new Exception("Fail to get todos configuration.");
+ }
+
+ _logger.LogInformation("Username: {}; ProjectNumber: {}; Count: {}", todoOptions.Username, todoOptions.ProjectNumber, todoOptions.Count);
+ _logger.LogInformation("Getting todos from GitHub GraphQL API...");
+
+ using var httpClient = new HttpClient();
+
+ using var requestContent = new StringContent(JsonSerializer.Serialize(new
+ {
+ query = CreateGraphQLQuery(todoOptions)
+ }));
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json, Encoding.UTF8.WebName);
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.github.com/graphql");
+ request.Content = requestContent;
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", todoOptions.Token);
+ request.Headers.TryAddWithoutValidation("User-Agent", todoOptions.Username);
+
+ using var response = await httpClient.SendAsync(request);
+ var responseBody = await response.Content.ReadAsStringAsync();
+
+ _logger.LogInformation("GitHub server returned status code: {}", response.StatusCode);
+ _logger.LogInformation("GitHub server returned body: {}", responseBody);
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var responseJson = JsonSerializer.Deserialize<JsonDocument>(responseBody);
+ if (responseJson is null)
+ {
+ throw new Exception("Fail to deserialize response body.");
+ }
+
+ var nodes = responseJson.RootElement.GetProperty("data").GetProperty("user").GetProperty("projectV2").GetProperty("items").GetProperty("nodes").EnumerateArray();
+
+ var result = new List<TodosItem>();
+
+ foreach (var node in nodes)
+ {
+ var content = node.GetProperty("content");
+ var title = content.GetProperty("title").GetString();
+ if (title is null)
+ {
+ throw new Exception("Fail to get title.");
+ }
+
+ bool done = false;
+
+ var statusField = node.GetProperty("fieldValueByName");
+ if (statusField.ValueKind != JsonValueKind.Null) // if there is a "Status" field
+ {
+ var statusName = statusField.GetProperty("name").GetString();
+ if (statusName is null)
+ {
+ throw new Exception("Fail to get status.");
+ }
+
+ // if name is "Done", then it is closed, otherwise we check if the issue is closed
+ if (statusName.Equals("Done", StringComparison.OrdinalIgnoreCase))
+ {
+ done = true;
+ }
+ }
+
+ JsonElement closedElement;
+ // if item has a "closed" field, then it is a pull request or an issue, and we check if it is closed
+ if (content.TryGetProperty("closed", out closedElement) && closedElement.GetBoolean())
+ {
+ done = true;
+ }
+
+ // If item "Status" field is "Done' or item is a pull request or issue and it is closed, then it is done.
+ // Otherwise it is not closed. Like:
+ // 1. it is a draft issue with no "Status" field or "Status" field is not "Done"
+ // 2. it is a pull request or issue with no "Status" field or "Status" field is not "Done" and it is not closed
+
+ result.Add(new TodosItem
+ {
+ Title = title,
+ Status = done ? "Done" : "Todo",
+ Closed = done,
+ Color = done ? "green" : "blue"
+ });
+ }
+
+ return result;
+ }
+ else
+ {
+ const string message = "Fail to get todos from GitHub.";
+ _logger.LogError(message);
+ throw new Exception(message);
+ }
+ }
+}
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a49d55d
--- /dev/null
+++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosServiceCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace CrupestApi.Todos;
+
+public static class TodosServiceCollectionExtensions
+{
+ public static IServiceCollection AddTodos(this IServiceCollection services)
+ {
+ services.AddOptions<TodosConfiguration>().BindConfiguration("CrupestApi:Todos");
+ services.PostConfigure<TodosConfiguration>(config =>
+ {
+ if (config.Count == 0)
+ {
+ config.Count = 20;
+ }
+ });
+ services.TryAddScoped<TodosService>();
+ return services;
+ }
+}
+
diff --git a/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs
new file mode 100644
index 0000000..0ff05a0
--- /dev/null
+++ b/dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos/TodosWebApplicationExtensions.cs
@@ -0,0 +1,32 @@
+using CrupestApi.Commons;
+
+namespace CrupestApi.Todos;
+
+public static class TodosWebApplicationExtensions
+{
+ public static WebApplication MapTodos(this WebApplication app, string path)
+ {
+ if (app is null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ app.MapGet(path, async (context) =>
+ {
+ var todosService = context.RequestServices.GetRequiredService<TodosService>();
+
+ try
+ {
+ var todos = await todosService.GetTodosAsync();
+ await context.Response.WriteJsonAsync(todos);
+
+ }
+ catch (Exception e)
+ {
+ await context.Response.WriteMessageAsync(e.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
+ }
+ });
+
+ return app;
+ }
+} \ No newline at end of file