diff options
author | crupest <crupest@outlook.com> | 2024-11-11 01:12:29 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2024-12-19 21:42:01 +0800 |
commit | f9aa02ec1a4c24e80a206857d4f68198bb027bb4 (patch) | |
tree | 5994f0a62733b13f9f330e3515260ae20dc4a0bd /dropped/docker/crupest-api/CrupestApi/CrupestApi.Todos | |
parent | 7b4d49e4bbdff6ddf1f8f7e937130e700024d5e9 (diff) | |
download | crupest-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')
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 |