✨ Chat realtime calls
This commit is contained in:
parent
02aee07116
commit
fb07071603
@ -25,6 +25,8 @@ public class AccountController(
|
|||||||
{
|
{
|
||||||
var account = await db.Accounts
|
var account = await db.Accounts
|
||||||
.Include(e => e.Profile)
|
.Include(e => e.Profile)
|
||||||
|
.Include(e => e.Profile.Picture)
|
||||||
|
.Include(e => e.Profile.Background)
|
||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
return account is null ? new NotFoundResult() : account;
|
return account is null ? new NotFoundResult() : account;
|
||||||
@ -105,13 +107,15 @@ public class AccountController(
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<Account>> GetMe()
|
public async Task<ActionResult<Account>> GetCurrentIdentity()
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var userId = currentUser.Id;
|
var userId = currentUser.Id;
|
||||||
|
|
||||||
var account = await db.Accounts
|
var account = await db.Accounts
|
||||||
.Include(e => e.Profile)
|
.Include(e => e.Profile)
|
||||||
|
.Include(e => e.Profile.Picture)
|
||||||
|
.Include(e => e.Profile.Background)
|
||||||
.Where(e => e.Id == userId)
|
.Where(e => e.Id == userId)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ public class AppDatabase(
|
|||||||
public DbSet<Chat.ChatRoom> ChatRooms { get; set; }
|
public DbSet<Chat.ChatRoom> ChatRooms { get; set; }
|
||||||
public DbSet<Chat.ChatMember> ChatMembers { get; set; }
|
public DbSet<Chat.ChatMember> ChatMembers { get; set; }
|
||||||
public DbSet<Chat.Message> ChatMessages { get; set; }
|
public DbSet<Chat.Message> ChatMessages { get; set; }
|
||||||
|
public DbSet<Chat.RealtimeCall> ChatRealtimeCall { get; set; }
|
||||||
public DbSet<Chat.MessageStatus> ChatStatuses { get; set; }
|
public DbSet<Chat.MessageStatus> ChatStatuses { get; set; }
|
||||||
public DbSet<Chat.MessageReaction> ChatReactions { get; set; }
|
public DbSet<Chat.MessageReaction> ChatReactions { get; set; }
|
||||||
|
|
||||||
@ -84,7 +85,9 @@ public class AppDatabase(
|
|||||||
PermissionService.NewPermissionNode("group:default", "global", "posts.react", true),
|
PermissionService.NewPermissionNode("group:default", "global", "posts.react", true),
|
||||||
PermissionService.NewPermissionNode("group:default", "global", "publishers.create", true),
|
PermissionService.NewPermissionNode("group:default", "global", "publishers.create", true),
|
||||||
PermissionService.NewPermissionNode("group:default", "global", "files.create", true),
|
PermissionService.NewPermissionNode("group:default", "global", "files.create", true),
|
||||||
PermissionService.NewPermissionNode("group:default", "global", "chat.create", true)
|
PermissionService.NewPermissionNode("group:default", "global", "chat.create", true),
|
||||||
|
PermissionService.NewPermissionNode("group:default", "global", "chat.messages.create", true),
|
||||||
|
PermissionService.NewPermissionNode("group:default", "global", "chat.realtime.create", true)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
@ -205,6 +208,16 @@ public class AppDatabase(
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(m => m.RepliedMessageId)
|
.HasForeignKey(m => m.RepliedMessageId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<Chat.RealtimeCall>()
|
||||||
|
.HasOne(m => m.Room)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.RoomId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<Chat.RealtimeCall>()
|
||||||
|
.HasOne(m => m.Sender)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.SenderId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -105,6 +106,7 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
|
|
||||||
[HttpPost("{roomId:long}/messages")]
|
[HttpPost("{roomId:long}/messages")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RequiredPermission("global", "chat.messages.create")]
|
||||||
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();
|
||||||
@ -122,6 +124,7 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
|||||||
|
|
||||||
var message = new Message
|
var message = new Message
|
||||||
{
|
{
|
||||||
|
Type = "text",
|
||||||
SenderId = member.Id,
|
SenderId = member.Id,
|
||||||
ChatRoomId = roomId,
|
ChatRoomId = roomId,
|
||||||
Nonce = request.Nonce ?? Guid.NewGuid().ToString(),
|
Nonce = request.Nonce ?? Guid.NewGuid().ToString(),
|
||||||
|
@ -11,16 +11,16 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
|||||||
{
|
{
|
||||||
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
message.UpdatedAt = message.CreatedAt;
|
message.UpdatedAt = message.CreatedAt;
|
||||||
|
|
||||||
// First complete the save operation
|
// First complete the save operation
|
||||||
db.ChatMessages.Add(message);
|
db.ChatMessages.Add(message);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Then start the delivery process
|
// Then start the delivery process
|
||||||
// Using ConfigureAwait(false) is correct here since we don't need context to flow
|
// Using ConfigureAwait(false) is correct here since we don't need context to flow
|
||||||
_ = Task.Run(() => DeliverMessageAsync(message, sender, room))
|
_ = Task.Run(() => DeliverMessageAsync(message, sender, room))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +37,8 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
|||||||
var members = await scopedDb.ChatMembers
|
var members = await scopedDb.ChatMembers
|
||||||
.Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != sender.AccountId)
|
.Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != sender.AccountId)
|
||||||
.Where(m => m.Notify != ChatMemberNotify.None)
|
.Where(m => m.Notify != ChatMemberNotify.None)
|
||||||
.Where(m => m.Notify != ChatMemberNotify.Mentions ||
|
.Where(m => m.Notify != ChatMemberNotify.Mentions ||
|
||||||
(message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id)))
|
(message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id)))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
foreach (var member in members)
|
foreach (var member in members)
|
||||||
@ -101,6 +101,85 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
|||||||
return messages.Count(m => !m.IsRead);
|
return messages.Count(m => !m.IsRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<long, int>> CountUnreadMessagesForJoinedRoomsAsync(long userId)
|
||||||
|
{
|
||||||
|
var userRooms = await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == userId)
|
||||||
|
.Select(m => m.ChatRoomId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var messages = await db.ChatMessages
|
||||||
|
.Where(m => userRooms.Contains(m.ChatRoomId))
|
||||||
|
.Select(m => new
|
||||||
|
{
|
||||||
|
m.ChatRoomId,
|
||||||
|
IsRead = m.Statuses.Any(rs => rs.Sender.AccountId == userId)
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return messages
|
||||||
|
.GroupBy(m => m.ChatRoomId)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Count(m => !m.IsRead)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RealtimeCall> CreateCallAsync(ChatRoom room, ChatMember sender)
|
||||||
|
{
|
||||||
|
var call = new RealtimeCall
|
||||||
|
{
|
||||||
|
RoomId = room.Id,
|
||||||
|
SenderId = sender.Id,
|
||||||
|
};
|
||||||
|
db.ChatRealtimeCall.Add(call);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await SendMessageAsync(new Message
|
||||||
|
{
|
||||||
|
Type = "realtime.start",
|
||||||
|
ChatRoomId = room.Id,
|
||||||
|
SenderId = sender.Id,
|
||||||
|
Meta = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "call", call.Id }
|
||||||
|
}
|
||||||
|
}, sender, room);
|
||||||
|
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndCallAsync(long roomId)
|
||||||
|
{
|
||||||
|
var call = await GetCallOngoingAsync(roomId);
|
||||||
|
if (call is null) throw new InvalidOperationException("No ongoing call was not found.");
|
||||||
|
|
||||||
|
call.EndedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
db.ChatRealtimeCall.Update(call);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await SendMessageAsync(new Message
|
||||||
|
{
|
||||||
|
Type = "realtime.ended",
|
||||||
|
ChatRoomId = call.RoomId,
|
||||||
|
SenderId = call.SenderId,
|
||||||
|
Meta = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "call", call.Id }
|
||||||
|
}
|
||||||
|
}, call.Sender, call.Room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RealtimeCall?> GetCallOngoingAsync(long roomId)
|
||||||
|
{
|
||||||
|
return await db.ChatRealtimeCall
|
||||||
|
.Where(c => c.RoomId == roomId)
|
||||||
|
.Where(c => c.EndedAt == null)
|
||||||
|
.Include(c => c.Room)
|
||||||
|
.Include(c => c.Sender)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SyncResponse> GetSyncDataAsync(long roomId, long lastSyncTimestamp)
|
public async Task<SyncResponse> GetSyncDataAsync(long roomId, long lastSyncTimestamp)
|
||||||
{
|
{
|
||||||
var timestamp = Instant.FromUnixTimeMilliseconds(lastSyncTimestamp);
|
var timestamp = Instant.FromUnixTimeMilliseconds(lastSyncTimestamp);
|
||||||
|
@ -9,7 +9,8 @@ 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(4096)] public string Content { get; set; } = string.Empty;
|
public string Type { get; set; } = null!;
|
||||||
|
[MaxLength(4096)] public string? Content { get; set; }
|
||||||
[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>? MembersMentioned { get; set; }
|
[Column(TypeName = "jsonb")] public List<Guid>? MembersMentioned { get; set; }
|
||||||
[MaxLength(36)] public string Nonce { get; set; } = null!;
|
[MaxLength(36)] public string Nonce { get; set; } = null!;
|
||||||
|
15
DysonNetwork.Sphere/Chat/RealtimeCall.cs
Normal file
15
DysonNetwork.Sphere/Chat/RealtimeCall.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
|
public class RealtimeCall : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public Instant? EndedAt { get; set; }
|
||||||
|
|
||||||
|
public Guid SenderId { get; set; }
|
||||||
|
public ChatMember Sender { get; set; } = null!;
|
||||||
|
public long RoomId { get; set; }
|
||||||
|
public ChatRoom Room { get; set; } = null!;
|
||||||
|
}
|
104
DysonNetwork.Sphere/Chat/RealtimeCallController.cs
Normal file
104
DysonNetwork.Sphere/Chat/RealtimeCallController.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using tencentyun;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
|
public class RealtimeChatConfiguration
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = null!;
|
||||||
|
public int AppId { get; set; }
|
||||||
|
[JsonIgnore] public string SecretKey { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/chat/realtime")]
|
||||||
|
public class RealtimeCallController(IConfiguration configuration, AppDatabase db, ChatService cs) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RealtimeChatConfiguration _config =
|
||||||
|
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public ActionResult<RealtimeChatConfiguration> GetConfiguration()
|
||||||
|
{
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RealtimeChatToken
|
||||||
|
{
|
||||||
|
public RealtimeChatConfiguration Config { get; set; } = null!;
|
||||||
|
public string Token { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{roomId:long}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RealtimeChatToken>> GetToken(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)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
|
return StatusCode(403,
|
||||||
|
"You need to be a normal member to get the token for joining the realtime chatroom."
|
||||||
|
);
|
||||||
|
|
||||||
|
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||||
|
if (ongoingCall is null) return BadRequest("No ongoing call.");
|
||||||
|
|
||||||
|
var api = new TLSSigAPIv2(_config.AppId, _config.SecretKey);
|
||||||
|
var sig = api.GenSig(currentUser.Name);
|
||||||
|
if (sig is null) return StatusCode(500, "Failed to generate the token.");
|
||||||
|
|
||||||
|
return Ok(new RealtimeChatToken
|
||||||
|
{
|
||||||
|
Config = _config,
|
||||||
|
Token = sig
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{roomId:long}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> StartCall(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.ChatRoom)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
|
return StatusCode(403, "You need to be a normal member to start a call.");
|
||||||
|
|
||||||
|
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||||
|
if (ongoingCall is not null) return StatusCode(423, "There is already an ongoing call inside the chatroom.");
|
||||||
|
var call = await cs.CreateCallAsync(member.ChatRoom, member);
|
||||||
|
return Ok(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{roomId:long}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> EndCall(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)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
|
return StatusCode(403, "You need to be a normal member to end a call.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await cs.EndCallAsync(roomId);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return BadRequest(exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -44,6 +44,7 @@
|
|||||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
|
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.0" />
|
||||||
|
<PackageReference Include="tls-sig-api-v2" Version="1.0.1" />
|
||||||
<PackageReference Include="tusdotnet" Version="2.8.1" />
|
<PackageReference Include="tusdotnet" Version="2.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
2435
DysonNetwork.Sphere/Migrations/20250506164731_AddChatRealtimeCall.Designer.cs
generated
Normal file
2435
DysonNetwork.Sphere/Migrations/20250506164731_AddChatRealtimeCall.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddChatRealtimeCall : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "content",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4096)",
|
||||||
|
oldMaxLength: 4096);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "type",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_realtime_call",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
title = table.Column<string>(type: "text", nullable: true),
|
||||||
|
ended_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
sender_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
room_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_chat_realtime_call", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_realtime_call_chat_members_sender_id",
|
||||||
|
column: x => x.sender_id,
|
||||||
|
principalTable: "chat_members",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_realtime_call_chat_rooms_room_id",
|
||||||
|
column: x => x.room_id,
|
||||||
|
principalTable: "chat_rooms",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_realtime_call_room_id",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
column: "room_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_realtime_call_sender_id",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
column: "sender_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_realtime_call");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "type",
|
||||||
|
table: "chat_messages");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "content",
|
||||||
|
table: "chat_messages",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4096)",
|
||||||
|
oldMaxLength: 4096,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -785,7 +785,6 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnName("chat_room_id");
|
.HasColumnName("chat_room_id");
|
||||||
|
|
||||||
b.Property<string>("Content")
|
b.Property<string>("Content")
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnType("character varying(4096)")
|
||||||
.HasColumnName("content");
|
.HasColumnName("content");
|
||||||
@ -828,6 +827,11 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("sender_id");
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.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");
|
||||||
@ -934,6 +938,53 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.ToTable("chat_statuses", (string)null);
|
b.ToTable("chat_statuses", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("EndedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("ended_at");
|
||||||
|
|
||||||
|
b.Property<long>("RoomId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("room_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_realtime_call");
|
||||||
|
|
||||||
|
b.HasIndex("RoomId")
|
||||||
|
.HasDatabaseName("ix_chat_realtime_call_room_id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId")
|
||||||
|
.HasDatabaseName("ix_chat_realtime_call_sender_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_realtime_call", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -2038,6 +2089,27 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("Sender");
|
b.Navigation("Sender");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "Room")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_realtime_call_chat_rooms_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SenderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_realtime_call_chat_members_sender_id");
|
||||||
|
|
||||||
|
b.Navigation("Room");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group")
|
b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group")
|
||||||
|
@ -141,9 +141,9 @@ public class FileService(
|
|||||||
List<Task> tasks = [];
|
List<Task> tasks = [];
|
||||||
|
|
||||||
var ogFilePath = Path.Join(configuration.GetValue<string>("Tus:StorePath"), file.Id);
|
var ogFilePath = Path.Join(configuration.GetValue<string>("Tus:StorePath"), file.Id);
|
||||||
var vipsImage = NetVips.Image.NewFromFile(ogFilePath);
|
using var vipsImage = NetVips.Image.NewFromFile(ogFilePath);
|
||||||
var imagePath = Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}");
|
var imagePath = Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}");
|
||||||
tasks.Add(Task.Run(() => vipsImage.WriteToFile(imagePath + ".webp")));
|
vipsImage.WriteToFile(imagePath + ".webp");
|
||||||
result.Add((imagePath + ".webp", string.Empty));
|
result.Add((imagePath + ".webp", string.Empty));
|
||||||
|
|
||||||
if (vipsImage.Width * vipsImage.Height >= 1024 * 1024)
|
if (vipsImage.Width * vipsImage.Height >= 1024 * 1024)
|
||||||
@ -151,21 +151,14 @@ public class FileService(
|
|||||||
var scale = 1024.0 / Math.Max(vipsImage.Width, vipsImage.Height);
|
var scale = 1024.0 / Math.Max(vipsImage.Width, vipsImage.Height);
|
||||||
var imageCompressedPath =
|
var imageCompressedPath =
|
||||||
Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}-compressed");
|
Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}-compressed");
|
||||||
|
|
||||||
// Create and save image within the same synchronous block to avoid disposal issues
|
// Create and save image within the same synchronous block to avoid disposal issues
|
||||||
tasks.Add(Task.Run(() => {
|
using var compressedImage = vipsImage.Resize(scale);
|
||||||
using var compressedImage = vipsImage.Resize(scale);
|
compressedImage.WriteToFile(imageCompressedPath + ".webp");
|
||||||
compressedImage.WriteToFile(imageCompressedPath + ".webp");
|
|
||||||
vipsImage.Dispose();
|
|
||||||
}));
|
|
||||||
|
|
||||||
result.Add((imageCompressedPath + ".webp", ".compressed"));
|
result.Add((imageCompressedPath + ".webp", ".compressed"));
|
||||||
file.HasCompression = true;
|
file.HasCompression = true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
vipsImage.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F9f_003Fc5bde8be_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fdaa8d9c408cd4b4286bbef7e35f1a42e31c00_003F9f_003Fc5bde8be_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndexAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe38f14ac86274ebb9b366729231d1c1a8838_003F8b_003F2890293d_003FIndexAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndexAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe38f14ac86274ebb9b366729231d1c1a8838_003F8b_003F2890293d_003FIndexAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIntentType_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fbf_003Ffcb84131_003FIntentType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInternalServerError_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01d30b32e2ff422cb80129ca2a441c4242600_003Fb5_003Fc55acdd2_003FInternalServerError_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIServiceCollectionQuartzConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003F67_003Faee36f5b_003FIServiceCollectionQuartzConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AITusStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003Fb1_003F7e861de5_003FITusStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializerOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5703920a18f94462b4354fab05326e6519a200_003F35_003F8536fc49_003FJsonSerializerOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializerOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5703920a18f94462b4354fab05326e6519a200_003F35_003F8536fc49_003FJsonSerializerOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user