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:
|
||||
|
||||
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:
|
||||
needs: determine-changes
|
||||
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
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
|
||||
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -21,11 +21,16 @@ var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop"
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithReference(sphereService);
|
||||
var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight")
|
||||
.WithReference(passService)
|
||||
.WithReference(ringService)
|
||||
.WithReference(sphereService)
|
||||
.WithReference(developService);
|
||||
|
||||
passService.WithReference(developService).WithReference(driveService);
|
||||
|
||||
List<IResourceBuilder<ProjectResource>> services =
|
||||
[ringService, passService, driveService, sphereService, developService];
|
||||
[ringService, passService, driveService, sphereService, developService, insightService];
|
||||
|
||||
for (var idx = 0; idx < services.Count; idx++)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" />
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -8,11 +9,12 @@
|
||||
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
|
||||
<RootNamespace>DysonNetwork.Control</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<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.Nats" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="9.5.1" />
|
||||
<PackageReference Include="Aspire.Hosting.Nats" Version="9.5.2" />
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
|
||||
@@ -21,5 +23,6 @@
|
||||
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" />
|
||||
<ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,7 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop;
|
||||
|
||||
@@ -30,6 +31,35 @@ public class AppDatabase(
|
||||
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);
|
||||
|
||||
@@ -9,16 +9,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<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="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" 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.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||
|
||||
@@ -19,7 +19,7 @@ public class BotAccountController(
|
||||
DeveloperService ds,
|
||||
DevProjectService projectService,
|
||||
ILogger<BotAccountController> logger,
|
||||
AccountClientHelper accounts,
|
||||
RemoteAccountService remoteAccounts,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
|
||||
)
|
||||
: ControllerBase
|
||||
@@ -222,7 +222,7 @@ public class BotAccountController(
|
||||
if (bot is null || bot.ProjectId != projectId)
|
||||
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.Nick is not null) botAccount.Nick = request.Nick;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace DysonNetwork.Develop.Identity;
|
||||
public class BotAccountService(
|
||||
AppDatabase db,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
|
||||
AccountClientHelper accounts
|
||||
RemoteAccountService remoteAccounts
|
||||
)
|
||||
{
|
||||
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
|
||||
@@ -158,7 +158,7 @@ public class BotAccountService(
|
||||
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ public class DeveloperController(
|
||||
try
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||
pub = SnPublisher.FromProto(pubResponse.Publisher);
|
||||
pub = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||
} catch (RpcException ex)
|
||||
{
|
||||
return NotFound(ex.Status.Detail);
|
||||
|
||||
@@ -13,7 +13,7 @@ public class DeveloperService(
|
||||
public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class DeveloperService(
|
||||
var pubRequest = new GetPublisherBatchRequest();
|
||||
pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
|
||||
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 =>
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddDysonAuth();
|
||||
builder.Services.AddPublisherService();
|
||||
builder.Services.AddSphereService();
|
||||
builder.Services.AddAccountService();
|
||||
builder.Services.AddDriveService();
|
||||
|
||||
@@ -35,6 +35,6 @@ using (var scope = app.Services.CreateScope())
|
||||
|
||||
app.ConfigureAppMiddleware(builder.Configuration);
|
||||
|
||||
app.UseSwaggerManifest();
|
||||
app.UseSwaggerManifest("DysonNetwork.Develop");
|
||||
|
||||
app.Run();
|
||||
@@ -4,11 +4,7 @@ using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Develop.Project;
|
||||
|
||||
public class DevProjectService(
|
||||
AppDatabase db,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
public class DevProjectService(AppDatabase db )
|
||||
{
|
||||
public async Task<SnDevProject> CreateProjectAsync(
|
||||
SnDeveloper developer,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using Prometheus;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
@@ -9,7 +8,6 @@ public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapMetrics();
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
@@ -23,6 +21,7 @@ public static class ApplicationConfiguration
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGrpcService<CustomAppServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public static class ServiceCollectionExtensions
|
||||
});
|
||||
|
||||
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
|
||||
@@ -18,9 +18,5 @@
|
||||
},
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
},
|
||||
"Service": {
|
||||
"Name": "DysonNetwork.Develop",
|
||||
"Url": "https://localhost:7192"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,27 +10,27 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<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="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeKit" Version="4.13.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.14.0" />
|
||||
<PackageReference Include="MimeTypes" Version="2.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="6.0.5" />
|
||||
<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>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.2" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<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.Design" Version="1.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
|
||||
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
|
||||
<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="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,7 +4,6 @@ using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using tusdotnet.Stores;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -16,13 +15,10 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV
|
||||
// Add application services
|
||||
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddDysonAuth();
|
||||
builder.Services.AddAccountService();
|
||||
|
||||
builder.Services.AddAppFileStorage(builder.Configuration);
|
||||
|
||||
builder.Services.AddAppFlushHandlers();
|
||||
builder.Services.AddAppBusinessServices();
|
||||
builder.Services.AddAppScheduledJobs();
|
||||
@@ -43,12 +39,11 @@ using (var scope = app.Services.CreateScope())
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
||||
app.ConfigureAppMiddleware(tusDiskStore);
|
||||
app.ConfigureAppMiddleware();
|
||||
|
||||
// Configure gRPC
|
||||
app.ConfigureGrpcServices();
|
||||
|
||||
app.UseSwaggerManifest();
|
||||
app.UseSwaggerManifest("DysonNetwork.Drive");
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using tusdotnet;
|
||||
using tusdotnet.Interfaces;
|
||||
|
||||
namespace DysonNetwork.Drive.Startup;
|
||||
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore)
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app)
|
||||
{
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration)));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -21,6 +17,7 @@ public static class ApplicationBuilderExtensions
|
||||
// Map your gRPC services here
|
||||
app.MapGrpcService<FileServiceGrpc>();
|
||||
app.MapGrpcService<FileReferenceServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using tusdotnet.Stores;
|
||||
|
||||
namespace DysonNetwork.Drive.Startup;
|
||||
|
||||
@@ -27,9 +24,7 @@ public static class ServiceCollectionExtensions
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
|
||||
// Register gRPC reflection for service discovery
|
||||
services.AddGrpc();
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
@@ -43,19 +38,6 @@ public static class ServiceCollectionExtensions
|
||||
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)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
@@ -69,17 +51,6 @@ public static class ServiceCollectionExtensions
|
||||
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)
|
||||
{
|
||||
services.AddScoped<Storage.FileService>();
|
||||
|
||||
@@ -187,7 +187,7 @@ public class FileController(
|
||||
|
||||
public class MarkFileRequest
|
||||
{
|
||||
public List<ContentSensitiveMark>? SensitiveMarks { get; set; }
|
||||
public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; }
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
@@ -298,7 +298,7 @@ public class FileController(
|
||||
public string? Description { get; set; }
|
||||
public Dictionary<string, object?>? UserMeta { 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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
],
|
||||
"Service": {
|
||||
"Name": "DysonNetwork.Drive",
|
||||
"Url": "https://localhost:7092"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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" />
|
||||
</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[]
|
||||
{
|
||||
@@ -90,7 +90,6 @@ var apiRoutes = serviceNames.Select(serviceName =>
|
||||
{
|
||||
var apiPath = serviceName switch
|
||||
{
|
||||
"pass" => "/id",
|
||||
_ => $"/{serviceName}"
|
||||
};
|
||||
return new RouteConfig
|
||||
@@ -123,9 +122,9 @@ var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray();
|
||||
var clusters = serviceNames.Select(serviceName => new ClusterConfig
|
||||
{
|
||||
ClusterId = serviceName,
|
||||
HealthCheck = new()
|
||||
HealthCheck = new HealthCheckConfig
|
||||
{
|
||||
Active = new()
|
||||
Active = new ActiveHealthCheckConfig
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = TimeSpan.FromSeconds(10),
|
||||
@@ -162,8 +161,6 @@ app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.MapReverseProxy().RequireRateLimiting("fixed");
|
||||
|
||||
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? Location { 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 List<ProfileLink>? Links { get; set; }
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ public class AccountEventService(
|
||||
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)
|
||||
{
|
||||
@@ -322,7 +322,11 @@ public class AccountEventService(
|
||||
}));
|
||||
|
||||
// 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
|
||||
.Where(x => x.AccountId == user.Id)
|
||||
|
||||
@@ -12,13 +12,11 @@ public class AccountServiceGrpc(
|
||||
AccountEventService accountEvents,
|
||||
RelationshipService relationships,
|
||||
SubscriptionService subscriptions,
|
||||
IClock clock,
|
||||
ILogger<AccountServiceGrpc> logger
|
||||
)
|
||||
: Shared.Proto.AccountService.AccountServiceBase
|
||||
{
|
||||
private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db));
|
||||
private readonly IClock _clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
|
||||
private readonly ILogger<AccountServiceGrpc>
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
@@ -160,6 +158,26 @@ public class AccountServiceGrpc(
|
||||
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,
|
||||
ServerCallContext context)
|
||||
{
|
||||
@@ -246,7 +264,7 @@ public class AccountServiceGrpc(
|
||||
|
||||
public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context)
|
||||
{
|
||||
var hasRelationship = false;
|
||||
bool hasRelationship;
|
||||
if (!request.HasStatus)
|
||||
hasRelationship = await relationships.HasExistingRelationship(
|
||||
Guid.Parse(request.AccountId),
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace DysonNetwork.Pass.Account;
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -32,8 +32,8 @@ public class ActionLogServiceGrpc : Shared.Proto.ActionLogService.ActionLogServi
|
||||
try
|
||||
{
|
||||
var meta = request.Meta
|
||||
?.Select(x => new KeyValuePair<string, object?>(x.Key, GrpcTypeHelper.ConvertValueToObject(x.Value)))
|
||||
.ToDictionary() ?? new Dictionary<string, object?>();
|
||||
?.Select(x => new KeyValuePair<string, object>(x.Key, GrpcTypeHelper.ConvertValueToObject(x.Value)))
|
||||
.ToDictionary() ?? new Dictionary<string, object>();
|
||||
|
||||
_actionLogService.CreateActionLog(
|
||||
accountId,
|
||||
@@ -41,6 +41,7 @@ public class ActionLogServiceGrpc : Shared.Proto.ActionLogService.ActionLogServi
|
||||
meta
|
||||
);
|
||||
|
||||
await Task.CompletedTask;
|
||||
return new CreateActionLogResponse();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -39,6 +39,9 @@ public class AppDatabase(
|
||||
public DbSet<SnAuthClient> AuthClients { 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<SnWalletPocket> WalletPockets { 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<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)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
@@ -128,6 +134,14 @@ public class AppDatabase(
|
||||
.WithMany(a => a.IncomingRelationships)
|
||||
.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
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
|
||||
@@ -343,8 +343,8 @@ public class OidcProviderController(
|
||||
{
|
||||
issuer,
|
||||
authorization_endpoint = $"{siteUrl}/auth/authorize",
|
||||
token_endpoint = $"{baseUrl}/id/auth/open/token",
|
||||
userinfo_endpoint = $"{baseUrl}/id/auth/open/userinfo",
|
||||
token_endpoint = $"{baseUrl}/pass/auth/open/token",
|
||||
userinfo_endpoint = $"{baseUrl}/pass/auth/open/userinfo",
|
||||
jwks_uri = $"{baseUrl}/.well-known/jwks",
|
||||
scopes_supported = new[] { "openid", "profile", "email" },
|
||||
response_types_supported = new[]
|
||||
|
||||
@@ -7,49 +7,44 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1"/>
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1"/>
|
||||
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5"/>
|
||||
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0"/>
|
||||
<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="BCrypt.Net-Next" Version="4.0.3"/>
|
||||
<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" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
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
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("ProductVersion", "9.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -1059,6 +1059,117 @@ namespace DysonNetwork.Pass.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1252,6 +1363,127 @@ namespace DysonNetwork.Pass.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2113,6 +2345,18 @@ namespace DysonNetwork.Pass.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||
@@ -2145,6 +2389,18 @@ namespace DysonNetwork.Pass.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||
@@ -2336,6 +2592,11 @@ namespace DysonNetwork.Pass.Migrations
|
||||
b.Navigation("Nodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
|
||||
{
|
||||
b.Navigation("Pockets");
|
||||
|
||||
@@ -13,7 +13,6 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
||||
|
||||
// Add application services
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddRingService();
|
||||
builder.Services.AddDriveService();
|
||||
@@ -52,6 +51,6 @@ app.ConfigureAppMiddleware(builder.Configuration);
|
||||
// Configure gRPC
|
||||
app.ConfigureGrpcServices();
|
||||
|
||||
app.UseSwaggerManifest();
|
||||
app.UseSwaggerManifest("DysonNetwork.Pass");
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||
using ActionLogService = DysonNetwork.Pass.Account.ActionLogService;
|
||||
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
namespace DysonNetwork.Pass.Realm;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/realms")]
|
||||
@@ -17,9 +20,9 @@ public class RealmController(
|
||||
RealmService rs,
|
||||
FileService.FileServiceClient files,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
AccountClientHelper accountsHelper
|
||||
ActionLogService als,
|
||||
RelationshipService rels,
|
||||
AccountEventService accountEvents
|
||||
) : Controller
|
||||
{
|
||||
[HttpGet("{slug}")]
|
||||
@@ -37,8 +40,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var members = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -54,8 +57,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnRealmMember>>> ListInvites()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var members = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -77,20 +80,18 @@ public class RealmController(
|
||||
public async Task<ActionResult<SnRealmMember>> InviteMember(string slug,
|
||||
[FromBody] RealmMemberRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var relatedUser =
|
||||
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
||||
var relatedUser = await db.Accounts.Where(a => a.Id == request.RelatedUserId).FirstOrDefaultAsync();
|
||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||
|
||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
RelatedId = request.RelatedUserId.ToString(),
|
||||
Status = -100
|
||||
});
|
||||
if (hasBlocked?.Value ?? false)
|
||||
var hasBlocked = await rels.HasRelationshipWithStatus(
|
||||
currentUser.Id,
|
||||
request.RelatedUserId,
|
||||
RelationshipStatus.Blocked
|
||||
);
|
||||
if (hasBlocked)
|
||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||
|
||||
var realm = await db.Realms
|
||||
@@ -102,7 +103,7 @@ public class RealmController(
|
||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||
|
||||
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)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingMember != null)
|
||||
@@ -116,26 +117,23 @@ public class RealmController(
|
||||
await db.SaveChangesAsync();
|
||||
await rs.SendInviteNotify(existingMember);
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.invite",
|
||||
Meta =
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.invite",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(existingMember.AccountId.ToString()) },
|
||||
{ "role", Value.ForNumber(request.Role) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(existingMember);
|
||||
}
|
||||
|
||||
var member = new SnRealmMember
|
||||
{
|
||||
AccountId = Guid.Parse(relatedUser.Id),
|
||||
AccountId = relatedUser.Id,
|
||||
RealmId = realm.Id,
|
||||
Role = request.Role,
|
||||
};
|
||||
@@ -143,21 +141,18 @@ public class RealmController(
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.invite",
|
||||
Meta =
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.invite",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "role", Value.ForNumber(request.Role) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
member.AccountId = Guid.Parse(relatedUser.Id);
|
||||
member.AccountId = relatedUser.Id;
|
||||
member.Realm = realm;
|
||||
await rs.SendInviteNotify(member);
|
||||
|
||||
@@ -168,8 +163,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -182,18 +177,15 @@ public class RealmController(
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.join",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
||||
{ "realm_id", member.RealmId.ToString() },
|
||||
{ "account_id", member.AccountId.ToString() }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
@@ -202,8 +194,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -215,19 +207,16 @@ public class RealmController(
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.decline_invite",
|
||||
Meta =
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.decline_invite",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "decliner_id", Value.ForString(currentUser.Id) }
|
||||
{ "decliner_id", Value.ForString(currentUser.Id.ToString()) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -248,8 +237,8 @@ public class RealmController(
|
||||
|
||||
if (!realm.IsPublic)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal))
|
||||
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)
|
||||
.ToListAsync();
|
||||
|
||||
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
|
||||
var memberStatuses = await accountEvents.GetStatuses(
|
||||
members.Select(m => m.AccountId).ToList()
|
||||
);
|
||||
|
||||
@@ -306,8 +295,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -323,8 +312,8 @@ public class RealmController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult> LeaveRealm(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
var accountId = currentUser.Id;
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
@@ -339,19 +328,16 @@ public class RealmController(
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.leave",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.leave",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "leaver_id", Value.ForString(currentUser.Id) }
|
||||
{ "realm_id", member.RealmId.ToString() },
|
||||
{ "account_id", member.AccountId.ToString() },
|
||||
{ "leaver_id", currentUser.Id }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -371,7 +357,7 @@ public class RealmController(
|
||||
[Authorize]
|
||||
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.Slug)) return BadRequest("You cannot create a realm without a slug.");
|
||||
|
||||
@@ -383,7 +369,7 @@ public class RealmController(
|
||||
Name = request.Name!,
|
||||
Slug = request.Slug!,
|
||||
Description = request.Description!,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
AccountId = currentUser.Id,
|
||||
IsCommunity = request.IsCommunity ?? false,
|
||||
IsPublic = request.IsPublic ?? false,
|
||||
Members = new List<SnRealmMember>
|
||||
@@ -391,7 +377,7 @@ public class RealmController(
|
||||
new()
|
||||
{
|
||||
Role = RealmMemberRole.Owner,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
AccountId = currentUser.Id,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
}
|
||||
}
|
||||
@@ -414,21 +400,18 @@ public class RealmController(
|
||||
db.Realms.Add(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.create",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.create",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "name", Value.ForString(realm.Name) },
|
||||
{ "slug", Value.ForString(realm.Slug) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
||||
{ "is_public", Value.ForBool(realm.IsPublic) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "name", realm.Name },
|
||||
{ "slug", realm.Slug },
|
||||
{ "is_community", realm.IsCommunity },
|
||||
{ "is_public", realm.IsPublic }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
var realmResourceId = $"realm:{realm.Id}";
|
||||
|
||||
@@ -459,14 +442,14 @@ public class RealmController(
|
||||
[Authorize]
|
||||
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
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var accountId = currentUser.Id;
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
@@ -542,24 +525,21 @@ public class RealmController(
|
||||
db.Realms.Update(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.update",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.update",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "name_updated", Value.ForBool(request.Name != null) },
|
||||
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
||||
{ "description_updated", Value.ForBool(request.Description != null) },
|
||||
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
||||
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
||||
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
||||
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "name_updated", request.Name != null },
|
||||
{ "slug_updated", request.Slug != null },
|
||||
{ "description_updated", request.Description != null },
|
||||
{ "picture_updated", request.PictureId != null },
|
||||
{ "background_updated", request.BackgroundId != null },
|
||||
{ "is_community_updated", request.IsCommunity != null },
|
||||
{ "is_public_updated", request.IsPublic != null }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(realm);
|
||||
}
|
||||
@@ -568,7 +548,7 @@ public class RealmController(
|
||||
[Authorize]
|
||||
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
|
||||
.Where(r => r.Slug == slug)
|
||||
@@ -579,7 +559,7 @@ public class RealmController(
|
||||
return StatusCode(403, "Only community realms can be joined without invitation.");
|
||||
|
||||
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();
|
||||
if (existingMember is not null)
|
||||
{
|
||||
@@ -592,26 +572,23 @@ public class RealmController(
|
||||
db.Update(existingMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.join",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(currentUser.Id) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||
{ "realm_id", existingMember.RealmId.ToString() },
|
||||
{ "account_id", currentUser.Id },
|
||||
{ "is_community", realm.IsCommunity }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(existingMember);
|
||||
}
|
||||
|
||||
var member = new SnRealmMember
|
||||
{
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
AccountId = currentUser.Id,
|
||||
RealmId = realm.Id,
|
||||
Role = RealmMemberRole.Normal,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
@@ -620,19 +597,16 @@ public class RealmController(
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.join",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(currentUser.Id) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "account_id", currentUser.Id },
|
||||
{ "is_community", realm.IsCommunity }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
@@ -641,7 +615,7 @@ public class RealmController(
|
||||
[Authorize]
|
||||
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
|
||||
.Where(r => r.Slug == slug)
|
||||
@@ -653,25 +627,22 @@ public class RealmController(
|
||||
.FirstOrDefaultAsync();
|
||||
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.");
|
||||
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.kick",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.kick",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||
{ "kicker_id", Value.ForString(currentUser.Id) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "account_id", memberId.ToString() },
|
||||
{ "kicker_id", currentUser.Id }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -681,7 +652,7 @@ public class RealmController(
|
||||
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 (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
@@ -693,7 +664,7 @@ public class RealmController(
|
||||
.FirstOrDefaultAsync();
|
||||
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))
|
||||
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);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.members.role_update",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.members.role_update",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||
{ "new_role", Value.ForNumber(newRole) },
|
||||
{ "updater_id", Value.ForString(currentUser.Id) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "account_id", memberId.ToString() },
|
||||
{ "new_role", newRole },
|
||||
{ "updater_id", currentUser.Id }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
@@ -723,7 +691,7 @@ public class RealmController(
|
||||
[Authorize]
|
||||
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();
|
||||
|
||||
@@ -732,16 +700,11 @@ public class RealmController(
|
||||
.FirstOrDefaultAsync();
|
||||
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.");
|
||||
|
||||
try
|
||||
{
|
||||
var chats = await db.ChatRooms
|
||||
.Where(c => c.RealmId == realm.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
db.Realms.Remove(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
@@ -749,15 +712,6 @@ public class RealmController(
|
||||
await db.RealmMembers
|
||||
.Where(m => m.RealmId == realm.Id)
|
||||
.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 transaction.CommitAsync();
|
||||
}
|
||||
@@ -767,19 +721,16 @@ public class RealmController(
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
als.CreateActionLogFromRequest(
|
||||
"realms.delete",
|
||||
new Dictionary<string, object>()
|
||||
{
|
||||
Action = "realms.delete",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "realm_name", Value.ForString(realm.Name) },
|
||||
{ "realm_slug", Value.ForString(realm.Slug) }
|
||||
{ "realm_id", realm.Id.ToString() },
|
||||
{ "realm_name", realm.Name },
|
||||
{ "realm_slug", realm.Slug }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
Request
|
||||
);
|
||||
|
||||
// Delete all file references for this realm
|
||||
var realmResourceId = $"realm:{realm.Id}";
|
||||
@@ -1,20 +1,18 @@
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Shared;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
namespace DysonNetwork.Pass.Realm;
|
||||
|
||||
public class RealmService(
|
||||
AppDatabase db,
|
||||
RingService.RingServiceClient pusher,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
AccountClientHelper accountsHelper,
|
||||
ICacheService cache
|
||||
)
|
||||
{
|
||||
@@ -42,13 +40,18 @@ public class RealmService(
|
||||
|
||||
public async Task SendInviteNotify(SnRealmMember member)
|
||||
{
|
||||
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
||||
CultureService.SetCultureInfo(account);
|
||||
var account = await db.Accounts
|
||||
.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(
|
||||
new SendPushNotificationToUserRequest
|
||||
{
|
||||
UserId = account.Id,
|
||||
UserId = account.Id.ToString(),
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "invites.realms",
|
||||
@@ -75,20 +78,26 @@ public class RealmService(
|
||||
|
||||
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
|
||||
{
|
||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
||||
member.Account = SnAccount.FromProtoValue(account);
|
||||
var account = await db.Accounts
|
||||
.Include(a => a.Profile)
|
||||
.FirstOrDefaultAsync(a => a.Id == member.AccountId);
|
||||
if (account != null)
|
||||
member.Account = account;
|
||||
return member;
|
||||
}
|
||||
|
||||
public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members)
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = SnAccount.FromProtoValue(account);
|
||||
if (accountsDict.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = account;
|
||||
return m;
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<value>{0} {1} removed from your wallet</value>
|
||||
</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>
|
||||
@@ -99,4 +99,10 @@
|
||||
<data name="TransactionNewBodyMinus" xml:space="preserve">
|
||||
<value>{0} {1} 从您的钱包移除</value>
|
||||
</data>
|
||||
<data name="GiftClaimedTitle" xml:space="preserve">
|
||||
<value>有人领取了你的礼物</value>
|
||||
</data>
|
||||
<data name="GiftClaimedBody" xml:space="preserve">
|
||||
<value>你的礼物 {0} 已被 {1} 领取</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -3,9 +3,9 @@ using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Credit;
|
||||
using DysonNetwork.Pass.Leveling;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using DysonNetwork.Pass.Realm;
|
||||
using DysonNetwork.Pass.Wallet;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using Prometheus;
|
||||
|
||||
namespace DysonNetwork.Pass.Startup;
|
||||
|
||||
@@ -13,7 +13,6 @@ public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapMetrics();
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
@@ -21,7 +20,6 @@ public static class ApplicationConfiguration
|
||||
app.ConfigureForwardedHeaders(configuration);
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
@@ -42,6 +40,8 @@ public static class ApplicationConfiguration
|
||||
app.MapGrpcService<BotAccountReceiverGrpc>();
|
||||
app.MapGrpcService<WalletServiceGrpc>();
|
||||
app.MapGrpcService<PaymentServiceGrpc>();
|
||||
app.MapGrpcService<RealmServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -104,9 +104,33 @@ public class BroadcastEventHandler(
|
||||
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
||||
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
|
||||
{
|
||||
// Not a subscription or gift order, skip
|
||||
// Not a subscription, gift, or lottery order, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ public static class ScheduledJobsConfiguration
|
||||
.WithIntervalInHours(1)
|
||||
.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);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using DysonNetwork.Pass.Credit;
|
||||
using DysonNetwork.Pass.Handlers;
|
||||
using DysonNetwork.Pass.Leveling;
|
||||
using DysonNetwork.Pass.Mailer;
|
||||
using DysonNetwork.Pass.Realm;
|
||||
using DysonNetwork.Pass.Safety;
|
||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
@@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.AddRingService();
|
||||
|
||||
@@ -91,19 +93,6 @@ public static class ServiceCollectionExtensions
|
||||
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)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
@@ -152,6 +141,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<SafetyService>();
|
||||
services.AddScoped<SocialCreditService>();
|
||||
services.AddScoped<ExperienceService>();
|
||||
services.AddScoped<RealmService>();
|
||||
services.AddScoped<Lotteries.LotteryService>();
|
||||
|
||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||
services.AddScoped<OidcProviderService>();
|
||||
|
||||
@@ -6,7 +6,6 @@ using Quartz;
|
||||
namespace DysonNetwork.Pass.Wallet;
|
||||
|
||||
public class FundExpirationJob(
|
||||
AppDatabase db,
|
||||
PaymentService paymentService,
|
||||
ILogger<FundExpirationJob> logger
|
||||
) : IJob
|
||||
|
||||
@@ -197,7 +197,8 @@ public class SubscriptionGiftController(
|
||||
|
||||
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;
|
||||
|
||||
@@ -250,6 +250,14 @@ public class SubscriptionService(
|
||||
: null;
|
||||
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(
|
||||
null,
|
||||
subscriptionInfo.Currency,
|
||||
@@ -684,6 +692,9 @@ public class SubscriptionService(
|
||||
if (now > gift.ExpiresAt)
|
||||
throw new InvalidOperationException("Gift has expired.");
|
||||
|
||||
if (gift.GifterId == redeemer.Id)
|
||||
throw new InvalidOperationException("You cannot redeem your own gift.");
|
||||
|
||||
// Validate redeemer permissions
|
||||
if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id)
|
||||
throw new InvalidOperationException("This gift is not intended for you.");
|
||||
@@ -972,7 +983,7 @@ public class SubscriptionService(
|
||||
{
|
||||
Topic = "gifts.claimed",
|
||||
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>
|
||||
{
|
||||
["gift_id"] = gift.Id.ToString(),
|
||||
|
||||
@@ -71,12 +71,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KnownProxies": ["127.0.0.1", "::1"],
|
||||
"Service": {
|
||||
"Name": "DysonNetwork.Pass",
|
||||
"Url": "https://localhost:7058"
|
||||
},
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
}
|
||||
"KnownProxies": ["127.0.0.1", "::1"]
|
||||
}
|
||||
|
||||
@@ -17,42 +17,52 @@ public class WebSocketController(
|
||||
INatsConnection nats
|
||||
) : ControllerBase
|
||||
{
|
||||
private static readonly List<string> AllowedDeviceAlternative = ["watch"];
|
||||
|
||||
[Route("/ws")]
|
||||
[Authorize]
|
||||
[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("CurrentSession", out var currentSessionValue);
|
||||
if (currentUserValue is not Account currentUser ||
|
||||
currentSessionValue is not AuthSession currentSession)
|
||||
if (
|
||||
currentUserValue is not Account currentUser
|
||||
|| currentSessionValue is not AuthSession currentSession
|
||||
)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id!);
|
||||
var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
return BadRequest("Unable to get device ID from session.");
|
||||
if (deviceAlt is not null)
|
||||
deviceId = $"{deviceId}+{deviceAlt}";
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext
|
||||
{ KeepAliveInterval = TimeSpan.FromSeconds(60) });
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(
|
||||
new WebSocketAcceptContext { KeepAliveInterval = TimeSpan.FromSeconds(60) }
|
||||
);
|
||||
var cts = new CancellationTokenSource();
|
||||
var connectionKey = (accountId, deviceId);
|
||||
|
||||
if (!ws.TryAdd(connectionKey, webSocket, cts))
|
||||
{
|
||||
await webSocket.SendAsync(
|
||||
new ArraySegment<byte>(new WebSocketPacket
|
||||
new ArraySegment<byte>(
|
||||
new WebSocketPacket
|
||||
{
|
||||
Type = "error.dupe",
|
||||
ErrorMessage = "Too many connections from the same device and account."
|
||||
}.ToBytes()),
|
||||
ErrorMessage = "Too many connections from the same device and account.",
|
||||
}.ToBytes()
|
||||
),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
@@ -62,21 +72,26 @@ public class WebSocketController(
|
||||
"Too many connections from the same device and account.",
|
||||
CancellationToken.None
|
||||
);
|
||||
return;
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
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
|
||||
await nats.PublishAsync(
|
||||
WebSocketConnectedEvent.Type,
|
||||
GrpcTypeHelper.ConvertObjectToByteString(new WebSocketConnectedEvent
|
||||
GrpcTypeHelper
|
||||
.ConvertObjectToByteString(
|
||||
new WebSocketConnectedEvent
|
||||
{
|
||||
AccountId = accountId,
|
||||
DeviceId = deviceId,
|
||||
IsOffline = false
|
||||
}).ToByteArray(),
|
||||
IsOffline = false,
|
||||
}
|
||||
)
|
||||
.ToByteArray(),
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
|
||||
@@ -84,7 +99,11 @@ public class WebSocketController(
|
||||
{
|
||||
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(
|
||||
"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)
|
||||
{
|
||||
logger.LogError(ex,
|
||||
logger.LogError(
|
||||
ex,
|
||||
"WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly",
|
||||
currentUser.Name,
|
||||
currentUser.Id,
|
||||
@@ -109,12 +129,16 @@ public class WebSocketController(
|
||||
// Broadcast WebSocket disconnected event
|
||||
await nats.PublishAsync(
|
||||
WebSocketDisconnectedEvent.Type,
|
||||
GrpcTypeHelper.ConvertObjectToByteString(new WebSocketDisconnectedEvent
|
||||
GrpcTypeHelper
|
||||
.ConvertObjectToByteString(
|
||||
new WebSocketDisconnectedEvent
|
||||
{
|
||||
AccountId = accountId,
|
||||
DeviceId = deviceId,
|
||||
IsOffline = !WebSocketService.GetAccountIsConnected(accountId)
|
||||
}).ToByteArray(),
|
||||
IsOffline = !WebSocketService.GetAccountIsConnected(accountId),
|
||||
}
|
||||
)
|
||||
.ToByteArray(),
|
||||
cancellationToken: cts.Token
|
||||
);
|
||||
|
||||
@@ -122,6 +146,8 @@ public class WebSocketController(
|
||||
$"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}"
|
||||
);
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private async Task _ConnectionEventLoop(
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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.PostgreSql" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
@@ -31,8 +31,8 @@
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="Quartz" 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.SwaggerUI" Version="9.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,6 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
||||
|
||||
// Add application services
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddDysonAuth();
|
||||
builder.Services.AddAccountService();
|
||||
@@ -45,6 +44,6 @@ app.ConfigureAppMiddleware(builder.Configuration);
|
||||
// Configure gRPC
|
||||
app.ConfigureGrpcServices();
|
||||
|
||||
app.UseSwaggerManifest();
|
||||
app.UseSwaggerManifest("DysonNetwork.Ring");
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -12,7 +12,6 @@ public static class ApplicationConfiguration
|
||||
app.ConfigureForwardedHeaders(configuration);
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -24,6 +23,7 @@ public static class ApplicationConfiguration
|
||||
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
||||
{
|
||||
app.MapGrpcService<RingServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -30,9 +30,7 @@ public static class ServiceCollectionExtensions
|
||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||
});
|
||||
|
||||
// Register gRPC reflection for service discovery
|
||||
services.AddGrpc();
|
||||
services.AddGrpcReflection();
|
||||
|
||||
// Register gRPC services
|
||||
services.AddScoped<RingServiceGrpc>();
|
||||
@@ -50,19 +48,6 @@ public static class ServiceCollectionExtensions
|
||||
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)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.32.1" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.33.0" />
|
||||
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||
<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.Tools" Version="2.72.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -20,32 +21,32 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||
<PackageReference Include="NATS.Net" Version="2.6.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageReference Include="NATS.Net" Version="2.6.11" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||
|
||||
<PackageReference Include="Aspire.NATS.Net" Version="9.4.2" />
|
||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.4.2" />
|
||||
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.4.2" />
|
||||
<PackageReference Include="Aspire.NATS.Net" Version="9.5.2" />
|
||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.2" />
|
||||
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.5.2" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2"/>
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.2" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -56,7 +56,7 @@ public static class SwaggerGen
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication UseSwaggerManifest(this WebApplication app)
|
||||
public static WebApplication UseSwaggerManifest(this WebApplication app, string serviceName)
|
||||
{
|
||||
app.MapOpenApi();
|
||||
|
||||
@@ -103,7 +103,7 @@ public static class SwaggerGen
|
||||
var publicBasePath = configuration["Swagger:PublicBasePath"]?.TrimEnd('/') ?? "";
|
||||
options.SwaggerEndpoint(
|
||||
$"{publicBasePath}/swagger/v1/swagger.json",
|
||||
"Develop API v1");
|
||||
$"{serviceName} API v1");
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
@@ -148,6 +148,32 @@ public class UsernameColor
|
||||
public string? Value { get; set; } // e.g. "red" or "#ff6600"
|
||||
public string? Direction { get; set; } // e.g. "to right"
|
||||
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
|
||||
@@ -218,6 +244,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
||||
AccountId = AccountId.ToString(),
|
||||
Verification = Verification?.ToProtoValue(),
|
||||
ActiveBadge = ActiveBadge?.ToProtoValue(),
|
||||
UsernameColor = UsernameColor?.ToProtoValue(),
|
||||
CreatedAt = CreatedAt.ToTimestamp(),
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
@@ -247,6 +274,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
||||
Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
|
||||
Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
|
||||
AccountId = Guid.Parse(proto.AccountId),
|
||||
UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null,
|
||||
CreatedAt = proto.CreatedAt.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 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? Background { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
|
||||
|
||||
public Guid? RealmId { get; set; }
|
||||
public SnRealm? Realm { get; set; }
|
||||
[NotMapped] public SnRealm? Realm { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
[JsonPropertyName("members")]
|
||||
|
||||
@@ -6,7 +6,7 @@ using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public class WalletCurrency
|
||||
public abstract class WalletCurrency
|
||||
{
|
||||
public const string SourcePoint = "points";
|
||||
public const string GoldenPoint = "golds";
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using NodaTime;
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum PostType
|
||||
{
|
||||
Moment,
|
||||
Article
|
||||
Article,
|
||||
}
|
||||
|
||||
public enum PostVisibility
|
||||
@@ -17,7 +18,7 @@ public enum PostVisibility
|
||||
Public,
|
||||
Friends,
|
||||
Unlisted,
|
||||
Private
|
||||
Private,
|
||||
}
|
||||
|
||||
public enum PostPinMode
|
||||
@@ -27,12 +28,18 @@ public enum PostPinMode
|
||||
ReplyPage,
|
||||
}
|
||||
|
||||
public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
||||
public class SnPost : ModelBase, IIdentifiedResource, ITimelineEvent
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(1024)] public string? Title { get; set; }
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
[MaxLength(1024)] public string? Slug { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
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? PublishedAt { get; set; }
|
||||
public PostVisibility Visibility { get; set; } = PostVisibility.Public;
|
||||
@@ -42,18 +49,30 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
||||
|
||||
public PostType Type { 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")] public PostEmbedView? EmbedView { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
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 ViewsTotal { get; set; }
|
||||
public int Upvotes { get; set; }
|
||||
public int Downvotes { get; set; }
|
||||
public decimal AwardedScore { get; set; }
|
||||
[NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new();
|
||||
[NotMapped] public int RepliesCount { get; set; }
|
||||
[NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
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 ForwardedGone { get; set; }
|
||||
@@ -64,29 +83,226 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
||||
public SnPost? ForwardedPost { get; set; }
|
||||
|
||||
public Guid? RealmId { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public SnRealm? Realm { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
||||
|
||||
[JsonIgnore] public NpgsqlTsVector SearchVector { get; set; } = null!;
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
||||
|
||||
public Guid PublisherId { get; set; }
|
||||
public SnPublisher Publisher { get; set; } = null!;
|
||||
|
||||
public ICollection<SnPostAward> Awards { get; set; } = null!;
|
||||
[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>();
|
||||
public List<SnPostAward> Awards { get; set; } = [];
|
||||
|
||||
[JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
|
||||
[NotMapped] public bool IsTruncated { get; set; } = false;
|
||||
[JsonIgnore]
|
||||
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 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,
|
||||
UpdatedAt = UpdatedAt,
|
||||
@@ -94,7 +310,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
||||
Id = Id,
|
||||
Type = RepliedPostId is null ? "posts.new" : "posts.new.replies",
|
||||
ResourceIdentifier = ResourceIdentifier,
|
||||
Data = this
|
||||
Data = this,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -102,21 +318,83 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
||||
public class SnPostTag : ModelBase
|
||||
{
|
||||
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 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
|
||||
@@ -133,23 +411,64 @@ public class SnPostCategorySubscription : ModelBase
|
||||
public class SnPostCollection : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(128)] public string Slug { get; set; } = null!;
|
||||
[MaxLength(256)] public string? Name { get; set; }
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
|
||||
[MaxLength(128)]
|
||||
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 ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
|
||||
public List<SnPost> Posts { get; set; } = new List<SnPost>();
|
||||
}
|
||||
|
||||
public class SnPostFeaturedRecord : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid PostId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public SnPost Post { get; set; } = null!;
|
||||
public Instant? FeaturedAt { 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
|
||||
@@ -162,12 +481,54 @@ public enum PostReactionAttitude
|
||||
public class SnPostReaction : ModelBase
|
||||
{
|
||||
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 Guid PostId { get; set; }
|
||||
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public SnPost Post { get; set; } = null!;
|
||||
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
|
||||
@@ -175,11 +536,32 @@ public class SnPostAward : ModelBase
|
||||
public Guid Id { get; set; }
|
||||
public decimal Amount { 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; }
|
||||
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public SnPost Post { get; set; } = null!;
|
||||
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>
|
||||
@@ -192,9 +574,34 @@ public class PostEmbedView
|
||||
public string Uri { get; set; } = null!;
|
||||
public double? AspectRatio { get; set; }
|
||||
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
|
||||
{
|
||||
WebView
|
||||
WebView,
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
||||
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
[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? Background { get; set; }
|
||||
|
||||
@@ -42,12 +38,12 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
||||
|
||||
public Guid? AccountId { 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; }
|
||||
|
||||
public string ResourceIdentifier => $"publisher:{Id}";
|
||||
|
||||
public static SnPublisher FromProto(Proto.Publisher proto)
|
||||
public static SnPublisher FromProtoValue(Proto.Publisher proto)
|
||||
{
|
||||
var publisher = new SnPublisher
|
||||
{
|
||||
@@ -89,7 +85,7 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public Proto.Publisher ToProto()
|
||||
public Proto.Publisher ToProtoValue()
|
||||
{
|
||||
var p = new Proto.Publisher()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
@@ -16,21 +18,41 @@ public class SnRealm : ModelBase, IIdentifiedResource
|
||||
public bool IsCommunity { 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? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
|
||||
|
||||
[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 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
|
||||
@@ -51,4 +73,40 @@ public class SnRealmMember : ModelBase
|
||||
public int Role { get; set; } = RealmMemberRole.Normal;
|
||||
public Instant? JoinedAt { 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
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -66,13 +67,15 @@ public abstract class GrpcTypeHelper
|
||||
try
|
||||
{
|
||||
// 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
|
||||
{
|
||||
// Fallback to raw string
|
||||
result[kvp.Key] = value.StringValue;
|
||||
}
|
||||
|
||||
break;
|
||||
case Value.KindOneofCase.NumberValue:
|
||||
result[kvp.Key] = value.NumberValue;
|
||||
@@ -106,6 +109,7 @@ public abstract class GrpcTypeHelper
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -117,7 +121,8 @@ public abstract class GrpcTypeHelper
|
||||
Value.KindOneofCase.NumberValue => value.NumberValue,
|
||||
Value.KindOneofCase.BoolValue => value.BoolValue,
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
message AccountProfile {
|
||||
string id = 1;
|
||||
@@ -89,6 +96,7 @@ message AccountProfile {
|
||||
|
||||
google.protobuf.Timestamp created_at = 22;
|
||||
google.protobuf.Timestamp updated_at = 23;
|
||||
optional UsernameColor username_color = 24;
|
||||
}
|
||||
|
||||
// AccountContact represents a contact method for an account
|
||||
@@ -254,6 +262,7 @@ service AccountService {
|
||||
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc SearchAccount(SearchAccountRequest) returns (GetAccountBatchResponse) {}
|
||||
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
|
||||
|
||||
rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {}
|
||||
@@ -343,6 +352,10 @@ message LookupAccountBatchRequest {
|
||||
repeated string names = 1;
|
||||
}
|
||||
|
||||
message SearchAccountRequest {
|
||||
string query = 1;
|
||||
}
|
||||
|
||||
message GetAccountBatchResponse {
|
||||
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