🗃️ Add nonce column to chat messages and fix column typo
This migration adds a new "nonce" column to the "chat_messages" table to ensure message uniqueness or integrity. Additionally, it corrects a typo in the "members_mentioned" column name to improve consistency and clarity.
This commit is contained in:
parent
f6acb3f2f0
commit
196547e50f
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@ -19,17 +20,75 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
public class SendMessageRequest
|
public class SendMessageRequest
|
||||||
{
|
{
|
||||||
[MaxLength(4096)] public string? Content { get; set; }
|
[MaxLength(4096)] public string? Content { get; set; }
|
||||||
public List<CloudFile>? Attachments { get; set; }
|
[MaxLength(36)] public string? Nonce { get; set; }
|
||||||
|
public List<string>? AttachmentsId { get; set; }
|
||||||
|
public Dictionary<string, object>? Meta { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{roomId:long}/members/me")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ChatMember>> GetCurrentIdentity(long roomId)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var member = await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||||
|
.Include(m => m.Account)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (member == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{roomId:long}/messages")]
|
||||||
|
public async Task<ActionResult<List<Message>>> ListMessages(long roomId, [FromQuery] int offset, [FromQuery] int take = 20)
|
||||||
|
{
|
||||||
|
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||||
|
|
||||||
|
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||||
|
if (room is null) return NotFound();
|
||||||
|
|
||||||
|
if (!room.IsPublic)
|
||||||
|
{
|
||||||
|
if (currentUser is null) return Unauthorized();
|
||||||
|
|
||||||
|
var member = await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member == null || member.Role < ChatMemberRole.Normal)
|
||||||
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await db.ChatMessages
|
||||||
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
|
.CountAsync();
|
||||||
|
var messages = await db.ChatMessages
|
||||||
|
.Where(m => m.ChatRoomId == roomId)
|
||||||
|
.OrderByDescending(m => m.CreatedAt)
|
||||||
|
.Include(m => m.Sender)
|
||||||
|
.Include(m => m.Sender.Account)
|
||||||
|
.Include(m => m.Attachments)
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
Response.Headers["X-Total"] = totalCount.ToString();
|
||||||
|
|
||||||
|
return Ok(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"@([A-Za-z0-9_-]+)")]
|
[GeneratedRegex(@"@([A-Za-z0-9_-]+)")]
|
||||||
private static partial Regex MentionRegex();
|
private static partial Regex MentionRegex();
|
||||||
|
|
||||||
[HttpPost("{roomId:long}/messages")]
|
[HttpPost("{roomId:long}/messages")]
|
||||||
|
[Authorize]
|
||||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, long roomId)
|
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, long roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
if (string.IsNullOrWhiteSpace(request.Content) && (request.Attachments == null || request.Attachments.Count == 0))
|
if (string.IsNullOrWhiteSpace(request.Content) && (request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||||
return BadRequest("You cannot send an empty message.");
|
return BadRequest("You cannot send an empty message.");
|
||||||
|
|
||||||
var member = await db.ChatMembers
|
var member = await db.ChatMembers
|
||||||
@ -38,16 +97,27 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
.Include(m => m.ChatRoom.Realm)
|
.Include(m => m.ChatRoom.Realm)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (member == null || member.Role < ChatMemberRole.Normal) return StatusCode(403, "You need to be a normal member to send messages here.");
|
if (member == null || member.Role < ChatMemberRole.Normal) return StatusCode(403, "You need to be a normal member to send messages here.");
|
||||||
|
currentUser.Profile = null;
|
||||||
|
member.Account = currentUser;
|
||||||
|
|
||||||
var message = new Message
|
var message = new Message
|
||||||
{
|
{
|
||||||
SenderId = member.Id,
|
SenderId = member.Id,
|
||||||
ChatRoomId = roomId,
|
ChatRoomId = roomId,
|
||||||
|
Nonce = request.Nonce ?? Guid.NewGuid().ToString(),
|
||||||
|
Meta = request.Meta ?? new Dictionary<string, object>(),
|
||||||
};
|
};
|
||||||
if (request.Content is not null)
|
if (request.Content is not null)
|
||||||
message.Content = request.Content;
|
message.Content = request.Content;
|
||||||
if (request.Attachments is not null)
|
if (request.AttachmentsId is not null)
|
||||||
message.Attachments = request.Attachments;
|
{
|
||||||
|
var attachments = await db.Files
|
||||||
|
.Where(f => request.AttachmentsId.Contains(f.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
message.Attachments = attachments
|
||||||
|
.OrderBy(f => request.AttachmentsId.IndexOf(f.Id))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Content is not null)
|
if (request.Content is not null)
|
||||||
{
|
{
|
||||||
@ -55,17 +125,16 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
.Matches(request.Content)
|
.Matches(request.Content)
|
||||||
.Select(m => m.Groups[1].Value)
|
.Select(m => m.Groups[1].Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (mentioned is not null && mentioned.Count > 0)
|
if (mentioned.Count > 0)
|
||||||
{
|
{
|
||||||
var mentionedMembers = await db.ChatMembers
|
var mentionedMembers = await db.ChatMembers
|
||||||
.Where(m => mentioned.Contains(m.Account.Name))
|
.Where(m => mentioned.Contains(m.Account.Name))
|
||||||
.Select(m => m.Id)
|
.Select(m => m.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
message.MembersMetioned = mentionedMembers;
|
message.MembersMentioned = mentionedMembers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
member.Account = currentUser;
|
|
||||||
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
@ -78,9 +147,9 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{roomId:long}/sync")]
|
[HttpGet("{roomId:long}/sync")]
|
||||||
public async Task<ActionResult<SyncResponse>> GetSyncData([FromQuery] SyncRequest request, long roomId)
|
public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, long roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var isMember = await db.ChatMembers
|
var isMember = await db.ChatMembers
|
||||||
|
@ -23,7 +23,7 @@ public class ChatService(AppDatabase db, NotificationService nty, WebSocketServi
|
|||||||
var member in db.ChatMembers
|
var member in db.ChatMembers
|
||||||
.Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != message.Sender.AccountId)
|
.Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != message.Sender.AccountId)
|
||||||
.Where(m => m.Notify != ChatMemberNotify.None)
|
.Where(m => m.Notify != ChatMemberNotify.None)
|
||||||
.Where(m => m.Notify != ChatMemberNotify.Mentions || (message.MembersMetioned != null && message.MembersMetioned.Contains(m.Id)))
|
.Where(m => m.Notify != ChatMemberNotify.Mentions || (message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id)))
|
||||||
.AsAsyncEnumerable()
|
.AsAsyncEnumerable()
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -94,7 +94,7 @@ public class ChatService(AppDatabase db, NotificationService nty, WebSocketServi
|
|||||||
.Select(m => new MessageChange
|
.Select(m => new MessageChange
|
||||||
{
|
{
|
||||||
MessageId = m.Id,
|
MessageId = m.Id,
|
||||||
Action = m.DeletedAt != null ? "delete" : (m.EditedAt == null ? "create" : "update"),
|
Action = m.DeletedAt != null ? "delete" : (m.UpdatedAt == null ? "create" : "update"),
|
||||||
Message = m.DeletedAt != null ? null : m,
|
Message = m.DeletedAt != null ? null : m,
|
||||||
Timestamp = m.DeletedAt != null ? m.DeletedAt.Value : m.UpdatedAt
|
Timestamp = m.DeletedAt != null ? m.DeletedAt.Value : m.UpdatedAt
|
||||||
})
|
})
|
||||||
|
@ -9,10 +9,10 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
public class Message : ModelBase
|
public class Message : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
[MaxLength(1024)] public string Type { get; set; } = null!;
|
|
||||||
[MaxLength(4096)] public string Content { get; set; } = string.Empty;
|
[MaxLength(4096)] public string Content { get; set; } = string.Empty;
|
||||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public List<Guid>? MembersMetioned { get; set; }
|
[Column(TypeName = "jsonb")] public List<Guid>? MembersMentioned { get; set; }
|
||||||
|
[MaxLength(36)] public string Nonce { get; set; } = null!;
|
||||||
public Instant? EditedAt { get; set; }
|
public Instant? EditedAt { get; set; }
|
||||||
|
|
||||||
public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>();
|
public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>();
|
||||||
@ -28,6 +28,33 @@ public class Message : ModelBase
|
|||||||
public ChatMember Sender { get; set; } = null!;
|
public ChatMember Sender { get; set; } = null!;
|
||||||
public long ChatRoomId { get; set; }
|
public long ChatRoomId { get; set; }
|
||||||
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
|
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
|
||||||
|
|
||||||
|
public Message Clone()
|
||||||
|
{
|
||||||
|
return new Message
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Content = Content,
|
||||||
|
Meta = Meta?.ToDictionary(entry => entry.Key, entry => entry.Value),
|
||||||
|
MembersMentioned = MembersMentioned?.ToList(),
|
||||||
|
Nonce = Nonce,
|
||||||
|
EditedAt = EditedAt,
|
||||||
|
Attachments = new List<CloudFile>(Attachments),
|
||||||
|
Reactions = new List<MessageReaction>(Reactions),
|
||||||
|
Statuses = new List<MessageStatus>(Statuses),
|
||||||
|
RepliedMessageId = RepliedMessageId,
|
||||||
|
RepliedMessage = RepliedMessage?.Clone() as Message,
|
||||||
|
ForwardedMessageId = ForwardedMessageId,
|
||||||
|
ForwardedMessage = ForwardedMessage?.Clone() as Message,
|
||||||
|
SenderId = SenderId,
|
||||||
|
Sender = Sender,
|
||||||
|
ChatRoomId = ChatRoomId,
|
||||||
|
ChatRoom = ChatRoom,
|
||||||
|
CreatedAt = CreatedAt,
|
||||||
|
UpdatedAt = UpdatedAt,
|
||||||
|
DeletedAt = DeletedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MessageReactionAttitude
|
public enum MessageReactionAttitude
|
||||||
|
2364
DysonNetwork.Sphere/Migrations/20250503032932_AddChatMessageNonce.Designer.cs
generated
Normal file
2364
DysonNetwork.Sphere/Migrations/20250503032932_AddChatMessageNonce.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddChatMessageNonce : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "members_metioned",
|
||||||
|
table: "chat_messages",
|
||||||
|
newName: "members_mentioned");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "nonce",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "nonce",
|
||||||
|
table: "chat_messages");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "members_mentioned",
|
||||||
|
table: "chat_messages",
|
||||||
|
newName: "members_metioned");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2359
DysonNetwork.Sphere/Migrations/20250503040922_DontKnowHowToNameThing2.Designer.cs
generated
Normal file
2359
DysonNetwork.Sphere/Migrations/20250503040922_DontKnowHowToNameThing2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DontKnowHowToNameThing2 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "type",
|
||||||
|
table: "chat_messages");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "nonce",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "character varying(36)",
|
||||||
|
maxLength: 36,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "nonce",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(36)",
|
||||||
|
oldMaxLength: 36);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "type",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "character varying(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -806,14 +806,20 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("forwarded_message_id");
|
.HasColumnName("forwarded_message_id");
|
||||||
|
|
||||||
b.Property<List<Guid>>("MembersMetioned")
|
b.Property<List<Guid>>("MembersMentioned")
|
||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("members_metioned");
|
.HasColumnName("members_mentioned");
|
||||||
|
|
||||||
b.Property<Dictionary<string, object>>("Meta")
|
b.Property<Dictionary<string, object>>("Meta")
|
||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("meta");
|
.HasColumnName("meta");
|
||||||
|
|
||||||
|
b.Property<string>("Nonce")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("character varying(36)")
|
||||||
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
b.Property<Guid?>("RepliedMessageId")
|
b.Property<Guid?>("RepliedMessageId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("replied_message_id");
|
.HasColumnName("replied_message_id");
|
||||||
@ -822,12 +828,6 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("sender_id");
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
b.Property<string>("Type")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(1024)
|
|
||||||
.HasColumnType("character varying(1024)")
|
|
||||||
.HasColumnName("type");
|
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user