✨ Implement realtime chat
This commit is contained in:
parent
59bc9edd4b
commit
9e7ba820c4
@ -27,8 +27,7 @@ public enum NotificationPushProvider
|
|||||||
Google
|
Google
|
||||||
}
|
}
|
||||||
|
|
||||||
[Index(nameof(DeviceId), IsUnique = true)]
|
[Index(nameof(DeviceToken), nameof(DeviceId), IsUnique = true)]
|
||||||
[Index(nameof(DeviceToken), IsUnique = true)]
|
|
||||||
public class NotificationPushSubscription : ModelBase
|
public class NotificationPushSubscription : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -6,7 +7,12 @@ using NodaTime;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory scopeFactory)
|
public class ChatService(
|
||||||
|
AppDatabase db,
|
||||||
|
FileService fs,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IRealtimeService realtime
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
|
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
|
||||||
{
|
{
|
||||||
@ -147,13 +153,33 @@ public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory sc
|
|||||||
{
|
{
|
||||||
RoomId = room.Id,
|
RoomId = room.Id,
|
||||||
SenderId = sender.Id,
|
SenderId = sender.Id,
|
||||||
|
ProviderName = realtime.ProviderName
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sessionConfig = await realtime.CreateSessionAsync(room.Id, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "room_id", room.Id },
|
||||||
|
{ "user_id", sender.AccountId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store session details
|
||||||
|
call.SessionId = sessionConfig.SessionId;
|
||||||
|
call.UpstreamConfig = sessionConfig.Parameters;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the exception but continue with call creation
|
||||||
|
throw new InvalidOperationException($"Failed to create {realtime.ProviderName} session: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
db.ChatRealtimeCall.Add(call);
|
db.ChatRealtimeCall.Add(call);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await SendMessageAsync(new Message
|
await SendMessageAsync(new Message
|
||||||
{
|
{
|
||||||
Type = "realtime.start",
|
Type = "call.start",
|
||||||
ChatRoomId = room.Id,
|
ChatRoomId = room.Id,
|
||||||
SenderId = sender.Id,
|
SenderId = sender.Id,
|
||||||
Meta = new Dictionary<string, object>
|
Meta = new Dictionary<string, object>
|
||||||
@ -170,13 +196,33 @@ public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory sc
|
|||||||
var call = await GetCallOngoingAsync(roomId);
|
var call = await GetCallOngoingAsync(roomId);
|
||||||
if (call is null) throw new InvalidOperationException("No ongoing call was not found.");
|
if (call is null) throw new InvalidOperationException("No ongoing call was not found.");
|
||||||
|
|
||||||
|
// End the realtime session if it exists
|
||||||
|
if (!string.IsNullOrEmpty(call.SessionId) && !string.IsNullOrEmpty(call.ProviderName))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = new RealtimeSessionConfig
|
||||||
|
{
|
||||||
|
SessionId = call.SessionId,
|
||||||
|
Parameters = call.UpstreamConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
await realtime.EndSessionAsync(call.SessionId, config);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the exception but continue with call ending
|
||||||
|
throw new InvalidOperationException($"Failed to end {call.ProviderName} session: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
call.EndedAt = SystemClock.Instance.GetCurrentInstant();
|
call.EndedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
db.ChatRealtimeCall.Update(call);
|
db.ChatRealtimeCall.Update(call);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await SendMessageAsync(new Message
|
await SendMessageAsync(new Message
|
||||||
{
|
{
|
||||||
Type = "realtime.ended",
|
Type = "call.ended",
|
||||||
ChatRoomId = call.RoomId,
|
ChatRoomId = call.RoomId,
|
||||||
SenderId = call.SenderId,
|
SenderId = call.SenderId,
|
||||||
Meta = new Dictionary<string, object>
|
Meta = new Dictionary<string, object>
|
||||||
|
56
DysonNetwork.Sphere/Chat/Realtime/IRealtimeService.cs
Normal file
56
DysonNetwork.Sphere/Chat/Realtime/IRealtimeService.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for real-time communication services (like Cloudflare, Agora, Twilio, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public interface IRealtimeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service provider name
|
||||||
|
/// </summary>
|
||||||
|
string ProviderName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new real-time session
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roomId">The room identifier</param>
|
||||||
|
/// <param name="metadata">Additional metadata to associate with the session</param>
|
||||||
|
/// <returns>Session configuration data</returns>
|
||||||
|
Task<RealtimeSessionConfig> CreateSessionAsync(Guid roomId, Dictionary<string, object> metadata);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends an existing real-time session
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId">The session identifier</param>
|
||||||
|
/// <param name="config">The session configuration</param>
|
||||||
|
Task EndSessionAsync(string sessionId, RealtimeSessionConfig config);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a token for user to join the session
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">The user identifier</param>
|
||||||
|
/// <param name="sessionId">The session identifier</param>
|
||||||
|
/// <param name="isAdmin">The user is the admin of session</param>
|
||||||
|
/// <returns>User-specific token for the session</returns>
|
||||||
|
string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Common configuration object for real-time sessions
|
||||||
|
/// </summary>
|
||||||
|
public class RealtimeSessionConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service-specific session identifier
|
||||||
|
/// </summary>
|
||||||
|
public string SessionId { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional provider-specific configuration parameters
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Parameters { get; set; } = new();
|
||||||
|
}
|
111
DysonNetwork.Sphere/Chat/Realtime/LivekitService.cs
Normal file
111
DysonNetwork.Sphere/Chat/Realtime/LivekitService.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
using Livekit.Server.Sdk.Dotnet;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LiveKit implementation of the real-time communication service
|
||||||
|
/// </summary>
|
||||||
|
public class LivekitRealtimeService : IRealtimeService
|
||||||
|
{
|
||||||
|
private readonly ILogger<LivekitRealtimeService> _logger;
|
||||||
|
private readonly RoomServiceClient _roomService;
|
||||||
|
private readonly AccessToken _accessToken;
|
||||||
|
|
||||||
|
public LivekitRealtimeService(IConfiguration configuration, ILogger<LivekitRealtimeService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Get LiveKit configuration from appsettings
|
||||||
|
var host = configuration["RealtimeChat:Endpoint"] ??
|
||||||
|
throw new ArgumentNullException("Endpoint configuration is required");
|
||||||
|
var apiKey = configuration["RealtimeChat:ApiKey"] ??
|
||||||
|
throw new ArgumentNullException("ApiKey configuration is required");
|
||||||
|
var apiSecret = configuration["RealtimeChat:ApiSecret"] ??
|
||||||
|
throw new ArgumentNullException("ApiSecret configuration is required");
|
||||||
|
|
||||||
|
_roomService = new RoomServiceClient(host, apiKey, apiSecret);
|
||||||
|
_accessToken = new AccessToken(apiKey, apiSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ProviderName => "LiveKit";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<RealtimeSessionConfig> CreateSessionAsync(Guid roomId, Dictionary<string, object> metadata)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var roomName = $"Call_{roomId.ToString().Replace("-", "")}";
|
||||||
|
|
||||||
|
// Convert metadata to a string dictionary for LiveKit
|
||||||
|
var roomMetadata = new Dictionary<string, string>();
|
||||||
|
foreach (var item in metadata)
|
||||||
|
{
|
||||||
|
roomMetadata[item.Key] = item.Value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create room in LiveKit
|
||||||
|
var room = await _roomService.CreateRoom(new CreateRoomRequest
|
||||||
|
{
|
||||||
|
Name = roomName,
|
||||||
|
EmptyTimeout = 300, // 5 minutes
|
||||||
|
Metadata = System.Text.Json.JsonSerializer.Serialize(roomMetadata)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return session config
|
||||||
|
return new RealtimeSessionConfig
|
||||||
|
{
|
||||||
|
SessionId = room.Name,
|
||||||
|
Parameters = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "sid", room.Sid },
|
||||||
|
{ "emptyTimeout", room.EmptyTimeout },
|
||||||
|
{ "creationTime", room.CreationTime }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create LiveKit room for roomId: {RoomId}", roomId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task EndSessionAsync(string sessionId, RealtimeSessionConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Delete the room in LiveKit
|
||||||
|
await _roomService.DeleteRoom(new DeleteRoomRequest
|
||||||
|
{
|
||||||
|
Room = sessionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to end LiveKit session: {SessionId}", sessionId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||||
|
{
|
||||||
|
var token = _accessToken.WithIdentity(account.Name)
|
||||||
|
.WithName(account.Nick)
|
||||||
|
.WithGrants(new VideoGrants
|
||||||
|
{
|
||||||
|
RoomJoin = true,
|
||||||
|
CanPublish = true,
|
||||||
|
CanPublishData = true,
|
||||||
|
CanSubscribe = true,
|
||||||
|
CanSubscribeMetrics = true,
|
||||||
|
RoomAdmin = isAdmin,
|
||||||
|
Room = sessionId
|
||||||
|
})
|
||||||
|
.WithAttributes(new Dictionary<string, string> { { "account_id", account.Id.ToString() } })
|
||||||
|
.WithTtl(TimeSpan.FromHours(1));
|
||||||
|
return token.ToJwt();
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
@ -5,11 +11,40 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
public class RealtimeCall : ModelBase
|
public class RealtimeCall : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public string? Title { get; set; }
|
|
||||||
public Instant? EndedAt { get; set; }
|
public Instant? EndedAt { get; set; }
|
||||||
|
|
||||||
public Guid SenderId { get; set; }
|
public Guid SenderId { get; set; }
|
||||||
public ChatMember Sender { get; set; } = null!;
|
public ChatMember Sender { get; set; } = null!;
|
||||||
public Guid RoomId { get; set; }
|
public Guid RoomId { get; set; }
|
||||||
public ChatRoom Room { get; set; } = null!;
|
public ChatRoom Room { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider name (e.g., "cloudflare", "agora", "twilio")
|
||||||
|
/// </summary>
|
||||||
|
public string? ProviderName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service provider's session identifier
|
||||||
|
/// </summary>
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSONB column containing provider-specific configuration
|
||||||
|
/// </summary>
|
||||||
|
[Column(name: "upstream", TypeName = "jsonb")]
|
||||||
|
public string? UpstreamConfigJson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialized upstream configuration
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public Dictionary<string, object> UpstreamConfig
|
||||||
|
{
|
||||||
|
get => string.IsNullOrEmpty(UpstreamConfigJson)
|
||||||
|
? new Dictionary<string, object>()
|
||||||
|
: JsonSerializer.Deserialize<Dictionary<string, object>>(UpstreamConfigJson) ?? new Dictionary<string, object>();
|
||||||
|
set => UpstreamConfigJson = value.Count > 0
|
||||||
|
? JsonSerializer.Serialize(value)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -6,16 +7,60 @@ namespace DysonNetwork.Sphere.Chat;
|
|||||||
|
|
||||||
public class RealtimeChatConfiguration
|
public class RealtimeChatConfiguration
|
||||||
{
|
{
|
||||||
public string Provider { get; set; } = null!;
|
public string Endpoint { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/chat/realtime")]
|
[Route("/chat/realtime")]
|
||||||
public class RealtimeCallController(IConfiguration configuration, AppDatabase db, ChatService cs) : ControllerBase
|
public class RealtimeCallController(IConfiguration configuration, AppDatabase db, ChatService cs, IRealtimeService realtime) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly RealtimeChatConfiguration _config =
|
private readonly RealtimeChatConfiguration _config =
|
||||||
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
||||||
|
|
||||||
|
[HttpGet("{roomId:guid}/join")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> JoinCall(Guid roomId)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
// Check if the user is a member of the chat room
|
||||||
|
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 member to join a call.");
|
||||||
|
|
||||||
|
// Get ongoing call
|
||||||
|
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||||
|
if (ongoingCall is null)
|
||||||
|
return NotFound("There is no ongoing call in this room.");
|
||||||
|
|
||||||
|
// Check if session ID exists
|
||||||
|
if (string.IsNullOrEmpty(ongoingCall.SessionId))
|
||||||
|
return BadRequest("Call session is not properly configured.");
|
||||||
|
|
||||||
|
var isAdmin = member.Role >= ChatMemberRole.Moderator;
|
||||||
|
var userToken = realtime.GetUserToken(currentUser, ongoingCall.SessionId, isAdmin);
|
||||||
|
|
||||||
|
// Get LiveKit endpoint from configuration
|
||||||
|
string endpoint = _config.Endpoint ??
|
||||||
|
throw new InvalidOperationException("LiveKit endpoint configuration is missing");
|
||||||
|
|
||||||
|
// Create response model
|
||||||
|
var response = new JoinCallResponse
|
||||||
|
{
|
||||||
|
Provider = realtime.ProviderName,
|
||||||
|
Endpoint = endpoint,
|
||||||
|
Token = userToken,
|
||||||
|
CallId = ongoingCall.Id,
|
||||||
|
RoomName = ongoingCall.SessionId,
|
||||||
|
IsAdmin = isAdmin
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{roomId:guid}")]
|
[HttpPost("{roomId:guid}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> StartCall(Guid roomId)
|
public async Task<IActionResult> StartCall(Guid roomId)
|
||||||
@ -58,3 +103,37 @@ public class RealtimeCallController(IConfiguration configuration, AppDatabase db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Response model for joining a call
|
||||||
|
public class JoinCallResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The service provider name (e.g., "LiveKit")
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The LiveKit server endpoint
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication token for the user
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The call identifier
|
||||||
|
/// </summary>
|
||||||
|
public Guid CallId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The room name in LiveKit
|
||||||
|
/// </summary>
|
||||||
|
public string RoomName { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the user is the admin of the call
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
|
||||||
<PackageReference Include="FFMpegCore" Version="5.2.0"/>
|
<PackageReference Include="FFMpegCore" Version="5.2.0"/>
|
||||||
|
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
||||||
<PackageReference Include="MailKit" Version="4.11.0"/>
|
<PackageReference Include="MailKit" Version="4.11.0"/>
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0"/>
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4"/>
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4"/>
|
||||||
|
3400
DysonNetwork.Sphere/Migrations/20250524215045_UpdateRealtimeChat.Designer.cs
generated
Normal file
3400
DysonNetwork.Sphere/Migrations/20250524215045_UpdateRealtimeChat.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateRealtimeChat : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_id",
|
||||||
|
table: "notification_push_subscriptions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_token",
|
||||||
|
table: "notification_push_subscriptions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "title",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
newName: "session_id");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "provider_name",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "upstream",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_token_device_id",
|
||||||
|
table: "notification_push_subscriptions",
|
||||||
|
columns: new[] { "device_token", "device_id" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_token_device_id",
|
||||||
|
table: "notification_push_subscriptions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "provider_name",
|
||||||
|
table: "chat_realtime_call");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "upstream",
|
||||||
|
table: "chat_realtime_call");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "session_id",
|
||||||
|
table: "chat_realtime_call",
|
||||||
|
newName: "title");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_id",
|
||||||
|
table: "notification_push_subscriptions",
|
||||||
|
column: "device_id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_notification_push_subscriptions_device_token",
|
||||||
|
table: "notification_push_subscriptions",
|
||||||
|
column: "device_token",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -515,13 +515,9 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.HasIndex("AccountId")
|
b.HasIndex("AccountId")
|
||||||
.HasDatabaseName("ix_notification_push_subscriptions_account_id");
|
.HasDatabaseName("ix_notification_push_subscriptions_account_id");
|
||||||
|
|
||||||
b.HasIndex("DeviceId")
|
b.HasIndex("DeviceToken", "DeviceId")
|
||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("ix_notification_push_subscriptions_device_id");
|
.HasDatabaseName("ix_notification_push_subscriptions_device_token_device_id");
|
||||||
|
|
||||||
b.HasIndex("DeviceToken")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("ix_notification_push_subscriptions_device_token");
|
|
||||||
|
|
||||||
b.ToTable("notification_push_subscriptions", (string)null);
|
b.ToTable("notification_push_subscriptions", (string)null);
|
||||||
});
|
});
|
||||||
@ -1202,6 +1198,10 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("ended_at");
|
.HasColumnName("ended_at");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("provider_name");
|
||||||
|
|
||||||
b.Property<Guid>("RoomId")
|
b.Property<Guid>("RoomId")
|
||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("room_id");
|
.HasColumnName("room_id");
|
||||||
@ -1210,14 +1210,18 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("sender_id");
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("SessionId")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("title");
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("UpstreamConfigJson")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("upstream");
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_chat_realtime_call");
|
.HasName("pk_chat_realtime_call");
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using DysonNetwork.Sphere.Email;
|
|||||||
using DysonNetwork.Sphere.Activity;
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
using DysonNetwork.Sphere.Connection.Handlers;
|
using DysonNetwork.Sphere.Connection.Handlers;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
@ -178,6 +179,7 @@ builder.Services.AddScoped<ChatService>();
|
|||||||
builder.Services.AddScoped<StickerService>();
|
builder.Services.AddScoped<StickerService>();
|
||||||
builder.Services.AddScoped<WalletService>();
|
builder.Services.AddScoped<WalletService>();
|
||||||
builder.Services.AddScoped<PaymentService>();
|
builder.Services.AddScoped<PaymentService>();
|
||||||
|
builder.Services.AddScoped<IRealtimeService, LivekitRealtimeService>();
|
||||||
|
|
||||||
// Timed task
|
// Timed task
|
||||||
|
|
||||||
|
@ -75,7 +75,9 @@
|
|||||||
"SubjectPrefix": "Solar Network"
|
"SubjectPrefix": "Solar Network"
|
||||||
},
|
},
|
||||||
"RealtimeChat": {
|
"RealtimeChat": {
|
||||||
"Provider": "cloudflare"
|
"Endpoint": "",
|
||||||
|
"ApiKey": "",
|
||||||
|
"ApiSecret": ""
|
||||||
},
|
},
|
||||||
"GeoIp": {
|
"GeoIp": {
|
||||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtSecurityTokenHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F477051138f1f40de9077b7b1cdc55c6215fb0_003Ff5_003Fd716e016_003FJwtSecurityTokenHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtSecurityTokenHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F477051138f1f40de9077b7b1cdc55c6215fb0_003Ff5_003Fd716e016_003FJwtSecurityTokenHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKestrelServerLimits_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e2e5dfcafad4407b569dd5df56a2fbf274e00_003Fa4_003F39445f62_003FKestrelServerLimits_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKestrelServerLimits_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1e2e5dfcafad4407b569dd5df56a2fbf274e00_003Fa4_003F39445f62_003FKestrelServerLimits_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKnownResamplers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003Fb3_003Fcdb3e080_003FKnownResamplers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKnownResamplers_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003Fb3_003Fcdb3e080_003FKnownResamplers_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALivekitRoom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F82666257d5ad47354add7af860f66dd85df55ec93e92e8a45891b9bff7bf80ac_003FLivekitRoom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMailboxAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e03e47c46b7469f97abc40667cbcf9b133000_003Fa6_003F83324248_003FMailboxAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMailboxAddress_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e03e47c46b7469f97abc40667cbcf9b133000_003Fa6_003F83324248_003FMailboxAddress_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMediaAnalysis_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffef366b36a224d469ff150d30f9a866d23c00_003Fd7_003F5c138865_003FMediaAnalysis_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMicrosoftDependencyInjectionJobFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1edbd6e24d7b430fabce72177269baa19200_003Fa8_003F91b091de_003FMicrosoftDependencyInjectionJobFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@ -81,9 +82,10 @@
|
|||||||
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FAccountEventResource_002Ezh_002DCN/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FAccountEventResource_002Ezh_002DCN/@EntryIndexedValue">False</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FAccountEventResource_002Ezh_002DCN/@EntryIndexRemoved">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FAccountEventResource_002Ezh_002DCN/@EntryIndexRemoved">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FAccountEventResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FAccountEventResource/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FAccountEventResource/@EntryIndexRemoved">True</s:Boolean>
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexRemoved">True</s:Boolean>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -96,14 +98,15 @@
|
|||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexedValue">False</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmails_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmails_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexedValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexRemoved">True</s:Boolean>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FSharedResource/@EntryIndexedValue">False</s:Boolean>
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FSharedResource/@EntryIndexRemoved">True</s:Boolean>
|
|
||||||
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FSharedResource/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user