🐛 Fix compile errors
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
@@ -21,14 +22,10 @@ public class Team : ModelBase, IIdentifiedResource
|
||||
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
[MaxLength(4096)] public string? Bio { get; set; }
|
||||
|
||||
// Outdated fields, for backward compability
|
||||
[MaxLength(32)] public string? PictureId { get; set; }
|
||||
[MaxLength(32)] public string? BackgroundId { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<TeamMember> Members { get; set; } = new List<TeamMember>();
|
||||
[JsonIgnore] public ICollection<TeamFeature> Features { get; set; } = new List<TeamFeature>();
|
||||
|
@@ -1,308 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
public class CloudflareRealtimeService : IRealtimeService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IConfiguration _configuration;
|
||||
private RSA? _publicKey;
|
||||
|
||||
public CloudflareRealtimeService(
|
||||
AppDatabase db,
|
||||
HttpClient httpClient,
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
_db = db;
|
||||
_httpClient = httpClient;
|
||||
_configuration = configuration;
|
||||
var apiKey = _configuration["Realtime:Cloudflare:ApiKey"];
|
||||
var apiSecret = _configuration["Realtime:Cloudflare:ApiSecret"]!;
|
||||
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:{apiSecret}"));
|
||||
_httpClient.BaseAddress = new Uri("https://rtk.realtime.cloudflare.com/v2/");
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
|
||||
public string ProviderName => "Cloudflare";
|
||||
|
||||
public async Task<RealtimeSessionConfig> CreateSessionAsync(Guid roomId, Dictionary<string, object> metadata)
|
||||
{
|
||||
var roomName = $"Room Call #{roomId.ToString().Replace("-", "")}";
|
||||
var requestBody = new
|
||||
{
|
||||
title = roomName,
|
||||
preferred_region = _configuration["Realtime:Cloudflare:PreferredRegion"]
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync("meetings", content);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
var meetingResponse = JsonSerializer.Deserialize<CfMeetingResponse>(responseContent);
|
||||
if (meetingResponse is null) throw new Exception("Failed to create meeting with cloudflare");
|
||||
|
||||
return new RealtimeSessionConfig
|
||||
{
|
||||
SessionId = meetingResponse.Data.Id,
|
||||
Parameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "meetingId", meetingResponse.Data.Id }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task EndSessionAsync(string sessionId, RealtimeSessionConfig config)
|
||||
{
|
||||
var requestBody = new
|
||||
{
|
||||
status = "INACTIVE"
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PatchAsync($"sessions/{sessionId}", content);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
return GetUserTokenAsync(account, sessionId, isAdmin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First try to get the participant by their custom ID
|
||||
var participantCheckResponse = await _httpClient
|
||||
.GetAsync($"meetings/{sessionId}/participants/{account.Id}");
|
||||
|
||||
if (participantCheckResponse.IsSuccessStatusCode)
|
||||
{
|
||||
// Participant exists, get a new token
|
||||
var tokenResponse = await _httpClient
|
||||
.PostAsync($"meetings/{sessionId}/participants/{account.Id}/token", null);
|
||||
tokenResponse.EnsureSuccessStatusCode();
|
||||
var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||
var tokenData = JsonSerializer.Deserialize<CfResponse<CfTokenResponse>>(tokenContent);
|
||||
if (tokenData == null || !tokenData.Success)
|
||||
{
|
||||
throw new Exception("Failed to get participant token");
|
||||
}
|
||||
|
||||
return tokenData.Data?.Token ?? throw new Exception("Token is null");
|
||||
}
|
||||
|
||||
// Participant doesn't exist, create a new one
|
||||
var baseUrl = _configuration["BaseUrl"];
|
||||
var requestBody = new
|
||||
{
|
||||
name = "@" + account.Name,
|
||||
picture = account.Profile.Picture is not null
|
||||
? $"{baseUrl}/api/files/{account.Profile.Picture.Id}"
|
||||
: null,
|
||||
preset_name = isAdmin ? "group_call_host" : "group_call_participant",
|
||||
custom_participant_id = account.Id.ToString()
|
||||
};
|
||||
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(requestBody),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
var createResponse = await _httpClient.PostAsync($"meetings/{sessionId}/participants", content);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var responseContent = await createResponse.Content.ReadAsStringAsync();
|
||||
var participantData = JsonSerializer.Deserialize<CfResponse<CfParticipantResponse>>(responseContent);
|
||||
if (participantData == null || !participantData.Success)
|
||||
{
|
||||
throw new Exception("Failed to create participant");
|
||||
}
|
||||
|
||||
return participantData.Data?.Token ?? throw new Exception("Token is null");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error or handle it appropriately
|
||||
throw new Exception($"Failed to get or create participant: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReceiveWebhook(string body, string authHeader)
|
||||
{
|
||||
if (string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
throw new ArgumentException("Auth header is missing");
|
||||
}
|
||||
|
||||
if (_publicKey == null)
|
||||
{
|
||||
await GetPublicKeyAsync();
|
||||
}
|
||||
|
||||
var signature = authHeader.Replace("Signature ", "");
|
||||
var bodyBytes = Encoding.UTF8.GetBytes(body);
|
||||
var signatureBytes = Convert.FromBase64String(signature);
|
||||
|
||||
if (!(_publicKey?.VerifyData(bodyBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ??
|
||||
false))
|
||||
{
|
||||
throw new SecurityTokenException("Webhook signature validation failed");
|
||||
}
|
||||
|
||||
// Process the webhook event
|
||||
var evt = JsonSerializer.Deserialize<CfWebhookEvent>(body);
|
||||
if (evt is null) return;
|
||||
|
||||
switch (evt.Type)
|
||||
{
|
||||
case "meeting.ended":
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await _db.ChatRealtimeCall
|
||||
.Where(c => c.SessionId == evt.Event.Meeting.Id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(p => p.EndedAt, now)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class WebhooksConfig
|
||||
{
|
||||
[JsonPropertyName("keys")] public List<WebhookKey> Keys { get; set; } = new List<WebhookKey>();
|
||||
}
|
||||
|
||||
private class WebhookKey
|
||||
{
|
||||
[JsonPropertyName("publicKeyPem")] public string PublicKeyPem { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private async Task GetPublicKeyAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://rtk.realtime.cloudflare.com/.well-known/webhooks.json");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var webhooksConfig = JsonSerializer.Deserialize<WebhooksConfig>(content);
|
||||
var publicKeyPem = webhooksConfig?.Keys.FirstOrDefault()?.PublicKeyPem;
|
||||
|
||||
if (string.IsNullOrEmpty(publicKeyPem))
|
||||
{
|
||||
throw new InvalidOperationException("Public key not found in webhooks configuration.");
|
||||
}
|
||||
|
||||
_publicKey = RSA.Create();
|
||||
_publicKey.ImportFromPem(publicKeyPem);
|
||||
}
|
||||
|
||||
private class CfMeetingResponse
|
||||
{
|
||||
[JsonPropertyName("data")] public CfMeetingData Data { get; set; } = new();
|
||||
}
|
||||
|
||||
private class CfMeetingData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
|
||||
[JsonPropertyName("roomName")] public string RoomName { get; set; } = string.Empty;
|
||||
[JsonPropertyName("title")] public string Title { get; set; } = string.Empty;
|
||||
[JsonPropertyName("status")] public string Status { get; set; } = string.Empty;
|
||||
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
||||
[JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
private class CfParticipant
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string CustomParticipantId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfResponse<T>
|
||||
{
|
||||
[JsonPropertyName("success")] public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("data")] public T? Data { get; set; }
|
||||
}
|
||||
|
||||
public class CfTokenResponse
|
||||
{
|
||||
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfParticipantResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("customUserId")] public string CustomUserId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("presetName")] public string PresetName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("isActive")] public bool IsActive { get; set; }
|
||||
|
||||
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CfWebhookEvent
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("webhookId")] public string WebhookId { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("event")] public EventData Event { get; set; }
|
||||
}
|
||||
|
||||
public class EventData
|
||||
{
|
||||
[JsonPropertyName("meeting")] public MeetingData Meeting { get; set; }
|
||||
|
||||
[JsonPropertyName("participant")] public ParticipantData Participant { get; set; }
|
||||
}
|
||||
|
||||
public class MeetingData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("roomName")] public string RoomName { get; set; }
|
||||
|
||||
[JsonPropertyName("title")] public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("status")] public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updatedAt")] public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ParticipantData
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")] public string UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("customParticipantId")]
|
||||
public string CustomParticipantId { get; set; }
|
||||
|
||||
[JsonPropertyName("presetName")] public string PresetName { get; set; }
|
||||
|
||||
[JsonPropertyName("joinedAt")] public DateTime JoinedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("leftAt")] public DateTime? LeftAt { get; set; }
|
||||
}
|
||||
}
|
@@ -31,7 +31,7 @@ public interface IRealtimeService
|
||||
Task EndSessionAsync(string sessionId, RealtimeSessionConfig config);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token for user to join the session (synchronous version for backward compatibility)
|
||||
/// Gets a token for user to join the session
|
||||
/// </summary>
|
||||
/// <param name="account">The user identifier</param>
|
||||
/// <param name="sessionId">The session identifier</param>
|
||||
@@ -39,15 +39,6 @@ public interface IRealtimeService
|
||||
/// <returns>User-specific token for the session</returns>
|
||||
string GetUserToken(Account account, string sessionId, bool isAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token for user to join the session asynchronously
|
||||
/// </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>Task that resolves to the user-specific token for the session</returns>
|
||||
Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Processes incoming webhook requests from the realtime service provider
|
||||
/// </summary>
|
||||
|
@@ -1,11 +1,8 @@
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Livekit.Server.Sdk.Dotnet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
@@ -111,7 +108,7 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
public string GetUserToken(Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
var token = _accessToken.WithIdentity(account.Name)
|
||||
.WithName(account.Nick)
|
||||
|
@@ -1,91 +0,0 @@
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
public class ParticipantInfoItem
|
||||
{
|
||||
public string Identity { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public Guid? AccountId { get; set; }
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
public DateTime JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
public class RealtimeStatusService(AppDatabase db, WebSocketService ws, ILogger<RealtimeStatusService> logger)
|
||||
{
|
||||
// Broadcast participant update to all participants in a room
|
||||
public async Task BroadcastParticipantUpdate(string roomName, List<ParticipantInfoItem> participantsInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the room ID from the session name
|
||||
var roomInfo = await db.ChatRealtimeCall
|
||||
.Where(c => c.SessionId == roomName && c.EndedAt == null)
|
||||
.Select(c => new { c.RoomId, c.Id })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (roomInfo == null)
|
||||
{
|
||||
logger.LogWarning("Could not find room info for session: {SessionName}", roomName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all room members who should receive this update
|
||||
var roomMembers = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && m.LeaveAt == null)
|
||||
.Select(m => m.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
// Get member profiles for participants who have account IDs
|
||||
var accountIds = participantsInfo
|
||||
.Where(p => p.AccountId.HasValue)
|
||||
.Select(p => p.AccountId!.Value)
|
||||
.ToList();
|
||||
|
||||
var memberProfiles = new Dictionary<Guid, ChatMember>();
|
||||
if (accountIds.Count != 0)
|
||||
{
|
||||
memberProfiles = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomInfo.RoomId && accountIds.Contains(m.AccountId))
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
.ToDictionaryAsync(m => m.AccountId, m => m);
|
||||
}
|
||||
|
||||
// Convert to CallParticipant objects
|
||||
var participants = participantsInfo.Select(p => new CallParticipant
|
||||
{
|
||||
Identity = p.Identity,
|
||||
Name = p.Name,
|
||||
AccountId = p.AccountId,
|
||||
JoinedAt = p.JoinedAt,
|
||||
Profile = p.AccountId.HasValue && memberProfiles.TryGetValue(p.AccountId.Value, out var profile)
|
||||
? profile
|
||||
: null
|
||||
}).ToList();
|
||||
|
||||
// Create the update packet with CallParticipant objects
|
||||
var updatePacket = new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.CallParticipantsUpdate,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "room_id", roomInfo.RoomId },
|
||||
{ "call_id", roomInfo.Id },
|
||||
{ "participants", participants }
|
||||
}
|
||||
};
|
||||
|
||||
// Send the update to all members
|
||||
foreach (var accountId in roomMembers)
|
||||
{
|
||||
ws.SendPacketToAccount(accountId, updatePacket);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error broadcasting participant update for room {RoomName}", roomName);
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,6 +14,9 @@ using NodaTime.Serialization.SystemTextJson;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||
using DysonNetwork.Pass.Wallet;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
@@ -164,7 +167,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<WalletService>();
|
||||
services.AddScoped<SubscriptionService>();
|
||||
services.AddScoped<PaymentService>();
|
||||
services.AddScoped<IRealtimeService, LivekitRealtimeService>();
|
||||
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
||||
services.AddScoped<WebReaderService>();
|
||||
services.AddScoped<WebFeedService>();
|
||||
services.AddScoped<DiscoveryService>();
|
||||
|
Reference in New Issue
Block a user