♻️ Move the realm service from sphere to the pass

This commit is contained in:
2025-10-21 23:45:36 +08:00
parent 46ebd92dc1
commit d6c37784e1
33 changed files with 6220 additions and 510 deletions

View File

@@ -19,7 +19,7 @@ public class BotAccountController(
DeveloperService ds,
DevProjectService projectService,
ILogger<BotAccountController> logger,
AccountClientHelper accounts,
RemoteAccountService remoteAccounts,
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
)
: ControllerBase
@@ -222,7 +222,7 @@ public class BotAccountController(
if (bot is null || bot.ProjectId != projectId)
return NotFound("Bot not found");
var botAccount = await accounts.GetBotAccount(bot.Id);
var botAccount = await remoteAccounts.GetBotAccount(bot.Id);
if (request.Name is not null) botAccount.Name = request.Name;
if (request.Nick is not null) botAccount.Nick = request.Nick;

View File

@@ -10,7 +10,7 @@ namespace DysonNetwork.Develop.Identity;
public class BotAccountService(
AppDatabase db,
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
AccountClientHelper accounts
RemoteAccountService remoteAccounts
)
{
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
@@ -158,7 +158,7 @@ public class BotAccountService(
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
{
var automatedIds = bots.Select(b => b.Id).ToList();
var data = await accounts.GetBotAccountBatch(automatedIds);
var data = await remoteAccounts.GetBotAccountBatch(automatedIds);
foreach (var bot in bots)
{

View File

@@ -39,6 +39,9 @@ public class AppDatabase(
public DbSet<SnAuthClient> AuthClients { get; set; } = null!;
public DbSet<SnApiKey> ApiKeys { get; set; } = null!;
public DbSet<SnRealm> Realms { get; set; } = null!;
public DbSet<SnRealmMember> RealmMembers { get; set; } = null!;
public DbSet<SnWallet> Wallets { get; set; } = null!;
public DbSet<SnWalletPocket> WalletPockets { get; set; } = null!;
public DbSet<SnWalletOrder> PaymentOrders { get; set; } = null!;
@@ -128,6 +131,14 @@ public class AppDatabase(
.WithMany(a => a.IncomingRelationships)
.HasForeignKey(r => r.RelatedId);
modelBuilder.Entity<SnRealmMember>()
.HasKey(pm => new { pm.RealmId, pm.AccountId });
modelBuilder.Entity<SnRealmMember>()
.HasOne(pm => pm.Realm)
.WithMany(p => p.Members)
.HasForeignKey(pm => pm.RealmId)
.OnDelete(DeleteBehavior.Cascade);
// Automatically apply soft-delete filter to all entities inheriting BaseModel
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -954,6 +954,159 @@ namespace DysonNetwork.Pass.Migrations
b.ToTable("auth_sessions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant?>("BreakUntil")
.HasColumnType("timestamp with time zone")
.HasColumnName("break_until");
b.Property<Guid>("ChatRoomId")
.HasColumnType("uuid")
.HasColumnName("chat_room_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<bool>("IsBot")
.HasColumnType("boolean")
.HasColumnName("is_bot");
b.Property<Instant?>("JoinedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at");
b.Property<Instant?>("LastReadAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_read_at");
b.Property<Instant?>("LeaveAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("leave_at");
b.Property<string>("Nick")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("nick");
b.Property<int>("Notify")
.HasColumnType("integer")
.HasColumnName("notify");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<ChatTimeoutCause>("TimeoutCause")
.HasColumnType("jsonb")
.HasColumnName("timeout_cause");
b.Property<Instant?>("TimeoutUntil")
.HasColumnType("timestamp with time zone")
.HasColumnName("timeout_until");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_sn_chat_member");
b.HasIndex("ChatRoomId")
.HasDatabaseName("ix_sn_chat_member_chat_room_id");
b.ToTable("sn_chat_member", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("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")
.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")
.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<Guid?>("RealmId")
.HasColumnType("uuid")
.HasColumnName("realm_id");
b.Property<Guid?>("SnRealmId")
.HasColumnType("uuid")
.HasColumnName("sn_realm_id");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_sn_chat_room");
b.HasIndex("SnRealmId")
.HasDatabaseName("ix_sn_chat_room_sn_realm_id");
b.ToTable("sn_chat_room", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>
{
b.Property<Guid>("Id")
@@ -1252,6 +1405,127 @@ namespace DysonNetwork.Pass.Migrations
b.ToTable("permission_nodes", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<SnCloudFileReferenceObject>("Background")
.HasColumnType("jsonb")
.HasColumnName("background");
b.Property<string>("BackgroundId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("background_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<bool>("IsCommunity")
.HasColumnType("boolean")
.HasColumnName("is_community");
b.Property<bool>("IsPublic")
.HasColumnType("boolean")
.HasColumnName("is_public");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<SnCloudFileReferenceObject>("Picture")
.HasColumnType("jsonb")
.HasColumnName("picture");
b.Property<string>("PictureId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("picture_id");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<SnVerificationMark>("Verification")
.HasColumnType("jsonb")
.HasColumnName("verification");
b.HasKey("Id")
.HasName("pk_realms");
b.HasIndex("Slug")
.IsUnique()
.HasDatabaseName("ix_realms_slug");
b.ToTable("realms", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b =>
{
b.Property<Guid>("RealmId")
.HasColumnType("uuid")
.HasColumnName("realm_id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("JoinedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("joined_at");
b.Property<Instant?>("LeaveAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("leave_at");
b.Property<int>("Role")
.HasColumnType("integer")
.HasColumnName("role");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("RealmId", "AccountId")
.HasName("pk_realm_members");
b.ToTable("realm_members", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
{
b.Property<Guid>("Id")
@@ -2089,6 +2363,26 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Challenge");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMember", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "ChatRoom")
.WithMany("Members")
.HasForeignKey("ChatRoomId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_sn_chat_member_sn_chat_room_chat_room_id");
b.Navigation("ChatRoom");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnRealm", null)
.WithMany("ChatRooms")
.HasForeignKey("SnRealmId")
.HasConstraintName("fk_sn_chat_room_realms_sn_realm_id");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
@@ -2145,6 +2439,18 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Group");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm")
.WithMany("Members")
.HasForeignKey("RealmId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_realm_members_realms_realm_id");
b.Navigation("Realm");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
@@ -2329,6 +2635,11 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Sessions");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b =>
{
b.Navigation("Members");
@@ -2336,6 +2647,13 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Nodes");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
{
b.Navigation("ChatRooms");
b.Navigation("Members");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
{
b.Navigation("Pockets");

View File

@@ -1,14 +1,14 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Google.Protobuf.WellKnownTypes;
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Sphere.Realm;
namespace DysonNetwork.Pass.Realm;
[ApiController]
[Route("/api/realms")]
@@ -19,7 +19,7 @@ public class RealmController(
FileReferenceService.FileReferenceServiceClient fileRefs,
ActionLogService.ActionLogServiceClient als,
AccountService.AccountServiceClient accounts,
AccountClientHelper accountsHelper
RemoteAccountService remoteAccountsHelper
) : Controller
{
[HttpGet("{slug}")]
@@ -37,7 +37,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var members = await db.RealmMembers
@@ -54,7 +54,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<List<SnRealmMember>>> ListInvites()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var members = await db.RealmMembers
@@ -77,7 +77,7 @@ public class RealmController(
public async Task<ActionResult<SnRealmMember>> InviteMember(string slug,
[FromBody] RealmMemberRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var relatedUser =
@@ -168,7 +168,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers
@@ -202,7 +202,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult> DeclineMemberInvite(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers
@@ -248,7 +248,7 @@ public class RealmController(
if (!realm.IsPublic)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
return StatusCode(403, "You must be a member to view this realm's members.");
}
@@ -263,7 +263,7 @@ public class RealmController(
.OrderBy(m => m.JoinedAt)
.ToListAsync();
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
var memberStatuses = await remoteAccountsHelper.GetAccountStatusBatch(
members.Select(m => m.AccountId).ToList()
);
@@ -306,7 +306,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers
@@ -323,7 +323,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult> LeaveRealm(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers
@@ -371,7 +371,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name.");
if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug.");
@@ -459,7 +459,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms
.Where(r => r.Slug == slug)
@@ -568,7 +568,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms
.Where(r => r.Slug == slug)
@@ -641,7 +641,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms
.Where(r => r.Slug == slug)
@@ -681,7 +681,7 @@ public class RealmController(
public async Task<ActionResult<SnRealmMember>> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole)
{
if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role.");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms
.Where(r => r.Slug == slug)
@@ -723,7 +723,7 @@ public class RealmController(
[Authorize]
public async Task<ActionResult> Delete(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
var transaction = await db.Database.BeginTransactionAsync();
@@ -737,11 +737,6 @@ public class RealmController(
try
{
var chats = await db.ChatRooms
.Where(c => c.RealmId == realm.Id)
.Select(c => c.Id)
.ToListAsync();
db.Realms.Remove(realm);
await db.SaveChangesAsync();
@@ -749,15 +744,6 @@ public class RealmController(
await db.RealmMembers
.Where(m => m.RealmId == realm.Id)
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
await db.ChatRooms
.Where(c => c.RealmId == realm.Id)
.ExecuteUpdateAsync(c => c.SetProperty(c => c.DeletedAt, now));
await db.ChatMessages
.Where(m => chats.Contains(m.ChatRoomId))
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
await db.ChatMembers
.Where(m => chats.Contains(m.ChatRoomId))
.ExecuteUpdateAsync(m => m.SetProperty(m => m.DeletedAt, now));
await db.SaveChangesAsync();
await transaction.CommitAsync();
}

View File

@@ -1,20 +1,18 @@
using DysonNetwork.Pass.Localization;
using DysonNetwork.Shared;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
namespace DysonNetwork.Sphere.Realm;
namespace DysonNetwork.Pass.Realm;
public class RealmService(
AppDatabase db,
RingService.RingServiceClient pusher,
AccountService.AccountServiceClient accounts,
IStringLocalizer<NotificationResource> localizer,
AccountClientHelper accountsHelper,
ICacheService cache
)
{
@@ -42,13 +40,18 @@ public class RealmService(
public async Task SendInviteNotify(SnRealmMember member)
{
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
CultureService.SetCultureInfo(account);
var account = await db.Accounts
.Include(a => a.Profile)
.FirstOrDefaultAsync(a => a.Id == member.AccountId);
if (account == null) throw new InvalidOperationException("Account not found");
CultureService.SetCultureInfo(account.Language);
await pusher.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest
{
UserId = account.Id,
UserId = account.Id.ToString(),
Notification = new PushNotification
{
Topic = "invites.realms",
@@ -75,20 +78,26 @@ public class RealmService(
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
{
var account = await accountsHelper.GetAccount(member.AccountId);
member.Account = SnAccount.FromProtoValue(account);
var account = await db.Accounts
.Include(a => a.Profile)
.FirstOrDefaultAsync(a => a.Id == member.AccountId);
if (account != null)
member.Account = account;
return member;
}
public async Task<List<SnRealmMember>> LoadMemberAccounts(ICollection<SnRealmMember> members)
{
var accountIds = members.Select(m => m.AccountId).ToList();
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
var accountsDict = await db.Accounts
.Include(a => a.Profile)
.Where(a => accountIds.Contains(a.Id))
.ToDictionaryAsync(a => a.Id, a => a);
return members.Select(m =>
{
if (accounts.TryGetValue(m.AccountId, out var account))
m.Account = SnAccount.FromProtoValue(account);
if (accountsDict.TryGetValue(m.AccountId, out var account))
m.Account = account;
return m;
}).ToList();
}

View File

@@ -0,0 +1,140 @@
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<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)
.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<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;
}
}

View File

@@ -3,6 +3,7 @@ using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Credit;
using DysonNetwork.Pass.Leveling;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Pass.Realm;
using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Http;
using Prometheus;
@@ -42,6 +43,7 @@ public static class ApplicationConfiguration
app.MapGrpcService<BotAccountReceiverGrpc>();
app.MapGrpcService<WalletServiceGrpc>();
app.MapGrpcService<PaymentServiceGrpc>();
app.MapGrpcService<RealmServiceGrpc>();
return app;
}

View File

@@ -17,6 +17,7 @@ using DysonNetwork.Pass.Credit;
using DysonNetwork.Pass.Handlers;
using DysonNetwork.Pass.Leveling;
using DysonNetwork.Pass.Mailer;
using DysonNetwork.Pass.Realm;
using DysonNetwork.Pass.Safety;
using DysonNetwork.Pass.Wallet.PaymentHandlers;
using DysonNetwork.Shared.Cache;
@@ -152,6 +153,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<SafetyService>();
services.AddScoped<SocialCreditService>();
services.AddScoped<ExperienceService>();
services.AddScoped<RealmService>();
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
services.AddScoped<OidcProviderService>();

View File

@@ -30,7 +30,7 @@ public class SnChatRoom : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
public Guid? RealmId { get; set; }
public SnRealm? Realm { get; set; }
[NotMapped] public SnRealm? Realm { get; set; }
[NotMapped]
[JsonPropertyName("members")]

View File

@@ -64,7 +64,7 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
public SnPost? ForwardedPost { 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; } = [];

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Shared.Models;
@@ -31,6 +33,28 @@ public class SnRealm : ModelBase, IIdentifiedResource
public Guid AccountId { get; set; }
public string ResourceIdentifier => $"realm:{Id}";
public Realm ToProtoValue()
{
return new Realm
{
Id = Id.ToString(),
Name = Name
};
}
public static SnRealm FromProtoValue(Realm proto)
{
return new SnRealm
{
Id = Guid.Parse(proto.Id),
Name = proto.Name,
Slug = "", // Required but not in proto
Description = "",
IsCommunity = false,
IsPublic = false
};
}
}
public abstract class RealmMemberRole
@@ -51,4 +75,40 @@ public class SnRealmMember : ModelBase
public int Role { get; set; } = RealmMemberRole.Normal;
public Instant? JoinedAt { get; set; }
public Instant? LeaveAt { get; set; }
public Proto.RealmMember ToProtoValue()
{
var proto = new Proto.RealmMember
{
AccountId = AccountId.ToString(),
RealmId = RealmId.ToString(),
Role = Role,
JoinedAt = JoinedAt?.ToTimestamp(),
LeaveAt = LeaveAt?.ToTimestamp(),
Realm = Realm.ToProtoValue()
};
if (Account != null)
{
proto.Account = Account.ToProtoValue();
}
return proto;
}
public static SnRealmMember FromProtoValue(RealmMember proto)
{
var member = new SnRealmMember
{
AccountId = Guid.Parse(proto.AccountId),
RealmId = Guid.Parse(proto.RealmId),
Role = proto.Role,
JoinedAt = proto.JoinedAt?.ToInstant(),
LeaveAt = proto.LeaveAt?.ToInstant(),
Realm = proto.Realm != null ? SnRealm.FromProtoValue(proto.Realm) : new SnRealm() // Provide default or handle null
};
if (proto.Account != null)
{
member.Account = SnAccount.FromProtoValue(proto.Account);
}
return member;
}
}

View File

@@ -0,0 +1,84 @@
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;
}
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 realms for a user
rpc GetUserRealms(GetUserRealmsRequest) returns (GetUserRealmsResponse) {}
// 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 GetUserRealmsResponse {
repeated string realm_ids = 1;
}
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;
}

View File

@@ -3,7 +3,7 @@ using DysonNetwork.Shared.Proto;
namespace DysonNetwork.Shared.Registry;
public class AccountClientHelper(AccountService.AccountServiceClient accounts)
public class RemoteAccountService(AccountService.AccountServiceClient accounts)
{
public async Task<Account> GetAccount(Guid id)
{

View File

@@ -0,0 +1,60 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
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 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();
}
}

View File

@@ -36,11 +36,11 @@ public static class ServiceInjectionHelper
public static IServiceCollection AddAccountService(this IServiceCollection services)
{
services
.AddGrpcClient<AccountService.AccountServiceClient>(o => o.Address = new Uri("https://_grpc.pass") )
.AddGrpcClient<AccountService.AccountServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
services.AddSingleton<AccountClientHelper>();
services.AddSingleton<RemoteAccountService>();
services
.AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o =>
@@ -60,6 +60,13 @@ public static class ServiceInjectionHelper
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
services
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
services.AddSingleton<RemoteRealmService>();
return services;
}
@@ -70,7 +77,8 @@ public static class ServiceInjectionHelper
{ 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()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
@@ -80,7 +88,8 @@ public static class ServiceInjectionHelper
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()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
@@ -90,7 +99,8 @@ public static class ServiceInjectionHelper
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()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);

View File

@@ -1,8 +1,8 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
using NodaTime;
@@ -13,7 +13,7 @@ public class ActivityService(
AppDatabase db,
Publisher.PublisherService pub,
PostService ps,
RealmService rs,
RemoteRealmService rs,
DiscoveryService ds,
AccountService.AccountServiceClient accounts
)

View File

@@ -1,6 +1,7 @@
using System.Linq.Expressions;
using System.Reflection;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.WebReader;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Query;
@@ -24,6 +25,10 @@ public class AppDatabase(
public DbSet<SnPublisherSubscription> PublisherSubscriptions { get; set; } = null!;
public DbSet<SnPublisherFeature> PublisherFeatures { get; set; } = null!;
// TODO Remove the realms table after the data migration is done
public DbSet<SnRealm> Realms { get; set; } = null!;
public DbSet<SnRealmMember> RealmMembers { get; set; } = null!;
public DbSet<SnPost> Posts { get; set; } = null!;
public DbSet<SnPostReaction> PostReactions { get; set; } = null!;
public DbSet<SnPostAward> PostAwards { get; set; } = null!;
@@ -33,26 +38,23 @@ public class AppDatabase(
public DbSet<SnPostFeaturedRecord> PostFeaturedRecords { 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<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<SnChatMember> ChatMembers { get; set; } = null!;
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
public DbSet<SnRealtimeCall> ChatRealtimeCall { 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<StickerPackOwnership> StickerPackOwnerships { get; set; } = null!;
public DbSet<WebReader.WebArticle> WebArticles { get; set; } = null!;
public DbSet<WebReader.WebFeed> WebFeeds { get; set; } = null!;
public DbSet<WebReader.WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
public DbSet<WebArticle> WebArticles { get; set; } = null!;
public DbSet<WebFeed> WebFeeds { get; set; } = null!;
public DbSet<WebFeedSubscription> WebFeedSubscriptions { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@@ -150,10 +152,10 @@ public class AppDatabase(
.HasForeignKey(m => m.SenderId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<WebReader.WebFeed>()
modelBuilder.Entity<WebFeed>()
.HasIndex(f => f.Url)
.IsUnique();
modelBuilder.Entity<WebReader.WebArticle>()
modelBuilder.Entity<WebArticle>()
.HasIndex(a => a.Url)
.IsUnique();

View File

@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Autocompletion;
public class AutocompletionService(AppDatabase db, AccountClientHelper accountsHelper)
public class AutocompletionService(AppDatabase db, RemoteAccountService remoteAccountsHelper)
{
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
{
@@ -47,7 +47,7 @@ public class AutocompletionService(AppDatabase db, AccountClientHelper accountsH
switch (type)
{
case "u":
var allAccounts = await accountsHelper.SearchAccounts(query);
var allAccounts = await remoteAccountsHelper.SearchAccounts(query);
var filteredAccounts = allAccounts;
if (chatId.HasValue)

View File

@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Localization;
using DysonNetwork.Sphere.Realm;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Localization;
@@ -20,14 +20,14 @@ namespace DysonNetwork.Sphere.Chat;
public class ChatRoomController(
AppDatabase db,
ChatRoomService crs,
RealmService rs,
RemoteRealmService rs,
IStringLocalizer<NotificationResource> localizer,
AccountService.AccountServiceClient accounts,
FileService.FileServiceClient files,
FileReferenceService.FileReferenceServiceClient fileRefs,
ActionLogService.ActionLogServiceClient als,
RingService.RingServiceClient pusher,
AccountClientHelper accountsHelper
RemoteAccountService remoteAccountsHelper
) : ControllerBase
{
[HttpGet("{id:guid}")]
@@ -203,7 +203,7 @@ public class ChatRoomController(
if (request.RealmId is not null)
{
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.");
chatRoom.RealmId = request.RealmId;
}
@@ -301,7 +301,7 @@ public class ChatRoomController(
if (chatRoom.RealmId is not null)
{
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.");
}
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
@@ -309,13 +309,9 @@ public class ChatRoomController(
if (request.RealmId is not null)
{
var member = await db.RealmMembers
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
.Where(m => m.RealmId == request.RealmId)
.FirstOrDefaultAsync();
if (member is null || member.Role < RealmMemberRole.Moderator)
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id), [RealmMemberRole.Moderator]))
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)
@@ -415,7 +411,7 @@ public class ChatRoomController(
if (chatRoom.RealmId is not null)
{
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.");
}
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner))
@@ -507,7 +503,7 @@ public class ChatRoomController(
.Select(m => m.AccountId)
.ToListAsync();
var memberStatuses = await accountsHelper.GetAccountStatusBatch(members);
var memberStatuses = await remoteAccountsHelper.GetAccountStatusBatch(members);
var onlineCount = memberStatuses.Count(s => s.Value.IsOnline);
@@ -546,7 +542,7 @@ public class ChatRoomController(
.OrderBy(m => m.JoinedAt)
.ToListAsync();
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
var memberStatuses = await remoteAccountsHelper.GetAccountStatusBatch(
members.Select(m => m.AccountId).ToList()
);
@@ -623,11 +619,7 @@ public class ChatRoomController(
// Handle realm-owned chat rooms
if (chatRoom.RealmId is not null)
{
var realmMember = await db.RealmMembers
.Where(m => m.AccountId == accountId)
.Where(m => m.RealmId == chatRoom.RealmId)
.FirstOrDefaultAsync();
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator]))
return StatusCode(403, "You need at least be a realm moderator to invite members to this chat.");
}
else
@@ -832,11 +824,7 @@ public class ChatRoomController(
// Check if the chat room is owned by a realm
if (chatRoom.RealmId is not null)
{
var realmMember = await db.RealmMembers
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
.Where(m => m.RealmId == chatRoom.RealmId)
.FirstOrDefaultAsync();
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id), [RealmMemberRole.Moderator]))
return StatusCode(403, "You need at least be a realm moderator to change member roles.");
}
else
@@ -899,12 +887,12 @@ public class ChatRoomController(
if (chatRoom.RealmId is not null)
{
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.");
}
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.");
}

View File

@@ -9,7 +9,7 @@ namespace DysonNetwork.Sphere.Chat;
public class ChatRoomService(
AppDatabase db,
ICacheService cache,
AccountClientHelper accountsHelper
RemoteAccountService remoteAccountsHelper
)
{
private const string ChatRoomGroupPrefix = "chatroom:";
@@ -147,7 +147,7 @@ public class ChatRoomService(
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);
return member;
}
@@ -155,7 +155,7 @@ public class ChatRoomService(
public async Task<List<SnChatMember>> LoadMemberAccounts(ICollection<SnChatMember> members)
{
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
[

View File

@@ -1,30 +1,27 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Realm;
namespace DysonNetwork.Sphere.Chat;
[ApiController]
[Route("/api/realms/{slug}")]
public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBase
public class RealmChatController(AppDatabase db, RemoteRealmService rs) : ControllerBase
{
[HttpGet("chat")]
[Authorize]
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 realm = await db.Realms
.Where(r => r.Slug == slug)
.FirstOrDefaultAsync();
if (realm is null) return NotFound();
var realm = await rs.GetRealmBySlug(slug);
if (!realm.IsPublic)
{
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.");
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ public class PollController(
AppDatabase db,
PollService polls,
Publisher.PublisherService pub,
AccountClientHelper accountsHelper
RemoteAccountService remoteAccountsHelper
) : ControllerBase
{
[HttpGet("{id:guid}")]
@@ -110,7 +110,7 @@ public class PollController(
if (!poll.IsAnonymous)
{
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
foreach (var answer in answers)

View File

@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Poll;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.WebReader;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -24,12 +24,12 @@ public class PostController(
AppDatabase db,
PostService ps,
PublisherService pub,
AccountClientHelper accountsHelper,
RemoteAccountService remoteAccountsHelper,
AccountService.AccountServiceClient accounts,
ActionLogService.ActionLogServiceClient als,
PaymentService.PaymentServiceClient payments,
PollService polls,
RealmService rs
RemoteRealmService rs
)
: ControllerBase
{
@@ -108,7 +108,7 @@ public class PostController(
var userRealms = currentUser is null ? [] : await rs.GetUserRealms(accountId);
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 : (realmName != null ? await rs.GetRealmBySlug(realmName) : null);
var query = db.Posts
.Include(e => e.Categories)
@@ -274,7 +274,7 @@ public class PostController(
.Skip(offset)
.ToListAsync();
var accountsProto = await accountsHelper.GetAccountBatch(reactions.Select(r => r.AccountId).ToList());
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)
@@ -480,9 +480,8 @@ public class PostController(
if (request.RealmId is not null)
{
var realm = await db.Realms.FindAsync(request.RealmId.Value);
if (realm is null) return BadRequest("Realm was not found.");
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
var realm = await rs.GetRealm(request.RealmId.Value.ToString());
if (!await rs.IsMemberWithRole(realm.Id, accountId, new List<int> { RealmMemberRole.Normal }))
return StatusCode(403, "You are not a member of this realm.");
post.RealmId = realm.Id;
}
@@ -708,7 +707,7 @@ public class PostController(
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");
}
@@ -756,7 +755,7 @@ public class PostController(
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");
}
@@ -865,9 +864,8 @@ public class PostController(
// The realm is the same as well as the poll
if (request.RealmId is not null)
{
var realm = await db.Realms.FindAsync(request.RealmId.Value);
if (realm is null) return BadRequest("Realm was not found.");
if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal))
var realm = await rs.GetRealm(request.RealmId.Value.ToString());
if (!await rs.IsMemberWithRole(realm.Id, accountId, new List<int> { RealmMemberRole.Normal }))
return StatusCode(403, "You are not a member of this realm.");
post.RealmId = realm.Id;
}

View File

@@ -938,7 +938,7 @@ public partial class PostService(
var pub = scope.ServiceProvider.GetRequiredService<Publisher.PublisherService>();
var nty = scope.ServiceProvider.GetRequiredService<RingService.RingServiceClient>();
var accounts = scope.ServiceProvider.GetRequiredService<AccountService.AccountServiceClient>();
var accountsHelper = scope.ServiceProvider.GetRequiredService<AccountClientHelper>();
var accountsHelper = scope.ServiceProvider.GetRequiredService<RemoteAccountService>();
try
{
var sender = await accountsHelper.GetAccount(accountId);

View File

@@ -11,7 +11,7 @@ public class PublisherService(
AppDatabase db,
FileReferenceService.FileReferenceServiceClient fileRefs,
ICacheService cache,
AccountClientHelper accountsHelper
RemoteAccountService remoteAccountsHelper
)
{
public async Task<SnPublisher?> GetPublisherByName(string name)
@@ -420,7 +420,7 @@ public class PublisherService(
public async Task<SnPublisherMember> LoadMemberAccount(SnPublisherMember member)
{
var account = await accountsHelper.GetAccount(member.AccountId);
var account = await remoteAccountsHelper.GetAccount(member.AccountId);
member.Account = SnAccount.FromProtoValue(account);
return member;
}
@@ -428,7 +428,7 @@ public class PublisherService(
public async Task<List<SnPublisherMember>> LoadMemberAccounts(ICollection<SnPublisherMember> members)
{
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 [.. members.Select(m =>
{

View File

@@ -134,10 +134,6 @@ public class BroadcastEventHandler(
.Where(m => m.AccountId == evt.AccountId)
.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);
try
{

View File

@@ -5,7 +5,6 @@ using DysonNetwork.Sphere.Chat.Realtime;
using DysonNetwork.Sphere.Localization;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Publisher;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.Sticker;
using Microsoft.AspNetCore.RateLimiting;
using NodaTime;
@@ -110,7 +109,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<PublisherSubscriptionService>();
services.AddScoped<ActivityService>();
services.AddScoped<PostService>();
services.AddScoped<RealmService>();
services.AddScoped<ChatRoomService>();
services.AddScoped<ChatService>();
services.AddScoped<StickerService>();
@@ -119,7 +117,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<WebFeedService>();
services.AddScoped<DiscoveryService>();
services.AddScoped<PollService>();
services.AddScoped<AccountClientHelper>();
services.AddScoped<RemoteAccountService>();
services.AddScoped<RemoteRealmService>();
services.AddScoped<AutocompletionService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower();