From 93e844c0c8e79c45d42cf55d8742aef812fbdfa3 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Tue, 17 Sep 2024 14:11:39 +0800 Subject: [PATCH 1/2] Migrate to `GitHub.Octokit.Client` --- LoongsonNeuq.Classroom/Program.cs | 54 ++--------- LoongsonNeuq.Classroom/StudentsTable.cs | 82 +++++++++------- .../DependencyInjectionExtensions.cs | 52 ++++++++++ .../GitHub/AcceptedAssignment.cs | 29 ------ LoongsonNeuq.Common/GitHub/Assignment.cs | 61 ------------ LoongsonNeuq.Common/GitHub/Classroom.cs | 57 ----------- .../GitHub/ContributionStat.cs | 17 +++- LoongsonNeuq.Common/GitHub/GitHubApi.cs | 95 ------------------- LoongsonNeuq.Common/GitHub/Student.cs | 33 ------- .../LoongsonNeuq.Common.csproj | 2 +- .../TestGitHubChecker.cs | 11 ++- LoongsonNeuq.ListFormatter/GitHubIDChecker.cs | 25 ++--- LoongsonNeuq.ListFormatter/Program.cs | 12 +-- 13 files changed, 143 insertions(+), 387 deletions(-) create mode 100644 LoongsonNeuq.Common/DependencyInjectionExtensions.cs delete mode 100644 LoongsonNeuq.Common/GitHub/AcceptedAssignment.cs delete mode 100644 LoongsonNeuq.Common/GitHub/Assignment.cs delete mode 100644 LoongsonNeuq.Common/GitHub/Classroom.cs delete mode 100644 LoongsonNeuq.Common/GitHub/GitHubApi.cs delete mode 100644 LoongsonNeuq.Common/GitHub/Student.cs diff --git a/LoongsonNeuq.Classroom/Program.cs b/LoongsonNeuq.Classroom/Program.cs index 77d884b..5b933ad 100644 --- a/LoongsonNeuq.Classroom/Program.cs +++ b/LoongsonNeuq.Classroom/Program.cs @@ -1,58 +1,22 @@ -using System.Net.Http.Headers; -using LoongsonNeuq.Classroom; +using LoongsonNeuq.Classroom; using LoongsonNeuq.Common; -using LoongsonNeuq.Common.GitHub; using LoongsonNeuq.Common.Auth; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Octokit; var services = new ServiceCollection(); -services.AddSingleton(new ProductInfoHeaderValue("LoongsonClassroom", "1.0")); -services.AddSingleton(); - -services.AddSingleton(); -services.AddSingleton(provider => -{ - var github = provider.GetRequiredService(); - - var classroomJson = github.GetAuthedAsync("https://raw.githubusercontent.com/Loongson-neuq/index/master/classroom.json").Result; - - var classroom = JsonConvert.DeserializeObject(classroomJson.Content.ReadAsStringAsync().Result) - ?? throw new InvalidOperationException("Failed to get classroom."); - - classroom.ServiceProvider = provider; - - return classroom; -}); - -services.AddSingleton(); +services.AddGitHubAuth() + .WithToken() + .AddGitHubClient(); services.AddSingleton(); -services.AddSingleton(new GitHubClient(new Octokit.ProductHeaderValue("LoongsonClassroom"))); +services.AddGitHubAuth(); +services.WithToken(); +services.AddGitHubClient(); -services.AddSingleton(); +services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); -// 目前本后端仅用于 Loongson-neuq/Summary,仅会在 GitHub Actions 中运行 -// 因此只有冷启动,无需考虑缓存更新问题 - -// var classroom = serviceProvider.GetRequiredService(); - -// var assignments = classroom.GetAssignments(); - -// foreach (var assignment in assignments) -// { -// Console.WriteLine($"{assignment.Title}"); - -// var accepted = assignment.GetAcceptedAssignments(); - -// foreach (var aa in accepted) -// { -// Console.WriteLine($" {aa.Students.Single().Login}"); -// } -// } +// TODO \ No newline at end of file diff --git a/LoongsonNeuq.Classroom/StudentsTable.cs b/LoongsonNeuq.Classroom/StudentsTable.cs index 73e29ac..537958f 100644 --- a/LoongsonNeuq.Classroom/StudentsTable.cs +++ b/LoongsonNeuq.Classroom/StudentsTable.cs @@ -1,63 +1,73 @@ using System.Collections.Concurrent; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; -using Octokit; -using LoongsonNeuq.Common.GitHub; +using LoongsonNeuq.Common; +using GitHub; +using System.Diagnostics; +using GitHub.Models; +using System.Collections.Frozen; namespace LoongsonNeuq.Classroom; public class StudentsTable { - private readonly GitHubApi _github; + private readonly GitHubClient _github; - private readonly ConcurrentDictionary _students = new(); + private FrozenDictionary? _students = null; private readonly IServiceProvider _serviceProvider; - public StudentsTable(GitHubApi github, IServiceProvider serviceProvider) + public FrozenDictionary Students + { + get + { + if (_students is null) + { + PopulateStudents(); + } + + return _students!; + } + } + + public StudentsTable(GitHubClient github, IServiceProvider serviceProvider) { _github = github; _serviceProvider = serviceProvider; } - public void PopulateUpdateStudents() + private string? ReadContent(ContentFile? contentFile) { - var response = _github.GetAuthedAsync("https://raw.githubusercontent.com/Loongson-neuq/index/master/student_list.json").Result; - var students = JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().Result) - ?? throw new InvalidOperationException("Failed to get students."); - - var mappedStudents = students.Select(s => new Student - { - Login = s.GitHubLogin, - ResearchFocus = s.ResearchFocus - }).ToList(); - - foreach (var student in mappedStudents) + if (contentFile is null) { - if (_students.ContainsKey(student.Login)) - { - _students.TryUpdate(student.Login, student, student); - } - else - { - _students.TryAdd(student.Login, student); - } + return null; } - var github = _serviceProvider.GetRequiredService(); + Debug.Assert(contentFile.Encoding == "base64"); - foreach (var student in _students.Values) - { - student.FillFields(github); - } + return contentFile.Content; } - private class StoredStudent + public void PopulateStudents() { - [JsonProperty("github_id")] - public string GitHubLogin { get; set; } = null!; + const string owner = "Loongson-neuq"; + const string repo = "index"; + + const string path = "student_list.json"; + + var response = _github.Repos[owner][repo].Contents[path].GetAsync().Result; + + Debug.Assert(response is not null); + + string? content = ReadContent(response.ContentFile); + + if (content is null) + { + throw new InvalidOperationException("Failed to get students."); + } + + var students = JsonConvert.DeserializeObject>(content) + ?? throw new InvalidOperationException("Failed to deserialize students."); - [JsonProperty("research_focus")] - public List ResearchFocus { get; set; } = null!; + _students = students.ToDictionary(s => s.GitHubId).ToFrozenDictionary(); } } \ No newline at end of file diff --git a/LoongsonNeuq.Common/DependencyInjectionExtensions.cs b/LoongsonNeuq.Common/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..908c731 --- /dev/null +++ b/LoongsonNeuq.Common/DependencyInjectionExtensions.cs @@ -0,0 +1,52 @@ +using GitHub; +using GitHub.Assignments; +using GitHub.Classrooms; +using GitHub.Classrooms.Item; +using GitHub.Models; +using GitHub.Octokit.Client; +using GitHub.Octokit.Client.Authentication; +using LoongsonNeuq.Common.Auth; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Authentication; + +namespace LoongsonNeuq.Common; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddGitHubAuth(this IServiceCollection services) => services.AddSingleton(p => + { + string token = p.GetRequiredService().Token; + + if (string.IsNullOrWhiteSpace(token)) + { + throw new InvalidOperationException("Token is empty."); + } + + return new TokenAuthProvider(new TokenProvider(token)); + }); + + public static IServiceCollection WithToken(this IServiceCollection services) where T : class, ITokenProvider + => services.AddSingleton(); + + public static IServiceCollection AddAnonymousAuth(this IServiceCollection services) + => services.AddSingleton(); + + public static IServiceCollection AddGitHubClient(this IServiceCollection services) + => services.AddSingleton(); + + public static IServiceCollection AddLogging(this TServiceCollection services) + where TServiceCollection : IServiceCollection + // Must be concrete type to hide Microsoft's AddLogging + => services.AddSingleton(); + + public static IServiceCollection AddClassroom(this IServiceCollection services, int id) => services.AddKeyedTransient(id, (p, key) => + { + int id = (int)key!; + + ArgumentNullException.ThrowIfNull(id, nameof(id)); + + return p.GetRequiredService().Classrooms[id]; + }); +} diff --git a/LoongsonNeuq.Common/GitHub/AcceptedAssignment.cs b/LoongsonNeuq.Common/GitHub/AcceptedAssignment.cs deleted file mode 100644 index a2235ab..0000000 --- a/LoongsonNeuq.Common/GitHub/AcceptedAssignment.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; - -namespace LoongsonNeuq.Common.GitHub; - -public class AcceptedAssignment -{ - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("submitted")] - public bool Submitted { get; set; } - - [JsonProperty("passing")] - public bool Passing { get; set; } - - [JsonProperty("commit_count")] - public long CommitCount { get; set; } - - [JsonProperty("grade")] - public long? Grade { get; set; } - - [JsonProperty("repository")] - public Repository AssignmentRepository { get; set; } = null!; - - [JsonProperty("students")] - public List Students { get; set; } = new(); - - public Classroom Classroom { get; set; } = null!; -} \ No newline at end of file diff --git a/LoongsonNeuq.Common/GitHub/Assignment.cs b/LoongsonNeuq.Common/GitHub/Assignment.cs deleted file mode 100644 index 4268c90..0000000 --- a/LoongsonNeuq.Common/GitHub/Assignment.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; - -namespace LoongsonNeuq.Common.GitHub; - -public partial class Assignment -{ - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("public_repo")] - public bool PublicRepo { get; set; } - - [JsonProperty("title")] - public string Title { get; set; } = null!; - - [JsonProperty("type")] - public string AssignmentType { get; set; } = null!; - - [JsonProperty("invite_link")] - public Uri InviteLink { get; set; } = null!; - - [JsonProperty("invitations_enabled")] - public bool InvitationsEnabled { get; set; } - - [JsonProperty("slug")] - public string Slug { get; set; } = null!; - - [JsonProperty("students_are_repo_admins")] - public bool StudentsAreRepoAdmins { get; set; } - - [JsonProperty("feedback_pull_requests_enabled")] - public bool FeedbackPullRequestsEnabled { get; set; } - - [JsonProperty("accepted")] - public long AcceptedCount { get; set; } - - [JsonProperty("submissions")] - public long Submissions { get; set; } - - [JsonProperty("passing")] - public long Passing { get; set; } - - public IServiceProvider ServiceProvider => Classroom.ServiceProvider; - - public Classroom Classroom { get; set; } = null!; - - public List GetAcceptedAssignments() - { - var github = ServiceProvider.GetRequiredService(); - - var response = github.GetAuthedAsync($"https://api.github.com/assignments/{Id}/accepted_assignments").Result; - - var acceptedAssignments = JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().Result) - ?? throw new InvalidOperationException("Failed to get accepted assignments."); - - acceptedAssignments.ForEach(aa => aa.Classroom = Classroom); - - return acceptedAssignments; - } -} \ No newline at end of file diff --git a/LoongsonNeuq.Common/GitHub/Classroom.cs b/LoongsonNeuq.Common/GitHub/Classroom.cs deleted file mode 100644 index 644c48f..0000000 --- a/LoongsonNeuq.Common/GitHub/Classroom.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; - -namespace LoongsonNeuq.Common.GitHub; - -public class Classroom -{ - public string Name { get; private set; } = null!; - - public long Id { get; private set; } - - public bool IsArchived { get; private set; } - - public string Url { get; private set; } = null!; - - public IServiceProvider ServiceProvider { get; set; } = null!; - - private static readonly ConcurrentDictionary _classrooms = new(); - - public static Classroom? TryGetClassroom(long id) - { - return _classrooms.TryGetValue(id, out var classroom) ? classroom : null; - } - - public List GetAssignments() - { - var github = ServiceProvider.GetRequiredService(); - - var response = github.GetAuthedAsync($"https://api.github.com/classrooms/{Id}/assignments").Result; - - var assignments = JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().Result) - ?? throw new InvalidOperationException("Failed to get assignments."); - assignments.ForEach(assignment => assignment.Classroom = this); - - return assignments; - } - - public Classroom(string name, long id, string url, bool isArchived) - { - Name = name; - Id = id; - Url = url; - IsArchived = isArchived; - - if (!_classrooms.TryAdd(id, this)) - { - _classrooms.TryUpdate(id, this, this); - } - } - - public Classroom(IServiceProvider serviceProvider, string name, long id, string url, bool isArchived) - : this(name, id, url, isArchived) - { - ServiceProvider = serviceProvider; - } -} \ No newline at end of file diff --git a/LoongsonNeuq.Common/GitHub/ContributionStat.cs b/LoongsonNeuq.Common/GitHub/ContributionStat.cs index 3dea5dd..13654c2 100644 --- a/LoongsonNeuq.Common/GitHub/ContributionStat.cs +++ b/LoongsonNeuq.Common/GitHub/ContributionStat.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; using Newtonsoft.Json; namespace LoongsonNeuq.Common.GitHub; @@ -15,14 +17,19 @@ private ContributionStat(List contributionStats) Contributions = contributionStats; } - public static async Task Fetch(GitHubApi github, string username) + public static async Task Fetch(string username) { - var response = await github.GetUnauthedAsync($"https://github-contributions-api.jogruber.de/v4/{username}?y=last"); + using (var webClient = new HttpClient()) + { + var response = await webClient.GetAsync($"https://github-contributions-api.jogruber.de/v4/{username}?y=last"); - var contributions = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result) - ?? throw new Exception("Failed to get contributions."); + if (response.StatusCode != HttpStatusCode.OK) + { + return null; + } - return contributions; + return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result); + } } public struct Contribution diff --git a/LoongsonNeuq.Common/GitHub/GitHubApi.cs b/LoongsonNeuq.Common/GitHub/GitHubApi.cs deleted file mode 100644 index 4d87b5e..0000000 --- a/LoongsonNeuq.Common/GitHub/GitHubApi.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Net.Http.Headers; -using Microsoft.Extensions.Logging; -using LoongsonNeuq.Common.Auth; - -namespace LoongsonNeuq.Common.GitHub; - -public class GitHubApi -{ - private readonly ProductInfoHeaderValue _productInfoHeaderValue; - private readonly ITokenProvider _githubTokenProvider; - - private readonly ILogger _logger; - - public GitHubApi(ProductInfoHeaderValue productHeaderValue, ITokenProvider githubTokenProvider, ILogger logger) - { - _productInfoHeaderValue = productHeaderValue; - _githubTokenProvider = githubTokenProvider; - _logger = logger; - } - - private HttpClient AuthedWebClient - => new HttpClient - { - DefaultRequestHeaders = - { - UserAgent = { _productInfoHeaderValue }, - Authorization = new AuthenticationHeaderValue("Bearer", _githubTokenProvider.Token), - } - }; - - private HttpClient UnauthedWebClient - => new HttpClient - { - DefaultRequestHeaders = - { - UserAgent = { _productInfoHeaderValue }, - } - }; - - public async Task GetUnauthedAsync(string url) - { - _logger.LogTrace($"[GitHubApi] [GET] {url}"); - using var client = UnauthedWebClient; - - return await client.GetAsync(url); - } - - public async Task GetUnauthedRetryAsync(string url, double retrySeconds = 1, int retryCount = 10) - { - for (var i = 0; i < retryCount; i++) - { - var response = await GetUnauthedAsync(url); - - if (response.IsSuccessStatusCode) - { - return response; - } - - _logger.LogWarning($"{i + 1} tries failed to get {url} with status code {response.StatusCode}. Retrying in {retrySeconds} seconds..."); - - await Task.Delay(TimeSpan.FromSeconds(retrySeconds)); - } - - throw new HttpRequestException($"Failed to get {url} after {retryCount} retries."); - } - - public async Task GetAuthedRetryAsync(string url, double retrySeconds = 1, int retryCount = 10) - { - for (var i = 0; i < retryCount; i++) - { - var response = await GetAuthedAsync(url); - - if (response.IsSuccessStatusCode) - { - return response; - } - - _logger.LogWarning($"{i + 1} tries failed to get {url} with status code {response.StatusCode}. Retrying in {retrySeconds} seconds..."); - - await Task.Delay(TimeSpan.FromSeconds(retrySeconds)); - } - - throw new HttpRequestException($"Failed to get {url} after {retryCount} retries."); - } - - public async Task GetAuthedAsync(string url) - { - _logger.LogTrace($"[Authed] [GitHubApi] [GET] {url}"); - using var client = AuthedWebClient; - - client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); - - return await client.GetAsync(url); - } -} \ No newline at end of file diff --git a/LoongsonNeuq.Common/GitHub/Student.cs b/LoongsonNeuq.Common/GitHub/Student.cs deleted file mode 100644 index 18af843..0000000 --- a/LoongsonNeuq.Common/GitHub/Student.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; -using Octokit; - -namespace LoongsonNeuq.Common.GitHub; -public class Student -{ - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("login")] - public string Login { get; set; } = null!; - - [JsonProperty("name")] - public string? FullName { get; set; } = null!; - - [JsonProperty("avatar_url")] - public string AvatarUrl { get; set; } = null!; - - [JsonProperty("html_url")] - public string HtmlUrl { get; set; } = null!; - - public List? ResearchFocus { get; set; } = null!; - - public void FillFields(GitHubClient github) - { - var user = github.User.Get(Login).Result; - - Id = user.Id; - FullName = user.Name; - AvatarUrl = user.AvatarUrl; - HtmlUrl = user.HtmlUrl; - } -} \ No newline at end of file diff --git a/LoongsonNeuq.Common/LoongsonNeuq.Common.csproj b/LoongsonNeuq.Common/LoongsonNeuq.Common.csproj index 085eb17..bdeb657 100644 --- a/LoongsonNeuq.Common/LoongsonNeuq.Common.csproj +++ b/LoongsonNeuq.Common/LoongsonNeuq.Common.csproj @@ -7,10 +7,10 @@ + - diff --git a/LoongsonNeuq.ListFormatter.Tests/TestGitHubChecker.cs b/LoongsonNeuq.ListFormatter.Tests/TestGitHubChecker.cs index 28de3f5..f56aff2 100644 --- a/LoongsonNeuq.ListFormatter.Tests/TestGitHubChecker.cs +++ b/LoongsonNeuq.ListFormatter.Tests/TestGitHubChecker.cs @@ -1,4 +1,7 @@ +using GitHub; +using GitHub.Octokit.Client; using LoongsonNeuq.Common; +using Microsoft.Kiota.Abstractions.Authentication; namespace LoongsonNeuq.ListFormatter.Tests; @@ -9,7 +12,13 @@ public class TestGitHubChecker [SetUp] public void Setup() { - _gitHubChecker = new GitHubIDChecker(DummyLogger.Instance); + if (_gitHubChecker != null) + return; + + var adapter = RequestAdapter.Create(new AnonymousAuthenticationProvider()); + var githubClient = new GitHubClient(adapter); + + _gitHubChecker = new GitHubIDChecker(DummyLogger.Instance, githubClient); } [Test] diff --git a/LoongsonNeuq.ListFormatter/GitHubIDChecker.cs b/LoongsonNeuq.ListFormatter/GitHubIDChecker.cs index dec03c7..92b7837 100644 --- a/LoongsonNeuq.ListFormatter/GitHubIDChecker.cs +++ b/LoongsonNeuq.ListFormatter/GitHubIDChecker.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using GitHub; using LoongsonNeuq.Common; using Microsoft.Extensions.Logging; @@ -8,22 +9,12 @@ public class GitHubIDChecker : IChecker { private readonly ILogger _logger; - private readonly HttpClient _client; + private readonly GitHubClient _client; - public GitHubIDChecker(ILogger logger) + public GitHubIDChecker(ILogger logger, GitHubClient client) { _logger = logger; - - _client = new HttpClient() - { - DefaultRequestHeaders = - { - UserAgent = - { - new ProductInfoHeaderValue("ListFormatter", "1.0") - } - } - }; + _client = client; } public bool CheckOrNormalize(ref ListRoot root) @@ -41,7 +32,7 @@ public bool CheckOrNormalize(ref ListRoot root) try { - allValid &= CheckStudent(student).GetAwaiter().GetResult(); + allValid &= CheckStudent(student).GetAwaiter().GetResult(); } catch (Exception e) { @@ -76,10 +67,8 @@ private async Task CheckStudent(StoredStudent student) /// whether valid private async Task CheckValidGitHubId(string githubId) { - var url = $"https://api.github.com/users/{githubId}"; - - HttpResponseMessage response = await _client.GetAsync(url); + var response = await _client.Users[githubId].GetAsync(); - return response.IsSuccessStatusCode; + return response is not null && (response.PublicUser is not null || response.PrivateUser is not null); } } \ No newline at end of file diff --git a/LoongsonNeuq.ListFormatter/Program.cs b/LoongsonNeuq.ListFormatter/Program.cs index 1417788..810221b 100644 --- a/LoongsonNeuq.ListFormatter/Program.cs +++ b/LoongsonNeuq.ListFormatter/Program.cs @@ -5,12 +5,12 @@ var services = new ServiceCollection(); -services.AddScoped(); - -services.AddTransient(); -services.AddTransient(); - -services.AddSingleton(); +services.AddLogging() + .AddAnonymousAuth() + .AddGitHubClient() + .AddTransient() + .AddTransient() + .AddSingleton(); var serviceProvider = services.BuildServiceProvider(); From 5f356b92b51480223070d1d64b3c50b53b0d55d4 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Tue, 17 Sep 2024 14:21:53 +0800 Subject: [PATCH 2/2] remove duplicate dependencies --- LoongsonNeuq.Classroom/Program.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/LoongsonNeuq.Classroom/Program.cs b/LoongsonNeuq.Classroom/Program.cs index 5b933ad..1a56052 100644 --- a/LoongsonNeuq.Classroom/Program.cs +++ b/LoongsonNeuq.Classroom/Program.cs @@ -11,10 +11,6 @@ services.AddSingleton(); -services.AddGitHubAuth(); -services.WithToken(); -services.AddGitHubClient(); - services.AddLogging(); var serviceProvider = services.BuildServiceProvider();