Compare commits
	
		
			58 Commits
		
	
	
		
			e624c2bb3e
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0b65bf8dd7 | |||
| ab23f87a66 | |||
| 8f1047ff5d | |||
| 43e50a00ce | |||
| 50133684c7 | |||
| befde25266 | |||
| 437f49fb20 | |||
| c3b6358f33 | |||
| 4347281fcd | |||
| 92cd6b5f7e | |||
| cf6e534d02 | |||
| 29c5971554 | |||
| cdfc3f6571 | |||
| f65a7360e2 | |||
| 85e706335a | |||
| fe74060df9 | |||
| e8d5f22395 | |||
| 83fa2568aa | |||
| bf1c8e0a85 | |||
| 323fa8ee15 | |||
| e7a46e96ed | |||
| 3a0dee11a6 | |||
| 43be47d526 | |||
| 48067af034 | |||
| 7e7e90ad24 | |||
| 3af4069581 | |||
| 609b130b4e | |||
| 93f7dfd379 | |||
| 40325c6df5 | |||
| bbcaa27ac5 | |||
| 19d833a522 | |||
| a94102e136 | |||
| fc693793fe | |||
| 8cfdabbae4 | |||
| 985ff41c72 | |||
| a79ea4ac49 | |||
| 7385caff9a | |||
| 15954dbfe2 | |||
| 4ba6206c9d | |||
| 266b9e36e2 | |||
| e6aa61b03b | |||
| 0c09ef25ec | |||
| dd5929c691 | |||
| cf87fdfb49 | |||
| ff03584518 | |||
| d6c37784e1 | |||
| 46ebd92dc1 | |||
| 7f8521bb40 | |||
| f01226d91a | |||
| 6cb6dee6be | |||
| 0e9caf67ff | |||
| ca70bb5487 | |||
| 59ed135f20 | |||
| 6077f91529 | |||
| 5c485bb1c3 | |||
| 27d979d77b | |||
| 15687a0c32 | |||
| 37ea882ef7 | 
							
								
								
									
										72
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										72
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,27 +7,69 @@ on: | |||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|  |   determine-changes: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     outputs: | ||||||
|  |       matrix: ${{ steps.changes.outputs.matrix }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 0 | ||||||
|  |  | ||||||
|  |       - name: Get changed files | ||||||
|  |         id: changed-files | ||||||
|  |         run: | | ||||||
|  |           echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT | ||||||
|  |  | ||||||
|  |       - name: Determine changed services | ||||||
|  |         id: changes | ||||||
|  |         run: | | ||||||
|  |           files="${{ steps.changed-files.outputs.files }}" | ||||||
|  |           matrix="{\"include\":[]}" | ||||||
|  |           services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight") | ||||||
|  |           images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight") | ||||||
|  |           changed_services=() | ||||||
|  |  | ||||||
|  |           for file in $files; do | ||||||
|  |             if [[ "$file" == DysonNetwork.Shared/* ]]; then | ||||||
|  |               changed_services=("${services[@]}") | ||||||
|  |               break | ||||||
|  |             fi | ||||||
|  |             for i in "${!services[@]}"; do | ||||||
|  |               if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then | ||||||
|  |                 # check if service is already in changed_services | ||||||
|  |                 if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then | ||||||
|  |                   changed_services+=("${services[$i]}") | ||||||
|  |                 fi | ||||||
|  |               fi | ||||||
|  |             done | ||||||
|  |           done | ||||||
|  |  | ||||||
|  |           if [ ${#changed_services[@]} -gt 0 ]; then | ||||||
|  |             json_objects="" | ||||||
|  |             for service in "${changed_services[@]}"; do | ||||||
|  |               for i in "${!services[@]}"; do | ||||||
|  |                 if [[ "${services[$i]}" == "$service" ]]; then | ||||||
|  |                   image="${images[$i]}" | ||||||
|  |                   break | ||||||
|  |                 fi | ||||||
|  |               done | ||||||
|  |               json_objects+="{\"service\":\"$service\",\"image\":\"$image\"}," | ||||||
|  |             done | ||||||
|  |             matrix="{\"include\":[${json_objects%,}]}" | ||||||
|  |           fi | ||||||
|  |           echo "matrix=$matrix" >> $GITHUB_OUTPUT | ||||||
|  |  | ||||||
|   build-and-push: |   build-and-push: | ||||||
|  |     needs: determine-changes | ||||||
|  |     if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }} | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|  |  | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }} | ||||||
|         include: |  | ||||||
|           - service: Sphere |  | ||||||
|             image: sphere |  | ||||||
|           - service: Pass |  | ||||||
|             image: pass |  | ||||||
|           - service: Ring |  | ||||||
|             image: ring |  | ||||||
|           - service: Drive |  | ||||||
|             image: drive |  | ||||||
|           - service: Develop |  | ||||||
|             image: develop |  | ||||||
|           - service: Gateway |  | ||||||
|             image: gateway |  | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|   | |||||||
| @@ -21,11 +21,16 @@ var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop" | |||||||
|     .WithReference(passService) |     .WithReference(passService) | ||||||
|     .WithReference(ringService) |     .WithReference(ringService) | ||||||
|     .WithReference(sphereService); |     .WithReference(sphereService); | ||||||
|  | var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight") | ||||||
|  |     .WithReference(passService) | ||||||
|  |     .WithReference(ringService) | ||||||
|  |     .WithReference(sphereService) | ||||||
|  |     .WithReference(developService); | ||||||
|  |  | ||||||
| passService.WithReference(developService).WithReference(driveService); | passService.WithReference(developService).WithReference(driveService); | ||||||
|  |  | ||||||
| List<IResourceBuilder<ProjectResource>> services = | List<IResourceBuilder<ProjectResource>> services = | ||||||
|     [ringService, passService, driveService, sphereService, developService]; |     [ringService, passService, driveService, sphereService, developService, insightService]; | ||||||
|  |  | ||||||
| for (var idx = 0; idx < services.Count; idx++) | for (var idx = 0; idx < services.Count; idx++) | ||||||
| { | { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|   <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" /> |   <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" /> | ||||||
|  |      | ||||||
|   <PropertyGroup> |   <PropertyGroup> | ||||||
|     <OutputType>Exe</OutputType> |     <OutputType>Exe</OutputType> | ||||||
|     <TargetFramework>net9.0</TargetFramework> |     <TargetFramework>net9.0</TargetFramework> | ||||||
| @@ -8,11 +9,12 @@ | |||||||
|     <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> |     <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> | ||||||
|     <RootNamespace>DysonNetwork.Control</RootNamespace> |     <RootNamespace>DysonNetwork.Control</RootNamespace> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |      | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" /> |     <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" /> | ||||||
|     <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> |     <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> | ||||||
|     <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.1" /> |     <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.2" /> | ||||||
|     <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.1" /> |     <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> |     <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> | ||||||
| @@ -21,5 +23,6 @@ | |||||||
|     <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> |     <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> | ||||||
|     <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> |     <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> | ||||||
|     <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" /> |     <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" /> | ||||||
|  |     <ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| </Project> | </Project> | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Design; | using Microsoft.EntityFrameworkCore.Design; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop; | namespace DysonNetwork.Develop; | ||||||
|  |  | ||||||
| @@ -30,6 +31,35 @@ public class AppDatabase( | |||||||
|         base.OnConfiguring(optionsBuilder); |         base.OnConfiguring(optionsBuilder); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) | ||||||
|  |     { | ||||||
|  |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |  | ||||||
|  |         foreach (var entry in ChangeTracker.Entries<ModelBase>()) | ||||||
|  |         { | ||||||
|  |             switch (entry.State) | ||||||
|  |             { | ||||||
|  |                 case EntityState.Added: | ||||||
|  |                     entry.Entity.CreatedAt = now; | ||||||
|  |                     entry.Entity.UpdatedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Modified: | ||||||
|  |                     entry.Entity.UpdatedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Deleted: | ||||||
|  |                     entry.State = EntityState.Modified; | ||||||
|  |                     entry.Entity.DeletedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Detached: | ||||||
|  |                 case EntityState.Unchanged: | ||||||
|  |                 default: | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return await base.SaveChangesAsync(cancellationToken); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) |     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||||
|     { |     { | ||||||
|         base.OnModelCreating(modelBuilder); |         base.OnModelCreating(modelBuilder); | ||||||
|   | |||||||
| @@ -9,16 +9,15 @@ | |||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> |         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> |  | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2"/> |         <PackageReference Include="NodaTime" Version="3.2.2"/> | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/> |         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/> | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ public class BotAccountController( | |||||||
|     DeveloperService ds, |     DeveloperService ds, | ||||||
|     DevProjectService projectService, |     DevProjectService projectService, | ||||||
|     ILogger<BotAccountController> logger, |     ILogger<BotAccountController> logger, | ||||||
|     AccountClientHelper accounts, |     RemoteAccountService remoteAccounts, | ||||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver |     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver | ||||||
| ) | ) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| @@ -222,7 +222,7 @@ public class BotAccountController( | |||||||
|         if (bot is null || bot.ProjectId != projectId) |         if (bot is null || bot.ProjectId != projectId) | ||||||
|             return NotFound("Bot not found"); |             return NotFound("Bot not found"); | ||||||
|  |  | ||||||
|         var botAccount = await accounts.GetBotAccount(bot.Id); |         var botAccount = await remoteAccounts.GetBotAccount(bot.Id); | ||||||
|  |  | ||||||
|         if (request.Name is not null) botAccount.Name = request.Name; |         if (request.Name is not null) botAccount.Name = request.Name; | ||||||
|         if (request.Nick is not null) botAccount.Nick = request.Nick; |         if (request.Nick is not null) botAccount.Nick = request.Nick; | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ namespace DysonNetwork.Develop.Identity; | |||||||
| public class BotAccountService( | public class BotAccountService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, |     BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, | ||||||
|     AccountClientHelper accounts |     RemoteAccountService remoteAccounts | ||||||
| ) | ) | ||||||
| { | { | ||||||
|     public async Task<SnBotAccount?> GetBotByIdAsync(Guid id) |     public async Task<SnBotAccount?> GetBotByIdAsync(Guid id) | ||||||
| @@ -158,7 +158,7 @@ public class BotAccountService( | |||||||
|     public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots) |     public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots) | ||||||
|     { |     { | ||||||
|         var automatedIds = bots.Select(b => b.Id).ToList(); |         var automatedIds = bots.Select(b => b.Id).ToList(); | ||||||
|         var data = await accounts.GetBotAccountBatch(automatedIds); |         var data = await remoteAccounts.GetBotAccountBatch(automatedIds); | ||||||
|  |  | ||||||
|         foreach (var bot in bots) |         foreach (var bot in bots) | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ public class DeveloperController( | |||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); |             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); | ||||||
|             pub = SnPublisher.FromProto(pubResponse.Publisher); |             pub = SnPublisher.FromProtoValue(pubResponse.Publisher); | ||||||
|         } catch (RpcException ex) |         } catch (RpcException ex) | ||||||
|         { |         { | ||||||
|             return NotFound(ex.Status.Detail); |             return NotFound(ex.Status.Detail); | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ public class DeveloperService( | |||||||
|     public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer) |     public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer) | ||||||
|     { |     { | ||||||
|         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); |         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); | ||||||
|         developer.Publisher = SnPublisher.FromProto(pubResponse.Publisher); |         developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher); | ||||||
|         return developer; |         return developer; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -25,7 +25,7 @@ public class DeveloperService( | |||||||
|         var pubRequest = new GetPublisherBatchRequest(); |         var pubRequest = new GetPublisherBatchRequest(); | ||||||
|         pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString())); |         pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString())); | ||||||
|         var pubResponse = await ps.GetPublisherBatchAsync(pubRequest); |         var pubResponse = await ps.GetPublisherBatchAsync(pubRequest); | ||||||
|         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProto); |         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue); | ||||||
|  |  | ||||||
|         return enumerable.Select(d => |         return enumerable.Select(d => | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration); | |||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddDysonAuth(); | builder.Services.AddDysonAuth(); | ||||||
| builder.Services.AddPublisherService(); | builder.Services.AddSphereService(); | ||||||
| builder.Services.AddAccountService(); | builder.Services.AddAccountService(); | ||||||
| builder.Services.AddDriveService(); | builder.Services.AddDriveService(); | ||||||
|  |  | ||||||
| @@ -35,6 +35,6 @@ using (var scope = app.Services.CreateScope()) | |||||||
|  |  | ||||||
| app.ConfigureAppMiddleware(builder.Configuration); | app.ConfigureAppMiddleware(builder.Configuration); | ||||||
|  |  | ||||||
| app.UseSwaggerManifest(); | app.UseSwaggerManifest("DysonNetwork.Develop"); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
| @@ -4,11 +4,7 @@ using DysonNetwork.Shared.Models; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Project; | namespace DysonNetwork.Develop.Project; | ||||||
|  |  | ||||||
| public class DevProjectService( | public class DevProjectService(AppDatabase db ) | ||||||
|     AppDatabase db, |  | ||||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, |  | ||||||
|     FileService.FileServiceClient files |  | ||||||
| ) |  | ||||||
| { | { | ||||||
|     public async Task<SnDevProject> CreateProjectAsync( |     public async Task<SnDevProject> CreateProjectAsync( | ||||||
|         SnDeveloper developer, |         SnDeveloper developer, | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| using DysonNetwork.Develop.Identity; | using DysonNetwork.Develop.Identity; | ||||||
| using DysonNetwork.Shared.Auth; | using DysonNetwork.Shared.Auth; | ||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using Prometheus; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Develop.Startup; | namespace DysonNetwork.Develop.Startup; | ||||||
|  |  | ||||||
| @@ -9,7 +8,6 @@ public static class ApplicationConfiguration | |||||||
| { | { | ||||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) |     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) | ||||||
|     { |     { | ||||||
|         app.MapMetrics(); |  | ||||||
|         app.MapOpenApi(); |         app.MapOpenApi(); | ||||||
|  |  | ||||||
|         app.UseRequestLocalization(); |         app.UseRequestLocalization(); | ||||||
| @@ -23,6 +21,7 @@ public static class ApplicationConfiguration | |||||||
|         app.MapControllers(); |         app.MapControllers(); | ||||||
|          |          | ||||||
|         app.MapGrpcService<CustomAppServiceGrpc>(); |         app.MapGrpcService<CustomAppServiceGrpc>(); | ||||||
|  |         app.MapGrpcReflectionService(); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ public static class ServiceCollectionExtensions | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); |         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); | ||||||
|  |         services.AddGrpcReflection(); | ||||||
|  |  | ||||||
|         services.Configure<RequestLocalizationOptions>(options => |         services.Configure<RequestLocalizationOptions>(options => | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -18,9 +18,5 @@ | |||||||
|   }, |   }, | ||||||
|   "Etcd": { |   "Etcd": { | ||||||
|     "Insecure": true |     "Insecure": true | ||||||
|   }, |  | ||||||
|   "Service": { |  | ||||||
|     "Name": "DysonNetwork.Develop", |  | ||||||
|     "Url": "https://localhost:7192" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,27 +10,27 @@ | |||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> |         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||||
|         <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> |         <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> | ||||||
|         <PackageReference Include="FFMpegCore" Version="5.2.0" /> |         <PackageReference Include="FFMpegCore" Version="5.3.0" /> | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> |         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> |         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|           <PrivateAssets>all</PrivateAssets> |           <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="MimeKit" Version="4.13.0" /> |         <PackageReference Include="MimeKit" Version="4.14.0" /> | ||||||
|         <PackageReference Include="MimeTypes" Version="2.5.2"> |         <PackageReference Include="MimeTypes" Version="2.5.2"> | ||||||
|           <PrivateAssets>all</PrivateAssets> |           <PrivateAssets>all</PrivateAssets> | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="Minio" Version="6.0.5" /> |         <PackageReference Include="Minio" Version="6.0.5" /> | ||||||
|         <PackageReference Include="Nanoid" Version="3.1.0" /> |         <PackageReference Include="Nanoid" Version="3.1.0" /> | ||||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> |         <PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118"> | ||||||
|           <PrivateAssets>all</PrivateAssets> |           <PrivateAssets>all</PrivateAssets> | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="NetVips" Version="3.1.0" /> |         <PackageReference Include="NetVips" Version="3.1.0" /> | ||||||
|         <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" /> |         <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.2" /> | ||||||
|         <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" /> |         <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.2" /> | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> |         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> |         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||||
| @@ -38,27 +38,21 @@ | |||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" /> | ||||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" /> |         <PackageReference Include="Quartz" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" /> |         <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> |         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" /> |         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" /> | ||||||
|         <PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" /> |         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" /> | ||||||
|         <PackageReference Include="Quartz" Version="3.14.0" /> |  | ||||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.14.0" /> |  | ||||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" /> |  | ||||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> |  | ||||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" /> |  | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" /> |         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" /> | ||||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" /> |         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> | ||||||
|         <PackageReference Include="tusdotnet" Version="2.10.0" /> |  | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ using DysonNetwork.Shared.Auth; | |||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using tusdotnet.Stores; |  | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
| @@ -16,13 +15,10 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV | |||||||
| // Add application services | // Add application services | ||||||
|  |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); |  | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddDysonAuth(); | builder.Services.AddDysonAuth(); | ||||||
| builder.Services.AddAccountService(); | builder.Services.AddAccountService(); | ||||||
|  |  | ||||||
| builder.Services.AddAppFileStorage(builder.Configuration); |  | ||||||
|  |  | ||||||
| builder.Services.AddAppFlushHandlers(); | builder.Services.AddAppFlushHandlers(); | ||||||
| builder.Services.AddAppBusinessServices(); | builder.Services.AddAppBusinessServices(); | ||||||
| builder.Services.AddAppScheduledJobs(); | builder.Services.AddAppScheduledJobs(); | ||||||
| @@ -43,12 +39,11 @@ using (var scope = app.Services.CreateScope()) | |||||||
|     await db.Database.MigrateAsync(); |     await db.Database.MigrateAsync(); | ||||||
| } | } | ||||||
|  |  | ||||||
| var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | app.ConfigureAppMiddleware(); | ||||||
| app.ConfigureAppMiddleware(tusDiskStore); |  | ||||||
|  |  | ||||||
| // Configure gRPC | // Configure gRPC | ||||||
| app.ConfigureGrpcServices(); | app.ConfigureGrpcServices(); | ||||||
|  |  | ||||||
| app.UseSwaggerManifest(); | app.UseSwaggerManifest("DysonNetwork.Drive"); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
|   | |||||||
| @@ -1,18 +1,14 @@ | |||||||
| using DysonNetwork.Drive.Storage; | using DysonNetwork.Drive.Storage; | ||||||
| using tusdotnet; |  | ||||||
| using tusdotnet.Interfaces; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; | namespace DysonNetwork.Drive.Startup; | ||||||
|  |  | ||||||
| public static class ApplicationBuilderExtensions | public static class ApplicationBuilderExtensions | ||||||
| { | { | ||||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore) |     public static WebApplication ConfigureAppMiddleware(this WebApplication app) | ||||||
|     { |     { | ||||||
|         app.UseAuthorization(); |         app.UseAuthorization(); | ||||||
|         app.MapControllers(); |         app.MapControllers(); | ||||||
|  |  | ||||||
|         app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration))); |  | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -21,6 +17,7 @@ public static class ApplicationBuilderExtensions | |||||||
|         // Map your gRPC services here |         // Map your gRPC services here | ||||||
|         app.MapGrpcService<FileServiceGrpc>(); |         app.MapGrpcService<FileServiceGrpc>(); | ||||||
|         app.MapGrpcService<FileReferenceServiceGrpc>(); |         app.MapGrpcService<FileReferenceServiceGrpc>(); | ||||||
|  |         app.MapGrpcReflectionService(); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using System.Threading.RateLimiting; |  | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using Microsoft.AspNetCore.RateLimiting; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Serialization.SystemTextJson; | using NodaTime.Serialization.SystemTextJson; | ||||||
| using tusdotnet.Stores; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; | namespace DysonNetwork.Drive.Startup; | ||||||
|  |  | ||||||
| @@ -27,9 +24,7 @@ public static class ServiceCollectionExtensions | |||||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|         }); |         }); | ||||||
|  |         services.AddGrpcReflection(); | ||||||
|         // Register gRPC reflection for service discovery |  | ||||||
|         services.AddGrpc(); |  | ||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |         services.AddControllers().AddJsonOptions(options => | ||||||
|         { |         { | ||||||
| @@ -43,19 +38,6 @@ public static class ServiceCollectionExtensions | |||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => |  | ||||||
|         { |  | ||||||
|             opts.Window = TimeSpan.FromMinutes(1); |  | ||||||
|             opts.PermitLimit = 120; |  | ||||||
|             opts.QueueLimit = 2; |  | ||||||
|             opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; |  | ||||||
|         })); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) |     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||||
|     { |     { | ||||||
|         services.AddAuthorization(); |         services.AddAuthorization(); | ||||||
| @@ -69,17 +51,6 @@ public static class ServiceCollectionExtensions | |||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration) |  | ||||||
|     { |  | ||||||
|         var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!; |  | ||||||
|         Directory.CreateDirectory(tusStorePath); |  | ||||||
|         var tusDiskStore = new TusDiskStore(tusStorePath); |  | ||||||
|  |  | ||||||
|         services.AddSingleton(tusDiskStore); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services) |     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services) | ||||||
|     { |     { | ||||||
|         services.AddScoped<Storage.FileService>(); |         services.AddScoped<Storage.FileService>(); | ||||||
|   | |||||||
| @@ -187,7 +187,7 @@ public class FileController( | |||||||
|  |  | ||||||
|     public class MarkFileRequest |     public class MarkFileRequest | ||||||
|     { |     { | ||||||
|         public List<ContentSensitiveMark>? SensitiveMarks { get; set; } |         public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     [Authorize] |     [Authorize] | ||||||
| @@ -298,7 +298,7 @@ public class FileController( | |||||||
|         public string? Description { get; set; } |         public string? Description { get; set; } | ||||||
|         public Dictionary<string, object?>? UserMeta { get; set; } |         public Dictionary<string, object?>? UserMeta { get; set; } | ||||||
|         public Dictionary<string, object?>? FileMeta { get; set; } |         public Dictionary<string, object?>? FileMeta { get; set; } | ||||||
|         public List<ContentSensitiveMark>? SensitiveMarks { get; set; } |         public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; } | ||||||
|         public Guid PoolId { get; set; } |         public Guid PoolId { get; set; } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,301 +0,0 @@ | |||||||
| using System.Net; |  | ||||||
| using System.Text; |  | ||||||
| using System.Text.Json; |  | ||||||
| using DysonNetwork.Drive.Billing; |  | ||||||
| using DysonNetwork.Shared.Auth; |  | ||||||
| using DysonNetwork.Shared.Proto; |  | ||||||
| using Microsoft.AspNetCore.Mvc; |  | ||||||
| using Microsoft.Extensions.Options; |  | ||||||
| using NodaTime; |  | ||||||
| using tusdotnet.Interfaces; |  | ||||||
| using tusdotnet.Models; |  | ||||||
| using tusdotnet.Models.Configuration; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Storage; |  | ||||||
|  |  | ||||||
| public abstract class TusService |  | ||||||
| { |  | ||||||
|     public static DefaultTusConfiguration BuildConfiguration( |  | ||||||
|         ITusStore store, |  | ||||||
|         IConfiguration configuration |  | ||||||
|     ) => new() |  | ||||||
|     { |  | ||||||
|         Store = store, |  | ||||||
|         Events = new Events |  | ||||||
|         { |  | ||||||
|             OnAuthorizeAsync = async eventContext => |  | ||||||
|             { |  | ||||||
|                 if (eventContext.Intent == IntentType.DeleteFile) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest( |  | ||||||
|                         HttpStatusCode.BadRequest, |  | ||||||
|                         "Deleting files from this endpoint was disabled, please refer to the Dyson Network File API." |  | ||||||
|                     ); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var httpContext = eventContext.HttpContext; |  | ||||||
|                 if (httpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.Unauthorized); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (eventContext.Intent != IntentType.CreateFile) return; |  | ||||||
|  |  | ||||||
|                 using var scope = httpContext.RequestServices.CreateScope(); |  | ||||||
|  |  | ||||||
|                 if (!currentUser.IsSuperuser) |  | ||||||
|                 { |  | ||||||
|                     var pm = scope.ServiceProvider.GetRequiredService<PermissionService.PermissionServiceClient>(); |  | ||||||
|                     var allowed = await pm.HasPermissionAsync(new HasPermissionRequest |  | ||||||
|                         { Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" }); |  | ||||||
|                     if (!allowed.HasPermission) |  | ||||||
|                         eventContext.FailRequest(HttpStatusCode.Forbidden); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault(); |  | ||||||
|                 if (string.IsNullOrEmpty(filePool)) filePool = configuration["Storage:PreferredRemote"]; |  | ||||||
|                 if (!Guid.TryParse(filePool, out _)) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file pool id"); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var fs = scope.ServiceProvider.GetRequiredService<FileService>(); |  | ||||||
|                 var pool = await fs.GetPoolAsync(Guid.Parse(filePool!)); |  | ||||||
|                 if (pool is null) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Pool not found"); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (pool.PolicyConfig.RequirePrivilege > 0) |  | ||||||
|                 { |  | ||||||
|                     if (currentUser.PerkSubscription is null) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.Forbidden, |  | ||||||
|                             $"You need to have join the Stellar Program to use this pool" |  | ||||||
|                         ); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     var privilege = |  | ||||||
|                         PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier); |  | ||||||
|                     if (privilege < pool.PolicyConfig.RequirePrivilege) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.Forbidden, |  | ||||||
|                             $"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use this pool, you are tier {privilege}" |  | ||||||
|                         ); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault(); |  | ||||||
|                 if (!string.IsNullOrEmpty(bundleId) && !Guid.TryParse(bundleId, out _)) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file bundle id"); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             OnFileCompleteAsync = async eventContext => |  | ||||||
|             { |  | ||||||
|                 using var scope = eventContext.HttpContext.RequestServices.CreateScope(); |  | ||||||
|                 var services = scope.ServiceProvider; |  | ||||||
|  |  | ||||||
|                 var httpContext = eventContext.HttpContext; |  | ||||||
|                 if (httpContext.Items["CurrentUser"] is not Account user) return; |  | ||||||
|  |  | ||||||
|                 var file = await eventContext.GetFileAsync(); |  | ||||||
|                 var metadata = await file.GetMetadataAsync(eventContext.CancellationToken); |  | ||||||
|                 var fileName = metadata.TryGetValue("filename", out var fn) |  | ||||||
|                     ? fn.GetString(Encoding.UTF8) |  | ||||||
|                     : "uploaded_file"; |  | ||||||
|                 var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null; |  | ||||||
|  |  | ||||||
|                 var filePath = Path.Combine(configuration.GetValue<string>("Tus:StorePath")!, file.Id); |  | ||||||
|  |  | ||||||
|                 var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault(); |  | ||||||
|                 var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault(); |  | ||||||
|                 var encryptPassword = httpContext.Request.Headers["X-FilePass"].FirstOrDefault(); |  | ||||||
|  |  | ||||||
|                 if (string.IsNullOrEmpty(filePool)) |  | ||||||
|                     filePool = configuration["Storage:PreferredRemote"]; |  | ||||||
|  |  | ||||||
|                 Instant? expiredAt = null; |  | ||||||
|                 var expiredString = httpContext.Request.Headers["X-FileExpire"].FirstOrDefault(); |  | ||||||
|                 if (!string.IsNullOrEmpty(expiredString) && int.TryParse(expiredString, out var expired)) |  | ||||||
|                     expiredAt = Instant.FromUnixTimeSeconds(expired); |  | ||||||
|                  |  | ||||||
|                 try |  | ||||||
|                 { |  | ||||||
|                     var fileService = services.GetRequiredService<FileService>(); |  | ||||||
|                     var info = await fileService.ProcessNewFileAsync( |  | ||||||
|                         user, |  | ||||||
|                         file.Id, |  | ||||||
|                         filePool!, |  | ||||||
|                         bundleId, |  | ||||||
|                         filePath, |  | ||||||
|                         fileName, |  | ||||||
|                         contentType, |  | ||||||
|                         encryptPassword, |  | ||||||
|                         expiredAt |  | ||||||
|                     ); |  | ||||||
|  |  | ||||||
|                     using var finalScope = eventContext.HttpContext.RequestServices.CreateScope(); |  | ||||||
|                     var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value |  | ||||||
|                         .JsonSerializerOptions; |  | ||||||
|                     var infoJson = JsonSerializer.Serialize(info, jsonOptions); |  | ||||||
|                     eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson); |  | ||||||
|                 } |  | ||||||
|                 catch (Exception ex) |  | ||||||
|                 { |  | ||||||
|                     var logger = services.GetRequiredService<ILogger<TusService>>(); |  | ||||||
|                     eventContext.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; |  | ||||||
|                     await eventContext.HttpContext.Response.WriteAsync(ex.Message); |  | ||||||
|                     logger.LogError(ex, "Error handling file upload..."); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             OnBeforeCreateAsync = async eventContext => |  | ||||||
|             { |  | ||||||
|                 var httpContext = eventContext.HttpContext; |  | ||||||
|                 if (httpContext.Items["CurrentUser"] is not Account currentUser) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.Unauthorized); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|                 var accountId = Guid.Parse(currentUser.Id); |  | ||||||
|  |  | ||||||
|                 var poolId = eventContext.HttpContext.Request.Headers["X-FilePool"].FirstOrDefault(); |  | ||||||
|                 if (string.IsNullOrEmpty(poolId)) poolId = configuration["Storage:PreferredRemote"]; |  | ||||||
|                 if (!Guid.TryParse(poolId, out _)) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file pool id"); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault(); |  | ||||||
|                 if (!string.IsNullOrEmpty(bundleId) && !Guid.TryParse(bundleId, out _)) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file bundle id"); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var metadata = eventContext.Metadata; |  | ||||||
|                 var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null; |  | ||||||
|  |  | ||||||
|                 var scope = eventContext.HttpContext.RequestServices.CreateScope(); |  | ||||||
|  |  | ||||||
|                 var rejected = false; |  | ||||||
|  |  | ||||||
|                 var fs = scope.ServiceProvider.GetRequiredService<FileService>(); |  | ||||||
|                 var pool = await fs.GetPoolAsync(Guid.Parse(poolId!)); |  | ||||||
|                 if (pool is null) |  | ||||||
|                 { |  | ||||||
|                     eventContext.FailRequest(HttpStatusCode.BadRequest, "Pool not found"); |  | ||||||
|                     rejected = true; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 var logger = scope.ServiceProvider.GetRequiredService<ILogger<TusService>>(); |  | ||||||
|  |  | ||||||
|                 // Do the policy check |  | ||||||
|                 var policy = pool!.PolicyConfig; |  | ||||||
|                 if (!rejected && !pool.PolicyConfig.AllowEncryption) |  | ||||||
|                 { |  | ||||||
|                     var encryptPassword = eventContext.HttpContext.Request.Headers["X-FilePass"].FirstOrDefault(); |  | ||||||
|                     if (!string.IsNullOrEmpty(encryptPassword)) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.Forbidden, |  | ||||||
|                             "File encryption is not allowed in this pool" |  | ||||||
|                         ); |  | ||||||
|                         rejected = true; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!rejected && policy.AcceptTypes is not null) |  | ||||||
|                 { |  | ||||||
|                     if (string.IsNullOrEmpty(contentType)) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.BadRequest, |  | ||||||
|                             "Content type is required by the pool's policy" |  | ||||||
|                         ); |  | ||||||
|                         rejected = true; |  | ||||||
|                     } |  | ||||||
|                     else |  | ||||||
|                     { |  | ||||||
|                         var foundMatch = false; |  | ||||||
|                         foreach (var acceptType in policy.AcceptTypes) |  | ||||||
|                         { |  | ||||||
|                             if (acceptType.EndsWith("/*", StringComparison.OrdinalIgnoreCase)) |  | ||||||
|                             { |  | ||||||
|                                 var type = acceptType[..^2]; |  | ||||||
|                                 if (!contentType.StartsWith($"{type}/", StringComparison.OrdinalIgnoreCase)) continue; |  | ||||||
|                                 foundMatch = true; |  | ||||||
|                                 break; |  | ||||||
|                             } |  | ||||||
|                             else if (acceptType.Equals(contentType, StringComparison.OrdinalIgnoreCase)) |  | ||||||
|                             { |  | ||||||
|                                 foundMatch = true; |  | ||||||
|                                 break; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         if (!foundMatch) |  | ||||||
|                         { |  | ||||||
|                             eventContext.FailRequest( |  | ||||||
|                                 HttpStatusCode.Forbidden, |  | ||||||
|                                 $"Content type {contentType} is not allowed by the pool's policy" |  | ||||||
|                             ); |  | ||||||
|                             rejected = true; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!rejected && policy.MaxFileSize is not null) |  | ||||||
|                 { |  | ||||||
|                     if (eventContext.UploadLength > policy.MaxFileSize) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.Forbidden, |  | ||||||
|                             $"File size {eventContext.UploadLength} is larger than the pool's maximum file size {policy.MaxFileSize}" |  | ||||||
|                         ); |  | ||||||
|                         rejected = true; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (!rejected) |  | ||||||
|                 { |  | ||||||
|                     var quotaService = scope.ServiceProvider.GetRequiredService<QuotaService>(); |  | ||||||
|                     var (ok, billableUnit, quota) = await quotaService.IsFileAcceptable( |  | ||||||
|                         accountId, |  | ||||||
|                         pool.BillingConfig.CostMultiplier ?? 1.0, |  | ||||||
|                         eventContext.UploadLength |  | ||||||
|                     ); |  | ||||||
|                     if (!ok) |  | ||||||
|                     { |  | ||||||
|                         eventContext.FailRequest( |  | ||||||
|                             HttpStatusCode.Forbidden, |  | ||||||
|                             $"File size {billableUnit} MiB is exceeded the user's quota {quota} MiB" |  | ||||||
|                         ); |  | ||||||
|                         rejected = true; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 if (rejected) |  | ||||||
|                     logger.LogInformation("File rejected #{FileId}", eventContext.FileId); |  | ||||||
|             }, |  | ||||||
|             OnCreateCompleteAsync = eventContext => |  | ||||||
|             { |  | ||||||
|                 var directUpload = eventContext.HttpContext.Request.Headers["X-DirectUpload"].FirstOrDefault(); |  | ||||||
|                 if (!string.IsNullOrEmpty(directUpload)) return Task.CompletedTask; |  | ||||||
|  |  | ||||||
|                 var gatewayUrl = configuration["GatewayUrl"]; |  | ||||||
|                 if (gatewayUrl is not null) |  | ||||||
|                     eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId)); |  | ||||||
|                 return Task.CompletedTask; |  | ||||||
|             }, |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| @@ -117,9 +117,5 @@ | |||||||
|   "KnownProxies": [ |   "KnownProxies": [ | ||||||
|     "127.0.0.1", |     "127.0.0.1", | ||||||
|     "::1" |     "::1" | ||||||
|   ], |   ] | ||||||
|   "Service": { |  | ||||||
|     "Name": "DysonNetwork.Drive", |  | ||||||
|     "Url": "https://localhost:7092" |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="9.4.2" /> |     <PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="9.5.2" /> | ||||||
|     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> |     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ builder.Services.AddRateLimiter(options => | |||||||
|         }; |         }; | ||||||
| }); | }); | ||||||
|  |  | ||||||
| var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop" }; | var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop", "insight" }; | ||||||
|  |  | ||||||
| var specialRoutes = new[] | var specialRoutes = new[] | ||||||
| { | { | ||||||
| @@ -90,7 +90,6 @@ var apiRoutes = serviceNames.Select(serviceName => | |||||||
| { | { | ||||||
|     var apiPath = serviceName switch |     var apiPath = serviceName switch | ||||||
|     { |     { | ||||||
|         "pass" => "/id", |  | ||||||
|         _ => $"/{serviceName}" |         _ => $"/{serviceName}" | ||||||
|     }; |     }; | ||||||
|     return new RouteConfig |     return new RouteConfig | ||||||
| @@ -123,9 +122,9 @@ var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray(); | |||||||
| var clusters = serviceNames.Select(serviceName => new ClusterConfig | var clusters = serviceNames.Select(serviceName => new ClusterConfig | ||||||
| { | { | ||||||
|     ClusterId = serviceName, |     ClusterId = serviceName, | ||||||
|     HealthCheck = new() |     HealthCheck = new HealthCheckConfig | ||||||
|     { |     { | ||||||
|         Active = new() |         Active = new ActiveHealthCheckConfig | ||||||
|         { |         { | ||||||
|             Enabled = true, |             Enabled = true, | ||||||
|             Interval = TimeSpan.FromSeconds(10), |             Interval = TimeSpan.FromSeconds(10), | ||||||
| @@ -162,8 +161,6 @@ app.UseForwardedHeaders(forwardedHeadersOptions); | |||||||
|  |  | ||||||
| app.UseCors(); | app.UseCors(); | ||||||
|  |  | ||||||
| app.UseRateLimiter(); |  | ||||||
|  |  | ||||||
| app.MapReverseProxy().RequireRateLimiting("fixed"); | app.MapReverseProxy().RequireRateLimiting("fixed"); | ||||||
|  |  | ||||||
| app.MapControllers(); | app.MapControllers(); | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								DysonNetwork.Insight/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								DysonNetwork.Insight/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Design; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight; | ||||||
|  |  | ||||||
|  | public class AppDatabase( | ||||||
|  |     DbContextOptions<AppDatabase> options, | ||||||
|  |     IConfiguration configuration | ||||||
|  | ) : DbContext(options) | ||||||
|  | { | ||||||
|  |     public DbSet<SnThinkingSequence> ThinkingSequences { get; set; } | ||||||
|  |     public DbSet<SnThinkingThought> ThinkingThoughts { get; set; } | ||||||
|  |      | ||||||
|  |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|  |     { | ||||||
|  |         optionsBuilder.UseNpgsql( | ||||||
|  |             configuration.GetConnectionString("App"), | ||||||
|  |             opt => opt | ||||||
|  |                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||||
|  |                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||||
|  |                 .UseNodaTime() | ||||||
|  |         ).UseSnakeCaseNamingConvention(); | ||||||
|  |  | ||||||
|  |         base.OnConfiguring(optionsBuilder); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) | ||||||
|  |     { | ||||||
|  |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |  | ||||||
|  |         foreach (var entry in ChangeTracker.Entries<ModelBase>()) | ||||||
|  |         { | ||||||
|  |             switch (entry.State) | ||||||
|  |             { | ||||||
|  |                 case EntityState.Added: | ||||||
|  |                     entry.Entity.CreatedAt = now; | ||||||
|  |                     entry.Entity.UpdatedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Modified: | ||||||
|  |                     entry.Entity.UpdatedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Deleted: | ||||||
|  |                     entry.State = EntityState.Modified; | ||||||
|  |                     entry.Entity.DeletedAt = now; | ||||||
|  |                     break; | ||||||
|  |                 case EntityState.Detached: | ||||||
|  |                 case EntityState.Unchanged: | ||||||
|  |                 default: | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return await base.SaveChangesAsync(cancellationToken); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||||
|  |     { | ||||||
|  |         base.OnModelCreating(modelBuilder); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> | ||||||
|  | { | ||||||
|  |     public AppDatabase CreateDbContext(string[] args) | ||||||
|  |     { | ||||||
|  |         var configuration = new ConfigurationBuilder() | ||||||
|  |             .SetBasePath(Directory.GetCurrentDirectory()) | ||||||
|  |             .AddJsonFile("appsettings.json") | ||||||
|  |             .Build(); | ||||||
|  |  | ||||||
|  |         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); | ||||||
|  |         return new AppDatabase(optionsBuilder.Options, configuration); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								DysonNetwork.Insight/Controllers/BillingController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Insight/Controllers/BillingController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | using DysonNetwork.Insight.Thought; | ||||||
|  | using DysonNetwork.Shared.Auth; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Controllers; | ||||||
|  |  | ||||||
|  | [ApiController] | ||||||
|  | [Route("/api/billing")] | ||||||
|  | public class BillingController(ThoughtService thoughtService, ILogger<BillingController> logger) : ControllerBase | ||||||
|  | { | ||||||
|  |     [HttpPost("settle")] | ||||||
|  |     [Authorize] | ||||||
|  |     [RequiredPermission("maintenance", "insight.billing.settle")] | ||||||
|  |     public async Task<IActionResult> ProcessTokenBilling() | ||||||
|  |     { | ||||||
|  |         await thoughtService.SettleThoughtBills(logger); | ||||||
|  |         return Ok(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								DysonNetwork.Insight/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Insight/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | #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. | ||||||
|  |  | ||||||
|  | FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||||
|  | USER app | ||||||
|  | WORKDIR /app | ||||||
|  | EXPOSE 8080 | ||||||
|  | EXPOSE 8081 | ||||||
|  |  | ||||||
|  | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||||
|  | ARG BUILD_CONFIGURATION=Release | ||||||
|  | WORKDIR /src | ||||||
|  | COPY ["DysonNetwork.Insight/DysonNetwork.Insight.csproj", "DysonNetwork.Insight/"] | ||||||
|  | COPY ["DysonNetwork.Shared/DysonNetwork.Shared.csproj", "DysonNetwork.Shared/"] | ||||||
|  | COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"] | ||||||
|  | RUN dotnet restore "DysonNetwork.Insight/DysonNetwork.Insight.csproj" | ||||||
|  | COPY . . | ||||||
|  | WORKDIR "/src/DysonNetwork.Insight" | ||||||
|  | RUN dotnet build "DysonNetwork.Insight.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||||
|  |  | ||||||
|  | FROM build AS publish | ||||||
|  | ARG BUILD_CONFIGURATION=Release | ||||||
|  | RUN dotnet publish "DysonNetwork.Insight.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||||
|  |  | ||||||
|  | FROM base AS final | ||||||
|  | WORKDIR /app | ||||||
|  | COPY --from=publish /app/publish . | ||||||
|  | ENTRYPOINT ["dotnet", "DysonNetwork.Insight.dll"] | ||||||
							
								
								
									
										34
									
								
								DysonNetwork.Insight/DysonNetwork.Insight.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Insight/DysonNetwork.Insight.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||||
|  |  | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <TargetFramework>net9.0</TargetFramework> | ||||||
|  |     <Nullable>enable</Nullable> | ||||||
|  |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |   </PropertyGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||||
|  |     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|  |     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||||
|  |       <PrivateAssets>all</PrivateAssets> | ||||||
|  |       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|  |     </PackageReference> | ||||||
|  |     <PackageReference Include="Microsoft.SemanticKernel" Version="1.66.0" /> | ||||||
|  |     <PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" /> | ||||||
|  |     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||||
|  |     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|  |     <PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" /> | ||||||
|  |     <PackageReference Include="Quartz" Version="3.15.0" /> | ||||||
|  |     <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> | ||||||
|  |     <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  |   <ItemGroup> | ||||||
|  |     <Folder Include="Controllers\" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |  | ||||||
|  | </Project> | ||||||
							
								
								
									
										124
									
								
								DysonNetwork.Insight/Migrations/20251025115921_AddThinkingThought.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								DysonNetwork.Insight/Migrations/20251025115921_AddThinkingThought.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Insight; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20251025115921_AddThinkingThought")] | ||||||
|  |     partial class AddThinkingThought | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.10") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Topic") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("topic"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_sequences"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_sequences", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Content") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("content"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnCloudFileReferenceObject>>("Files") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("files"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("SequenceId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_thoughts"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("SequenceId") | ||||||
|  |                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_thoughts", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("SequenceId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Sequence"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddThinkingThought : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "thinking_sequences", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     topic = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_thinking_sequences", x => x.id); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "thinking_thoughts", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     content = table.Column<string>(type: "text", nullable: true), | ||||||
|  |                     files = table.Column<List<SnCloudFileReferenceObject>>(type: "jsonb", nullable: false), | ||||||
|  |                     role = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     sequence_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_thinking_thoughts", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_thinking_thoughts_thinking_sequences_sequence_id", | ||||||
|  |                         column: x => x.sequence_id, | ||||||
|  |                         principalTable: "thinking_sequences", | ||||||
|  |                         principalColumn: "id", | ||||||
|  |                         onDelete: ReferentialAction.Cascade); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_thinking_thoughts_sequence_id", | ||||||
|  |                 table: "thinking_thoughts", | ||||||
|  |                 column: "sequence_id"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "thinking_thoughts"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "thinking_sequences"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										129
									
								
								DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Insight; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20251026045505_AddThinkingChunk")] | ||||||
|  |     partial class AddThinkingChunk | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.10") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Topic") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("topic"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_sequences"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_sequences", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnThinkingChunk>>("Chunks") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("chunks"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Content") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("content"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnCloudFileReferenceObject>>("Files") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("files"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("SequenceId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_thoughts"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("SequenceId") | ||||||
|  |                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_thoughts", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("SequenceId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Sequence"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddThinkingChunk : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<List<SnThinkingChunk>>( | ||||||
|  |                 name: "chunks", | ||||||
|  |                 table: "thinking_thoughts", | ||||||
|  |                 type: "jsonb", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: new List<SnThinkingChunk>() | ||||||
|  |                 ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "chunks", | ||||||
|  |                 table: "thinking_thoughts"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										146
									
								
								DysonNetwork.Insight/Migrations/20251026134218_AddBilling.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								DysonNetwork.Insight/Migrations/20251026134218_AddBilling.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Insight; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20251026134218_AddBilling")] | ||||||
|  |     partial class AddBilling | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.10") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("PaidToken") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("paid_token"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Topic") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("topic"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("TotalToken") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("total_token"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_sequences"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_sequences", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnThinkingChunk>>("Chunks") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("chunks"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Content") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("content"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnCloudFileReferenceObject>>("Files") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("files"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ModelName") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("model_name"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("SequenceId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("TokenCount") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("token_count"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_thoughts"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("SequenceId") | ||||||
|  |                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_thoughts", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("SequenceId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Sequence"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								DysonNetwork.Insight/Migrations/20251026134218_AddBilling.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								DysonNetwork.Insight/Migrations/20251026134218_AddBilling.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddBilling : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "model_name", | ||||||
|  |                 table: "thinking_thoughts", | ||||||
|  |                 type: "character varying(4096)", | ||||||
|  |                 maxLength: 4096, | ||||||
|  |                 nullable: true); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<long>( | ||||||
|  |                 name: "token_count", | ||||||
|  |                 table: "thinking_thoughts", | ||||||
|  |                 type: "bigint", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: 0L); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<long>( | ||||||
|  |                 name: "paid_token", | ||||||
|  |                 table: "thinking_sequences", | ||||||
|  |                 type: "bigint", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: 0L); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<long>( | ||||||
|  |                 name: "total_token", | ||||||
|  |                 table: "thinking_sequences", | ||||||
|  |                 type: "bigint", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: 0L); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "model_name", | ||||||
|  |                 table: "thinking_thoughts"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "token_count", | ||||||
|  |                 table: "thinking_thoughts"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "paid_token", | ||||||
|  |                 table: "thinking_sequences"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "total_token", | ||||||
|  |                 table: "thinking_sequences"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										143
									
								
								DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Insight; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     partial class AppDatabaseModelSnapshot : ModelSnapshot | ||||||
|  |     { | ||||||
|  |         protected override void BuildModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.10") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("PaidToken") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("paid_token"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Topic") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("topic"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("TotalToken") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("total_token"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_sequences"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_sequences", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnThinkingChunk>>("Chunks") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("chunks"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Content") | ||||||
|  |                         .HasColumnType("text") | ||||||
|  |                         .HasColumnName("content"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<SnCloudFileReferenceObject>>("Files") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("files"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ModelName") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("model_name"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("SequenceId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("TokenCount") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("token_count"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_thinking_thoughts"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("SequenceId") | ||||||
|  |                         .HasDatabaseName("ix_thinking_thoughts_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("thinking_thoughts", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("SequenceId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Sequence"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								DysonNetwork.Insight/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								DysonNetwork.Insight/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | using DysonNetwork.Insight; | ||||||
|  | using DysonNetwork.Insight.Startup; | ||||||
|  | using DysonNetwork.Shared.Auth; | ||||||
|  | using DysonNetwork.Shared.Http; | ||||||
|  | using DysonNetwork.Shared.Registry; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
|  | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
|  | builder.AddServiceDefaults(); | ||||||
|  |  | ||||||
|  | builder.ConfigureAppKestrel(builder.Configuration); | ||||||
|  |  | ||||||
|  | builder.Services.AddControllers(); | ||||||
|  | builder.Services.AddAppServices(); | ||||||
|  | builder.Services.AddAppAuthentication(); | ||||||
|  | builder.Services.AddAppFlushHandlers(); | ||||||
|  | builder.Services.AddAppBusinessServices(); | ||||||
|  | builder.Services.AddAppScheduledJobs(); | ||||||
|  |  | ||||||
|  | builder.Services.AddDysonAuth(); | ||||||
|  | builder.Services.AddAccountService(); | ||||||
|  | builder.Services.AddSphereService(); | ||||||
|  | builder.Services.AddThinkingServices(builder.Configuration); | ||||||
|  |  | ||||||
|  | builder.AddSwaggerManifest( | ||||||
|  |     "DysonNetwork.Insight", | ||||||
|  |     "The insight service in the Solar Network." | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | var app = builder.Build(); | ||||||
|  |  | ||||||
|  | app.MapDefaultEndpoints(); | ||||||
|  |  | ||||||
|  | using (var scope = app.Services.CreateScope()) | ||||||
|  | { | ||||||
|  |     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||||
|  |     await db.Database.MigrateAsync(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | app.ConfigureAppMiddleware(builder.Configuration); | ||||||
|  |  | ||||||
|  | app.UseSwaggerManifest("DysonNetwork.Insight"); | ||||||
|  |  | ||||||
|  | app.Run(); | ||||||
							
								
								
									
										21
									
								
								DysonNetwork.Insight/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Insight/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||||
|  |   "profiles": { | ||||||
|  |     "http": { | ||||||
|  |       "commandName": "Project", | ||||||
|  |       "dotnetRunMessages": true, | ||||||
|  |       "launchBrowser": false, | ||||||
|  |       "environmentVariables": { | ||||||
|  |         "ASPNETCORE_ENVIRONMENT": "Development" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "https": { | ||||||
|  |       "commandName": "Project", | ||||||
|  |       "dotnetRunMessages": true, | ||||||
|  |       "launchBrowser": false, | ||||||
|  |       "environmentVariables": { | ||||||
|  |         "ASPNETCORE_ENVIRONMENT": "Development" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								DysonNetwork.Insight/Startup/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								DysonNetwork.Insight/Startup/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | using DysonNetwork.Shared.Http; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Startup; | ||||||
|  |  | ||||||
|  | public static class ApplicationConfiguration | ||||||
|  | { | ||||||
|  |     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) | ||||||
|  |     { | ||||||
|  |         app.MapOpenApi(); | ||||||
|  |  | ||||||
|  |         app.UseRequestLocalization(); | ||||||
|  |  | ||||||
|  |         app.ConfigureForwardedHeaders(configuration); | ||||||
|  |  | ||||||
|  |         app.UseAuthentication(); | ||||||
|  |         app.UseAuthorization(); | ||||||
|  |  | ||||||
|  |         app.MapControllers(); | ||||||
|  |  | ||||||
|  |         return app; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | using Quartz; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Startup; | ||||||
|  |  | ||||||
|  | public static class ScheduledJobsConfiguration | ||||||
|  | { | ||||||
|  |     public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services) | ||||||
|  |     { | ||||||
|  |         services.AddQuartz(q => | ||||||
|  |         { | ||||||
|  |             var tokenBillingJob = new JobKey("TokenBilling"); | ||||||
|  |             q.AddJob<TokenBillingJob>(opts => opts.WithIdentity(tokenBillingJob)); | ||||||
|  |             q.AddTrigger(opts => opts | ||||||
|  |                 .ForJob(tokenBillingJob) | ||||||
|  |                 .WithIdentity("TokenBillingTrigger") | ||||||
|  |                 .WithSimpleSchedule(o => o | ||||||
|  |                     .WithIntervalInMinutes(5) | ||||||
|  |                     .RepeatForever()) | ||||||
|  |             ); | ||||||
|  |         }); | ||||||
|  |         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); | ||||||
|  |  | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										79
									
								
								DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
|  | using DysonNetwork.Insight.Thought; | ||||||
|  | using DysonNetwork.Shared.Cache; | ||||||
|  | using Microsoft.SemanticKernel; | ||||||
|  | using NodaTime; | ||||||
|  | using NodaTime.Serialization.SystemTextJson; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Startup; | ||||||
|  |  | ||||||
|  | public static class ServiceCollectionExtensions | ||||||
|  | { | ||||||
|  |     public static IServiceCollection AddAppServices(this IServiceCollection services) | ||||||
|  |     { | ||||||
|  |         services.AddDbContext<AppDatabase>(); | ||||||
|  |         services.AddSingleton<IClock>(SystemClock.Instance); | ||||||
|  |         services.AddHttpContextAccessor(); | ||||||
|  |         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||||
|  |  | ||||||
|  |         services.AddHttpClient(); | ||||||
|  |  | ||||||
|  |         // Register gRPC services | ||||||
|  |         services.AddGrpc(options => | ||||||
|  |         { | ||||||
|  |             options.EnableDetailedErrors = true; // Will be adjusted in Program.cs | ||||||
|  |             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|  |             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|  |         }); | ||||||
|  |         services.AddGrpcReflection(); | ||||||
|  |  | ||||||
|  |         // Register gRPC services | ||||||
|  |  | ||||||
|  |         // Register OIDC services | ||||||
|  |         services.AddControllers().AddJsonOptions(options => | ||||||
|  |         { | ||||||
|  |             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||||
|  |             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|  |             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|  |  | ||||||
|  |             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||||
|  |     { | ||||||
|  |         services.AddAuthorization(); | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services) | ||||||
|  |     { | ||||||
|  |         services.AddSingleton<FlushBufferService>(); | ||||||
|  |  | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services) | ||||||
|  |     { | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static IServiceCollection AddThinkingServices(this IServiceCollection services, IConfiguration configuration) | ||||||
|  |     { | ||||||
|  |         services.AddSingleton<ThoughtProvider>(); | ||||||
|  |         services.AddScoped<ThoughtService>(); | ||||||
|  |  | ||||||
|  |         // Add gRPC clients for ThoughtService | ||||||
|  |         services.AddGrpcClient<Shared.Proto.PaymentService.PaymentServiceClient>(o => o.Address = new Uri("https://_grpc.pass")) | ||||||
|  |             .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() | ||||||
|  |                 { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }); | ||||||
|  |         services.AddGrpcClient<Shared.Proto.WalletService.WalletServiceClient>(o => o.Address = new Uri("https://_grpc.pass")) | ||||||
|  |             .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() | ||||||
|  |                 { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }); | ||||||
|  |  | ||||||
|  |         return services; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								DysonNetwork.Insight/Startup/TokenBillingJob.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								DysonNetwork.Insight/Startup/TokenBillingJob.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | using DysonNetwork.Insight.Thought; | ||||||
|  | using Quartz; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Startup; | ||||||
|  |  | ||||||
|  | public class TokenBillingJob(ThoughtService thoughtService, ILogger<TokenBillingJob> logger) : IJob | ||||||
|  | { | ||||||
|  |     public async Task Execute(IJobExecutionContext context) | ||||||
|  |     { | ||||||
|  |         await thoughtService.SettleThoughtBills(logger); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										161
									
								
								DysonNetwork.Insight/Thought/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								DysonNetwork.Insight/Thought/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | # DysonNetwork Insight Thought API | ||||||
|  |  | ||||||
|  | The Thought API provides conversational AI capabilities for users of the Solar Network. It allows users to engage in chat-like conversations with an AI assistant powered by semantic kernel and connected to various tools. | ||||||
|  |  | ||||||
|  | This service is handled by the Insight, when using with the Gateway, the `/api` should be replaced with `/insight` | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - Streaming chat responses using Server-Sent Events (SSE) | ||||||
|  | - Conversation context management with sequences | ||||||
|  | - Caching for improved performance | ||||||
|  | - Authentication required for all operations | ||||||
|  |  | ||||||
|  | ## Endpoints | ||||||
|  |  | ||||||
|  | ### POST /api/thought | ||||||
|  |  | ||||||
|  | Initiates or continues a chat conversation. | ||||||
|  |  | ||||||
|  | #### Parameters | ||||||
|  | - `UserMessage` (string, required): The message from the user | ||||||
|  | - `SequenceId` (Guid, optional): ID of existing conversation sequence. If not provided, a new sequence is created. | ||||||
|  |  | ||||||
|  | #### Response | ||||||
|  | - Content-Type: `text/event-stream` | ||||||
|  | - Streaming response with assistant messages | ||||||
|  | - Status: 401 if not authenticated | ||||||
|  | - Status: 403 if sequence doesn't belong to user | ||||||
|  |  | ||||||
|  | #### Example Usage | ||||||
|  | ```bash | ||||||
|  | curl -X POST "http://localhost:5000/api/thought" \ | ||||||
|  |   -H "Content-Type: application/json" \ | ||||||
|  |   -d '{ | ||||||
|  |     "UserMessage": "Hello, how can I help with the Solar Network?", | ||||||
|  |     "SequenceId": null | ||||||
|  |   }' | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### GET /api/thought/sequences | ||||||
|  |  | ||||||
|  | Lists all thinking sequences for the authenticated user. | ||||||
|  |  | ||||||
|  | #### Parameters | ||||||
|  | - `offset` (int, default 0): Number of sequences to skip for pagination | ||||||
|  | - `take` (int, default 20): Maximum number of sequences to return | ||||||
|  |  | ||||||
|  | #### Response | ||||||
|  | - `200 OK`: Array of `SnThinkingSequence` | ||||||
|  | - `401 Unauthorized`: If not authenticated | ||||||
|  | - Headers: | ||||||
|  |   - `X-Total`: Total number of sequences before pagination | ||||||
|  |  | ||||||
|  | #### Example Usage | ||||||
|  | ```bash | ||||||
|  | curl -X GET "http://localhost:5000/api/thought/sequences?take=10" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### GET /api/thought/sequences/{sequenceId} | ||||||
|  |  | ||||||
|  | Retrieves all thoughts (messages) in a specific conversation sequence. | ||||||
|  |  | ||||||
|  | #### Parameters | ||||||
|  | - `sequenceId` (Guid, path): ID of the sequence to retrieve | ||||||
|  |  | ||||||
|  | #### Response | ||||||
|  | - `200 OK`: Array of `SnThinkingThought` ordered by creation date | ||||||
|  | - `401 Unauthorized`: If not authenticated | ||||||
|  | - `404 Not Found`: If sequence doesn't exist or doesn't belong to user | ||||||
|  |  | ||||||
|  | #### Example Usage | ||||||
|  | ```bash | ||||||
|  | curl -X GET "http://localhost:5000/api/thought/sequences/12345678-1234-1234-1234-123456789abc" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Data Models | ||||||
|  |  | ||||||
|  | ### StreamThinkingRequest | ||||||
|  | ```csharp | ||||||
|  | { | ||||||
|  |   string UserMessage, // Required | ||||||
|  |   Guid? SequenceId    // Optional | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### SnThinkingSequence | ||||||
|  | ```csharp | ||||||
|  | { | ||||||
|  |   Guid Id, | ||||||
|  |   string? Topic, | ||||||
|  |   Guid AccountId | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### SnThinkingThought | ||||||
|  | ```csharp | ||||||
|  | { | ||||||
|  |   Guid Id, | ||||||
|  |   string? Content, | ||||||
|  |   List<SnCloudFileReferenceObject> Files, | ||||||
|  |   ThinkingThoughtRole Role, | ||||||
|  |   Guid SequenceId, | ||||||
|  |   SnThinkingSequence Sequence | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ThinkingThoughtRole (enum) | ||||||
|  | - `Assistant` | ||||||
|  | - `User` | ||||||
|  |  | ||||||
|  | ## Caching | ||||||
|  |  | ||||||
|  | The API uses Redis-based caching for conversation thoughts: | ||||||
|  | - Thoughts are cached for 10 minutes with group-based invalidation | ||||||
|  | - Cache is invalidated when new thoughts are added to a sequence | ||||||
|  | - Improves performance for accessing conversation history | ||||||
|  |  | ||||||
|  | ## Authentication | ||||||
|  |  | ||||||
|  | All endpoints require authentication through the current user session. Sequence access is validated against the authenticated user's account ID. | ||||||
|  |  | ||||||
|  | ## Error Responses | ||||||
|  |  | ||||||
|  | - `401 Unauthorized`: Authentication required | ||||||
|  | - `403 Forbidden`: Access denied (sequence ownership) | ||||||
|  | - `404 Not Found`: Resource not found | ||||||
|  |  | ||||||
|  | ## Streaming Details | ||||||
|  |  | ||||||
|  | The POST endpoint returns a stream of assistant responses using Server-Sent Events format. Clients should handle the streaming response and display messages incrementally. | ||||||
|  |  | ||||||
|  | ### Streaming Message Format | ||||||
|  |  | ||||||
|  | The streaming response sends several types of JSON messages: | ||||||
|  |  | ||||||
|  | - **Text messages**: `{"type": "text", "data": "..." }` | ||||||
|  | - **Function calls**: `{"type": "function_call", "data": {...} }` (when AI uses tools) | ||||||
|  | - **Topic updates**: `{"type": "topic", "data": "..." }` (sent at end if topic was generated) | ||||||
|  | - **Thought completion**: `{"type": "thought", "data": {...} }` (sent at end with saved thought details) | ||||||
|  |  | ||||||
|  | All streaming chunks during generation use the SSE event format: | ||||||
|  | ``` | ||||||
|  | data: {"type": "...", "data": ...} | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Final messages (topic and thought) use custom event types: | ||||||
|  | ``` | ||||||
|  | topic: {"type": "topic", "data": "..."} | ||||||
|  |  | ||||||
|  | thought: {"type": "thought", "data": {...}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Clients should parse these JSON messages and handle different types appropriately, such as displaying text in real-time and processing tool calls. | ||||||
|  |  | ||||||
|  | ## Implementation Notes | ||||||
|  |  | ||||||
|  | - Built with ASP.NET Core and Semantic Kernel | ||||||
|  | - Uses PostgreSQL via Entity Framework Core | ||||||
|  | - Integrated with Ollama for AI completion | ||||||
|  | - Caching via Redis | ||||||
							
								
								
									
										275
									
								
								DysonNetwork.Insight/Thought/ThoughtController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								DysonNetwork.Insight/Thought/ThoughtController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  | using System.Diagnostics.CodeAnalysis; | ||||||
|  | using System.IO; | ||||||
|  | using System.Text; | ||||||
|  | using System.Text.Json; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using Microsoft.SemanticKernel; | ||||||
|  | using Microsoft.SemanticKernel.ChatCompletion; | ||||||
|  | using Microsoft.SemanticKernel.Connectors.Ollama; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Thought; | ||||||
|  |  | ||||||
|  | [ApiController] | ||||||
|  | [Route("/api/thought")] | ||||||
|  | public class ThoughtController(ThoughtProvider provider, ThoughtService service) : ControllerBase | ||||||
|  | { | ||||||
|  |     public static readonly List<string> AvailableProposals = ["post_create"]; | ||||||
|  |  | ||||||
|  |     public class StreamThinkingRequest | ||||||
|  |     { | ||||||
|  |         [Required] public string UserMessage { get; set; } = null!; | ||||||
|  |         public Guid? SequenceId { get; set; } | ||||||
|  |         public List<string>? AttachedPosts { get; set; } | ||||||
|  |         public List<Dictionary<string, dynamic>>? AttachedMessages { get; set; } | ||||||
|  |         public List<string> AcceptProposals { get; set; } = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpPost] | ||||||
|  |     [Experimental("SKEXP0110")] | ||||||
|  |     public async Task<ActionResult> Think([FromBody] StreamThinkingRequest request) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|  |  | ||||||
|  |         if (request.AcceptProposals.Any(e => !AvailableProposals.Contains(e))) | ||||||
|  |             return BadRequest("Request contains unavailable proposal"); | ||||||
|  |  | ||||||
|  |         // Generate a topic if creating a new sequence | ||||||
|  |         string? topic = null; | ||||||
|  |         if (!request.SequenceId.HasValue) | ||||||
|  |         { | ||||||
|  |             // Use AI to summarize a topic from a user message | ||||||
|  |             var summaryHistory = new ChatHistory( | ||||||
|  |                 "You are a helpful assistant. Summarize the following user message into a concise topic title (max 100 characters).\n" + | ||||||
|  |                 "Direct give the topic you summerized, do not add extra prefix / suffix." | ||||||
|  |             ); | ||||||
|  |             summaryHistory.AddUserMessage(request.UserMessage); | ||||||
|  |  | ||||||
|  |             var summaryResult = await provider.Kernel | ||||||
|  |                 .GetRequiredService<IChatCompletionService>() | ||||||
|  |                 .GetChatMessageContentAsync(summaryHistory); | ||||||
|  |  | ||||||
|  |             topic = summaryResult.Content?[..Math.Min(summaryResult.Content.Length, 4096)]; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Handle sequence | ||||||
|  |         var sequence = await service.GetOrCreateSequenceAsync(accountId, request.SequenceId, topic); | ||||||
|  |         if (sequence == null) return Forbid(); // or NotFound | ||||||
|  |  | ||||||
|  |         // Save user thought | ||||||
|  |         await service.SaveThoughtAsync(sequence, request.UserMessage, ThinkingThoughtRole.User); | ||||||
|  |  | ||||||
|  |         // Build chat history | ||||||
|  |         var chatHistory = new ChatHistory( | ||||||
|  |             "You're a helpful assistant on the Solar Network, a social network.\n" + | ||||||
|  |             "Your name is Sn-chan (or SN 酱 in chinese), a cute sweet heart with passion for almost everything.\n" + | ||||||
|  |             "When you talk to user, you can add some modal particles and emoticons to your response to be cute, but prevent use a lot of emojis." + | ||||||
|  |             "Your creator is @littlesheep, which is also the creator of the Solar Network, if you met some problems you was unable to solve, trying guide the user to ask (DM) the @littlesheep.\n" + | ||||||
|  |             "\n" + | ||||||
|  |             "The ID on the Solar Network is UUID, so mostly hard to read, so do not show ID to user unless user ask to do so or necessary.\n" + | ||||||
|  |             "\n" + | ||||||
|  |             "Your aim is to helping solving questions for the users on the Solar Network.\n" + | ||||||
|  |             "And the Solar Network is the social network platform you live on.\n" + | ||||||
|  |             "When the user asks questions about the Solar Network (also known as SN and Solian), try use the tools you have to get latest and accurate data." | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         chatHistory.AddSystemMessage( | ||||||
|  |             "You can issue some proposals to user, like creating a post. The proposal syntax is like a xml tag, with an attribute indicates which proposal.\n" + | ||||||
|  |             "Depends on the proposal type, the payload (content inside the xml tag) might be different.\n" + | ||||||
|  |             "\n" + | ||||||
|  |             "Example: <proposal type=\"post_create\">...post content...</proposal>\n" + | ||||||
|  |             "\n" + | ||||||
|  |             "Here are some references of the proposals you can issue, but if you want to issue one, make sure the user is accept it.\n" + | ||||||
|  |             "1. post_create: body takes simple string, create post for user." + | ||||||
|  |             "\n" + | ||||||
|  |             $"The user currently accept these proposals: {string.Join(',', request.AcceptProposals)}" | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         chatHistory.AddSystemMessage( | ||||||
|  |             $"The user you're currently talking to is {currentUser.Nick} ({currentUser.Name}), ID is {currentUser.Id}" | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (request.AttachedPosts is { Count: > 0 }) | ||||||
|  |         { | ||||||
|  |             chatHistory.AddUserMessage( | ||||||
|  |                 $"Attached post IDs: {string.Join(',', request.AttachedPosts!)}"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (request.AttachedMessages is { Count: > 0 }) | ||||||
|  |         { | ||||||
|  |             chatHistory.AddUserMessage( | ||||||
|  |                 $"Attached chat messages data: {JsonSerializer.Serialize(request.AttachedMessages)}"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add previous thoughts (excluding the current user thought, which is the first one since descending) | ||||||
|  |         var previousThoughts = await service.GetPreviousThoughtsAsync(sequence); | ||||||
|  |         var count = previousThoughts.Count; | ||||||
|  |         for (var i = 1; i < count; i++) // skip first (the newest, current user) | ||||||
|  |         { | ||||||
|  |             var thought = previousThoughts[i]; | ||||||
|  |             switch (thought.Role) | ||||||
|  |             { | ||||||
|  |                 case ThinkingThoughtRole.User: | ||||||
|  |                     chatHistory.AddUserMessage(thought.Content ?? ""); | ||||||
|  |                     break; | ||||||
|  |                 case ThinkingThoughtRole.Assistant: | ||||||
|  |                     chatHistory.AddAssistantMessage(thought.Content ?? ""); | ||||||
|  |                     break; | ||||||
|  |                 default: | ||||||
|  |                     throw new ArgumentOutOfRangeException(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         chatHistory.AddUserMessage(request.UserMessage); | ||||||
|  |  | ||||||
|  |         // Set response for streaming | ||||||
|  |         Response.Headers.Append("Content-Type", "text/event-stream"); | ||||||
|  |         Response.StatusCode = 200; | ||||||
|  |  | ||||||
|  |         var kernel = provider.Kernel; | ||||||
|  |         var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); | ||||||
|  |  | ||||||
|  |         // Kick off streaming generation | ||||||
|  |         var accumulatedContent = new StringBuilder(); | ||||||
|  |         var thinkingChunks = new List<SnThinkingChunk>(); | ||||||
|  |         await foreach (var chunk in chatCompletionService.GetStreamingChatMessageContentsAsync( | ||||||
|  |                            chatHistory, | ||||||
|  |                            provider.CreatePromptExecutionSettings(), | ||||||
|  |                            kernel: kernel | ||||||
|  |                        )) | ||||||
|  |         { | ||||||
|  |             // Process each item in the chunk for detailed streaming | ||||||
|  |             foreach (var item in chunk.Items) | ||||||
|  |             { | ||||||
|  |                 var streamingChunk = item switch | ||||||
|  |                 { | ||||||
|  |                     StreamingTextContent textContent => new SnThinkingChunk | ||||||
|  |                         { Type = StreamingContentType.Text, Data = new() { ["text"] = textContent.Text ?? "" } }, | ||||||
|  |                     StreamingReasoningContent reasoningContent => new SnThinkingChunk | ||||||
|  |                     { | ||||||
|  |                         Type = StreamingContentType.Reasoning, Data = new() { ["text"] = reasoningContent.Text } | ||||||
|  |                     }, | ||||||
|  |                     StreamingFunctionCallUpdateContent functionCall => string.IsNullOrEmpty(functionCall.CallId) | ||||||
|  |                         ? null | ||||||
|  |                         : new SnThinkingChunk | ||||||
|  |                         { | ||||||
|  |                             Type = StreamingContentType.FunctionCall, | ||||||
|  |                             Data = JsonSerializer.Deserialize<Dictionary<string, object>>( | ||||||
|  |                                 JsonSerializer.Serialize(functionCall)) ?? new Dictionary<string, object>() | ||||||
|  |                         }, | ||||||
|  |                     _ => new SnThinkingChunk | ||||||
|  |                     { | ||||||
|  |                         Type = StreamingContentType.Unknown, Data = new() { ["data"] = JsonSerializer.Serialize(item) } | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 if (streamingChunk == null) continue; | ||||||
|  |  | ||||||
|  |                 thinkingChunks.Add(streamingChunk); | ||||||
|  |  | ||||||
|  |                 var messageJson = item switch | ||||||
|  |                 { | ||||||
|  |                     StreamingTextContent textContent => | ||||||
|  |                         JsonSerializer.Serialize(new { type = "text", data = textContent.Text ?? "" }), | ||||||
|  |                     StreamingReasoningContent reasoningContent => | ||||||
|  |                         JsonSerializer.Serialize(new { type = "reasoning", data = reasoningContent.Text }), | ||||||
|  |                     StreamingFunctionCallUpdateContent functionCall => | ||||||
|  |                         JsonSerializer.Serialize(new { type = "function_call", data = functionCall }), | ||||||
|  |                     _ => | ||||||
|  |                         JsonSerializer.Serialize(new { type = "unknown", data = item }) | ||||||
|  |                 }; | ||||||
|  |  | ||||||
|  |                 // Write a structured JSON message to the HTTP response as SSE | ||||||
|  |                 var messageBytes = Encoding.UTF8.GetBytes($"data: {messageJson}\n\n"); | ||||||
|  |                 await Response.Body.WriteAsync(messageBytes); | ||||||
|  |                 await Response.Body.FlushAsync(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Accumulate content for saving (only text content) | ||||||
|  |             accumulatedContent.Append(chunk.Content ?? ""); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Save assistant thought | ||||||
|  |         var savedThought = await service.SaveThoughtAsync( | ||||||
|  |             sequence, | ||||||
|  |             accumulatedContent.ToString(), | ||||||
|  |             ThinkingThoughtRole.Assistant, | ||||||
|  |             thinkingChunks, | ||||||
|  |             provider.ModelDefault | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // Write the topic if it was newly set, then the thought object as JSON to the stream | ||||||
|  |         using (var streamBuilder = new MemoryStream()) | ||||||
|  |         { | ||||||
|  |             await streamBuilder.WriteAsync("\n\n"u8.ToArray()); | ||||||
|  |             if (topic != null) | ||||||
|  |             { | ||||||
|  |                 var topicJson = JsonSerializer.Serialize(new { type = "topic", data = sequence.Topic ?? "" }); | ||||||
|  |                 await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"topic: {topicJson}\n\n")); | ||||||
|  |                 savedThought.Sequence.Topic = topic; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var thoughtJson = JsonSerializer.Serialize(new { type = "thought", data = savedThought }, | ||||||
|  |                 GrpcTypeHelper.SerializerOptions); | ||||||
|  |             await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"thought: {thoughtJson}\n\n")); | ||||||
|  |             var outputBytes = streamBuilder.ToArray(); | ||||||
|  |             await Response.Body.WriteAsync(outputBytes); | ||||||
|  |             await Response.Body.FlushAsync(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Return empty result since we're streaming | ||||||
|  |         return new EmptyResult(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Retrieves a paginated list of thinking sequences for the authenticated user. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="offset">The number of sequences to skip for pagination.</param> | ||||||
|  |     /// <param name="take">The maximum number of sequences to return (default: 20).</param> | ||||||
|  |     /// <returns> | ||||||
|  |     /// Returns an ActionResult containing a list of thinking sequences. | ||||||
|  |     /// Includes an X-Total header with the total count of sequences before pagination. | ||||||
|  |     /// </returns> | ||||||
|  |     [HttpGet("sequences")] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|  |     public async Task<ActionResult<List<SnThinkingSequence>>> ListSequences( | ||||||
|  |         [FromQuery] int offset = 0, | ||||||
|  |         [FromQuery] int take = 20 | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|  |  | ||||||
|  |         var (totalCount, sequences) = await service.ListSequencesAsync(accountId, offset, take); | ||||||
|  |  | ||||||
|  |         Response.Headers["X-Total"] = totalCount.ToString(); | ||||||
|  |  | ||||||
|  |         return Ok(sequences); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Retrieves the thoughts in a specific thinking sequence. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="sequenceId">The ID of the sequence to retrieve thoughts from.</param> | ||||||
|  |     /// <returns> | ||||||
|  |     /// Returns an ActionResult containing a list of thoughts in the sequence, ordered by creation date. | ||||||
|  |     /// </returns> | ||||||
|  |     [HttpGet("sequences/{sequenceId:guid}")] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status200OK)] | ||||||
|  |     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|  |     public async Task<ActionResult<List<SnThinkingThought>>> GetSequenceThoughts(Guid sequenceId) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|  |  | ||||||
|  |         var sequence = await service.GetOrCreateSequenceAsync(accountId, sequenceId); | ||||||
|  |         if (sequence == null) return NotFound(); | ||||||
|  |  | ||||||
|  |         var thoughts = await service.GetPreviousThoughtsAsync(sequence); | ||||||
|  |  | ||||||
|  |         return Ok(thoughts); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										198
									
								
								DysonNetwork.Insight/Thought/ThoughtProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								DysonNetwork.Insight/Thought/ThoughtProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | |||||||
|  | using System.ClientModel; | ||||||
|  | using System.Diagnostics.CodeAnalysis; | ||||||
|  | using System.Text.Json; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Microsoft.SemanticKernel; | ||||||
|  | using Microsoft.SemanticKernel.Connectors.Ollama; | ||||||
|  | using Microsoft.SemanticKernel.Connectors.OpenAI; | ||||||
|  | using OpenAI; | ||||||
|  | using PostType = DysonNetwork.Shared.Proto.PostType; | ||||||
|  | using Microsoft.SemanticKernel.Plugins.Web; | ||||||
|  | using Microsoft.SemanticKernel.Plugins.Web.Bing; | ||||||
|  | using Microsoft.SemanticKernel.Plugins.Web.Google; | ||||||
|  | using NodaTime.Serialization.Protobuf; | ||||||
|  | using NodaTime.Text; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Thought; | ||||||
|  |  | ||||||
|  | public class ThoughtProvider | ||||||
|  | { | ||||||
|  |     private readonly PostService.PostServiceClient _postClient; | ||||||
|  |     private readonly AccountService.AccountServiceClient _accountClient; | ||||||
|  |     private readonly IConfiguration _configuration; | ||||||
|  |     private readonly ILogger<ThoughtProvider> _logger; | ||||||
|  |  | ||||||
|  |     public Kernel Kernel { get; } | ||||||
|  |  | ||||||
|  |     private string? ModelProviderType { get; set; } | ||||||
|  |     public string? ModelDefault { get; set; } | ||||||
|  |  | ||||||
|  |     [Experimental("SKEXP0050")] | ||||||
|  |     public ThoughtProvider( | ||||||
|  |         IConfiguration configuration, | ||||||
|  |         PostService.PostServiceClient postServiceClient, | ||||||
|  |         AccountService.AccountServiceClient accountServiceClient, | ||||||
|  |         ILogger<ThoughtProvider> logger | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         _logger = logger; | ||||||
|  |         _postClient = postServiceClient; | ||||||
|  |         _accountClient = accountServiceClient; | ||||||
|  |         _configuration = configuration; | ||||||
|  |  | ||||||
|  |         Kernel = InitializeThinkingProvider(configuration); | ||||||
|  |         InitializeHelperFunctions(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Kernel InitializeThinkingProvider(IConfiguration configuration) | ||||||
|  |     { | ||||||
|  |         var cfg = configuration.GetSection("Thinking"); | ||||||
|  |         ModelProviderType = cfg.GetValue<string>("Provider")?.ToLower(); | ||||||
|  |         ModelDefault = cfg.GetValue<string>("Model"); | ||||||
|  |         var endpoint = cfg.GetValue<string>("Endpoint"); | ||||||
|  |         var apiKey = cfg.GetValue<string>("ApiKey"); | ||||||
|  |  | ||||||
|  |         var builder = Kernel.CreateBuilder(); | ||||||
|  |  | ||||||
|  |         switch (ModelProviderType) | ||||||
|  |         { | ||||||
|  |             case "ollama": | ||||||
|  |                 builder.AddOllamaChatCompletion(ModelDefault!, new Uri(endpoint ?? "http://localhost:11434/api")); | ||||||
|  |                 break; | ||||||
|  |             case "deepseek": | ||||||
|  |                 var client = new OpenAIClient( | ||||||
|  |                     new ApiKeyCredential(apiKey!), | ||||||
|  |                     new OpenAIClientOptions { Endpoint = new Uri(endpoint ?? "https://api.deepseek.com/v1") } | ||||||
|  |                 ); | ||||||
|  |                 builder.AddOpenAIChatCompletion(ModelDefault!, client); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return builder.Build(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [Experimental("SKEXP0050")] | ||||||
|  |     private void InitializeHelperFunctions() | ||||||
|  |     { | ||||||
|  |         // Add Solar Network tools plugin | ||||||
|  |         Kernel.ImportPluginFromFunctions("solar_network", [ | ||||||
|  |             KernelFunctionFactory.CreateFromMethod(async (string userId) => | ||||||
|  |             { | ||||||
|  |                 var request = new GetAccountRequest { Id = userId }; | ||||||
|  |                 var response = await _accountClient.GetAccountAsync(request); | ||||||
|  |                 return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions); | ||||||
|  |             }, "get_user", "Get a user profile from the Solar Network."), | ||||||
|  |             KernelFunctionFactory.CreateFromMethod(async (string postId) => | ||||||
|  |             { | ||||||
|  |                 var request = new GetPostRequest { Id = postId }; | ||||||
|  |                 var response = await _postClient.GetPostAsync(request); | ||||||
|  |                 return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions); | ||||||
|  |             }, "get_post", "Get a single post by ID from the Solar Network."), | ||||||
|  |             KernelFunctionFactory.CreateFromMethod(async (string query) => | ||||||
|  |                 { | ||||||
|  |                     var request = new SearchPostsRequest { Query = query, PageSize = 10 }; | ||||||
|  |                     var response = await _postClient.SearchPostsAsync(request); | ||||||
|  |                     return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions); | ||||||
|  |                 }, "search_posts", | ||||||
|  |                 "Search posts by query from the Solar Network. The input query is will be used to search with title, description and body content"), | ||||||
|  |             KernelFunctionFactory.CreateFromMethod(async ( | ||||||
|  |                     string? orderBy = null, | ||||||
|  |                     string? afterIso = null, | ||||||
|  |                     string? beforeIso = null | ||||||
|  |                 ) => | ||||||
|  |                 { | ||||||
|  |                     _logger.LogInformation("Begin building request to list post from sphere..."); | ||||||
|  |  | ||||||
|  |                     var request = new ListPostsRequest | ||||||
|  |                     { | ||||||
|  |                         PageSize = 20, | ||||||
|  |                         OrderBy = orderBy, | ||||||
|  |                     }; | ||||||
|  |                     if (!string.IsNullOrEmpty(afterIso)) | ||||||
|  |                         try | ||||||
|  |                         { | ||||||
|  |                             request.After = InstantPattern.General.Parse(afterIso).Value.ToTimestamp(); | ||||||
|  |                         } | ||||||
|  |                         catch (Exception) | ||||||
|  |                         { | ||||||
|  |                             _logger.LogWarning("Invalid afterIso format: {AfterIso}", afterIso); | ||||||
|  |                         } | ||||||
|  |                     if (!string.IsNullOrEmpty(beforeIso)) | ||||||
|  |                         try | ||||||
|  |                         { | ||||||
|  |                             request.Before = InstantPattern.General.Parse(beforeIso).Value.ToTimestamp(); | ||||||
|  |                         } | ||||||
|  |                         catch (Exception) | ||||||
|  |                         { | ||||||
|  |                             _logger.LogWarning("Invalid beforeIso format: {BeforeIso}", beforeIso); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                     _logger.LogInformation("Request built, {Request}", request); | ||||||
|  |  | ||||||
|  |                     var response = await _postClient.ListPostsAsync(request); | ||||||
|  |  | ||||||
|  |                     var data = response.Posts.Select(SnPost.FromProtoValue); | ||||||
|  |                     _logger.LogInformation("Sphere service returned posts: {Posts}", data); | ||||||
|  |                     return JsonSerializer.Serialize(data, GrpcTypeHelper.SerializerOptions); | ||||||
|  |                 }, "list_posts", | ||||||
|  |                 "Get posts from the Solar Network.\n" + | ||||||
|  |                 "Parameters:\n" + | ||||||
|  |                 "orderBy (optional, string: order by published date, accept asc or desc)\n" + | ||||||
|  |                 "afterIso (optional, string: ISO date for posts after this date)\n" + | ||||||
|  |                 "beforeIso (optional, string: ISO date for posts before this date)" | ||||||
|  |             ) | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         // Add web search plugins if configured | ||||||
|  |         var bingApiKey = _configuration.GetValue<string>("Thinking:BingApiKey"); | ||||||
|  |         if (!string.IsNullOrEmpty(bingApiKey)) | ||||||
|  |         { | ||||||
|  |             var bingConnector = new BingConnector(bingApiKey); | ||||||
|  |             var bing = new WebSearchEnginePlugin(bingConnector); | ||||||
|  |             Kernel.ImportPluginFromObject(bing, "bing"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var googleApiKey = _configuration.GetValue<string>("Thinking:GoogleApiKey"); | ||||||
|  |         var googleCx = _configuration.GetValue<string>("Thinking:GoogleCx"); | ||||||
|  |         if (!string.IsNullOrEmpty(googleApiKey) && !string.IsNullOrEmpty(googleCx)) | ||||||
|  |         { | ||||||
|  |             var googleConnector = new GoogleConnector( | ||||||
|  |                 apiKey: googleApiKey, | ||||||
|  |                 searchEngineId: googleCx); | ||||||
|  |             var google = new WebSearchEnginePlugin(googleConnector); | ||||||
|  |             Kernel.ImportPluginFromObject(google, "google"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public PromptExecutionSettings CreatePromptExecutionSettings() | ||||||
|  |     { | ||||||
|  |         switch (ModelProviderType) | ||||||
|  |         { | ||||||
|  |             case "ollama": | ||||||
|  |                 return new OllamaPromptExecutionSettings | ||||||
|  |                 { | ||||||
|  |                     FunctionChoiceBehavior = FunctionChoiceBehavior.Auto( | ||||||
|  |                         options: new FunctionChoiceBehaviorOptions | ||||||
|  |                         { | ||||||
|  |                             AllowParallelCalls = true, | ||||||
|  |                             AllowConcurrentInvocation = true | ||||||
|  |                         }) | ||||||
|  |                 }; | ||||||
|  |             case "deepseek": | ||||||
|  |                 return new OpenAIPromptExecutionSettings | ||||||
|  |                 { | ||||||
|  |                     FunctionChoiceBehavior = FunctionChoiceBehavior.Auto( | ||||||
|  |                         options: new FunctionChoiceBehaviorOptions | ||||||
|  |                         { | ||||||
|  |                             AllowParallelCalls = true, | ||||||
|  |                             AllowConcurrentInvocation = true | ||||||
|  |                         }) | ||||||
|  |                 }; | ||||||
|  |             default: | ||||||
|  |                 throw new InvalidOperationException("Unknown provider: " + ModelProviderType); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										152
									
								
								DysonNetwork.Insight/Thought/ThoughtService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								DysonNetwork.Insight/Thought/ThoughtService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using PaymentService = DysonNetwork.Shared.Proto.PaymentService; | ||||||
|  | using TransactionType = DysonNetwork.Shared.Proto.TransactionType; | ||||||
|  | using WalletService = DysonNetwork.Shared.Proto.WalletService; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Insight.Thought; | ||||||
|  |  | ||||||
|  | public class ThoughtService(AppDatabase db, ICacheService cache, PaymentService.PaymentServiceClient paymentService, WalletService.WalletServiceClient walletService) | ||||||
|  | { | ||||||
|  |     public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId, | ||||||
|  |         string? topic = null) | ||||||
|  |     { | ||||||
|  |         if (sequenceId.HasValue) | ||||||
|  |         { | ||||||
|  |             var seq = await db.ThinkingSequences.FindAsync(sequenceId.Value); | ||||||
|  |             if (seq == null || seq.AccountId != accountId) return null; | ||||||
|  |             return seq; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             var seq = new SnThinkingSequence { AccountId = accountId, Topic = topic }; | ||||||
|  |             db.ThinkingSequences.Add(seq); | ||||||
|  |             await db.SaveChangesAsync(); | ||||||
|  |             return seq; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<SnThinkingThought> SaveThoughtAsync( | ||||||
|  |         SnThinkingSequence sequence, | ||||||
|  |         string content, | ||||||
|  |         ThinkingThoughtRole role, | ||||||
|  |         List<SnThinkingChunk>? chunks = null, | ||||||
|  |         string? model = null | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         // Approximate token count (1 token ≈ 4 characters for GPT-like models) | ||||||
|  |         var tokenCount = content?.Length / 4 ?? 0; | ||||||
|  |  | ||||||
|  |         var thought = new SnThinkingThought | ||||||
|  |         { | ||||||
|  |             SequenceId = sequence.Id, | ||||||
|  |             Content = content, | ||||||
|  |             Role = role, | ||||||
|  |             TokenCount = tokenCount, | ||||||
|  |             ModelName = model, | ||||||
|  |             Chunks = chunks ?? new List<SnThinkingChunk>() | ||||||
|  |         }; | ||||||
|  |         db.ThinkingThoughts.Add(thought); | ||||||
|  |  | ||||||
|  |         // Update sequence total tokens only for assistant responses | ||||||
|  |         if (role == ThinkingThoughtRole.Assistant) | ||||||
|  |             sequence.TotalToken += tokenCount; | ||||||
|  |  | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         // Invalidate cache for this sequence's thoughts | ||||||
|  |         await cache.RemoveGroupAsync($"sequence:{sequence.Id}"); | ||||||
|  |  | ||||||
|  |         return thought; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<SnThinkingThought>> GetPreviousThoughtsAsync(SnThinkingSequence sequence) | ||||||
|  |     { | ||||||
|  |         var cacheKey = $"thoughts:{sequence.Id}"; | ||||||
|  |         var (found, cachedThoughts) = await cache.GetAsyncWithStatus<List<SnThinkingThought>>(cacheKey); | ||||||
|  |         if (found && cachedThoughts != null) | ||||||
|  |         { | ||||||
|  |             return cachedThoughts; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var thoughts = await db.ThinkingThoughts | ||||||
|  |             .Where(t => t.SequenceId == sequence.Id) | ||||||
|  |             .OrderByDescending(t => t.CreatedAt) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         // Cache for 10 minutes | ||||||
|  |         await cache.SetWithGroupsAsync(cacheKey, thoughts, [$"sequence:{sequence.Id}"], TimeSpan.FromMinutes(10)); | ||||||
|  |  | ||||||
|  |         return thoughts; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<(int total, List<SnThinkingSequence> sequences)> ListSequencesAsync(Guid accountId, int offset, | ||||||
|  |         int take) | ||||||
|  |     { | ||||||
|  |         var query = db.ThinkingSequences.Where(s => s.AccountId == accountId); | ||||||
|  |         var totalCount = await query.CountAsync(); | ||||||
|  |         var sequences = await query | ||||||
|  |             .OrderByDescending(s => s.CreatedAt) | ||||||
|  |             .Skip(offset) | ||||||
|  |             .Take(take) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         return (totalCount, sequences); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task SettleThoughtBills(ILogger logger) | ||||||
|  |     { | ||||||
|  |         var sequences = await db.ThinkingSequences | ||||||
|  |             .Where(s => s.PaidToken < s.TotalToken) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         if (sequences.Count == 0) | ||||||
|  |         { | ||||||
|  |             logger.LogInformation("No unpaid sequences found."); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Group by account | ||||||
|  |         var groupedByAccount = sequences.GroupBy(s => s.AccountId); | ||||||
|  |  | ||||||
|  |         foreach (var accountGroup in groupedByAccount) | ||||||
|  |         { | ||||||
|  |             var accountId = accountGroup.Key; | ||||||
|  |             var totalUnpaidTokens = accountGroup.Sum(s => s.TotalToken - s.PaidToken); | ||||||
|  |             var cost = (long)Math.Ceiling(totalUnpaidTokens / 1000.0); | ||||||
|  |  | ||||||
|  |             if (cost == 0) continue; | ||||||
|  |  | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var walletResponse = await walletService.GetWalletAsync(new GetWalletRequest { AccountId = accountId.ToString() }); | ||||||
|  |                 var walletId = Guid.Parse(walletResponse.Id); | ||||||
|  |  | ||||||
|  |                 var date = DateTime.Now.ToString("yyyy-MM-dd"); | ||||||
|  |                 await paymentService.CreateTransactionAsync(new CreateTransactionRequest | ||||||
|  |                 { | ||||||
|  |                     PayerWalletId = walletId.ToString(), | ||||||
|  |                     PayeeWalletId = null, | ||||||
|  |                     Currency = WalletCurrency.SourcePoint, | ||||||
|  |                     Amount = cost.ToString(), | ||||||
|  |                     Remarks = $"Wage for SN-chan on {date}", | ||||||
|  |                     Type = TransactionType.System | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 // Mark all sequences for this account as paid | ||||||
|  |                 foreach (var sequence in accountGroup) | ||||||
|  |                     sequence.PaidToken = sequence.TotalToken; | ||||||
|  |  | ||||||
|  |                 logger.LogInformation("Billed {cost} points for account {accountId}", cost, accountId); | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 logger.LogError(ex, "Error billing for account {accountId}", accountId); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								DysonNetwork.Insight/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Insight/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | { | ||||||
|  |     "Debug": true, | ||||||
|  |     "BaseUrl": "http://localhost:5071", | ||||||
|  |     "SiteUrl": "https://solian.app", | ||||||
|  |     "Logging": { | ||||||
|  |         "LogLevel": { | ||||||
|  |             "Default": "Information", | ||||||
|  |             "Microsoft.AspNetCore": "Warning" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "AllowedHosts": "*", | ||||||
|  |     "ConnectionStrings": { | ||||||
|  |         "App": "Host=localhost;Port=5432;Database=dyson_insight;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||||
|  |     }, | ||||||
|  |     "KnownProxies": [ | ||||||
|  |         "127.0.0.1", | ||||||
|  |         "::1" | ||||||
|  |     ], | ||||||
|  |     "Etcd": { | ||||||
|  |         "Insecure": true | ||||||
|  |     }, | ||||||
|  |     "Thinking": { | ||||||
|  |         "Provider": "deepseek", | ||||||
|  |         "Model": "deepseek-chat", | ||||||
|  |         "ApiKey": "sk-bd20f6a2e9fa40b98c46899baa0e9f09" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -80,7 +80,7 @@ public class AccountCurrentController( | |||||||
|         [MaxLength(1024)] public string? TimeZone { get; set; } |         [MaxLength(1024)] public string? TimeZone { get; set; } | ||||||
|         [MaxLength(1024)] public string? Location { get; set; } |         [MaxLength(1024)] public string? Location { get; set; } | ||||||
|         [MaxLength(4096)] public string? Bio { get; set; } |         [MaxLength(4096)] public string? Bio { get; set; } | ||||||
|         public UsernameColor? UsernameColor { get; set; } |         public Shared.Models.UsernameColor? UsernameColor { get; set; } | ||||||
|         public Instant? Birthday { get; set; } |         public Instant? Birthday { get; set; } | ||||||
|         public List<ProfileLink>? Links { get; set; } |         public List<ProfileLink>? Links { get; set; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -271,7 +271,7 @@ public class AccountEventService( | |||||||
|         return backdatedCheckInMonths < 4; |         return backdatedCheckInMonths < 4; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public const string CheckInLockKey = "checkin:lock:"; |     private const string CheckInLockKey = "checkin:lock:"; | ||||||
|  |  | ||||||
|     public async Task<SnCheckInResult> CheckInDaily(SnAccount user, Instant? backdated = null) |     public async Task<SnCheckInResult> CheckInDaily(SnAccount user, Instant? backdated = null) | ||||||
|     { |     { | ||||||
| @@ -322,7 +322,11 @@ public class AccountEventService( | |||||||
|         })); |         })); | ||||||
|  |  | ||||||
|         // The 5 is specialized, keep it alone. |         // The 5 is specialized, keep it alone. | ||||||
|         var checkInLevel = (CheckInResultLevel)Random.Next(Enum.GetValues<CheckInResultLevel>().Length - 1); |         var sum = 0; | ||||||
|  |         var maxLevel = Enum.GetValues<CheckInResultLevel>().Length - 1; | ||||||
|  |         for (var i = 0; i < 5; i++) | ||||||
|  |             sum += Random.Next(maxLevel); | ||||||
|  |         var checkInLevel = (CheckInResultLevel)(sum / 5); | ||||||
|  |  | ||||||
|         var accountBirthday = await db.AccountProfiles |         var accountBirthday = await db.AccountProfiles | ||||||
|             .Where(x => x.AccountId == user.Id) |             .Where(x => x.AccountId == user.Id) | ||||||
|   | |||||||
| @@ -12,13 +12,11 @@ public class AccountServiceGrpc( | |||||||
|     AccountEventService accountEvents, |     AccountEventService accountEvents, | ||||||
|     RelationshipService relationships, |     RelationshipService relationships, | ||||||
|     SubscriptionService subscriptions, |     SubscriptionService subscriptions, | ||||||
|     IClock clock, |  | ||||||
|     ILogger<AccountServiceGrpc> logger |     ILogger<AccountServiceGrpc> logger | ||||||
| ) | ) | ||||||
|     : Shared.Proto.AccountService.AccountServiceBase |     : Shared.Proto.AccountService.AccountServiceBase | ||||||
| { | { | ||||||
|     private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db)); |     private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db)); | ||||||
|     private readonly IClock _clock = clock ?? throw new ArgumentNullException(nameof(clock)); |  | ||||||
|  |  | ||||||
|     private readonly ILogger<AccountServiceGrpc> |     private readonly ILogger<AccountServiceGrpc> | ||||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||||
| @@ -160,6 +158,26 @@ public class AccountServiceGrpc( | |||||||
|         return response; |         return response; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public override async Task<GetAccountBatchResponse> SearchAccount(SearchAccountRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var accounts = await _db.Accounts | ||||||
|  |             .AsNoTracking() | ||||||
|  |             .Where(a => EF.Functions.ILike(a.Name, $"%{request.Query}%")) | ||||||
|  |             .Include(a => a.Profile) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         var perks = await subscriptions.GetPerkSubscriptionsAsync( | ||||||
|  |             accounts.Select(x => x.Id).ToList() | ||||||
|  |         ); | ||||||
|  |         foreach (var account in accounts) | ||||||
|  |             if (perks.TryGetValue(account.Id, out var perk)) | ||||||
|  |                 account.PerkSubscription = perk?.ToReference(); | ||||||
|  |  | ||||||
|  |         var response = new GetAccountBatchResponse(); | ||||||
|  |         response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request, |     public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request, | ||||||
|         ServerCallContext context) |         ServerCallContext context) | ||||||
|     { |     { | ||||||
| @@ -246,7 +264,7 @@ public class AccountServiceGrpc( | |||||||
|  |  | ||||||
|     public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context) |     public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context) | ||||||
|     { |     { | ||||||
|         var hasRelationship = false; |         bool hasRelationship; | ||||||
|         if (!request.HasStatus) |         if (!request.HasStatus) | ||||||
|             hasRelationship = await relationships.HasExistingRelationship( |             hasRelationship = await relationships.HasExistingRelationship( | ||||||
|                 Guid.Parse(request.AccountId), |                 Guid.Parse(request.AccountId), | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ namespace DysonNetwork.Pass.Account; | |||||||
|  |  | ||||||
| public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||||
| { | { | ||||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object?> meta) |     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) | ||||||
|     { |     { | ||||||
|         var log = new SnActionLog |         var log = new SnActionLog | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -32,8 +32,8 @@ public class ActionLogServiceGrpc : Shared.Proto.ActionLogService.ActionLogServi | |||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var meta = request.Meta |             var meta = request.Meta | ||||||
|                 ?.Select(x => new KeyValuePair<string, object?>(x.Key, GrpcTypeHelper.ConvertValueToObject(x.Value))) |                 ?.Select(x => new KeyValuePair<string, object>(x.Key, GrpcTypeHelper.ConvertValueToObject(x.Value))) | ||||||
|                 .ToDictionary() ?? new Dictionary<string, object?>(); |                 .ToDictionary() ?? new Dictionary<string, object>(); | ||||||
|  |  | ||||||
|             _actionLogService.CreateActionLog( |             _actionLogService.CreateActionLog( | ||||||
|                 accountId, |                 accountId, | ||||||
| @@ -41,6 +41,7 @@ public class ActionLogServiceGrpc : Shared.Proto.ActionLogService.ActionLogServi | |||||||
|                 meta |                 meta | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|  |             await Task.CompletedTask; | ||||||
|             return new CreateActionLogResponse(); |             return new CreateActionLogResponse(); | ||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|   | |||||||
| @@ -39,6 +39,9 @@ public class AppDatabase( | |||||||
|     public DbSet<SnAuthClient> AuthClients { get; set; } = null!; |     public DbSet<SnAuthClient> AuthClients { get; set; } = null!; | ||||||
|     public DbSet<SnApiKey> ApiKeys { get; set; } = null!; |     public DbSet<SnApiKey> ApiKeys { get; set; } = null!; | ||||||
|      |      | ||||||
|  |     public DbSet<SnRealm> Realms { get; set; } = null!; | ||||||
|  |     public DbSet<SnRealmMember> RealmMembers { get; set; } = null!; | ||||||
|  |  | ||||||
|     public DbSet<SnWallet> Wallets { get; set; } = null!; |     public DbSet<SnWallet> Wallets { get; set; } = null!; | ||||||
|     public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!; |     public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!; | ||||||
|     public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!; |     public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!; | ||||||
| @@ -54,6 +57,9 @@ public class AppDatabase( | |||||||
|     public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!; |     public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!; | ||||||
|     public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!; |     public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     public DbSet<SnLottery> Lotteries { get; set; } = null!; | ||||||
|  |     public DbSet<SnLotteryRecord> LotteryRecords { get; set; } = null!; | ||||||
|  |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|     { |     { | ||||||
|         optionsBuilder.UseNpgsql( |         optionsBuilder.UseNpgsql( | ||||||
| @@ -128,6 +134,14 @@ public class AppDatabase( | |||||||
|             .WithMany(a => a.IncomingRelationships) |             .WithMany(a => a.IncomingRelationships) | ||||||
|             .HasForeignKey(r => r.RelatedId); |             .HasForeignKey(r => r.RelatedId); | ||||||
|          |          | ||||||
|  |         modelBuilder.Entity<SnRealmMember>() | ||||||
|  |             .HasKey(pm => new { pm.RealmId, pm.AccountId }); | ||||||
|  |         modelBuilder.Entity<SnRealmMember>() | ||||||
|  |             .HasOne(pm => pm.Realm) | ||||||
|  |             .WithMany(p => p.Members) | ||||||
|  |             .HasForeignKey(pm => pm.RealmId) | ||||||
|  |             .OnDelete(DeleteBehavior.Cascade); | ||||||
|  |  | ||||||
|         // Automatically apply soft-delete filter to all entities inheriting BaseModel |         // Automatically apply soft-delete filter to all entities inheriting BaseModel | ||||||
|         foreach (var entityType in modelBuilder.Model.GetEntityTypes()) |         foreach (var entityType in modelBuilder.Model.GetEntityTypes()) | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -343,8 +343,8 @@ public class OidcProviderController( | |||||||
|         { |         { | ||||||
|             issuer, |             issuer, | ||||||
|             authorization_endpoint = $"{siteUrl}/auth/authorize", |             authorization_endpoint = $"{siteUrl}/auth/authorize", | ||||||
|             token_endpoint = $"{baseUrl}/id/auth/open/token", |             token_endpoint = $"{baseUrl}/pass/auth/open/token", | ||||||
|             userinfo_endpoint = $"{baseUrl}/id/auth/open/userinfo", |             userinfo_endpoint = $"{baseUrl}/pass/auth/open/userinfo", | ||||||
|             jwks_uri = $"{baseUrl}/.well-known/jwks", |             jwks_uri = $"{baseUrl}/.well-known/jwks", | ||||||
|             scopes_supported = new[] { "openid", "profile", "email" }, |             scopes_supported = new[] { "openid", "profile", "email" }, | ||||||
|             response_types_supported = new[] |             response_types_supported = new[] | ||||||
|   | |||||||
| @@ -7,49 +7,44 @@ | |||||||
|     </PropertyGroup> |     </PropertyGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/> |         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> |         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="Nager.Holiday" Version="1.0.1" /> |         <PackageReference Include="Nager.Holiday" Version="1.0.1" /> | ||||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> |         <PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118"> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2"/> |         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/> |         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2"/> |         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> |         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" /> | ||||||
|         <PackageReference Include="Otp.NET" Version="1.4.0"/> |         <PackageReference Include="Otp.NET" Version="1.4.0" /> | ||||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> |         <PackageReference Include="Quartz" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1"/> |         <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1"/> |         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> | ||||||
|         <PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5"/> |         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||||
|         <PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0"/> |         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" /> | ||||||
|         <PackageReference Include="Quartz" Version="3.14.0"/> |         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" /> | ||||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/> |         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3"/> |         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> | ||||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/> |  | ||||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/> |  | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/> |  | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> |  | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" /> |  | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/> |         <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Leveling; | namespace DysonNetwork.Pass.Leveling; | ||||||
|  |  | ||||||
| public class ExperienceService(AppDatabase db, SubscriptionService subscriptions, ICacheService cache) | public class ExperienceService(AppDatabase db, SubscriptionService subscriptions) | ||||||
| { | { | ||||||
|     public async Task<SnExperienceRecord> AddRecord(string reasonType, string reason, long delta, Guid accountId) |     public async Task<SnExperienceRecord> AddRecord(string reasonType, string reason, long delta, Guid accountId) | ||||||
|     { |     { | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								DysonNetwork.Pass/Lotteries/LotteryController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								DysonNetwork.Pass/Lotteries/LotteryController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Pass.Wallet; | ||||||
|  | using DysonNetwork.Pass.Permission; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Lotteries; | ||||||
|  |  | ||||||
|  | [ApiController] | ||||||
|  | [Route("/api/lotteries")] | ||||||
|  | public class LotteryController(AppDatabase db, LotteryService lotteryService) : ControllerBase | ||||||
|  | { | ||||||
|  |     public class CreateLotteryRequest | ||||||
|  |     { | ||||||
|  |         [Required] | ||||||
|  |         public List<int> RegionOneNumbers { get; set; } = null!; | ||||||
|  |         [Required] | ||||||
|  |         [Range(0, 99)] | ||||||
|  |         public int RegionTwoNumber { get; set; } | ||||||
|  |         [Range(1, int.MaxValue)] | ||||||
|  |         public int Multiplier { get; set; } = 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpPost] | ||||||
|  |     [Authorize] | ||||||
|  |     public async Task<ActionResult<SnWalletOrder>> CreateLottery([FromBody] CreateLotteryRequest request) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             var order = await lotteryService.CreateLotteryOrderAsync( | ||||||
|  |                 accountId: currentUser.Id, | ||||||
|  |                 region1: request.RegionOneNumbers, | ||||||
|  |                 region2: request.RegionTwoNumber, | ||||||
|  |                 multiplier: request.Multiplier); | ||||||
|  |  | ||||||
|  |             return Ok(order); | ||||||
|  |         } | ||||||
|  |         catch (ArgumentException err) | ||||||
|  |         { | ||||||
|  |             return BadRequest(err.Message); | ||||||
|  |         } | ||||||
|  |         catch (InvalidOperationException err) | ||||||
|  |         { | ||||||
|  |             return BadRequest(err.Message); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpGet] | ||||||
|  |     [Authorize] | ||||||
|  |     public async Task<ActionResult<List<SnLottery>>> GetLotteries( | ||||||
|  |         [FromQuery] int offset = 0, | ||||||
|  |         [FromQuery] int limit = 20) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|  |         var lotteries = await lotteryService.GetUserTicketsAsync(currentUser.Id, offset, limit); | ||||||
|  |         var total = await lotteryService.GetUserTicketCountAsync(currentUser.Id); | ||||||
|  |  | ||||||
|  |         Response.Headers["X-Total"] = total.ToString(); | ||||||
|  |  | ||||||
|  |         return Ok(lotteries); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpGet("{id}")] | ||||||
|  |     [Authorize] | ||||||
|  |     public async Task<ActionResult<SnLottery>> GetLottery(Guid id) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|  |         var lottery = await lotteryService.GetTicketAsync(id); | ||||||
|  |         if (lottery == null || lottery.AccountId != currentUser.Id) | ||||||
|  |             return NotFound(); | ||||||
|  |  | ||||||
|  |         return Ok(lottery); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpPost("draw")] | ||||||
|  |     [Authorize] | ||||||
|  |     [RequiredPermission("maintenance", "lotteries.draw.perform")] | ||||||
|  |     public async Task<IActionResult> PerformLotteryDraw() | ||||||
|  |     { | ||||||
|  |         await lotteryService.DrawLotteries(); | ||||||
|  |         return Ok(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpGet("records")] | ||||||
|  |     public async Task<ActionResult<List<SnLotteryRecord>>> GetLotteryRecords( | ||||||
|  |         [FromQuery] Instant? startDate = null, | ||||||
|  |         [FromQuery] Instant? endDate = null, | ||||||
|  |         [FromQuery] int offset = 0, | ||||||
|  |         [FromQuery] int limit = 20) | ||||||
|  |     { | ||||||
|  |         var query = db.LotteryRecords | ||||||
|  |             .OrderByDescending(r => r.CreatedAt) | ||||||
|  |             .AsQueryable(); | ||||||
|  |  | ||||||
|  |         if (startDate.HasValue) | ||||||
|  |             query = query.Where(r => r.DrawDate >= startDate.Value); | ||||||
|  |         if (endDate.HasValue) | ||||||
|  |             query = query.Where(r => r.DrawDate <= endDate.Value); | ||||||
|  |  | ||||||
|  |         var total = await query.CountAsync(); | ||||||
|  |         Response.Headers["X-Total"] = total.ToString(); | ||||||
|  |  | ||||||
|  |         var records = await query | ||||||
|  |             .Skip(offset) | ||||||
|  |             .Take(limit) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         return Ok(records); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								DysonNetwork.Pass/Lotteries/LotteryDrawJob.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Pass/Lotteries/LotteryDrawJob.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | using Quartz; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Lotteries; | ||||||
|  |  | ||||||
|  | public class LotteryDrawJob(LotteryService lotteryService, ILogger<LotteryDrawJob> logger) : IJob | ||||||
|  | { | ||||||
|  |     public async Task Execute(IJobExecutionContext context) | ||||||
|  |     { | ||||||
|  |         logger.LogInformation("Starting daily lottery draw..."); | ||||||
|  |  | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             await lotteryService.DrawLotteries(); | ||||||
|  |             logger.LogInformation("Daily lottery draw completed successfully."); | ||||||
|  |         } | ||||||
|  |         catch (Exception ex) | ||||||
|  |         { | ||||||
|  |             logger.LogError(ex, "Error occurred during daily lottery draw."); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										274
									
								
								DysonNetwork.Pass/Lotteries/LotteryService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								DysonNetwork.Pass/Lotteries/LotteryService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,274 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Pass.Wallet; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using NodaTime; | ||||||
|  | using System.Text.Json; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Lotteries; | ||||||
|  |  | ||||||
|  | public class LotteryOrderMetaData | ||||||
|  | { | ||||||
|  |     public Guid AccountId { get; set; } | ||||||
|  |     public List<int> RegionOneNumbers { get; set; } = new(); | ||||||
|  |     public int RegionTwoNumber { get; set; } | ||||||
|  |     public int Multiplier { get; set; } = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class LotteryService( | ||||||
|  |     AppDatabase db, | ||||||
|  |     PaymentService paymentService, | ||||||
|  |     WalletService walletService, | ||||||
|  |     ILogger<LotteryService> logger) | ||||||
|  | { | ||||||
|  |     private static bool ValidateNumbers(List<int> region1, int region2) | ||||||
|  |     { | ||||||
|  |         if (region1.Count != 5 || region1.Distinct().Count() != 5) | ||||||
|  |             return false; | ||||||
|  |         if (region1.Any(n => n < 0 || n > 99)) | ||||||
|  |             return false; | ||||||
|  |         if (region2 < 0 || region2 > 99) | ||||||
|  |             return false; | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<SnLottery> CreateTicketAsync(Guid accountId, List<int> region1, int region2, int multiplier = 1) | ||||||
|  |     { | ||||||
|  |         if (!ValidateNumbers(region1, region2)) | ||||||
|  |             throw new ArgumentException("Invalid lottery numbers"); | ||||||
|  |  | ||||||
|  |         var lottery = new SnLottery | ||||||
|  |         { | ||||||
|  |             AccountId = accountId, | ||||||
|  |             RegionOneNumbers = region1, | ||||||
|  |             RegionTwoNumber = region2, | ||||||
|  |             Multiplier = multiplier | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         db.Lotteries.Add(lottery); | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         return lottery; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<SnLottery>> GetUserTicketsAsync(Guid accountId, int offset = 0, int limit = 20) | ||||||
|  |     { | ||||||
|  |         return await db.Lotteries | ||||||
|  |             .Where(l => l.AccountId == accountId) | ||||||
|  |             .OrderByDescending(l => l.CreatedAt) | ||||||
|  |             .Skip(offset) | ||||||
|  |             .Take(limit) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<SnLottery?> GetTicketAsync(Guid id) | ||||||
|  |     { | ||||||
|  |         return await db.Lotteries.FirstOrDefaultAsync(l => l.Id == id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> GetUserTicketCountAsync(Guid accountId) | ||||||
|  |     { | ||||||
|  |         return await db.Lotteries.CountAsync(l => l.AccountId == accountId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static decimal CalculateLotteryPrice(int multiplier) | ||||||
|  |     { | ||||||
|  |         return 10 + (multiplier - 1) * 10; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<SnWalletOrder> CreateLotteryOrderAsync(Guid accountId, List<int> region1, int region2, | ||||||
|  |         int multiplier = 1) | ||||||
|  |     { | ||||||
|  |         if (!ValidateNumbers(region1, region2)) | ||||||
|  |             throw new ArgumentException("Invalid lottery numbers"); | ||||||
|  |  | ||||||
|  |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |         var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc() | ||||||
|  |             .ToInstant(); | ||||||
|  |         var hasPurchasedToday = await db.Lotteries.AnyAsync(l => | ||||||
|  |             l.AccountId == accountId && | ||||||
|  |             l.CreatedAt >= todayStart && | ||||||
|  |             l.DrawStatus == LotteryDrawStatus.Pending | ||||||
|  |         ); | ||||||
|  |         if (hasPurchasedToday) | ||||||
|  |             throw new InvalidOperationException("You can only purchase one lottery per day."); | ||||||
|  |  | ||||||
|  |         var price = CalculateLotteryPrice(multiplier); | ||||||
|  |  | ||||||
|  |         var lotteryData = new LotteryOrderMetaData | ||||||
|  |         { | ||||||
|  |             AccountId = accountId, | ||||||
|  |             RegionOneNumbers = region1, | ||||||
|  |             RegionTwoNumber = region2, | ||||||
|  |             Multiplier = multiplier | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return await paymentService.CreateOrderAsync( | ||||||
|  |             null, | ||||||
|  |             WalletCurrency.SourcePoint, | ||||||
|  |             price, | ||||||
|  |             appIdentifier: "lottery", | ||||||
|  |             productIdentifier: "lottery", | ||||||
|  |             meta: new Dictionary<string, object> | ||||||
|  |             { | ||||||
|  |                 ["data"] = JsonSerializer.Serialize(lotteryData) | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task HandleLotteryOrder(SnWalletOrder order) | ||||||
|  |     { | ||||||
|  |         if (order.Status == OrderStatus.Finished) | ||||||
|  |             return; // Already processed | ||||||
|  |  | ||||||
|  |         if (order.Status != OrderStatus.Paid || | ||||||
|  |             !order.Meta.TryGetValue("data", out var dataValue) || | ||||||
|  |             dataValue is null || | ||||||
|  |             dataValue is not JsonElement { ValueKind: JsonValueKind.String } jsonElem) | ||||||
|  |             throw new InvalidOperationException("Invalid order."); | ||||||
|  |  | ||||||
|  |         var jsonString = jsonElem.GetString(); | ||||||
|  |         if (jsonString is null) | ||||||
|  |             throw new InvalidOperationException("Invalid order."); | ||||||
|  |  | ||||||
|  |         var data = JsonSerializer.Deserialize<LotteryOrderMetaData>(jsonString); | ||||||
|  |         if (data is null) | ||||||
|  |             throw new InvalidOperationException("Invalid order data."); | ||||||
|  |  | ||||||
|  |         await CreateTicketAsync(data.AccountId, data.RegionOneNumbers, data.RegionTwoNumber, data.Multiplier); | ||||||
|  |  | ||||||
|  |         order.Status = OrderStatus.Finished; | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static int CalculateReward(int region1Matches, bool region2Match) | ||||||
|  |     { | ||||||
|  |         var reward = region1Matches switch | ||||||
|  |         { | ||||||
|  |             0 => 0, | ||||||
|  |             1 => 10, | ||||||
|  |             2 => 100, | ||||||
|  |             3 => 500, | ||||||
|  |             4 => 1000, | ||||||
|  |             5 => 10000, | ||||||
|  |             _ => 0 | ||||||
|  |         }; | ||||||
|  |         if (region2Match) reward *= 10; | ||||||
|  |         return reward; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static List<int> GenerateUniqueRandomNumbers(int count, int min, int max) | ||||||
|  |     { | ||||||
|  |         var numbers = new List<int>(); | ||||||
|  |         var random = new Random(); | ||||||
|  |         while (numbers.Count < count) | ||||||
|  |         { | ||||||
|  |             var num = random.Next(min, max + 1); | ||||||
|  |             if (!numbers.Contains(num)) numbers.Add(num); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return numbers.OrderBy(n => n).ToList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int CountMatches(List<int> playerNumbers, List<int> winningNumbers) | ||||||
|  |     { | ||||||
|  |         return playerNumbers.Intersect(winningNumbers).Count(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task DrawLotteries() | ||||||
|  |     { | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             logger.LogInformation("Starting drawing lotteries..."); | ||||||
|  |  | ||||||
|  |             var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |  | ||||||
|  |             // All pending lottery tickets | ||||||
|  |             var tickets = await db.Lotteries | ||||||
|  |                 .Where(l => l.DrawStatus == LotteryDrawStatus.Pending) | ||||||
|  |                 .ToListAsync(); | ||||||
|  |  | ||||||
|  |             if (tickets.Count == 0) | ||||||
|  |             { | ||||||
|  |                 logger.LogInformation("No pending lottery tickets"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             logger.LogInformation("Found {Count} pending lottery tickets for draw", tickets.Count); | ||||||
|  |  | ||||||
|  |             // Generate winning numbers | ||||||
|  |             var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99); | ||||||
|  |             var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0]; | ||||||
|  |  | ||||||
|  |             logger.LogInformation("Winning numbers generated: Region1 [{Region1}], Region2 [{Region2}]", | ||||||
|  |                 string.Join(",", winningRegion1), winningRegion2); | ||||||
|  |  | ||||||
|  |             var drawDate = Instant.FromDateTimeUtc(new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, | ||||||
|  |                 DateTime.UtcNow.Day, 0, 0, 0, DateTimeKind.Utc).AddDays(-1)); // Yesterday's date | ||||||
|  |  | ||||||
|  |             var totalPrizesAwarded = 0; | ||||||
|  |             long totalPrizeAmount = 0; | ||||||
|  |  | ||||||
|  |             // Process each ticket | ||||||
|  |             foreach (var ticket in tickets) | ||||||
|  |             { | ||||||
|  |                 var region1Matches = CountMatches(ticket.RegionOneNumbers, winningRegion1); | ||||||
|  |                 var region2Match = ticket.RegionTwoNumber == winningRegion2; | ||||||
|  |                 var reward = CalculateReward(region1Matches, region2Match); | ||||||
|  |  | ||||||
|  |                 // Record match results | ||||||
|  |                 ticket.MatchedRegionOneNumbers = ticket.RegionOneNumbers.Intersect(winningRegion1).ToList(); | ||||||
|  |                 ticket.MatchedRegionTwoNumber = region2Match ? (int?)winningRegion2 : null; | ||||||
|  |  | ||||||
|  |                 if (reward > 0) | ||||||
|  |                 { | ||||||
|  |                     var wallet = await walletService.GetWalletAsync(ticket.AccountId); | ||||||
|  |                     if (wallet != null) | ||||||
|  |                     { | ||||||
|  |                         await paymentService.CreateTransactionAsync( | ||||||
|  |                             payerWalletId: null, | ||||||
|  |                             payeeWalletId: wallet.Id, | ||||||
|  |                             currency: WalletCurrency.SourcePoint, | ||||||
|  |                             amount: reward, | ||||||
|  |                             remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}" | ||||||
|  |                         ); | ||||||
|  |                         logger.LogInformation( | ||||||
|  |                             "Awarded {Amount} to account {AccountId} for {Matches} matches{(Special ? \" + special\" : \"\")}", | ||||||
|  |                             reward, ticket.AccountId, region1Matches, region2Match ? " + special" : ""); | ||||||
|  |                         totalPrizesAwarded++; | ||||||
|  |                         totalPrizeAmount += reward; | ||||||
|  |                     } | ||||||
|  |                     else | ||||||
|  |                     { | ||||||
|  |                         logger.LogWarning("Wallet not found for account {AccountId}, skipping prize award", | ||||||
|  |                             ticket.AccountId); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 ticket.DrawStatus = LotteryDrawStatus.Drawn; | ||||||
|  |                 ticket.DrawDate = drawDate; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Save the draw record | ||||||
|  |             var lotteryRecord = new SnLotteryRecord | ||||||
|  |             { | ||||||
|  |                 DrawDate = drawDate, | ||||||
|  |                 WinningRegionOneNumbers = winningRegion1, | ||||||
|  |                 WinningRegionTwoNumber = winningRegion2, | ||||||
|  |                 TotalTickets = tickets.Count, | ||||||
|  |                 TotalPrizesAwarded = totalPrizesAwarded, | ||||||
|  |                 TotalPrizeAmount = totalPrizeAmount | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             db.LotteryRecords.Add(lotteryRecord); | ||||||
|  |             await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |             logger.LogInformation("Daily lottery draw completed: {Prizes} prizes awarded, total amount {Amount}", | ||||||
|  |                 totalPrizesAwarded, totalPrizeAmount); | ||||||
|  |         } | ||||||
|  |         catch (Exception ex) | ||||||
|  |         { | ||||||
|  |             logger.LogError(ex, "An error occurred during the daily lottery draw"); | ||||||
|  |             throw; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2677
									
								
								DysonNetwork.Pass/Migrations/20251021153439_AddRealmFromSphere.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2677
									
								
								DysonNetwork.Pass/Migrations/20251021153439_AddRealmFromSphere.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,160 @@ | |||||||
|  | using System; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddRealmFromSphere : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "realms", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||||
|  |                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||||
|  |                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), | ||||||
|  |                     is_community = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     is_public = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_realms", x => x.id); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "realm_members", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     realm_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     role = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_realm_members", x => new { x.realm_id, x.account_id }); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_realm_members_realms_realm_id", | ||||||
|  |                         column: x => x.realm_id, | ||||||
|  |                         principalTable: "realms", | ||||||
|  |                         principalColumn: "id", | ||||||
|  |                         onDelete: ReferentialAction.Cascade); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "sn_chat_room", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), | ||||||
|  |                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||||
|  |                     type = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     is_community = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     is_public = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     realm_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||||
|  |                     sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_sn_chat_room", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_sn_chat_room_realms_sn_realm_id", | ||||||
|  |                         column: x => x.sn_realm_id, | ||||||
|  |                         principalTable: "realms", | ||||||
|  |                         principalColumn: "id"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "sn_chat_member", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     chat_room_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), | ||||||
|  |                     role = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     notify = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     is_bot = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_sn_chat_member", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_sn_chat_member_sn_chat_room_chat_room_id", | ||||||
|  |                         column: x => x.chat_room_id, | ||||||
|  |                         principalTable: "sn_chat_room", | ||||||
|  |                         principalColumn: "id", | ||||||
|  |                         onDelete: ReferentialAction.Cascade); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_realms_slug", | ||||||
|  |                 table: "realms", | ||||||
|  |                 column: "slug", | ||||||
|  |                 unique: true); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_sn_chat_member_chat_room_id", | ||||||
|  |                 table: "sn_chat_member", | ||||||
|  |                 column: "chat_room_id"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_sn_chat_room_sn_realm_id", | ||||||
|  |                 table: "sn_chat_room", | ||||||
|  |                 column: "sn_realm_id"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "realm_members"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "sn_chat_member"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "sn_chat_room"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "realms"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2497
									
								
								DysonNetwork.Pass/Migrations/20251022164134_RemoveChatRoom.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2497
									
								
								DysonNetwork.Pass/Migrations/20251022164134_RemoveChatRoom.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | using System; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class RemoveChatRoom : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "sn_chat_member"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "sn_chat_room"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "sn_chat_room", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||||
|  |                     is_community = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     is_public = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), | ||||||
|  |                     picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||||
|  |                     picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), | ||||||
|  |                     realm_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||||
|  |                     sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||||
|  |                     type = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_sn_chat_room", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_sn_chat_room_realms_sn_realm_id", | ||||||
|  |                         column: x => x.sn_realm_id, | ||||||
|  |                         principalTable: "realms", | ||||||
|  |                         principalColumn: "id"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "sn_chat_member", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     chat_room_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     is_bot = table.Column<bool>(type: "boolean", nullable: false), | ||||||
|  |                     joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), | ||||||
|  |                     notify = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     role = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true), | ||||||
|  |                     timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_sn_chat_member", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_sn_chat_member_sn_chat_room_chat_room_id", | ||||||
|  |                         column: x => x.chat_room_id, | ||||||
|  |                         principalTable: "sn_chat_room", | ||||||
|  |                         principalColumn: "id", | ||||||
|  |                         onDelete: ReferentialAction.Cascade); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_sn_chat_member_chat_room_id", | ||||||
|  |                 table: "sn_chat_member", | ||||||
|  |                 column: "chat_room_id"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_sn_chat_room_sn_realm_id", | ||||||
|  |                 table: "sn_chat_room", | ||||||
|  |                 column: "sn_realm_id"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2612
									
								
								DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2612
									
								
								DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										78
									
								
								DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddLotteries : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "lotteries", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false), | ||||||
|  |                     region_two_number = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     multiplier = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     draw_status = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_lotteries", x => x.id); | ||||||
|  |                     table.ForeignKey( | ||||||
|  |                         name: "fk_lotteries_accounts_account_id", | ||||||
|  |                         column: x => x.account_id, | ||||||
|  |                         principalTable: "accounts", | ||||||
|  |                         principalColumn: "id", | ||||||
|  |                         onDelete: ReferentialAction.Cascade); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateTable( | ||||||
|  |                 name: "lottery_records", | ||||||
|  |                 columns: table => new | ||||||
|  |                 { | ||||||
|  |                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||||
|  |                     draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     winning_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false), | ||||||
|  |                     winning_region_two_number = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     total_tickets = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     total_prizes_awarded = table.Column<int>(type: "integer", nullable: false), | ||||||
|  |                     total_prize_amount = table.Column<long>(type: "bigint", nullable: false), | ||||||
|  |                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||||
|  |                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||||
|  |                 }, | ||||||
|  |                 constraints: table => | ||||||
|  |                 { | ||||||
|  |                     table.PrimaryKey("pk_lottery_records", x => x.id); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             migrationBuilder.CreateIndex( | ||||||
|  |                 name: "ix_lotteries_account_id", | ||||||
|  |                 table: "lotteries", | ||||||
|  |                 column: "account_id"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "lotteries"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropTable( | ||||||
|  |                 name: "lottery_records"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2620
									
								
								DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2620
									
								
								DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddDetailLotteriesStatus : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<List<int>>( | ||||||
|  |                 name: "matched_region_one_numbers", | ||||||
|  |                 table: "lotteries", | ||||||
|  |                 type: "jsonb", | ||||||
|  |                 nullable: true); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<int>( | ||||||
|  |                 name: "matched_region_two_number", | ||||||
|  |                 table: "lotteries", | ||||||
|  |                 type: "integer", | ||||||
|  |                 nullable: true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "matched_region_one_numbers", | ||||||
|  |                 table: "lotteries"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "matched_region_two_number", | ||||||
|  |                 table: "lotteries"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -22,7 +22,7 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|         { |         { | ||||||
| #pragma warning disable 612, 618 | #pragma warning disable 612, 618 | ||||||
|             modelBuilder |             modelBuilder | ||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |                 .HasAnnotation("ProductVersion", "9.0.9") | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
| @@ -1059,6 +1059,117 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                     b.ToTable("experience_records", (string)null); |                     b.ToTable("experience_records", (string)null); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DrawDate") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("draw_date"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("DrawStatus") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("draw_status"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<int>>("MatchedRegionOneNumbers") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("matched_region_one_numbers"); | ||||||
|  |  | ||||||
|  |                     b.Property<int?>("MatchedRegionTwoNumber") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("matched_region_two_number"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Multiplier") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("multiplier"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<int>>("RegionOneNumbers") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("region_one_numbers"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("RegionTwoNumber") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("region_two_number"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_lotteries"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("AccountId") | ||||||
|  |                         .HasDatabaseName("ix_lotteries_account_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("lotteries", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("DrawDate") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("draw_date"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("TotalPrizeAmount") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("total_prize_amount"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("TotalPrizesAwarded") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("total_prizes_awarded"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("TotalTickets") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("total_tickets"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<int>>("WinningRegionOneNumbers") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("winning_region_one_numbers"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("WinningRegionTwoNumber") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("winning_region_two_number"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_lottery_records"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("lottery_records", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
| @@ -1252,6 +1363,127 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                     b.ToTable("permission_nodes", (string)null); |                     b.ToTable("permission_nodes", (string)null); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<SnCloudFileReferenceObject>("Background") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("background"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("BackgroundId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("background_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Description") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsCommunity") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_community"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsPublic") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_public"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("picture"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("PictureId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("picture_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Slug") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("slug"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<SnVerificationMark>("Verification") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("verification"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_realms"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("Slug") | ||||||
|  |                         .IsUnique() | ||||||
|  |                         .HasDatabaseName("ix_realms_slug"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("realms", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("RealmId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("realm_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("JoinedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("joined_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("LeaveAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("leave_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("Role") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("role"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("RealmId", "AccountId") | ||||||
|  |                         .HasName("pk_realm_members"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("realm_members", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
| @@ -2113,6 +2345,18 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                     b.Navigation("Account"); |                     b.Navigation("Account"); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("AccountId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_lotteries_accounts_account_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Account"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => | ||||||
|                 { |                 { | ||||||
|                     b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") |                     b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") | ||||||
| @@ -2145,6 +2389,18 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                     b.Navigation("Group"); |                     b.Navigation("Group"); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm") | ||||||
|  |                         .WithMany("Members") | ||||||
|  |                         .HasForeignKey("RealmId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_realm_members_realms_realm_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Realm"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => | ||||||
|                 { |                 { | ||||||
|                     b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") |                     b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") | ||||||
| @@ -2336,6 +2592,11 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                     b.Navigation("Nodes"); |                     b.Navigation("Nodes"); | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => | ||||||
|  |                 { | ||||||
|  |                     b.Navigation("Members"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => |             modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => | ||||||
|                 { |                 { | ||||||
|                     b.Navigation("Pockets"); |                     b.Navigation("Pockets"); | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ builder.ConfigureAppKestrel(builder.Configuration); | |||||||
|  |  | ||||||
| // Add application services | // Add application services | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); |  | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddRingService(); | builder.Services.AddRingService(); | ||||||
| builder.Services.AddDriveService(); | builder.Services.AddDriveService(); | ||||||
| @@ -52,6 +51,6 @@ app.ConfigureAppMiddleware(builder.Configuration); | |||||||
| // Configure gRPC | // Configure gRPC | ||||||
| app.ConfigureGrpcServices(); | app.ConfigureGrpcServices(); | ||||||
|  |  | ||||||
| app.UseSwaggerManifest(); | app.UseSwaggerManifest("DysonNetwork.Pass"); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
|   | |||||||
| @@ -1,14 +1,17 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Pass.Account; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
|  | using Google.Protobuf.WellKnownTypes; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Google.Protobuf.WellKnownTypes; | using AccountService = DysonNetwork.Pass.Account.AccountService; | ||||||
| using DysonNetwork.Shared.Models; | using ActionLogService = DysonNetwork.Pass.Account.ActionLogService; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Realm; | namespace DysonNetwork.Pass.Realm; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/api/realms")] | [Route("/api/realms")] | ||||||
| @@ -17,9 +20,9 @@ public class RealmController( | |||||||
|     RealmService rs, |     RealmService rs, | ||||||
|     FileService.FileServiceClient files, |     FileService.FileServiceClient files, | ||||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, |     FileReferenceService.FileReferenceServiceClient fileRefs, | ||||||
|     ActionLogService.ActionLogServiceClient als, |     ActionLogService als, | ||||||
|     AccountService.AccountServiceClient accounts, |     RelationshipService rels, | ||||||
|     AccountClientHelper accountsHelper |     AccountEventService accountEvents | ||||||
| ) : Controller | ) : Controller | ||||||
| { | { | ||||||
|     [HttpGet("{slug}")] |     [HttpGet("{slug}")] | ||||||
| @@ -37,8 +40,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms() |     public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms() | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var members = await db.RealmMembers |         var members = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -54,8 +57,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<SnRealmMember>>> ListInvites() |     public async Task<ActionResult<List<SnRealmMember>>> ListInvites() | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var members = await db.RealmMembers |         var members = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -77,20 +80,18 @@ public class RealmController( | |||||||
|     public async Task<ActionResult<SnRealmMember>> InviteMember(string slug, |     public async Task<ActionResult<SnRealmMember>> InviteMember(string slug, | ||||||
|         [FromBody] RealmMemberRequest request) |         [FromBody] RealmMemberRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var relatedUser = |         var relatedUser = await db.Accounts.Where(a => a.Id == request.RelatedUserId).FirstOrDefaultAsync(); | ||||||
|             await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() }); |  | ||||||
|         if (relatedUser == null) return BadRequest("Related user was not found"); |         if (relatedUser == null) return BadRequest("Related user was not found"); | ||||||
| 
 | 
 | ||||||
|         var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest() |         var hasBlocked = await rels.HasRelationshipWithStatus( | ||||||
|         { |             currentUser.Id, | ||||||
|             AccountId = currentUser.Id, |             request.RelatedUserId, | ||||||
|             RelatedId = request.RelatedUserId.ToString(), |             RelationshipStatus.Blocked | ||||||
|             Status = -100 |         ); | ||||||
|         }); |         if (hasBlocked) | ||||||
|         if (hasBlocked?.Value ?? false) |  | ||||||
|             return StatusCode(403, "You cannot invite a user that blocked you."); |             return StatusCode(403, "You cannot invite a user that blocked you."); | ||||||
| 
 | 
 | ||||||
|         var realm = await db.Realms |         var realm = await db.Realms | ||||||
| @@ -102,7 +103,7 @@ public class RealmController( | |||||||
|             return StatusCode(403, "You cannot invite member has higher permission than yours."); |             return StatusCode(403, "You cannot invite member has higher permission than yours."); | ||||||
| 
 | 
 | ||||||
|         var existingMember = await db.RealmMembers |         var existingMember = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == Guid.Parse(relatedUser.Id)) |             .Where(m => m.AccountId == relatedUser.Id) | ||||||
|             .Where(m => m.RealmId == realm.Id) |             .Where(m => m.RealmId == realm.Id) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (existingMember != null) |         if (existingMember != null) | ||||||
| @@ -116,26 +117,23 @@ public class RealmController( | |||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
|             await rs.SendInviteNotify(existingMember); |             await rs.SendInviteNotify(existingMember); | ||||||
| 
 | 
 | ||||||
|             _ = als.CreateActionLogAsync(new CreateActionLogRequest |             als.CreateActionLogFromRequest( | ||||||
|             { |                 "realms.members.invite", | ||||||
|                 Action = "realms.members.invite", |                 new Dictionary<string, object>() | ||||||
|                 Meta = |  | ||||||
|                 { |                 { | ||||||
|                     { "realm_id", Value.ForString(realm.Id.ToString()) }, |                     { "realm_id", Value.ForString(realm.Id.ToString()) }, | ||||||
|                     { "account_id", Value.ForString(existingMember.AccountId.ToString()) }, |                     { "account_id", Value.ForString(existingMember.AccountId.ToString()) }, | ||||||
|                     { "role", Value.ForNumber(request.Role) } |                     { "role", Value.ForNumber(request.Role) } | ||||||
|                 }, |                 }, | ||||||
|                 AccountId = currentUser.Id, |                 Request | ||||||
|                 UserAgent = Request.Headers.UserAgent.ToString(), |             ); | ||||||
|                 IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             return Ok(existingMember); |             return Ok(existingMember); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var member = new SnRealmMember |         var member = new SnRealmMember | ||||||
|         { |         { | ||||||
|             AccountId = Guid.Parse(relatedUser.Id), |             AccountId = relatedUser.Id, | ||||||
|             RealmId = realm.Id, |             RealmId = realm.Id, | ||||||
|             Role = request.Role, |             Role = request.Role, | ||||||
|         }; |         }; | ||||||
| @@ -143,21 +141,18 @@ public class RealmController( | |||||||
|         db.RealmMembers.Add(member); |         db.RealmMembers.Add(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|          |          | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|         { |             "realms.members.invite", | ||||||
|             Action = "realms.members.invite", |             new Dictionary<string, object>() | ||||||
|             Meta = |  | ||||||
|             { |             { | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |                 { "realm_id", Value.ForString(realm.Id.ToString()) }, | ||||||
|                 { "account_id", Value.ForString(member.AccountId.ToString()) }, |                 { "account_id", Value.ForString(member.AccountId.ToString()) }, | ||||||
|                 { "role", Value.ForNumber(request.Role) } |                 { "role", Value.ForNumber(request.Role) } | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         member.AccountId = Guid.Parse(relatedUser.Id); |         member.AccountId = relatedUser.Id; | ||||||
|         member.Realm = realm; |         member.Realm = realm; | ||||||
|         await rs.SendInviteNotify(member); |         await rs.SendInviteNotify(member); | ||||||
| 
 | 
 | ||||||
| @@ -168,8 +163,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug) |     public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var member = await db.RealmMembers |         var member = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -182,18 +177,15 @@ public class RealmController( | |||||||
|         db.Update(member); |         db.Update(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.members.join", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.members.join", |                 { "realm_id", member.RealmId.ToString() }, | ||||||
|             Meta = |                 { "account_id", member.AccountId.ToString() } | ||||||
|             { |  | ||||||
|                 { "realm_id", Value.ForString(member.RealmId.ToString()) }, |  | ||||||
|                 { "account_id", Value.ForString(member.AccountId.ToString()) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
|     } |     } | ||||||
| @@ -202,8 +194,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> DeclineMemberInvite(string slug) |     public async Task<ActionResult> DeclineMemberInvite(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var member = await db.RealmMembers |         var member = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -215,19 +207,16 @@ public class RealmController( | |||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|         { |             "realms.members.decline_invite", | ||||||
|             Action = "realms.members.decline_invite", |             new Dictionary<string, object>() | ||||||
|             Meta = |  | ||||||
|             { |             { | ||||||
|                 { "realm_id", Value.ForString(member.RealmId.ToString()) }, |                 { "realm_id", Value.ForString(member.RealmId.ToString()) }, | ||||||
|                 { "account_id", Value.ForString(member.AccountId.ToString()) }, |                 { "account_id", Value.ForString(member.AccountId.ToString()) }, | ||||||
|                 { "decliner_id", Value.ForString(currentUser.Id) } |                 { "decliner_id", Value.ForString(currentUser.Id.ToString()) } | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
| @@ -248,8 +237,8 @@ public class RealmController( | |||||||
| 
 | 
 | ||||||
|         if (!realm.IsPublic) |         if (!realm.IsPublic) | ||||||
|         { |         { | ||||||
|             if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |             if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|             if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal)) |             if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal)) | ||||||
|                 return StatusCode(403, "You must be a member to view this realm's members."); |                 return StatusCode(403, "You must be a member to view this realm's members."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -263,7 +252,7 @@ public class RealmController( | |||||||
|                 .OrderBy(m => m.JoinedAt) |                 .OrderBy(m => m.JoinedAt) | ||||||
|                 .ToListAsync(); |                 .ToListAsync(); | ||||||
| 
 | 
 | ||||||
|             var memberStatuses = await accountsHelper.GetAccountStatusBatch( |             var memberStatuses = await accountEvents.GetStatuses( | ||||||
|                 members.Select(m => m.AccountId).ToList() |                 members.Select(m => m.AccountId).ToList() | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
| @@ -306,8 +295,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug) |     public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var member = await db.RealmMembers |         var member = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -323,8 +312,8 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> LeaveRealm(string slug) |     public async Task<ActionResult> LeaveRealm(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var member = await db.RealmMembers |         var member = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId) |             .Where(m => m.AccountId == accountId) | ||||||
| @@ -339,19 +328,16 @@ public class RealmController( | |||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.members.leave", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.members.leave", |                 { "realm_id", member.RealmId.ToString() }, | ||||||
|             Meta = |                 { "account_id", member.AccountId.ToString() }, | ||||||
|             { |                 { "leaver_id", currentUser.Id } | ||||||
|                 { "realm_id", Value.ForString(member.RealmId.ToString()) }, |  | ||||||
|                 { "account_id", Value.ForString(member.AccountId.ToString()) }, |  | ||||||
|                 { "leaver_id", Value.ForString(currentUser.Id) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
| @@ -371,7 +357,7 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request) |     public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
|         if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name."); |         if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name."); | ||||||
|         if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug."); |         if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug."); | ||||||
| 
 | 
 | ||||||
| @@ -383,7 +369,7 @@ public class RealmController( | |||||||
|             Name = request.Name!, |             Name = request.Name!, | ||||||
|             Slug = request.Slug!, |             Slug = request.Slug!, | ||||||
|             Description = request.Description!, |             Description = request.Description!, | ||||||
|             AccountId = Guid.Parse(currentUser.Id), |             AccountId = currentUser.Id, | ||||||
|             IsCommunity = request.IsCommunity ?? false, |             IsCommunity = request.IsCommunity ?? false, | ||||||
|             IsPublic = request.IsPublic ?? false, |             IsPublic = request.IsPublic ?? false, | ||||||
|             Members = new List<SnRealmMember> |             Members = new List<SnRealmMember> | ||||||
| @@ -391,7 +377,7 @@ public class RealmController( | |||||||
|                 new() |                 new() | ||||||
|                 { |                 { | ||||||
|                     Role = RealmMemberRole.Owner, |                     Role = RealmMemberRole.Owner, | ||||||
|                     AccountId = Guid.Parse(currentUser.Id), |                     AccountId = currentUser.Id, | ||||||
|                     JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) |                     JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -414,21 +400,18 @@ public class RealmController( | |||||||
|         db.Realms.Add(realm); |         db.Realms.Add(realm); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.create", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.create", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "name", realm.Name }, | ||||||
|             { |                 { "slug", realm.Slug }, | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |                 { "is_community", realm.IsCommunity }, | ||||||
|                 { "name", Value.ForString(realm.Name) }, |                 { "is_public", realm.IsPublic } | ||||||
|                 { "slug", Value.ForString(realm.Slug) }, |  | ||||||
|                 { "is_community", Value.ForBool(realm.IsCommunity) }, |  | ||||||
|                 { "is_public", Value.ForBool(realm.IsPublic) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         var realmResourceId = $"realm:{realm.Id}"; |         var realmResourceId = $"realm:{realm.Id}"; | ||||||
| 
 | 
 | ||||||
| @@ -459,14 +442,14 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request) |     public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var realm = await db.Realms |         var realm = await db.Realms | ||||||
|             .Where(r => r.Slug == slug) |             .Where(r => r.Slug == slug) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (realm is null) return NotFound(); |         if (realm is null) return NotFound(); | ||||||
| 
 | 
 | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = currentUser.Id; | ||||||
|         var member = await db.RealmMembers |         var member = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null) |             .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
| @@ -542,24 +525,21 @@ public class RealmController( | |||||||
|         db.Realms.Update(realm); |         db.Realms.Update(realm); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.update", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.update", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "name_updated", request.Name != null }, | ||||||
|             { |                 { "slug_updated", request.Slug != null }, | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |                 { "description_updated", request.Description != null }, | ||||||
|                 { "name_updated", Value.ForBool(request.Name != null) }, |                 { "picture_updated", request.PictureId != null }, | ||||||
|                 { "slug_updated", Value.ForBool(request.Slug != null) }, |                 { "background_updated", request.BackgroundId != null }, | ||||||
|                 { "description_updated", Value.ForBool(request.Description != null) }, |                 { "is_community_updated", request.IsCommunity != null }, | ||||||
|                 { "picture_updated", Value.ForBool(request.PictureId != null) }, |                 { "is_public_updated", request.IsPublic != null } | ||||||
|                 { "background_updated", Value.ForBool(request.BackgroundId != null) }, |  | ||||||
|                 { "is_community_updated", Value.ForBool(request.IsCommunity != null) }, |  | ||||||
|                 { "is_public_updated", Value.ForBool(request.IsPublic != null) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return Ok(realm); |         return Ok(realm); | ||||||
|     } |     } | ||||||
| @@ -568,7 +548,7 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug) |     public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var realm = await db.Realms |         var realm = await db.Realms | ||||||
|             .Where(r => r.Slug == slug) |             .Where(r => r.Slug == slug) | ||||||
| @@ -579,7 +559,7 @@ public class RealmController( | |||||||
|             return StatusCode(403, "Only community realms can be joined without invitation."); |             return StatusCode(403, "Only community realms can be joined without invitation."); | ||||||
| 
 | 
 | ||||||
|         var existingMember = await db.RealmMembers |         var existingMember = await db.RealmMembers | ||||||
|             .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id) |             .Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (existingMember is not null) |         if (existingMember is not null) | ||||||
|         { |         { | ||||||
| @@ -592,26 +572,23 @@ public class RealmController( | |||||||
|             db.Update(existingMember); |             db.Update(existingMember); | ||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|             _ = als.CreateActionLogAsync(new CreateActionLogRequest |             als.CreateActionLogFromRequest( | ||||||
|  |                 "realms.members.join", | ||||||
|  |                 new Dictionary<string, object>() | ||||||
|                 { |                 { | ||||||
|                 Action = "realms.members.join", |                     { "realm_id", existingMember.RealmId.ToString() }, | ||||||
|                 Meta = |                     { "account_id", currentUser.Id }, | ||||||
|             { |                     { "is_community", realm.IsCommunity } | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |  | ||||||
|                 { "account_id", Value.ForString(currentUser.Id) }, |  | ||||||
|                 { "is_community", Value.ForBool(realm.IsCommunity) } |  | ||||||
|                 }, |                 }, | ||||||
|                 AccountId = currentUser.Id, |                 Request | ||||||
|                 UserAgent = Request.Headers.UserAgent.ToString(), |             ); | ||||||
|                 IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             return Ok(existingMember); |             return Ok(existingMember); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var member = new SnRealmMember |         var member = new SnRealmMember | ||||||
|         { |         { | ||||||
|             AccountId = Guid.Parse(currentUser.Id), |             AccountId = currentUser.Id, | ||||||
|             RealmId = realm.Id, |             RealmId = realm.Id, | ||||||
|             Role = RealmMemberRole.Normal, |             Role = RealmMemberRole.Normal, | ||||||
|             JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) |             JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) | ||||||
| @@ -620,19 +597,16 @@ public class RealmController( | |||||||
|         db.RealmMembers.Add(member); |         db.RealmMembers.Add(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.members.join", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.members.join", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "account_id", currentUser.Id }, | ||||||
|             { |                 { "is_community", realm.IsCommunity } | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |  | ||||||
|                 { "account_id", Value.ForString(currentUser.Id) }, |  | ||||||
|                 { "is_community", Value.ForBool(realm.IsCommunity) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
|     } |     } | ||||||
| @@ -641,7 +615,7 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> RemoveMember(string slug, Guid memberId) |     public async Task<ActionResult> RemoveMember(string slug, Guid memberId) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var realm = await db.Realms |         var realm = await db.Realms | ||||||
|             .Where(r => r.Slug == slug) |             .Where(r => r.Slug == slug) | ||||||
| @@ -653,25 +627,22 @@ public class RealmController( | |||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (member is null) return NotFound(); |         if (member is null) return NotFound(); | ||||||
| 
 | 
 | ||||||
|         if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role)) |         if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role)) | ||||||
|             return StatusCode(403, "You do not have permission to remove members from this realm."); |             return StatusCode(403, "You do not have permission to remove members from this realm."); | ||||||
| 
 | 
 | ||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.members.kick", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.members.kick", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "account_id", memberId.ToString() }, | ||||||
|             { |                 { "kicker_id", currentUser.Id } | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |  | ||||||
|                 { "account_id", Value.ForString(memberId.ToString()) }, |  | ||||||
|                 { "kicker_id", Value.ForString(currentUser.Id) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
| @@ -681,7 +652,7 @@ public class RealmController( | |||||||
|     public async Task<ActionResult<SnRealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole) |     public async Task<ActionResult<SnRealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole) | ||||||
|     { |     { | ||||||
|         if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role."); |         if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role."); | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var realm = await db.Realms |         var realm = await db.Realms | ||||||
|             .Where(r => r.Slug == slug) |             .Where(r => r.Slug == slug) | ||||||
| @@ -693,7 +664,7 @@ public class RealmController( | |||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (member is null) return NotFound(); |         if (member is null) return NotFound(); | ||||||
| 
 | 
 | ||||||
|         if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role, |         if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role, | ||||||
|                 newRole)) |                 newRole)) | ||||||
|             return StatusCode(403, "You do not have permission to update member roles in this realm."); |             return StatusCode(403, "You do not have permission to update member roles in this realm."); | ||||||
| 
 | 
 | ||||||
| @@ -701,20 +672,17 @@ public class RealmController( | |||||||
|         db.RealmMembers.Update(member); |         db.RealmMembers.Update(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.members.role_update", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.members.role_update", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "account_id", memberId.ToString() }, | ||||||
|             { |                 { "new_role", newRole }, | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |                 { "updater_id", currentUser.Id } | ||||||
|                 { "account_id", Value.ForString(memberId.ToString()) }, |  | ||||||
|                 { "new_role", Value.ForNumber(newRole) }, |  | ||||||
|                 { "updater_id", Value.ForString(currentUser.Id) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
|     } |     } | ||||||
| @@ -723,7 +691,7 @@ public class RealmController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> Delete(string slug) |     public async Task<ActionResult> Delete(string slug) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var transaction = await db.Database.BeginTransactionAsync(); |         var transaction = await db.Database.BeginTransactionAsync(); | ||||||
| 
 | 
 | ||||||
| @@ -732,16 +700,11 @@ public class RealmController( | |||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (realm is null) return NotFound(); |         if (realm is null) return NotFound(); | ||||||
| 
 | 
 | ||||||
|         if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner)) |         if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Owner)) | ||||||
|             return StatusCode(403, "Only the owner can delete this realm."); |             return StatusCode(403, "Only the owner can delete this realm."); | ||||||
| 
 | 
 | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var chats = await db.ChatRooms |  | ||||||
|                 .Where(c => c.RealmId == realm.Id) |  | ||||||
|                 .Select(c => c.Id) |  | ||||||
|                 .ToListAsync(); |  | ||||||
| 
 |  | ||||||
|             db.Realms.Remove(realm); |             db.Realms.Remove(realm); | ||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
| @@ -749,15 +712,6 @@ public class RealmController( | |||||||
|             await db.RealmMembers |             await db.RealmMembers | ||||||
|                 .Where(m => m.RealmId == realm.Id) |                 .Where(m => m.RealmId == realm.Id) | ||||||
|                 .ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now)); |                 .ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now)); | ||||||
|             await db.ChatRooms |  | ||||||
|                 .Where(c => c.RealmId == realm.Id) |  | ||||||
|                 .ExecuteUpdateAsync(c => c.SetProperty(c => c.DeletedAt, now)); |  | ||||||
|             await db.ChatMessages |  | ||||||
|                 .Where(m => chats.Contains(m.ChatRoomId)) |  | ||||||
|                 .ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now)); |  | ||||||
|             await db.ChatMembers |  | ||||||
|                 .Where(m => chats.Contains(m.ChatRoomId)) |  | ||||||
|                 .ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now)); |  | ||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
|             await transaction.CommitAsync(); |             await transaction.CommitAsync(); | ||||||
|         } |         } | ||||||
| @@ -767,19 +721,16 @@ public class RealmController( | |||||||
|             throw; |             throw; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest |         als.CreateActionLogFromRequest( | ||||||
|  |             "realms.delete", | ||||||
|  |             new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|             Action = "realms.delete", |                 { "realm_id", realm.Id.ToString() }, | ||||||
|             Meta = |                 { "realm_name", realm.Name }, | ||||||
|             { |                 { "realm_slug", realm.Slug } | ||||||
|                 { "realm_id", Value.ForString(realm.Id.ToString()) }, |  | ||||||
|                 { "realm_name", Value.ForString(realm.Name) }, |  | ||||||
|                 { "realm_slug", Value.ForString(realm.Slug) } |  | ||||||
|             }, |             }, | ||||||
|             AccountId = currentUser.Id, |             Request | ||||||
|             UserAgent = Request.Headers.UserAgent.ToString(), |         ); | ||||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         // Delete all file references for this realm |         // Delete all file references for this realm | ||||||
|         var realmResourceId = $"realm:{realm.Id}"; |         var realmResourceId = $"realm:{realm.Id}"; | ||||||
| @@ -1,20 +1,18 @@ | |||||||
|  | using DysonNetwork.Pass.Localization; | ||||||
| using DysonNetwork.Shared; | using DysonNetwork.Shared; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
| using DysonNetwork.Sphere.Localization; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Realm; | namespace DysonNetwork.Pass.Realm; | ||||||
| 
 | 
 | ||||||
| public class RealmService( | public class RealmService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     RingService.RingServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     AccountService.AccountServiceClient accounts, |  | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     AccountClientHelper accountsHelper, |  | ||||||
|     ICacheService cache |     ICacheService cache | ||||||
| ) | ) | ||||||
| { | { | ||||||
| @@ -42,13 +40,18 @@ public class RealmService( | |||||||
|      |      | ||||||
|     public async Task SendInviteNotify(SnRealmMember member) |     public async Task SendInviteNotify(SnRealmMember member) | ||||||
|     { |     { | ||||||
|         var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() }); |         var account = await db.Accounts | ||||||
|         CultureService.SetCultureInfo(account); |             .Include(a => a.Profile) | ||||||
|  |             .FirstOrDefaultAsync(a => a.Id == member.AccountId); | ||||||
|  |          | ||||||
|  |         if (account == null) throw new InvalidOperationException("Account not found"); | ||||||
|  |          | ||||||
|  |         CultureService.SetCultureInfo(account.Language); | ||||||
| 
 | 
 | ||||||
|         await pusher.SendPushNotificationToUserAsync( |         await pusher.SendPushNotificationToUserAsync( | ||||||
|             new SendPushNotificationToUserRequest |             new SendPushNotificationToUserRequest | ||||||
|             { |             { | ||||||
|                 UserId = account.Id, |                 UserId = account.Id.ToString(), | ||||||
|                 Notification = new PushNotification |                 Notification = new PushNotification | ||||||
|                 { |                 { | ||||||
|                     Topic = "invites.realms", |                     Topic = "invites.realms", | ||||||
| @@ -75,20 +78,26 @@ public class RealmService( | |||||||
| 
 | 
 | ||||||
|     public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member) |     public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member) | ||||||
|     { |     { | ||||||
|         var account = await accountsHelper.GetAccount(member.AccountId); |         var account = await db.Accounts | ||||||
|         member.Account = SnAccount.FromProtoValue(account); |             .Include(a => a.Profile) | ||||||
|  |             .FirstOrDefaultAsync(a => a.Id == member.AccountId); | ||||||
|  |         if (account != null) | ||||||
|  |             member.Account = account; | ||||||
|         return member; |         return member; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members) |     public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members) | ||||||
|     { |     { | ||||||
|         var accountIds = members.Select(m => m.AccountId).ToList(); |         var accountIds = members.Select(m => m.AccountId).ToList(); | ||||||
|         var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a); |         var accountsDict = await db.Accounts | ||||||
|  |             .Include(a => a.Profile) | ||||||
|  |             .Where(a => accountIds.Contains(a.Id)) | ||||||
|  |             .ToDictionaryAsync(a => a.Id, a => a); | ||||||
| 
 | 
 | ||||||
|         return members.Select(m => |         return members.Select(m => | ||||||
|         { |         { | ||||||
|             if (accounts.TryGetValue(m.AccountId, out var account)) |             if (accountsDict.TryGetValue(m.AccountId, out var account)) | ||||||
|                 m.Account = SnAccount.FromProtoValue(account); |                 m.Account = account; | ||||||
|             return m; |             return m; | ||||||
|         }).ToList(); |         }).ToList(); | ||||||
|     } |     } | ||||||
							
								
								
									
										170
									
								
								DysonNetwork.Pass/Realm/RealmServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								DysonNetwork.Pass/Realm/RealmServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Google.Protobuf.WellKnownTypes; | ||||||
|  | using Grpc.Core; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using DysonNetwork.Pass.Localization; | ||||||
|  | using DysonNetwork.Shared; | ||||||
|  | using DysonNetwork.Shared.Cache; | ||||||
|  | using Microsoft.Extensions.Localization; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Realm; | ||||||
|  |  | ||||||
|  | public class RealmServiceGrpc( | ||||||
|  |     AppDatabase db, | ||||||
|  |     RingService.RingServiceClient pusher, | ||||||
|  |     IStringLocalizer<NotificationResource> localizer, | ||||||
|  |     ICacheService cache | ||||||
|  | ) | ||||||
|  |     : Shared.Proto.RealmService.RealmServiceBase | ||||||
|  | { | ||||||
|  |     private const string CacheKeyPrefix = "account:realms:"; | ||||||
|  |  | ||||||
|  |     public override async Task<Shared.Proto.Realm> GetRealm(GetRealmRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var realm = request.QueryCase switch | ||||||
|  |         { | ||||||
|  |             GetRealmRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id) => await db.Realms.FindAsync( | ||||||
|  |                 Guid.Parse(request.Id)), | ||||||
|  |             GetRealmRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug) => await db.Realms | ||||||
|  |                 .FirstOrDefaultAsync(r => r.Slug == request.Slug), | ||||||
|  |             _ => throw new RpcException(new Status(StatusCode.InvalidArgument, "Must provide either id or slug")) | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return realm == null | ||||||
|  |             ? throw new RpcException(new Status(StatusCode.NotFound, "Realm not found")) | ||||||
|  |             : realm.ToProtoValue(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<GetRealmBatchResponse> GetRealmBatch(GetRealmBatchRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var ids = request.Ids.Select(Guid.Parse).ToList(); | ||||||
|  |         var realms = await db.Realms.Where(r => ids.Contains(r.Id)).ToListAsync(); | ||||||
|  |         var response = new GetRealmBatchResponse(); | ||||||
|  |         response.Realms.AddRange(realms.Select(r => r.ToProtoValue())); | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<GetUserRealmsResponse> GetUserRealms(GetUserRealmsRequest request, | ||||||
|  |         ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var accountId = Guid.Parse(request.AccountId); | ||||||
|  |         var cacheKey = $"{CacheKeyPrefix}{accountId}"; | ||||||
|  |         var (found, cachedRealms) = await cache.GetAsyncWithStatus<List<Guid>>(cacheKey); | ||||||
|  |         if (found && cachedRealms != null) | ||||||
|  |             return new GetUserRealmsResponse { RealmIds = { cachedRealms.Select(g => g.ToString()) } }; | ||||||
|  |  | ||||||
|  |         var realms = await db.RealmMembers | ||||||
|  |             .Include(m => m.Realm) | ||||||
|  |             .Where(m => m.AccountId == accountId) | ||||||
|  |             .Where(m => m.JoinedAt != null && m.LeaveAt == null) | ||||||
|  |             .Where(m => m.Realm != null) | ||||||
|  |             .Select(m => m.Realm!.Id) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         // Cache the result for 5 minutes | ||||||
|  |         await cache.SetAsync(cacheKey, realms, TimeSpan.FromMinutes(5)); | ||||||
|  |  | ||||||
|  |         return new GetUserRealmsResponse { RealmIds = { realms.Select(g => g.ToString()) } }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<GetPublicRealmsResponse> GetPublicRealms(Empty request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var realms = await db.Realms.Where(r => r.IsPublic).ToListAsync(); | ||||||
|  |         var response = new GetPublicRealmsResponse(); | ||||||
|  |         response.Realms.AddRange(realms.Select(r => r.ToProtoValue())); | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<GetPublicRealmsResponse> SearchRealms(SearchRealmsRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var realms = await db.Realms | ||||||
|  |             .Where(r => r.IsPublic) | ||||||
|  |             .Where(r => EF.Functions.Like(r.Slug, $"{request.Query}%") || EF.Functions.Like(r.Name, $"{request.Query}%")) | ||||||
|  |             .Take(request.Limit) | ||||||
|  |             .ToListAsync(); | ||||||
|  |         var response = new GetPublicRealmsResponse(); | ||||||
|  |         response.Realms.AddRange(realms.Select(r => r.ToProtoValue())); | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<Empty> SendInviteNotify(SendInviteNotifyRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var member = request.Member; | ||||||
|  |         var account = await db.Accounts | ||||||
|  |             .AsNoTracking() | ||||||
|  |             .Include(a => a.Profile) | ||||||
|  |             .FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId)); | ||||||
|  |          | ||||||
|  |         if (account == null) throw new RpcException(new Status(StatusCode.NotFound, "Account not found")); | ||||||
|  |          | ||||||
|  |         CultureService.SetCultureInfo(account.Language); | ||||||
|  |  | ||||||
|  |         await pusher.SendPushNotificationToUserAsync( | ||||||
|  |             new SendPushNotificationToUserRequest | ||||||
|  |             { | ||||||
|  |                 UserId = account.Id.ToString(), | ||||||
|  |                 Notification = new PushNotification | ||||||
|  |                 { | ||||||
|  |                     Topic = "invites.realms", | ||||||
|  |                     Title = localizer["RealmInviteTitle"], | ||||||
|  |                     Body = localizer["RealmInviteBody", member.Realm?.Name ?? "Unknown Realm"], | ||||||
|  |                     ActionUri = "/realms", | ||||||
|  |                     IsSavable = true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return new Empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<BoolValue> IsMemberWithRole(IsMemberWithRoleRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         if (request.RequiredRoles.Count == 0) | ||||||
|  |             return new BoolValue { Value = false }; | ||||||
|  |  | ||||||
|  |         var maxRequiredRole = request.RequiredRoles.Max(); | ||||||
|  |         var member = await db.RealmMembers | ||||||
|  |             .Where(m => m.RealmId == Guid.Parse(request.RealmId) && m.AccountId == Guid.Parse(request.AccountId) && | ||||||
|  |                         m.JoinedAt != null && m.LeaveAt == null) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |         return new BoolValue { Value = member?.Role >= maxRequiredRole }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<RealmMember> LoadMemberAccount(LoadMemberAccountRequest request, | ||||||
|  |         ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var member = request.Member; | ||||||
|  |         var account = await db.Accounts | ||||||
|  |             .AsNoTracking() | ||||||
|  |             .Include(a => a.Profile) | ||||||
|  |             .FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId)); | ||||||
|  |          | ||||||
|  |         var response = new RealmMember(member) { Account = account?.ToProtoValue() }; | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<LoadMemberAccountsResponse> LoadMemberAccounts(LoadMemberAccountsRequest request, | ||||||
|  |         ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var accountIds = request.Members.Select(m => Guid.Parse(m.AccountId)).ToList(); | ||||||
|  |         var accounts = await db.Accounts | ||||||
|  |             .AsNoTracking() | ||||||
|  |             .Include(a => a.Profile) | ||||||
|  |             .Where(a => accountIds.Contains(a.Id)) | ||||||
|  |             .ToDictionaryAsync(a => a.Id, a => a.ToProtoValue()); | ||||||
|  |  | ||||||
|  |         var response = new LoadMemberAccountsResponse(); | ||||||
|  |         foreach (var member in request.Members) | ||||||
|  |         { | ||||||
|  |             var updatedMember = new RealmMember(member); | ||||||
|  |             if (accounts.TryGetValue(Guid.Parse(member.AccountId), out var account)) | ||||||
|  |             { | ||||||
|  |                 updatedMember.Account = account; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             response.Members.Add(updatedMember); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return response; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -212,5 +212,17 @@ namespace DysonNetwork.Sphere.Resources.Localization { | |||||||
|                 return ResourceManager.GetString("TransactionNewBodyMinus", resourceCulture); |                 return ResourceManager.GetString("TransactionNewBodyMinus", resourceCulture); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |          | ||||||
|  |         internal static string GiftClaimedTitle { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("GiftClaimedTitle", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string GiftClaimedBody { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("GiftClaimedBody", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -107,4 +107,10 @@ | |||||||
|     <data name="TransactionNewBodyMinus" xml:space="preserve"> |     <data name="TransactionNewBodyMinus" xml:space="preserve"> | ||||||
|         <value>{0} {1} removed from your wallet</value> |         <value>{0} {1} removed from your wallet</value> | ||||||
|     </data> |     </data> | ||||||
|  |     <data name="GiftClaimedTitle" xml:space="preserve"> | ||||||
|  |         <value>Someone claimed your gift</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="GiftClaimedBody" xml:space="preserve"> | ||||||
|  |         <value>Your gift {0} has been claimed by {1}</value> | ||||||
|  |     </data> | ||||||
| </root> | </root> | ||||||
| @@ -99,4 +99,10 @@ | |||||||
|     <data name="TransactionNewBodyMinus" xml:space="preserve"> |     <data name="TransactionNewBodyMinus" xml:space="preserve"> | ||||||
|         <value>{0} {1} 从您的钱包移除</value> |         <value>{0} {1} 从您的钱包移除</value> | ||||||
|     </data> |     </data> | ||||||
|  |     <data name="GiftClaimedTitle" xml:space="preserve"> | ||||||
|  |         <value>有人领取了你的礼物</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="GiftClaimedBody" xml:space="preserve"> | ||||||
|  |         <value>你的礼物 {0} 已被 {1} 领取</value> | ||||||
|  |     </data> | ||||||
| </root> | </root> | ||||||
| @@ -3,9 +3,9 @@ using DysonNetwork.Pass.Auth; | |||||||
| using DysonNetwork.Pass.Credit; | using DysonNetwork.Pass.Credit; | ||||||
| using DysonNetwork.Pass.Leveling; | using DysonNetwork.Pass.Leveling; | ||||||
| using DysonNetwork.Pass.Permission; | using DysonNetwork.Pass.Permission; | ||||||
|  | using DysonNetwork.Pass.Realm; | ||||||
| using DysonNetwork.Pass.Wallet; | using DysonNetwork.Pass.Wallet; | ||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using Prometheus; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Startup; | namespace DysonNetwork.Pass.Startup; | ||||||
|  |  | ||||||
| @@ -13,7 +13,6 @@ public static class ApplicationConfiguration | |||||||
| { | { | ||||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) |     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) | ||||||
|     { |     { | ||||||
|         app.MapMetrics(); |  | ||||||
|         app.MapOpenApi(); |         app.MapOpenApi(); | ||||||
|  |  | ||||||
|         app.UseRequestLocalization(); |         app.UseRequestLocalization(); | ||||||
| @@ -21,7 +20,6 @@ public static class ApplicationConfiguration | |||||||
|         app.ConfigureForwardedHeaders(configuration); |         app.ConfigureForwardedHeaders(configuration); | ||||||
|  |  | ||||||
|         app.UseWebSockets(); |         app.UseWebSockets(); | ||||||
|         app.UseRateLimiter(); |  | ||||||
|         app.UseAuthentication(); |         app.UseAuthentication(); | ||||||
|         app.UseAuthorization(); |         app.UseAuthorization(); | ||||||
|         app.UseMiddleware<PermissionMiddleware>(); |         app.UseMiddleware<PermissionMiddleware>(); | ||||||
| @@ -42,6 +40,8 @@ public static class ApplicationConfiguration | |||||||
|         app.MapGrpcService<BotAccountReceiverGrpc>(); |         app.MapGrpcService<BotAccountReceiverGrpc>(); | ||||||
|         app.MapGrpcService<WalletServiceGrpc>(); |         app.MapGrpcService<WalletServiceGrpc>(); | ||||||
|         app.MapGrpcService<PaymentServiceGrpc>(); |         app.MapGrpcService<PaymentServiceGrpc>(); | ||||||
|  |         app.MapGrpcService<RealmServiceGrpc>(); | ||||||
|  |         app.MapGrpcReflectionService(); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -104,9 +104,33 @@ public class BroadcastEventHandler( | |||||||
|                     logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); |                     logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); | ||||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); |                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||||
|                 } |                 } | ||||||
|  |                 else if (evt.ProductIdentifier == "lottery") | ||||||
|  |                 { | ||||||
|  |                     logger.LogInformation("Handling lottery order: {OrderId}", evt.OrderId); | ||||||
|  |  | ||||||
|  |                     await using var scope = serviceProvider.CreateAsyncScope(); | ||||||
|  |                     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||||
|  |                     var lotteries = scope.ServiceProvider.GetRequiredService<Lotteries.LotteryService>(); | ||||||
|  |  | ||||||
|  |                     var order = await db.PaymentOrders.FindAsync( | ||||||
|  |                         [evt.OrderId], | ||||||
|  |                         cancellationToken: stoppingToken | ||||||
|  |                     ); | ||||||
|  |                     if (order == null) | ||||||
|  |                     { | ||||||
|  |                         logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); | ||||||
|  |                         await msg.NakAsync(cancellationToken: stoppingToken); | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     await lotteries.HandleLotteryOrder(order); | ||||||
|  |  | ||||||
|  |                     logger.LogInformation("Lottery ticket for order {OrderId} created successfully.", evt.OrderId); | ||||||
|  |                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||||
|  |                 } | ||||||
|                 else |                 else | ||||||
|                 { |                 { | ||||||
|                     // Not a subscription or gift order, skip |                     // Not a subscription, gift, or lottery order, skip | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -66,6 +66,13 @@ public static class ScheduledJobsConfiguration | |||||||
|                     .WithIntervalInHours(1) |                     .WithIntervalInHours(1) | ||||||
|                     .RepeatForever()) |                     .RepeatForever()) | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|  |             var lotteryDrawJob = new JobKey("LotteryDraw"); | ||||||
|  |             q.AddJob<Lotteries.LotteryDrawJob>(opts => opts.WithIdentity(lotteryDrawJob)); | ||||||
|  |             q.AddTrigger(opts => opts | ||||||
|  |                 .ForJob(lotteryDrawJob) | ||||||
|  |                 .WithIdentity("LotteryDrawTrigger") | ||||||
|  |                 .WithCronSchedule("0 0 0 * * ?")); | ||||||
|         }); |         }); | ||||||
|         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); |         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ using DysonNetwork.Pass.Credit; | |||||||
| using DysonNetwork.Pass.Handlers; | using DysonNetwork.Pass.Handlers; | ||||||
| using DysonNetwork.Pass.Leveling; | using DysonNetwork.Pass.Leveling; | ||||||
| using DysonNetwork.Pass.Mailer; | using DysonNetwork.Pass.Mailer; | ||||||
|  | using DysonNetwork.Pass.Realm; | ||||||
| using DysonNetwork.Pass.Safety; | using DysonNetwork.Pass.Safety; | ||||||
| using DysonNetwork.Pass.Wallet.PaymentHandlers; | using DysonNetwork.Pass.Wallet.PaymentHandlers; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| @@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions | |||||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|         }); |         }); | ||||||
|  |         services.AddGrpcReflection(); | ||||||
|  |  | ||||||
|         services.AddRingService(); |         services.AddRingService(); | ||||||
|  |  | ||||||
| @@ -91,19 +93,6 @@ public static class ServiceCollectionExtensions | |||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => |  | ||||||
|         { |  | ||||||
|             opts.Window = TimeSpan.FromMinutes(1); |  | ||||||
|             opts.PermitLimit = 120; |  | ||||||
|             opts.QueueLimit = 2; |  | ||||||
|             opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; |  | ||||||
|         })); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) |     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||||
|     { |     { | ||||||
|         services.AddAuthorization(); |         services.AddAuthorization(); | ||||||
| @@ -152,6 +141,8 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<SafetyService>(); |         services.AddScoped<SafetyService>(); | ||||||
|         services.AddScoped<SocialCreditService>(); |         services.AddScoped<SocialCreditService>(); | ||||||
|         services.AddScoped<ExperienceService>(); |         services.AddScoped<ExperienceService>(); | ||||||
|  |         services.AddScoped<RealmService>(); | ||||||
|  |         services.AddScoped<Lotteries.LotteryService>(); | ||||||
|          |          | ||||||
|         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); |         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); | ||||||
|         services.AddScoped<OidcProviderService>(); |         services.AddScoped<OidcProviderService>(); | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ using Quartz; | |||||||
| namespace DysonNetwork.Pass.Wallet; | namespace DysonNetwork.Pass.Wallet; | ||||||
|  |  | ||||||
| public class FundExpirationJob( | public class FundExpirationJob( | ||||||
|     AppDatabase db, |  | ||||||
|     PaymentService paymentService, |     PaymentService paymentService, | ||||||
|     ILogger<FundExpirationJob> logger |     ILogger<FundExpirationJob> logger | ||||||
| ) : IJob | ) : IJob | ||||||
|   | |||||||
| @@ -197,7 +197,8 @@ public class SubscriptionGiftController( | |||||||
|  |  | ||||||
|         if (currentUser.Profile.Level < MinimumAccountLevel) |         if (currentUser.Profile.Level < MinimumAccountLevel) | ||||||
|         { |         { | ||||||
|             return StatusCode(403, "Account level must be at least 60 to purchase a gift."); |             if (currentUser.PerkSubscription is null) | ||||||
|  |                 return StatusCode(403, "Account level must be at least 60 or a member of the Stellar Program to purchase a gift."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Duration? giftDuration = null; |         Duration? giftDuration = null; | ||||||
|   | |||||||
| @@ -250,6 +250,14 @@ public class SubscriptionService( | |||||||
|             : null; |             : null; | ||||||
|         if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found."); |         if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found."); | ||||||
|  |  | ||||||
|  |         if (subscriptionInfo.RequiredLevel > 0) | ||||||
|  |         { | ||||||
|  |             var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == subscription.AccountId); | ||||||
|  |             if (profile is null) throw new InvalidOperationException("Account must have a profile"); | ||||||
|  |             if (profile.Level < subscriptionInfo.RequiredLevel) | ||||||
|  |                 throw new InvalidOperationException("Account level must be at least 60 to purchase a gift."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return await payment.CreateOrderAsync( |         return await payment.CreateOrderAsync( | ||||||
|             null, |             null, | ||||||
|             subscriptionInfo.Currency, |             subscriptionInfo.Currency, | ||||||
| @@ -684,6 +692,9 @@ public class SubscriptionService( | |||||||
|         if (now > gift.ExpiresAt) |         if (now > gift.ExpiresAt) | ||||||
|             throw new InvalidOperationException("Gift has expired."); |             throw new InvalidOperationException("Gift has expired."); | ||||||
|  |  | ||||||
|  |         if (gift.GifterId == redeemer.Id) | ||||||
|  |             throw new InvalidOperationException("You cannot redeem your own gift."); | ||||||
|  |  | ||||||
|         // Validate redeemer permissions |         // Validate redeemer permissions | ||||||
|         if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id) |         if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id) | ||||||
|             throw new InvalidOperationException("This gift is not intended for you."); |             throw new InvalidOperationException("This gift is not intended for you."); | ||||||
| @@ -972,7 +983,7 @@ public class SubscriptionService( | |||||||
|         { |         { | ||||||
|             Topic = "gifts.claimed", |             Topic = "gifts.claimed", | ||||||
|             Title = localizer["GiftClaimedTitle"], |             Title = localizer["GiftClaimedTitle"], | ||||||
|             Body = localizer["GiftClaimedBody", humanReadableName, redeemer.Name ?? redeemer.Id.ToString()], |             Body = localizer["GiftClaimedBody", humanReadableName, redeemer.Name], | ||||||
|             Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object> |             Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object> | ||||||
|             { |             { | ||||||
|                 ["gift_id"] = gift.Id.ToString(), |                 ["gift_id"] = gift.Id.ToString(), | ||||||
|   | |||||||
| @@ -71,12 +71,5 @@ | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "KnownProxies": ["127.0.0.1", "::1"], |   "KnownProxies": ["127.0.0.1", "::1"] | ||||||
|   "Service": { |  | ||||||
|     "Name": "DysonNetwork.Pass", |  | ||||||
|     "Url": "https://localhost:7058" |  | ||||||
|   }, |  | ||||||
|   "Etcd": { |  | ||||||
|     "Insecure": true |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,42 +17,52 @@ public class WebSocketController( | |||||||
|     INatsConnection nats |     INatsConnection nats | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|  |     private static readonly List<string> AllowedDeviceAlternative = ["watch"]; | ||||||
|  |  | ||||||
|     [Route("/ws")] |     [Route("/ws")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [SwaggerIgnore] |     [SwaggerIgnore] | ||||||
|     public async Task TheGateway() |     public async Task<ActionResult> TheGateway([FromQuery] string? deviceAlt) | ||||||
|     { |     { | ||||||
|  |         if (string.IsNullOrWhiteSpace(deviceAlt)) | ||||||
|  |             deviceAlt = null; | ||||||
|  |         if (deviceAlt is not null && !AllowedDeviceAlternative.Contains(deviceAlt)) | ||||||
|  |             return BadRequest("Unsupported device alternative: " + deviceAlt); | ||||||
|  |  | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); |         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); | ||||||
|         if (currentUserValue is not Account currentUser || |         if ( | ||||||
|             currentSessionValue is not AuthSession currentSession) |             currentUserValue is not Account currentUser | ||||||
|  |             || currentSessionValue is not AuthSession currentSession | ||||||
|  |         ) | ||||||
|         { |         { | ||||||
|             HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; |             return Unauthorized(); | ||||||
|             return; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id!); |         var accountId = Guid.Parse(currentUser.Id!); | ||||||
|         var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString(); |         var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString(); | ||||||
|  |  | ||||||
|         if (string.IsNullOrEmpty(deviceId)) |         if (string.IsNullOrEmpty(deviceId)) | ||||||
|         { |             return BadRequest("Unable to get device ID from session."); | ||||||
|             HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; |         if (deviceAlt is not null) | ||||||
|             return; |             deviceId = $"{deviceId}+{deviceAlt}"; | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext |         var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync( | ||||||
|             { KeepAliveInterval = TimeSpan.FromSeconds(60) }); |             new WebSocketAcceptContext { KeepAliveInterval = TimeSpan.FromSeconds(60) } | ||||||
|  |         ); | ||||||
|         var cts = new CancellationTokenSource(); |         var cts = new CancellationTokenSource(); | ||||||
|         var connectionKey = (accountId, deviceId); |         var connectionKey = (accountId, deviceId); | ||||||
|  |  | ||||||
|         if (!ws.TryAdd(connectionKey, webSocket, cts)) |         if (!ws.TryAdd(connectionKey, webSocket, cts)) | ||||||
|         { |         { | ||||||
|             await webSocket.SendAsync( |             await webSocket.SendAsync( | ||||||
|                 new ArraySegment<byte>(new WebSocketPacket |                 new ArraySegment<byte>( | ||||||
|  |                     new WebSocketPacket | ||||||
|                     { |                     { | ||||||
|                         Type = "error.dupe", |                         Type = "error.dupe", | ||||||
|                     ErrorMessage = "Too many connections from the same device and account." |                         ErrorMessage = "Too many connections from the same device and account.", | ||||||
|                 }.ToBytes()), |                     }.ToBytes() | ||||||
|  |                 ), | ||||||
|                 WebSocketMessageType.Binary, |                 WebSocketMessageType.Binary, | ||||||
|                 true, |                 true, | ||||||
|                 CancellationToken.None |                 CancellationToken.None | ||||||
| @@ -62,21 +72,26 @@ public class WebSocketController( | |||||||
|                 "Too many connections from the same device and account.", |                 "Too many connections from the same device and account.", | ||||||
|                 CancellationToken.None |                 CancellationToken.None | ||||||
|             ); |             ); | ||||||
|             return; |             return new EmptyResult(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         logger.LogDebug( |         logger.LogDebug( | ||||||
|             $"Connection established with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}"); |             $"Connection established with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}" | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         // Broadcast WebSocket connected event |         // Broadcast WebSocket connected event | ||||||
|         await nats.PublishAsync( |         await nats.PublishAsync( | ||||||
|             WebSocketConnectedEvent.Type, |             WebSocketConnectedEvent.Type, | ||||||
|             GrpcTypeHelper.ConvertObjectToByteString(new WebSocketConnectedEvent |             GrpcTypeHelper | ||||||
|  |                 .ConvertObjectToByteString( | ||||||
|  |                     new WebSocketConnectedEvent | ||||||
|                     { |                     { | ||||||
|                         AccountId = accountId, |                         AccountId = accountId, | ||||||
|                         DeviceId = deviceId, |                         DeviceId = deviceId, | ||||||
|                 IsOffline = false |                         IsOffline = false, | ||||||
|             }).ToByteArray(), |                     } | ||||||
|  |                 ) | ||||||
|  |                 .ToByteArray(), | ||||||
|             cancellationToken: cts.Token |             cancellationToken: cts.Token | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
| @@ -84,7 +99,11 @@ public class WebSocketController( | |||||||
|         { |         { | ||||||
|             await _ConnectionEventLoop(deviceId, currentUser, webSocket, cts.Token); |             await _ConnectionEventLoop(deviceId, currentUser, webSocket, cts.Token); | ||||||
|         } |         } | ||||||
|         catch (WebSocketException ex) when (ex.Message.Contains("The remote party closed the WebSocket connection without completing the close handshake")) |         catch (WebSocketException ex) | ||||||
|  |             when (ex.Message.Contains( | ||||||
|  |                     "The remote party closed the WebSocket connection without completing the close handshake" | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|         { |         { | ||||||
|             logger.LogDebug( |             logger.LogDebug( | ||||||
|                 "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} - client closed connection without proper handshake", |                 "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} - client closed connection without proper handshake", | ||||||
| @@ -95,7 +114,8 @@ public class WebSocketController( | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             logger.LogError(ex, |             logger.LogError( | ||||||
|  |                 ex, | ||||||
|                 "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly", |                 "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly", | ||||||
|                 currentUser.Name, |                 currentUser.Name, | ||||||
|                 currentUser.Id, |                 currentUser.Id, | ||||||
| @@ -109,12 +129,16 @@ public class WebSocketController( | |||||||
|             // Broadcast WebSocket disconnected event |             // Broadcast WebSocket disconnected event | ||||||
|             await nats.PublishAsync( |             await nats.PublishAsync( | ||||||
|                 WebSocketDisconnectedEvent.Type, |                 WebSocketDisconnectedEvent.Type, | ||||||
|                 GrpcTypeHelper.ConvertObjectToByteString(new WebSocketDisconnectedEvent |                 GrpcTypeHelper | ||||||
|  |                     .ConvertObjectToByteString( | ||||||
|  |                         new WebSocketDisconnectedEvent | ||||||
|                         { |                         { | ||||||
|                             AccountId = accountId, |                             AccountId = accountId, | ||||||
|                             DeviceId = deviceId, |                             DeviceId = deviceId, | ||||||
|                     IsOffline = !WebSocketService.GetAccountIsConnected(accountId) |                             IsOffline = !WebSocketService.GetAccountIsConnected(accountId), | ||||||
|                 }).ToByteArray(), |                         } | ||||||
|  |                     ) | ||||||
|  |                     .ToByteArray(), | ||||||
|                 cancellationToken: cts.Token |                 cancellationToken: cts.Token | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
| @@ -122,6 +146,8 @@ public class WebSocketController( | |||||||
|                 $"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}" |                 $"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}" | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         return new EmptyResult(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task _ConnectionEventLoop( |     private async Task _ConnectionEventLoop( | ||||||
|   | |||||||
| @@ -9,14 +9,14 @@ | |||||||
|     </PropertyGroup> |     </PropertyGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="CorePush" Version="4.3.0" /> |         <PackageReference Include="CorePush" Version="4.4.0" /> | ||||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> |         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> | ||||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" /> |         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" /> | ||||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> |         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> |         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> | ||||||
|         <PackageReference Include="MailKit" Version="4.13.0" /> |         <PackageReference Include="MailKit" Version="4.14.1" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> |         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|           <PrivateAssets>all</PrivateAssets> |           <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
| @@ -31,8 +31,8 @@ | |||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|         <PackageReference Include="Quartz" Version="3.14.0" /> |         <PackageReference Include="Quartz" Version="3.14.0" /> | ||||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" /> |         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ builder.ConfigureAppKestrel(builder.Configuration); | |||||||
|  |  | ||||||
| // Add application services | // Add application services | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); |  | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddDysonAuth(); | builder.Services.AddDysonAuth(); | ||||||
| builder.Services.AddAccountService(); | builder.Services.AddAccountService(); | ||||||
| @@ -45,6 +44,6 @@ app.ConfigureAppMiddleware(builder.Configuration); | |||||||
| // Configure gRPC | // Configure gRPC | ||||||
| app.ConfigureGrpcServices(); | app.ConfigureGrpcServices(); | ||||||
|  |  | ||||||
| app.UseSwaggerManifest(); | app.UseSwaggerManifest("DysonNetwork.Ring"); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ public static class ApplicationConfiguration | |||||||
|         app.ConfigureForwardedHeaders(configuration); |         app.ConfigureForwardedHeaders(configuration); | ||||||
|  |  | ||||||
|         app.UseWebSockets(); |         app.UseWebSockets(); | ||||||
|         app.UseRateLimiter(); |  | ||||||
|         app.UseAuthentication(); |         app.UseAuthentication(); | ||||||
|         app.UseAuthorization(); |         app.UseAuthorization(); | ||||||
|  |  | ||||||
| @@ -24,6 +23,7 @@ public static class ApplicationConfiguration | |||||||
|     public static WebApplication ConfigureGrpcServices(this WebApplication app) |     public static WebApplication ConfigureGrpcServices(this WebApplication app) | ||||||
|     { |     { | ||||||
|         app.MapGrpcService<RingServiceGrpc>(); |         app.MapGrpcService<RingServiceGrpc>(); | ||||||
|  |         app.MapGrpcReflectionService(); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -30,9 +30,7 @@ public static class ServiceCollectionExtensions | |||||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|         }); |         }); | ||||||
|  |         services.AddGrpcReflection(); | ||||||
|         // Register gRPC reflection for service discovery |  | ||||||
|         services.AddGrpc(); |  | ||||||
|  |  | ||||||
|         // Register gRPC services |         // Register gRPC services | ||||||
|         services.AddScoped<RingServiceGrpc>(); |         services.AddScoped<RingServiceGrpc>(); | ||||||
| @@ -50,19 +48,6 @@ public static class ServiceCollectionExtensions | |||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) |  | ||||||
|     { |  | ||||||
|         services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => |  | ||||||
|         { |  | ||||||
|             opts.Window = TimeSpan.FromMinutes(1); |  | ||||||
|             opts.PermitLimit = 120; |  | ||||||
|             opts.QueueLimit = 2; |  | ||||||
|             opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; |  | ||||||
|         })); |  | ||||||
|  |  | ||||||
|         return services; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) |     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||||
|     { |     { | ||||||
|         services.AddAuthorization(); |         services.AddAuthorization(); | ||||||
|   | |||||||
| @@ -9,10 +9,11 @@ | |||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> |         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||||
|         <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" /> |         <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" /> | ||||||
|         <PackageReference Include="Google.Protobuf" Version="3.32.1" /> |         <PackageReference Include="Google.Protobuf" Version="3.33.0" /> | ||||||
|         <PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" /> |         <PackageReference Include="Google.Protobuf.Tools" Version="3.33.0" /> | ||||||
|         <PackageReference Include="Grpc" Version="2.46.6" /> |         <PackageReference Include="Grpc" Version="2.46.6" /> | ||||||
|         <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" /> |         <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" /> | ||||||
|  |         <PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" /> | ||||||
|         <PackageReference Include="Grpc.Net.Client" Version="2.71.0" /> |         <PackageReference Include="Grpc.Net.Client" Version="2.71.0" /> | ||||||
|         <PackageReference Include="Grpc.Tools" Version="2.72.0"> |         <PackageReference Include="Grpc.Tools" Version="2.72.0"> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
| @@ -20,32 +21,32 @@ | |||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> |         <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> |         <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||||
|         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" /> |         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" /> | ||||||
|         <PackageReference Include="NATS.Net" Version="2.6.8" /> |         <PackageReference Include="NATS.Net" Version="2.6.11" /> | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> |         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> |         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> |         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||||
|         <PackageReference Include="Otp.NET" Version="1.4.0" /> |         <PackageReference Include="Otp.NET" Version="1.4.0" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" /> |         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> | ||||||
|         <PackageReference Include="System.Net.Http" Version="4.3.4" /> |         <PackageReference Include="System.Net.Http" Version="4.3.4" /> | ||||||
|         <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> |         <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||||
|  |  | ||||||
|         <PackageReference Include="Aspire.NATS.Net" Version="9.4.2" /> |         <PackageReference Include="Aspire.NATS.Net" Version="9.5.2" /> | ||||||
|         <PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.4.2" /> |         <PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.2" /> | ||||||
|         <PackageReference Include="Aspire.StackExchange.Redis" Version="9.4.2" /> |         <PackageReference Include="Aspire.StackExchange.Redis" Version="9.5.2" /> | ||||||
|  |  | ||||||
|         <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/> |         <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" /> | ||||||
|         <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2"/> |         <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.2" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" /> |         <PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ public static class SwaggerGen | |||||||
|         return builder; |         return builder; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static WebApplication UseSwaggerManifest(this WebApplication app) |     public static WebApplication UseSwaggerManifest(this WebApplication app, string serviceName) | ||||||
|     { |     { | ||||||
|         app.MapOpenApi(); |         app.MapOpenApi(); | ||||||
|          |          | ||||||
| @@ -103,7 +103,7 @@ public static class SwaggerGen | |||||||
|             var publicBasePath = configuration["Swagger:PublicBasePath"]?.TrimEnd('/') ?? ""; |             var publicBasePath = configuration["Swagger:PublicBasePath"]?.TrimEnd('/') ?? ""; | ||||||
|             options.SwaggerEndpoint( |             options.SwaggerEndpoint( | ||||||
|                 $"{publicBasePath}/swagger/v1/swagger.json", |                 $"{publicBasePath}/swagger/v1/swagger.json", | ||||||
|                 "Develop API v1"); |                 $"{serviceName} API v1"); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|   | |||||||
| @@ -148,6 +148,32 @@ public class UsernameColor | |||||||
|     public string? Value { get; set; }          // e.g. "red" or "#ff6600" |     public string? Value { get; set; }          // e.g. "red" or "#ff6600" | ||||||
|     public string? Direction { get; set; }      // e.g. "to right" |     public string? Direction { get; set; }      // e.g. "to right" | ||||||
|     public List<string>? Colors { get; set; }   // e.g. ["#ff0000", "#00ff00"] |     public List<string>? Colors { get; set; }   // e.g. ["#ff0000", "#00ff00"] | ||||||
|  |  | ||||||
|  |     public Proto.UsernameColor ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new Proto.UsernameColor | ||||||
|  |         { | ||||||
|  |             Type = Type, | ||||||
|  |             Value = Value, | ||||||
|  |             Direction = Direction, | ||||||
|  |         }; | ||||||
|  |         if (Colors is not null) | ||||||
|  |         { | ||||||
|  |             proto.Colors.AddRange(Colors); | ||||||
|  |         } | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static UsernameColor FromProtoValue(Proto.UsernameColor proto) | ||||||
|  |     { | ||||||
|  |         return new UsernameColor | ||||||
|  |         { | ||||||
|  |             Type = proto.Type, | ||||||
|  |             Value = proto.Value, | ||||||
|  |             Direction = proto.Direction, | ||||||
|  |             Colors = proto.Colors?.ToList() | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnAccountProfile : ModelBase, IIdentifiedResource | public class SnAccountProfile : ModelBase, IIdentifiedResource | ||||||
| @@ -218,6 +244,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource | |||||||
|             AccountId = AccountId.ToString(), |             AccountId = AccountId.ToString(), | ||||||
|             Verification = Verification?.ToProtoValue(), |             Verification = Verification?.ToProtoValue(), | ||||||
|             ActiveBadge = ActiveBadge?.ToProtoValue(), |             ActiveBadge = ActiveBadge?.ToProtoValue(), | ||||||
|  |             UsernameColor = UsernameColor?.ToProtoValue(), | ||||||
|             CreatedAt = CreatedAt.ToTimestamp(), |             CreatedAt = CreatedAt.ToTimestamp(), | ||||||
|             UpdatedAt = UpdatedAt.ToTimestamp() |             UpdatedAt = UpdatedAt.ToTimestamp() | ||||||
|         }; |         }; | ||||||
| @@ -247,6 +274,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource | |||||||
|             Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture), |             Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture), | ||||||
|             Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background), |             Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background), | ||||||
|             AccountId = Guid.Parse(proto.AccountId), |             AccountId = Guid.Parse(proto.AccountId), | ||||||
|  |             UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null, | ||||||
|             CreatedAt = proto.CreatedAt.ToInstant(), |             CreatedAt = proto.CreatedAt.ToInstant(), | ||||||
|             UpdatedAt = proto.UpdatedAt.ToInstant() |             UpdatedAt = proto.UpdatedAt.ToInstant() | ||||||
|         }; |         }; | ||||||
|   | |||||||
| @@ -1,37 +0,0 @@ | |||||||
| using System.ComponentModel.DataAnnotations; |  | ||||||
| using System.ComponentModel.DataAnnotations.Schema; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Models; |  | ||||||
|  |  | ||||||
| public interface IActivity |  | ||||||
| { |  | ||||||
|     public SnActivity ToActivity(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| [NotMapped] |  | ||||||
| public class SnActivity : ModelBase |  | ||||||
| { |  | ||||||
|     public Guid Id { get; set; } |  | ||||||
|     [MaxLength(1024)] public string Type { get; set; } = null!; |  | ||||||
|     [MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!; |  | ||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); |  | ||||||
|  |  | ||||||
|     public object? Data { get; set; } |  | ||||||
|  |  | ||||||
|     // Outdated fields, for backward compability |  | ||||||
|     public int Visibility => 0; |  | ||||||
|  |  | ||||||
|     public static SnActivity Empty() |  | ||||||
|     { |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         return new SnActivity |  | ||||||
|         { |  | ||||||
|             CreatedAt = now, |  | ||||||
|             UpdatedAt = now, |  | ||||||
|             Id = Guid.NewGuid(), |  | ||||||
|             Type = "empty", |  | ||||||
|             ResourceIdentifier = "none" |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -20,17 +20,13 @@ public class SnChatRoom : ModelBase, IIdentifiedResource | |||||||
|     public bool IsCommunity { get; set; } |     public bool IsCommunity { get; set; } | ||||||
|     public bool IsPublic { get; set; } |     public bool IsPublic { get; set; } | ||||||
|  |  | ||||||
|     // Outdated fields, for backward compability |  | ||||||
|     [MaxLength(32)] public string? PictureId { get; set; } |  | ||||||
|     [MaxLength(32)] public string? BackgroundId { get; set; } |  | ||||||
|  |  | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } | ||||||
|  |  | ||||||
|     [JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>(); |     [JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>(); | ||||||
|  |  | ||||||
|     public Guid? RealmId { get; set; } |     public Guid? RealmId { get; set; } | ||||||
|     public SnRealm? Realm { get; set; } |     [NotMapped] public SnRealm? Realm { get; set; } | ||||||
|  |  | ||||||
|     [NotMapped] |     [NotMapped] | ||||||
|     [JsonPropertyName("members")] |     [JsonPropertyName("members")] | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ using NodaTime.Serialization.Protobuf; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Models; | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| public class WalletCurrency | public abstract class WalletCurrency | ||||||
| { | { | ||||||
|     public const string SourcePoint = "points"; |     public const string SourcePoint = "points"; | ||||||
|     public const string GoldenPoint = "golds"; |     public const string GoldenPoint = "golds"; | ||||||
|   | |||||||
| @@ -1,15 +1,16 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Google.Protobuf.WellKnownTypes; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using NpgsqlTypes; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Models; | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| public enum PostType | public enum PostType | ||||||
| { | { | ||||||
|     Moment, |     Moment, | ||||||
|     Article |     Article, | ||||||
| } | } | ||||||
|  |  | ||||||
| public enum PostVisibility | public enum PostVisibility | ||||||
| @@ -17,7 +18,7 @@ public enum PostVisibility | |||||||
|     Public, |     Public, | ||||||
|     Friends, |     Friends, | ||||||
|     Unlisted, |     Unlisted, | ||||||
|     Private |     Private, | ||||||
| } | } | ||||||
|  |  | ||||||
| public enum PostPinMode | public enum PostPinMode | ||||||
| @@ -27,12 +28,18 @@ public enum PostPinMode | |||||||
|     ReplyPage, |     ReplyPage, | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnPost : ModelBase, IIdentifiedResource, IActivity | public class SnPost : ModelBase, IIdentifiedResource, ITimelineEvent | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     [MaxLength(1024)] public string? Title { get; set; } |  | ||||||
|     [MaxLength(4096)] public string? Description { get; set; } |     [MaxLength(1024)] | ||||||
|     [MaxLength(1024)] public string? Slug { get; set; } |     public string? Title { get; set; } | ||||||
|  |  | ||||||
|  |     [MaxLength(4096)] | ||||||
|  |     public string? Description { get; set; } | ||||||
|  |  | ||||||
|  |     [MaxLength(1024)] | ||||||
|  |     public string? Slug { get; set; } | ||||||
|     public Instant? EditedAt { get; set; } |     public Instant? EditedAt { get; set; } | ||||||
|     public Instant? PublishedAt { get; set; } |     public Instant? PublishedAt { get; set; } | ||||||
|     public PostVisibility Visibility { get; set; } = PostVisibility.Public; |     public PostVisibility Visibility { get; set; } = PostVisibility.Public; | ||||||
| @@ -42,18 +49,30 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | |||||||
|  |  | ||||||
|     public PostType Type { get; set; } |     public PostType Type { get; set; } | ||||||
|     public PostPinMode? PinMode { get; set; } |     public PostPinMode? PinMode { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } |  | ||||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; |     [Column(TypeName = "jsonb")] | ||||||
|     [Column(TypeName = "jsonb")] public PostEmbedView? EmbedView { get; set; } |     public Dictionary<string, object>? Meta { get; set; } | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public PostEmbedView? EmbedView { get; set; } | ||||||
|  |  | ||||||
|     public int ViewsUnique { get; set; } |     public int ViewsUnique { get; set; } | ||||||
|     public int ViewsTotal { get; set; } |     public int ViewsTotal { get; set; } | ||||||
|     public int Upvotes { get; set; } |     public int Upvotes { get; set; } | ||||||
|     public int Downvotes { get; set; } |     public int Downvotes { get; set; } | ||||||
|     public decimal AwardedScore { get; set; } |     public decimal AwardedScore { get; set; } | ||||||
|     [NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new(); |  | ||||||
|     [NotMapped] public int RepliesCount { get; set; } |     [NotMapped] | ||||||
|     [NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; } |     public Dictionary<string, int> ReactionsCount { get; set; } = new(); | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public int RepliesCount { get; set; } | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public Dictionary<string, bool>? ReactionsMade { get; set; } | ||||||
|  |  | ||||||
|     public bool RepliedGone { get; set; } |     public bool RepliedGone { get; set; } | ||||||
|     public bool ForwardedGone { get; set; } |     public bool ForwardedGone { get; set; } | ||||||
| @@ -64,29 +83,226 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | |||||||
|     public SnPost? ForwardedPost { get; set; } |     public SnPost? ForwardedPost { get; set; } | ||||||
|  |  | ||||||
|     public Guid? RealmId { get; set; } |     public Guid? RealmId { get; set; } | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|     public SnRealm? Realm { get; set; } |     public SnRealm? Realm { get; set; } | ||||||
|  |  | ||||||
|     [Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = []; |     [Column(TypeName = "jsonb")] | ||||||
|  |     public List<SnCloudFileReferenceObject> Attachments { get; set; } = []; | ||||||
|     [JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!; |  | ||||||
|  |  | ||||||
|     public Guid PublisherId { get; set; } |     public Guid PublisherId { get; set; } | ||||||
|     public SnPublisher Publisher { get; set; } = null!; |     public SnPublisher Publisher { get; set; } = null!; | ||||||
|  |  | ||||||
|     public ICollection<SnPostAward> Awards { get; set; } = null!; |     public List<SnPostAward> Awards { get; set; } = []; | ||||||
|     [JsonIgnore] public ICollection<SnPostReaction> Reactions { get; set; } = new List<SnPostReaction>(); |  | ||||||
|     public ICollection<SnPostTag> Tags { get; set; } = new List<SnPostTag>(); |  | ||||||
|     public ICollection<SnPostCategory> Categories { get; set; } = new List<SnPostCategory>(); |  | ||||||
|     [JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = new List<SnPostCollection>(); |  | ||||||
|  |  | ||||||
|     [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; |     [JsonIgnore] | ||||||
|     [NotMapped] public bool IsTruncated { get; set; } = false; |     public List<SnPostReaction> Reactions { get; set; } = []; | ||||||
|  |     public List<SnPostTag> Tags { get; set; } = []; | ||||||
|  |     public List<SnPostCategory> Categories { get; set; } = []; | ||||||
|  |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public List<SnPostCollection> Collections { get; set; } = []; | ||||||
|  |     public List<SnPostFeaturedRecord> FeaturedRecords { get; set; } = []; | ||||||
|  |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public bool IsTruncated { get; set; } = false; | ||||||
|  |  | ||||||
|     public string ResourceIdentifier => $"post:{Id}"; |     public string ResourceIdentifier => $"post:{Id}"; | ||||||
|  |  | ||||||
|     public SnActivity ToActivity() |     public Post ToProtoValue() | ||||||
|     { |     { | ||||||
|         return new SnActivity() |         var proto = new Post | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Title = Title ?? string.Empty, | ||||||
|  |             Description = Description ?? string.Empty, | ||||||
|  |             Slug = Slug ?? string.Empty, | ||||||
|  |             Visibility = (Proto.PostVisibility)((int)Visibility + 1), | ||||||
|  |             Type = (Proto.PostType)((int)Type + 1), | ||||||
|  |             ViewsUnique = ViewsUnique, | ||||||
|  |             ViewsTotal = ViewsTotal, | ||||||
|  |             Upvotes = Upvotes, | ||||||
|  |             Downvotes = Downvotes, | ||||||
|  |             AwardedScore = (double)AwardedScore, | ||||||
|  |             ReactionsCount = { ReactionsCount }, | ||||||
|  |             RepliesCount = RepliesCount, | ||||||
|  |             ReactionsMade = { ReactionsMade ?? new Dictionary<string, bool>() }, | ||||||
|  |             RepliedGone = RepliedGone, | ||||||
|  |             ForwardedGone = ForwardedGone, | ||||||
|  |             PublisherId = PublisherId.ToString(), | ||||||
|  |             Publisher = Publisher.ToProtoValue(), | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (EditedAt.HasValue) | ||||||
|  |             proto.EditedAt = Timestamp.FromDateTimeOffset(EditedAt.Value.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         if (PublishedAt.HasValue) | ||||||
|  |             proto.PublishedAt = Timestamp.FromDateTimeOffset(PublishedAt.Value.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         if (Content != null) | ||||||
|  |             proto.Content = Content; | ||||||
|  |  | ||||||
|  |         if (PinMode.HasValue) | ||||||
|  |             proto.PinMode = (Proto.PostPinMode)((int)PinMode.Value + 1); | ||||||
|  |  | ||||||
|  |         if (Meta != null) | ||||||
|  |             proto.Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta); | ||||||
|  |  | ||||||
|  |         if (SensitiveMarks != null) | ||||||
|  |             proto.SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks); | ||||||
|  |  | ||||||
|  |         if (EmbedView != null) | ||||||
|  |             proto.EmbedView = EmbedView.ToProtoValue(); | ||||||
|  |  | ||||||
|  |         if (RepliedPostId.HasValue) | ||||||
|  |         { | ||||||
|  |             proto.RepliedPostId = RepliedPostId.Value.ToString(); | ||||||
|  |             if (RepliedPost != null) | ||||||
|  |             { | ||||||
|  |                 proto.RepliedPost = RepliedPost.ToProtoValue(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (ForwardedPostId.HasValue) | ||||||
|  |         { | ||||||
|  |             proto.ForwardedPostId = ForwardedPostId.Value.ToString(); | ||||||
|  |             if (ForwardedPost != null) | ||||||
|  |             { | ||||||
|  |                 proto.ForwardedPost = ForwardedPost.ToProtoValue(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (RealmId.HasValue) | ||||||
|  |         { | ||||||
|  |             proto.RealmId = RealmId.Value.ToString(); | ||||||
|  |             if (Realm != null) | ||||||
|  |             { | ||||||
|  |                 proto.Realm = Realm.ToProtoValue(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         proto.Attachments.AddRange(Attachments.Select(a => a.ToProtoValue())); | ||||||
|  |         proto.Awards.AddRange(Awards.Select(a => a.ToProtoValue())); | ||||||
|  |         proto.Reactions.AddRange(Reactions.Select(r => r.ToProtoValue())); | ||||||
|  |         proto.Tags.AddRange(Tags.Select(t => t.ToProtoValue())); | ||||||
|  |         proto.Categories.AddRange(Categories.Select(c => c.ToProtoValue())); | ||||||
|  |         proto.FeaturedRecords.AddRange(FeaturedRecords.Select(f => f.ToProtoValue())); | ||||||
|  |  | ||||||
|  |         if (DeletedAt.HasValue) | ||||||
|  |             proto.DeletedAt = Timestamp.FromDateTimeOffset(DeletedAt.Value.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnPost FromProtoValue(Post proto) | ||||||
|  |     { | ||||||
|  |         var post = new SnPost | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             Title = string.IsNullOrEmpty(proto.Title) ? null : proto.Title, | ||||||
|  |             Description = string.IsNullOrEmpty(proto.Description) ? null : proto.Description, | ||||||
|  |             Slug = string.IsNullOrEmpty(proto.Slug) ? null : proto.Slug, | ||||||
|  |             Visibility = (PostVisibility)((int)proto.Visibility - 1), | ||||||
|  |             Type = (PostType)((int)proto.Type - 1), | ||||||
|  |             ViewsUnique = proto.ViewsUnique, | ||||||
|  |             ViewsTotal = proto.ViewsTotal, | ||||||
|  |             Upvotes = proto.Upvotes, | ||||||
|  |             Downvotes = proto.Downvotes, | ||||||
|  |             AwardedScore = (decimal)proto.AwardedScore, | ||||||
|  |             ReactionsCount = proto.ReactionsCount.ToDictionary(kv => kv.Key, kv => kv.Value), | ||||||
|  |             RepliesCount = proto.RepliesCount, | ||||||
|  |             ReactionsMade = proto.ReactionsMade.ToDictionary(kv => kv.Key, kv => kv.Value), | ||||||
|  |             RepliedGone = proto.RepliedGone, | ||||||
|  |             ForwardedGone = proto.ForwardedGone, | ||||||
|  |             PublisherId = Guid.Parse(proto.PublisherId), | ||||||
|  |             Publisher = SnPublisher.FromProtoValue(proto.Publisher), | ||||||
|  |             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (proto.EditedAt is not null) | ||||||
|  |             post.EditedAt = Instant.FromDateTimeOffset(proto.EditedAt.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         if (proto.PublishedAt is not null) | ||||||
|  |             post.PublishedAt = Instant.FromDateTimeOffset(proto.PublishedAt.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(proto.Content)) | ||||||
|  |             post.Content = proto.Content; | ||||||
|  |  | ||||||
|  |         if (proto is { HasPinMode: true, PinMode: > 0 }) | ||||||
|  |             post.PinMode = (PostPinMode)(proto.PinMode - 1); | ||||||
|  |  | ||||||
|  |         if (proto.Meta != null) | ||||||
|  |             post.Meta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>( | ||||||
|  |                 proto.Meta | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |         if (proto.SensitiveMarks != null) | ||||||
|  |             post.SensitiveMarks = GrpcTypeHelper.ConvertByteStringToObject< | ||||||
|  |                 List<ContentSensitiveMark> | ||||||
|  |             >(proto.SensitiveMarks); | ||||||
|  |  | ||||||
|  |         if (proto.EmbedView is not null) | ||||||
|  |             post.EmbedView = PostEmbedView.FromProtoValue(proto.EmbedView); | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(proto.RepliedPostId)) | ||||||
|  |         { | ||||||
|  |             post.RepliedPostId = Guid.Parse(proto.RepliedPostId); | ||||||
|  |             if (proto.RepliedPost is not null) | ||||||
|  |                 post.RepliedPost = FromProtoValue(proto.RepliedPost); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(proto.ForwardedPostId)) | ||||||
|  |         { | ||||||
|  |             post.ForwardedPostId = Guid.Parse(proto.ForwardedPostId); | ||||||
|  |             if (proto.ForwardedPost is not null) | ||||||
|  |                 post.ForwardedPost = FromProtoValue(proto.ForwardedPost); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!string.IsNullOrEmpty(proto.RealmId)) | ||||||
|  |         { | ||||||
|  |             post.RealmId = Guid.Parse(proto.RealmId); | ||||||
|  |             if (proto.Realm is not null) | ||||||
|  |                 post.Realm = SnRealm.FromProtoValue(proto.Realm); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         post.Attachments.AddRange( | ||||||
|  |             proto.Attachments.Select(SnCloudFileReferenceObject.FromProtoValue) | ||||||
|  |         ); | ||||||
|  |         post.Awards.AddRange( | ||||||
|  |             proto.Awards.Select(a => new SnPostAward | ||||||
|  |             { | ||||||
|  |                 Id = Guid.Parse(a.Id), | ||||||
|  |                 PostId = Guid.Parse(a.PostId), | ||||||
|  |                 AccountId = Guid.Parse(a.AccountId), | ||||||
|  |                 Amount = (decimal)a.Amount, | ||||||
|  |                 Attitude = (PostReactionAttitude)((int)a.Attitude - 1), | ||||||
|  |                 Message = string.IsNullOrEmpty(a.Message) ? null : a.Message, | ||||||
|  |                 CreatedAt = Instant.FromDateTimeOffset(a.CreatedAt.ToDateTimeOffset()), | ||||||
|  |                 UpdatedAt = Instant.FromDateTimeOffset(a.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  |         post.Reactions.AddRange(proto.Reactions.Select(SnPostReaction.FromProtoValue)); | ||||||
|  |         post.Tags.AddRange(proto.Tags.Select(SnPostTag.FromProtoValue)); | ||||||
|  |         post.Categories.AddRange(proto.Categories.Select(SnPostCategory.FromProtoValue)); | ||||||
|  |         post.FeaturedRecords.AddRange( | ||||||
|  |             proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (proto.DeletedAt is not null) | ||||||
|  |             post.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset()); | ||||||
|  |  | ||||||
|  |         return post; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public SnTimelineEvent ToActivity() | ||||||
|  |     { | ||||||
|  |         return new SnTimelineEvent() | ||||||
|         { |         { | ||||||
|             CreatedAt = PublishedAt ?? CreatedAt, |             CreatedAt = PublishedAt ?? CreatedAt, | ||||||
|             UpdatedAt = UpdatedAt, |             UpdatedAt = UpdatedAt, | ||||||
| @@ -94,7 +310,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | |||||||
|             Id = Id, |             Id = Id, | ||||||
|             Type = RepliedPostId is null ? "posts.new" : "posts.new.replies", |             Type = RepliedPostId is null ? "posts.new" : "posts.new.replies", | ||||||
|             ResourceIdentifier = ResourceIdentifier, |             ResourceIdentifier = ResourceIdentifier, | ||||||
|             Data = this |             Data = this, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -102,21 +318,83 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity | |||||||
| public class SnPostTag : ModelBase | public class SnPostTag : ModelBase | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; |  | ||||||
|     [MaxLength(256)] public string? Name { get; set; } |  | ||||||
|     [JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>(); |  | ||||||
|  |  | ||||||
|     [NotMapped] public int? Usage { get; set; } |     [MaxLength(128)] | ||||||
|  |     public string Slug { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     [MaxLength(256)] | ||||||
|  |     public string? Name { get; set; } | ||||||
|  |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public int? Usage { get; set; } | ||||||
|  |  | ||||||
|  |     public PostTag ToProtoValue() | ||||||
|  |     { | ||||||
|  |         return new PostTag | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Slug = Slug, | ||||||
|  |             Name = Name ?? string.Empty, | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnPostTag FromProtoValue(PostTag proto) | ||||||
|  |     { | ||||||
|  |         return new SnPostTag | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             Slug = proto.Slug, | ||||||
|  |             Name = proto.Name != string.Empty ? proto.Name : null, | ||||||
|  |             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnPostCategory : ModelBase | public class SnPostCategory : ModelBase | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; |  | ||||||
|     [MaxLength(256)] public string? Name { get; set; } |  | ||||||
|     [JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>(); |  | ||||||
|  |  | ||||||
|     [NotMapped] public int? Usage { get; set; } |     [MaxLength(128)] | ||||||
|  |     public string Slug { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     [MaxLength(256)] | ||||||
|  |     public string? Name { get; set; } | ||||||
|  |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public int? Usage { get; set; } | ||||||
|  |  | ||||||
|  |     public PostCategory ToProtoValue() | ||||||
|  |     { | ||||||
|  |         return new PostCategory | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Slug = Slug, | ||||||
|  |             Name = Name ?? string.Empty, | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnPostCategory FromProtoValue(PostCategory proto) | ||||||
|  |     { | ||||||
|  |         return new SnPostCategory | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             Slug = proto.Slug, | ||||||
|  |             Name = proto.Name != string.Empty ? proto.Name : null, | ||||||
|  |             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnPostCategorySubscription : ModelBase | public class SnPostCategorySubscription : ModelBase | ||||||
| @@ -133,23 +411,64 @@ public class SnPostCategorySubscription : ModelBase | |||||||
| public class SnPostCollection : ModelBase | public class SnPostCollection : ModelBase | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     [MaxLength(128)] public string Slug { get; set; } = null!; |  | ||||||
|     [MaxLength(256)] public string? Name { get; set; } |     [MaxLength(128)] | ||||||
|     [MaxLength(4096)] public string? Description { get; set; } |     public string Slug { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     [MaxLength(256)] | ||||||
|  |     public string? Name { get; set; } | ||||||
|  |  | ||||||
|  |     [MaxLength(4096)] | ||||||
|  |     public string? Description { get; set; } | ||||||
|  |  | ||||||
|     public SnPublisher Publisher { get; set; } = null!; |     public SnPublisher Publisher { get; set; } = null!; | ||||||
|  |  | ||||||
|     public ICollection<SnPost> Posts { get; set; } = new List<SnPost>(); |     public List<SnPost> Posts { get; set; } = new List<SnPost>(); | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnPostFeaturedRecord : ModelBase | public class SnPostFeaturedRecord : ModelBase | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|  |  | ||||||
|     public Guid PostId { get; set; } |     public Guid PostId { get; set; } | ||||||
|  |  | ||||||
|  |     [JsonIgnore] | ||||||
|     public SnPost Post { get; set; } = null!; |     public SnPost Post { get; set; } = null!; | ||||||
|     public Instant? FeaturedAt { get; set; } |     public Instant? FeaturedAt { get; set; } | ||||||
|     public int SocialCredits { get; set; } |     public int SocialCredits { get; set; } | ||||||
|  |  | ||||||
|  |     public PostFeaturedRecord ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new PostFeaturedRecord | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             PostId = PostId.ToString(), | ||||||
|  |             SocialCredits = SocialCredits, | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |         if (FeaturedAt.HasValue) | ||||||
|  |         { | ||||||
|  |             proto.FeaturedAt = Timestamp.FromDateTimeOffset(FeaturedAt.Value.ToDateTimeOffset()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnPostFeaturedRecord FromProtoValue(PostFeaturedRecord proto) | ||||||
|  |     { | ||||||
|  |         return new SnPostFeaturedRecord | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             PostId = Guid.Parse(proto.PostId), | ||||||
|  |             SocialCredits = proto.SocialCredits, | ||||||
|  |             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |             FeaturedAt = | ||||||
|  |                 proto.FeaturedAt != null | ||||||
|  |                     ? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset()) | ||||||
|  |                     : null, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public enum PostReactionAttitude | public enum PostReactionAttitude | ||||||
| @@ -162,12 +481,54 @@ public enum PostReactionAttitude | |||||||
| public class SnPostReaction : ModelBase | public class SnPostReaction : ModelBase | ||||||
| { | { | ||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     [MaxLength(256)] public string Symbol { get; set; } = null!; |  | ||||||
|  |     [MaxLength(256)] | ||||||
|  |     public string Symbol { get; set; } = null!; | ||||||
|     public PostReactionAttitude Attitude { get; set; } |     public PostReactionAttitude Attitude { get; set; } | ||||||
|  |  | ||||||
|     public Guid PostId { get; set; } |     public Guid PostId { get; set; } | ||||||
|     [JsonIgnore] public SnPost Post { get; set; } = null!; |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public SnPost Post { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|  |  | ||||||
|  |     [NotMapped] | ||||||
|  |     public SnAccount? Account { get; set; } | ||||||
|  |  | ||||||
|  |     public PostReaction ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new PostReaction | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Symbol = Symbol, | ||||||
|  |             Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1), | ||||||
|  |             PostId = PostId.ToString(), | ||||||
|  |             AccountId = AccountId.ToString(), | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |         if (Account != null) | ||||||
|  |         { | ||||||
|  |             proto.Account = Account.ToProtoValue(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnPostReaction FromProtoValue(Proto.PostReaction proto) | ||||||
|  |     { | ||||||
|  |         return new SnPostReaction | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             Symbol = proto.Symbol, | ||||||
|  |             Attitude = (PostReactionAttitude)((int)proto.Attitude - 1), | ||||||
|  |             PostId = Guid.Parse(proto.PostId), | ||||||
|  |             AccountId = Guid.Parse(proto.AccountId), | ||||||
|  |             Account = proto.Account != null ? SnAccount.FromProtoValue(proto.Account) : null, | ||||||
|  |             CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public class SnPostAward : ModelBase | public class SnPostAward : ModelBase | ||||||
| @@ -175,11 +536,32 @@ public class SnPostAward : ModelBase | |||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     public decimal Amount { get; set; } |     public decimal Amount { get; set; } | ||||||
|     public PostReactionAttitude Attitude { get; set; } |     public PostReactionAttitude Attitude { get; set; } | ||||||
|     [MaxLength(4096)] public string? Message { get; set; } |  | ||||||
|  |     [MaxLength(4096)] | ||||||
|  |     public string? Message { get; set; } | ||||||
|  |  | ||||||
|     public Guid PostId { get; set; } |     public Guid PostId { get; set; } | ||||||
|     [JsonIgnore] public SnPost Post { get; set; } = null!; |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     public SnPost Post { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|  |  | ||||||
|  |     public PostAward ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new PostAward | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Amount = (double)Amount, | ||||||
|  |             Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1), | ||||||
|  |             PostId = PostId.ToString(), | ||||||
|  |             AccountId = AccountId.ToString(), | ||||||
|  |             CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()), | ||||||
|  |             UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()), | ||||||
|  |         }; | ||||||
|  |         if (Message != null) | ||||||
|  |             proto.Message = Message; | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// <summary> | /// <summary> | ||||||
| @@ -192,9 +574,34 @@ public class PostEmbedView | |||||||
|     public string Uri { get; set; } = null!; |     public string Uri { get; set; } = null!; | ||||||
|     public double? AspectRatio { get; set; } |     public double? AspectRatio { get; set; } | ||||||
|     public PostEmbedViewRenderer Renderer { get; set; } = PostEmbedViewRenderer.WebView; |     public PostEmbedViewRenderer Renderer { get; set; } = PostEmbedViewRenderer.WebView; | ||||||
|  |  | ||||||
|  |     public Proto.PostEmbedView ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new Proto.PostEmbedView | ||||||
|  |         { | ||||||
|  |             Uri = Uri, | ||||||
|  |             Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer, | ||||||
|  |         }; | ||||||
|  |         if (AspectRatio.HasValue) | ||||||
|  |         { | ||||||
|  |             proto.AspectRatio = AspectRatio.Value; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static PostEmbedView FromProtoValue(Proto.PostEmbedView proto) | ||||||
|  |     { | ||||||
|  |         return new PostEmbedView | ||||||
|  |         { | ||||||
|  |             Uri = proto.Uri, | ||||||
|  |             AspectRatio = proto.HasAspectRatio ? proto.AspectRatio : null, | ||||||
|  |             Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public enum PostEmbedViewRenderer | public enum PostEmbedViewRenderer | ||||||
| { | { | ||||||
|     WebView |     WebView, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,10 +22,6 @@ public class SnPublisher : ModelBase, IIdentifiedResource | |||||||
|     [MaxLength(256)] public string Nick { get; set; } = string.Empty; |     [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||||
|     [MaxLength(4096)] public string? Bio { get; set; } |     [MaxLength(4096)] public string? Bio { get; set; } | ||||||
|  |  | ||||||
|     // Outdated fields, for backward compability |  | ||||||
|     [MaxLength(32)] public string? PictureId { get; set; } |  | ||||||
|     [MaxLength(32)] public string? BackgroundId { get; set; } |  | ||||||
|  |  | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } | ||||||
|  |  | ||||||
| @@ -42,12 +38,12 @@ public class SnPublisher : ModelBase, IIdentifiedResource | |||||||
|  |  | ||||||
|     public Guid? AccountId { get; set; } |     public Guid? AccountId { get; set; } | ||||||
|     public Guid? RealmId { get; set; } |     public Guid? RealmId { get; set; } | ||||||
|     [JsonIgnore] public SnRealm? Realm { get; set; } |     [NotMapped] public SnRealm? Realm { get; set; } | ||||||
|     [NotMapped] public SnAccount? Account { get; set; } |     [NotMapped] public SnAccount? Account { get; set; } | ||||||
|  |  | ||||||
|     public string ResourceIdentifier => $"publisher:{Id}"; |     public string ResourceIdentifier => $"publisher:{Id}"; | ||||||
|  |  | ||||||
|     public static SnPublisher FromProto(Proto.Publisher proto) |     public static SnPublisher FromProtoValue(Proto.Publisher proto) | ||||||
|     { |     { | ||||||
|         var publisher = new SnPublisher |         var publisher = new SnPublisher | ||||||
|         { |         { | ||||||
| @@ -89,7 +85,7 @@ public class SnPublisher : ModelBase, IIdentifiedResource | |||||||
|         return publisher; |         return publisher; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public Proto.Publisher ToProto() |     public Proto.Publisher ToProtoValue() | ||||||
|     { |     { | ||||||
|         var p = new Proto.Publisher() |         var p = new Proto.Publisher() | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using NodaTime.Serialization.Protobuf; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Models; | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| @@ -16,21 +18,41 @@ public class SnRealm : ModelBase, IIdentifiedResource | |||||||
|     public bool IsCommunity { get; set; } |     public bool IsCommunity { get; set; } | ||||||
|     public bool IsPublic { get; set; } |     public bool IsPublic { get; set; } | ||||||
|  |  | ||||||
|     // Outdated fields, for backward compability |  | ||||||
|     [MaxLength(32)] public string? PictureId { get; set; } |  | ||||||
|     [MaxLength(32)] public string? BackgroundId { get; set; } |  | ||||||
|  |  | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Picture { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } |     [Column(TypeName = "jsonb")] public SnCloudFileReferenceObject? Background { get; set; } | ||||||
|      |      | ||||||
|     [Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; } |     [Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; } | ||||||
|  |  | ||||||
|     [JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>(); |     [JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>(); | ||||||
|     [JsonIgnore] public ICollection<SnChatRoom> ChatRooms { get; set; } = new List<SnChatRoom>(); |  | ||||||
|  |  | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|  |  | ||||||
|     public string ResourceIdentifier => $"realm:{Id}"; |     public string ResourceIdentifier => $"realm:{Id}"; | ||||||
|  |  | ||||||
|  |     public Realm ToProtoValue() | ||||||
|  |     { | ||||||
|  |         return new Realm | ||||||
|  |         { | ||||||
|  |             Id = Id.ToString(), | ||||||
|  |             Name = Name, | ||||||
|  |             Slug = Slug, | ||||||
|  |             IsCommunity = IsCommunity, | ||||||
|  |             IsPublic = IsPublic | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnRealm FromProtoValue(Realm proto) | ||||||
|  |     { | ||||||
|  |         return new SnRealm | ||||||
|  |         { | ||||||
|  |             Id = Guid.Parse(proto.Id), | ||||||
|  |             Name = proto.Name, | ||||||
|  |             Slug = proto.Slug, | ||||||
|  |             Description = "", | ||||||
|  |             IsCommunity = proto.IsCommunity, | ||||||
|  |             IsPublic = proto.IsPublic | ||||||
|  |         }; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| public abstract class RealmMemberRole | public abstract class RealmMemberRole | ||||||
| @@ -51,4 +73,40 @@ public class SnRealmMember : ModelBase | |||||||
|     public int Role { get; set; } = RealmMemberRole.Normal; |     public int Role { get; set; } = RealmMemberRole.Normal; | ||||||
|     public Instant? JoinedAt { get; set; } |     public Instant? JoinedAt { get; set; } | ||||||
|     public Instant? LeaveAt { get; set; } |     public Instant? LeaveAt { get; set; } | ||||||
|  |  | ||||||
|  |     public Proto.RealmMember ToProtoValue() | ||||||
|  |     { | ||||||
|  |         var proto = new Proto.RealmMember | ||||||
|  |         { | ||||||
|  |             AccountId = AccountId.ToString(), | ||||||
|  |             RealmId = RealmId.ToString(), | ||||||
|  |             Role = Role, | ||||||
|  |             JoinedAt = JoinedAt?.ToTimestamp(), | ||||||
|  |             LeaveAt = LeaveAt?.ToTimestamp(), | ||||||
|  |             Realm = Realm.ToProtoValue() | ||||||
|  |         }; | ||||||
|  |         if (Account != null) | ||||||
|  |         { | ||||||
|  |             proto.Account = Account.ToProtoValue(); | ||||||
|  |         } | ||||||
|  |         return proto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static SnRealmMember FromProtoValue(RealmMember proto) | ||||||
|  |     { | ||||||
|  |         var member = new SnRealmMember | ||||||
|  |         { | ||||||
|  |             AccountId = Guid.Parse(proto.AccountId), | ||||||
|  |             RealmId = Guid.Parse(proto.RealmId), | ||||||
|  |             Role = proto.Role, | ||||||
|  |             JoinedAt = proto.JoinedAt?.ToInstant(), | ||||||
|  |             LeaveAt = proto.LeaveAt?.ToInstant(), | ||||||
|  |             Realm = proto.Realm != null ? SnRealm.FromProtoValue(proto.Realm) : new SnRealm() // Provide default or handle null | ||||||
|  |         }; | ||||||
|  |         if (proto.Account != null) | ||||||
|  |         { | ||||||
|  |             member.Account = SnAccount.FromProtoValue(proto.Account); | ||||||
|  |         } | ||||||
|  |         return member; | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										54
									
								
								DysonNetwork.Shared/Models/SnLottery.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								DysonNetwork.Shared/Models/SnLottery.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  |  | ||||||
|  | public enum LotteryDrawStatus | ||||||
|  | { | ||||||
|  |     Pending = 0, | ||||||
|  |     Drawn = 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class SnLotteryRecord : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; set; } = Guid.NewGuid(); | ||||||
|  |  | ||||||
|  |     public Instant DrawDate { get; set; } // Date of the draw | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public List<int> WinningRegionOneNumbers { get; set; } = new(); // 5 winning numbers | ||||||
|  |  | ||||||
|  |     [Range(0, 99)] | ||||||
|  |     public int WinningRegionTwoNumber { get; set; } // 1 winning number | ||||||
|  |  | ||||||
|  |     public int TotalTickets { get; set; } // Total tickets processed for this draw | ||||||
|  |     public int TotalPrizesAwarded { get; set; } // Total prizes awarded | ||||||
|  |     public long TotalPrizeAmount { get; set; } // Total ISP prize amount awarded | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class SnLottery : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; init; } = Guid.NewGuid(); | ||||||
|  |  | ||||||
|  |     public SnAccount Account { get; init; } = null!; | ||||||
|  |     public Guid AccountId { get; init; } | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public List<int> RegionOneNumbers { get; set; } = []; // 5 numbers, 0-99, unique | ||||||
|  |  | ||||||
|  |     [Range(0, 99)] | ||||||
|  |     public int RegionTwoNumber { get; init; } // 1 number, 0-99, can repeat | ||||||
|  |  | ||||||
|  |     public int Multiplier { get; init; } = 1; // Default 1x | ||||||
|  |  | ||||||
|  |     public LotteryDrawStatus DrawStatus { get; set; } = LotteryDrawStatus.Pending; // Status to track draw processing | ||||||
|  |  | ||||||
|  |     public Instant? DrawDate { get; set; } // Date when this ticket was drawn | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public List<int>? MatchedRegionOneNumbers { get; set; } // The actual numbers that matched in region one | ||||||
|  |  | ||||||
|  |     public int? MatchedRegionTwoNumber { get; set; } // The matched number if special number matched (null otherwise) | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								DysonNetwork.Shared/Models/ThinkingSequence.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Shared/Models/ThinkingSequence.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
|  | public class SnThinkingSequence : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; set; } = Guid.NewGuid(); | ||||||
|  |     [MaxLength(4096)] public string? Topic { get; set; } | ||||||
|  |      | ||||||
|  |     public long TotalToken { get; set; } | ||||||
|  |     public long PaidToken { get; set; } | ||||||
|  |  | ||||||
|  |     public Guid AccountId { get; set; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public enum ThinkingThoughtRole | ||||||
|  | { | ||||||
|  |     Assistant, | ||||||
|  |     User | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public enum StreamingContentType | ||||||
|  | { | ||||||
|  |     Text, | ||||||
|  |     Reasoning, | ||||||
|  |     FunctionCall, | ||||||
|  |     Unknown | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class SnThinkingChunk | ||||||
|  | { | ||||||
|  |     public StreamingContentType Type { get; set; } | ||||||
|  |     public Dictionary<string, object>? Data { get; set; } = new(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public class SnThinkingThought : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; set; } = Guid.NewGuid(); | ||||||
|  |     public string? Content { get; set; } | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = []; | ||||||
|  |     [Column(TypeName = "jsonb")] public List<SnThinkingChunk> Chunks { get; set; } = []; | ||||||
|  |  | ||||||
|  |     public ThinkingThoughtRole Role { get; set; } | ||||||
|  |  | ||||||
|  |     public long TokenCount { get; set; } | ||||||
|  |     [MaxLength(4096)] public string? ModelName { get; set; } | ||||||
|  |      | ||||||
|  |     public Guid SequenceId { get; set; } | ||||||
|  |     [JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!; | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								DysonNetwork.Shared/Models/Timeline.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								DysonNetwork.Shared/Models/Timeline.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | using System.ComponentModel.DataAnnotations; | ||||||
|  | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
|  | public interface ITimelineEvent | ||||||
|  | { | ||||||
|  |     public SnTimelineEvent ToActivity(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [NotMapped] | ||||||
|  | public class SnTimelineEvent : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; set; } | ||||||
|  |  | ||||||
|  |     [MaxLength(1024)] | ||||||
|  |     public string Type { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     [MaxLength(4096)] | ||||||
|  |     public string ResourceIdentifier { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public Dictionary<string, object> Meta { get; set; } = new(); | ||||||
|  |  | ||||||
|  |     public object? Data { get; set; } | ||||||
|  |  | ||||||
|  |     public static SnTimelineEvent Empty() | ||||||
|  |     { | ||||||
|  |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |         return new SnTimelineEvent | ||||||
|  |         { | ||||||
|  |             CreatedAt = now, | ||||||
|  |             UpdatedAt = now, | ||||||
|  |             Id = Guid.NewGuid(), | ||||||
|  |             Type = "empty", | ||||||
|  |             ResourceIdentifier = "none", | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -51,6 +51,7 @@ public abstract class GrpcTypeHelper | |||||||
|                 _ => Value.ForString(JsonSerializer.Serialize(kvp.Value, SerializerOptions)) // fallback to JSON string |                 _ => Value.ForString(JsonSerializer.Serialize(kvp.Value, SerializerOptions)) // fallback to JSON string | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -66,13 +67,15 @@ public abstract class GrpcTypeHelper | |||||||
|                     try |                     try | ||||||
|                     { |                     { | ||||||
|                         // Try to parse as JSON object or primitive |                         // Try to parse as JSON object or primitive | ||||||
|                         result[kvp.Key] = JsonNode.Parse(value.StringValue)?.AsObject() ?? JsonObject.Create(new JsonElement()); |                         result[kvp.Key] = JsonNode.Parse(value.StringValue)?.AsObject() ?? | ||||||
|  |                                           JsonObject.Create(new JsonElement()); | ||||||
|                     } |                     } | ||||||
|                     catch |                     catch | ||||||
|                     { |                     { | ||||||
|                         // Fallback to raw string |                         // Fallback to raw string | ||||||
|                         result[kvp.Key] = value.StringValue; |                         result[kvp.Key] = value.StringValue; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     break; |                     break; | ||||||
|                 case Value.KindOneofCase.NumberValue: |                 case Value.KindOneofCase.NumberValue: | ||||||
|                     result[kvp.Key] = value.NumberValue; |                     result[kvp.Key] = value.NumberValue; | ||||||
| @@ -106,6 +109,7 @@ public abstract class GrpcTypeHelper | |||||||
|                     break; |                     break; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -117,7 +121,8 @@ public abstract class GrpcTypeHelper | |||||||
|             Value.KindOneofCase.NumberValue => value.NumberValue, |             Value.KindOneofCase.NumberValue => value.NumberValue, | ||||||
|             Value.KindOneofCase.BoolValue => value.BoolValue, |             Value.KindOneofCase.BoolValue => value.BoolValue, | ||||||
|             Value.KindOneofCase.NullValue => null, |             Value.KindOneofCase.NullValue => null, | ||||||
|             _ => JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(value, SerializerOptions), SerializerOptions) |             _ => JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(value, SerializerOptions), | ||||||
|  |                 SerializerOptions) | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -165,6 +170,6 @@ public abstract class GrpcTypeHelper | |||||||
|  |  | ||||||
|     public static T? ConvertByteStringToObject<T>(ByteString bytes) |     public static T? ConvertByteStringToObject<T>(ByteString bytes) | ||||||
|     { |     { | ||||||
|         return JsonSerializer.Deserialize<T>(bytes.ToStringUtf8(), SerializerOptions); |         return bytes.IsEmpty ? default : JsonSerializer.Deserialize<T>(bytes.ToStringUtf8(), SerializerOptions); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -59,6 +59,13 @@ message AccountStatus { | |||||||
|     bytes meta = 10; |     bytes meta = 10; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | message UsernameColor { | ||||||
|  |     string type = 1; | ||||||
|  |     google.protobuf.StringValue value = 2; | ||||||
|  |     google.protobuf.StringValue direction = 3; | ||||||
|  |     repeated string colors = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
| // Profile contains detailed information about a user | // Profile contains detailed information about a user | ||||||
| message AccountProfile { | message AccountProfile { | ||||||
|     string id = 1; |     string id = 1; | ||||||
| @@ -89,6 +96,7 @@ message AccountProfile { | |||||||
|  |  | ||||||
|     google.protobuf.Timestamp created_at = 22; |     google.protobuf.Timestamp created_at = 22; | ||||||
|     google.protobuf.Timestamp updated_at = 23; |     google.protobuf.Timestamp updated_at = 23; | ||||||
|  |     optional UsernameColor username_color = 24; | ||||||
| } | } | ||||||
|  |  | ||||||
| // AccountContact represents a contact method for an account | // AccountContact represents a contact method for an account | ||||||
| @@ -254,6 +262,7 @@ service AccountService { | |||||||
|     rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} |     rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} | ||||||
|     rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {} |     rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {} | ||||||
|     rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} |     rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} | ||||||
|  |     rpc SearchAccount(SearchAccountRequest) returns (GetAccountBatchResponse) {} | ||||||
|     rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} |     rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} | ||||||
|  |  | ||||||
|     rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {} |     rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {} | ||||||
| @@ -343,6 +352,10 @@ message LookupAccountBatchRequest { | |||||||
|     repeated string names = 1; |     repeated string names = 1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | message SearchAccountRequest { | ||||||
|  |     string query = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
| message GetAccountBatchResponse { | message GetAccountBatchResponse { | ||||||
|     repeated Account accounts = 1;  // List of accounts |     repeated Account accounts = 1;  // List of accounts | ||||||
| } | } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user