Skip to content

Commit 83391f4

Browse files
committed
Periodically check for new stars and remove old stars
1 parent 78d4fe4 commit 83391f4

File tree

7 files changed

+151
-14
lines changed

7 files changed

+151
-14
lines changed

Components/Pages/Home.razor

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,7 @@ else
241241
Logger.LogInformation("Indexing {Count} repositories starred by {Username}", starred.Count, GithubUsername);
242242
await SearchService.IndexRepositories(starred
243243
.Where(x => !x.Private) // skip private repositories, the token used should not have access anyway, but just in case
244-
.Select(x => new Repository
245-
{
246-
Id = Repository.ComputeRepositoryId(GithubUsername, x.Id),
247-
Slug = x.Name,
248-
Owner = x.Owner.Login,
249-
Url = x.HtmlUrl,
250-
UpdatedAt = x.UpdatedAt,
251-
StarredBy = GithubUsername,
252-
Description = x.Description,
253-
}));
244+
.Select(x => Repository.FromGithubRepository(x, GithubUsername)));
254245
Logger.LogInformation("User {Username} onboarded successfully", GithubUsername);
255246
Snackbar.Add("Repositories indexed successfully", Severity.Success);
256247
}

Program.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using GithubStarSearch.Components;
44
using GithubStarSearch.Searching;
55
using MudBlazor.Services;
6+
using Octokit;
67
using Serilog;
78

89
var builder = WebApplication.CreateBuilder(args);
@@ -21,9 +22,28 @@
2122
.AddInteractiveServerComponents();
2223

2324
// Application logic services
25+
builder.Services.AddScoped<GitHubClient>(x =>
26+
{
27+
var configuration = x.GetRequiredService<IConfiguration>();
28+
var logger = x.GetRequiredService<ILogger<Program>>();
29+
var token = configuration["Github:FineGrainedToken"];
30+
31+
var client = new GitHubClient(new ProductHeaderValue("GithubStarSearch"));
32+
if (string.IsNullOrEmpty(token))
33+
{
34+
logger.LogCritical("No Github:FineGrainedToken found in configuration");
35+
}
36+
else
37+
{
38+
client.Credentials = new Credentials(token);
39+
}
40+
41+
return client;
42+
});
2443
builder.AddMeilisearch();
2544
builder.Services.AddBlazoredLocalStorage();
26-
builder.Services.AddHostedService<Indexer>();
45+
builder.Services.AddHostedService<RepositoryUpdater>();
46+
builder.Services.AddHostedService<StarsUpdater>();
2747

2848
var app = builder.Build();
2949

Indexer.cs renamed to RepositoryUpdater.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace GithubStarSearch;
99
/// <summary>
1010
/// Background worker that periodically updates repositories.
1111
/// </summary>
12-
public class Indexer(ILogger<Indexer> logger, IServiceProvider serviceProvider) : BackgroundService
12+
public class RepositoryUpdater(ILogger<RepositoryUpdater> logger, IServiceProvider serviceProvider) : BackgroundService
1313
{
1414
private const int RequestLimit = 200;
1515
private readonly PeriodicTimer _timer = new(TimeSpan.FromMinutes(5));
@@ -66,7 +66,7 @@ private async Task<ResourceResults<IEnumerable<Repository>>> DoWork(int offset)
6666

6767
private async Task UpdateRepository(Repository repository, GitHubClient github)
6868
{
69-
var details = await github.Repository.Get(repository.Owner, repository.Slug);
69+
var details = await GetDetails(repository, github);
7070
if (details is null)
7171
{
7272
logger.LogWarning("Repository {Owner}/{Slug} not found", repository.Owner, repository.Slug);
@@ -80,6 +80,25 @@ private async Task UpdateRepository(Repository repository, GitHubClient github)
8080
repository.Readme = readme;
8181
}
8282

83+
private async Task<Octokit.Repository?> GetDetails(Repository repository, GitHubClient github)
84+
{
85+
try
86+
{
87+
return await github.Repository.Get(repository.Owner, repository.Slug);
88+
}
89+
catch (ForbiddenException e)
90+
{
91+
logger.LogWarning(e, "Forbidden while fetching details for {Owner}/{Slug}", repository.Owner,
92+
repository.Slug);
93+
return null;
94+
}
95+
catch (NotFoundException e)
96+
{
97+
logger.LogWarning(e, "Repository {Owner}/{Slug} not found", repository.Owner, repository.Slug);
98+
return null;
99+
}
100+
}
101+
83102
private GitHubClient CreateClient()
84103
{
85104
using var scope = serviceProvider.CreateScope();

Searching/Repository.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,18 @@ public class Repository
1717

1818
public string Readme { get; set; } = "";
1919

20-
public static string ComputeRepositoryId(string starredBy, long id)
20+
public static Repository FromGithubRepository(Octokit.Repository repository, string starredBy) => new()
21+
{
22+
Id = ComputeRepositoryId(starredBy, repository.Id),
23+
Slug = repository.Name,
24+
Owner = repository.Owner.Login,
25+
Url = repository.HtmlUrl,
26+
UpdatedAt = repository.UpdatedAt,
27+
StarredBy = starredBy,
28+
Description = repository.Description,
29+
};
30+
31+
private static string ComputeRepositoryId(string starredBy, long id)
2132
{
2233
// having unique id composed of owner and slug is not enough
2334
// because the same repository can be starred by multiple users
@@ -26,4 +37,8 @@ public static string ComputeRepositoryId(string starredBy, long id)
2637
// A document identifier can be of type integer or string, only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and underscores (_).
2738
return $"{starredBy}-{id}";
2839
}
40+
41+
public override int GetHashCode() => Id.GetHashCode();
42+
43+
public override bool Equals(object? obj) => obj is Repository other && other.Id == Id;
2944
}

Searching/SearchService.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ public async Task IndexRepositories(IEnumerable<Repository> repositories)
4242
}
4343
}
4444

45+
public async Task RemoveRepositories(IEnumerable<Repository> repositories)
46+
{
47+
var index = client.Index(searchOptions.Value.RepositoriesIndexName);
48+
var task = await index.DeleteDocumentsAsync(repositories.Select(x => x.Id));
49+
50+
var info = await index.WaitForTaskAsync(task.TaskUid);
51+
if (info.Status != TaskInfoStatus.Succeeded)
52+
{
53+
var builder = new StringBuilder();
54+
foreach (var (key, value) in info.Error)
55+
{
56+
builder.AppendLine($"{key}: {value}");
57+
}
58+
59+
logger.LogError("Failed to remove repositories: {Error}", builder.ToString());
60+
}
61+
else
62+
{
63+
logger.LogInformation("Removing succeeded in {Duration}", info.Duration);
64+
}
65+
}
66+
4567
public async Task<IReadOnlyCollection<Repository>> SearchRepositories(string starredBy,
4668
string term,
4769
SearchOptions options)
@@ -91,6 +113,23 @@ public Task<ResourceResults<IEnumerable<Repository>>> GetRepositories(int limit,
91113
});
92114
}
93115

116+
public async IAsyncEnumerable<Repository> GetAllRepositories()
117+
{
118+
var index = client.Index(searchOptions.Value.RepositoriesIndexName);
119+
var offset = 0;
120+
ResourceResults<IEnumerable<Repository>>? result;
121+
do
122+
{
123+
result = await index.GetDocumentsAsync<Repository>(new DocumentsQuery { Offset = offset });
124+
foreach (var repository in result.Results)
125+
{
126+
yield return repository;
127+
}
128+
129+
offset += result.Results.Count();
130+
} while (result.Results.Any());
131+
}
132+
94133
public async Task<bool> IsIndexed(string githubUsername)
95134
{
96135
var index = client.Index(searchOptions.Value.RepositoriesIndexName);

StarsUpdater.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using GithubStarSearch.Searching;
2+
using Octokit;
3+
using Repository = GithubStarSearch.Searching.Repository;
4+
5+
namespace GithubStarSearch;
6+
7+
/// <summary>
8+
/// Periodically checks stars of monitored users.
9+
/// Adds newly starred repositories to the full-text search index.
10+
/// Removes un-starred repositories from the full-text search index.
11+
/// </summary>
12+
public class StarsUpdater(ILogger<StarsUpdater> logger, IServiceProvider serviceProvider) : BackgroundService
13+
{
14+
private const int RequestLimit = 200;
15+
private readonly PeriodicTimer _timer = new(TimeSpan.FromMinutes(5));
16+
17+
protected override async Task ExecuteAsync(CancellationToken ct)
18+
{
19+
do
20+
{
21+
try
22+
{
23+
using var scope = serviceProvider.CreateScope();
24+
var service = scope.ServiceProvider.GetRequiredService<SearchService>();
25+
var github = scope.ServiceProvider.GetRequiredService<GitHubClient>();
26+
27+
var allRepositories = new Dictionary<string, HashSet<Repository>>();
28+
await foreach (var repository in service.GetAllRepositories().WithCancellation(ct))
29+
{
30+
allRepositories.TryAdd(repository.StarredBy, []);
31+
allRepositories[repository.StarredBy].Add(repository);
32+
}
33+
34+
foreach (var (user, currentStars) in allRepositories)
35+
{
36+
var starred = await github.Activity.Starring.GetAllForUser(user) ?? [];
37+
var githubStars = starred.Select(x => Repository.FromGithubRepository(x, user)).ToHashSet();
38+
var unstarred = currentStars.Except(githubStars).ToList();
39+
var newlyStarred = githubStars.Except(currentStars).ToList();
40+
await service.IndexRepositories(newlyStarred);
41+
await service.RemoveRepositories(unstarred);
42+
}
43+
44+
logger.LogInformation("Waiting {Period} for next tick", _timer.Period);
45+
}
46+
catch (Exception e)
47+
{
48+
logger.LogError(e, "Error while updating stars");
49+
}
50+
} while (!ct.IsCancellationRequested && await _timer.WaitForNextTickAsync(ct));
51+
}
52+
}

github-star-search.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<PackageReference Include="Octokit" Version="14.0.0"/>
1818
<PackageReference Include="MeiliSearch" Version="0.15.5"/>
1919
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
20+
<PackageReference Include="System.Linq.Async" Version="6.0.1"/>
2021
</ItemGroup>
2122
</Project>

0 commit comments

Comments
 (0)