diff --git a/src/Domain/Entities/Github/GithubProfileData.cs b/src/Domain/Entities/Github/GithubProfileData.cs index 8ec69599..60cbda3c 100644 --- a/src/Domain/Entities/Github/GithubProfileData.cs +++ b/src/Domain/Entities/Github/GithubProfileData.cs @@ -46,6 +46,8 @@ public record GithubProfileData public int MonthsToFetchCommits { get; init; } + public DateTime UserCreatedAt { get; init; } + public DateTime CreatedAt { get; init; } public GithubProfileData() @@ -105,6 +107,7 @@ public string GetTelegramFormattedText() var result = $"Github stats for {Username}\n" + $"Name: {Name}\n" + + $"Profile created: {UserCreatedAt:yyyy-MM-dd}\n" + $"Profile URL\n\n" + $"🌟 Social stats:\n" + $"- Followers: {Followers:N0}\n" + diff --git a/src/Infrastructure/Services/Github/GithubGraphQLService.cs b/src/Infrastructure/Services/Github/GithubGraphQLService.cs index 6703a8f8..4cb598d3 100644 --- a/src/Infrastructure/Services/Github/GithubGraphQLService.cs +++ b/src/Infrastructure/Services/Github/GithubGraphQLService.cs @@ -366,7 +366,8 @@ private GithubProfileData MapToGithubProfileData( user.ContributionsCollection?.CommitContributionsByRepository, topLanguagesCount), MonthsToFetchCommits = monthsToFetchCommits, - CreatedAt = user.CreatedAt + UserCreatedAt = user.CreatedAt, + CreatedAt = DateTime.UtcNow, }; } @@ -487,10 +488,15 @@ public record UserProfile public string Id { get; set; } public string Name { get; set; } public string Login { get; set; } + public string Url { get; set; } + public CountInfo Followers { get; set; } + public CountInfo Following { get; set; } + public CountInfo Repositories { get; set; } + public CountInfo StarredRepositories { get; set; } public CountInfo Issues { get; set; } public CountInfo PullRequests { get; set; } diff --git a/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobCommand.cs b/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobCommand.cs new file mode 100644 index 00000000..b75fedf3 --- /dev/null +++ b/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobCommand.cs @@ -0,0 +1,5 @@ +namespace Web.Api.Features.Github.DeleteGithubProcessingJob; + +#pragma warning disable SA1313 +public record DeleteGithubProcessingJobCommand( + string Username); \ No newline at end of file diff --git a/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobHandler.cs b/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobHandler.cs new file mode 100644 index 00000000..33e479b8 --- /dev/null +++ b/src/Web.Api/Features/Github/DeleteGithubProcessingJob/DeleteGithubProcessingJobHandler.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Database; +using Infrastructure.Services.Mediator; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Web.Api.Features.Github.DeleteGithubProcessingJob; + +public class DeleteGithubProcessingJobHandler : IRequestHandler +{ + private readonly DatabaseContext _context; + + public DeleteGithubProcessingJobHandler(DatabaseContext context) + { + _context = context; + } + + public async Task Handle( + DeleteGithubProcessingJobCommand request, + CancellationToken cancellationToken) + { + var job = await _context.GithubProfileProcessingJobs + .FirstOrDefaultAsync(x => x.Username == request.Username, cancellationToken); + + if (job == null) + { + throw new BadHttpRequestException($"Github processing job with username '{request.Username}' not found"); + } + + _context.GithubProfileProcessingJobs.Remove(job); + await _context.TrySaveChangesAsync(cancellationToken); + + return Nothing.Value; + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/Dtos/GithubProfileBotChatDto.cs b/src/Web.Api/Features/Github/Dtos/GithubProfileBotChatDto.cs new file mode 100644 index 00000000..89fbe5f9 --- /dev/null +++ b/src/Web.Api/Features/Github/Dtos/GithubProfileBotChatDto.cs @@ -0,0 +1,37 @@ +using System; +using Domain.Entities.Github; + +namespace Web.Api.Features.Github.Dtos; + +public record GithubProfileBotChatDto +{ + public Guid Id { get; init; } + + public long ChatId { get; init; } + + public string Username { get; init; } + + public bool IsAdmin { get; init; } + + public int MessagesCount { get; init; } + + public DateTimeOffset CreatedAt { get; init; } + + public DateTimeOffset UpdatedAt { get; init; } + + public GithubProfileBotChatDto() + { + } + + public GithubProfileBotChatDto( + GithubProfileBotChat chat) + { + Id = chat.Id; + ChatId = chat.ChatId; + Username = chat.Username; + IsAdmin = chat.IsAdmin; + MessagesCount = chat.MessagesCount; + CreatedAt = chat.CreatedAt; + UpdatedAt = chat.UpdatedAt; + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/Dtos/GithubProfileDto.cs b/src/Web.Api/Features/Github/Dtos/GithubProfileDto.cs new file mode 100644 index 00000000..7dce8296 --- /dev/null +++ b/src/Web.Api/Features/Github/Dtos/GithubProfileDto.cs @@ -0,0 +1,34 @@ +using System; +using Domain.Entities.Github; + +namespace Web.Api.Features.Github.Dtos; + +public record GithubProfileDto +{ + public string Username { get; init; } + + public int Version { get; init; } + + public int RequestsCount { get; init; } + + public DateTime DataSyncedAt { get; init; } + + public DateTimeOffset CreatedAt { get; init; } + + public DateTimeOffset UpdatedAt { get; init; } + + public GithubProfileDto() + { + } + + public GithubProfileDto( + GithubProfile profile) + { + Username = profile.Username; + Version = profile.Version; + RequestsCount = profile.RequestsCount; + DataSyncedAt = profile.DataSyncedAt; + CreatedAt = profile.CreatedAt; + UpdatedAt = profile.UpdatedAt; + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/Dtos/GithubProfileProcessingJobDto.cs b/src/Web.Api/Features/Github/Dtos/GithubProfileProcessingJobDto.cs new file mode 100644 index 00000000..01394443 --- /dev/null +++ b/src/Web.Api/Features/Github/Dtos/GithubProfileProcessingJobDto.cs @@ -0,0 +1,24 @@ +using System; +using Domain.Entities.Github; + +namespace Web.Api.Features.Github.Dtos; + +public record GithubProfileProcessingJobDto +{ + public string Username { get; init; } + + public DateTimeOffset CreatedAt { get; init; } + + public DateTimeOffset UpdatedAt { get; init; } + + public GithubProfileProcessingJobDto() + { + } + + public GithubProfileProcessingJobDto(GithubProfileProcessingJob job) + { + Username = job.Username; + CreatedAt = job.CreatedAt; + UpdatedAt = job.UpdatedAt; + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsHandler.cs b/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsHandler.cs new file mode 100644 index 00000000..18474569 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsHandler.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Database; +using Infrastructure.Services.Mediator; +using Microsoft.EntityFrameworkCore; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProcessingJobs; + +public class GetGithubProcessingJobsHandler : IRequestHandler +{ + private readonly DatabaseContext _context; + + public GetGithubProcessingJobsHandler(DatabaseContext context) + { + _context = context; + } + + public async Task Handle( + Nothing request, + CancellationToken cancellationToken) + { + var jobs = await _context.GithubProfileProcessingJobs + .OrderByDescending(x => x.CreatedAt) + .ToListAsync(cancellationToken); + + return new GetGithubProcessingJobsResponse( + jobs.Select(x => new GithubProfileProcessingJobDto(x)).ToList()); + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsResponse.cs b/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsResponse.cs new file mode 100644 index 00000000..55bb5f5f --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProcessingJobs/GetGithubProcessingJobsResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProcessingJobs; + +#pragma warning disable SA1313 +public record GetGithubProcessingJobsResponse( + IReadOnlyCollection Results); \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsHandler.cs b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsHandler.cs new file mode 100644 index 00000000..2b7efa5a --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsHandler.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Database; +using Infrastructure.Services.Mediator; +using Microsoft.EntityFrameworkCore; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProfileChats; + +public class GetGithubProfileChatsHandler + : IRequestHandler +{ + private readonly DatabaseContext _context; + + public GetGithubProfileChatsHandler(DatabaseContext context) + { + _context = context; + } + + public async Task Handle( + GetGithubProfileChatsQueryParams request, + CancellationToken cancellationToken) + { + var chats = await _context.GithubProfileBotChats + .OrderByDescending(x => x.MessagesCount) + .ThenByDescending(x => x.CreatedAt) + .AsPaginatedAsync( + request, + cancellationToken); + + return new GetGithubProfileChatsResponse( + chats.CurrentPage, + chats.PageSize, + chats.TotalItems, + chats.Results + .Select(x => new GithubProfileBotChatDto(x)) + .ToList()); + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsQueryParams.cs b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsQueryParams.cs new file mode 100644 index 00000000..8b226319 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsQueryParams.cs @@ -0,0 +1,7 @@ +using Domain.ValueObjects.Pagination; + +namespace Web.Api.Features.Github.GetGithubProfileChats; + +public record GetGithubProfileChatsQueryParams : PageModel +{ +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsResponse.cs b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsResponse.cs new file mode 100644 index 00000000..b299a5e9 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfileChats/GetGithubProfileChatsResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Domain.ValueObjects.Pagination; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProfileChats; + +#pragma warning disable SA1313 +public record GetGithubProfileChatsResponse( + int CurrentPage, + int PageSize, + int TotalItems, + IReadOnlyCollection Results) + : Pageable(CurrentPage, PageSize, TotalItems, Results); \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesHandler.cs b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesHandler.cs new file mode 100644 index 00000000..30bfa060 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesHandler.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Infrastructure.Database; +using Infrastructure.Services.Mediator; +using Microsoft.EntityFrameworkCore; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProfiles; + +public class GetGithubProfilesHandler : IRequestHandler +{ + private readonly DatabaseContext _context; + + public GetGithubProfilesHandler(DatabaseContext context) + { + _context = context; + } + + public async Task Handle( + GetGithubProfilesQueryParams request, + CancellationToken cancellationToken) + { + var profiles = await _context.GithubProfiles + .OrderByDescending(x => x.CreatedAt) + .AsPaginatedAsync( + request, + cancellationToken); + + return new GetGithubProfilesResponse( + profiles.CurrentPage, + profiles.PageSize, + profiles.TotalItems, + profiles.Results + .Select(x => new GithubProfileDto(x)) + .ToList()); + } +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesQueryParams.cs b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesQueryParams.cs new file mode 100644 index 00000000..b7ecf685 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesQueryParams.cs @@ -0,0 +1,7 @@ +using Domain.ValueObjects.Pagination; + +namespace Web.Api.Features.Github.GetGithubProfiles; + +public record GetGithubProfilesQueryParams : PageModel +{ +} \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesResponse.cs b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesResponse.cs new file mode 100644 index 00000000..725e7ef8 --- /dev/null +++ b/src/Web.Api/Features/Github/GetGithubProfiles/GetGithubProfilesResponse.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Domain.ValueObjects.Pagination; +using Web.Api.Features.Github.Dtos; + +namespace Web.Api.Features.Github.GetGithubProfiles; + +#pragma warning disable SA1313 +public record GetGithubProfilesResponse( + int CurrentPage, + int PageSize, + int TotalItems, + IReadOnlyCollection Results) + : Pageable(CurrentPage, PageSize, TotalItems, Results); \ No newline at end of file diff --git a/src/Web.Api/Features/Github/GithubController.cs b/src/Web.Api/Features/Github/GithubController.cs new file mode 100644 index 00000000..7a74eab6 --- /dev/null +++ b/src/Web.Api/Features/Github/GithubController.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Domain.Enums; +using Infrastructure.Services.Mediator; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Web.Api.Features.Github.DeleteGithubProcessingJob; +using Web.Api.Features.Github.GetGithubProcessingJobs; +using Web.Api.Features.Github.GetGithubProfileChats; +using Web.Api.Features.Github.GetGithubProfiles; +using Web.Api.Setup.Attributes; + +namespace Web.Api.Features.Github; + +[ApiController] +[Route("api/github")] +[HasAnyRole(Role.Admin)] +public class GithubController : ControllerBase +{ + private readonly IServiceProvider _serviceProvider; + + public GithubController(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + [HttpGet("profiles")] + public async Task GetGithubProfiles( + [FromQuery] GetGithubProfilesQueryParams queryParams, + CancellationToken cancellationToken) + { + return Ok( + await _serviceProvider.GetRequiredService() + .Handle(queryParams, cancellationToken)); + } + + [HttpGet("chats")] + public async Task GetGithubProfileChats( + [FromQuery] GetGithubProfileChatsQueryParams queryParams, + CancellationToken cancellationToken) + { + return Ok( + await _serviceProvider.GetRequiredService() + .Handle(queryParams, cancellationToken)); + } + + [HttpGet("processing-jobs")] + public async Task GetGithubProcessingJobs( + CancellationToken cancellationToken) + { + return Ok( + await _serviceProvider.GetRequiredService() + .Handle(Nothing.Value, cancellationToken)); + } + + [HttpDelete("processing-jobs/{username}")] + public async Task DeleteGithubProcessingJob( + string username, + CancellationToken cancellationToken) + { + try + { + await _serviceProvider.GetRequiredService() + .Handle(new DeleteGithubProcessingJobCommand(username), cancellationToken); + + return Ok(); + } + catch (BadHttpRequestException ex) + { + return NotFound(ex.Message); + } + } +} \ No newline at end of file