Initial commit of the rewrite

Tim Burga 7daab45d

+30
.dockerignore
··· 1 + **/.classpath 2 + **/.dockerignore 3 + **/.env 4 + **/.git 5 + **/.gitignore 6 + **/.project 7 + **/.settings 8 + **/.toolstarget 9 + **/.vs 10 + **/.vscode 11 + **/*.*proj.user 12 + **/*.dbmdl 13 + **/*.jfm 14 + **/azds.yaml 15 + **/bin 16 + **/charts 17 + **/docker-compose* 18 + **/Dockerfile* 19 + **/node_modules 20 + **/npm-debug.log 21 + **/obj 22 + **/secrets.dev.yaml 23 + **/values.dev.yaml 24 + LICENSE 25 + README.md 26 + !**/.gitignore 27 + !.git/HEAD 28 + !.git/config 29 + !.git/packed-refs 30 + !.git/refs/heads/**
+484
.gitignore
··· 1 + ## Ignore Visual Studio temporary files, build results, and 2 + ## files generated by popular Visual Studio add-ons. 3 + ## 4 + ## Get latest from `dotnet new gitignore` 5 + 6 + # dotenv files 7 + .env 8 + 9 + # User-specific files 10 + *.rsuser 11 + *.suo 12 + *.user 13 + *.userosscache 14 + *.sln.docstates 15 + 16 + # User-specific files (MonoDevelop/Xamarin Studio) 17 + *.userprefs 18 + 19 + # Mono auto generated files 20 + mono_crash.* 21 + 22 + # Build results 23 + [Dd]ebug/ 24 + [Dd]ebugPublic/ 25 + [Rr]elease/ 26 + [Rr]eleases/ 27 + x64/ 28 + x86/ 29 + [Ww][Ii][Nn]32/ 30 + [Aa][Rr][Mm]/ 31 + [Aa][Rr][Mm]64/ 32 + bld/ 33 + [Bb]in/ 34 + [Oo]bj/ 35 + [Ll]og/ 36 + [Ll]ogs/ 37 + 38 + # Visual Studio 2015/2017 cache/options directory 39 + .vs/ 40 + # Uncomment if you have tasks that create the project's static files in wwwroot 41 + #wwwroot/ 42 + 43 + # Visual Studio 2017 auto generated files 44 + Generated\ Files/ 45 + 46 + # MSTest test Results 47 + [Tt]est[Rr]esult*/ 48 + [Bb]uild[Ll]og.* 49 + 50 + # NUnit 51 + *.VisualState.xml 52 + TestResult.xml 53 + nunit-*.xml 54 + 55 + # Build Results of an ATL Project 56 + [Dd]ebugPS/ 57 + [Rr]eleasePS/ 58 + dlldata.c 59 + 60 + # Benchmark Results 61 + BenchmarkDotNet.Artifacts/ 62 + 63 + # .NET 64 + project.lock.json 65 + project.fragment.lock.json 66 + artifacts/ 67 + 68 + # Tye 69 + .tye/ 70 + 71 + # ASP.NET Scaffolding 72 + ScaffoldingReadMe.txt 73 + 74 + # StyleCop 75 + StyleCopReport.xml 76 + 77 + # Files built by Visual Studio 78 + *_i.c 79 + *_p.c 80 + *_h.h 81 + *.ilk 82 + *.meta 83 + *.obj 84 + *.iobj 85 + *.pch 86 + *.pdb 87 + *.ipdb 88 + *.pgc 89 + *.pgd 90 + *.rsp 91 + *.sbr 92 + *.tlb 93 + *.tli 94 + *.tlh 95 + *.tmp 96 + *.tmp_proj 97 + *_wpftmp.csproj 98 + *.log 99 + *.tlog 100 + *.vspscc 101 + *.vssscc 102 + .builds 103 + *.pidb 104 + *.svclog 105 + *.scc 106 + 107 + # Chutzpah Test files 108 + _Chutzpah* 109 + 110 + # Visual C++ cache files 111 + ipch/ 112 + *.aps 113 + *.ncb 114 + *.opendb 115 + *.opensdf 116 + *.sdf 117 + *.cachefile 118 + *.VC.db 119 + *.VC.VC.opendb 120 + 121 + # Visual Studio profiler 122 + *.psess 123 + *.vsp 124 + *.vspx 125 + *.sap 126 + 127 + # Visual Studio Trace Files 128 + *.e2e 129 + 130 + # TFS 2012 Local Workspace 131 + $tf/ 132 + 133 + # Guidance Automation Toolkit 134 + *.gpState 135 + 136 + # ReSharper is a .NET coding add-in 137 + _ReSharper*/ 138 + *.[Rr]e[Ss]harper 139 + *.DotSettings.user 140 + 141 + # TeamCity is a build add-in 142 + _TeamCity* 143 + 144 + # DotCover is a Code Coverage Tool 145 + *.dotCover 146 + 147 + # AxoCover is a Code Coverage Tool 148 + .axoCover/* 149 + !.axoCover/settings.json 150 + 151 + # Coverlet is a free, cross platform Code Coverage Tool 152 + coverage*.json 153 + coverage*.xml 154 + coverage*.info 155 + 156 + # Visual Studio code coverage results 157 + *.coverage 158 + *.coveragexml 159 + 160 + # NCrunch 161 + _NCrunch_* 162 + .*crunch*.local.xml 163 + nCrunchTemp_* 164 + 165 + # MightyMoose 166 + *.mm.* 167 + AutoTest.Net/ 168 + 169 + # Web workbench (sass) 170 + .sass-cache/ 171 + 172 + # Installshield output folder 173 + [Ee]xpress/ 174 + 175 + # DocProject is a documentation generator add-in 176 + DocProject/buildhelp/ 177 + DocProject/Help/*.HxT 178 + DocProject/Help/*.HxC 179 + DocProject/Help/*.hhc 180 + DocProject/Help/*.hhk 181 + DocProject/Help/*.hhp 182 + DocProject/Help/Html2 183 + DocProject/Help/html 184 + 185 + # Click-Once directory 186 + publish/ 187 + 188 + # Publish Web Output 189 + *.[Pp]ublish.xml 190 + *.azurePubxml 191 + # Note: Comment the next line if you want to checkin your web deploy settings, 192 + # but database connection strings (with potential passwords) will be unencrypted 193 + *.pubxml 194 + *.publishproj 195 + 196 + # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 + # checkin your Azure Web App publish settings, but sensitive information contained 198 + # in these scripts will be unencrypted 199 + PublishScripts/ 200 + 201 + # NuGet Packages 202 + *.nupkg 203 + # NuGet Symbol Packages 204 + *.snupkg 205 + # The packages folder can be ignored because of Package Restore 206 + **/[Pp]ackages/* 207 + # except build/, which is used as an MSBuild target. 208 + !**/[Pp]ackages/build/ 209 + # Uncomment if necessary however generally it will be regenerated when needed 210 + #!**/[Pp]ackages/repositories.config 211 + # NuGet v3's project.json files produces more ignorable files 212 + *.nuget.props 213 + *.nuget.targets 214 + 215 + # Microsoft Azure Build Output 216 + csx/ 217 + *.build.csdef 218 + 219 + # Microsoft Azure Emulator 220 + ecf/ 221 + rcf/ 222 + 223 + # Windows Store app package directories and files 224 + AppPackages/ 225 + BundleArtifacts/ 226 + Package.StoreAssociation.xml 227 + _pkginfo.txt 228 + *.appx 229 + *.appxbundle 230 + *.appxupload 231 + 232 + # Visual Studio cache files 233 + # files ending in .cache can be ignored 234 + *.[Cc]ache 235 + # but keep track of directories ending in .cache 236 + !?*.[Cc]ache/ 237 + 238 + # Others 239 + ClientBin/ 240 + ~$* 241 + *~ 242 + *.dbmdl 243 + *.dbproj.schemaview 244 + *.jfm 245 + *.pfx 246 + *.publishsettings 247 + orleans.codegen.cs 248 + 249 + # Including strong name files can present a security risk 250 + # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 + #*.snk 252 + 253 + # Since there are multiple workflows, uncomment next line to ignore bower_components 254 + # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 + #bower_components/ 256 + 257 + # RIA/Silverlight projects 258 + Generated_Code/ 259 + 260 + # Backup & report files from converting an old project file 261 + # to a newer Visual Studio version. Backup files are not needed, 262 + # because we have git ;-) 263 + _UpgradeReport_Files/ 264 + Backup*/ 265 + UpgradeLog*.XML 266 + UpgradeLog*.htm 267 + ServiceFabricBackup/ 268 + *.rptproj.bak 269 + 270 + # SQL Server files 271 + *.mdf 272 + *.ldf 273 + *.ndf 274 + 275 + # Business Intelligence projects 276 + *.rdl.data 277 + *.bim.layout 278 + *.bim_*.settings 279 + *.rptproj.rsuser 280 + *- [Bb]ackup.rdl 281 + *- [Bb]ackup ([0-9]).rdl 282 + *- [Bb]ackup ([0-9][0-9]).rdl 283 + 284 + # Microsoft Fakes 285 + FakesAssemblies/ 286 + 287 + # GhostDoc plugin setting file 288 + *.GhostDoc.xml 289 + 290 + # Node.js Tools for Visual Studio 291 + .ntvs_analysis.dat 292 + node_modules/ 293 + 294 + # Visual Studio 6 build log 295 + *.plg 296 + 297 + # Visual Studio 6 workspace options file 298 + *.opt 299 + 300 + # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 + *.vbw 302 + 303 + # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 + *.vbp 305 + 306 + # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 + *.dsw 308 + *.dsp 309 + 310 + # Visual Studio 6 technical files 311 + *.ncb 312 + *.aps 313 + 314 + # Visual Studio LightSwitch build output 315 + **/*.HTMLClient/GeneratedArtifacts 316 + **/*.DesktopClient/GeneratedArtifacts 317 + **/*.DesktopClient/ModelManifest.xml 318 + **/*.Server/GeneratedArtifacts 319 + **/*.Server/ModelManifest.xml 320 + _Pvt_Extensions 321 + 322 + # Paket dependency manager 323 + .paket/paket.exe 324 + paket-files/ 325 + 326 + # FAKE - F# Make 327 + .fake/ 328 + 329 + # CodeRush personal settings 330 + .cr/personal 331 + 332 + # Python Tools for Visual Studio (PTVS) 333 + __pycache__/ 334 + *.pyc 335 + 336 + # Cake - Uncomment if you are using it 337 + # tools/** 338 + # !tools/packages.config 339 + 340 + # Tabs Studio 341 + *.tss 342 + 343 + # Telerik's JustMock configuration file 344 + *.jmconfig 345 + 346 + # BizTalk build output 347 + *.btp.cs 348 + *.btm.cs 349 + *.odx.cs 350 + *.xsd.cs 351 + 352 + # OpenCover UI analysis results 353 + OpenCover/ 354 + 355 + # Azure Stream Analytics local run output 356 + ASALocalRun/ 357 + 358 + # MSBuild Binary and Structured Log 359 + *.binlog 360 + 361 + # NVidia Nsight GPU debugger configuration file 362 + *.nvuser 363 + 364 + # MFractors (Xamarin productivity tool) working folder 365 + .mfractor/ 366 + 367 + # Local History for Visual Studio 368 + .localhistory/ 369 + 370 + # Visual Studio History (VSHistory) files 371 + .vshistory/ 372 + 373 + # BeatPulse healthcheck temp database 374 + healthchecksdb 375 + 376 + # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 + MigrationBackup/ 378 + 379 + # Ionide (cross platform F# VS Code tools) working folder 380 + .ionide/ 381 + 382 + # Fody - auto-generated XML schema 383 + FodyWeavers.xsd 384 + 385 + # VS Code files for those working on multiple tools 386 + .vscode/* 387 + !.vscode/settings.json 388 + !.vscode/tasks.json 389 + !.vscode/launch.json 390 + !.vscode/extensions.json 391 + *.code-workspace 392 + 393 + # Local History for Visual Studio Code 394 + .history/ 395 + 396 + # Windows Installer files from build outputs 397 + *.cab 398 + *.msi 399 + *.msix 400 + *.msm 401 + *.msp 402 + 403 + # JetBrains Rider 404 + *.sln.iml 405 + .idea/ 406 + 407 + ## 408 + ## Visual studio for Mac 409 + ## 410 + 411 + 412 + # globs 413 + Makefile.in 414 + *.userprefs 415 + *.usertasks 416 + config.make 417 + config.status 418 + aclocal.m4 419 + install-sh 420 + autom4te.cache/ 421 + *.tar.gz 422 + tarballs/ 423 + test-results/ 424 + 425 + # Mac bundle stuff 426 + *.dmg 427 + *.app 428 + 429 + # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore 430 + # General 431 + .DS_Store 432 + .AppleDouble 433 + .LSOverride 434 + 435 + # Icon must end with two \r 436 + Icon 437 + 438 + 439 + # Thumbnails 440 + ._* 441 + 442 + # Files that might appear in the root of a volume 443 + .DocumentRevisions-V100 444 + .fseventsd 445 + .Spotlight-V100 446 + .TemporaryItems 447 + .Trashes 448 + .VolumeIcon.icns 449 + .com.apple.timemachine.donotpresent 450 + 451 + # Directories potentially created on remote AFP share 452 + .AppleDB 453 + .AppleDesktop 454 + Network Trash Folder 455 + Temporary Items 456 + .apdisk 457 + 458 + # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore 459 + # Windows thumbnail cache files 460 + Thumbs.db 461 + ehthumbs.db 462 + ehthumbs_vista.db 463 + 464 + # Dump file 465 + *.stackdump 466 + 467 + # Folder config file 468 + [Dd]esktop.ini 469 + 470 + # Recycle Bin used on file shares 471 + $RECYCLE.BIN/ 472 + 473 + # Windows Installer files 474 + *.cab 475 + *.msi 476 + *.msix 477 + *.msm 478 + *.msp 479 + 480 + # Windows shortcuts 481 + *.lnk 482 + 483 + # Vim temporary swap files 484 + *.swp
+21
AltBot.Api/AltBot.Api.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk.Web"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>net9.0</TargetFramework> 5 + <ImplicitUsings>enable</ImplicitUsings> 6 + <Nullable>enable</Nullable> 7 + </PropertyGroup> 8 + 9 + <ItemGroup> 10 + <ProjectReference Include="..\AltBot.Data\AltBot.Data.csproj" /> 11 + <ProjectReference Include="..\AltBot.ServiceDefaults\AltBot.ServiceDefaults.csproj" /> 12 + <ProjectReference Include="..\AltBot.Core\AltBot.Core.csproj" /> 13 + </ItemGroup> 14 + 15 + <ItemGroup> 16 + <PackageReference Include="Microsoft.AspNetCore.OpenApi" /> 17 + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> 18 + <PackageReference Include="Scalar.AspNetCore" /> 19 + </ItemGroup> 20 + 21 + </Project>
+163
AltBot.Api/Extensions.cs
··· 1 + using AltBot.Core.Models; 2 + using AltBot.Data; 3 + using AltBot.ServiceDefaults; 4 + using Microsoft.EntityFrameworkCore; 5 + 6 + namespace AltBot.Api; 7 + 8 + public static class Extensions 9 + { 10 + // DTOs 11 + record SubscriberCreate(Did Did, string? Handle); 12 + record SubscriberUpdate(string? Handle, bool Active, string Rkey); 13 + record PostCreate(Did Did, Cid Cid, string Rkey); 14 + 15 + public static void MapEndpoints(this WebApplication app) 16 + { 17 + app.MapGet("/health", () => Results.Ok("Healthy")); 18 + 19 + // Subscribers endpoints 20 + app.MapGet("/subscribers", async (DataContext db) => 21 + { 22 + var subscribers = await db.Subscribers 23 + .Select(s => new 24 + { 25 + s.Did, 26 + s.Handle, 27 + s.Active, 28 + s.Timestamp, 29 + s.Label 30 + }) 31 + .ToListAsync(); 32 + return Results.Ok(subscribers); 33 + }) 34 + .WithName("GetSubscribers") 35 + .WithOpenApi(); 36 + 37 + app.MapGet("/subscribers/{did}", async (Did did, DataContext db) => 38 + { 39 + var subscriber = await db.Subscribers.FirstOrDefaultAsync(s => s.Did == did); 40 + return subscriber == null ? Results.NotFound() : Results.Ok(subscriber); 41 + }) 42 + .WithName("GetSubscriber") 43 + .WithOpenApi(); 44 + 45 + app.MapGet("subscribers/{did}/posts", async (Did did, DataContext db) => 46 + { 47 + var posts = await db.Posts 48 + .Where(x => x.Did == did) 49 + .Select(p => new 50 + { 51 + p.Cid, 52 + p.Rkey, 53 + p.Timestamp 54 + }) 55 + .ToListAsync(); 56 + return Results.Ok(posts); 57 + }) 58 + .WithName("GetPosts") 59 + .WithOpenApi(); 60 + 61 + app.MapPost("/subscribers", async (SubscriberCreate request, DataContext db) => 62 + { 63 + if (await db.Subscribers.AnyAsync(s => s.Did == request.Did)) 64 + { 65 + return Results.Conflict("Subscriber already exists"); 66 + } 67 + 68 + var subscriber = new Subscriber 69 + { 70 + Did = request.Did, 71 + Handle = request.Handle, 72 + Active = true, 73 + Timestamp = DateTime.UtcNow 74 + }; 75 + 76 + db.Subscribers.Add(subscriber); 77 + await db.SaveChangesAsync(); 78 + 79 + return Results.Created($"/subscribers/{subscriber.Did}", subscriber); 80 + }) 81 + .WithName("CreateSubscriber") 82 + .WithOpenApi(); 83 + 84 + app.MapPut("/subscribers/{did}", async (Did did, SubscriberUpdate request, DataContext db) => 85 + { 86 + var subscriber = await db.Subscribers.FindAsync(did); 87 + if (subscriber == null) 88 + { 89 + return Results.NotFound(); 90 + } 91 + 92 + subscriber.Handle = request.Handle ?? subscriber.Handle; 93 + subscriber.Active = request.Active; 94 + subscriber.Rkey = request.Rkey; 95 + 96 + await db.SaveChangesAsync(); 97 + return Results.Ok(subscriber); 98 + }) 99 + .WithName("UpdateSubscriber") 100 + .WithOpenApi(); 101 + 102 + app.MapDelete("/subscribers/{did}", async (Did did, DataContext db) => 103 + { 104 + var subscriber = await db.Subscribers.FindAsync(did); 105 + if (subscriber == null) 106 + { 107 + return Results.NotFound(); 108 + } 109 + 110 + db.Subscribers.Remove(subscriber); 111 + await db.SaveChangesAsync(); 112 + 113 + return Results.NoContent(); 114 + }) 115 + .WithName("DeleteSubscriber") 116 + .WithOpenApi(); 117 + 118 + 119 + // Posts endpoints 120 + app.MapPost("/posts", async (PostCreate request, DataContext db) => 121 + { 122 + if (!await db.Subscribers.AnyAsync(s => s.Did == request.Did)) 123 + { 124 + return Results.NotFound("Subscriber not found"); 125 + } 126 + 127 + var post = new ImagePost 128 + { 129 + Did = request.Did, 130 + Cid = request.Cid, 131 + Rkey = request.Rkey, 132 + Timestamp = DateTime.UtcNow 133 + }; 134 + 135 + db.Posts.Add(post); 136 + await db.SaveChangesAsync(); 137 + 138 + return Results.Created($"/subscribers/{post.Did}/posts/{post.Cid}", post); 139 + }) 140 + .WithName("CreatePost") 141 + .WithOpenApi(); 142 + 143 + app.MapDelete("/posts/{did}/{cid}/{rKey}", async (Did did, Cid cid, string rKey, DataContext db) => 144 + { 145 + var post = await db.Posts.FindAsync(did, cid); 146 + if (post == null) 147 + { 148 + return Results.NotFound(); 149 + } 150 + 151 + db.Posts.Remove(post); 152 + await db.SaveChangesAsync(); 153 + 154 + return Results.NoContent(); 155 + }) 156 + .WithName("DeletePost") 157 + .WithOpenApi(); 158 + 159 + app.MapDefaultEndpoints(); 160 + 161 + app.Run(); 162 + } 163 + }
+52
AltBot.Api/Program.cs
··· 1 + using AltBot.Api; 2 + using AltBot.Data; 3 + using AltBot.ServiceDefaults; 4 + using Microsoft.EntityFrameworkCore; 5 + using Scalar.AspNetCore; 6 + 7 + var builder = WebApplication.CreateBuilder(args); 8 + 9 + // Add service defaults & Aspire client integrations. 10 + builder.AddServiceDefaults(); 11 + 12 + // Add services to the container. 13 + builder.Services.AddProblemDetails(); 14 + 15 + // Add DbContext 16 + builder.Services.AddDbContext<DataContext>(options => 17 + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); 18 + 19 + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi 20 + builder.Services.AddOpenApi(); 21 + 22 + var app = builder.Build(); 23 + 24 + // Configure the HTTP request pipeline. 25 + app.UseExceptionHandler(); 26 + 27 + if (app.Environment.IsDevelopment()) 28 + { 29 + app.MapOpenApi(); 30 + app.MapScalarApiReference(options => 31 + { 32 + List<ScalarServer> servers = []; 33 + 34 + string? httpsPort = Environment.GetEnvironmentVariable("ASPNETCORE_HTTPS_PORT"); 35 + if (httpsPort is not null) 36 + { 37 + servers.Add(new ScalarServer($"https://localhost:{httpsPort}")); 38 + } 39 + 40 + string? httpPort = Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORT"); 41 + if (httpPort is not null) 42 + { 43 + servers.Add(new ScalarServer($"http://localhost:{httpPort}")); 44 + } 45 + 46 + options.Servers = servers; 47 + options.Title = "AltHeroes Bot Management API"; 48 + options.ShowSidebar = true; 49 + }); 50 + } 51 + 52 + app.MapEndpoints();
+23
AltBot.Api/Properties/launchSettings.json
··· 1 + { 2 + "$schema": "https://json.schemastore.org/launchsettings.json", 3 + "profiles": { 4 + "http": { 5 + "commandName": "Project", 6 + "dotnetRunMessages": true, 7 + "launchBrowser": false, 8 + "applicationUrl": "http://localhost:5535", 9 + "environmentVariables": { 10 + "ASPNETCORE_ENVIRONMENT": "Development" 11 + } 12 + }, 13 + "https": { 14 + "commandName": "Project", 15 + "dotnetRunMessages": true, 16 + "launchBrowser": false, 17 + "applicationUrl": "https://localhost:7478;http://localhost:5535", 18 + "environmentVariables": { 19 + "ASPNETCORE_ENVIRONMENT": "Development" 20 + } 21 + } 22 + } 23 + }
+8
AltBot.Api/appsettings.Development.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning" 6 + } 7 + } 8 + }
+12
AltBot.Api/appsettings.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning" 6 + } 7 + }, 8 + "AllowedHosts": "*", 9 + "ConnectionStrings": { 10 + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=LabelerBot;Trusted_Connection=True;MultipleActiveResultSets=true" 11 + } 12 + }
+22
AltBot.AppHost/AltBot.AppHost.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk"> 2 + 3 + <Sdk Name="Aspire.AppHost.Sdk" Version="9.3.1" /> 4 + 5 + <PropertyGroup> 6 + <OutputType>Exe</OutputType> 7 + <TargetFramework>net9.0</TargetFramework> 8 + <ImplicitUsings>enable</ImplicitUsings> 9 + <Nullable>enable</Nullable> 10 + <UserSecretsId>c08ca60f-6055-49fe-bb10-e2b939dbdf4f</UserSecretsId> 11 + </PropertyGroup> 12 + 13 + <ItemGroup> 14 + <ProjectReference Include="..\AltBot.Api\AltBot.Api.csproj" /> 15 + <ProjectReference Include="..\AltBot.Worker\AltBot.Worker.csproj" /> 16 + </ItemGroup> 17 + 18 + <ItemGroup> 19 + <PackageReference Include="Aspire.Hosting.AppHost" /> 20 + </ItemGroup> 21 + 22 + </Project>
+8
AltBot.AppHost/AppHost.cs
··· 1 + var builder = DistributedApplication.CreateBuilder(args); 2 + 3 + var apiService = builder.AddProject<Projects.AltBot_Api>("apiservice") 4 + .WithHttpHealthCheck("/health"); 5 + 6 + builder.AddProject<Projects.AltBot_Worker>("altbot-worker"); 7 + 8 + builder.Build().Run();
+29
AltBot.AppHost/Properties/launchSettings.json
··· 1 + { 2 + "$schema": "https://json.schemastore.org/launchsettings.json", 3 + "profiles": { 4 + "https": { 5 + "commandName": "Project", 6 + "dotnetRunMessages": true, 7 + "launchBrowser": true, 8 + "applicationUrl": "https://localhost:17034;http://localhost:15182", 9 + "environmentVariables": { 10 + "ASPNETCORE_ENVIRONMENT": "Development", 11 + "DOTNET_ENVIRONMENT": "Development", 12 + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21244", 13 + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22128" 14 + } 15 + }, 16 + "http": { 17 + "commandName": "Project", 18 + "dotnetRunMessages": true, 19 + "launchBrowser": true, 20 + "applicationUrl": "http://localhost:15182", 21 + "environmentVariables": { 22 + "ASPNETCORE_ENVIRONMENT": "Development", 23 + "DOTNET_ENVIRONMENT": "Development", 24 + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19021", 25 + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20070" 26 + } 27 + } 28 + } 29 + }
+8
AltBot.AppHost/appsettings.Development.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning" 6 + } 7 + } 8 + }
+9
AltBot.AppHost/appsettings.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.AspNetCore": "Warning", 6 + "Aspire.Hosting.Dcp": "Warning" 7 + } 8 + } 9 + }
+14
AltBot.Core/AltBot.Core.csproj
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <Project Sdk="Microsoft.NET.Sdk"> 3 + 4 + <PropertyGroup> 5 + <TargetFramework>net9.0</TargetFramework> 6 + <ImplicitUsings>enable</ImplicitUsings> 7 + <Nullable>enable</Nullable> 8 + </PropertyGroup> 9 + 10 + <ItemGroup> 11 + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> 12 + </ItemGroup> 13 + 14 + </Project>
+116
AltBot.Core/Models/Cid.cs
··· 1 + using Microsoft.Extensions.Logging; 2 + using System.Diagnostics.CodeAnalysis; 3 + using System.Text.RegularExpressions; 4 + 5 + namespace AltBot.Core.Models; 6 + 7 + /// <summary> 8 + /// Represents a Content Identifier (CID) 9 + /// </summary> 10 + public class Cid : IParsable<Cid> 11 + { 12 + //private static readonly ILogger<Cid>? _logger; 13 + private const int MaxLength = 1024; 14 + 15 + // Internal constructor for EF Core 16 + internal Cid(string value) 17 + { 18 + Value = value; 19 + } 20 + 21 + public string Value { get; } 22 + 23 + /// <summary> 24 + /// Creates a new Cid instance after validating the input string 25 + /// </summary> 26 + /// <param name="rawCid">The raw CID string to validate</param> 27 + /// <returns>A new Cid instance if valid, null otherwise</returns> 28 + public static Cid? Create(string rawCid) 29 + { 30 + return ValidateCid(rawCid) ? new Cid(rawCid) : null; 31 + } 32 + 33 + // For use when the Cid is known to be valid (e.g., loading from database) 34 + public static Cid Load(string rawCid) 35 + { 36 + return new Cid(rawCid); 37 + } 38 + 39 + private static bool ValidateCid(string cid) 40 + { 41 + if (string.IsNullOrWhiteSpace(cid)) 42 + { 43 + //_logger?.LogError("CID cannot be null or empty"); 44 + return false; 45 + } 46 + 47 + if (cid.Length > MaxLength) 48 + { 49 + //_logger?.LogError("CID is too long ({MaxLength} chars max)", MaxLength); 50 + return false; 51 + } 52 + 53 + // Basic CID validation - should be base32 or base58 encoded 54 + if (!Regex.IsMatch(cid, "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$")) 55 + { 56 + //_logger?.LogError("CID contains invalid characters"); 57 + return false; 58 + } 59 + 60 + return true; 61 + } 62 + 63 + public static Cid Parse(string s, IFormatProvider? provider) 64 + { 65 + if (TryParse(s, provider, out var result)) 66 + { 67 + return result; 68 + } 69 + throw new FormatException($"Unable to parse CID: {s}"); 70 + } 71 + 72 + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Cid result) 73 + { 74 + if (s != null && ValidateCid(s)) 75 + { 76 + result = new Cid(s); 77 + return true; 78 + } 79 + result = null; 80 + return false; 81 + } 82 + 83 + public override string ToString() => Value; 84 + 85 + public override bool Equals(object? obj) 86 + { 87 + if (obj is Cid other) 88 + { 89 + return Value.Equals(other.Value, StringComparison.Ordinal); 90 + } 91 + return false; 92 + } 93 + 94 + public override int GetHashCode() => Value.GetHashCode(StringComparison.Ordinal); 95 + 96 + public static implicit operator string(Cid cid) => cid.Value; 97 + 98 + public static explicit operator Cid(string s) => Parse(s, null); 99 + 100 + public static bool operator ==(Cid? left, Cid? right) 101 + { 102 + if (ReferenceEquals(left, right)) 103 + { 104 + return true; 105 + } 106 + 107 + if (left is null || right is null) 108 + { 109 + return false; 110 + } 111 + 112 + return left.Equals(right); 113 + } 114 + 115 + public static bool operator !=(Cid? left, Cid? right) => !(left == right); 116 + }
+115
AltBot.Core/Models/Did.cs
··· 1 + using System.Diagnostics.CodeAnalysis; 2 + using System.Text.RegularExpressions; 3 + using Microsoft.Extensions.Logging; 4 + 5 + namespace AltBot.Core.Models; 6 + 7 + /// <summary> 8 + /// Represents a Decentralized Identifier (DID) 9 + /// </summary> 10 + public class Did : IParsable<Did> 11 + { 12 + //private static readonly ILogger<Did>? _logger; 13 + private const int MaxLength = 2048; 14 + 15 + // Internal constructor for EF Core 16 + internal Did(string value) 17 + { 18 + Value = value; 19 + } 20 + 21 + public string Value { get; } 22 + 23 + /// <summary> 24 + /// Creates a new Did instance after validating the input string 25 + /// </summary> 26 + /// <param name="rawDid">The raw DID string to validate</param> 27 + /// <returns>A new Did instance if valid, null otherwise</returns> 28 + public static Did? Create(string rawDid) 29 + { 30 + return ValidateDid(rawDid) ? new Did(rawDid) : null; 31 + } 32 + 33 + // For use when the Did is known to be valid (e.g., loading from database) 34 + public static Did Load(string rawDid) 35 + { 36 + return new Did(rawDid); 37 + } 38 + 39 + private static bool ValidateDid(string did) 40 + { 41 + if (string.IsNullOrWhiteSpace(did)) 42 + { 43 + //_logger?.LogError("DID cannot be null or empty"); 44 + return false; 45 + } 46 + 47 + if (did.Split(':').Length < 3) 48 + { 49 + //_logger?.LogError("DID requires prefix, method, and method-specific content"); 50 + return false; 51 + } 52 + 53 + if (did.Length > MaxLength) 54 + { 55 + //_logger?.LogError("DID is too long ({MaxLength} chars max)", MaxLength); 56 + return false; 57 + } 58 + 59 + return true; 60 + } 61 + 62 + public static Did Parse(string s, IFormatProvider? provider) 63 + { 64 + if (TryParse(s, provider, out var result)) 65 + { 66 + return result; 67 + } 68 + throw new FormatException($"Unable to parse DID: {s}"); 69 + } 70 + 71 + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Did result) 72 + { 73 + if (s != null && ValidateDid(s)) 74 + { 75 + result = new Did(s); 76 + return true; 77 + } 78 + result = null; 79 + return false; 80 + } 81 + 82 + public override string ToString() => Value; 83 + 84 + public override bool Equals(object? obj) 85 + { 86 + if (obj is Did other) 87 + { 88 + return Value.Equals(other.Value, StringComparison.Ordinal); 89 + } 90 + return false; 91 + } 92 + 93 + public override int GetHashCode() => Value.GetHashCode(StringComparison.Ordinal); 94 + 95 + public static implicit operator string(Did did) => did.Value; 96 + 97 + public static explicit operator Did(string s) => Parse(s, null); 98 + 99 + public static bool operator ==(Did? left, Did? right) 100 + { 101 + if (ReferenceEquals(left, right)) 102 + { 103 + return true; 104 + } 105 + 106 + if (left is null || right is null) 107 + { 108 + return false; 109 + } 110 + 111 + return left.Equals(right); 112 + } 113 + 114 + public static bool operator !=(Did? left, Did? right) => !(left == right); 115 + }
+12
AltBot.Core/Models/ImagePost.cs
··· 1 + 2 + namespace AltBot.Core.Models; 3 + 4 + public class ImagePost 5 + { 6 + public required Did Did { get; set; } 7 + public required Cid Cid { get; set; } 8 + public string? Rkey { get; set; } 9 + public bool ValidAlt { get; set; } 10 + public DateTime Timestamp { get; set; } 11 + public virtual Subscriber? Subscriber { get; set; } 12 + }
+10
AltBot.Core/Models/LabelLevel.cs
··· 1 + namespace AltBot.Core.Models; 2 + 3 + public enum LabelLevel 4 + { 5 + None = 0, 6 + Bronze = 70, 7 + Silver = 85, 8 + Gold = 95, 9 + Hero = 100 10 + }
+10
AltBot.Core/Models/LabelLevelEnum.cs
··· 1 + namespace AltBot.Data; 2 + 3 + public enum LabelLevel 4 + { 5 + None = 0, 6 + Bronze = 70, 7 + Silver = 85, 8 + Gold = 95, 9 + Hero = 100 10 + }
+13
AltBot.Core/Models/Subscriber.cs
··· 1 + 2 + namespace AltBot.Core.Models; 3 + 4 + public class Subscriber 5 + { 6 + public required Did Did { get; set; } 7 + public DateTime Timestamp { get; set; } 8 + public bool Active { get; set; } 9 + public string? Handle { get; set; } 10 + public string? Rkey { get; set; } 11 + public virtual ICollection<ImagePost> Posts { get; set; } = new List<ImagePost>(); 12 + public virtual LabelLevel Label { get; set; } 13 + }
+29
AltBot.Data/AltBot.Data.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>net9.0</TargetFramework> 5 + <ImplicitUsings>enable</ImplicitUsings> 6 + <Nullable>enable</Nullable> 7 + </PropertyGroup> 8 + 9 + <ItemGroup> 10 + <Compile Remove="sql\**" /> 11 + <EmbeddedResource Remove="sql\**" /> 12 + <None Remove="sql\**" /> 13 + </ItemGroup> 14 + 15 + <ItemGroup> 16 + <PackageReference Include="Microsoft.EntityFrameworkCore" /> 17 + <PackageReference Include="Microsoft.EntityFrameworkCore.Design"> 18 + <PrivateAssets>all</PrivateAssets> 19 + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> 20 + </PackageReference> 21 + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> 22 + <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /> 23 + </ItemGroup> 24 + 25 + <ItemGroup> 26 + <ProjectReference Include="..\AltBot.Core\AltBot.Core.csproj" /> 27 + </ItemGroup> 28 + 29 + </Project>
+81
AltBot.Data/DataContext.cs
··· 1 + using AltBot.Core.Models; 2 + using Microsoft.EntityFrameworkCore; 3 + using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 4 + 5 + namespace AltBot.Data; 6 + 7 + public class DataContext(DbContextOptions<DataContext> options) : DbContext(options) 8 + { 9 + public DbSet<ImagePost> Posts { get; set; } 10 + public DbSet<Subscriber> Subscribers { get; set; } 11 + 12 + protected override void OnModelCreating(ModelBuilder modelBuilder) 13 + { 14 + base.OnModelCreating(modelBuilder); 15 + 16 + var didConverter = new ValueConverter<Did, string>( 17 + v => v.Value, 18 + v => Did.Load(v)); 19 + 20 + var labelConverter = new ValueConverter<LabelLevel, string>( 21 + v => v.ToString(), 22 + v => Enum.Parse<LabelLevel>(v)); 23 + 24 + modelBuilder.Entity<ImagePost>(entity => 25 + { 26 + entity.ToTable("imagePost"); 27 + 28 + entity.HasKey(x => new { x.Did, x.Cid }); 29 + 30 + entity.Property(x => x.Did) 31 + .HasColumnName("did") 32 + .HasMaxLength(2048) 33 + .HasConversion(didConverter); 34 + 35 + entity.Property(x => x.Cid) 36 + .HasColumnName("cid") 37 + .HasMaxLength(1000) 38 + .HasConversion( 39 + v => v.ToString(), 40 + v => Cid.Load(v)); 41 + 42 + entity.Property(x => x.Rkey) 43 + .HasColumnName("rKey") 44 + .HasMaxLength(512); 45 + 46 + entity.HasOne(x => x.Subscriber) 47 + .WithMany(x => x.Posts) 48 + .HasForeignKey(x => x.Did) 49 + .HasConstraintName("FK_ImagePost_Subscriber"); 50 + }); 51 + 52 + modelBuilder.Entity<Subscriber>(entity => 53 + { 54 + entity.ToTable("subscriber"); 55 + 56 + entity.HasKey(x => x.Did); 57 + 58 + entity.Property(x => x.Did) 59 + .HasColumnName("did") 60 + .HasMaxLength(2048) 61 + .HasConversion(didConverter); 62 + 63 + entity.Property(x => x.Active) 64 + .HasDefaultValue(true); 65 + 66 + entity.Property(x => x.Handle) 67 + .IsUnicode() 68 + .HasMaxLength(250); 69 + 70 + entity.Property(x => x.Label) 71 + .HasColumnName("label") 72 + .HasConversion(labelConverter) 73 + .HasDefaultValue(LabelLevel.None); 74 + 75 + 76 + entity.Property(x => x.Rkey) 77 + .HasColumnName("RKey") 78 + .HasMaxLength(100); 79 + }); 80 + } 81 + }
+28
AltBot.ServiceDefaults/AltBot.ServiceDefaults.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>net9.0</TargetFramework> 5 + <ImplicitUsings>enable</ImplicitUsings> 6 + <Nullable>enable</Nullable> 7 + <IsAspireSharedProject>true</IsAspireSharedProject> 8 + </PropertyGroup> 9 + 10 + <ItemGroup> 11 + <FrameworkReference Include="Microsoft.AspNetCore.App" /> 12 + 13 + <PackageReference Include="Microsoft.Extensions.Http.Resilience" /> 14 + <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" /> 15 + <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" /> 16 + <PackageReference Include="OpenTelemetry.Extensions.Hosting" /> 17 + <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" /> 18 + <PackageReference Include="OpenTelemetry.Instrumentation.Http" /> 19 + <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" /> 20 + <PackageReference Include="Serilog.AspNetCore" /> 21 + <PackageReference Include="Serilog.Enrichers.Environment" /> 22 + <PackageReference Include="Serilog.Enrichers.Thread" /> 23 + <PackageReference Include="Serilog.Settings.Configuration" /> 24 + <PackageReference Include="Serilog.Sinks.Console" /> 25 + <PackageReference Include="Serilog.Sinks.Debug" /> 26 + </ItemGroup> 27 + 28 + </Project>
+151
AltBot.ServiceDefaults/Extensions.cs
··· 1 + using Microsoft.AspNetCore.Builder; 2 + using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 + using Microsoft.Extensions.DependencyInjection; 4 + using Microsoft.Extensions.Diagnostics.HealthChecks; 5 + using Microsoft.Extensions.Hosting; 6 + using Microsoft.Extensions.Logging; 7 + using Microsoft.Extensions.ServiceDiscovery; 8 + using OpenTelemetry; 9 + using OpenTelemetry.Metrics; 10 + using OpenTelemetry.Trace; 11 + using Serilog; 12 + using Serilog.Events; 13 + 14 + namespace AltBot.ServiceDefaults; 15 + 16 + // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. 17 + // This project should be referenced by each service project in your solution. 18 + // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults 19 + public static class Extensions 20 + { 21 + private const string HealthEndpointPath = "/health"; 22 + private const string AlivenessEndpointPath = "/alive"; 23 + 24 + public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder 25 + { 26 + builder.ConfigureSerilog(); 27 + 28 + builder.ConfigureOpenTelemetry(); 29 + 30 + builder.AddDefaultHealthChecks(); 31 + 32 + builder.Services.AddServiceDiscovery(); 33 + 34 + builder.Services.ConfigureHttpClientDefaults(http => 35 + { 36 + // Turn on resilience by default 37 + http.AddStandardResilienceHandler(); 38 + 39 + // Turn on service discovery by default 40 + http.AddServiceDiscovery(); 41 + }); 42 + 43 + return builder; 44 + } 45 + 46 + private static TBuilder ConfigureSerilog<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder 47 + { 48 + // Create Serilog logger configuration 49 + var loggerConfiguration = new LoggerConfiguration() 50 + .ReadFrom.Configuration(builder.Configuration) 51 + .MinimumLevel.Information() 52 + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 53 + .MinimumLevel.Override("System", LogEventLevel.Warning) 54 + .Enrich.FromLogContext() 55 + .Enrich.WithEnvironmentName() 56 + .Enrich.WithThreadId(); 57 + 58 + // Add console sink in development 59 + if (builder.Environment.IsDevelopment()) 60 + { 61 + loggerConfiguration 62 + .WriteTo.Console() 63 + .WriteTo.Debug(); 64 + } 65 + else 66 + { 67 + loggerConfiguration 68 + .WriteTo.Console(); 69 + } 70 + 71 + // Create logger 72 + Log.Logger = loggerConfiguration.CreateLogger(); 73 + 74 + // Configure application to use Serilog 75 + builder.Logging.AddSerilog(dispose: true); 76 + 77 + return builder; 78 + } 79 + 80 + public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder 81 + { 82 + builder.Logging.AddOpenTelemetry(logging => 83 + { 84 + logging.IncludeFormattedMessage = true; 85 + logging.IncludeScopes = true; 86 + }); 87 + 88 + builder.Services.AddOpenTelemetry() 89 + .WithMetrics(metrics => 90 + { 91 + metrics.AddAspNetCoreInstrumentation() 92 + .AddHttpClientInstrumentation() 93 + .AddRuntimeInstrumentation(); 94 + }) 95 + .WithTracing(tracing => 96 + { 97 + tracing.AddSource(builder.Environment.ApplicationName) 98 + .AddAspNetCoreInstrumentation(tracing => 99 + // Exclude health check requests from tracing 100 + tracing.Filter = context => 101 + !context.Request.Path.StartsWithSegments(HealthEndpointPath) 102 + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) 103 + ) 104 + .AddHttpClientInstrumentation(); 105 + }); 106 + 107 + builder.AddOpenTelemetryExporters(); 108 + 109 + return builder; 110 + } 111 + 112 + private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder 113 + { 114 + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); 115 + 116 + if (useOtlpExporter) 117 + { 118 + builder.Services.AddOpenTelemetry().UseOtlpExporter(); 119 + } 120 + 121 + return builder; 122 + } 123 + 124 + public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder 125 + { 126 + builder.Services.AddHealthChecks() 127 + // Add a default liveness check to ensure app is responsive 128 + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); 129 + 130 + return builder; 131 + } 132 + 133 + public static WebApplication MapDefaultEndpoints(this WebApplication app) 134 + { 135 + // Adding health checks endpoints to applications in non-development environments has security implications. 136 + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. 137 + if (app.Environment.IsDevelopment()) 138 + { 139 + // All health checks must pass for app to be considered ready to accept traffic after starting 140 + app.MapHealthChecks(HealthEndpointPath); 141 + 142 + // Only health checks tagged with the "live" tag must pass for app to be considered alive 143 + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions 144 + { 145 + Predicate = r => r.Tags.Contains("live") 146 + }); 147 + } 148 + 149 + return app; 150 + } 151 + }
+19
AltBot.Worker/AltBot.Worker.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk.Worker"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>net9.0</TargetFramework> 5 + <Nullable>enable</Nullable> 6 + <ImplicitUsings>enable</ImplicitUsings> 7 + <UserSecretsId>dotnet-AltBot.Worker-b4ad104b-8a9e-44fa-a4dd-9d7d868d5a35</UserSecretsId> 8 + <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> 9 + </PropertyGroup> 10 + 11 + <ItemGroup> 12 + <PackageReference Include="idunno.AtProto" /> 13 + <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> 14 + </ItemGroup> 15 + 16 + <ItemGroup> 17 + <ProjectReference Include="..\AltBot.ServiceDefaults\AltBot.ServiceDefaults.csproj" /> 18 + </ItemGroup> 19 + </Project>
+29
AltBot.Worker/Dockerfile
··· 1 + # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 + 3 + # This stage is used when running from VS in fast mode (Default for Debug configuration) 4 + FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base 5 + USER $APP_UID 6 + WORKDIR /app 7 + 8 + 9 + # This stage is used to build the service project 10 + FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build 11 + ARG BUILD_CONFIGURATION=Release 12 + WORKDIR /src 13 + COPY ["Directory.Packages.props", "."] 14 + COPY ["AltBot.Worker/AltBot.Worker.csproj", "AltBot.Worker/"] 15 + RUN dotnet restore "./AltBot.Worker/AltBot.Worker.csproj" 16 + COPY . . 17 + WORKDIR "/src/AltBot.Worker" 18 + RUN dotnet build "./AltBot.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/build 19 + 20 + # This stage is used to publish the service project to be copied to the final stage 21 + FROM build AS publish 22 + ARG BUILD_CONFIGURATION=Release 23 + RUN dotnet publish "./AltBot.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 24 + 25 + # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) 26 + FROM base AS final 27 + WORKDIR /app 28 + COPY --from=publish /app/publish . 29 + ENTRYPOINT ["dotnet", "AltBot.Worker.dll"]
+10
AltBot.Worker/Program.cs
··· 1 + using AltBot.ServiceDefaults; 2 + using AltBot.Worker; 3 + 4 + var builder = Host.CreateApplicationBuilder(args); 5 + 6 + builder.AddServiceDefaults(); 7 + builder.Services.AddHostedService<Worker>(); 8 + 9 + var host = builder.Build(); 10 + host.Run();
+15
AltBot.Worker/Properties/launchSettings.json
··· 1 + { 2 + "profiles": { 3 + "AltBot.Worker": { 4 + "commandName": "Project", 5 + "environmentVariables": { 6 + "DOTNET_ENVIRONMENT": "Development" 7 + }, 8 + "dotnetRunMessages": true 9 + }, 10 + "Container (Dockerfile)": { 11 + "commandName": "Docker" 12 + } 13 + }, 14 + "$schema": "https://json.schemastore.org/launchsettings.json" 15 + }
+23
AltBot.Worker/Worker.cs
··· 1 + namespace AltBot.Worker; 2 + 3 + public class Worker : BackgroundService 4 + { 5 + private readonly ILogger<Worker> _logger; 6 + 7 + public Worker(ILogger<Worker> logger) 8 + { 9 + _logger = logger; 10 + } 11 + 12 + protected override async Task ExecuteAsync(CancellationToken stoppingToken) 13 + { 14 + while (!stoppingToken.IsCancellationRequested) 15 + { 16 + if (_logger.IsEnabled(LogLevel.Information)) 17 + { 18 + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 19 + } 20 + await Task.Delay(1000, stoppingToken); 21 + } 22 + } 23 + }
+8
AltBot.Worker/appsettings.Development.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.Hosting.Lifetime": "Information" 6 + } 7 + } 8 + }
+8
AltBot.Worker/appsettings.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "Microsoft.Hosting.Lifetime": "Information" 6 + } 7 + } 8 + }
+54
AltBot.sln
··· 1 + Microsoft Visual Studio Solution File, Format Version 12.00 2 + # Visual Studio Version 18 3 + VisualStudioVersion = 18.0.11018.127 4 + MinimumVisualStudioVersion = 10.0.40219.1 5 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.AppHost", "AltBot.AppHost\AltBot.AppHost.csproj", "{07FAEC9B-E11E-46CD-A30F-55D47BB65CE8}" 6 + EndProject 7 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.ServiceDefaults", "AltBot.ServiceDefaults\AltBot.ServiceDefaults.csproj", "{F928DEAE-0ED9-E7E7-AFAA-F66602E366D8}" 8 + EndProject 9 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.Api", "AltBot.Api\AltBot.Api.csproj", "{B6BBCF2D-C652-3B38-246B-F4AC6ED07B24}" 10 + EndProject 11 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.Data", "AltBot.Data\AltBot.Data.csproj", "{59D55BE0-CE8D-C025-E265-02ACCDD1F49E}" 12 + EndProject 13 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.Core", "AltBot.Core\AltBot.Core.csproj", "{393C2DD8-A5E5-5F63-56D7-0E3890AB6E17}" 14 + EndProject 15 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltBot.Worker", "AltBot.Worker\AltBot.Worker.csproj", "{B0853F52-805C-69D3-53B7-5E7EF2EA6C8F}" 16 + EndProject 17 + Global 18 + GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 + Debug|Any CPU = Debug|Any CPU 20 + Release|Any CPU = Release|Any CPU 21 + EndGlobalSection 22 + GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 + {07FAEC9B-E11E-46CD-A30F-55D47BB65CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 + {07FAEC9B-E11E-46CD-A30F-55D47BB65CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 + {07FAEC9B-E11E-46CD-A30F-55D47BB65CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 + {07FAEC9B-E11E-46CD-A30F-55D47BB65CE8}.Release|Any CPU.Build.0 = Release|Any CPU 27 + {F928DEAE-0ED9-E7E7-AFAA-F66602E366D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 + {F928DEAE-0ED9-E7E7-AFAA-F66602E366D8}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 + {F928DEAE-0ED9-E7E7-AFAA-F66602E366D8}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 + {F928DEAE-0ED9-E7E7-AFAA-F66602E366D8}.Release|Any CPU.Build.0 = Release|Any CPU 31 + {B6BBCF2D-C652-3B38-246B-F4AC6ED07B24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 + {B6BBCF2D-C652-3B38-246B-F4AC6ED07B24}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 + {B6BBCF2D-C652-3B38-246B-F4AC6ED07B24}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 + {B6BBCF2D-C652-3B38-246B-F4AC6ED07B24}.Release|Any CPU.Build.0 = Release|Any CPU 35 + {59D55BE0-CE8D-C025-E265-02ACCDD1F49E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 + {59D55BE0-CE8D-C025-E265-02ACCDD1F49E}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 + {59D55BE0-CE8D-C025-E265-02ACCDD1F49E}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 + {59D55BE0-CE8D-C025-E265-02ACCDD1F49E}.Release|Any CPU.Build.0 = Release|Any CPU 39 + {393C2DD8-A5E5-5F63-56D7-0E3890AB6E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 + {393C2DD8-A5E5-5F63-56D7-0E3890AB6E17}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 + {393C2DD8-A5E5-5F63-56D7-0E3890AB6E17}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 + {393C2DD8-A5E5-5F63-56D7-0E3890AB6E17}.Release|Any CPU.Build.0 = Release|Any CPU 43 + {B0853F52-805C-69D3-53B7-5E7EF2EA6C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 + {B0853F52-805C-69D3-53B7-5E7EF2EA6C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 + {B0853F52-805C-69D3-53B7-5E7EF2EA6C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 + {B0853F52-805C-69D3-53B7-5E7EF2EA6C8F}.Release|Any CPU.Build.0 = Release|Any CPU 47 + EndGlobalSection 48 + GlobalSection(SolutionProperties) = preSolution 49 + HideSolutionNode = FALSE 50 + EndGlobalSection 51 + GlobalSection(ExtensibilityGlobals) = postSolution 52 + SolutionGuid = {91CE63A7-19CC-43C5-AD9D-B1EBC32F805A} 53 + EndGlobalSection 54 + EndGlobal
+40
Directory.Packages.props
··· 1 + <Project> 2 + <PropertyGroup> 3 + <!-- Enable central package management, https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management --> 4 + <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> 5 + </PropertyGroup> 6 + <ItemGroup> 7 + <!-- Aspire --> 8 + <PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.1" /> 9 + <PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.1" /> 10 + <!-- Entity Framework --> 11 + <PackageVersion Include="idunno.AtProto" Version="1.1.0" /> 12 + <PackageVersion Include="idunno.Bluesky" Version="1.1.0" /> 13 + <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.9" /> 14 + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" /> 15 + <PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" /> 16 + <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" /> 17 + <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" /> 18 + <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> 19 + <!-- ASP.NET Core --> 20 + <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" /> 21 + <PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" /> 22 + <PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.1" /> 23 + <!-- Testing --> 24 + <PackageVersion Include="MSTest" Version="3.11.0" /> 25 + <!-- OpenTelemetry --> 26 + <PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" /> 27 + <PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" /> 28 + <PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> 29 + <PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> 30 + <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" /> 31 + <!-- Serilog --> 32 + <PackageVersion Include="Scalar.AspNetCore" Version="2.8.10" /> 33 + <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" /> 34 + <PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" /> 35 + <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> 36 + <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" /> 37 + <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" /> 38 + <PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" /> 39 + </ItemGroup> 40 + </Project>