✨ Notify level API on chat
🗃️ Enrich the settings of chat members 🐛 Fix role settings on both chat & realm
This commit is contained in:
parent
0c48694493
commit
9e17be38d8
@ -41,11 +41,11 @@ public class ChatRoom : ModelBase, IIdentifiedResource
|
||||
public string ResourceIdentifier => $"chatroom/{Id}";
|
||||
}
|
||||
|
||||
public enum ChatMemberRole
|
||||
public abstract class ChatMemberRole
|
||||
{
|
||||
Owner = 100,
|
||||
Moderator = 50,
|
||||
Member = 0
|
||||
public const int Owner = 100;
|
||||
public const int Moderator = 50;
|
||||
public const int Member = 0;
|
||||
}
|
||||
|
||||
public enum ChatMemberNotify
|
||||
@ -55,6 +55,18 @@ public enum ChatMemberNotify
|
||||
None
|
||||
}
|
||||
|
||||
public enum ChatTimeoutCauseType
|
||||
{
|
||||
ByModerator = 0,
|
||||
BySlowMode = 1,
|
||||
}
|
||||
|
||||
public class ChatTimeoutCause
|
||||
{
|
||||
public ChatTimeoutCauseType Type { get; set; }
|
||||
public Guid? SenderId { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMember : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
@ -65,12 +77,27 @@ public class ChatMember : ModelBase
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
||||
public ChatMemberRole Role { get; set; } = ChatMemberRole.Member;
|
||||
public int Role { get; set; } = ChatMemberRole.Member;
|
||||
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
|
||||
public Instant? LastReadAt { get; set; }
|
||||
public Instant? JoinedAt { get; set; }
|
||||
public Instant? LeaveAt { get; set; }
|
||||
public bool IsBot { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The break time is the user doesn't receive any message from this member for a while.
|
||||
/// Expect mentioned him or her.
|
||||
/// </summary>
|
||||
public Instant? BreakUntil { get; set; }
|
||||
/// <summary>
|
||||
/// The timeout is the user can't send any message.
|
||||
/// Set by the moderator of the chat room.
|
||||
/// </summary>
|
||||
public Instant? TimeoutUntil { get; set; }
|
||||
/// <summary>
|
||||
/// The timeout cause is the reason why the user is timeout.
|
||||
/// </summary>
|
||||
[Column(TypeName = "jsonb")] public ChatTimeoutCause? TimeoutCause { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMemberTransmissionObject : ModelBase
|
||||
@ -82,11 +109,15 @@ public class ChatMemberTransmissionObject : ModelBase
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
||||
public ChatMemberRole Role { get; set; } = ChatMemberRole.Member;
|
||||
public int Role { get; set; } = ChatMemberRole.Member;
|
||||
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
public Instant? LeaveAt { get; set; }
|
||||
public bool IsBot { get; set; } = false;
|
||||
|
||||
public Instant? BreakUntil { get; set; }
|
||||
public Instant? TimeoutUntil { get; set; }
|
||||
public ChatTimeoutCause? TimeoutCause { get; set; }
|
||||
|
||||
public static ChatMemberTransmissionObject FromEntity(ChatMember member)
|
||||
{
|
||||
@ -102,6 +133,9 @@ public class ChatMemberTransmissionObject : ModelBase
|
||||
JoinedAt = member.JoinedAt,
|
||||
LeaveAt = member.LeaveAt,
|
||||
IsBot = member.IsBot,
|
||||
BreakUntil = member.BreakUntil,
|
||||
TimeoutUntil = member.TimeoutUntil,
|
||||
TimeoutCause = member.TimeoutCause,
|
||||
CreatedAt = member.CreatedAt,
|
||||
UpdatedAt = member.UpdatedAt,
|
||||
DeletedAt = member.DeletedAt
|
||||
|
@ -195,15 +195,15 @@ public class ChatRoomController(
|
||||
|
||||
if (chatRoom.Picture is not null)
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
chatRoom.Picture.Id,
|
||||
"chat.room.picture",
|
||||
chatRoom.Picture.Id,
|
||||
"chat.room.picture",
|
||||
chatRoomResourceId
|
||||
);
|
||||
|
||||
if (chatRoom.Background is not null)
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
chatRoom.Background.Id,
|
||||
"chat.room.background",
|
||||
chatRoom.Background.Id,
|
||||
"chat.room.background",
|
||||
chatRoomResourceId
|
||||
);
|
||||
|
||||
@ -254,7 +254,8 @@ public class ChatRoomController(
|
||||
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for pictures
|
||||
var oldPictureRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.picture");
|
||||
var oldPictureRefs =
|
||||
await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.picture");
|
||||
foreach (var oldRef in oldPictureRefs)
|
||||
{
|
||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||
@ -262,8 +263,8 @@ public class ChatRoomController(
|
||||
|
||||
// Add a new reference
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
picture.Id,
|
||||
"chat.room.picture",
|
||||
picture.Id,
|
||||
"chat.room.picture",
|
||||
chatRoomResourceId
|
||||
);
|
||||
|
||||
@ -276,7 +277,8 @@ public class ChatRoomController(
|
||||
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for backgrounds
|
||||
var oldBackgroundRefs = await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.background");
|
||||
var oldBackgroundRefs =
|
||||
await fileRefService.GetResourceReferencesAsync(chatRoomResourceId, "chat.room.background");
|
||||
foreach (var oldRef in oldBackgroundRefs)
|
||||
{
|
||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||
@ -284,8 +286,8 @@ public class ChatRoomController(
|
||||
|
||||
// Add a new reference
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
background.Id,
|
||||
"chat.room.background",
|
||||
background.Id,
|
||||
"chat.room.background",
|
||||
chatRoomResourceId
|
||||
);
|
||||
|
||||
@ -404,7 +406,7 @@ public class ChatRoomController(
|
||||
public class ChatMemberRequest
|
||||
{
|
||||
[Required] public Guid RelatedUserId { get; set; }
|
||||
[Required] public ChatMemberRole Role { get; set; }
|
||||
[Required] public int Role { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("invites/{roomId:guid}")]
|
||||
@ -551,10 +553,47 @@ public class ChatRoomController(
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public class ChatMemberNotifyRequest
|
||||
{
|
||||
public ChatMemberNotify? NotifyLevel { get; set; }
|
||||
public Instant? BreakUntil { get; set; }
|
||||
}
|
||||
|
||||
[HttpPatch("{roomId:guid}/members/me/notify")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberNotify(
|
||||
Guid roomId,
|
||||
Guid memberId,
|
||||
[FromBody] ChatMemberNotifyRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(r => r.Id == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (chatRoom is null) return NotFound();
|
||||
|
||||
var targetMember = await db.ChatMembers
|
||||
.Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (targetMember is null) return BadRequest("You have not joined this chat room.");
|
||||
if (request.NotifyLevel is not null)
|
||||
targetMember.Notify = request.NotifyLevel.Value;
|
||||
if (request.BreakUntil is not null)
|
||||
targetMember.BreakUntil = request.BreakUntil.Value;
|
||||
|
||||
db.ChatMembers.Update(targetMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
return Ok(targetMember);
|
||||
}
|
||||
|
||||
[HttpPatch("{roomId:guid}/members/{memberId:guid}/role")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId,
|
||||
[FromBody] ChatMemberRole newRole)
|
||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
|
||||
{
|
||||
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
@ -597,6 +636,8 @@ public class ChatRoomController(
|
||||
db.ChatMembers.Update(targetMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmAdjustRole,
|
||||
new Dictionary<string, object>
|
||||
|
@ -9,14 +9,14 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
public const string ChatRoomGroupPrefix = "ChatRoom_";
|
||||
private const string RoomMembersCacheKeyPrefix = "ChatRoomMembers_";
|
||||
private const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
|
||||
|
||||
|
||||
public async Task<List<ChatMember>> ListRoomMembers(Guid roomId)
|
||||
{
|
||||
var cacheKey = RoomMembersCacheKeyPrefix + roomId;
|
||||
var cachedMembers = await cache.GetAsync<List<ChatMember>>(cacheKey);
|
||||
if (cachedMembers != null)
|
||||
return cachedMembers;
|
||||
|
||||
|
||||
var members = await db.ChatMembers
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
@ -27,18 +27,18 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
|
||||
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||
await cache.SetWithGroupsAsync(cacheKey, members,
|
||||
[chatRoomGroup],
|
||||
[chatRoomGroup],
|
||||
TimeSpan.FromMinutes(5));
|
||||
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ChatMember?> GetRoomMember(Guid accountId, Guid chatRoomId)
|
||||
{
|
||||
var cacheKey = string.Format(ChatMemberCacheKey, accountId, chatRoomId);
|
||||
var member = await cache.GetAsync<ChatMember?>(cacheKey);
|
||||
if (member is not null) return member;
|
||||
|
||||
|
||||
member = await db.ChatMembers
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
@ -50,12 +50,12 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
if (member == null) return member;
|
||||
var chatRoomGroup = ChatRoomGroupPrefix + chatRoomId;
|
||||
await cache.SetWithGroupsAsync(cacheKey, member,
|
||||
[chatRoomGroup],
|
||||
[chatRoomGroup],
|
||||
TimeSpan.FromMinutes(5));
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
|
||||
public async Task PurgeRoomMembersCache(Guid roomId)
|
||||
{
|
||||
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||
@ -70,15 +70,15 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.GroupBy(m => m.ChatRoomId)
|
||||
.Select(g => new { RoomId = g.Key, CreatedAt = g.Max(m => m.CreatedAt) })
|
||||
.ToDictionaryAsync(g => g.RoomId, m => m.CreatedAt);
|
||||
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var sortedRooms = rooms
|
||||
.OrderByDescending(r => lastMessages.TryGetValue(r.Id, out var time) ? time : now)
|
||||
.ToList();
|
||||
|
||||
|
||||
return sortedRooms;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<ChatRoom>> LoadDirectMessageMembers(List<ChatRoom> rooms, Guid userId)
|
||||
{
|
||||
var directRoomsId = rooms
|
||||
@ -86,7 +86,7 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.Select(r => r.Id)
|
||||
.ToList();
|
||||
if (directRoomsId.Count == 0) return rooms;
|
||||
|
||||
|
||||
var directMembers = directRoomsId.Count != 0
|
||||
? await db.ChatMembers
|
||||
.Where(m => directRoomsId.Contains(m.ChatRoomId))
|
||||
@ -97,7 +97,7 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.GroupBy(m => m.ChatRoomId)
|
||||
.ToDictionaryAsync(g => g.Key, g => g.ToList())
|
||||
: new Dictionary<Guid, List<ChatMember>>();
|
||||
|
||||
|
||||
return rooms.Select(r =>
|
||||
{
|
||||
if (r.Type == ChatRoomType.DirectMessage && directMembers.TryGetValue(r.Id, out var otherMembers))
|
||||
@ -105,7 +105,7 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
return r;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
||||
public async Task<ChatRoom> LoadDirectMessageMembers(ChatRoom room, Guid userId)
|
||||
{
|
||||
if (room.Type != ChatRoomType.DirectMessage) return room;
|
||||
@ -115,17 +115,17 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
if (members.Count > 0)
|
||||
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
|
||||
return room;
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid roomId, Guid accountId, params ChatMemberRole[] requiredRoles)
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid roomId, Guid accountId, params int[] requiredRoles)
|
||||
{
|
||||
if (requiredRoles.Length == 0)
|
||||
return false;
|
||||
|
||||
|
||||
var maxRequiredRole = requiredRoles.Max();
|
||||
var member = await db.ChatMembers
|
||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
|
||||
|
3357
DysonNetwork.Sphere/Migrations/20250609153232_EnrichChatMembers.Designer.cs
generated
Normal file
3357
DysonNetwork.Sphere/Migrations/20250609153232_EnrichChatMembers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class EnrichChatMembers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "break_until",
|
||||
table: "chat_members",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<ChatTimeoutCause>(
|
||||
name: "timeout_cause",
|
||||
table: "chat_members",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "timeout_until",
|
||||
table: "chat_members",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "break_until",
|
||||
table: "chat_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "timeout_cause",
|
||||
table: "chat_members");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "timeout_until",
|
||||
table: "chat_members");
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Chat;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -885,6 +886,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.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");
|
||||
@ -926,6 +931,14 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.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");
|
||||
|
@ -36,11 +36,11 @@ public class Realm : ModelBase, IIdentifiedResource
|
||||
public string ResourceIdentifier => $"realm/{Id}";
|
||||
}
|
||||
|
||||
public enum RealmMemberRole
|
||||
public abstract class RealmMemberRole
|
||||
{
|
||||
Owner = 100,
|
||||
Moderator = 50,
|
||||
Normal = 0
|
||||
public const int Owner = 100;
|
||||
public const int Moderator = 50;
|
||||
public const int Normal = 0;
|
||||
}
|
||||
|
||||
public class RealmMember : ModelBase
|
||||
@ -50,7 +50,7 @@ public class RealmMember : ModelBase
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
|
||||
public RealmMemberRole Role { get; set; } = RealmMemberRole.Normal;
|
||||
public int Role { get; set; } = RealmMemberRole.Normal;
|
||||
public Instant? JoinedAt { get; set; }
|
||||
public Instant? LeaveAt { get; set; }
|
||||
}
|
@ -67,7 +67,7 @@ public class RealmController(
|
||||
public class RealmMemberRequest
|
||||
{
|
||||
[Required] public Guid RelatedUserId { get; set; }
|
||||
[Required] public RealmMemberRole Role { get; set; }
|
||||
[Required] public int Role { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("invites/{slug}")]
|
||||
@ -516,8 +516,7 @@ public class RealmController(
|
||||
|
||||
[HttpPatch("{slug}/members/{memberId:guid}/role")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealmMember>> UpdateMemberRole(string slug, Guid memberId,
|
||||
[FromBody] RealmMemberRole newRole)
|
||||
public async Task<ActionResult<RealmMember>> 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.Account currentUser) return Unauthorized();
|
||||
|
@ -19,7 +19,7 @@ public class RealmService(AppDatabase db, NotificationService nty, IStringLocali
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, params RealmMemberRole[] requiredRoles)
|
||||
public async Task<bool> IsMemberWithRole(Guid realmId, Guid accountId, params int[] requiredRoles)
|
||||
{
|
||||
if (requiredRoles.Length == 0)
|
||||
return false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user