♻️ 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, 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;

View File

@@ -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)
{ {

View File

@@ -38,6 +38,9 @@ public class AppDatabase(
public DbSet<SnAuthChallenge> AuthChallenges { get; set; } = null!; public DbSet<SnAuthChallenge> AuthChallenges { get; set; } = null!;
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!;
@@ -127,6 +130,14 @@ public class AppDatabase(
.HasOne(r => r.Related) .HasOne(r => r.Related)
.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())

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); 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 => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -1252,6 +1405,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")
@@ -2089,6 +2363,26 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Challenge"); 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 => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
@@ -2145,6 +2439,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")
@@ -2329,6 +2635,11 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Sessions"); b.Navigation("Sessions");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
{
b.Navigation("Members");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b =>
{ {
b.Navigation("Members"); b.Navigation("Members");
@@ -2336,6 +2647,13 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Nodes"); b.Navigation("Nodes");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b =>
{
b.Navigation("ChatRooms");
b.Navigation("Members");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b =>
{ {
b.Navigation("Pockets"); b.Navigation("Pockets");

View File

@@ -1,14 +1,14 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
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 DysonNetwork.Shared.Models;
namespace DysonNetwork.Sphere.Realm; namespace DysonNetwork.Pass.Realm;
[ApiController] [ApiController]
[Route("/api/realms")] [Route("/api/realms")]
@@ -19,7 +19,7 @@ public class RealmController(
FileReferenceService.FileReferenceServiceClient fileRefs, FileReferenceService.FileReferenceServiceClient fileRefs,
ActionLogService.ActionLogServiceClient als, ActionLogService.ActionLogServiceClient als,
AccountService.AccountServiceClient accounts, AccountService.AccountServiceClient accounts,
AccountClientHelper accountsHelper RemoteAccountService remoteAccountsHelper
) : Controller ) : Controller
{ {
[HttpGet("{slug}")] [HttpGet("{slug}")]
@@ -37,7 +37,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var members = await db.RealmMembers var members = await db.RealmMembers
@@ -54,7 +54,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var members = await db.RealmMembers var members = await db.RealmMembers
@@ -77,7 +77,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var relatedUser = var relatedUser =
@@ -168,7 +168,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers var member = await db.RealmMembers
@@ -202,7 +202,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers var member = await db.RealmMembers
@@ -248,7 +248,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal)) 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."); 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) .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()
); );
@@ -306,7 +306,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers var member = await db.RealmMembers
@@ -323,7 +323,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id); var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers var member = await db.RealmMembers
@@ -371,7 +371,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 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.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.");
@@ -459,7 +459,7 @@ 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 Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms var realm = await db.Realms
.Where(r => r.Slug == slug) .Where(r => r.Slug == slug)
@@ -568,7 +568,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 Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms var realm = await db.Realms
.Where(r => r.Slug == slug) .Where(r => r.Slug == slug)
@@ -641,7 +641,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 Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms var realm = await db.Realms
.Where(r => r.Slug == slug) .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) 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 Shared.Proto.Account currentUser) return Unauthorized();
var realm = await db.Realms var realm = await db.Realms
.Where(r => r.Slug == slug) .Where(r => r.Slug == slug)
@@ -723,7 +723,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 Shared.Proto.Account currentUser) return Unauthorized();
var transaction = await db.Database.BeginTransactionAsync(); var transaction = await db.Database.BeginTransactionAsync();
@@ -737,11 +737,6 @@ public class RealmController(
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 +744,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();
} }

View File

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

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.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;
@@ -42,6 +43,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;
} }

View File

@@ -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;
@@ -152,6 +153,7 @@ 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.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
services.AddScoped<OidcProviderService>(); services.AddScoped<OidcProviderService>();

View File

@@ -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")]

View File

@@ -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; } = [];

View File

@@ -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;
@@ -31,6 +33,28 @@ public class SnRealm : ModelBase, IIdentifiedResource
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
};
}
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 public abstract class RealmMemberRole
@@ -51,4 +75,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;
}
}

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; 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)
{ {

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) public static IServiceCollection AddAccountService(this IServiceCollection services)
{ {
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() .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true } { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
); );
services.AddSingleton<AccountClientHelper>(); services.AddSingleton<RemoteAccountService>();
services services
.AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o => .AddGrpcClient<BotAccountReceiverService.BotAccountReceiverServiceClient>(o =>
@@ -59,10 +59,17 @@ public static class ServiceInjectionHelper
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ 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;
} }
public static IServiceCollection AddDriveService(this IServiceCollection services) public static IServiceCollection AddDriveService(this IServiceCollection services)
{ {
services.AddGrpcClient<FileService.FileServiceClient>(o => o.Address = new Uri("https://_grpc.drive")) services.AddGrpcClient<FileService.FileServiceClient>(o => o.Address = new Uri("https://_grpc.drive"))
@@ -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,11 +99,12 @@ 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 }
); );
return services; return services;
} }
} }

View File

@@ -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
) )
@@ -345,4 +345,4 @@ public class ActivityService(
var postCount = posts.Count; var postCount = posts.Count;
return score + postCount; return score + postCount;
} }
} }

View File

@@ -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;
@@ -24,6 +25,10 @@ public class AppDatabase(
public DbSet<SnPublisherSubscription> PublisherSubscriptions { get; set; } = null!; public DbSet<SnPublisherSubscription> PublisherSubscriptions { get; set; } = null!;
public DbSet<SnPublisherFeature> PublisherFeatures { 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<SnPost> Posts { get; set; } = null!;
public DbSet<SnPostReaction> PostReactions { get; set; } = null!; public DbSet<SnPostReaction> PostReactions { get; set; } = null!;
public DbSet<SnPostAward> PostAwards { 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<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)
{ {
@@ -150,10 +152,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();

View File

@@ -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)
{ {
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)
{ {
@@ -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)

View File

@@ -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}")]
@@ -203,7 +203,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 +301,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 +309,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 +411,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 +503,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 +542,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 +619,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 +824,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
@@ -898,13 +886,13 @@ 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)
{ {
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.");
} }

View File

@@ -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
[ [

View File

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

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, 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)

View File

@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry; 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;
@@ -24,12 +24,12 @@ public class PostController(
AppDatabase db, AppDatabase db,
PostService ps, PostService ps,
PublisherService pub, PublisherService pub,
AccountClientHelper accountsHelper, 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
{ {
@@ -108,7 +108,7 @@ public class PostController(
var userRealms = currentUser is null ? [] : await rs.GetUserRealms(accountId); var userRealms = currentUser is null ? [] : await rs.GetUserRealms(accountId);
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 : (realmName != null ? await rs.GetRealmBySlug(realmName) : null);
var query = db.Posts var query = db.Posts
.Include(e => e.Categories) .Include(e => e.Categories)
@@ -274,7 +274,7 @@ public class PostController(
.Skip(offset) .Skip(offset)
.ToListAsync(); .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)); var accounts = accountsProto.ToDictionary(a => Guid.Parse(a.Id), a => SnAccount.FromProtoValue(a));
foreach (var reaction in reactions) foreach (var reaction in reactions)
@@ -480,9 +480,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;
} }
@@ -708,7 +707,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");
} }
@@ -756,7 +755,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");
} }
@@ -865,9 +864,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;
} }

View File

@@ -938,7 +938,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);

View File

@@ -11,7 +11,7 @@ public class PublisherService(
AppDatabase db, AppDatabase db,
FileReferenceService.FileReferenceServiceClient fileRefs, FileReferenceService.FileReferenceServiceClient fileRefs,
ICacheService cache, ICacheService cache,
AccountClientHelper accountsHelper RemoteAccountService remoteAccountsHelper
) )
{ {
public async Task<SnPublisher?> GetPublisherByName(string name) public async Task<SnPublisher?> GetPublisherByName(string name)
@@ -420,7 +420,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 remoteAccountsHelper.GetAccount(member.AccountId);
member.Account = SnAccount.FromProtoValue(account); member.Account = SnAccount.FromProtoValue(account);
return member; return member;
} }
@@ -428,7 +428,7 @@ 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 remoteAccountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
return [.. members.Select(m => return [.. members.Select(m =>
{ {

View File

@@ -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
{ {

View File

@@ -5,7 +5,6 @@ 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 Microsoft.AspNetCore.RateLimiting;
using NodaTime; using NodaTime;
@@ -110,7 +109,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>();
@@ -119,7 +117,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();