♻️ Move the realm service from sphere to the pass
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -345,4 +345,4 @@ public class ActivityService(
|
||||
var postCount = posts.Count;
|
||||
return score + postCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -898,13 +886,13 @@ public class ChatRoomController(
|
||||
// Check if the chat room is owned by a realm
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||
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 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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
[
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
2130
DysonNetwork.Sphere/Migrations/20251021154500_ChangeRealmReferenceMode.Designer.cs
generated
Normal file
2130
DysonNetwork.Sphere/Migrations/20251021154500_ChangeRealmReferenceMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangeRealmReferenceMode : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_chat_rooms_realms_realm_id",
|
||||
table: "chat_rooms");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_posts_realms_realm_id",
|
||||
table: "posts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_posts_realm_id",
|
||||
table: "posts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_chat_rooms_realm_id",
|
||||
table: "chat_rooms");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "sn_realm_id",
|
||||
table: "chat_rooms",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_rooms_sn_realm_id",
|
||||
table: "chat_rooms",
|
||||
column: "sn_realm_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||
table: "chat_rooms",
|
||||
column: "sn_realm_id",
|
||||
principalTable: "realms",
|
||||
principalColumn: "id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_chat_rooms_realms_sn_realm_id",
|
||||
table: "chat_rooms");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_chat_rooms_sn_realm_id",
|
||||
table: "chat_rooms");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "sn_realm_id",
|
||||
table: "chat_rooms");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_posts_realm_id",
|
||||
table: "posts",
|
||||
column: "realm_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_chat_rooms_realm_id",
|
||||
table: "chat_rooms",
|
||||
column: "realm_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_chat_rooms_realms_realm_id",
|
||||
table: "chat_rooms",
|
||||
column: "realm_id",
|
||||
principalTable: "realms",
|
||||
principalColumn: "id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_posts_realms_realm_id",
|
||||
table: "posts",
|
||||
column: "realm_id",
|
||||
principalTable: "realms",
|
||||
principalColumn: "id");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -1,793 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
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;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/realms")]
|
||||
public class RealmController(
|
||||
AppDatabase db,
|
||||
RealmService rs,
|
||||
FileService.FileServiceClient files,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
AccountClientHelper accountsHelper
|
||||
) : Controller
|
||||
{
|
||||
[HttpGet("{slug}")]
|
||||
public async Task<ActionResult<SnRealm>> GetRealm(string slug)
|
||||
{
|
||||
var realm = await db.Realms
|
||||
.Where(e => e.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
return Ok(realm);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnRealm>>> ListJoinedRealms()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var members = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||
.Include(e => e.Realm)
|
||||
.Select(m => m.Realm)
|
||||
.ToListAsync();
|
||||
|
||||
return members.ToList();
|
||||
}
|
||||
|
||||
[HttpGet("invites")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnRealmMember>>> ListInvites()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var members = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt == null && m.LeaveAt == null)
|
||||
.Include(e => e.Realm)
|
||||
.ToListAsync();
|
||||
|
||||
return await rs.LoadMemberAccounts(members);
|
||||
}
|
||||
|
||||
public class RealmMemberRequest
|
||||
{
|
||||
[Required] public Guid RelatedUserId { get; set; }
|
||||
[Required] public int Role { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("invites/{slug}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealmMember>> InviteMember(string slug,
|
||||
[FromBody] RealmMemberRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var relatedUser =
|
||||
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||
|
||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
RelatedId = request.RelatedUserId.ToString(),
|
||||
Status = -100
|
||||
});
|
||||
if (hasBlocked?.Value ?? false)
|
||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(p => p.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role))
|
||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||
|
||||
var existingMember = await db.RealmMembers
|
||||
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
|
||||
.Where(m => m.RealmId == realm.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingMember != null)
|
||||
{
|
||||
if (existingMember.LeaveAt == null)
|
||||
return BadRequest("This user already in the realm cannot be invited again.");
|
||||
|
||||
existingMember.LeaveAt = null;
|
||||
existingMember.JoinedAt = null;
|
||||
db.RealmMembers.Update(existingMember);
|
||||
await db.SaveChangesAsync();
|
||||
await rs.SendInviteNotify(existingMember);
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.invite",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(existingMember.AccountId.ToString()) },
|
||||
{ "role", Value.ForNumber(request.Role) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(existingMember);
|
||||
}
|
||||
|
||||
var member = new SnRealmMember
|
||||
{
|
||||
AccountId = Guid.Parse(relatedUser.Id),
|
||||
RealmId = realm.Id,
|
||||
Role = request.Role,
|
||||
};
|
||||
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.invite",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "role", Value.ForNumber(request.Role) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
member.AccountId = Guid.Parse(relatedUser.Id);
|
||||
member.Realm = realm;
|
||||
await rs.SendInviteNotify(member);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
[HttpPost("invites/{slug}/accept")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealm>> AcceptMemberInvite(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.Realm.Slug == slug)
|
||||
.Where(m => m.JoinedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
|
||||
member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
[HttpPost("invites/{slug}/decline")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeclineMemberInvite(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.Realm.Slug == slug)
|
||||
.Where(m => m.JoinedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.decline_invite",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "decliner_id", Value.ForString(currentUser.Id) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{slug}/members")]
|
||||
public async Task<ActionResult<List<SnRealmMember>>> ListMembers(
|
||||
string slug,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] bool withStatus = false
|
||||
)
|
||||
{
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
if (!realm.IsPublic)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal))
|
||||
return StatusCode(403, "You must be a member to view this realm's members.");
|
||||
}
|
||||
|
||||
var query = db.RealmMembers
|
||||
.Where(m => m.RealmId == realm.Id)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null);
|
||||
|
||||
if (withStatus)
|
||||
{
|
||||
var members = await query
|
||||
.OrderBy(m => m.JoinedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var memberStatuses = await accountsHelper.GetAccountStatusBatch(
|
||||
members.Select(m => m.AccountId).ToList()
|
||||
);
|
||||
|
||||
members = members
|
||||
.Select(m =>
|
||||
{
|
||||
m.Status = memberStatuses.TryGetValue(m.AccountId, out var s) ? s : null;
|
||||
return m;
|
||||
})
|
||||
.OrderByDescending(m => m.Status?.IsOnline ?? false)
|
||||
.ToList();
|
||||
|
||||
var total = members.Count;
|
||||
Response.Headers.Append("X-Total", total.ToString());
|
||||
|
||||
var result = members.Skip(offset).Take(take).ToList();
|
||||
|
||||
members = await rs.LoadMemberAccounts(result);
|
||||
|
||||
return Ok(members.Where(m => m.Account is not null).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var total = await query.CountAsync();
|
||||
Response.Headers["X-Total"] = total.ToString();
|
||||
|
||||
var members = await query
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
members = await rs.LoadMemberAccounts(members);
|
||||
|
||||
return Ok(members.Where(m => m.Account is not null).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("{slug}/members/me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealmMember>> GetCurrentIdentity(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.Realm.Slug == slug)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (member is null) return NotFound();
|
||||
return Ok(await rs.LoadMemberAccount(member));
|
||||
}
|
||||
|
||||
[HttpDelete("{slug}/members/me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> LeaveRealm(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.Realm.Slug == slug)
|
||||
.Where(m => m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
|
||||
if (member.Role == RealmMemberRole.Owner)
|
||||
return StatusCode(403, "Owner cannot leave their own realm.");
|
||||
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.leave",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(member.RealmId.ToString()) },
|
||||
{ "account_id", Value.ForString(member.AccountId.ToString()) },
|
||||
{ "leaver_id", Value.ForString(currentUser.Id) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public class RealmRequest
|
||||
{
|
||||
[MaxLength(1024)] public string? Slug { get; set; }
|
||||
[MaxLength(1024)] public string? Name { get; set; }
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
public string? PictureId { get; set; }
|
||||
public string? BackgroundId { get; set; }
|
||||
public bool? IsCommunity { get; set; }
|
||||
public bool? IsPublic { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealm>> CreateRealm(RealmRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not 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.");
|
||||
|
||||
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
||||
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
||||
|
||||
var realm = new SnRealm
|
||||
{
|
||||
Name = request.Name!,
|
||||
Slug = request.Slug!,
|
||||
Description = request.Description!,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
IsCommunity = request.IsCommunity ?? false,
|
||||
IsPublic = request.IsPublic ?? false,
|
||||
Members = new List<SnRealmMember>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Role = RealmMemberRole.Owner,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
realm.Picture = SnCloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||
}
|
||||
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
realm.Background = SnCloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||
}
|
||||
|
||||
db.Realms.Add(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.create",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "name", Value.ForString(realm.Name) },
|
||||
{ "slug", Value.ForString(realm.Slug) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) },
|
||||
{ "is_public", Value.ForBool(realm.IsPublic) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
var realmResourceId = $"realm:{realm.Id}";
|
||||
|
||||
if (realm.Picture is not null)
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = realm.Picture.Id,
|
||||
Usage = "realm.picture",
|
||||
ResourceId = realmResourceId
|
||||
});
|
||||
}
|
||||
|
||||
if (realm.Background is not null)
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = realm.Background.Id,
|
||||
Usage = "realm.background",
|
||||
ResourceId = realmResourceId
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(realm);
|
||||
}
|
||||
|
||||
[HttpPatch("{slug}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealm>> Update(string slug, [FromBody] RealmRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
||||
return StatusCode(403, "You do not have permission to update this realm.");
|
||||
|
||||
if (request.Slug is not null && request.Slug != realm.Slug)
|
||||
{
|
||||
var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug);
|
||||
if (slugExists) return BadRequest("Realm with this slug already exists.");
|
||||
realm.Slug = request.Slug;
|
||||
}
|
||||
|
||||
if (request.Name is not null)
|
||||
realm.Name = request.Name;
|
||||
if (request.Description is not null)
|
||||
realm.Description = request.Description;
|
||||
if (request.IsCommunity is not null)
|
||||
realm.IsCommunity = request.IsCommunity.Value;
|
||||
if (request.IsPublic is not null)
|
||||
realm.IsPublic = request.IsPublic.Value;
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for the realm picture
|
||||
if (realm.Picture is not null)
|
||||
{
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
});
|
||||
}
|
||||
|
||||
realm.Picture = SnCloudFileReferenceObject.FromProtoValue(pictureResult);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = realm.Picture.Id,
|
||||
Usage = "realm.picture",
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
});
|
||||
}
|
||||
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for the realm background
|
||||
if (realm.Background is not null)
|
||||
{
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
});
|
||||
}
|
||||
|
||||
realm.Background = SnCloudFileReferenceObject.FromProtoValue(backgroundResult);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = realm.Background.Id,
|
||||
Usage = "realm.background",
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
});
|
||||
}
|
||||
|
||||
db.Realms.Update(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.update",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "name_updated", Value.ForBool(request.Name != null) },
|
||||
{ "slug_updated", Value.ForBool(request.Slug != null) },
|
||||
{ "description_updated", Value.ForBool(request.Description != null) },
|
||||
{ "picture_updated", Value.ForBool(request.PictureId != null) },
|
||||
{ "background_updated", Value.ForBool(request.BackgroundId != null) },
|
||||
{ "is_community_updated", Value.ForBool(request.IsCommunity != null) },
|
||||
{ "is_public_updated", Value.ForBool(request.IsPublic != null) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(realm);
|
||||
}
|
||||
|
||||
[HttpPost("{slug}/members/me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<SnRealmMember>> JoinRealm(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
if (!realm.IsCommunity)
|
||||
return StatusCode(403, "Only community realms can be joined without invitation.");
|
||||
|
||||
var existingMember = await db.RealmMembers
|
||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingMember is not null)
|
||||
{
|
||||
if (existingMember.LeaveAt == null)
|
||||
return BadRequest("You are already a member of this realm.");
|
||||
|
||||
existingMember.LeaveAt = null;
|
||||
existingMember.JoinedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
db.Update(existingMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(currentUser.Id) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(existingMember);
|
||||
}
|
||||
|
||||
var member = new SnRealmMember
|
||||
{
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
RealmId = realm.Id,
|
||||
Role = RealmMemberRole.Normal,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
};
|
||||
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.join",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(currentUser.Id) },
|
||||
{ "is_community", Value.ForBool(realm.IsCommunity) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
[HttpDelete("{slug}/members/{memberId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> RemoveMember(string slug, Guid memberId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
|
||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role))
|
||||
return StatusCode(403, "You do not have permission to remove members from this realm.");
|
||||
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.kick",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||
{ "kicker_id", Value.ForString(currentUser.Id) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPatch("{slug}/members/{memberId:guid}/role")]
|
||||
[Authorize]
|
||||
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();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
|
||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role,
|
||||
newRole))
|
||||
return StatusCode(403, "You do not have permission to update member roles in this realm.");
|
||||
|
||||
member.Role = newRole;
|
||||
db.RealmMembers.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.members.role_update",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "account_id", Value.ForString(memberId.ToString()) },
|
||||
{ "new_role", Value.ForNumber(newRole) },
|
||||
{ "updater_id", Value.ForString(currentUser.Id) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
[HttpDelete("{slug}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> Delete(string slug)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var transaction = await db.Database.BeginTransactionAsync();
|
||||
|
||||
var realm = await db.Realms
|
||||
.Where(r => r.Slug == slug)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realm is null) return NotFound();
|
||||
|
||||
if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner))
|
||||
return StatusCode(403, "Only the owner can delete this realm.");
|
||||
|
||||
try
|
||||
{
|
||||
var chats = await db.ChatRooms
|
||||
.Where(c => c.RealmId == realm.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
db.Realms.Remove(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
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();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "realms.delete",
|
||||
Meta =
|
||||
{
|
||||
{ "realm_id", Value.ForString(realm.Id.ToString()) },
|
||||
{ "realm_name", Value.ForString(realm.Name) },
|
||||
{ "realm_slug", Value.ForString(realm.Slug) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent.ToString(),
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? ""
|
||||
});
|
||||
|
||||
// Delete all file references for this realm
|
||||
var realmResourceId = $"realm:{realm.Id}";
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = realmResourceId
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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;
|
||||
|
||||
public class RealmService(
|
||||
AppDatabase db,
|
||||
RingService.RingServiceClient pusher,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
AccountClientHelper accountsHelper,
|
||||
ICacheService cache
|
||||
)
|
||||
{
|
||||
private const string CacheKeyPrefix = "account:realms:";
|
||||
|
||||
public async Task<List<Guid>> GetUserRealms(Guid accountId)
|
||||
{
|
||||
var cacheKey = $"{CacheKeyPrefix}{accountId}";
|
||||
var (found, cachedRealms) = await cache.GetAsyncWithStatus<List<Guid>>(cacheKey);
|
||||
if (found && cachedRealms != null)
|
||||
return cachedRealms;
|
||||
|
||||
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 realms;
|
||||
}
|
||||
|
||||
public async Task SendInviteNotify(SnRealmMember member)
|
||||
{
|
||||
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
||||
CultureService.SetCultureInfo(account);
|
||||
|
||||
await pusher.SendPushNotificationToUserAsync(
|
||||
new SendPushNotificationToUserRequest
|
||||
{
|
||||
UserId = account.Id,
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "invites.realms",
|
||||
Title = localizer["RealmInviteTitle"],
|
||||
Body = localizer["RealmInviteBody", member.Realm.Name],
|
||||
ActionUri = "/realms",
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, params int[] requiredRoles)
|
||||
{
|
||||
if (requiredRoles.Length == 0)
|
||||
return false;
|
||||
|
||||
var maxRequiredRole = requiredRoles.Max();
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.RealmId == realmId && m.AccountId == accountId && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
return member?.Role >= maxRequiredRole;
|
||||
}
|
||||
|
||||
public async Task<SnRealmMember> LoadMemberAccount(SnRealmMember member)
|
||||
{
|
||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
||||
member.Account = SnAccount.FromProtoValue(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);
|
||||
|
||||
return members.Select(m =>
|
||||
{
|
||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = SnAccount.FromProtoValue(account);
|
||||
return m;
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user