Compare commits
25 Commits
15687a0c32
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
<PackageReference Include="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" Version="3.2.2"/>
|
||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class BotAccountController(
|
|||||||
DeveloperService ds,
|
DeveloperService ds,
|
||||||
DevProjectService projectService,
|
DevProjectService projectService,
|
||||||
ILogger<BotAccountController> logger,
|
ILogger<BotAccountController> logger,
|
||||||
AccountClientHelper accounts,
|
RemoteAccountService remoteAccounts,
|
||||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
|
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
|
||||||
)
|
)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
@@ -222,7 +222,7 @@ public class BotAccountController(
|
|||||||
if (bot is null || bot.ProjectId != projectId)
|
if (bot is null || bot.ProjectId != projectId)
|
||||||
return NotFound("Bot not found");
|
return NotFound("Bot not found");
|
||||||
|
|
||||||
var botAccount = await accounts.GetBotAccount(bot.Id);
|
var botAccount = await remoteAccounts.GetBotAccount(bot.Id);
|
||||||
|
|
||||||
if (request.Name is not null) botAccount.Name = request.Name;
|
if (request.Name is not null) botAccount.Name = request.Name;
|
||||||
if (request.Nick is not null) botAccount.Nick = request.Nick;
|
if (request.Nick is not null) botAccount.Nick = request.Nick;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace DysonNetwork.Develop.Identity;
|
|||||||
public class BotAccountService(
|
public class BotAccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
|
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
|
||||||
AccountClientHelper accounts
|
RemoteAccountService remoteAccounts
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
|
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
|
||||||
@@ -158,7 +158,7 @@ public class BotAccountService(
|
|||||||
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
|
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
|
||||||
{
|
{
|
||||||
var automatedIds = bots.Select(b => b.Id).ToList();
|
var automatedIds = bots.Select(b => b.Id).ToList();
|
||||||
var data = await accounts.GetBotAccountBatch(automatedIds);
|
var data = await remoteAccounts.GetBotAccountBatch(automatedIds);
|
||||||
|
|
||||||
foreach (var bot in bots)
|
foreach (var bot in bots)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ using (var scope = app.Services.CreateScope())
|
|||||||
|
|
||||||
app.ConfigureAppMiddleware(builder.Configuration);
|
app.ConfigureAppMiddleware(builder.Configuration);
|
||||||
|
|
||||||
app.UseSwaggerManifest();
|
app.UseSwaggerManifest("DysonNetwork.Develop");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.AspNetCore" Version="1.13.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" 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" Version="8.2.1" />
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" 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" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV
|
|||||||
// Add application services
|
// Add application services
|
||||||
|
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddDysonAuth();
|
builder.Services.AddDysonAuth();
|
||||||
builder.Services.AddAccountService();
|
builder.Services.AddAccountService();
|
||||||
@@ -49,6 +48,6 @@ app.ConfigureAppMiddleware(tusDiskStore);
|
|||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
app.ConfigureGrpcServices();
|
app.ConfigureGrpcServices();
|
||||||
|
|
||||||
app.UseSwaggerManifest();
|
app.UseSwaggerManifest("DysonNetwork.Drive");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -43,19 +43,6 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
|
||||||
{
|
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
|
||||||
opts.PermitLimit = 120;
|
|
||||||
opts.QueueLimit = 2;
|
|
||||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ var apiRoutes = serviceNames.Select(serviceName =>
|
|||||||
{
|
{
|
||||||
var apiPath = serviceName switch
|
var apiPath = serviceName switch
|
||||||
{
|
{
|
||||||
"pass" => "/id",
|
|
||||||
_ => $"/{serviceName}"
|
_ => $"/{serviceName}"
|
||||||
};
|
};
|
||||||
return new RouteConfig
|
return new RouteConfig
|
||||||
@@ -162,8 +161,6 @@ app.UseForwardedHeaders(forwardedHeadersOptions);
|
|||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
|
||||||
app.UseRateLimiter();
|
|
||||||
|
|
||||||
app.MapReverseProxy().RequireRateLimiting("fixed");
|
app.MapReverseProxy().RequireRateLimiting("fixed");
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class AccountCurrentController(
|
|||||||
[MaxLength(1024)] public string? TimeZone { get; set; }
|
[MaxLength(1024)] public string? TimeZone { get; set; }
|
||||||
[MaxLength(1024)] public string? Location { get; set; }
|
[MaxLength(1024)] public string? Location { get; set; }
|
||||||
[MaxLength(4096)] public string? Bio { get; set; }
|
[MaxLength(4096)] public string? Bio { get; set; }
|
||||||
public UsernameColor? UsernameColor { get; set; }
|
public Shared.Models.UsernameColor? UsernameColor { get; set; }
|
||||||
public Instant? Birthday { get; set; }
|
public Instant? Birthday { get; set; }
|
||||||
public List<ProfileLink>? Links { get; set; }
|
public List<ProfileLink>? Links { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ public class AccountEventService(
|
|||||||
return backdatedCheckInMonths < 4;
|
return backdatedCheckInMonths < 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string CheckInLockKey = "checkin:lock:";
|
private const string CheckInLockKey = "checkin:lock:";
|
||||||
|
|
||||||
public async Task<SnCheckInResult> CheckInDaily(SnAccount user, Instant? backdated = null)
|
public async Task<SnCheckInResult> CheckInDaily(SnAccount user, Instant? backdated = null)
|
||||||
{
|
{
|
||||||
@@ -322,7 +322,11 @@ public class AccountEventService(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// The 5 is specialized, keep it alone.
|
// The 5 is specialized, keep it alone.
|
||||||
var checkInLevel = (CheckInResultLevel)Random.Next(Enum.GetValues<CheckInResultLevel>().Length - 1);
|
var sum = 0;
|
||||||
|
var maxLevel = Enum.GetValues<CheckInResultLevel>().Length - 1;
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
sum += Random.Next(maxLevel);
|
||||||
|
var checkInLevel = (CheckInResultLevel)(sum / 5);
|
||||||
|
|
||||||
var accountBirthday = await db.AccountProfiles
|
var accountBirthday = await db.AccountProfiles
|
||||||
.Where(x => x.AccountId == user.Id)
|
.Where(x => x.AccountId == user.Id)
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ public class AccountServiceGrpc(
|
|||||||
AccountEventService accountEvents,
|
AccountEventService accountEvents,
|
||||||
RelationshipService relationships,
|
RelationshipService relationships,
|
||||||
SubscriptionService subscriptions,
|
SubscriptionService subscriptions,
|
||||||
IClock clock,
|
|
||||||
ILogger<AccountServiceGrpc> logger
|
ILogger<AccountServiceGrpc> logger
|
||||||
)
|
)
|
||||||
: Shared.Proto.AccountService.AccountServiceBase
|
: Shared.Proto.AccountService.AccountServiceBase
|
||||||
{
|
{
|
||||||
private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db));
|
private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db));
|
||||||
private readonly IClock _clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
|
||||||
|
|
||||||
private readonly ILogger<AccountServiceGrpc>
|
private readonly ILogger<AccountServiceGrpc>
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
@@ -266,7 +264,7 @@ public class AccountServiceGrpc(
|
|||||||
|
|
||||||
public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context)
|
public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
var hasRelationship = false;
|
bool hasRelationship;
|
||||||
if (!request.HasStatus)
|
if (!request.HasStatus)
|
||||||
hasRelationship = await relationships.HasExistingRelationship(
|
hasRelationship = await relationships.HasExistingRelationship(
|
||||||
Guid.Parse(request.AccountId),
|
Guid.Parse(request.AccountId),
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ public class AppDatabase(
|
|||||||
public DbSet<SnAuthClient> AuthClients { get; set; } = null!;
|
public DbSet<SnAuthClient> AuthClients { get; set; } = null!;
|
||||||
public DbSet<SnApiKey> ApiKeys { get; set; } = null!;
|
public DbSet<SnApiKey> ApiKeys { get; set; } = null!;
|
||||||
|
|
||||||
|
public DbSet<SnRealm> Realms { get; set; } = null!;
|
||||||
|
public DbSet<SnRealmMember> RealmMembers { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<SnWallet> Wallets { get; set; } = null!;
|
public DbSet<SnWallet> Wallets { get; set; } = null!;
|
||||||
public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!;
|
public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!;
|
||||||
public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!;
|
public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!;
|
||||||
@@ -54,6 +57,9 @@ public class AppDatabase(
|
|||||||
public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
public DbSet<SnSocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
||||||
public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!;
|
public DbSet<SnExperienceRecord> ExperienceRecords { get; set; } = null!;
|
||||||
|
|
||||||
|
public DbSet<SnLottery> Lotteries { get; set; } = null!;
|
||||||
|
public DbSet<SnLotteryRecord> LotteryRecords { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder.UseNpgsql(
|
optionsBuilder.UseNpgsql(
|
||||||
@@ -128,6 +134,14 @@ public class AppDatabase(
|
|||||||
.WithMany(a => a.IncomingRelationships)
|
.WithMany(a => a.IncomingRelationships)
|
||||||
.HasForeignKey(r => r.RelatedId);
|
.HasForeignKey(r => r.RelatedId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SnRealmMember>()
|
||||||
|
.HasKey(pm => new { pm.RealmId, pm.AccountId });
|
||||||
|
modelBuilder.Entity<SnRealmMember>()
|
||||||
|
.HasOne(pm => pm.Realm)
|
||||||
|
.WithMany(p => p.Members)
|
||||||
|
.HasForeignKey(pm => pm.RealmId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -343,8 +343,8 @@ public class OidcProviderController(
|
|||||||
{
|
{
|
||||||
issuer,
|
issuer,
|
||||||
authorization_endpoint = $"{siteUrl}/auth/authorize",
|
authorization_endpoint = $"{siteUrl}/auth/authorize",
|
||||||
token_endpoint = $"{baseUrl}/id/auth/open/token",
|
token_endpoint = $"{baseUrl}/pass/auth/open/token",
|
||||||
userinfo_endpoint = $"{baseUrl}/id/auth/open/userinfo",
|
userinfo_endpoint = $"{baseUrl}/pass/auth/open/userinfo",
|
||||||
jwks_uri = $"{baseUrl}/.well-known/jwks",
|
jwks_uri = $"{baseUrl}/.well-known/jwks",
|
||||||
scopes_supported = new[] { "openid", "profile", "email" },
|
scopes_supported = new[] { "openid", "profile", "email" },
|
||||||
response_types_supported = new[]
|
response_types_supported = new[]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.AspNetCore" Version="1.13.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0"/>
|
<PackageReference Include="Otp.NET" Version="1.4.0"/>
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
|
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
|
||||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
DysonNetwork.Pass/Lotteries/LotteryService.cs
Normal file
276
DysonNetwork.Pass/Lotteries/LotteryService.cs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
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 readonly ILogger<LotteryService> _logger = logger;
|
||||||
|
|
||||||
|
private static bool ValidateNumbers(List<int> region1, int region2)
|
||||||
|
{
|
||||||
|
if (region1.Count != 5 || region1.Distinct().Count() != 5)
|
||||||
|
return false;
|
||||||
|
if (region1.Any(n => n < 0 || n > 99))
|
||||||
|
return false;
|
||||||
|
if (region2 < 0 || region2 > 99)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnLottery> CreateTicketAsync(Guid accountId, List<int> region1, int region2, int multiplier = 1)
|
||||||
|
{
|
||||||
|
if (!ValidateNumbers(region1, region2))
|
||||||
|
throw new ArgumentException("Invalid lottery numbers");
|
||||||
|
|
||||||
|
var lottery = new SnLottery
|
||||||
|
{
|
||||||
|
AccountId = accountId,
|
||||||
|
RegionOneNumbers = region1,
|
||||||
|
RegionTwoNumber = region2,
|
||||||
|
Multiplier = multiplier
|
||||||
|
};
|
||||||
|
|
||||||
|
db.Lotteries.Add(lottery);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return lottery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnLottery>> GetUserTicketsAsync(Guid accountId, int offset = 0, int limit = 20)
|
||||||
|
{
|
||||||
|
return await db.Lotteries
|
||||||
|
.Where(l => l.AccountId == accountId)
|
||||||
|
.OrderByDescending(l => l.CreatedAt)
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(limit)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnLottery?> GetTicketAsync(Guid id)
|
||||||
|
{
|
||||||
|
return await db.Lotteries.FirstOrDefaultAsync(l => l.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetUserTicketCountAsync(Guid accountId)
|
||||||
|
{
|
||||||
|
return await db.Lotteries.CountAsync(l => l.AccountId == accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal CalculateLotteryPrice(int multiplier)
|
||||||
|
{
|
||||||
|
return 10 + (multiplier - 1) * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnWalletOrder> CreateLotteryOrderAsync(Guid accountId, List<int> region1, int region2,
|
||||||
|
int multiplier = 1)
|
||||||
|
{
|
||||||
|
if (!ValidateNumbers(region1, region2))
|
||||||
|
throw new ArgumentException("Invalid lottery numbers");
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc()
|
||||||
|
.ToInstant();
|
||||||
|
var hasPurchasedToday = await db.Lotteries.AnyAsync(l =>
|
||||||
|
l.AccountId == accountId &&
|
||||||
|
l.CreatedAt >= todayStart &&
|
||||||
|
l.DrawStatus == LotteryDrawStatus.Pending
|
||||||
|
);
|
||||||
|
if (hasPurchasedToday)
|
||||||
|
throw new InvalidOperationException("You can only purchase one lottery per day.");
|
||||||
|
|
||||||
|
var price = CalculateLotteryPrice(multiplier);
|
||||||
|
|
||||||
|
var lotteryData = new LotteryOrderMetaData
|
||||||
|
{
|
||||||
|
AccountId = accountId,
|
||||||
|
RegionOneNumbers = region1,
|
||||||
|
RegionTwoNumber = region2,
|
||||||
|
Multiplier = multiplier
|
||||||
|
};
|
||||||
|
|
||||||
|
return await paymentService.CreateOrderAsync(
|
||||||
|
null,
|
||||||
|
WalletCurrency.SourcePoint,
|
||||||
|
price,
|
||||||
|
appIdentifier: "lottery",
|
||||||
|
productIdentifier: "lottery",
|
||||||
|
meta: new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["data"] = JsonSerializer.Serialize(lotteryData)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleLotteryOrder(SnWalletOrder order)
|
||||||
|
{
|
||||||
|
if (order.Status == OrderStatus.Finished)
|
||||||
|
return; // Already processed
|
||||||
|
|
||||||
|
if (order.Status != OrderStatus.Paid ||
|
||||||
|
!order.Meta.TryGetValue("data", out var dataValue) ||
|
||||||
|
dataValue is null ||
|
||||||
|
dataValue is not JsonElement { ValueKind: JsonValueKind.String } jsonElem)
|
||||||
|
throw new InvalidOperationException("Invalid order.");
|
||||||
|
|
||||||
|
var jsonString = jsonElem.GetString();
|
||||||
|
if (jsonString is null)
|
||||||
|
throw new InvalidOperationException("Invalid order.");
|
||||||
|
|
||||||
|
var data = JsonSerializer.Deserialize<LotteryOrderMetaData>(jsonString);
|
||||||
|
if (data is null)
|
||||||
|
throw new InvalidOperationException("Invalid order data.");
|
||||||
|
|
||||||
|
await CreateTicketAsync(data.AccountId, data.RegionOneNumbers, data.RegionTwoNumber, data.Multiplier);
|
||||||
|
|
||||||
|
order.Status = OrderStatus.Finished;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CalculateReward(int region1Matches, bool region2Match)
|
||||||
|
{
|
||||||
|
var reward = region1Matches switch
|
||||||
|
{
|
||||||
|
0 => 0,
|
||||||
|
1 => 10,
|
||||||
|
2 => 100,
|
||||||
|
3 => 500,
|
||||||
|
4 => 1000,
|
||||||
|
5 => 10000,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
if (region2Match) reward *= 10;
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<int> GenerateUniqueRandomNumbers(int count, int min, int max)
|
||||||
|
{
|
||||||
|
var numbers = new List<int>();
|
||||||
|
var random = new Random();
|
||||||
|
while (numbers.Count < count)
|
||||||
|
{
|
||||||
|
var num = random.Next(min, max + 1);
|
||||||
|
if (!numbers.Contains(num)) numbers.Add(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbers.OrderBy(n => n).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CountMatches(List<int> playerNumbers, List<int> winningNumbers)
|
||||||
|
{
|
||||||
|
return playerNumbers.Intersect(winningNumbers).Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DrawLotteries()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting drawing lotteries...");
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
// All pending lottery tickets
|
||||||
|
var tickets = await db.Lotteries
|
||||||
|
.Where(l => l.DrawStatus == LotteryDrawStatus.Pending)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (tickets.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No pending lottery tickets");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Found {Count} pending lottery tickets for draw", tickets.Count);
|
||||||
|
|
||||||
|
// Generate winning numbers
|
||||||
|
var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99);
|
||||||
|
var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0];
|
||||||
|
|
||||||
|
_logger.LogInformation("Winning numbers generated: Region1 [{Region1}], Region2 [{Region2}]",
|
||||||
|
string.Join(",", winningRegion1), winningRegion2);
|
||||||
|
|
||||||
|
var drawDate = Instant.FromDateTimeUtc(new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month,
|
||||||
|
DateTime.UtcNow.Day, 0, 0, 0, DateTimeKind.Utc).AddDays(-1)); // Yesterday's date
|
||||||
|
|
||||||
|
var totalPrizesAwarded = 0;
|
||||||
|
long totalPrizeAmount = 0;
|
||||||
|
|
||||||
|
// Process each ticket
|
||||||
|
foreach (var ticket in tickets)
|
||||||
|
{
|
||||||
|
var region1Matches = CountMatches(ticket.RegionOneNumbers, winningRegion1);
|
||||||
|
var region2Match = ticket.RegionTwoNumber == winningRegion2;
|
||||||
|
var reward = CalculateReward(region1Matches, region2Match);
|
||||||
|
|
||||||
|
// Record match results
|
||||||
|
ticket.MatchedRegionOneNumbers = ticket.RegionOneNumbers.Intersect(winningRegion1).ToList();
|
||||||
|
ticket.MatchedRegionTwoNumber = region2Match ? (int?)winningRegion2 : null;
|
||||||
|
|
||||||
|
if (reward > 0)
|
||||||
|
{
|
||||||
|
var wallet = await walletService.GetWalletAsync(ticket.AccountId);
|
||||||
|
if (wallet != null)
|
||||||
|
{
|
||||||
|
await paymentService.CreateTransactionAsync(
|
||||||
|
payerWalletId: null,
|
||||||
|
payeeWalletId: wallet.Id,
|
||||||
|
currency: WalletCurrency.SourcePoint,
|
||||||
|
amount: reward,
|
||||||
|
remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}"
|
||||||
|
);
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Awarded {Amount} to account {AccountId} for {Matches} matches{(Special ? \" + special\" : \"\")}",
|
||||||
|
reward, ticket.AccountId, region1Matches, region2Match ? " + special" : "");
|
||||||
|
totalPrizesAwarded++;
|
||||||
|
totalPrizeAmount += reward;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Wallet not found for account {AccountId}, skipping prize award",
|
||||||
|
ticket.AccountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket.DrawStatus = LotteryDrawStatus.Drawn;
|
||||||
|
ticket.DrawDate = drawDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the draw record
|
||||||
|
var lotteryRecord = new SnLotteryRecord
|
||||||
|
{
|
||||||
|
DrawDate = drawDate,
|
||||||
|
WinningRegionOneNumbers = winningRegion1,
|
||||||
|
WinningRegionTwoNumber = winningRegion2,
|
||||||
|
TotalTickets = tickets.Count,
|
||||||
|
TotalPrizesAwarded = totalPrizesAwarded,
|
||||||
|
TotalPrizeAmount = totalPrizeAmount
|
||||||
|
};
|
||||||
|
|
||||||
|
db.LotteryRecords.Add(lotteryRecord);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Daily lottery draw completed: {Prizes} prizes awarded, total amount {Amount}",
|
||||||
|
totalPrizesAwarded, totalPrizeAmount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "An error occurred during the daily lottery draw");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2677
DysonNetwork.Pass/Migrations/20251021153439_AddRealmFromSphere.Designer.cs
generated
Normal file
2677
DysonNetwork.Pass/Migrations/20251021153439_AddRealmFromSphere.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRealmFromSphere : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "realms",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||||
|
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||||
|
is_community = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_public = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_realms", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "realm_members",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
realm_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_realm_members", x => new { x.realm_id, x.account_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_realm_members_realms_realm_id",
|
||||||
|
column: x => x.realm_id,
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sn_chat_room",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
is_community = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_public = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
realm_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_sn_chat_room", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_sn_chat_room_realms_sn_realm_id",
|
||||||
|
column: x => x.sn_realm_id,
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sn_chat_member",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
chat_room_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
notify = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
is_bot = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_sn_chat_member", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_sn_chat_member_sn_chat_room_chat_room_id",
|
||||||
|
column: x => x.chat_room_id,
|
||||||
|
principalTable: "sn_chat_room",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_realms_slug",
|
||||||
|
table: "realms",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_sn_chat_member_chat_room_id",
|
||||||
|
table: "sn_chat_member",
|
||||||
|
column: "chat_room_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_sn_chat_room_sn_realm_id",
|
||||||
|
table: "sn_chat_room",
|
||||||
|
column: "sn_realm_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "realm_members");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sn_chat_member");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sn_chat_room");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "realms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2497
DysonNetwork.Pass/Migrations/20251022164134_RemoveChatRoom.Designer.cs
generated
Normal file
2497
DysonNetwork.Pass/Migrations/20251022164134_RemoveChatRoom.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveChatRoom : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sn_chat_member");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sn_chat_room");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sn_chat_room",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
is_community = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_public = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
realm_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_sn_chat_room", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_sn_chat_room_realms_sn_realm_id",
|
||||||
|
column: x => x.sn_realm_id,
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sn_chat_member",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
chat_room_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
is_bot = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
notify = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true),
|
||||||
|
timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_sn_chat_member", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_sn_chat_member_sn_chat_room_chat_room_id",
|
||||||
|
column: x => x.chat_room_id,
|
||||||
|
principalTable: "sn_chat_room",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_sn_chat_member_chat_room_id",
|
||||||
|
table: "sn_chat_member",
|
||||||
|
column: "chat_room_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_sn_chat_room_sn_realm_id",
|
||||||
|
table: "sn_chat_room",
|
||||||
|
column: "sn_realm_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2612
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
generated
Normal file
2612
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
Normal file
78
DysonNetwork.Pass/Migrations/20251023173204_AddLotteries.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLotteries : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lotteries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
|
||||||
|
region_two_number = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
multiplier = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
draw_status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_lotteries", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_lotteries_accounts_account_id",
|
||||||
|
column: x => x.account_id,
|
||||||
|
principalTable: "accounts",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "lottery_records",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
winning_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
|
||||||
|
winning_region_two_number = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_tickets = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_prizes_awarded = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
total_prize_amount = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_lottery_records", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_lotteries_account_id",
|
||||||
|
table: "lotteries",
|
||||||
|
column: "account_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lotteries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "lottery_records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2620
DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
generated
Normal file
2620
DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDetailLotteriesStatus : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<List<int>>(
|
||||||
|
name: "matched_region_one_numbers",
|
||||||
|
table: "lotteries",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "matched_region_two_number",
|
||||||
|
table: "lotteries",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "matched_region_one_numbers",
|
||||||
|
table: "lotteries");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "matched_region_two_number",
|
||||||
|
table: "lotteries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.7")
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -1059,6 +1059,117 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.ToTable("experience_records", (string)null);
|
b.ToTable("experience_records", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DrawDate")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("draw_date");
|
||||||
|
|
||||||
|
b.Property<int>("DrawStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("draw_status");
|
||||||
|
|
||||||
|
b.Property<List<int>>("MatchedRegionOneNumbers")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("matched_region_one_numbers");
|
||||||
|
|
||||||
|
b.Property<int?>("MatchedRegionTwoNumber")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("matched_region_two_number");
|
||||||
|
|
||||||
|
b.Property<int>("Multiplier")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("multiplier");
|
||||||
|
|
||||||
|
b.Property<List<int>>("RegionOneNumbers")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("region_one_numbers");
|
||||||
|
|
||||||
|
b.Property<int>("RegionTwoNumber")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("region_two_number");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_lotteries");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.HasDatabaseName("ix_lotteries_account_id");
|
||||||
|
|
||||||
|
b.ToTable("lotteries", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant>("DrawDate")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("draw_date");
|
||||||
|
|
||||||
|
b.Property<long>("TotalPrizeAmount")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("total_prize_amount");
|
||||||
|
|
||||||
|
b.Property<int>("TotalPrizesAwarded")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total_prizes_awarded");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTickets")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("total_tickets");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<List<int>>("WinningRegionOneNumbers")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("winning_region_one_numbers");
|
||||||
|
|
||||||
|
b.Property<int>("WinningRegionTwoNumber")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("winning_region_two_number");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_lottery_records");
|
||||||
|
|
||||||
|
b.ToTable("lottery_records", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1252,6 +1363,127 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.ToTable("permission_nodes", (string)null);
|
b.ToTable("permission_nodes", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Background")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("background");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("background_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCommunity")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_community");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPublic")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_public");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("picture");
|
||||||
|
|
||||||
|
b.Property<string>("PictureId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("picture_id");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<SnVerificationMark>("Verification")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("verification");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_realms");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_realms_slug");
|
||||||
|
|
||||||
|
b.ToTable("realms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RealmId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("realm_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("JoinedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("joined_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LeaveAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("leave_at");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("RealmId", "AccountId")
|
||||||
|
.HasName("pk_realm_members");
|
||||||
|
|
||||||
|
b.ToTable("realm_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2113,6 +2345,18 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.Navigation("Account");
|
b.Navigation("Account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_lotteries_accounts_account_id");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||||
@@ -2145,6 +2389,18 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.Navigation("Group");
|
b.Navigation("Group");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("RealmId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_realm_members_realms_realm_id");
|
||||||
|
|
||||||
|
b.Navigation("Realm");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
|
||||||
@@ -2336,6 +2592,11 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
b.Navigation("Nodes");
|
b.Navigation("Nodes");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Pockets");
|
b.Navigation("Pockets");
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
|||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddRingService();
|
builder.Services.AddRingService();
|
||||||
builder.Services.AddDriveService();
|
builder.Services.AddDriveService();
|
||||||
@@ -52,6 +51,6 @@ app.ConfigureAppMiddleware(builder.Configuration);
|
|||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
app.ConfigureGrpcServices();
|
app.ConfigureGrpcServices();
|
||||||
|
|
||||||
app.UseSwaggerManifest();
|
app.UseSwaggerManifest("DysonNetwork.Pass");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||||
using DysonNetwork.Shared.Models;
|
using ActionLogService = DysonNetwork.Pass.Account.ActionLogService;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
namespace DysonNetwork.Pass.Realm;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/realms")]
|
[Route("/api/realms")]
|
||||||
@@ -17,9 +20,9 @@ public class RealmController(
|
|||||||
RealmService rs,
|
RealmService rs,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
ActionLogService.ActionLogServiceClient als,
|
ActionLogService als,
|
||||||
AccountService.AccountServiceClient accounts,
|
RelationshipService rels,
|
||||||
AccountClientHelper accountsHelper
|
AccountEventService accountEvents
|
||||||
) : Controller
|
) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("{slug}")]
|
[HttpGet("{slug}")]
|
||||||
@@ -37,8 +40,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms()
|
public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
var members = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -54,8 +57,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<SnRealmMember>>> ListInvites()
|
public async Task<ActionResult<List<SnRealmMember>>> ListInvites()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var members = await db.RealmMembers
|
var members = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -77,20 +80,18 @@ public class RealmController(
|
|||||||
public async Task<ActionResult<SnRealmMember>> InviteMember(string slug,
|
public async Task<ActionResult<SnRealmMember>> InviteMember(string slug,
|
||||||
[FromBody] RealmMemberRequest request)
|
[FromBody] RealmMemberRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var relatedUser =
|
var relatedUser = await db.Accounts.Where(a => a.Id == request.RelatedUserId).FirstOrDefaultAsync();
|
||||||
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
|
||||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||||
|
|
||||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
var hasBlocked = await rels.HasRelationshipWithStatus(
|
||||||
{
|
currentUser.Id,
|
||||||
AccountId = currentUser.Id,
|
request.RelatedUserId,
|
||||||
RelatedId = request.RelatedUserId.ToString(),
|
RelationshipStatus.Blocked
|
||||||
Status = -100
|
);
|
||||||
});
|
if (hasBlocked)
|
||||||
if (hasBlocked?.Value ?? false)
|
|
||||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
@@ -102,7 +103,7 @@ public class RealmController(
|
|||||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||||
|
|
||||||
var existingMember = await db.RealmMembers
|
var existingMember = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
|
.Where(m => m.AccountId == relatedUser.Id)
|
||||||
.Where(m => m.RealmId == realm.Id)
|
.Where(m => m.RealmId == realm.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (existingMember != null)
|
if (existingMember != null)
|
||||||
@@ -116,26 +117,23 @@ public class RealmController(
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await rs.SendInviteNotify(existingMember);
|
await rs.SendInviteNotify(existingMember);
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
{
|
"realms.members.invite",
|
||||||
Action = "realms.members.invite",
|
new Dictionary<string, object>()
|
||||||
Meta =
|
|
||||||
{
|
{
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
{ "account_id", Value.ForString(existingMember.AccountId.ToString()) },
|
{ "account_id", Value.ForString(existingMember.AccountId.ToString()) },
|
||||||
{ "role", Value.ForNumber(request.Role) }
|
{ "role", Value.ForNumber(request.Role) }
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(existingMember);
|
return Ok(existingMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
var member = new SnRealmMember
|
var member = new SnRealmMember
|
||||||
{
|
{
|
||||||
AccountId = Guid.Parse(relatedUser.Id),
|
AccountId = relatedUser.Id,
|
||||||
RealmId = realm.Id,
|
RealmId = realm.Id,
|
||||||
Role = request.Role,
|
Role = request.Role,
|
||||||
};
|
};
|
||||||
@@ -143,21 +141,18 @@ public class RealmController(
|
|||||||
db.RealmMembers.Add(member);
|
db.RealmMembers.Add(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
{
|
"realms.members.invite",
|
||||||
Action = "realms.members.invite",
|
new Dictionary<string, object>()
|
||||||
Meta =
|
|
||||||
{
|
{
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
{ "role", Value.ForNumber(request.Role) }
|
{ "role", Value.ForNumber(request.Role) }
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
member.AccountId = Guid.Parse(relatedUser.Id);
|
member.AccountId = relatedUser.Id;
|
||||||
member.Realm = realm;
|
member.Realm = realm;
|
||||||
await rs.SendInviteNotify(member);
|
await rs.SendInviteNotify(member);
|
||||||
|
|
||||||
@@ -168,8 +163,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug)
|
public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -182,18 +177,15 @@ public class RealmController(
|
|||||||
db.Update(member);
|
db.Update(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.join",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.join",
|
{ "realm_id", member.RealmId.ToString() },
|
||||||
Meta =
|
{ "account_id", member.AccountId.ToString() }
|
||||||
{
|
|
||||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -202,8 +194,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -215,19 +207,16 @@ public class RealmController(
|
|||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
{
|
"realms.members.decline_invite",
|
||||||
Action = "realms.members.decline_invite",
|
new Dictionary<string, object>()
|
||||||
Meta =
|
|
||||||
{
|
{
|
||||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||||
{ "decliner_id", Value.ForString(currentUser.Id) }
|
{ "decliner_id", Value.ForString(currentUser.Id.ToString()) }
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -248,8 +237,8 @@ public class RealmController(
|
|||||||
|
|
||||||
if (!realm.IsPublic)
|
if (!realm.IsPublic)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal))
|
||||||
return StatusCode(403, "You must be a member to view this realm's members.");
|
return StatusCode(403, "You must be a member to view this realm's members.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +252,7 @@ public class RealmController(
|
|||||||
.OrderBy(m => m.JoinedAt)
|
.OrderBy(m => m.JoinedAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
|
var memberStatuses = await accountEvents.GetStatuses(
|
||||||
members.Select(m => m.AccountId).ToList()
|
members.Select(m => m.AccountId).ToList()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -306,8 +295,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug)
|
public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -323,8 +312,8 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> LeaveRealm(string slug)
|
public async Task<ActionResult> LeaveRealm(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
|
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId)
|
.Where(m => m.AccountId == accountId)
|
||||||
@@ -339,19 +328,16 @@ public class RealmController(
|
|||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.leave",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.leave",
|
{ "realm_id", member.RealmId.ToString() },
|
||||||
Meta =
|
{ "account_id", member.AccountId.ToString() },
|
||||||
{
|
{ "leaver_id", currentUser.Id }
|
||||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
|
||||||
{ "leaver_id", Value.ForString(currentUser.Id) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -371,7 +357,7 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request)
|
public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name.");
|
if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name.");
|
||||||
if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug.");
|
if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug.");
|
||||||
|
|
||||||
@@ -383,7 +369,7 @@ public class RealmController(
|
|||||||
Name = request.Name!,
|
Name = request.Name!,
|
||||||
Slug = request.Slug!,
|
Slug = request.Slug!,
|
||||||
Description = request.Description!,
|
Description = request.Description!,
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
AccountId = currentUser.Id,
|
||||||
IsCommunity = request.IsCommunity ?? false,
|
IsCommunity = request.IsCommunity ?? false,
|
||||||
IsPublic = request.IsPublic ?? false,
|
IsPublic = request.IsPublic ?? false,
|
||||||
Members = new List<SnRealmMember>
|
Members = new List<SnRealmMember>
|
||||||
@@ -391,7 +377,7 @@ public class RealmController(
|
|||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Role = RealmMemberRole.Owner,
|
Role = RealmMemberRole.Owner,
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
AccountId = currentUser.Id,
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,21 +400,18 @@ public class RealmController(
|
|||||||
db.Realms.Add(realm);
|
db.Realms.Add(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.create",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.create",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "name", realm.Name },
|
||||||
{
|
{ "slug", realm.Slug },
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
{ "is_community", realm.IsCommunity },
|
||||||
{ "name", Value.ForString(realm.Name) },
|
{ "is_public", realm.IsPublic }
|
||||||
{ "slug", Value.ForString(realm.Slug) },
|
|
||||||
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
|
||||||
{ "is_public", Value.ForBool(realm.IsPublic) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
|
|
||||||
@@ -459,14 +442,14 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request)
|
public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (realm is null) return NotFound();
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = currentUser.Id;
|
||||||
var member = await db.RealmMembers
|
var member = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
@@ -542,24 +525,21 @@ public class RealmController(
|
|||||||
db.Realms.Update(realm);
|
db.Realms.Update(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.update",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.update",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "name_updated", request.Name != null },
|
||||||
{
|
{ "slug_updated", request.Slug != null },
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
{ "description_updated", request.Description != null },
|
||||||
{ "name_updated", Value.ForBool(request.Name != null) },
|
{ "picture_updated", request.PictureId != null },
|
||||||
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
{ "background_updated", request.BackgroundId != null },
|
||||||
{ "description_updated", Value.ForBool(request.Description != null) },
|
{ "is_community_updated", request.IsCommunity != null },
|
||||||
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
{ "is_public_updated", request.IsPublic != null }
|
||||||
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
|
||||||
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
|
||||||
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(realm);
|
return Ok(realm);
|
||||||
}
|
}
|
||||||
@@ -568,7 +548,7 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug)
|
public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
@@ -579,7 +559,7 @@ public class RealmController(
|
|||||||
return StatusCode(403, "Only community realms can be joined without invitation.");
|
return StatusCode(403, "Only community realms can be joined without invitation.");
|
||||||
|
|
||||||
var existingMember = await db.RealmMembers
|
var existingMember = await db.RealmMembers
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
|
.Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (existingMember is not null)
|
if (existingMember is not null)
|
||||||
{
|
{
|
||||||
@@ -592,26 +572,23 @@ public class RealmController(
|
|||||||
db.Update(existingMember);
|
db.Update(existingMember);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.join",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.join",
|
{ "realm_id", existingMember.RealmId.ToString() },
|
||||||
Meta =
|
{ "account_id", currentUser.Id },
|
||||||
{
|
{ "is_community", realm.IsCommunity }
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(currentUser.Id) },
|
|
||||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(existingMember);
|
return Ok(existingMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
var member = new SnRealmMember
|
var member = new SnRealmMember
|
||||||
{
|
{
|
||||||
AccountId = Guid.Parse(currentUser.Id),
|
AccountId = currentUser.Id,
|
||||||
RealmId = realm.Id,
|
RealmId = realm.Id,
|
||||||
Role = RealmMemberRole.Normal,
|
Role = RealmMemberRole.Normal,
|
||||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||||
@@ -620,19 +597,16 @@ public class RealmController(
|
|||||||
db.RealmMembers.Add(member);
|
db.RealmMembers.Add(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.join",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.join",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "account_id", currentUser.Id },
|
||||||
{
|
{ "is_community", realm.IsCommunity }
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(currentUser.Id) },
|
|
||||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -641,7 +615,7 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
|
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
@@ -653,25 +627,22 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member is null) return NotFound();
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role))
|
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role))
|
||||||
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
||||||
|
|
||||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.kick",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.kick",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "account_id", memberId.ToString() },
|
||||||
{
|
{ "kicker_id", currentUser.Id }
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
|
||||||
{ "kicker_id", Value.ForString(currentUser.Id) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -681,7 +652,7 @@ public class RealmController(
|
|||||||
public async Task<ActionResult<SnRealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole)
|
public async Task<ActionResult<SnRealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole)
|
||||||
{
|
{
|
||||||
if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role.");
|
if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role.");
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await db.Realms
|
||||||
.Where(r => r.Slug == slug)
|
.Where(r => r.Slug == slug)
|
||||||
@@ -693,7 +664,7 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member is null) return NotFound();
|
if (member is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role,
|
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role,
|
||||||
newRole))
|
newRole))
|
||||||
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
||||||
|
|
||||||
@@ -701,20 +672,17 @@ public class RealmController(
|
|||||||
db.RealmMembers.Update(member);
|
db.RealmMembers.Update(member);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.members.role_update",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.members.role_update",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "account_id", memberId.ToString() },
|
||||||
{
|
{ "new_role", newRole },
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
{ "updater_id", currentUser.Id }
|
||||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
|
||||||
{ "new_role", Value.ForNumber(newRole) },
|
|
||||||
{ "updater_id", Value.ForString(currentUser.Id) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(member);
|
return Ok(member);
|
||||||
}
|
}
|
||||||
@@ -723,7 +691,7 @@ public class RealmController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult> Delete(string slug)
|
public async Task<ActionResult> Delete(string slug)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||||
|
|
||||||
var transaction = await db.Database.BeginTransactionAsync();
|
var transaction = await db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
@@ -732,16 +700,11 @@ public class RealmController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (realm is null) return NotFound();
|
if (realm is null) return NotFound();
|
||||||
|
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner))
|
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Owner))
|
||||||
return StatusCode(403, "Only the owner can delete this realm.");
|
return StatusCode(403, "Only the owner can delete this realm.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var chats = await db.ChatRooms
|
|
||||||
.Where(c => c.RealmId == realm.Id)
|
|
||||||
.Select(c => c.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
db.Realms.Remove(realm);
|
db.Realms.Remove(realm);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -749,15 +712,6 @@ public class RealmController(
|
|||||||
await db.RealmMembers
|
await db.RealmMembers
|
||||||
.Where(m => m.RealmId == realm.Id)
|
.Where(m => m.RealmId == realm.Id)
|
||||||
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
|
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
|
||||||
await db.ChatRooms
|
|
||||||
.Where(c => c.RealmId == realm.Id)
|
|
||||||
.ExecuteUpdateAsync(c => c.SetProperty(c => c.DeletedAt, now));
|
|
||||||
await db.ChatMessages
|
|
||||||
.Where(m => chats.Contains(m.ChatRoomId))
|
|
||||||
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
|
|
||||||
await db.ChatMembers
|
|
||||||
.Where(m => chats.Contains(m.ChatRoomId))
|
|
||||||
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await transaction.CommitAsync();
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
@@ -767,19 +721,16 @@ public class RealmController(
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
als.CreateActionLogFromRequest(
|
||||||
|
"realms.delete",
|
||||||
|
new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
Action = "realms.delete",
|
{ "realm_id", realm.Id.ToString() },
|
||||||
Meta =
|
{ "realm_name", realm.Name },
|
||||||
{
|
{ "realm_slug", realm.Slug }
|
||||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
|
||||||
{ "realm_name", Value.ForString(realm.Name) },
|
|
||||||
{ "realm_slug", Value.ForString(realm.Slug) }
|
|
||||||
},
|
},
|
||||||
AccountId = currentUser.Id,
|
Request
|
||||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
);
|
||||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete all file references for this realm
|
// Delete all file references for this realm
|
||||||
var realmResourceId = $"realm:{realm.Id}";
|
var realmResourceId = $"realm:{realm.Id}";
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Shared;
|
using DysonNetwork.Shared;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Localization;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
namespace DysonNetwork.Pass.Realm;
|
||||||
|
|
||||||
public class RealmService(
|
public class RealmService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
AccountService.AccountServiceClient accounts,
|
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
AccountClientHelper accountsHelper,
|
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -42,13 +40,18 @@ public class RealmService(
|
|||||||
|
|
||||||
public async Task SendInviteNotify(SnRealmMember member)
|
public async Task SendInviteNotify(SnRealmMember member)
|
||||||
{
|
{
|
||||||
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
var account = await db.Accounts
|
||||||
CultureService.SetCultureInfo(account);
|
.Include(a => a.Profile)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == member.AccountId);
|
||||||
|
|
||||||
|
if (account == null) throw new InvalidOperationException("Account not found");
|
||||||
|
|
||||||
|
CultureService.SetCultureInfo(account.Language);
|
||||||
|
|
||||||
await pusher.SendPushNotificationToUserAsync(
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
new SendPushNotificationToUserRequest
|
new SendPushNotificationToUserRequest
|
||||||
{
|
{
|
||||||
UserId = account.Id,
|
UserId = account.Id.ToString(),
|
||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "invites.realms",
|
Topic = "invites.realms",
|
||||||
@@ -75,20 +78,26 @@ public class RealmService(
|
|||||||
|
|
||||||
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
|
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
|
||||||
{
|
{
|
||||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
var account = await db.Accounts
|
||||||
member.Account = SnAccount.FromProtoValue(account);
|
.Include(a => a.Profile)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == member.AccountId);
|
||||||
|
if (account != null)
|
||||||
|
member.Account = account;
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members)
|
public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members)
|
||||||
{
|
{
|
||||||
var accountIds = members.Select(m => m.AccountId).ToList();
|
var accountIds = members.Select(m => m.AccountId).ToList();
|
||||||
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
var accountsDict = await db.Accounts
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.Where(a => accountIds.Contains(a.Id))
|
||||||
|
.ToDictionaryAsync(a => a.Id, a => a);
|
||||||
|
|
||||||
return members.Select(m =>
|
return members.Select(m =>
|
||||||
{
|
{
|
||||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
if (accountsDict.TryGetValue(m.AccountId, out var account))
|
||||||
m.Account = SnAccount.FromProtoValue(account);
|
m.Account = account;
|
||||||
return m;
|
return m;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
170
DysonNetwork.Pass/Realm/RealmServiceGrpc.cs
Normal file
170
DysonNetwork.Pass/Realm/RealmServiceGrpc.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Pass.Localization;
|
||||||
|
using DysonNetwork.Shared;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Realm;
|
||||||
|
|
||||||
|
public class RealmServiceGrpc(
|
||||||
|
AppDatabase db,
|
||||||
|
RingService.RingServiceClient pusher,
|
||||||
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
|
ICacheService cache
|
||||||
|
)
|
||||||
|
: Shared.Proto.RealmService.RealmServiceBase
|
||||||
|
{
|
||||||
|
private const string CacheKeyPrefix = "account:realms:";
|
||||||
|
|
||||||
|
public override async Task<Shared.Proto.Realm> GetRealm(GetRealmRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var realm = request.QueryCase switch
|
||||||
|
{
|
||||||
|
GetRealmRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id) => await db.Realms.FindAsync(
|
||||||
|
Guid.Parse(request.Id)),
|
||||||
|
GetRealmRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug) => await db.Realms
|
||||||
|
.FirstOrDefaultAsync(r => r.Slug == request.Slug),
|
||||||
|
_ => throw new RpcException(new Status(StatusCode.InvalidArgument, "Must provide either id or slug"))
|
||||||
|
};
|
||||||
|
|
||||||
|
return realm == null
|
||||||
|
? throw new RpcException(new Status(StatusCode.NotFound, "Realm not found"))
|
||||||
|
: realm.ToProtoValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetRealmBatchResponse> GetRealmBatch(GetRealmBatchRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var ids = request.Ids.Select(Guid.Parse).ToList();
|
||||||
|
var realms = await db.Realms.Where(r => ids.Contains(r.Id)).ToListAsync();
|
||||||
|
var response = new GetRealmBatchResponse();
|
||||||
|
response.Realms.AddRange(realms.Select(r => r.ToProtoValue()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetUserRealmsResponse> GetUserRealms(GetUserRealmsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var accountId = Guid.Parse(request.AccountId);
|
||||||
|
var cacheKey = $"{CacheKeyPrefix}{accountId}";
|
||||||
|
var (found, cachedRealms) = await cache.GetAsyncWithStatus<List<Guid>>(cacheKey);
|
||||||
|
if (found && cachedRealms != null)
|
||||||
|
return new GetUserRealmsResponse { RealmIds = { cachedRealms.Select(g => g.ToString()) } };
|
||||||
|
|
||||||
|
var realms = await db.RealmMembers
|
||||||
|
.Include(m => m.Realm)
|
||||||
|
.Where(m => m.AccountId == accountId)
|
||||||
|
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||||
|
.Where(m => m.Realm != null)
|
||||||
|
.Select(m => m.Realm!.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Cache the result for 5 minutes
|
||||||
|
await cache.SetAsync(cacheKey, realms, TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
|
return new GetUserRealmsResponse { RealmIds = { realms.Select(g => g.ToString()) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetPublicRealmsResponse> GetPublicRealms(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var realms = await db.Realms.Where(r => r.IsPublic).ToListAsync();
|
||||||
|
var response = new GetPublicRealmsResponse();
|
||||||
|
response.Realms.AddRange(realms.Select(r => r.ToProtoValue()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetPublicRealmsResponse> SearchRealms(SearchRealmsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var realms = await db.Realms
|
||||||
|
.Where(r => r.IsPublic)
|
||||||
|
.Where(r => EF.Functions.Like(r.Slug, $"{request.Query}%") || EF.Functions.Like(r.Name, $"{request.Query}%"))
|
||||||
|
.Take(request.Limit)
|
||||||
|
.ToListAsync();
|
||||||
|
var response = new GetPublicRealmsResponse();
|
||||||
|
response.Realms.AddRange(realms.Select(r => r.ToProtoValue()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> SendInviteNotify(SendInviteNotifyRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var member = request.Member;
|
||||||
|
var account = await db.Accounts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId));
|
||||||
|
|
||||||
|
if (account == null) throw new RpcException(new Status(StatusCode.NotFound, "Account not found"));
|
||||||
|
|
||||||
|
CultureService.SetCultureInfo(account.Language);
|
||||||
|
|
||||||
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
|
{
|
||||||
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "invites.realms",
|
||||||
|
Title = localizer["RealmInviteTitle"],
|
||||||
|
Body = localizer["RealmInviteBody", member.Realm?.Name ?? "Unknown Realm"],
|
||||||
|
ActionUri = "/realms",
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<BoolValue> IsMemberWithRole(IsMemberWithRoleRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.RequiredRoles.Count == 0)
|
||||||
|
return new BoolValue { Value = false };
|
||||||
|
|
||||||
|
var maxRequiredRole = request.RequiredRoles.Max();
|
||||||
|
var member = await db.RealmMembers
|
||||||
|
.Where(m => m.RealmId == Guid.Parse(request.RealmId) && m.AccountId == Guid.Parse(request.AccountId) &&
|
||||||
|
m.JoinedAt != null && m.LeaveAt == null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return new BoolValue { Value = member?.Role >= maxRequiredRole };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<RealmMember> LoadMemberAccount(LoadMemberAccountRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var member = request.Member;
|
||||||
|
var account = await db.Accounts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId));
|
||||||
|
|
||||||
|
var response = new RealmMember(member) { Account = account?.ToProtoValue() };
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<LoadMemberAccountsResponse> LoadMemberAccounts(LoadMemberAccountsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var accountIds = request.Members.Select(m => Guid.Parse(m.AccountId)).ToList();
|
||||||
|
var accounts = await db.Accounts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.Where(a => accountIds.Contains(a.Id))
|
||||||
|
.ToDictionaryAsync(a => a.Id, a => a.ToProtoValue());
|
||||||
|
|
||||||
|
var response = new LoadMemberAccountsResponse();
|
||||||
|
foreach (var member in request.Members)
|
||||||
|
{
|
||||||
|
var updatedMember = new RealmMember(member);
|
||||||
|
if (accounts.TryGetValue(Guid.Parse(member.AccountId), out var account))
|
||||||
|
{
|
||||||
|
updatedMember.Account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Members.Add(updatedMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using DysonNetwork.Pass.Auth;
|
|||||||
using DysonNetwork.Pass.Credit;
|
using DysonNetwork.Pass.Credit;
|
||||||
using DysonNetwork.Pass.Leveling;
|
using DysonNetwork.Pass.Leveling;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Pass.Realm;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
@@ -21,7 +22,6 @@ public static class ApplicationConfiguration
|
|||||||
app.ConfigureForwardedHeaders(configuration);
|
app.ConfigureForwardedHeaders(configuration);
|
||||||
|
|
||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseRateLimiter();
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseMiddleware<PermissionMiddleware>();
|
app.UseMiddleware<PermissionMiddleware>();
|
||||||
@@ -42,6 +42,7 @@ public static class ApplicationConfiguration
|
|||||||
app.MapGrpcService<BotAccountReceiverGrpc>();
|
app.MapGrpcService<BotAccountReceiverGrpc>();
|
||||||
app.MapGrpcService<WalletServiceGrpc>();
|
app.MapGrpcService<WalletServiceGrpc>();
|
||||||
app.MapGrpcService<PaymentServiceGrpc>();
|
app.MapGrpcService<PaymentServiceGrpc>();
|
||||||
|
app.MapGrpcService<RealmServiceGrpc>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,9 +104,33 @@ public class BroadcastEventHandler(
|
|||||||
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
||||||
await msg.AckAsync(cancellationToken: stoppingToken);
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
}
|
}
|
||||||
|
else if (evt.ProductIdentifier == "lottery")
|
||||||
|
{
|
||||||
|
logger.LogInformation("Handling lottery order: {OrderId}", evt.OrderId);
|
||||||
|
|
||||||
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
var lotteries = scope.ServiceProvider.GetRequiredService<Lotteries.LotteryService>();
|
||||||
|
|
||||||
|
var order = await db.PaymentOrders.FindAsync(
|
||||||
|
[evt.OrderId],
|
||||||
|
cancellationToken: stoppingToken
|
||||||
|
);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId);
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await lotteries.HandleLotteryOrder(order);
|
||||||
|
|
||||||
|
logger.LogInformation("Lottery ticket for order {OrderId} created successfully.", evt.OrderId);
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Not a subscription or gift order, skip
|
// Not a subscription, gift, or lottery order, skip
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ public static class ScheduledJobsConfiguration
|
|||||||
.WithIntervalInHours(1)
|
.WithIntervalInHours(1)
|
||||||
.RepeatForever())
|
.RepeatForever())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var lotteryDrawJob = new JobKey("LotteryDraw");
|
||||||
|
q.AddJob<Lotteries.LotteryDrawJob>(opts => opts.WithIdentity(lotteryDrawJob));
|
||||||
|
q.AddTrigger(opts => opts
|
||||||
|
.ForJob(lotteryDrawJob)
|
||||||
|
.WithIdentity("LotteryDrawTrigger")
|
||||||
|
.WithCronSchedule("0 0 0 * * ?"));
|
||||||
});
|
});
|
||||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using DysonNetwork.Pass.Credit;
|
|||||||
using DysonNetwork.Pass.Handlers;
|
using DysonNetwork.Pass.Handlers;
|
||||||
using DysonNetwork.Pass.Leveling;
|
using DysonNetwork.Pass.Leveling;
|
||||||
using DysonNetwork.Pass.Mailer;
|
using DysonNetwork.Pass.Mailer;
|
||||||
|
using DysonNetwork.Pass.Realm;
|
||||||
using DysonNetwork.Pass.Safety;
|
using DysonNetwork.Pass.Safety;
|
||||||
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
using DysonNetwork.Pass.Wallet.PaymentHandlers;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
@@ -91,19 +92,6 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
|
||||||
{
|
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
|
||||||
opts.PermitLimit = 120;
|
|
||||||
opts.QueueLimit = 2;
|
|
||||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
@@ -152,6 +140,8 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<SafetyService>();
|
services.AddScoped<SafetyService>();
|
||||||
services.AddScoped<SocialCreditService>();
|
services.AddScoped<SocialCreditService>();
|
||||||
services.AddScoped<ExperienceService>();
|
services.AddScoped<ExperienceService>();
|
||||||
|
services.AddScoped<RealmService>();
|
||||||
|
services.AddScoped<Lotteries.LotteryService>();
|
||||||
|
|
||||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||||
services.AddScoped<OidcProviderService>();
|
services.AddScoped<OidcProviderService>();
|
||||||
|
|||||||
@@ -197,7 +197,8 @@ public class SubscriptionGiftController(
|
|||||||
|
|
||||||
if (currentUser.Profile.Level < MinimumAccountLevel)
|
if (currentUser.Profile.Level < MinimumAccountLevel)
|
||||||
{
|
{
|
||||||
return StatusCode(403, "Account level must be at least 60 to purchase a gift.");
|
if (currentUser.PerkSubscription is null)
|
||||||
|
return StatusCode(403, "Account level must be at least 60 or a member of the Stellar Program to purchase a gift.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration? giftDuration = null;
|
Duration? giftDuration = null;
|
||||||
|
|||||||
@@ -250,6 +250,14 @@ public class SubscriptionService(
|
|||||||
: null;
|
: null;
|
||||||
if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found.");
|
if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found.");
|
||||||
|
|
||||||
|
if (subscriptionInfo.RequiredLevel > 0)
|
||||||
|
{
|
||||||
|
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == subscription.AccountId);
|
||||||
|
if (profile is null) throw new InvalidOperationException("Account must have a profile");
|
||||||
|
if (profile.Level < subscriptionInfo.RequiredLevel)
|
||||||
|
throw new InvalidOperationException("Account level must be at least 60 to purchase a gift.");
|
||||||
|
}
|
||||||
|
|
||||||
return await payment.CreateOrderAsync(
|
return await payment.CreateOrderAsync(
|
||||||
null,
|
null,
|
||||||
subscriptionInfo.Currency,
|
subscriptionInfo.Currency,
|
||||||
@@ -684,6 +692,9 @@ public class SubscriptionService(
|
|||||||
if (now > gift.ExpiresAt)
|
if (now > gift.ExpiresAt)
|
||||||
throw new InvalidOperationException("Gift has expired.");
|
throw new InvalidOperationException("Gift has expired.");
|
||||||
|
|
||||||
|
if (gift.GifterId == redeemer.Id)
|
||||||
|
throw new InvalidOperationException("You cannot redeem your own gift.");
|
||||||
|
|
||||||
// Validate redeemer permissions
|
// Validate redeemer permissions
|
||||||
if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id)
|
if (!gift.IsOpenGift && gift.RecipientId != redeemer.Id)
|
||||||
throw new InvalidOperationException("This gift is not intended for you.");
|
throw new InvalidOperationException("This gift is not intended for you.");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
|||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddDysonAuth();
|
builder.Services.AddDysonAuth();
|
||||||
builder.Services.AddAccountService();
|
builder.Services.AddAccountService();
|
||||||
@@ -45,6 +44,6 @@ app.ConfigureAppMiddleware(builder.Configuration);
|
|||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
app.ConfigureGrpcServices();
|
app.ConfigureGrpcServices();
|
||||||
|
|
||||||
app.UseSwaggerManifest();
|
app.UseSwaggerManifest("DysonNetwork.Ring");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ public static class ApplicationConfiguration
|
|||||||
app.ConfigureForwardedHeaders(configuration);
|
app.ConfigureForwardedHeaders(configuration);
|
||||||
|
|
||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
app.UseRateLimiter();
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
|||||||
@@ -50,19 +50,6 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
|
||||||
{
|
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
|
||||||
opts.PermitLimit = 120;
|
|
||||||
opts.QueueLimit = 2;
|
|
||||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||||
<PackageReference Include="NATS.Net" Version="2.6.8" />
|
<PackageReference Include="NATS.Net" Version="2.6.8" />
|
||||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||||
@@ -29,20 +29,20 @@
|
|||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
|
|
||||||
<PackageReference Include="Aspire.NATS.Net" Version="9.4.2" />
|
<PackageReference Include="Aspire.NATS.Net" Version="9.5.1" />
|
||||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.4.2" />
|
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.1" />
|
||||||
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.4.2" />
|
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.5.1" />
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/>
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2"/>
|
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2"/>
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.AspNetCore" Version="1.13.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public static class SwaggerGen
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WebApplication UseSwaggerManifest(this WebApplication app)
|
public static WebApplication UseSwaggerManifest(this WebApplication app, string serviceName)
|
||||||
{
|
{
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ public static class SwaggerGen
|
|||||||
var publicBasePath = configuration["Swagger:PublicBasePath"]?.TrimEnd('/') ?? "";
|
var publicBasePath = configuration["Swagger:PublicBasePath"]?.TrimEnd('/') ?? "";
|
||||||
options.SwaggerEndpoint(
|
options.SwaggerEndpoint(
|
||||||
$"{publicBasePath}/swagger/v1/swagger.json",
|
$"{publicBasePath}/swagger/v1/swagger.json",
|
||||||
"Develop API v1");
|
$"{serviceName} API v1");
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -148,6 +148,32 @@ public class UsernameColor
|
|||||||
public string? Value { get; set; } // e.g. "red" or "#ff6600"
|
public string? Value { get; set; } // e.g. "red" or "#ff6600"
|
||||||
public string? Direction { get; set; } // e.g. "to right"
|
public string? Direction { get; set; } // e.g. "to right"
|
||||||
public List<string>? Colors { get; set; } // e.g. ["#ff0000", "#00ff00"]
|
public List<string>? Colors { get; set; } // e.g. ["#ff0000", "#00ff00"]
|
||||||
|
|
||||||
|
public Proto.UsernameColor ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new Proto.UsernameColor
|
||||||
|
{
|
||||||
|
Type = Type,
|
||||||
|
Value = Value,
|
||||||
|
Direction = Direction,
|
||||||
|
};
|
||||||
|
if (Colors is not null)
|
||||||
|
{
|
||||||
|
proto.Colors.AddRange(Colors);
|
||||||
|
}
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UsernameColor FromProtoValue(Proto.UsernameColor proto)
|
||||||
|
{
|
||||||
|
return new UsernameColor
|
||||||
|
{
|
||||||
|
Type = proto.Type,
|
||||||
|
Value = proto.Value,
|
||||||
|
Direction = proto.Direction,
|
||||||
|
Colors = proto.Colors?.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnAccountProfile : ModelBase, IIdentifiedResource
|
public class SnAccountProfile : ModelBase, IIdentifiedResource
|
||||||
@@ -218,6 +244,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
|||||||
AccountId = AccountId.ToString(),
|
AccountId = AccountId.ToString(),
|
||||||
Verification = Verification?.ToProtoValue(),
|
Verification = Verification?.ToProtoValue(),
|
||||||
ActiveBadge = ActiveBadge?.ToProtoValue(),
|
ActiveBadge = ActiveBadge?.ToProtoValue(),
|
||||||
|
UsernameColor = UsernameColor?.ToProtoValue(),
|
||||||
CreatedAt = CreatedAt.ToTimestamp(),
|
CreatedAt = CreatedAt.ToTimestamp(),
|
||||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||||
};
|
};
|
||||||
@@ -247,6 +274,7 @@ public class SnAccountProfile : ModelBase, IIdentifiedResource
|
|||||||
Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
|
Picture = proto.Picture is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Picture),
|
||||||
Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
|
Background = proto.Background is null ? null : SnCloudFileReferenceObject.FromProtoValue(proto.Background),
|
||||||
AccountId = Guid.Parse(proto.AccountId),
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
UsernameColor = proto.UsernameColor is not null ? UsernameColor.FromProtoValue(proto.UsernameColor) : null,
|
||||||
CreatedAt = proto.CreatedAt.ToInstant(),
|
CreatedAt = proto.CreatedAt.ToInstant(),
|
||||||
UpdatedAt = proto.UpdatedAt.ToInstant()
|
UpdatedAt = proto.UpdatedAt.ToInstant()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class SnChatRoom : ModelBase, IIdentifiedResource
|
|||||||
[JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
|
[JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
|
||||||
|
|
||||||
public Guid? RealmId { get; set; }
|
public Guid? RealmId { get; set; }
|
||||||
public SnRealm? Realm { get; set; }
|
[NotMapped] public SnRealm? Realm { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
[JsonPropertyName("members")]
|
[JsonPropertyName("members")]
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
|||||||
public SnPost? ForwardedPost { get; set; }
|
public SnPost? ForwardedPost { get; set; }
|
||||||
|
|
||||||
public Guid? RealmId { get; set; }
|
public Guid? RealmId { get; set; }
|
||||||
public SnRealm? Realm { get; set; }
|
[NotMapped] public SnRealm? Realm { get; set; }
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
||||||
|
|
||||||
@@ -73,11 +73,12 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
|||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
public SnPublisher Publisher { get; set; } = null!;
|
public SnPublisher Publisher { get; set; } = null!;
|
||||||
|
|
||||||
public ICollection<SnPostAward> Awards { get; set; } = null!;
|
public List<SnPostAward> Awards { get; set; } = null!;
|
||||||
[JsonIgnore] public ICollection<SnPostReaction> Reactions { get; set; } = new List<SnPostReaction>();
|
[JsonIgnore] public List<SnPostReaction> Reactions { get; set; } = [];
|
||||||
public ICollection<SnPostTag> Tags { get; set; } = new List<SnPostTag>();
|
public List<SnPostTag> Tags { get; set; } = [];
|
||||||
public ICollection<SnPostCategory> Categories { get; set; } = new List<SnPostCategory>();
|
public List<SnPostCategory> Categories { get; set; } = [];
|
||||||
[JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = new List<SnPostCollection>();
|
[JsonIgnore] public List<SnPostCollection> Collections { get; set; } = [];
|
||||||
|
public List<SnPostFeaturedRecord> FeaturedRecords { get; set; } = [];
|
||||||
|
|
||||||
[JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
|
[JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
|
||||||
[NotMapped] public bool IsTruncated { get; set; } = false;
|
[NotMapped] public bool IsTruncated { get; set; } = false;
|
||||||
@@ -104,7 +105,7 @@ public class SnPostTag : ModelBase
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
[MaxLength(128)] public string Slug { get; set; } = null!;
|
[MaxLength(128)] public string Slug { get; set; } = null!;
|
||||||
[MaxLength(256)] public string? Name { get; set; }
|
[MaxLength(256)] public string? Name { get; set; }
|
||||||
[JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
|
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
|
||||||
|
|
||||||
[NotMapped] public int? Usage { get; set; }
|
[NotMapped] public int? Usage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ public class SnPostCategory : ModelBase
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
[MaxLength(128)] public string Slug { get; set; } = null!;
|
[MaxLength(128)] public string Slug { get; set; } = null!;
|
||||||
[MaxLength(256)] public string? Name { get; set; }
|
[MaxLength(256)] public string? Name { get; set; }
|
||||||
[JsonIgnore] public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
|
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
|
||||||
|
|
||||||
[NotMapped] public int? Usage { get; set; }
|
[NotMapped] public int? Usage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -139,15 +140,14 @@ public class SnPostCollection : ModelBase
|
|||||||
|
|
||||||
public SnPublisher Publisher { get; set; } = null!;
|
public SnPublisher Publisher { get; set; } = null!;
|
||||||
|
|
||||||
public ICollection<SnPost> Posts { get; set; } = new List<SnPost>();
|
public List<SnPost> Posts { get; set; } = new List<SnPost>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnPostFeaturedRecord : ModelBase
|
public class SnPostFeaturedRecord : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
public Guid PostId { get; set; }
|
public Guid PostId { get; set; }
|
||||||
public SnPost Post { get; set; } = null!;
|
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||||
public Instant? FeaturedAt { get; set; }
|
public Instant? FeaturedAt { get; set; }
|
||||||
public int SocialCredits { get; set; }
|
public int SocialCredits { get; set; }
|
||||||
}
|
}
|
||||||
@@ -168,6 +168,7 @@ public class SnPostReaction : ModelBase
|
|||||||
public Guid PostId { get; set; }
|
public Guid PostId { get; set; }
|
||||||
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
[JsonIgnore] public SnPost Post { get; set; } = null!;
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
|
[NotMapped] public SnAccount? Account { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnPostAward : ModelBase
|
public class SnPostAward : ModelBase
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
public Guid? AccountId { get; set; }
|
public Guid? AccountId { get; set; }
|
||||||
public Guid? RealmId { get; set; }
|
public Guid? RealmId { get; set; }
|
||||||
[JsonIgnore] public SnRealm? Realm { get; set; }
|
[NotMapped] public SnRealm? Realm { get; set; }
|
||||||
[NotMapped] public SnAccount? Account { get; set; }
|
[NotMapped] public SnAccount? Account { get; set; }
|
||||||
|
|
||||||
public string ResourceIdentifier => $"publisher:{Id}";
|
public string ResourceIdentifier => $"publisher:{Id}";
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.Protobuf;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
@@ -26,11 +28,35 @@ public class SnRealm : ModelBase, IIdentifiedResource
|
|||||||
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
|
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>();
|
[JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>();
|
||||||
[JsonIgnore] public ICollection<SnChatRoom> ChatRooms { get; set; } = new List<SnChatRoom>();
|
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
|
|
||||||
public string ResourceIdentifier => $"realm:{Id}";
|
public string ResourceIdentifier => $"realm:{Id}";
|
||||||
|
|
||||||
|
public Realm ToProtoValue()
|
||||||
|
{
|
||||||
|
return new Realm
|
||||||
|
{
|
||||||
|
Id = Id.ToString(),
|
||||||
|
Name = Name,
|
||||||
|
Slug = Slug,
|
||||||
|
IsCommunity = IsCommunity,
|
||||||
|
IsPublic = IsPublic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnRealm FromProtoValue(Realm proto)
|
||||||
|
{
|
||||||
|
return new SnRealm
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Name = proto.Name,
|
||||||
|
Slug = proto.Slug,
|
||||||
|
Description = "",
|
||||||
|
IsCommunity = proto.IsCommunity,
|
||||||
|
IsPublic = proto.IsPublic
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class RealmMemberRole
|
public abstract class RealmMemberRole
|
||||||
@@ -51,4 +77,40 @@ public class SnRealmMember : ModelBase
|
|||||||
public int Role { get; set; } = RealmMemberRole.Normal;
|
public int Role { get; set; } = RealmMemberRole.Normal;
|
||||||
public Instant? JoinedAt { get; set; }
|
public Instant? JoinedAt { get; set; }
|
||||||
public Instant? LeaveAt { get; set; }
|
public Instant? LeaveAt { get; set; }
|
||||||
|
|
||||||
|
public Proto.RealmMember ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new Proto.RealmMember
|
||||||
|
{
|
||||||
|
AccountId = AccountId.ToString(),
|
||||||
|
RealmId = RealmId.ToString(),
|
||||||
|
Role = Role,
|
||||||
|
JoinedAt = JoinedAt?.ToTimestamp(),
|
||||||
|
LeaveAt = LeaveAt?.ToTimestamp(),
|
||||||
|
Realm = Realm.ToProtoValue()
|
||||||
|
};
|
||||||
|
if (Account != null)
|
||||||
|
{
|
||||||
|
proto.Account = Account.ToProtoValue();
|
||||||
|
}
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnRealmMember FromProtoValue(RealmMember proto)
|
||||||
|
{
|
||||||
|
var member = new SnRealmMember
|
||||||
|
{
|
||||||
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
RealmId = Guid.Parse(proto.RealmId),
|
||||||
|
Role = proto.Role,
|
||||||
|
JoinedAt = proto.JoinedAt?.ToInstant(),
|
||||||
|
LeaveAt = proto.LeaveAt?.ToInstant(),
|
||||||
|
Realm = proto.Realm != null ? SnRealm.FromProtoValue(proto.Realm) : new SnRealm() // Provide default or handle null
|
||||||
|
};
|
||||||
|
if (proto.Account != null)
|
||||||
|
{
|
||||||
|
member.Account = SnAccount.FromProtoValue(proto.Account);
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
54
DysonNetwork.Shared/Models/SnLottery.cs
Normal file
54
DysonNetwork.Shared/Models/SnLottery.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
public enum LotteryDrawStatus
|
||||||
|
{
|
||||||
|
Pending = 0,
|
||||||
|
Drawn = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnLotteryRecord : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Instant DrawDate { get; set; } // Date of the draw
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public List<int> WinningRegionOneNumbers { get; set; } = new(); // 5 winning numbers
|
||||||
|
|
||||||
|
[Range(0, 99)]
|
||||||
|
public int WinningRegionTwoNumber { get; set; } // 1 winning number
|
||||||
|
|
||||||
|
public int TotalTickets { get; set; } // Total tickets processed for this draw
|
||||||
|
public int TotalPrizesAwarded { get; set; } // Total prizes awarded
|
||||||
|
public long TotalPrizeAmount { get; set; } // Total ISP prize amount awarded
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnLottery : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public SnAccount Account { get; init; } = null!;
|
||||||
|
public Guid AccountId { get; init; }
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public List<int> RegionOneNumbers { get; set; } = []; // 5 numbers, 0-99, unique
|
||||||
|
|
||||||
|
[Range(0, 99)]
|
||||||
|
public int RegionTwoNumber { get; init; } // 1 number, 0-99, can repeat
|
||||||
|
|
||||||
|
public int Multiplier { get; init; } = 1; // Default 1x
|
||||||
|
|
||||||
|
public LotteryDrawStatus DrawStatus { get; set; } = LotteryDrawStatus.Pending; // Status to track draw processing
|
||||||
|
|
||||||
|
public Instant? DrawDate { get; set; } // Date when this ticket was drawn
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public List<int>? MatchedRegionOneNumbers { get; set; } // The actual numbers that matched in region one
|
||||||
|
|
||||||
|
public int? MatchedRegionTwoNumber { get; set; } // The matched number if special number matched (null otherwise)
|
||||||
|
}
|
||||||
@@ -59,6 +59,13 @@ message AccountStatus {
|
|||||||
bytes meta = 10;
|
bytes meta = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UsernameColor {
|
||||||
|
string type = 1;
|
||||||
|
google.protobuf.StringValue value = 2;
|
||||||
|
google.protobuf.StringValue direction = 3;
|
||||||
|
repeated string colors = 4;
|
||||||
|
}
|
||||||
|
|
||||||
// Profile contains detailed information about a user
|
// Profile contains detailed information about a user
|
||||||
message AccountProfile {
|
message AccountProfile {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
@@ -89,6 +96,7 @@ message AccountProfile {
|
|||||||
|
|
||||||
google.protobuf.Timestamp created_at = 22;
|
google.protobuf.Timestamp created_at = 22;
|
||||||
google.protobuf.Timestamp updated_at = 23;
|
google.protobuf.Timestamp updated_at = 23;
|
||||||
|
optional UsernameColor username_color = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountContact represents a contact method for an account
|
// AccountContact represents a contact method for an account
|
||||||
|
|||||||
110
DysonNetwork.Shared/Proto/realm.proto
Normal file
110
DysonNetwork.Shared/Proto/realm.proto
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package proto;
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Shared.Proto";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "google/protobuf/wrappers.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
import 'account.proto';
|
||||||
|
|
||||||
|
// Message Definitions
|
||||||
|
|
||||||
|
message Realm {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string slug = 3;
|
||||||
|
bool is_community = 4;
|
||||||
|
bool is_public = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RealmMember {
|
||||||
|
string account_id = 1;
|
||||||
|
string realm_id = 2;
|
||||||
|
int32 role = 3;
|
||||||
|
optional google.protobuf.Timestamp joined_at = 4;
|
||||||
|
optional google.protobuf.Timestamp leave_at = 5;
|
||||||
|
optional Account account = 6;
|
||||||
|
optional Realm realm = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service Definitions
|
||||||
|
|
||||||
|
service RealmService {
|
||||||
|
// Get realm by id or slug
|
||||||
|
rpc GetRealm(GetRealmRequest) returns (Realm) {}
|
||||||
|
// Get realm batch by ids
|
||||||
|
rpc GetRealmBatch(GetRealmBatchRequest) returns (GetRealmBatchResponse) {}
|
||||||
|
// Get realms for a user
|
||||||
|
rpc GetUserRealms(GetUserRealmsRequest) returns (GetUserRealmsResponse) {}
|
||||||
|
// Get public realms
|
||||||
|
rpc GetPublicRealms(google.protobuf.Empty) returns (GetPublicRealmsResponse) {}
|
||||||
|
// Search public realms
|
||||||
|
rpc SearchRealms(SearchRealmsRequest) returns (GetPublicRealmsResponse) {}
|
||||||
|
// Send invitation notification
|
||||||
|
rpc SendInviteNotify(SendInviteNotifyRequest) returns (google.protobuf.Empty) {}
|
||||||
|
// Check if member has required role
|
||||||
|
rpc IsMemberWithRole(IsMemberWithRoleRequest) returns (google.protobuf.BoolValue) {}
|
||||||
|
// Load account for a member
|
||||||
|
rpc LoadMemberAccount(LoadMemberAccountRequest) returns (RealmMember) {}
|
||||||
|
// Load accounts for members
|
||||||
|
rpc LoadMemberAccounts(LoadMemberAccountsRequest) returns (LoadMemberAccountsResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request/Response Messages
|
||||||
|
|
||||||
|
message GetRealmRequest {
|
||||||
|
oneof query {
|
||||||
|
string id = 1;
|
||||||
|
string slug = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserRealmsRequest {
|
||||||
|
string account_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRealmBatchRequest {
|
||||||
|
repeated string ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRealmBatchResponse {
|
||||||
|
repeated Realm realms = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserRealmsResponse {
|
||||||
|
repeated string realm_ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPublicRealmsResponse {
|
||||||
|
repeated Realm realms = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SearchRealmsRequest {
|
||||||
|
string query = 1;
|
||||||
|
int32 limit = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SendInviteNotifyRequest {
|
||||||
|
RealmMember member = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IsMemberWithRoleRequest {
|
||||||
|
string realm_id = 1;
|
||||||
|
string account_id = 2;
|
||||||
|
repeated int32 required_roles = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoadMemberAccountRequest {
|
||||||
|
RealmMember member = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoadMemberAccountsRequest {
|
||||||
|
repeated RealmMember members = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoadMemberAccountsResponse {
|
||||||
|
repeated RealmMember members = 1;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
|
|
||||||
namespace DysonNetwork.Shared.Registry;
|
namespace DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
public class AccountClientHelper(AccountService.AccountServiceClient accounts)
|
public class RemoteAccountService(AccountService.AccountServiceClient accounts)
|
||||||
{
|
{
|
||||||
public async Task<Account> GetAccount(Guid id)
|
public async Task<Account> GetAccount(Guid id)
|
||||||
{
|
{
|
||||||
82
DysonNetwork.Shared/Registry/RemoteRealmService.cs
Normal file
82
DysonNetwork.Shared/Registry/RemoteRealmService.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
|
public class RemoteRealmService(RealmService.RealmServiceClient realms)
|
||||||
|
{
|
||||||
|
public async Task<SnRealm> GetRealm(string id)
|
||||||
|
{
|
||||||
|
var request = new GetRealmRequest { Id = id };
|
||||||
|
var response = await realms.GetRealmAsync(request);
|
||||||
|
return SnRealm.FromProtoValue(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnRealm> GetRealmBySlug(string slug)
|
||||||
|
{
|
||||||
|
var request = new GetRealmRequest { Slug = slug };
|
||||||
|
var response = await realms.GetRealmAsync(request);
|
||||||
|
return SnRealm.FromProtoValue(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Guid>> GetUserRealms(Guid accountId)
|
||||||
|
{
|
||||||
|
var request = new GetUserRealmsRequest { AccountId = accountId.ToString() };
|
||||||
|
var response = await realms.GetUserRealmsAsync(request);
|
||||||
|
return response.RealmIds.Select(Guid.Parse).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnRealm>> GetPublicRealms()
|
||||||
|
{
|
||||||
|
var response = await realms.GetPublicRealmsAsync(new Empty());
|
||||||
|
return response.Realms.Select(SnRealm.FromProtoValue).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnRealm>> SearchRealms(string query, int limit)
|
||||||
|
{
|
||||||
|
var request = new SearchRealmsRequest { Query = query, Limit = limit };
|
||||||
|
var response = await realms.SearchRealmsAsync(request);
|
||||||
|
return response.Realms.Select(SnRealm.FromProtoValue).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnRealm>> GetRealmBatch(List<string> ids)
|
||||||
|
{
|
||||||
|
var request = new GetRealmBatchRequest();
|
||||||
|
request.Ids.AddRange(ids);
|
||||||
|
var response = await realms.GetRealmBatchAsync(request);
|
||||||
|
return response.Realms.Select(SnRealm.FromProtoValue).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendInviteNotify(SnRealmMember member)
|
||||||
|
{
|
||||||
|
var protoMember = member.ToProtoValue();
|
||||||
|
var request = new SendInviteNotifyRequest { Member = protoMember };
|
||||||
|
await realms.SendInviteNotifyAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, List<int> requiredRoles)
|
||||||
|
{
|
||||||
|
var request = new IsMemberWithRoleRequest { RealmId = realmId.ToString(), AccountId = accountId.ToString() };
|
||||||
|
request.RequiredRoles.AddRange(requiredRoles);
|
||||||
|
var response = await realms.IsMemberWithRoleAsync(request);
|
||||||
|
return response.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
|
||||||
|
{
|
||||||
|
var protoMember = member.ToProtoValue();
|
||||||
|
var request = new LoadMemberAccountRequest { Member = protoMember };
|
||||||
|
var response = await realms.LoadMemberAccountAsync(request);
|
||||||
|
return SnRealmMember.FromProtoValue(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnRealmMember>> LoadMemberAccounts(List<SnRealmMember> members)
|
||||||
|
{
|
||||||
|
var protoMembers = members.Select(m => m.ToProtoValue()).ToList();
|
||||||
|
var request = new LoadMemberAccountsRequest();
|
||||||
|
request.Members.AddRange(protoMembers);
|
||||||
|
var response = await realms.LoadMemberAccountsAsync(request);
|
||||||
|
return response.Members.Select(SnRealmMember.FromProtoValue).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ public static class ServiceInjectionHelper
|
|||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
services.AddSingleton<AccountClientHelper>();
|
services.AddSingleton<RemoteAccountService>();
|
||||||
|
|
||||||
services
|
services
|
||||||
.AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o =>
|
.AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o =>
|
||||||
@@ -60,6 +60,13 @@ public static class ServiceInjectionHelper
|
|||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
|
);
|
||||||
|
services.AddSingleton<RemoteRealmService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +77,8 @@ public static class ServiceInjectionHelper
|
|||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
|
|
||||||
services.AddGrpcClient<FileReferenceService.FileReferenceServiceClient>(o => o.Address = new Uri("https://_grpc.drive"))
|
services.AddGrpcClient<FileReferenceService.FileReferenceServiceClient>(o =>
|
||||||
|
o.Address = new Uri("https://_grpc.drive"))
|
||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
@@ -80,7 +88,8 @@ public static class ServiceInjectionHelper
|
|||||||
|
|
||||||
public static IServiceCollection AddPublisherService(this IServiceCollection services)
|
public static IServiceCollection AddPublisherService(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddGrpcClient<PublisherService.PublisherServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
|
services
|
||||||
|
.AddGrpcClient<PublisherService.PublisherServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
|
||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
@@ -90,7 +99,8 @@ public static class ServiceInjectionHelper
|
|||||||
|
|
||||||
public static IServiceCollection AddDevelopService(this IServiceCollection services)
|
public static IServiceCollection AddDevelopService(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddGrpcClient<CustomAppService.CustomAppServiceClient>(o => o.Address = new Uri("https://_grpc.develop"))
|
services.AddGrpcClient<CustomAppService.CustomAppServiceClient>(o =>
|
||||||
|
o.Address = new Uri("https://_grpc.develop"))
|
||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Discovery;
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -13,7 +13,7 @@ public class ActivityService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
Publisher.PublisherService pub,
|
Publisher.PublisherService pub,
|
||||||
PostService ps,
|
PostService ps,
|
||||||
RealmService rs,
|
RemoteRealmService rs,
|
||||||
DiscoveryService ds,
|
DiscoveryService ds,
|
||||||
AccountService.AccountServiceClient accounts
|
AccountService.AccountServiceClient accounts
|
||||||
)
|
)
|
||||||
@@ -40,19 +40,15 @@ public class ActivityService(
|
|||||||
debugInclude ??= new HashSet<string>();
|
debugInclude ??= new HashSet<string>();
|
||||||
|
|
||||||
// Get and process posts
|
// Get and process posts
|
||||||
var postsQuery = db.Posts
|
var publicRealms = await rs.GetPublicRealms();
|
||||||
.Include(e => e.RepliedPost)
|
var publicRealmIds = publicRealms.Select(r => r.Id).ToList();
|
||||||
.Include(e => e.ForwardedPost)
|
|
||||||
.Include(e => e.Categories)
|
var postsQuery = BuildPostsQuery(cursor, null, publicRealmIds)
|
||||||
.Include(e => e.Tags)
|
|
||||||
.Include(e => e.Realm)
|
|
||||||
.Where(e => e.RepliedPostId == null)
|
|
||||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
|
||||||
.OrderByDescending(p => p.PublishedAt)
|
|
||||||
.FilterWithVisibility(null, [], [], isListing: true)
|
.FilterWithVisibility(null, [], [], isListing: true)
|
||||||
.Take(take * 5);
|
.Take(take * 5);
|
||||||
|
|
||||||
var posts = await GetAndProcessPosts(postsQuery);
|
var posts = await GetAndProcessPosts(postsQuery);
|
||||||
|
await LoadPostsRealmsAsync(posts, rs);
|
||||||
posts = RankPosts(posts, take);
|
posts = RankPosts(posts, take);
|
||||||
|
|
||||||
var interleaved = new List<SnActivity>();
|
var interleaved = new List<SnActivity>();
|
||||||
@@ -102,7 +98,7 @@ public class ActivityService(
|
|||||||
|
|
||||||
var userRealms = await rs.GetUserRealms(Guid.Parse(currentUser.Id));
|
var userRealms = await rs.GetUserRealms(Guid.Parse(currentUser.Id));
|
||||||
|
|
||||||
// Build and execute the posts query
|
// Build and execute the post query
|
||||||
var postsQuery = BuildPostsQuery(cursor, filteredPublishersId, userRealms);
|
var postsQuery = BuildPostsQuery(cursor, filteredPublishersId, userRealms);
|
||||||
|
|
||||||
// Apply visibility filtering and execute
|
// Apply visibility filtering and execute
|
||||||
@@ -118,10 +114,10 @@ public class ActivityService(
|
|||||||
var posts = await GetAndProcessPosts(
|
var posts = await GetAndProcessPosts(
|
||||||
postsQuery,
|
postsQuery,
|
||||||
currentUser,
|
currentUser,
|
||||||
userFriends,
|
|
||||||
userPublishers,
|
|
||||||
trackViews: true);
|
trackViews: true);
|
||||||
|
|
||||||
|
await LoadPostsRealmsAsync(posts, rs);
|
||||||
|
|
||||||
posts = RankPosts(posts, take);
|
posts = RankPosts(posts, take);
|
||||||
|
|
||||||
var interleaved = new List<SnActivity>();
|
var interleaved = new List<SnActivity>();
|
||||||
@@ -219,15 +215,19 @@ public class ActivityService(
|
|||||||
|
|
||||||
private async Task<SnActivity?> GetShuffledPostsActivity(int count = 5)
|
private async Task<SnActivity?> GetShuffledPostsActivity(int count = 5)
|
||||||
{
|
{
|
||||||
|
var publicRealms = await rs.GetPublicRealms();
|
||||||
|
var publicRealmIds = publicRealms.Select(r => r.Id).ToList();
|
||||||
|
|
||||||
var postsQuery = db.Posts
|
var postsQuery = db.Posts
|
||||||
.Include(p => p.Categories)
|
.Include(p => p.Categories)
|
||||||
.Include(p => p.Tags)
|
.Include(p => p.Tags)
|
||||||
.Include(p => p.Realm)
|
|
||||||
.Where(p => p.RepliedPostId == null)
|
.Where(p => p.RepliedPostId == null)
|
||||||
|
.Where(p => p.RealmId == null || publicRealmIds.Contains(p.RealmId.Value))
|
||||||
.OrderBy(_ => EF.Functions.Random())
|
.OrderBy(_ => EF.Functions.Random())
|
||||||
.Take(count);
|
.Take(count);
|
||||||
|
|
||||||
var posts = await GetAndProcessPosts(postsQuery, trackViews: false);
|
var posts = await GetAndProcessPosts(postsQuery, trackViews: false);
|
||||||
|
await LoadPostsRealmsAsync(posts, rs);
|
||||||
|
|
||||||
return posts.Count == 0
|
return posts.Count == 0
|
||||||
? null
|
? null
|
||||||
@@ -272,8 +272,6 @@ public class ActivityService(
|
|||||||
private async Task<List<SnPost>> GetAndProcessPosts(
|
private async Task<List<SnPost>> GetAndProcessPosts(
|
||||||
IQueryable<SnPost> baseQuery,
|
IQueryable<SnPost> baseQuery,
|
||||||
Account? currentUser = null,
|
Account? currentUser = null,
|
||||||
List<Guid>? userFriends = null,
|
|
||||||
List<Shared.Models.SnPublisher>? userPublishers = null,
|
|
||||||
bool trackViews = true)
|
bool trackViews = true)
|
||||||
{
|
{
|
||||||
var posts = await baseQuery.ToListAsync();
|
var posts = await baseQuery.ToListAsync();
|
||||||
@@ -306,7 +304,7 @@ public class ActivityService(
|
|||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Include(e => e.Realm)
|
.Include(e => e.FeaturedRecords)
|
||||||
.Where(e => e.RepliedPostId == null)
|
.Where(e => e.RepliedPostId == null)
|
||||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
.Where(p => cursor == null || p.PublishedAt < cursor)
|
||||||
.OrderByDescending(p => p.PublishedAt)
|
.OrderByDescending(p => p.PublishedAt)
|
||||||
@@ -315,10 +313,14 @@ public class ActivityService(
|
|||||||
if (filteredPublishersId != null && filteredPublishersId.Count != 0)
|
if (filteredPublishersId != null && filteredPublishersId.Count != 0)
|
||||||
query = query.Where(p => filteredPublishersId.Contains(p.PublisherId));
|
query = query.Where(p => filteredPublishersId.Contains(p.PublisherId));
|
||||||
if (userRealms == null)
|
if (userRealms == null)
|
||||||
query = query.Where(p => p.Realm == null || p.Realm.IsPublic);
|
{
|
||||||
|
// For anonymous users, only show public realm posts or posts without realm
|
||||||
|
// Get public realm ids in the caller and pass them
|
||||||
|
query = query.Where(p => p.RealmId == null); // Modify in caller
|
||||||
|
}
|
||||||
else
|
else
|
||||||
query = query.Where(p =>
|
query = query.Where(p =>
|
||||||
p.Realm == null || p.Realm.IsPublic || p.RealmId == null || userRealms.Contains(p.RealmId.Value));
|
p.RealmId == null || userRealms.Contains(p.RealmId.Value));
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
@@ -339,6 +341,23 @@ public class ActivityService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task LoadPostsRealmsAsync(List<SnPost> posts, RemoteRealmService rs)
|
||||||
|
{
|
||||||
|
var postRealmIds = posts.Where(p => p.RealmId != null).Select(p => p.RealmId.Value).Distinct().ToList();
|
||||||
|
if (!postRealmIds.Any()) return;
|
||||||
|
|
||||||
|
var realms = await rs.GetRealmBatch(postRealmIds.Select(id => id.ToString()).ToList());
|
||||||
|
var realmDict = realms.ToDictionary(r => r.Id, r => r);
|
||||||
|
|
||||||
|
foreach (var post in posts.Where(p => p.RealmId != null))
|
||||||
|
{
|
||||||
|
if (post.RealmId != null && realmDict.TryGetValue(post.RealmId.Value, out var realm))
|
||||||
|
{
|
||||||
|
post.Realm = realm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static double CalculatePopularity(List<SnPost> posts)
|
private static double CalculatePopularity(List<SnPost> posts)
|
||||||
{
|
{
|
||||||
var score = posts.Sum(p => p.Upvotes - p.Downvotes);
|
var score = posts.Sum(p => p.Upvotes - p.Downvotes);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
@@ -33,26 +34,23 @@ public class AppDatabase(
|
|||||||
public DbSet<SnPostFeaturedRecord> PostFeaturedRecords { get; set; } = null!;
|
public DbSet<SnPostFeaturedRecord> PostFeaturedRecords { get; set; } = null!;
|
||||||
public DbSet<SnPostCategorySubscription> PostCategorySubscriptions { get; set; } = null!;
|
public DbSet<SnPostCategorySubscription> PostCategorySubscriptions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<Shared.Models.SnPoll> Polls { get; set; } = null!;
|
public DbSet<SnPoll> Polls { get; set; } = null!;
|
||||||
public DbSet<SnPollQuestion> PollQuestions { get; set; } = null!;
|
public DbSet<SnPollQuestion> PollQuestions { get; set; } = null!;
|
||||||
public DbSet<SnPollAnswer> PollAnswers { get; set; } = null!;
|
public DbSet<SnPollAnswer> PollAnswers { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<Shared.Models.SnRealm> Realms { get; set; } = null!;
|
|
||||||
public DbSet<SnRealmMember> RealmMembers { get; set; } = null!;
|
|
||||||
|
|
||||||
public DbSet<SnChatRoom> ChatRooms { get; set; } = null!;
|
public DbSet<SnChatRoom> ChatRooms { get; set; } = null!;
|
||||||
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
||||||
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
||||||
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
||||||
public DbSet<SnChatMessageReaction> ChatReactions { get; set; } = null!;
|
public DbSet<SnChatMessageReaction> ChatReactions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<Shared.Models.SnSticker> Stickers { get; set; } = null!;
|
public DbSet<SnSticker> Stickers { get; set; } = null!;
|
||||||
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
|
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
|
||||||
public DbSet<StickerPackOwnership> StickerPackOwnerships { get; set; } = null!;
|
public DbSet<StickerPackOwnership> StickerPackOwnerships { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<WebReader.WebArticle> WebArticles { get; set; } = null!;
|
public DbSet<WebArticle> WebArticles { get; set; } = null!;
|
||||||
public DbSet<WebReader.WebFeed> WebFeeds { get; set; } = null!;
|
public DbSet<WebFeed> WebFeeds { get; set; } = null!;
|
||||||
public DbSet<WebReader.WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
|
public DbSet<WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@@ -112,14 +110,6 @@ public class AppDatabase(
|
|||||||
.WithMany(c => c.Posts)
|
.WithMany(c => c.Posts)
|
||||||
.UsingEntity(j => j.ToTable("post_collection_links"));
|
.UsingEntity(j => j.ToTable("post_collection_links"));
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
modelBuilder.Entity<SnChatMember>()
|
modelBuilder.Entity<SnChatMember>()
|
||||||
.HasKey(pm => new { pm.Id });
|
.HasKey(pm => new { pm.Id });
|
||||||
modelBuilder.Entity<SnChatMember>()
|
modelBuilder.Entity<SnChatMember>()
|
||||||
@@ -150,10 +140,10 @@ public class AppDatabase(
|
|||||||
.HasForeignKey(m => m.SenderId)
|
.HasForeignKey(m => m.SenderId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.Entity<WebReader.WebFeed>()
|
modelBuilder.Entity<WebFeed>()
|
||||||
.HasIndex(f => f.Url)
|
.HasIndex(f => f.Url)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
modelBuilder.Entity<WebReader.WebArticle>()
|
modelBuilder.Entity<WebArticle>()
|
||||||
.HasIndex(a => a.Url)
|
.HasIndex(a => a.Url)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Autocompletion;
|
namespace DysonNetwork.Sphere.Autocompletion;
|
||||||
|
|
||||||
public class AutocompletionService(AppDatabase db, AccountClientHelper accountsHelper)
|
public class AutocompletionService(AppDatabase db, RemoteAccountService remoteAccountsHelper, RemoteRealmService remoteRealmService)
|
||||||
{
|
{
|
||||||
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
|
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
|
||||||
{
|
{
|
||||||
@@ -16,7 +16,7 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
|
|||||||
var afterAt = content[1..];
|
var afterAt = content[1..];
|
||||||
string type;
|
string type;
|
||||||
string query;
|
string query;
|
||||||
bool hadSlash = afterAt.Contains('/');
|
var hadSlash = afterAt.Contains('/');
|
||||||
if (hadSlash)
|
if (hadSlash)
|
||||||
{
|
{
|
||||||
var parts = afterAt.Split('/', 2);
|
var parts = afterAt.Split('/', 2);
|
||||||
@@ -47,7 +47,7 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "u":
|
case "u":
|
||||||
var allAccounts = await accountsHelper.SearchAccounts(query);
|
var allAccounts = await remoteAccountsHelper.SearchAccounts(query);
|
||||||
var filteredAccounts = allAccounts;
|
var filteredAccounts = allAccounts;
|
||||||
|
|
||||||
if (chatId.HasValue)
|
if (chatId.HasValue)
|
||||||
@@ -61,12 +61,13 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
|
|||||||
}
|
}
|
||||||
else if (realmId.HasValue)
|
else if (realmId.HasValue)
|
||||||
{
|
{
|
||||||
var realmMemberIds = await db.RealmMembers
|
// TODO: Filter to realm members only - needs efficient implementation
|
||||||
.Where(m => m.RealmId == realmId.Value && m.LeaveAt == null)
|
// var realmMemberIds = await db.RealmMembers
|
||||||
.Select(m => m.AccountId)
|
// .Where(m => m.RealmId == realmId.Value && m.LeaveAt == null)
|
||||||
.ToListAsync();
|
// .Select(m => m.AccountId)
|
||||||
var realmMemberIdStrings = realmMemberIds.Select(id => id.ToString()).ToHashSet();
|
// .ToListAsync();
|
||||||
filteredAccounts = allAccounts.Where(a => realmMemberIdStrings.Contains(a.Id)).ToList();
|
// var realmMemberIdStrings = realmMemberIds.Select(id => id.ToString()).ToHashSet();
|
||||||
|
// filteredAccounts = allAccounts.Where(a => realmMemberIdStrings.Contains(a.Id)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var users = filteredAccounts
|
var users = filteredAccounts
|
||||||
@@ -95,17 +96,14 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "r":
|
case "r":
|
||||||
var realms = await db.Realms
|
var realms = await remoteRealmService.SearchRealms(query, limit);
|
||||||
.Where(r => EF.Functions.Like(r.Slug, $"{query}%") || EF.Functions.Like(r.Name, $"{query}%"))
|
var autocompletions = realms.Select(r => new DysonNetwork.Shared.Models.Autocompletion
|
||||||
.Take(limit)
|
|
||||||
.Select(r => new DysonNetwork.Shared.Models.Autocompletion
|
|
||||||
{
|
{
|
||||||
Type = "realm",
|
Type = "realm",
|
||||||
Keyword = "@r/" + r.Slug,
|
Keyword = "@r/" + r.Slug,
|
||||||
Data = r
|
Data = r
|
||||||
})
|
});
|
||||||
.ToListAsync();
|
results.AddRange(autocompletions);
|
||||||
results.AddRange(realms);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "c":
|
case "c":
|
||||||
@@ -130,30 +128,17 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
|
|||||||
{
|
{
|
||||||
var stickers = await db.Stickers
|
var stickers = await db.Stickers
|
||||||
.Include(s => s.Pack)
|
.Include(s => s.Pack)
|
||||||
.Where(s => EF.Functions.Like(s.Slug, $"{query}%"))
|
.Where(s => EF.Functions.Like(s.Pack.Prefix + "+" + s.Slug, $"{query}%"))
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.Select(s => new DysonNetwork.Shared.Models.Autocompletion
|
.Select(s => new DysonNetwork.Shared.Models.Autocompletion
|
||||||
{
|
{
|
||||||
Type = "sticker",
|
Type = "sticker",
|
||||||
Keyword = s.Slug,
|
Keyword = $":{s.Pack.Prefix}+{s.Slug}:",
|
||||||
Data = s
|
Data = s
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// Also possibly search by pack prefix? But user said slug after :
|
var results = stickers.ToList();
|
||||||
// Perhaps combine or search packs
|
|
||||||
var packs = await db.StickerPacks
|
|
||||||
.Where(p => EF.Functions.Like(p.Prefix, $"{query}%"))
|
|
||||||
.Take(limit)
|
|
||||||
.Select(p => new DysonNetwork.Shared.Models.Autocompletion
|
|
||||||
{
|
|
||||||
Type = "sticker_pack",
|
|
||||||
Keyword = p.Prefix,
|
|
||||||
Data = p
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var results = stickers.Concat(packs).Take(limit).ToList();
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
|
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null &&
|
||||||
|
m.LeaveAt == null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member == null || member.Role < ChatMemberRole.Member)
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
@@ -129,7 +130,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
|
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null &&
|
||||||
|
m.LeaveAt == null)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member == null || member.Role < ChatMemberRole.Member)
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
@@ -148,9 +150,74 @@ public partial class ChatController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[GeneratedRegex("@([A-Za-z0-9_-]+)")]
|
[GeneratedRegex(@"@(?:u/)?([A-Za-z0-9_-]+)")]
|
||||||
private static partial Regex MentionRegex();
|
private static partial Regex MentionRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts mentioned users from message content, replies, and forwards
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<Guid>> ExtractMentionedUsersAsync(string? content, Guid? repliedMessageId,
|
||||||
|
Guid? forwardedMessageId, Guid roomId, Guid? excludeSenderId = null)
|
||||||
|
{
|
||||||
|
var mentionedUsers = new List<Guid>();
|
||||||
|
|
||||||
|
// Add sender of a replied message
|
||||||
|
if (repliedMessageId.HasValue)
|
||||||
|
{
|
||||||
|
var replyingTo = await db.ChatMessages
|
||||||
|
.Where(m => m.Id == repliedMessageId.Value && m.ChatRoomId == roomId)
|
||||||
|
.Include(m => m.Sender)
|
||||||
|
.Select(m => m.Sender)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (replyingTo != null)
|
||||||
|
mentionedUsers.Add(replyingTo.AccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sender of a forwarded message
|
||||||
|
if (forwardedMessageId.HasValue)
|
||||||
|
{
|
||||||
|
var forwardedMessage = await db.ChatMessages
|
||||||
|
.Where(m => m.Id == forwardedMessageId.Value)
|
||||||
|
.Select(m => new { m.SenderId })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (forwardedMessage != null)
|
||||||
|
{
|
||||||
|
mentionedUsers.Add(forwardedMessage.SenderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract mentions from content using regex
|
||||||
|
if (!string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
var mentionedNames = MentionRegex()
|
||||||
|
.Matches(content)
|
||||||
|
.Select(m => m.Groups[1].Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (mentionedNames.Count > 0)
|
||||||
|
{
|
||||||
|
var queryRequest = new LookupAccountBatchRequest();
|
||||||
|
queryRequest.Names.AddRange(mentionedNames);
|
||||||
|
var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts;
|
||||||
|
var mentionedIds = queryResponse.Select(a => Guid.Parse(a.Id)).ToList();
|
||||||
|
|
||||||
|
if (mentionedIds.Count > 0)
|
||||||
|
{
|
||||||
|
var mentionedMembers = await db.ChatMembers
|
||||||
|
.Where(m => m.ChatRoomId == roomId && mentionedIds.Contains(m.AccountId))
|
||||||
|
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||||
|
.Where(m => excludeSenderId == null || m.AccountId != excludeSenderId.Value)
|
||||||
|
.Select(m => m.AccountId)
|
||||||
|
.ToListAsync();
|
||||||
|
mentionedUsers.AddRange(mentionedMembers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mentionedUsers.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{roomId:guid}/messages")]
|
[HttpPost("{roomId:guid}/messages")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "chat.messages.create")]
|
[RequiredPermission("global", "chat.messages.create")]
|
||||||
@@ -188,6 +255,7 @@ public partial class ChatController(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate reply and forward message IDs exist
|
||||||
if (request.RepliedMessageId.HasValue)
|
if (request.RepliedMessageId.HasValue)
|
||||||
{
|
{
|
||||||
var repliedMessage = await db.ChatMessages
|
var repliedMessage = await db.ChatMessages
|
||||||
@@ -208,28 +276,9 @@ public partial class ChatController(
|
|||||||
message.ForwardedMessageId = forwardedMessage.Id;
|
message.ForwardedMessageId = forwardedMessage.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Content is not null)
|
// Extract mentioned users
|
||||||
{
|
message.MembersMentioned = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
|
||||||
var mentioned = MentionRegex()
|
request.ForwardedMessageId, roomId);
|
||||||
.Matches(request.Content)
|
|
||||||
.Select(m => m.Groups[1].Value)
|
|
||||||
.ToList();
|
|
||||||
if (mentioned.Count > 0)
|
|
||||||
{
|
|
||||||
var queryRequest = new LookupAccountBatchRequest();
|
|
||||||
queryRequest.Names.AddRange(mentioned);
|
|
||||||
var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts;
|
|
||||||
var mentionedId = queryResponse
|
|
||||||
.Select(a => Guid.Parse(a.Id))
|
|
||||||
.ToList();
|
|
||||||
var mentionedMembers = await db.ChatMembers
|
|
||||||
.Where(m => m.ChatRoomId == roomId && mentionedId.Contains(m.AccountId))
|
|
||||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
|
||||||
.Select(m => m.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
message.MembersMentioned = mentionedMembers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
||||||
|
|
||||||
@@ -259,6 +308,7 @@ public partial class ChatController(
|
|||||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||||
return BadRequest("You cannot send an empty message.");
|
return BadRequest("You cannot send an empty message.");
|
||||||
|
|
||||||
|
// Validate reply and forward message IDs exist
|
||||||
if (request.RepliedMessageId.HasValue)
|
if (request.RepliedMessageId.HasValue)
|
||||||
{
|
{
|
||||||
var repliedMessage = await db.ChatMessages
|
var repliedMessage = await db.ChatMessages
|
||||||
@@ -275,6 +325,11 @@ public partial class ChatController(
|
|||||||
return BadRequest("The message you're forwarding does not exist.");
|
return BadRequest("The message you're forwarding does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update mentions based on new content and references
|
||||||
|
var updatedMentions = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
|
||||||
|
request.ForwardedMessageId, roomId, accountId);
|
||||||
|
message.MembersMentioned = updatedMentions;
|
||||||
|
|
||||||
// Call service method to update the message
|
// Call service method to update the message
|
||||||
await cs.UpdateMessageAsync(
|
await cs.UpdateMessageAsync(
|
||||||
message,
|
message,
|
||||||
@@ -324,7 +379,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var isMember = await db.ChatMembers
|
var isMember = await db.ChatMembers
|
||||||
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
.AnyAsync(m =>
|
||||||
|
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
||||||
if (!isMember)
|
if (!isMember)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
|
|
||||||
@@ -333,14 +389,16 @@ public partial class ChatController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{roomId:guid}/autocomplete")]
|
[HttpPost("{roomId:guid}/autocomplete")]
|
||||||
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId)
|
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete(
|
||||||
|
[FromBody] AutocompletionRequest request, Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var isMember = await db.ChatMembers
|
var isMember = await db.ChatMembers
|
||||||
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
.AnyAsync(m =>
|
||||||
|
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
||||||
if (!isMember)
|
if (!isMember)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
@@ -20,14 +20,14 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
public class ChatRoomController(
|
public class ChatRoomController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ChatRoomService crs,
|
ChatRoomService crs,
|
||||||
RealmService rs,
|
RemoteRealmService rs,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
ActionLogService.ActionLogServiceClient als,
|
ActionLogService.ActionLogServiceClient als,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
AccountClientHelper accountsHelper
|
RemoteAccountService remoteAccountsHelper
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
@@ -35,9 +35,12 @@ public class ChatRoomController(
|
|||||||
{
|
{
|
||||||
var chatRoom = await db.ChatRooms
|
var chatRoom = await db.ChatRooms
|
||||||
.Where(c => c.Id == id)
|
.Where(c => c.Id == id)
|
||||||
.Include(e => e.Realm)
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (chatRoom is null) return NotFound();
|
if (chatRoom is null) return NotFound();
|
||||||
|
|
||||||
|
if (chatRoom.RealmId != null)
|
||||||
|
chatRoom.Realm = await rs.GetRealm(chatRoom.RealmId.Value.ToString());
|
||||||
|
|
||||||
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
||||||
|
|
||||||
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
||||||
@@ -203,7 +206,7 @@ public class ChatRoomController(
|
|||||||
if (request.RealmId is not null)
|
if (request.RealmId is not null)
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id),
|
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||||
RealmMemberRole.Moderator))
|
[RealmMemberRole.Moderator]))
|
||||||
return StatusCode(403, "You need at least be a moderator to create chat linked to the realm.");
|
return StatusCode(403, "You need at least be a moderator to create chat linked to the realm.");
|
||||||
chatRoom.RealmId = request.RealmId;
|
chatRoom.RealmId = request.RealmId;
|
||||||
}
|
}
|
||||||
@@ -301,7 +304,7 @@ public class ChatRoomController(
|
|||||||
if (chatRoom.RealmId is not null)
|
if (chatRoom.RealmId is not null)
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||||
RealmMemberRole.Moderator))
|
[RealmMemberRole.Moderator]))
|
||||||
return StatusCode(403, "You need at least be a realm moderator to update the chat.");
|
return StatusCode(403, "You need at least be a realm moderator to update the chat.");
|
||||||
}
|
}
|
||||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
||||||
@@ -309,13 +312,9 @@ public class ChatRoomController(
|
|||||||
|
|
||||||
if (request.RealmId is not null)
|
if (request.RealmId is not null)
|
||||||
{
|
{
|
||||||
var member = await db.RealmMembers
|
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id), [RealmMemberRole.Moderator]))
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
|
||||||
.Where(m => m.RealmId == request.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a moderator to transfer the chat linked to the realm.");
|
return StatusCode(403, "You need at least be a moderator to transfer the chat linked to the realm.");
|
||||||
chatRoom.RealmId = member.RealmId;
|
chatRoom.RealmId = request.RealmId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
if (request.PictureId is not null)
|
||||||
@@ -415,7 +414,7 @@ public class ChatRoomController(
|
|||||||
if (chatRoom.RealmId is not null)
|
if (chatRoom.RealmId is not null)
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||||
RealmMemberRole.Moderator))
|
[RealmMemberRole.Moderator]))
|
||||||
return StatusCode(403, "You need at least be a realm moderator to delete the chat.");
|
return StatusCode(403, "You need at least be a realm moderator to delete the chat.");
|
||||||
}
|
}
|
||||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner))
|
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner))
|
||||||
@@ -507,7 +506,7 @@ public class ChatRoomController(
|
|||||||
.Select(m => m.AccountId)
|
.Select(m => m.AccountId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var memberStatuses = await accountsHelper.GetAccountStatusBatch(members);
|
var memberStatuses = await remoteAccountsHelper.GetAccountStatusBatch(members);
|
||||||
|
|
||||||
var onlineCount = memberStatuses.Count(s => s.Value.IsOnline);
|
var onlineCount = memberStatuses.Count(s => s.Value.IsOnline);
|
||||||
|
|
||||||
@@ -546,7 +545,7 @@ public class ChatRoomController(
|
|||||||
.OrderBy(m => m.JoinedAt)
|
.OrderBy(m => m.JoinedAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
|
var memberStatuses = await remoteAccountsHelper.GetAccountStatusBatch(
|
||||||
members.Select(m => m.AccountId).ToList()
|
members.Select(m => m.AccountId).ToList()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -623,11 +622,7 @@ public class ChatRoomController(
|
|||||||
// Handle realm-owned chat rooms
|
// Handle realm-owned chat rooms
|
||||||
if (chatRoom.RealmId is not null)
|
if (chatRoom.RealmId is not null)
|
||||||
{
|
{
|
||||||
var realmMember = await db.RealmMembers
|
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
|
||||||
.Where(m => m.AccountId == accountId)
|
|
||||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to invite members to this chat.");
|
return StatusCode(403, "You need at least be a realm moderator to invite members to this chat.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -832,11 +827,7 @@ public class ChatRoomController(
|
|||||||
// Check if the chat room is owned by a realm
|
// Check if the chat room is owned by a realm
|
||||||
if (chatRoom.RealmId is not null)
|
if (chatRoom.RealmId is not null)
|
||||||
{
|
{
|
||||||
var realmMember = await db.RealmMembers
|
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id), [RealmMemberRole.Moderator]))
|
||||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
|
||||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
|
||||||
return StatusCode(403, "You need at least be a realm moderator to change member roles.");
|
return StatusCode(403, "You need at least be a realm moderator to change member roles.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -899,12 +890,12 @@ public class ChatRoomController(
|
|||||||
if (chatRoom.RealmId is not null)
|
if (chatRoom.RealmId is not null)
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||||
RealmMemberRole.Moderator))
|
[RealmMemberRole.Moderator]))
|
||||||
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), [ChatMemberRole.Moderator]))
|
||||||
return StatusCode(403, "You need at least be a moderator to remove members.");
|
return StatusCode(403, "You need at least be a moderator to remove members.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
public class ChatRoomService(
|
public class ChatRoomService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
AccountClientHelper accountsHelper
|
RemoteAccountService remoteAccountsHelper
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private const string ChatRoomGroupPrefix = "chatroom:";
|
private const string ChatRoomGroupPrefix = "chatroom:";
|
||||||
@@ -147,7 +147,7 @@ public class ChatRoomService(
|
|||||||
|
|
||||||
public async Task<SnChatMember> LoadMemberAccount(SnChatMember member)
|
public async Task<SnChatMember> LoadMemberAccount(SnChatMember member)
|
||||||
{
|
{
|
||||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
var account = await remoteAccountsHelper.GetAccount(member.AccountId);
|
||||||
member.Account = SnAccount.FromProtoValue(account);
|
member.Account = SnAccount.FromProtoValue(account);
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ public class ChatRoomService(
|
|||||||
public async Task<List<SnChatMember>> LoadMemberAccounts(ICollection<SnChatMember> members)
|
public async Task<List<SnChatMember>> LoadMemberAccounts(ICollection<SnChatMember> members)
|
||||||
{
|
{
|
||||||
var accountIds = members.Select(m => m.AccountId).ToList();
|
var accountIds = members.Select(m => m.AccountId).ToList();
|
||||||
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
var accounts = (await remoteAccountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
||||||
|
|
||||||
return
|
return
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -198,8 +198,6 @@ public partial class ChatService(
|
|||||||
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
|
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
||||||
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
message.UpdatedAt = message.CreatedAt;
|
|
||||||
|
|
||||||
// First complete the save operation
|
// First complete the save operation
|
||||||
db.ChatMessages.Add(message);
|
db.ChatMessages.Add(message);
|
||||||
@@ -209,20 +207,25 @@ public partial class ChatService(
|
|||||||
await CreateFileReferencesForMessageAsync(message);
|
await CreateFileReferencesForMessageAsync(message);
|
||||||
|
|
||||||
// Then start the delivery process
|
// Then start the delivery process
|
||||||
|
var localMessage = message;
|
||||||
|
var localSender = sender;
|
||||||
|
var localRoom = room;
|
||||||
|
var localLogger = logger;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DeliverMessageAsync(message, sender, room);
|
await DeliverMessageAsync(localMessage, localSender, localRoom);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}");
|
localLogger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process link preview in the background to avoid delaying message sending
|
// Process link preview in the background to avoid delaying message sending
|
||||||
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message));
|
var localMessageForPreview = message;
|
||||||
|
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(localMessageForPreview));
|
||||||
|
|
||||||
message.Sender = sender;
|
message.Sender = sender;
|
||||||
message.ChatRoom = room;
|
message.ChatRoom = room;
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Realm;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/realms/{slug}")]
|
[Route("/api/realms/{slug}")]
|
||||||
public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBase
|
public class RealmChatController(AppDatabase db, RemoteRealmService rs) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("chat")]
|
[HttpGet("chat")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<List<SnChatRoom>>> ListRealmChat(string slug)
|
public async Task<ActionResult<List<SnChatRoom>>> ListRealmChat(string slug)
|
||||||
{
|
{
|
||||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Proto.Account;
|
||||||
var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id);
|
var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
var realm = await db.Realms
|
var realm = await rs.GetRealmBySlug(slug);
|
||||||
.Where(r => r.Slug == slug)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (realm is null) return NotFound();
|
|
||||||
if (!realm.IsPublic)
|
if (!realm.IsPublic)
|
||||||
{
|
{
|
||||||
if (currentUser is null) return Unauthorized();
|
if (currentUser is null) return Unauthorized();
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, [RealmMemberRole.Normal]))
|
||||||
return StatusCode(403, "You need at least one member to view the realm's chat.");
|
return StatusCode(403, "You need at least one member to view the realm's chat.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,30 +1,31 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using DysonNetwork.Shared.Registry;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Discovery;
|
namespace DysonNetwork.Sphere.Discovery;
|
||||||
|
|
||||||
public class DiscoveryService(AppDatabase appDatabase)
|
public class DiscoveryService(RemoteRealmService remoteRealmService)
|
||||||
{
|
{
|
||||||
public Task<List<Shared.Models.SnRealm>> GetCommunityRealmAsync(
|
public async Task<List<Shared.Models.SnRealm>> GetCommunityRealmAsync(
|
||||||
string? query,
|
string? query,
|
||||||
int take = 10,
|
int take = 10,
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
bool randomizer = false
|
bool randomizer = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var realmsQuery = appDatabase.Realms
|
var allRealms = await remoteRealmService.GetPublicRealms();
|
||||||
.Where(r => r.IsCommunity)
|
var communityRealms = allRealms.Where(r => r.IsCommunity);
|
||||||
.OrderByDescending(r => r.CreatedAt)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(query))
|
if (!string.IsNullOrEmpty(query))
|
||||||
realmsQuery = realmsQuery.Where(r =>
|
{
|
||||||
EF.Functions.ILike(r.Name, $"%{query}%") ||
|
communityRealms = communityRealms.Where(r =>
|
||||||
EF.Functions.ILike(r.Description, $"%{query}%")
|
r.Name.Contains(query, StringComparison.OrdinalIgnoreCase)
|
||||||
);
|
);
|
||||||
realmsQuery = randomizer
|
}
|
||||||
? realmsQuery.OrderBy(r => EF.Functions.Random())
|
|
||||||
: realmsQuery.OrderByDescending(r => r.CreatedAt);
|
|
||||||
|
|
||||||
return realmsQuery.Skip(offset).Take(take).ToListAsync();
|
// Since we don't have CreatedAt in the proto model, we'll just apply randomizer if requested
|
||||||
|
var orderedRealms = randomizer
|
||||||
|
? communityRealms.OrderBy(_ => Random.Shared.Next())
|
||||||
|
: communityRealms;
|
||||||
|
|
||||||
|
return orderedRealms.Skip(offset).Take(take).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.3" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.3" />
|
||||||
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.10" />
|
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.10" />
|
||||||
<PackageReference Include="Markdig" Version="0.41.3"/>
|
<PackageReference Include="Markdig" Version="0.41.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" 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.AspNetCore" Version="1.13.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" 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" Version="8.2.1"/>
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
|
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
|
||||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/>
|
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0"/>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.7" />
|
||||||
<PackageReference Include="TencentCloudSDK.Tmt" Version="3.0.1276" />
|
<PackageReference Include="TencentCloudSDK.Tmt" Version="3.0.1276" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
2130
DysonNetwork.Sphere/Migrations/20251021154500_ChangeRealmReferenceMode.Designer.cs
generated
Normal file
2130
DysonNetwork.Sphere/Migrations/20251021154500_ChangeRealmReferenceMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ChangeRealmReferenceMode : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_posts_realms_realm_id",
|
||||||
|
table: "posts");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_posts_realm_id",
|
||||||
|
table: "posts");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_chat_rooms_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_rooms_sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "sn_realm_id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "sn_realm_id",
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_chat_rooms_sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_posts_realm_id",
|
||||||
|
table: "posts",
|
||||||
|
column: "realm_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_rooms_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "realm_id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "realm_id",
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_posts_realms_realm_id",
|
||||||
|
table: "posts",
|
||||||
|
column: "realm_id",
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1962
DysonNetwork.Sphere/Migrations/20251022170100_RemoveRealms.Designer.cs
generated
Normal file
1962
DysonNetwork.Sphere/Migrations/20251022170100_RemoveRealms.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
132
DysonNetwork.Sphere/Migrations/20251022170100_RemoveRealms.cs
Normal file
132
DysonNetwork.Sphere/Migrations/20251022170100_RemoveRealms.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveRealms : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_publishers_realms_realm_id",
|
||||||
|
table: "publishers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "realm_members");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "realms");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_publishers_realm_id",
|
||||||
|
table: "publishers");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_chat_rooms_sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "sn_realm_id",
|
||||||
|
table: "chat_rooms");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "realms",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_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: false),
|
||||||
|
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: false),
|
||||||
|
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
verification = table.Column<SnVerificationMark>(type: "jsonb", 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),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_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),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
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.CreateIndex(
|
||||||
|
name: "ix_publishers_realm_id",
|
||||||
|
table: "publishers",
|
||||||
|
column: "realm_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_rooms_sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "sn_realm_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_realms_slug",
|
||||||
|
table: "realms",
|
||||||
|
column: "slug",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||||
|
table: "chat_rooms",
|
||||||
|
column: "sn_realm_id",
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_publishers_realms_realm_id",
|
||||||
|
table: "publishers",
|
||||||
|
column: "realm_id",
|
||||||
|
principalTable: "realms",
|
||||||
|
principalColumn: "id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ public class PollController(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PollService polls,
|
PollService polls,
|
||||||
Publisher.PublisherService pub,
|
Publisher.PublisherService pub,
|
||||||
AccountClientHelper accountsHelper
|
RemoteAccountService remoteAccountsHelper
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
@@ -110,7 +110,7 @@ public class PollController(
|
|||||||
if (!poll.IsAnonymous)
|
if (!poll.IsAnonymous)
|
||||||
{
|
{
|
||||||
var answeredAccountsId = answers.Select(x => x.AccountId).Distinct().ToList();
|
var answeredAccountsId = answers.Select(x => x.AccountId).Distinct().ToList();
|
||||||
var answeredAccounts = await accountsHelper.GetAccountBatch(answeredAccountsId);
|
var answeredAccounts = await remoteAccountsHelper.GetAccountBatch(answeredAccountsId);
|
||||||
|
|
||||||
// Populate Account field for each answer
|
// Populate Account field for each answer
|
||||||
foreach (var answer in answers)
|
foreach (var answer in answers)
|
||||||
|
|||||||
0
DysonNetwork.Sphere/Post/AccountHelperClient.cs
Normal file
0
DysonNetwork.Sphere/Post/AccountHelperClient.cs
Normal file
@@ -4,8 +4,9 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Poll;
|
using DysonNetwork.Sphere.Poll;
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -23,11 +24,12 @@ public class PostController(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PostService ps,
|
PostService ps,
|
||||||
PublisherService pub,
|
PublisherService pub,
|
||||||
|
RemoteAccountService remoteAccountsHelper,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
ActionLogService.ActionLogServiceClient als,
|
ActionLogService.ActionLogServiceClient als,
|
||||||
PaymentService.PaymentServiceClient payments,
|
PaymentService.PaymentServiceClient payments,
|
||||||
PollService polls,
|
PollService polls,
|
||||||
RealmService rs
|
RemoteRealmService rs
|
||||||
)
|
)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
@@ -103,17 +105,20 @@ public class PostController(
|
|||||||
|
|
||||||
var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id);
|
var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id);
|
||||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(accountId);
|
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(accountId);
|
||||||
var userRealms = currentUser is null ? [] : await rs.GetUserRealms(accountId);
|
var userRealms = currentUser is null ? new List<Guid>() : await rs.GetUserRealms(accountId);
|
||||||
|
var publicRealms = await rs.GetPublicRealms();
|
||||||
|
var publicRealmIds = publicRealms.Select(r => r.Id).ToList();
|
||||||
|
var visibleRealmIds = userRealms.Concat(publicRealmIds).Distinct().ToList();
|
||||||
|
|
||||||
var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName);
|
var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName);
|
||||||
var realm = realmName == null ? null : await db.Realms.FirstOrDefaultAsync(r => r.Slug == realmName);
|
var realm = realmName == null ? null : (await rs.GetRealmBySlug(realmName));
|
||||||
|
|
||||||
var query = db.Posts
|
var query = db.Posts
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Realm)
|
.Include(e => e.FeaturedRecords)
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
if (publisher != null)
|
if (publisher != null)
|
||||||
query = query.Where(p => p.PublisherId == publisher.Id);
|
query = query.Where(p => p.PublisherId == publisher.Id);
|
||||||
@@ -129,8 +134,7 @@ public class PostController(
|
|||||||
query = query.Where(e => e.Attachments.Count > 0);
|
query = query.Where(e => e.Attachments.Count > 0);
|
||||||
|
|
||||||
if (realm == null)
|
if (realm == null)
|
||||||
query = query.Where(p =>
|
query = query.Where(p => p.RealmId == null || visibleRealmIds.Contains(p.RealmId.Value));
|
||||||
p.RealmId == null || p.Realm == null || userRealms.Contains(p.RealmId.Value) || p.Realm.IsPublic);
|
|
||||||
|
|
||||||
switch (pinned)
|
switch (pinned)
|
||||||
{
|
{
|
||||||
@@ -183,11 +187,31 @@ public class PostController(
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
||||||
|
|
||||||
|
// Load realm data for posts that have realm
|
||||||
|
await LoadPostsRealmsAsync(posts, rs);
|
||||||
|
|
||||||
Response.Headers["X-Total"] = totalCount.ToString();
|
Response.Headers["X-Total"] = totalCount.ToString();
|
||||||
|
|
||||||
return Ok(posts);
|
return Ok(posts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task LoadPostsRealmsAsync(List<SnPost> posts, RemoteRealmService rs)
|
||||||
|
{
|
||||||
|
var postRealmIds = posts.Where(p => p.RealmId != null).Select(p => p.RealmId.Value).Distinct().ToList();
|
||||||
|
if (!postRealmIds.Any()) return;
|
||||||
|
|
||||||
|
var realms = await rs.GetRealmBatch(postRealmIds.Select(id => id.ToString()).ToList());
|
||||||
|
var realmDict = realms.GroupBy(r => r.Id).ToDictionary(g => g.Key, g => g.FirstOrDefault());
|
||||||
|
|
||||||
|
foreach (var post in posts.Where(p => p.RealmId != null))
|
||||||
|
{
|
||||||
|
if (post.RealmId != null && realmDict.TryGetValue(post.RealmId.Value, out var realm))
|
||||||
|
{
|
||||||
|
post.Realm = realm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{publisherName}/{slug}")]
|
[HttpGet("{publisherName}/{slug}")]
|
||||||
public async Task<ActionResult<SnPost>> GetPost(string publisherName, string slug)
|
public async Task<ActionResult<SnPost>> GetPost(string publisherName, string slug)
|
||||||
{
|
{
|
||||||
@@ -206,15 +230,17 @@ public class PostController(
|
|||||||
var post = await db.Posts
|
var post = await db.Posts
|
||||||
.Include(e => e.Publisher)
|
.Include(e => e.Publisher)
|
||||||
.Where(e => e.Slug == slug && e.Publisher.Name == publisherName)
|
.Where(e => e.Slug == slug && e.Publisher.Name == publisherName)
|
||||||
.Include(e => e.Realm)
|
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
|
.Include(e => e.FeaturedRecords)
|
||||||
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (post is null) return NotFound();
|
if (post is null) return NotFound();
|
||||||
post = await ps.LoadPostInfo(post, currentUser);
|
post = await ps.LoadPostInfo(post, currentUser);
|
||||||
|
if (post.RealmId != null)
|
||||||
|
post.Realm = await rs.GetRealm(post.RealmId.Value.ToString());
|
||||||
|
|
||||||
return Ok(post);
|
return Ok(post);
|
||||||
}
|
}
|
||||||
@@ -237,15 +263,19 @@ public class PostController(
|
|||||||
var post = await db.Posts
|
var post = await db.Posts
|
||||||
.Where(e => e.Id == id)
|
.Where(e => e.Id == id)
|
||||||
.Include(e => e.Publisher)
|
.Include(e => e.Publisher)
|
||||||
.Include(e => e.Realm)
|
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
|
.Include(e => e.FeaturedRecords)
|
||||||
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (post is null) return NotFound();
|
if (post is null) return NotFound();
|
||||||
post = await ps.LoadPostInfo(post, currentUser);
|
post = await ps.LoadPostInfo(post, currentUser);
|
||||||
|
if (post.RealmId != null)
|
||||||
|
{
|
||||||
|
post.Realm = await rs.GetRealm(post.RealmId.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(post);
|
return Ok(post);
|
||||||
}
|
}
|
||||||
@@ -271,6 +301,14 @@ public class PostController(
|
|||||||
.Take(take)
|
.Take(take)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
var accountsProto = await remoteAccountsHelper.GetAccountBatch(reactions.Select(r => r.AccountId).ToList());
|
||||||
|
var accounts = accountsProto.ToDictionary(a => Guid.Parse(a.Id), a => SnAccount.FromProtoValue(a));
|
||||||
|
|
||||||
|
foreach (var reaction in reactions)
|
||||||
|
if (accounts.TryGetValue(reaction.AccountId, out var account))
|
||||||
|
reaction.Account = account;
|
||||||
|
|
||||||
return Ok(reactions);
|
return Ok(reactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +400,7 @@ public class PostController(
|
|||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
|
.Include(e => e.FeaturedRecords)
|
||||||
.FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true)
|
||||||
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
@@ -448,7 +487,10 @@ public class PostController(
|
|||||||
|
|
||||||
if (request.RepliedPostId is not null)
|
if (request.RepliedPostId is not null)
|
||||||
{
|
{
|
||||||
var repliedPost = await db.Posts.FindAsync(request.RepliedPostId.Value);
|
var repliedPost = await db.Posts
|
||||||
|
.Where(p => p.Id == request.RepliedPostId.Value)
|
||||||
|
.Include(p => p.Publisher)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
if (repliedPost is null) return BadRequest("Post replying to was not found.");
|
if (repliedPost is null) return BadRequest("Post replying to was not found.");
|
||||||
post.RepliedPost = repliedPost;
|
post.RepliedPost = repliedPost;
|
||||||
post.RepliedPostId = repliedPost.Id;
|
post.RepliedPostId = repliedPost.Id;
|
||||||
@@ -456,7 +498,10 @@ public class PostController(
|
|||||||
|
|
||||||
if (request.ForwardedPostId is not null)
|
if (request.ForwardedPostId is not null)
|
||||||
{
|
{
|
||||||
var forwardedPost = await db.Posts.FindAsync(request.ForwardedPostId.Value);
|
var forwardedPost = await db.Posts
|
||||||
|
.Where(p => p.Id == request.ForwardedPostId.Value)
|
||||||
|
.Include(p => p.Publisher)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
if (forwardedPost is null) return BadRequest("Forwarded post was not found.");
|
if (forwardedPost is null) return BadRequest("Forwarded post was not found.");
|
||||||
post.ForwardedPost = forwardedPost;
|
post.ForwardedPost = forwardedPost;
|
||||||
post.ForwardedPostId = forwardedPost.Id;
|
post.ForwardedPostId = forwardedPost.Id;
|
||||||
@@ -464,9 +509,8 @@ public class PostController(
|
|||||||
|
|
||||||
if (request.RealmId is not null)
|
if (request.RealmId is not null)
|
||||||
{
|
{
|
||||||
var realm = await db.Realms.FindAsync(request.RealmId.Value);
|
var realm = await rs.GetRealm(request.RealmId.Value.ToString());
|
||||||
if (realm is null) return BadRequest("Realm was not found.");
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, new List<int> { RealmMemberRole.Normal }))
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
|
|
||||||
return StatusCode(403, "You are not a member of this realm.");
|
return StatusCode(403, "You are not a member of this realm.");
|
||||||
post.RealmId = realm.Id;
|
post.RealmId = realm.Id;
|
||||||
}
|
}
|
||||||
@@ -692,7 +736,7 @@ public class PostController(
|
|||||||
|
|
||||||
if (request.Mode == PostPinMode.RealmPage && post.RealmId != null)
|
if (request.Mode == PostPinMode.RealmPage && post.RealmId != null)
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator))
|
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
|
||||||
return StatusCode(403, "You are not a moderator of this realm");
|
return StatusCode(403, "You are not a moderator of this realm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +784,7 @@ public class PostController(
|
|||||||
|
|
||||||
if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null })
|
if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null })
|
||||||
{
|
{
|
||||||
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator))
|
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
|
||||||
return StatusCode(403, "You are not a moderator of this realm");
|
return StatusCode(403, "You are not a moderator of this realm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +829,7 @@ public class PostController(
|
|||||||
.Include(e => e.Publisher)
|
.Include(e => e.Publisher)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
|
.Include(e => e.FeaturedRecords)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (post is null) return NotFound();
|
if (post is null) return NotFound();
|
||||||
|
|
||||||
@@ -849,9 +894,8 @@ public class PostController(
|
|||||||
// The realm is the same as well as the poll
|
// The realm is the same as well as the poll
|
||||||
if (request.RealmId is not null)
|
if (request.RealmId is not null)
|
||||||
{
|
{
|
||||||
var realm = await db.Realms.FindAsync(request.RealmId.Value);
|
var realm = await rs.GetRealm(request.RealmId.Value.ToString());
|
||||||
if (realm is null) return BadRequest("Realm was not found.");
|
if (!await rs.IsMemberWithRole(realm.Id, accountId, new List<int> { RealmMemberRole.Normal }))
|
||||||
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
|
|
||||||
return StatusCode(403, "You are not a member of this realm.");
|
return StatusCode(403, "You are not a member of this realm.");
|
||||||
post.RealmId = realm.Id;
|
post.RealmId = realm.Id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ public partial class PostService(
|
|||||||
ILogger<PostService> logger,
|
ILogger<PostService> logger,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
PollService polls,
|
|
||||||
Publisher.PublisherService ps,
|
Publisher.PublisherService ps,
|
||||||
WebReaderService reader
|
WebReaderService reader,
|
||||||
|
AccountService.AccountServiceClient accounts
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private const string PostFileUsageIdentifier = "post";
|
private const string PostFileUsageIdentifier = "post";
|
||||||
@@ -685,6 +685,8 @@ public partial class PostService(
|
|||||||
post.ForwardedPost.Publisher = forwardedPublisher;
|
post.ForwardedPost.Publisher = forwardedPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ps.LoadIndividualPublisherAccounts(publishers.Values);
|
||||||
|
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,6 +702,16 @@ public partial class PostService(
|
|||||||
: new Dictionary<Guid, Dictionary<string, bool>>();
|
: new Dictionary<Guid, Dictionary<string, bool>>();
|
||||||
var repliesCountMap = await GetPostRepliesCountBatch(postsId);
|
var repliesCountMap = await GetPostRepliesCountBatch(postsId);
|
||||||
|
|
||||||
|
// Load user friends if the current user exists
|
||||||
|
List<SnPublisher> publishers = [];
|
||||||
|
List<Guid> userFriends = [];
|
||||||
|
if (currentUser is not null)
|
||||||
|
{
|
||||||
|
var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest { AccountId = currentUser.Id });
|
||||||
|
userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
|
||||||
|
publishers = await ps.GetUserPublishers(Guid.Parse(currentUser.Id));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var post in posts)
|
foreach (var post in posts)
|
||||||
{
|
{
|
||||||
// Set reaction count
|
// Set reaction count
|
||||||
@@ -717,6 +729,26 @@ public partial class PostService(
|
|||||||
? repliesCount
|
? repliesCount
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
// Check visibility for replied post
|
||||||
|
if (post.RepliedPost != null)
|
||||||
|
{
|
||||||
|
if (!CanViewPost(post.RepliedPost, currentUser, publishers, userFriends))
|
||||||
|
{
|
||||||
|
post.RepliedPost = null;
|
||||||
|
post.RepliedGone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check visibility for forwarded post
|
||||||
|
if (post.ForwardedPost != null)
|
||||||
|
{
|
||||||
|
if (!CanViewPost(post.ForwardedPost, currentUser, publishers, userFriends))
|
||||||
|
{
|
||||||
|
post.ForwardedPost = null;
|
||||||
|
post.ForwardedGone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Track view for each post in the list
|
// Track view for each post in the list
|
||||||
if (currentUser != null)
|
if (currentUser != null)
|
||||||
await IncreaseViewCount(post.Id, currentUser.Id);
|
await IncreaseViewCount(post.Id, currentUser.Id);
|
||||||
@@ -727,6 +759,39 @@ public partial class PostService(
|
|||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanViewPost(SnPost post, Account? currentUser, List<SnPublisher> publishers, List<Guid> userFriends)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var publishersId = publishers.Select(e => e.Id).ToList();
|
||||||
|
|
||||||
|
// Check if post is deleted
|
||||||
|
if (post.DeletedAt != null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (currentUser is null)
|
||||||
|
{
|
||||||
|
// Anonymous user can only view public posts that are published
|
||||||
|
return post.PublishedAt != null && now >= post.PublishedAt && post.Visibility == PostVisibility.Public;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check publication status - either published or user is member
|
||||||
|
var isPublished = post.PublishedAt != null && now >= post.PublishedAt;
|
||||||
|
var isMember = publishersId.Contains(post.PublisherId);
|
||||||
|
if (!isPublished && !isMember)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check visibility
|
||||||
|
if (post.Visibility == PostVisibility.Private && !isMember)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (post.Visibility == PostVisibility.Friends &&
|
||||||
|
!(post.Publisher.AccountId.HasValue && userFriends.Contains(post.Publisher.AccountId.Value) || isMember))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Public and Unlisted are allowed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<Guid, int>> GetPostRepliesCountBatch(List<Guid> postIds)
|
private async Task<Dictionary<Guid, int>> GetPostRepliesCountBatch(List<Guid> postIds)
|
||||||
{
|
{
|
||||||
return await db.Posts
|
return await db.Posts
|
||||||
@@ -738,46 +803,6 @@ public partial class PostService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPostEmbed(SnPost post, Account? currentUser)
|
|
||||||
{
|
|
||||||
if (!post.Meta!.TryGetValue("embeds", out var value))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var embeds = value switch
|
|
||||||
{
|
|
||||||
JsonElement e => e.Deserialize<List<Dictionary<string, object>>>(),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
if (embeds is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Find the index of the poll embed first
|
|
||||||
var pollIndex = embeds.FindIndex(e =>
|
|
||||||
e.ContainsKey("type") && ((JsonElement)e["type"]).ToString() == "poll"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pollIndex < 0)
|
|
||||||
{
|
|
||||||
post.Meta["embeds"] = embeds;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pollEmbed = embeds[pollIndex];
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var pollId = Guid.Parse(((JsonElement)pollEmbed["id"]).ToString());
|
|
||||||
|
|
||||||
Guid? currentUserId = currentUser is not null ? Guid.Parse(currentUser.Id) : null;
|
|
||||||
var updatedPoll = await polls.LoadPollEmbed(pollId, currentUserId);
|
|
||||||
embeds[pollIndex] = EmbeddableBase.ToDictionary(updatedPoll);
|
|
||||||
post.Meta["embeds"] = embeds;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to load poll embed for post {PostId}", post.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<SnPost>> LoadPostInfo(
|
public async Task<List<SnPost>> LoadPostInfo(
|
||||||
List<SnPost> posts,
|
List<SnPost> posts,
|
||||||
Account? currentUser = null,
|
Account? currentUser = null,
|
||||||
@@ -789,12 +814,6 @@ public partial class PostService(
|
|||||||
posts = await LoadPublishers(posts);
|
posts = await LoadPublishers(posts);
|
||||||
posts = await LoadInteractive(posts, currentUser);
|
posts = await LoadInteractive(posts, currentUser);
|
||||||
|
|
||||||
foreach (
|
|
||||||
var post in posts
|
|
||||||
.Where(e => e.Meta is not null && e.Meta.ContainsKey("embeds"))
|
|
||||||
)
|
|
||||||
await LoadPostEmbed(post, currentUser);
|
|
||||||
|
|
||||||
if (truncate)
|
if (truncate)
|
||||||
posts = TruncatePostContent(posts);
|
posts = TruncatePostContent(posts);
|
||||||
|
|
||||||
@@ -896,6 +915,7 @@ public partial class PostService(
|
|||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Publisher)
|
.Include(e => e.Publisher)
|
||||||
|
.Include(e => e.FeaturedRecords)
|
||||||
.Take(featuredIds.Count)
|
.Take(featuredIds.Count)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
posts = posts.OrderBy(e => featuredIds.IndexOf(e.Id)).ToList();
|
posts = posts.OrderBy(e => featuredIds.IndexOf(e.Id)).ToList();
|
||||||
@@ -938,7 +958,7 @@ public partial class PostService(
|
|||||||
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
|
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
|
||||||
var nty = scope.ServiceProvider.GetRequiredService<RingService.RingServiceClient>();
|
var nty = scope.ServiceProvider.GetRequiredService<RingService.RingServiceClient>();
|
||||||
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
|
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
|
||||||
var accountsHelper = scope.ServiceProvider.GetRequiredService<AccountClientHelper>();
|
var accountsHelper = scope.ServiceProvider.GetRequiredService<RemoteAccountService>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sender = await accountsHelper.GetAccount(accountId);
|
var sender = await accountsHelper.GetAccount(accountId);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ builder.ConfigureAppKestrel(builder.Configuration);
|
|||||||
// Add application services
|
// Add application services
|
||||||
|
|
||||||
builder.Services.AddAppServices();
|
builder.Services.AddAppServices();
|
||||||
builder.Services.AddAppRateLimiting();
|
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddDysonAuth();
|
builder.Services.AddDysonAuth();
|
||||||
builder.Services.AddAccountService();
|
builder.Services.AddAccountService();
|
||||||
@@ -45,6 +44,6 @@ using (var scope = app.Services.CreateScope())
|
|||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(builder.Configuration);
|
app.ConfigureAppMiddleware(builder.Configuration);
|
||||||
|
|
||||||
app.UseSwaggerManifest();
|
app.UseSwaggerManifest("DysonNetwork.Sphere");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -17,7 +18,8 @@ public class PublisherController(
|
|||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
ActionLogService.ActionLogServiceClient als
|
ActionLogService.ActionLogServiceClient als,
|
||||||
|
RemoteRealmService remoteRealmService
|
||||||
)
|
)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
@@ -352,13 +354,11 @@ public class PublisherController(
|
|||||||
return BadRequest("Name and Nick are required.");
|
return BadRequest("Name and Nick are required.");
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var realm = await db.Realms.FirstOrDefaultAsync(r => r.Slug == realmSlug);
|
var realm = await remoteRealmService.GetRealmBySlug(realmSlug);
|
||||||
if (realm == null) return NotFound("Realm not found");
|
if (realm == null) return NotFound("Realm not found");
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var isAdmin = await db.RealmMembers
|
var isAdmin = await remoteRealmService.IsMemberWithRole(realm.Id, accountId, [RealmMemberRole.Moderator]);
|
||||||
.AnyAsync(m =>
|
|
||||||
m.RealmId == realm.Id && m.AccountId == accountId && m.Role >= RealmMemberRole.Moderator);
|
|
||||||
if (!isAdmin)
|
if (!isAdmin)
|
||||||
return StatusCode(403, "You need to be a moderator of the realm to create an organization publisher");
|
return StatusCode(403, "You need to be a moderator of the realm to create an organization publisher");
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using PublisherType = DysonNetwork.Shared.Models.PublisherType;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Publisher;
|
namespace DysonNetwork.Sphere.Publisher;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ public class PublisherService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
AccountClientHelper accountsHelper
|
RemoteAccountService remoteAccounts
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<SnPublisher?> GetPublisherByName(string name)
|
public async Task<SnPublisher?> GetPublisherByName(string name)
|
||||||
@@ -408,7 +409,8 @@ public class PublisherService(
|
|||||||
return isEnabled.Value;
|
return isEnabled.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, Shared.Models.PublisherMemberRole requiredRole)
|
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId,
|
||||||
|
Shared.Models.PublisherMemberRole requiredRole)
|
||||||
{
|
{
|
||||||
var member = await db.Publishers
|
var member = await db.Publishers
|
||||||
.Where(p => p.Id == publisherId)
|
.Where(p => p.Id == publisherId)
|
||||||
@@ -420,7 +422,7 @@ public class PublisherService(
|
|||||||
|
|
||||||
public async Task<SnPublisherMember> LoadMemberAccount(SnPublisherMember member)
|
public async Task<SnPublisherMember> LoadMemberAccount(SnPublisherMember member)
|
||||||
{
|
{
|
||||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
var account = await remoteAccounts.GetAccount(member.AccountId);
|
||||||
member.Account = SnAccount.FromProtoValue(account);
|
member.Account = SnAccount.FromProtoValue(account);
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
@@ -428,13 +430,35 @@ public class PublisherService(
|
|||||||
public async Task<List<SnPublisherMember>> LoadMemberAccounts(ICollection<SnPublisherMember> members)
|
public async Task<List<SnPublisherMember>> LoadMemberAccounts(ICollection<SnPublisherMember> members)
|
||||||
{
|
{
|
||||||
var accountIds = members.Select(m => m.AccountId).ToList();
|
var accountIds = members.Select(m => m.AccountId).ToList();
|
||||||
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
var accounts = (await remoteAccounts.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
||||||
|
|
||||||
return [.. members.Select(m =>
|
return
|
||||||
|
[
|
||||||
|
.. members.Select(m =>
|
||||||
{
|
{
|
||||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||||
m.Account = SnAccount.FromProtoValue(account);
|
m.Account = SnAccount.FromProtoValue(account);
|
||||||
return m;
|
return m;
|
||||||
})];
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SnPublisher>> LoadIndividualPublisherAccounts(ICollection<SnPublisher> publishers)
|
||||||
|
{
|
||||||
|
var accountIds = publishers
|
||||||
|
.Where(p => p.AccountId.HasValue && p.Type == PublisherType.Individual)
|
||||||
|
.Select(p => p.AccountId!.Value)
|
||||||
|
.ToList();
|
||||||
|
if (accountIds.Count == 0) return publishers.ToList();
|
||||||
|
|
||||||
|
var accounts = (await remoteAccounts.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
||||||
|
|
||||||
|
foreach (var p in publishers)
|
||||||
|
{
|
||||||
|
if (p.AccountId.HasValue && accounts.TryGetValue(p.AccountId.Value, out var account))
|
||||||
|
p.Account = SnAccount.FromProtoValue(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return publishers.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class BroadcastEventHandler(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
await msg.NakAsync(cancellationToken: stoppingToken);
|
// ignore
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,10 +134,6 @@ public class BroadcastEventHandler(
|
|||||||
.Where(m => m.AccountId == evt.AccountId)
|
.Where(m => m.AccountId == evt.AccountId)
|
||||||
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
await db.RealmMembers
|
|
||||||
.Where(m => m.AccountId == evt.AccountId)
|
|
||||||
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
|
||||||
|
|
||||||
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken);
|
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ using DysonNetwork.Sphere.Chat.Realtime;
|
|||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using DysonNetwork.Sphere.Sticker;
|
using DysonNetwork.Sphere.Sticker;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.RateLimiting;
|
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.GeoIp;
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
@@ -41,7 +38,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
|
||||||
|
|
||||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
}).AddDataAnnotationsLocalization(options =>
|
}).AddDataAnnotationsLocalization(options =>
|
||||||
@@ -75,19 +71,6 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
|
||||||
{
|
|
||||||
opts.Window = TimeSpan.FromMinutes(1);
|
|
||||||
opts.PermitLimit = 120;
|
|
||||||
opts.QueueLimit = 2;
|
|
||||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
@@ -111,7 +94,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<PublisherSubscriptionService>();
|
services.AddScoped<PublisherSubscriptionService>();
|
||||||
services.AddScoped<ActivityService>();
|
services.AddScoped<ActivityService>();
|
||||||
services.AddScoped<PostService>();
|
services.AddScoped<PostService>();
|
||||||
services.AddScoped<RealmService>();
|
|
||||||
services.AddScoped<ChatRoomService>();
|
services.AddScoped<ChatRoomService>();
|
||||||
services.AddScoped<ChatService>();
|
services.AddScoped<ChatService>();
|
||||||
services.AddScoped<StickerService>();
|
services.AddScoped<StickerService>();
|
||||||
@@ -120,7 +102,8 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<WebFeedService>();
|
services.AddScoped<WebFeedService>();
|
||||||
services.AddScoped<DiscoveryService>();
|
services.AddScoped<DiscoveryService>();
|
||||||
services.AddScoped<PollService>();
|
services.AddScoped<PollService>();
|
||||||
services.AddScoped<AccountClientHelper>();
|
services.AddScoped<RemoteAccountService>();
|
||||||
|
services.AddScoped<RemoteRealmService>();
|
||||||
services.AddScoped<AutocompletionService>();
|
services.AddScoped<AutocompletionService>();
|
||||||
|
|
||||||
var translationProvider = configuration["Translation:Provider"]?.ToLower();
|
var translationProvider = configuration["Translation:Provider"]?.ToLower();
|
||||||
|
|||||||
@@ -237,6 +237,22 @@ public class StickerController(
|
|||||||
return Redirect($"/drive/files/{sticker.Image.Id}?original=true");
|
return Redirect($"/drive/files/{sticker.Image.Id}?original=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<List<SnSticker>>> SearchSticker([FromQuery] string query, [FromQuery] int take = 10, [FromQuery] int offset = 0)
|
||||||
|
{
|
||||||
|
var queryable = db.Stickers
|
||||||
|
.Include(s => s.Pack)
|
||||||
|
.Where(s => EF.Functions.Like(s.Pack.Prefix + "+" + s.Slug, $"{query}%"))
|
||||||
|
.OrderByDescending(s => s.CreatedAt)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
var totalCount = await queryable.CountAsync();
|
||||||
|
Response.Headers["X-Total"] = totalCount.ToString();
|
||||||
|
|
||||||
|
var stickers = await queryable.Take(take).Skip(offset).ToListAsync();
|
||||||
|
return Ok(stickers);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{packId:guid}/content/{id:guid}")]
|
[HttpGet("{packId:guid}/content/{id:guid}")]
|
||||||
public async Task<ActionResult<SnSticker>> GetSticker(Guid packId, Guid id)
|
public async Task<ActionResult<SnSticker>> GetSticker(Guid packId, Guid id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class WebFeed : ModelBase
|
|||||||
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
|
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
|
||||||
|
|
||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
public Shared.Models.SnPublisher Publisher { get; set; } = null!;
|
public SnPublisher Publisher { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,9 @@
|
|||||||
<Assembly Path="/opt/homebrew/Cellar/dotnet/9.0.6/libexec/packs/Microsoft.AspNetCore.App.Ref/9.0.6/ref/net9.0/Microsoft.AspNetCore.RateLimiting.dll" />
|
<Assembly Path="/opt/homebrew/Cellar/dotnet/9.0.6/libexec/packs/Microsoft.AspNetCore.App.Ref/9.0.6/ref/net9.0/Microsoft.AspNetCore.RateLimiting.dll" />
|
||||||
<Assembly Path="/Users/littlesheep/.nuget/packages/nodatime/3.2.2/lib/net8.0/NodaTime.dll" />
|
<Assembly Path="/Users/littlesheep/.nuget/packages/nodatime/3.2.2/lib/net8.0/NodaTime.dll" />
|
||||||
</AssemblyExplorer></s:String>
|
</AssemblyExplorer></s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d3d47f4e_002D5d7b_002D4bb3_002D9fd2_002D0e52828cc908/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
|
<Solution />
|
||||||
|
</SessionState></s:String>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002EPass_002FResources_002FLocalization_002FAccountEventResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002EPass_002FResources_002FLocalization_002FAccountEventResource/@EntryIndexedValue">False</s:Boolean>
|
||||||
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002EPass_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002EPass_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">False</s:Boolean>
|
||||||
|
|||||||
403
README_LOTTERY.md
Normal file
403
README_LOTTERY.md
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
:bug# Lottery System API Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The DysonNetwork Lottery System provides a daily lottery where users can purchase tickets with custom number selections. Each day features a new draw with random winning numbers. Users purchase tickets using ISP (Dyson Network Points), with results announced each morning.
|
||||||
|
|
||||||
|
The API is handled by the DysonNetwork.Pass service. Which means if you use it with the Gateway the `/api` should be replaced with `/pass`
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Daily Draws**: Automated draws at midnight UTC
|
||||||
|
- **Custom Number Selection**: Users choose 5 unique numbers (0-99) + 1 special number (0-99)
|
||||||
|
- **Flexible Pricing**: Base cost 10 ISP + extra ISP per multiplier (e.g., multiplier=2 costs 20 ISP)
|
||||||
|
- **Daily Limits**: One ticket purchase per user per day
|
||||||
|
- **Prize System**: Multiple prize tiers based on matches
|
||||||
|
- **Instant Payment**: Tickets purchased using in-app points
|
||||||
|
- **Historical Records**: Complete draw history and statistics
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### LotteryDrawStatus Enum
|
||||||
|
```csharp
|
||||||
|
public enum LotteryDrawStatus
|
||||||
|
{
|
||||||
|
Pending = 0, // Ticket awaiting draw
|
||||||
|
Drawn = 1 // Ticket has been processed in draw
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SnLottery Model
|
||||||
|
```csharp
|
||||||
|
public class SnLottery : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public SnAccount Account { get; set; } = null!;
|
||||||
|
public Guid AccountId { get; set; }
|
||||||
|
public List<int> RegionOneNumbers { get; set; } = new(); // 5 numbers (0-99)
|
||||||
|
public int RegionTwoNumber { get; set; } // Special number (0-99)
|
||||||
|
public int Multiplier { get; set; } = 1; // Prize multiplier (≥1)
|
||||||
|
public LotteryDrawStatus DrawStatus { get; set; }
|
||||||
|
public DateTime? DrawDate { get; set; } // Date when drawn
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SnLotteryRecord Model
|
||||||
|
```csharp
|
||||||
|
public class SnLotteryRecord : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public DateTime DrawDate { get; set; }
|
||||||
|
public List<int> WinningRegionOneNumbers { get; set; } = new(); // 5 winning numbers
|
||||||
|
public int WinningRegionTwoNumber { get; set; } // Winning special number
|
||||||
|
public int TotalTickets { get; set; } // Total tickets processed
|
||||||
|
public int TotalPrizesAwarded { get; set; } // Number of winning tickets
|
||||||
|
public long TotalPrizeAmount { get; set; } // Total ISP prize amount
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prize Structure
|
||||||
|
|
||||||
|
| Region 1 Matches | Base Prize (ISP) | Notes |
|
||||||
|
|-----------------|------------------|-------|
|
||||||
|
| 0 | 0 | No prize |
|
||||||
|
| 1 | 10 | Minimum win |
|
||||||
|
| 2 | 20 | Double minimum |
|
||||||
|
| 3 | 50 | Five times minimum |
|
||||||
|
| 4 | 100 | Ten times minimum |
|
||||||
|
| 5 | 1000 | Maximum prize |
|
||||||
|
|
||||||
|
**Special Number Bonus**: If Region 2 number matches, multiply any prize by 10x.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All endpoints require authentication via Bearer token.
|
||||||
|
|
||||||
|
### Purchase Ticket
|
||||||
|
**POST** `/api/lotteries`
|
||||||
|
|
||||||
|
Creates a lottery order and deducts ISP from user's wallet.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"RegionOneNumbers": [5, 23, 47, 68, 89],
|
||||||
|
"RegionTwoNumber": 42,
|
||||||
|
"Multiplier": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "guid",
|
||||||
|
"accountId": "guid",
|
||||||
|
"createdAt": "2025-10-24T00:00:00Z",
|
||||||
|
"status": "Paid",
|
||||||
|
"currency": "isp",
|
||||||
|
"amount": 10,
|
||||||
|
"productIdentifier": "lottery"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules:**
|
||||||
|
- `RegionOneNumbers`: Exactly 5 unique integers between 0-99
|
||||||
|
- `RegionTwoNumber`: Single integer between 0-99
|
||||||
|
- `Multiplier`: Integer ≥ 1
|
||||||
|
- User can only purchase 1 ticket per day
|
||||||
|
|
||||||
|
**Pricing:**
|
||||||
|
- Base cost: 10 ISP
|
||||||
|
- Additional cost: (Multiplier - 1) × 10 ISP
|
||||||
|
- Total cost = (Multiplier × 10) ISP
|
||||||
|
|
||||||
|
### Get User Tickets
|
||||||
|
**GET** `/api/lotteries`
|
||||||
|
|
||||||
|
Retrieves user's lottery tickets with pagination.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `offset` (optional, default 0): Page offset
|
||||||
|
- `limit` (optional, default 20, max 100): Items per page
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "guid",
|
||||||
|
"regionOneNumbers": [5, 23, 47, 68, 89],
|
||||||
|
"regionTwoNumber": 42,
|
||||||
|
"multiplier": 1,
|
||||||
|
"drawStatus": "Pending",
|
||||||
|
"drawDate": null,
|
||||||
|
"createdAt": "2025-10-24T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Headers:**
|
||||||
|
```
|
||||||
|
X-Total: 42 // Total number of user's tickets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Specific Ticket
|
||||||
|
**GET** `/api/lotteries/{id}`
|
||||||
|
|
||||||
|
Retrieves a specific lottery ticket by ID.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
Same structure as individual items from Get User Tickets.
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- `404 Not Found`: Ticket doesn't exist or user doesn't own it
|
||||||
|
|
||||||
|
### Get Lottery Records
|
||||||
|
**GET** `/api/lotteries/records`
|
||||||
|
|
||||||
|
Retrieves historical lottery draw results.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `startDate` (optional): Filter by draw date (YYYY-MM-DD)
|
||||||
|
- `endDate` (optional): Filter by draw date (YYYY-MM-DD)
|
||||||
|
- `offset` (optional, default 0): Page offset
|
||||||
|
- `limit` (optional, default 20): Items per page
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "guid",
|
||||||
|
"drawDate": "2025-10-24T00:00:00Z",
|
||||||
|
"winningRegionOneNumbers": [7, 15, 23, 46, 82],
|
||||||
|
"winningRegionTwoNumber": 19,
|
||||||
|
"totalTickets": 245,
|
||||||
|
"totalPrizesAwarded": 23,
|
||||||
|
"totalPrizeAmount": 4820
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### Frontend Integration (JavaScript/React)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Purchase a lottery ticket
|
||||||
|
async function purchaseLottery(numbers, specialNumber, multiplier = 1) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lotteries', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${userToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
RegionOneNumbers: numbers, // Array of 5 unique numbers 0-99
|
||||||
|
RegionTwoNumber: specialNumber, // Number 0-99
|
||||||
|
Multiplier: multiplier // Optional, defaults to 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Ticket purchased successfully!', order);
|
||||||
|
// Refresh user ISP balance
|
||||||
|
updateWalletBalance();
|
||||||
|
} else {
|
||||||
|
console.error('Purchase failed:', order);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's tickets
|
||||||
|
async function getUserTickets() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lotteries?limit=20', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${userToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tickets = await response.json();
|
||||||
|
const totalTickets = response.headers.get('X-Total');
|
||||||
|
|
||||||
|
return { tickets, total: parseInt(totalTickets) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tickets:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get draw history
|
||||||
|
async function getDrawHistory() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lotteries/records', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${userToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching history:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile Integration (React Native/TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LotteryTicket {
|
||||||
|
id: string;
|
||||||
|
regionOneNumbers: number[];
|
||||||
|
regionTwoNumber: number;
|
||||||
|
multiplier: number;
|
||||||
|
drawStatus: 'Pending' | 'Drawn';
|
||||||
|
drawDate?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PurchaseRequest {
|
||||||
|
RegionOneNumbers: number[];
|
||||||
|
RegionTwoNumber: number;
|
||||||
|
Multiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LotteryService {
|
||||||
|
private apiUrl = 'https://your-api-domain.com/api/lotteries';
|
||||||
|
|
||||||
|
async purchaseTicket(
|
||||||
|
ticket: Omit<PurchaseRequest, 'RegionOneNumbers'> & { numbers: number[] },
|
||||||
|
token: string
|
||||||
|
): Promise<any> {
|
||||||
|
const request: PurchaseRequest = {
|
||||||
|
RegionOneNumbers: ticket.numbers,
|
||||||
|
RegionTwoNumber: ticket.RegionTwoNumber,
|
||||||
|
Multiplier: ticket.Multiplier
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(this.apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTickets(token: string, offset = 0, limit = 20): Promise<LotteryTicket[]> {
|
||||||
|
const response = await fetch(`${this.apiUrl}?offset=${offset}&limit=${limit}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDrawRecords(token: string): Promise<any[]> {
|
||||||
|
const response = await fetch(`${this.apiUrl}/records`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Number Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function validateLotteryNumbers(numbers, specialNumber, multiplier = 1) {
|
||||||
|
// Validate region one numbers
|
||||||
|
if (!Array.isArray(numbers) || numbers.length !== 5) {
|
||||||
|
return { valid: false, error: 'Must select exactly 5 numbers' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueNumbers = new Set(numbers);
|
||||||
|
if (uniqueNumbers.size !== 5) {
|
||||||
|
return { valid: false, error: 'Numbers must be unique' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check range 0-99
|
||||||
|
for (const num of numbers) {
|
||||||
|
if (!Number.isInteger(num) || num < 0 || num > 99) {
|
||||||
|
return { valid: false, error: 'Numbers must be integers between 0-99' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate special number
|
||||||
|
if (!Number.isInteger(specialNumber) || specialNumber < 0 || specialNumber > 99) {
|
||||||
|
return { valid: false, error: 'Special number must be between 0-99' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate multiplier
|
||||||
|
if (!Number.isInteger(multiplier) || multiplier < 1) {
|
||||||
|
return { valid: false, error: 'Multiplier must be 1 or greater' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
const validation = validateLotteryNumbers([5, 12, 23, 47, 89], 42, 2);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.error(validation.error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Daily Draw Schedule
|
||||||
|
|
||||||
|
- **Draw Time**: Every midnight UTC (00:00 UTC)
|
||||||
|
- **Processing**: Only tickets from the previous day are included
|
||||||
|
- **Prize Distribution**: Winners automatically receive ISP credits
|
||||||
|
- **History**: Draws are preserved indefinitely
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
- `400 Bad Request`: Invalid request data (bad numbers, duplicate purchase, etc.)
|
||||||
|
- `401 Unauthorized`: Missing or invalid authentication token
|
||||||
|
- `404 Not Found`: Ticket doesn't exist or access denied
|
||||||
|
- `403 Forbidden`: Insufficient permissions (admin endpoints)
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "You can only purchase one lottery per day.",
|
||||||
|
"type": "ArgumentException",
|
||||||
|
"statusCode": 400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### Test Cases
|
||||||
|
1. **Valid Purchase**: Select valid numbers, verify wallet deduction
|
||||||
|
2. **Invalid Numbers**: Try duplicate region one numbers, out-of-range values
|
||||||
|
3. **Daily Limit**: Attempt second purchase in same day
|
||||||
|
4. **Insufficient Funds**: Try purchase without enough ISP
|
||||||
|
5. **Draw Processing**: Verify winning tickets receive correct prizes
|
||||||
|
6. **Historical Data**: Check draw records match processed tickets
|
||||||
|
|
||||||
|
### Test Data Examples
|
||||||
|
```javascript
|
||||||
|
// Valid ticket
|
||||||
|
{ numbers: [1, 15, 23, 67, 89], special: 42, multiplier: 1 }
|
||||||
|
|
||||||
|
// Invalid - duplicate numbers
|
||||||
|
{ numbers: [1, 15, 23, 15, 89], special: 42, multiplier: 1 }
|
||||||
|
|
||||||
|
// Invalid - out of range
|
||||||
|
{ numbers: [1, 15, 23, 67, 150], special: 42, multiplier: 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For API integration questions or support:
|
||||||
|
- Check network documentation for authentication details
|
||||||
|
- Contact Dyson Network development team for assistance
|
||||||
|
- Monitor API response headers for pagination metadata
|
||||||
Reference in New Issue
Block a user