🐛 Bug fixes 💄 Optimizations

This commit is contained in:
LittleSheep 2025-05-18 20:05:15 +08:00
parent 5b9b28d77a
commit cf9084b8c0
15 changed files with 7314 additions and 60 deletions

View File

@ -32,6 +32,7 @@ public class ActionLogType
public const string RealmJoin = "realms.join"; public const string RealmJoin = "realms.join";
public const string RealmLeave = "realms.leave"; public const string RealmLeave = "realms.leave";
public const string RealmKick = "realms.kick"; public const string RealmKick = "realms.kick";
public const string RealmAdjustRole = "realms.role.edit";
public const string ChatroomCreate = "chatrooms.create"; public const string ChatroomCreate = "chatrooms.create";
public const string ChatroomUpdate = "chatrooms.update"; public const string ChatroomUpdate = "chatrooms.update";
public const string ChatroomDelete = "chatrooms.delete"; public const string ChatroomDelete = "chatrooms.delete";
@ -39,6 +40,7 @@ public class ActionLogType
public const string ChatroomJoin = "chatrooms.join"; public const string ChatroomJoin = "chatrooms.join";
public const string ChatroomLeave = "chatrooms.leave"; public const string ChatroomLeave = "chatrooms.leave";
public const string ChatroomKick = "chatrooms.kick"; public const string ChatroomKick = "chatrooms.kick";
public const string ChatroomAdjustRole = "chatrooms.role.edit";
} }
public class ActionLog : ModelBase public class ActionLog : ModelBase

View File

@ -115,7 +115,7 @@ public class AppDatabase(
"chat.realtime.create", "chat.realtime.create",
"accounts.statuses.create", "accounts.statuses.create",
"accounts.statuses.update", "accounts.statuses.update",
"stickers.pack.create", "stickers.packs.create",
"stickers.create" "stickers.create"
}.Select(permission => }.Select(permission =>
PermissionService.NewPermissionNode("group:default", "global", permission, true)) PermissionService.NewPermissionNode("group:default", "global", permission, true))

View File

@ -18,6 +18,7 @@ public class ChatRoom : ModelBase
[MaxLength(1024)] public string? Name { get; set; } [MaxLength(1024)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
public ChatRoomType Type { get; set; } public ChatRoomType Type { get; set; }
public bool IsCommunity { get; set; }
public bool IsPublic { get; set; } public bool IsPublic { get; set; }
[MaxLength(32)] public string? PictureId { get; set; } [MaxLength(32)] public string? PictureId { get; set; }

View File

@ -127,6 +127,8 @@ public class ChatRoomController(
[MaxLength(32)] public string? PictureId { get; set; } [MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; } [MaxLength(32)] public string? BackgroundId { get; set; }
public Guid? RealmId { get; set; } public Guid? RealmId { get; set; }
public bool? IsCommunity { get; set; }
public bool? IsPublic { get; set; }
} }
[HttpPost] [HttpPost]
@ -141,6 +143,8 @@ public class ChatRoomController(
{ {
Name = request.Name, Name = request.Name,
Description = request.Description ?? string.Empty, Description = request.Description ?? string.Empty,
IsCommunity = request.IsCommunity ?? false,
IsPublic = request.IsPublic ?? false,
Type = ChatRoomType.Group, Type = ChatRoomType.Group,
Members = new List<ChatMember> Members = new List<ChatMember>
{ {
@ -243,6 +247,10 @@ public class ChatRoomController(
chatRoom.Name = request.Name; chatRoom.Name = request.Name;
if (request.Description is not null) if (request.Description is not null)
chatRoom.Description = request.Description; chatRoom.Description = request.Description;
if (request.IsCommunity is not null)
chatRoom.IsCommunity = request.IsCommunity.Value;
if (request.IsPublic is not null)
chatRoom.IsPublic = request.IsPublic.Value;
db.ChatRooms.Update(chatRoom); db.ChatRooms.Update(chatRoom);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -507,29 +515,34 @@ public class ChatRoomController(
} }
else else
{ {
// Check if the current user has permission to change roles
var currentMember = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
.FirstOrDefaultAsync();
if (currentMember is null || currentMember.Role < ChatMemberRole.Moderator)
return StatusCode(403, "You need at least be a moderator to change member roles.");
// Find the target member
var targetMember = await db.ChatMembers var targetMember = await db.ChatMembers
.Where(m => m.AccountId == memberId && m.ChatRoomId == roomId) .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (targetMember is null) return NotFound(); if (targetMember is null) return NotFound();
// Check if the current user has sufficient permissions // Check if the current user has permission to change roles
if (currentMember.Role <= targetMember.Role) if (
return StatusCode(403, "You cannot modify the role of members with equal or higher roles."); !await crs.IsMemberWithRole(
if (currentMember.Role <= newRole) chatRoom.Id,
return StatusCode(403, "You cannot assign a role equal to or higher than your own."); currentUser.Id,
ChatMemberRole.Moderator,
targetMember.Role,
newRole
)
)
return StatusCode(403, "You don't have enough permission to edit the roles of members.");
targetMember.Role = newRole; targetMember.Role = newRole;
db.ChatMembers.Update(targetMember); db.ChatMembers.Update(targetMember);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
als.CreateActionLogFromRequest(
ActionLogType.RealmAdjustRole,
new Dictionary<string, object>
{ { "chatroom_id", roomId }, { "account_id", memberId }, { "new_role", newRole } },
Request
);
return Ok(targetMember); return Ok(targetMember);
} }
@ -550,20 +563,12 @@ 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, currentUser.Id, RealmMemberRole.Moderator))
.Where(m => m.AccountId == 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 remove members."); return StatusCode(403, "You need at least be a realm moderator to remove members.");
} }
else else
{ {
// Check if the current user has permission to remove members if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Moderator))
var currentMember = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
.FirstOrDefaultAsync();
if (currentMember is null || currentMember.Role < 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.");
// Find the target member // Find the target member
@ -572,8 +577,8 @@ public class ChatRoomController(
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (targetMember is null) return NotFound(); if (targetMember is null) return NotFound();
// Check if current user has sufficient permissions // Check if the current user has sufficient permissions
if (currentMember.Role <= targetMember.Role) if (!await crs.IsMemberWithRole(chatRoom.Id, memberId, targetMember.Role))
return StatusCode(403, "You cannot remove members with equal or higher roles."); return StatusCode(403, "You cannot remove members with equal or higher roles.");
db.ChatMembers.Remove(targetMember); db.ChatMembers.Remove(targetMember);
@ -602,8 +607,8 @@ public class ChatRoomController(
.Where(r => r.Id == roomId) .Where(r => r.Id == roomId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (chatRoom is null) return NotFound(); if (chatRoom is null) return NotFound();
if (!chatRoom.IsPublic) if (!chatRoom.IsCommunity)
return StatusCode(403, "This chat room is private. You need an invitation to join."); return StatusCode(403, "This chat room isn't a community. You need an invitation to join.");
var existingMember = await db.ChatMembers var existingMember = await db.ChatMembers
.FirstOrDefaultAsync(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId); .FirstOrDefaultAsync(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId);
@ -666,7 +671,7 @@ public class ChatRoomController(
return NoContent(); return NoContent();
} }
private async Task _SendInviteNotify(ChatMember member) private async Task _SendInviteNotify(ChatMember member)
{ {
await nty.SendNotification(member.Account, "invites.chats", "New Chat Invitation", null, await nty.SendNotification(member.Account, "invites.chats", "New Chat Invitation", null,

View File

@ -72,10 +72,14 @@ public class ChatRoomService(AppDatabase db, IMemoryCache cache)
return room; return room;
} }
public async Task<bool> IsMemberWithRole(Guid roomId, Guid accountId, ChatMemberRole requiredRole) public async Task<bool> IsMemberWithRole(Guid roomId, Guid accountId, params ChatMemberRole[] requiredRoles)
{ {
if (requiredRoles.Length == 0)
return false;
var maxRequiredRole = requiredRoles.Max();
var member = await db.ChatMembers var member = await db.ChatMembers
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId); .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
return member?.Role >= requiredRole; return member?.Role >= maxRequiredRole;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,244 @@
using System.Collections.Generic;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class OptimizeFileStorage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "image_id",
table: "stickers",
type: "character varying(32)",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(128)");
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "realms",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "realms",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "publishers",
type: "character varying(32)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "publishers",
type: "character varying(32)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "id",
table: "files",
type: "character varying(32)",
maxLength: 32,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128);
migrationBuilder.AddColumn<List<CloudFileSensitiveMark>>(
name: "sensitive_marks",
table: "files",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "storage_id",
table: "files",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "storage_url",
table: "files",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "chat_rooms",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "chat_rooms",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "account_profiles",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "account_profiles",
type: "character varying(32)",
maxLength: 32,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "sensitive_marks",
table: "files");
migrationBuilder.DropColumn(
name: "storage_id",
table: "files");
migrationBuilder.DropColumn(
name: "storage_url",
table: "files");
migrationBuilder.AlterColumn<string>(
name: "image_id",
table: "stickers",
type: "character varying(128)",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(32)");
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "realms",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "realms",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "publishers",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "publishers",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "id",
table: "files",
type: "character varying(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "chat_rooms",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "chat_rooms",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "picture_id",
table: "account_profiles",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "background_id",
table: "account_profiles",
type: "character varying(128)",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(32)",
oldMaxLength: 32,
oldNullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Sphere.Migrations
{
/// <inheritdoc />
public partial class DontKnowHowToNameThing : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "ix_stickers_slug",
table: "stickers",
column: "slug");
migrationBuilder.CreateIndex(
name: "ix_sticker_packs_prefix",
table: "sticker_packs",
column: "prefix",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "ix_stickers_slug",
table: "stickers");
migrationBuilder.DropIndex(
name: "ix_sticker_packs_prefix",
table: "sticker_packs");
}
}
}

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Sphere; using DysonNetwork.Sphere;
using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -11,7 +12,6 @@ using NetTopologySuite.Geometries;
using NodaTime; using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using NpgsqlTypes; using NpgsqlTypes;
using Point = NetTopologySuite.Geometries.Point;
#nullable disable #nullable disable
@ -537,7 +537,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("account_id"); .HasColumnName("account_id");
b.Property<string>("BackgroundId") b.Property<string>("BackgroundId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("background_id"); .HasColumnName("background_id");
b.Property<string>("Bio") b.Property<string>("Bio")
@ -573,7 +574,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("middle_name"); .HasColumnName("middle_name");
b.Property<string>("PictureId") b.Property<string>("PictureId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("picture_id"); .HasColumnName("picture_id");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
@ -957,7 +959,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("id"); .HasColumnName("id");
b.Property<string>("BackgroundId") b.Property<string>("BackgroundId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("background_id"); .HasColumnName("background_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
@ -983,7 +986,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("name"); .HasColumnName("name");
b.Property<string>("PictureId") b.Property<string>("PictureId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("picture_id"); .HasColumnName("picture_id");
b.Property<Guid?>("RealmId") b.Property<Guid?>("RealmId")
@ -1766,7 +1770,7 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("account_id"); .HasColumnName("account_id");
b.Property<string>("BackgroundId") b.Property<string>("BackgroundId")
.HasColumnType("character varying(128)") .HasColumnType("character varying(32)")
.HasColumnName("background_id"); .HasColumnName("background_id");
b.Property<string>("Bio") b.Property<string>("Bio")
@ -1795,7 +1799,7 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("nick"); .HasColumnName("nick");
b.Property<string>("PictureId") b.Property<string>("PictureId")
.HasColumnType("character varying(128)") .HasColumnType("character varying(32)")
.HasColumnName("picture_id"); .HasColumnName("picture_id");
b.Property<Guid?>("RealmId") b.Property<Guid?>("RealmId")
@ -1972,7 +1976,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("account_id"); .HasColumnName("account_id");
b.Property<string>("BackgroundId") b.Property<string>("BackgroundId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("background_id"); .HasColumnName("background_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
@ -2004,7 +2009,8 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnName("name"); .HasColumnName("name");
b.Property<string>("PictureId") b.Property<string>("PictureId")
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("picture_id"); .HasColumnName("picture_id");
b.Property<string>("Slug") b.Property<string>("Slug")
@ -2101,7 +2107,8 @@ namespace DysonNetwork.Sphere.Migrations
b.Property<string>("ImageId") b.Property<string>("ImageId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(128)") .HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("image_id"); .HasColumnName("image_id");
b.Property<Guid>("PackId") b.Property<Guid>("PackId")
@ -2127,6 +2134,9 @@ namespace DysonNetwork.Sphere.Migrations
b.HasIndex("PackId") b.HasIndex("PackId")
.HasDatabaseName("ix_stickers_pack_id"); .HasDatabaseName("ix_stickers_pack_id");
b.HasIndex("Slug")
.HasDatabaseName("ix_stickers_slug");
b.ToTable("stickers", (string)null); b.ToTable("stickers", (string)null);
}); });
@ -2174,6 +2184,10 @@ namespace DysonNetwork.Sphere.Migrations
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_sticker_packs"); .HasName("pk_sticker_packs");
b.HasIndex("Prefix")
.IsUnique()
.HasDatabaseName("ix_sticker_packs_prefix");
b.HasIndex("PublisherId") b.HasIndex("PublisherId")
.HasDatabaseName("ix_sticker_packs_publisher_id"); .HasDatabaseName("ix_sticker_packs_publisher_id");
@ -2183,8 +2197,8 @@ namespace DysonNetwork.Sphere.Migrations
modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
.HasMaxLength(128) .HasMaxLength(32)
.HasColumnType("character varying(128)") .HasColumnType("character varying(32)")
.HasColumnName("id"); .HasColumnName("id");
b.Property<Guid>("AccountId") b.Property<Guid>("AccountId")
@ -2240,10 +2254,24 @@ namespace DysonNetwork.Sphere.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("post_id"); .HasColumnName("post_id");
b.Property<List<CloudFileSensitiveMark>>("SensitiveMarks")
.HasColumnType("jsonb")
.HasColumnName("sensitive_marks");
b.Property<long>("Size") b.Property<long>("Size")
.HasColumnType("bigint") .HasColumnType("bigint")
.HasColumnName("size"); .HasColumnName("size");
b.Property<string>("StorageId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("storage_id");
b.Property<string>("StorageUrl")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("storage_url");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");

View File

@ -177,8 +177,7 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Ac
} }
var query = db.RealmMembers var query = db.RealmMembers
.Where(m => m.RealmId == realm.Id) .Where(m => m.RealmId == realm.Id);
.Where(m => m.JoinedAt != null);
var total = await query.CountAsync(); var total = await query.CountAsync();
Response.Headers["X-Total"] = total.ToString(); Response.Headers["X-Total"] = total.ToString();
@ -240,7 +239,7 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Ac
return NoContent(); return NoContent();
} }
public class RealmRequest public class RealmRequest
{ {
[MaxLength(1024)] public string? Slug { get; set; } [MaxLength(1024)] public string? Slug { get; set; }
@ -372,6 +371,118 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Ac
return Ok(realm); return Ok(realm);
} }
[HttpPost("{slug}/members/me")]
[Authorize]
public async Task<ActionResult<RealmMember>> JoinRealm(string slug)
{
if (HttpContext.Items["CurrentUser"] is not Account.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 == currentUser.Id && m.RealmId == realm.Id)
.FirstOrDefaultAsync();
if (existingMember is not null)
return BadRequest("You are already a member of this realm.");
var member = new RealmMember
{
AccountId = currentUser.Id,
RealmId = realm.Id,
Role = RealmMemberRole.Normal,
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
};
db.RealmMembers.Add(member);
await db.SaveChangesAsync();
als.CreateActionLogFromRequest(
ActionLogType.RealmJoin,
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", currentUser.Id } },
Request
);
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.Account currentUser) return Unauthorized();
var realm = await db.Realms
.Where(r => r.Slug == slug)
.FirstOrDefaultAsync();
if (realm is null) return NotFound();
var currentMember = await db.RealmMembers
.Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id && m.JoinedAt != null)
.FirstOrDefaultAsync();
if (currentMember is null || currentMember.Role < RealmMemberRole.Moderator)
return StatusCode(403, "You do not have permission to remove members from this realm.");
var memberToRemove = await db.RealmMembers
.Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
.FirstOrDefaultAsync();
if (memberToRemove is null) return NotFound();
if (memberToRemove.Role >= currentMember.Role)
return StatusCode(403, "You cannot remove members with equal or higher roles.");
db.RealmMembers.Remove(memberToRemove);
await db.SaveChangesAsync();
als.CreateActionLogFromRequest(
ActionLogType.ChatroomKick,
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", memberId } },
Request
);
return NoContent();
}
[HttpPatch("{slug}/members/{memberId:guid}/role")]
[Authorize]
public async Task<ActionResult<RealmMember>> UpdateMemberRole(string slug, Guid memberId,
[FromBody] RealmMemberRole newRole)
{
if (HttpContext.Items["CurrentUser"] is not Account.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)
.Include(m => m.Account)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role, newRole))
return StatusCode(403, "You do not have permission to update member roles in this realm.");
member.Role = newRole;
db.RealmMembers.Update(member);
await db.SaveChangesAsync();
als.CreateActionLogFromRequest(
ActionLogType.RealmAdjustRole,
new Dictionary<string, object>
{ { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } },
Request
);
return Ok(member);
}
[HttpDelete("{slug}")] [HttpDelete("{slug}")]
[Authorize] [Authorize]
public async Task<ActionResult> Delete(string slug) public async Task<ActionResult> Delete(string slug)
@ -385,10 +496,7 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Ac
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (realm is null) return NotFound(); if (realm is null) return NotFound();
var member = await db.RealmMembers if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Owner))
.Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id && m.JoinedAt != null)
.FirstOrDefaultAsync();
if (member is null || member.Role < RealmMemberRole.Owner)
return StatusCode(403, "Only the owner can delete this realm."); return StatusCode(403, "Only the owner can delete this realm.");
db.Realms.Remove(realm); db.Realms.Remove(realm);

View File

@ -11,10 +11,14 @@ public class RealmService(AppDatabase db, NotificationService nty)
$"You just got invited to join {member.Realm.Name}"); $"You just got invited to join {member.Realm.Name}");
} }
public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, RealmMemberRole requiredRole) public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, params RealmMemberRole[] requiredRoles)
{ {
if (requiredRoles.Length == 0)
return false;
var maxRequiredRole = requiredRoles.Max();
var member = await db.RealmMembers var member = await db.RealmMembers
.FirstOrDefaultAsync(m => m.RealmId == realmId && m.AccountId == accountId); .FirstOrDefaultAsync(m => m.RealmId == realmId && m.AccountId == accountId);
return member?.Role >= requiredRole; return member?.Role >= maxRequiredRole;
} }
} }

View File

@ -1,20 +1,23 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Sticker; namespace DysonNetwork.Sphere.Sticker;
[Index(nameof(Slug))] // The slug index shouldn't be unique, the sticker slug can be repeated across packs.
public class Sticker : ModelBase public class Sticker : ModelBase
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
[MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(128)] public string Slug { get; set; } = null!;
public string ImageId { get; set; } = null!; [MaxLength(32)] public string ImageId { get; set; } = null!;
public CloudFile Image { get; set; } = null!; public CloudFile Image { get; set; } = null!;
public Guid PackId { get; set; } public Guid PackId { get; set; }
public StickerPack Pack { get; set; } = null!; public StickerPack Pack { get; set; } = null!;
} }
[Index(nameof(Prefix), IsUnique = true)]
public class StickerPack : ModelBase public class StickerPack : ModelBase
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();

View File

@ -34,7 +34,7 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Invalidate cache for this sticker // Invalidate cache for this sticker
InvalidateStickerCache(sticker); PurgeStickerCache(sticker);
return sticker; return sticker;
} }
@ -45,7 +45,7 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
await fs.MarkUsageAsync(sticker.Image, -1); await fs.MarkUsageAsync(sticker.Image, -1);
// Invalidate cache for this sticker // Invalidate cache for this sticker
InvalidateStickerCache(sticker); PurgeStickerCache(sticker);
} }
public async Task DeleteStickerPackAsync(StickerPack pack) public async Task DeleteStickerPackAsync(StickerPack pack)
{ {
@ -65,7 +65,7 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
// Invalidate cache for all stickers in this pack // Invalidate cache for all stickers in this pack
foreach (var sticker in stickers) foreach (var sticker in stickers)
{ {
InvalidateStickerCache(sticker); PurgeStickerCache(sticker);
} }
} }
@ -91,14 +91,12 @@ public class StickerService(AppDatabase db, FileService fs, IMemoryCache cache)
// Store in cache if found // Store in cache if found
if (sticker != null) if (sticker != null)
{
cache.Set(cacheKey, sticker, CacheDuration); cache.Set(cacheKey, sticker, CacheDuration);
}
return sticker; return sticker;
} }
private void InvalidateStickerCache(Sticker sticker) private void PurgeStickerCache(Sticker sticker)
{ {
// Remove both possible cache entries // Remove both possible cache entries
cache.Remove($"StickerLookup_{sticker.Id}"); cache.Remove($"StickerLookup_{sticker.Id}");

View File

@ -28,7 +28,7 @@ public class CloudFile : ModelBase
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object>? FileMeta { get; set; } = null!; [Column(TypeName = "jsonb")] public Dictionary<string, object>? FileMeta { get; set; } = null!;
[Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!; [Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!;
[Column(TypeName = "jsonb")] public List<CloudFileSensitiveMark> SensitiveMarks { get; set; } = new(); [Column(TypeName = "jsonb")] public List<CloudFileSensitiveMark>? SensitiveMarks { get; set; } = [];
[MaxLength(256)] public string? MimeType { get; set; } [MaxLength(256)] public string? MimeType { get; set; }
[MaxLength(256)] public string? Hash { get; set; } [MaxLength(256)] public string? Hash { get; set; }
public long Size { get; set; } public long Size { get; set; }