Compare commits
2 Commits
02aee07116
...
ee7dc31b20
Author | SHA1 | Date | |
---|---|---|---|
ee7dc31b20 | |||
fb07071603 |
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -15,6 +16,7 @@ public class AccountController(
|
||||
FileService fs,
|
||||
AuthService auth,
|
||||
AccountService accounts,
|
||||
AccountEventService events,
|
||||
MagicSpellService spells
|
||||
) : ControllerBase
|
||||
{
|
||||
@ -25,6 +27,8 @@ public class AccountController(
|
||||
{
|
||||
var account = await db.Accounts
|
||||
.Include(e => e.Profile)
|
||||
.Include(e => e.Profile.Picture)
|
||||
.Include(e => e.Profile.Background)
|
||||
.Where(a => a.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
return account is null ? new NotFoundResult() : account;
|
||||
@ -105,13 +109,15 @@ public class AccountController(
|
||||
[Authorize]
|
||||
[HttpGet("me")]
|
||||
[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();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var account = await db.Accounts
|
||||
.Include(e => e.Profile)
|
||||
.Include(e => e.Profile.Picture)
|
||||
.Include(e => e.Profile.Background)
|
||||
.Where(e => e.Id == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@ -199,6 +205,62 @@ public class AccountController(
|
||||
return profile;
|
||||
}
|
||||
|
||||
public class StatusRequest
|
||||
{
|
||||
public StatusAttitude Attitude { get; set; }
|
||||
public bool IsInvisible { get; set; }
|
||||
public bool IsNotDisturb { get; set; }
|
||||
[MaxLength(1024)] public string? Label { get; set; }
|
||||
public Instant? ClearedAt { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("me/statuses")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Status>> GetCurrentStatus()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var status = await events.GetStatus(currentUser.Id);
|
||||
return Ok(status);
|
||||
}
|
||||
|
||||
[HttpPost("me/statuses")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "accounts.statuses.create")]
|
||||
public async Task<ActionResult<Status>> CreateStatus([FromBody] StatusRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var status = new Status
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
Attitude = request.Attitude,
|
||||
IsInvisible = request.IsInvisible,
|
||||
IsNotDisturb = request.IsNotDisturb,
|
||||
Label = request.Label,
|
||||
ClearedAt = request.ClearedAt
|
||||
};
|
||||
|
||||
return await events.CreateStatus(currentUser, status);
|
||||
}
|
||||
|
||||
[HttpDelete("me/statuses")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeleteStatus()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var status = await db.AccountStatuses
|
||||
.Where(s => s.AccountId == currentUser.Id)
|
||||
.Where(s => s.ClearedAt == null || s.ClearedAt > now)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
if (status is null) return NotFound();
|
||||
|
||||
await events.ClearStatus(currentUser, status);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
||||
{
|
||||
|
79
DysonNetwork.Sphere/Account/AccountEventService.cs
Normal file
79
DysonNetwork.Sphere/Account/AccountEventService.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using DysonNetwork.Sphere.Activity;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NodaTime;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class AccountEventService(AppDatabase db, ActivityService act, WebSocketService ws, IMemoryCache cache)
|
||||
{
|
||||
private const string StatusCacheKey = "account_status_";
|
||||
|
||||
public async Task<Status> GetStatus(long userId)
|
||||
{
|
||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||
if (cache.TryGetValue(cacheKey, out Status? cachedStatus))
|
||||
return cachedStatus!;
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var status = await db.AccountStatuses
|
||||
.Where(e => e.AccountId == userId)
|
||||
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
if (status is not null)
|
||||
{
|
||||
cache.Set(cacheKey, status, TimeSpan.FromMinutes(5));
|
||||
return status;
|
||||
}
|
||||
|
||||
var isOnline = ws.GetAccountIsConnected(userId);
|
||||
if (isOnline)
|
||||
{
|
||||
return new Status
|
||||
{
|
||||
Attitude = StatusAttitude.Neutral,
|
||||
IsOnline = true,
|
||||
Label = "Online",
|
||||
AccountId = userId,
|
||||
};
|
||||
}
|
||||
|
||||
return new Status
|
||||
{
|
||||
Attitude = StatusAttitude.Neutral,
|
||||
IsOnline = false,
|
||||
Label = "Offline",
|
||||
AccountId = userId,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Status> CreateStatus(Account user, Status status)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.AccountStatuses
|
||||
.Where(x => x.AccountId == user.Id && (x.ClearedAt == null || x.ClearedAt > now))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(x => x.ClearedAt, now));
|
||||
|
||||
db.AccountStatuses.Add(status);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await act.CreateActivity(
|
||||
user,
|
||||
"accounts.status",
|
||||
$"account.statuses/{status.Id}",
|
||||
ActivityVisibility.Friends
|
||||
);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task ClearStatus(Account user, Status status)
|
||||
{
|
||||
status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
db.Update(status);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
26
DysonNetwork.Sphere/Account/Status.cs
Normal file
26
DysonNetwork.Sphere/Account/Status.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public enum StatusAttitude
|
||||
{
|
||||
Positive,
|
||||
Negative,
|
||||
Neutral
|
||||
}
|
||||
|
||||
public class Status : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public StatusAttitude Attitude { get; set; }
|
||||
[NotMapped] public bool IsOnline { get; set; }
|
||||
public bool IsInvisible { get; set; }
|
||||
public bool IsNotDisturb { get; set; }
|
||||
[MaxLength(1024)] public string? Label { get; set; }
|
||||
public Instant? ClearedAt { get; set; }
|
||||
|
||||
public long AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
}
|
@ -30,6 +30,7 @@ public class AppDatabase(
|
||||
public DbSet<Account.AccountContact> AccountContacts { get; set; }
|
||||
public DbSet<Account.AccountAuthFactor> AccountAuthFactors { get; set; }
|
||||
public DbSet<Account.Relationship> AccountRelationships { get; set; }
|
||||
public DbSet<Account.Status> AccountStatuses { get; set; }
|
||||
public DbSet<Account.Notification> Notifications { get; set; }
|
||||
public DbSet<Account.NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
||||
|
||||
@ -54,6 +55,7 @@ public class AppDatabase(
|
||||
public DbSet<Chat.ChatRoom> ChatRooms { get; set; }
|
||||
public DbSet<Chat.ChatMember> ChatMembers { 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.MessageReaction> ChatReactions { get; set; }
|
||||
|
||||
@ -84,7 +86,10 @@ public class AppDatabase(
|
||||
PermissionService.NewPermissionNode("group:default", "global", "posts.react", true),
|
||||
PermissionService.NewPermissionNode("group:default", "global", "publishers.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),
|
||||
PermissionService.NewPermissionNode("group:default", "global", "accounts.statuses.create", true)
|
||||
}
|
||||
});
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
@ -205,6 +210,16 @@ public class AppDatabase(
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.RepliedMessageId)
|
||||
.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
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -105,6 +106,7 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller
|
||||
|
||||
[HttpPost("{roomId:long}/messages")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "chat.messages.create")]
|
||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, long roomId)
|
||||
{
|
||||
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
|
||||
{
|
||||
Type = "text",
|
||||
SenderId = member.Id,
|
||||
ChatRoomId = roomId,
|
||||
Nonce = request.Nonce ?? Guid.NewGuid().ToString(),
|
||||
|
@ -38,7 +38,7 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
||||
.Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != sender.AccountId)
|
||||
.Where(m => m.Notify != ChatMemberNotify.None)
|
||||
.Where(m => m.Notify != ChatMemberNotify.Mentions ||
|
||||
(message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id)))
|
||||
(message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id)))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var member in members)
|
||||
@ -101,6 +101,85 @@ public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory)
|
||||
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)
|
||||
{
|
||||
var timestamp = Instant.FromUnixTimeMilliseconds(lastSyncTimestamp);
|
||||
|
@ -9,7 +9,8 @@ namespace DysonNetwork.Sphere.Chat;
|
||||
public class Message : ModelBase
|
||||
{
|
||||
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 List<Guid>? MembersMentioned { get; set; }
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
@ -41,6 +41,11 @@ public class WebSocketService
|
||||
ActiveConnections.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public bool GetAccountIsConnected(long accountId)
|
||||
{
|
||||
return ActiveConnections.Any(c => c.Key.AccountId == accountId);
|
||||
}
|
||||
|
||||
public void SendPacketToAccount(long userId, WebSocketPacket packet)
|
||||
{
|
||||
var connections = ActiveConnections.Where(c => c.Key.AccountId == userId);
|
||||
|
@ -44,6 +44,7 @@
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" 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" />
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
2500
DysonNetwork.Sphere/Migrations/20250506172100_AddAccountStatuses.Designer.cs
generated
Normal file
2500
DysonNetwork.Sphere/Migrations/20250506172100_AddAccountStatuses.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAccountStatuses : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "account_statuses",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
attitude = table.Column<int>(type: "integer", nullable: false),
|
||||
is_invisible = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_not_disturb = table.Column<bool>(type: "boolean", nullable: false),
|
||||
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
cleared_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
account_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_account_statuses", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_account_statuses_accounts_account_id",
|
||||
column: x => x.account_id,
|
||||
principalTable: "accounts",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_account_statuses_account_id",
|
||||
table: "account_statuses",
|
||||
column: "account_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "account_statuses");
|
||||
}
|
||||
}
|
||||
}
|
@ -454,6 +454,59 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.ToTable("account_relationships", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AccountId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<int>("Attitude")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("attitude");
|
||||
|
||||
b.Property<Instant?>("ClearedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("cleared_at");
|
||||
|
||||
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<bool>("IsInvisible")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_invisible");
|
||||
|
||||
b.Property<bool>("IsNotDisturb")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_not_disturb");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("label");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_account_statuses");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_account_statuses_account_id");
|
||||
|
||||
b.ToTable("account_statuses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Activity.Activity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -785,7 +838,6 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnName("chat_room_id");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("content");
|
||||
@ -828,6 +880,11 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("sender_id");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
@ -934,6 +991,53 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -1869,6 +1973,18 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("Related");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_account_statuses_accounts_account_id");
|
||||
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Activity.Activity", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
@ -2038,6 +2154,27 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group")
|
||||
|
@ -131,6 +131,7 @@ builder.Services.AddScoped<WebSocketService>();
|
||||
builder.Services.AddScoped<EmailService>();
|
||||
builder.Services.AddScoped<PermissionService>();
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<AccountEventService>();
|
||||
builder.Services.AddScoped<RelationshipService>();
|
||||
builder.Services.AddScoped<MagicSpellService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
|
@ -141,9 +141,9 @@ public class FileService(
|
||||
List<Task> tasks = [];
|
||||
|
||||
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}");
|
||||
tasks.Add(Task.Run(() => vipsImage.WriteToFile(imagePath + ".webp")));
|
||||
vipsImage.WriteToFile(imagePath + ".webp");
|
||||
result.Add((imagePath + ".webp", string.Empty));
|
||||
|
||||
if (vipsImage.Width * vipsImage.Height >= 1024 * 1024)
|
||||
@ -153,19 +153,12 @@ public class FileService(
|
||||
Path.Join(Path.GetTempPath(), $"{TempFilePrefix}#{file.Id}-compressed");
|
||||
|
||||
// Create and save image within the same synchronous block to avoid disposal issues
|
||||
tasks.Add(Task.Run(() => {
|
||||
using var compressedImage = vipsImage.Resize(scale);
|
||||
compressedImage.WriteToFile(imageCompressedPath + ".webp");
|
||||
vipsImage.Dispose();
|
||||
}));
|
||||
using var compressedImage = vipsImage.Resize(scale);
|
||||
compressedImage.WriteToFile(imageCompressedPath + ".webp");
|
||||
|
||||
result.Add((imageCompressedPath + ".webp", ".compressed"));
|
||||
file.HasCompression = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
vipsImage.Dispose();
|
||||
}
|
||||
|
||||
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_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_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_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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user