♻️ Move the chat part of the Sphere service to the Messager service
This commit is contained in:
@@ -14,6 +14,12 @@ public class AppDatabase(
|
|||||||
IConfiguration configuration
|
IConfiguration configuration
|
||||||
) : DbContext(options)
|
) : DbContext(options)
|
||||||
{
|
{
|
||||||
|
public DbSet<SnChatRoom> ChatRooms { get; set; } = null!;
|
||||||
|
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
||||||
|
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
||||||
|
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
||||||
|
public DbSet<SnChatReaction> ChatReactions { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder.UseNpgsql(
|
optionsBuilder.UseNpgsql(
|
||||||
@@ -31,6 +37,36 @@ public class AppDatabase(
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SnChatMember>()
|
||||||
|
.HasKey(pm => new { pm.Id });
|
||||||
|
modelBuilder.Entity<SnChatMember>()
|
||||||
|
.HasAlternateKey(pm => new { pm.ChatRoomId, pm.AccountId });
|
||||||
|
modelBuilder.Entity<SnChatMember>()
|
||||||
|
.HasOne(pm => pm.ChatRoom)
|
||||||
|
.WithMany(p => p.Members)
|
||||||
|
.HasForeignKey(pm => pm.ChatRoomId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<SnChatMessage>()
|
||||||
|
.HasOne(m => m.ForwardedMessage)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.ForwardedMessageId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<SnChatMessage>()
|
||||||
|
.HasOne(m => m.RepliedMessage)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.RepliedMessageId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
modelBuilder.Entity<SnRealtimeCall>()
|
||||||
|
.HasOne(m => m.Room)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.RoomId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<SnRealtimeCall>()
|
||||||
|
.HasOne(m => m.Sender)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(m => m.SenderId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.ApplySoftDeleteFilters();
|
modelBuilder.ApplySoftDeleteFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Autocompletion;
|
using DysonNetwork.Messager.Poll;
|
||||||
using DysonNetwork.Sphere.Poll;
|
using DysonNetwork.Messager.Wallet;
|
||||||
using DysonNetwork.Sphere.Wallet;
|
using DysonNetwork.Messager.WebReader;
|
||||||
using DysonNetwork.Sphere.WebReader;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/chat")]
|
[Route("/api/chat")]
|
||||||
@@ -25,9 +23,8 @@ public partial class ChatController(
|
|||||||
ChatRoomService crs,
|
ChatRoomService crs,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
AutocompletionService aus,
|
|
||||||
PaymentService.PaymentServiceClient paymentClient,
|
PaymentService.PaymentServiceClient paymentClient,
|
||||||
PollService polls
|
PollService.PollServiceClient pollClient
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
public class MarkMessageReadRequest
|
public class MarkMessageReadRequest
|
||||||
@@ -293,12 +290,16 @@ public partial class ChatController(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||||
// Poll validation is handled by the MakePollEmbed method
|
// Poll validation is handled by gRPC call
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest("The specified poll does not exist.");
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid poll ID.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +330,8 @@ public partial class ChatController(
|
|||||||
// Add embed for poll if provided
|
// Add embed for poll if provided
|
||||||
if (request.PollId.HasValue)
|
if (request.PollId.HasValue)
|
||||||
{
|
{
|
||||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||||
|
var pollEmbed = new PollEmbed { Id = Guid.Parse(pollResponse.Id) };
|
||||||
message.Meta ??= new Dictionary<string, object>();
|
message.Meta ??= new Dictionary<string, object>();
|
||||||
if (
|
if (
|
||||||
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
||||||
@@ -472,7 +474,8 @@ public partial class ChatController(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pollEmbed = await polls.MakePollEmbed(request.PollId.Value);
|
var pollResponse = await pollClient.GetPollAsync(new GetPollRequest { Id = request.PollId.Value.ToString() });
|
||||||
|
var pollEmbed = new PollEmbed { Id = Guid.Parse(pollResponse.Id) };
|
||||||
message.Meta ??= new Dictionary<string, object>();
|
message.Meta ??= new Dictionary<string, object>();
|
||||||
if (
|
if (
|
||||||
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
!message.Meta.TryGetValue("embeds", out var existingEmbeds)
|
||||||
@@ -487,9 +490,13 @@ public partial class ChatController(
|
|||||||
embeds.Add(EmbeddableBase.ToDictionary(pollEmbed));
|
embeds.Add(EmbeddableBase.ToDictionary(pollEmbed));
|
||||||
message.Meta["embeds"] = embeds;
|
message.Meta["embeds"] = embeds;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||||
{
|
{
|
||||||
return BadRequest(ex.Message);
|
return BadRequest("The specified poll does not exist.");
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid poll ID.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -565,21 +572,4 @@ public partial class ChatController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[SwaggerIgnore]
|
|
||||||
public async Task<ActionResult<List<Shared.Models.Autocompletion>>> ChatAutoComplete(
|
|
||||||
[FromBody] AutocompletionRequest request, Guid roomId)
|
|
||||||
{
|
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
var isMember = await db.ChatMembers
|
|
||||||
.AnyAsync(m =>
|
|
||||||
m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
|
|
||||||
if (!isMember)
|
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
|
||||||
|
|
||||||
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,14 +5,13 @@ using DysonNetwork.Shared;
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Localization;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/chat")]
|
[Route("/api/chat")]
|
||||||
@@ -20,7 +19,6 @@ public class ChatRoomController(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ChatRoomService crs,
|
ChatRoomService crs,
|
||||||
RemoteRealmService rs,
|
RemoteRealmService rs,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
FileService.FileServiceClient files,
|
FileService.FileServiceClient files,
|
||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
@@ -1084,12 +1082,11 @@ public class ChatRoomController(
|
|||||||
{
|
{
|
||||||
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
|
||||||
CultureService.SetCultureInfo(account);
|
CultureService.SetCultureInfo(account);
|
||||||
|
var title = "Chat Invite";
|
||||||
|
var body = member.ChatRoom.Type == ChatRoomType.DirectMessage
|
||||||
|
? $"{sender.Nick} sent you a direct message"
|
||||||
|
: $"You have been invited to {member.ChatRoom.Name ?? "Unnamed"}";
|
||||||
|
|
||||||
string title = localizer["ChatInviteTitle"];
|
|
||||||
|
|
||||||
string body = member.ChatRoom.Type == ChatRoomType.DirectMessage
|
|
||||||
? localizer["ChatInviteDirectBody", sender.Nick]
|
|
||||||
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
|
||||||
|
|
||||||
await pusher.SendPushNotificationToUserAsync(
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
new SendPushNotificationToUserRequest
|
new SendPushNotificationToUserRequest
|
||||||
@@ -4,7 +4,7 @@ using DysonNetwork.Shared.Registry;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
public class ChatRoomService(
|
public class ChatRoomService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Messager.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.WebReader;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Messager.WebReader;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using WebSocketPacket = DysonNetwork.Shared.Proto.WebSocketPacket;
|
using WebSocketPacket = DysonNetwork.Shared.Proto.WebSocketPacket;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
public partial class ChatService(
|
public partial class ChatService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/api/realms/{slug}")]
|
[Route("/api/realms/{slug}")]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
namespace DysonNetwork.Messager.Chat.Realtime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for real-time communication services (like Cloudflare, Agora, Twilio, etc.)
|
/// Interface for real-time communication services (like Cloudflare, Agora, Twilio, etc.)
|
||||||
@@ -5,7 +5,7 @@ using System.Text.Json;
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
namespace DysonNetwork.Messager.Chat.Realtime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// LiveKit implementation of the real-time communication service
|
/// LiveKit implementation of the real-time communication service
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Messager.Chat.Realtime;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Messager.Chat;
|
||||||
|
|
||||||
public class RealtimeChatConfiguration
|
public class RealtimeChatConfiguration
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" />
|
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.76.0" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
|
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||||
|
|||||||
478
DysonNetwork.Messager/Migrations/20260101140847_InitialMigration.Designer.cs
generated
Normal file
478
DysonNetwork.Messager/Migrations/20260101140847_InitialMigration.Designer.cs
generated
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Messager;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDatabase))]
|
||||||
|
[Migration("20260101140847_InitialMigration")]
|
||||||
|
partial class InitialMigration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant?>("BreakUntil")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("break_until");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChatRoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_room_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<Guid?>("InvitedById")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("invited_by_id");
|
||||||
|
|
||||||
|
b.Property<Instant?>("JoinedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("joined_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LastReadAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_read_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LeaveAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("leave_at");
|
||||||
|
|
||||||
|
b.Property<string>("Nick")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("nick");
|
||||||
|
|
||||||
|
b.Property<int>("Notify")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("notify");
|
||||||
|
|
||||||
|
b.Property<ChatTimeoutCause>("TimeoutCause")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("timeout_cause");
|
||||||
|
|
||||||
|
b.Property<Instant?>("TimeoutUntil")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("timeout_until");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_members");
|
||||||
|
|
||||||
|
b.HasAlternateKey("ChatRoomId", "AccountId")
|
||||||
|
.HasName("ak_chat_members_chat_room_id_account_id");
|
||||||
|
|
||||||
|
b.HasIndex("InvitedById")
|
||||||
|
.HasDatabaseName("ix_chat_members_invited_by_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<List<SnCloudFileReferenceObject>>("Attachments")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("attachments");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChatRoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_room_id");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("content");
|
||||||
|
|
||||||
|
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?>("EditedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("edited_at");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ForwardedMessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("forwarded_message_id");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("MembersMentioned")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("members_mentioned");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("Meta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("meta");
|
||||||
|
|
||||||
|
b.Property<string>("Nonce")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("character varying(36)")
|
||||||
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RepliedMessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("replied_message_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_messages");
|
||||||
|
|
||||||
|
b.HasIndex("ChatRoomId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_chat_room_id");
|
||||||
|
|
||||||
|
b.HasIndex("ForwardedMessageId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_forwarded_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("RepliedMessageId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_replied_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_sender_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_messages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatReaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int>("Attitude")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("attitude");
|
||||||
|
|
||||||
|
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<Guid>("MessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("message_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("symbol");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_reactions");
|
||||||
|
|
||||||
|
b.HasIndex("MessageId")
|
||||||
|
.HasDatabaseName("ix_chat_reactions_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId")
|
||||||
|
.HasDatabaseName("ix_chat_reactions_sender_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_reactions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Background")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("background");
|
||||||
|
|
||||||
|
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<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCommunity")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_community");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPublic")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_public");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("picture");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RealmId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("realm_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_rooms");
|
||||||
|
|
||||||
|
b.ToTable("chat_rooms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealtimeCall", 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<string>("ProviderName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("provider_name");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("room_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("UpstreamConfigJson")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("upstream");
|
||||||
|
|
||||||
|
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.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "ChatRoom")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("ChatRoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_members_chat_rooms_chat_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "InvitedBy")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InvitedById")
|
||||||
|
.HasConstraintName("fk_chat_members_chat_members_invited_by_id");
|
||||||
|
|
||||||
|
b.Navigation("ChatRoom");
|
||||||
|
|
||||||
|
b.Navigation("InvitedBy");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "ChatRoom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChatRoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_rooms_chat_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "ForwardedMessage")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ForwardedMessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_messages_forwarded_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "RepliedMessage")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RepliedMessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_messages_replied_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "Sender")
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("SenderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_members_sender_id");
|
||||||
|
|
||||||
|
b.Navigation("ChatRoom");
|
||||||
|
|
||||||
|
b.Navigation("ForwardedMessage");
|
||||||
|
|
||||||
|
b.Navigation("RepliedMessage");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatReaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "Message")
|
||||||
|
.WithMany("Reactions")
|
||||||
|
.HasForeignKey("MessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_reactions_chat_messages_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "Sender")
|
||||||
|
.WithMany("Reactions")
|
||||||
|
.HasForeignKey("SenderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_reactions_chat_members_sender_id");
|
||||||
|
|
||||||
|
b.Navigation("Message");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealtimeCall", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "Room")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_realtime_call_chat_rooms_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "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.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
|
||||||
|
b.Navigation("Reactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Reactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialMigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_rooms",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
is_community = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
is_public = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
realm_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
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_rooms", x => x.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_members",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
chat_room_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
notify = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
invited_by_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true),
|
||||||
|
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_members", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_chat_members_chat_room_id_account_id", x => new { x.chat_room_id, x.account_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_members_chat_members_invited_by_id",
|
||||||
|
column: x => x.invited_by_id,
|
||||||
|
principalTable: "chat_members",
|
||||||
|
principalColumn: "id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_members_chat_rooms_chat_room_id",
|
||||||
|
column: x => x.chat_room_id,
|
||||||
|
principalTable: "chat_rooms",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_messages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
type = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||||
|
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
|
||||||
|
members_mentioned = table.Column<string>(type: "jsonb", nullable: true),
|
||||||
|
nonce = table.Column<string>(type: "character varying(36)", maxLength: 36, nullable: false),
|
||||||
|
edited_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
attachments = table.Column<List<SnCloudFileReferenceObject>>(type: "jsonb", nullable: false),
|
||||||
|
replied_message_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
forwarded_message_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
sender_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
chat_room_id = table.Column<Guid>(type: "uuid", 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_messages", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_messages_chat_members_sender_id",
|
||||||
|
column: x => x.sender_id,
|
||||||
|
principalTable: "chat_members",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_messages_chat_messages_forwarded_message_id",
|
||||||
|
column: x => x.forwarded_message_id,
|
||||||
|
principalTable: "chat_messages",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_messages_chat_messages_replied_message_id",
|
||||||
|
column: x => x.replied_message_id,
|
||||||
|
principalTable: "chat_messages",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_messages_chat_rooms_chat_room_id",
|
||||||
|
column: x => x.chat_room_id,
|
||||||
|
principalTable: "chat_rooms",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_realtime_call",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
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<Guid>(type: "uuid", nullable: false),
|
||||||
|
provider_name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
session_id = table.Column<string>(type: "text", nullable: true),
|
||||||
|
upstream = table.Column<string>(type: "jsonb", nullable: true),
|
||||||
|
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.CreateTable(
|
||||||
|
name: "chat_reactions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
message_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
sender_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
symbol = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
attitude = table.Column<int>(type: "integer", 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_reactions", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_reactions_chat_members_sender_id",
|
||||||
|
column: x => x.sender_id,
|
||||||
|
principalTable: "chat_members",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_chat_reactions_chat_messages_message_id",
|
||||||
|
column: x => x.message_id,
|
||||||
|
principalTable: "chat_messages",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_members_invited_by_id",
|
||||||
|
table: "chat_members",
|
||||||
|
column: "invited_by_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_messages_chat_room_id",
|
||||||
|
table: "chat_messages",
|
||||||
|
column: "chat_room_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_messages_forwarded_message_id",
|
||||||
|
table: "chat_messages",
|
||||||
|
column: "forwarded_message_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_messages_replied_message_id",
|
||||||
|
table: "chat_messages",
|
||||||
|
column: "replied_message_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_messages_sender_id",
|
||||||
|
table: "chat_messages",
|
||||||
|
column: "sender_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_reactions_message_id",
|
||||||
|
table: "chat_reactions",
|
||||||
|
column: "message_id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_chat_reactions_sender_id",
|
||||||
|
table: "chat_reactions",
|
||||||
|
column: "sender_id");
|
||||||
|
|
||||||
|
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_reactions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_realtime_call");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_messages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_members");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_rooms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
475
DysonNetwork.Messager/Migrations/AppDatabaseModelSnapshot.cs
Normal file
475
DysonNetwork.Messager/Migrations/AppDatabaseModelSnapshot.cs
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Messager;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDatabase))]
|
||||||
|
partial class AppDatabaseModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant?>("BreakUntil")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("break_until");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChatRoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_room_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<Guid?>("InvitedById")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("invited_by_id");
|
||||||
|
|
||||||
|
b.Property<Instant?>("JoinedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("joined_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LastReadAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_read_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LeaveAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("leave_at");
|
||||||
|
|
||||||
|
b.Property<string>("Nick")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("nick");
|
||||||
|
|
||||||
|
b.Property<int>("Notify")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("notify");
|
||||||
|
|
||||||
|
b.Property<ChatTimeoutCause>("TimeoutCause")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("timeout_cause");
|
||||||
|
|
||||||
|
b.Property<Instant?>("TimeoutUntil")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("timeout_until");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_members");
|
||||||
|
|
||||||
|
b.HasAlternateKey("ChatRoomId", "AccountId")
|
||||||
|
.HasName("ak_chat_members_chat_room_id_account_id");
|
||||||
|
|
||||||
|
b.HasIndex("InvitedById")
|
||||||
|
.HasDatabaseName("ix_chat_members_invited_by_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<List<SnCloudFileReferenceObject>>("Attachments")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("attachments");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChatRoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("chat_room_id");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("content");
|
||||||
|
|
||||||
|
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?>("EditedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("edited_at");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ForwardedMessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("forwarded_message_id");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("MembersMentioned")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("members_mentioned");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("Meta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("meta");
|
||||||
|
|
||||||
|
b.Property<string>("Nonce")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("character varying(36)")
|
||||||
|
.HasColumnName("nonce");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RepliedMessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("replied_message_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_messages");
|
||||||
|
|
||||||
|
b.HasIndex("ChatRoomId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_chat_room_id");
|
||||||
|
|
||||||
|
b.HasIndex("ForwardedMessageId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_forwarded_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("RepliedMessageId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_replied_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId")
|
||||||
|
.HasDatabaseName("ix_chat_messages_sender_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_messages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatReaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int>("Attitude")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("attitude");
|
||||||
|
|
||||||
|
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<Guid>("MessageId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("message_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("Symbol")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("symbol");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_reactions");
|
||||||
|
|
||||||
|
b.HasIndex("MessageId")
|
||||||
|
.HasDatabaseName("ix_chat_reactions_message_id");
|
||||||
|
|
||||||
|
b.HasIndex("SenderId")
|
||||||
|
.HasDatabaseName("ix_chat_reactions_sender_id");
|
||||||
|
|
||||||
|
b.ToTable("chat_reactions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Background")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("background");
|
||||||
|
|
||||||
|
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<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCommunity")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_community");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPublic")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_public");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("picture");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RealmId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("realm_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_chat_rooms");
|
||||||
|
|
||||||
|
b.ToTable("chat_rooms", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealtimeCall", 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<string>("ProviderName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("provider_name");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoomId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("room_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("SenderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sender_id");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("UpstreamConfigJson")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("upstream");
|
||||||
|
|
||||||
|
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.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "ChatRoom")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("ChatRoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_members_chat_rooms_chat_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "InvitedBy")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InvitedById")
|
||||||
|
.HasConstraintName("fk_chat_members_chat_members_invited_by_id");
|
||||||
|
|
||||||
|
b.Navigation("ChatRoom");
|
||||||
|
|
||||||
|
b.Navigation("InvitedBy");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "ChatRoom")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ChatRoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_rooms_chat_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "ForwardedMessage")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ForwardedMessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_messages_forwarded_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "RepliedMessage")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RepliedMessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_messages_replied_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "Sender")
|
||||||
|
.WithMany("Messages")
|
||||||
|
.HasForeignKey("SenderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_messages_chat_members_sender_id");
|
||||||
|
|
||||||
|
b.Navigation("ChatRoom");
|
||||||
|
|
||||||
|
b.Navigation("ForwardedMessage");
|
||||||
|
|
||||||
|
b.Navigation("RepliedMessage");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatReaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMessage", "Message")
|
||||||
|
.WithMany("Reactions")
|
||||||
|
.HasForeignKey("MessageId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_reactions_chat_messages_message_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "Sender")
|
||||||
|
.WithMany("Reactions")
|
||||||
|
.HasForeignKey("SenderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_reactions_chat_members_sender_id");
|
||||||
|
|
||||||
|
b.Navigation("Message");
|
||||||
|
|
||||||
|
b.Navigation("Sender");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealtimeCall", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatRoom", "Room")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoomId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_chat_realtime_call_chat_rooms_room_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnChatMember", "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.Shared.Models.SnChatMember", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Messages");
|
||||||
|
|
||||||
|
b.Navigation("Reactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatMessage", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Reactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnChatRoom", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
DysonNetwork.Messager/Poll/PollEmbed.cs
Normal file
6
DysonNetwork.Messager/Poll/PollEmbed.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Messager.Poll;
|
||||||
|
|
||||||
|
public class PollEmbed
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
}
|
||||||
172
DysonNetwork.Messager/Rewind/MessagerRewindServiceGrpc.cs
Normal file
172
DysonNetwork.Messager/Rewind/MessagerRewindServiceGrpc.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using DysonNetwork.Messager.Chat;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Registry;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using PostReactionAttitude = DysonNetwork.Shared.Proto.PostReactionAttitude;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Rewind;
|
||||||
|
|
||||||
|
public class MessagerRewindServiceGrpc(
|
||||||
|
AppDatabase db,
|
||||||
|
RemoteAccountService remoteAccounts,
|
||||||
|
ChatRoomService crs
|
||||||
|
) : RewindService.RewindServiceBase
|
||||||
|
{
|
||||||
|
public override async Task<RewindEvent> GetRewindEvent(
|
||||||
|
RequestRewindEvent request,
|
||||||
|
ServerCallContext context
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var accountId = Guid.Parse(request.AccountId);
|
||||||
|
var year = request.Year;
|
||||||
|
|
||||||
|
var startDate = new LocalDate(year - 1, 12, 26).AtMidnight().InUtc().ToInstant();
|
||||||
|
var endDate = new LocalDate(year, 12, 26).AtMidnight().InUtc().ToInstant();
|
||||||
|
|
||||||
|
// Chat data
|
||||||
|
var messagesQuery = db
|
||||||
|
.ChatMessages.Include(m => m.Sender)
|
||||||
|
.Include(m => m.ChatRoom)
|
||||||
|
.Where(m => m.CreatedAt >= startDate && m.CreatedAt < endDate)
|
||||||
|
.Where(m => m.Sender.AccountId == accountId)
|
||||||
|
.AsQueryable();
|
||||||
|
var mostMessagedChatInfo = await messagesQuery
|
||||||
|
.Where(m => m.ChatRoom.Type == ChatRoomType.Group)
|
||||||
|
.GroupBy(m => m.ChatRoomId)
|
||||||
|
.OrderByDescending(g => g.Count())
|
||||||
|
.Select(g => new { ChatRoom = g.First().ChatRoom, MessageCount = g.Count() })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
var mostMessagedChat = mostMessagedChatInfo?.ChatRoom;
|
||||||
|
var mostMessagedDirectChatInfo = await messagesQuery
|
||||||
|
.Where(m => m.ChatRoom.Type == ChatRoomType.DirectMessage)
|
||||||
|
.GroupBy(m => m.ChatRoomId)
|
||||||
|
.OrderByDescending(g => g.Count())
|
||||||
|
.Select(g => new { ChatRoom = g.First().ChatRoom, MessageCount = g.Count() })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
var mostMessagedDirectChat = mostMessagedDirectChatInfo is not null
|
||||||
|
? await crs.LoadDirectMessageMembers(mostMessagedDirectChatInfo.ChatRoom, accountId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Call data
|
||||||
|
var callQuery = db
|
||||||
|
.ChatRealtimeCall.Include(c => c.Sender)
|
||||||
|
.Include(c => c.Room)
|
||||||
|
.Where(c => c.CreatedAt >= startDate && c.CreatedAt < endDate)
|
||||||
|
.Where(c => c.Sender.AccountId == accountId)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var groupCallRecords = await callQuery
|
||||||
|
.Where(c => c.Room.Type == ChatRoomType.Group)
|
||||||
|
.Select(c => new
|
||||||
|
{
|
||||||
|
c.RoomId,
|
||||||
|
c.CreatedAt,
|
||||||
|
c.EndedAt,
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
var callDurations = groupCallRecords
|
||||||
|
.Select(c => new { c.RoomId, Duration = (c.EndedAt ?? now).Minus(c.CreatedAt).Seconds })
|
||||||
|
.ToList();
|
||||||
|
var mostCalledRoomInfo = callDurations
|
||||||
|
.GroupBy(c => c.RoomId)
|
||||||
|
.Select(g => new { RoomId = g.Key, TotalDuration = g.Sum(c => c.Duration) })
|
||||||
|
.OrderByDescending(g => g.TotalDuration)
|
||||||
|
.FirstOrDefault();
|
||||||
|
var mostCalledRoom =
|
||||||
|
mostCalledRoomInfo != null && mostCalledRoomInfo.RoomId != Guid.Empty
|
||||||
|
? await db.ChatRooms.FindAsync(mostCalledRoomInfo.RoomId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<SnAccount>? mostCalledChatTopMembers = null;
|
||||||
|
if (mostCalledRoom != null)
|
||||||
|
mostCalledChatTopMembers = await crs.GetTopActiveMembers(
|
||||||
|
mostCalledRoom.Id,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
var directCallRecords = await callQuery
|
||||||
|
.Where(c => c.Room.Type == ChatRoomType.DirectMessage)
|
||||||
|
.Select(c => new
|
||||||
|
{
|
||||||
|
c.RoomId,
|
||||||
|
c.CreatedAt,
|
||||||
|
c.EndedAt,
|
||||||
|
c.Room,
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
var directCallDurations = directCallRecords
|
||||||
|
.Select(c => new
|
||||||
|
{
|
||||||
|
c.RoomId,
|
||||||
|
c.Room,
|
||||||
|
Duration = (c.EndedAt ?? now).Minus(c.CreatedAt).Seconds,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
var mostCalledDirectRooms = directCallDurations
|
||||||
|
.GroupBy(c => c.RoomId)
|
||||||
|
.Select(g => new { ChatRoom = g.First().Room, TotalDuration = g.Sum(c => c.Duration) })
|
||||||
|
.OrderByDescending(g => g.TotalDuration)
|
||||||
|
.Take(3)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var accountIds = new List<Guid>();
|
||||||
|
foreach (var item in mostCalledDirectRooms)
|
||||||
|
{
|
||||||
|
var room = await crs.LoadDirectMessageMembers(item.ChatRoom, accountId);
|
||||||
|
var otherMember = room.DirectMembers.FirstOrDefault(m => m.AccountId != accountId);
|
||||||
|
if (otherMember != null)
|
||||||
|
accountIds.Add(otherMember.AccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var accounts = await remoteAccounts.GetAccountBatch(accountIds);
|
||||||
|
var mostCalledAccounts = accounts
|
||||||
|
.Zip(
|
||||||
|
mostCalledDirectRooms,
|
||||||
|
(account, room) =>
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["account"] = account,
|
||||||
|
["duration"] = room.TotalDuration,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["most_messaged_chat"] = mostMessagedChatInfo is not null
|
||||||
|
? new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["chat"] = mostMessagedChat,
|
||||||
|
["message_counts"] = mostMessagedChatInfo.MessageCount,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
["most_messaged_direct_chat"] = mostMessagedDirectChatInfo is not null
|
||||||
|
? new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["chat"] = mostMessagedDirectChat,
|
||||||
|
["message_counts"] = mostMessagedDirectChatInfo.MessageCount,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
["most_called_chat"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["chat"] = mostCalledRoom,
|
||||||
|
["duration"] = mostCalledRoomInfo?.TotalDuration,
|
||||||
|
},
|
||||||
|
["most_called_chat_top_members"] = mostCalledChatTopMembers,
|
||||||
|
["most_called_accounts"] = mostCalledAccounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new RewindEvent
|
||||||
|
{
|
||||||
|
ServiceId = "messager",
|
||||||
|
AccountId = request.AccountId,
|
||||||
|
Data = GrpcTypeHelper.ConvertObjectToByteString(data, withoutIgnore: true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
345
DysonNetwork.Messager/Startup/BroadcastEventHandler.cs
Normal file
345
DysonNetwork.Messager/Startup/BroadcastEventHandler.cs
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Queue;
|
||||||
|
using DysonNetwork.Messager.Chat;
|
||||||
|
using Google.Protobuf;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NATS.Client.Core;
|
||||||
|
using NATS.Client.JetStream.Models;
|
||||||
|
using NATS.Net;
|
||||||
|
using NodaTime;
|
||||||
|
using WebSocketPacket = DysonNetwork.Shared.Models.WebSocketPacket;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Startup;
|
||||||
|
|
||||||
|
public class BroadcastEventHandler(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<BroadcastEventHandler> logger,
|
||||||
|
INatsConnection nats,
|
||||||
|
RingService.RingServiceClient pusher
|
||||||
|
) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var accountTask = HandleAccountDeletions(stoppingToken);
|
||||||
|
var websocketTask = HandleWebSocketPackets(stoppingToken);
|
||||||
|
var accountStatusTask = HandleAccountStatusUpdates(stoppingToken);
|
||||||
|
|
||||||
|
await Task.WhenAll(accountTask, websocketTask, accountStatusTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleAccountDeletions(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var js = nats.CreateJetStreamContext();
|
||||||
|
|
||||||
|
await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]);
|
||||||
|
|
||||||
|
var consumer = await js.CreateOrUpdateConsumerAsync("account_events",
|
||||||
|
new ConsumerConfig("messager_account_deleted_handler"), cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
|
||||||
|
if (evt == null)
|
||||||
|
{
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Account deleted: {AccountId}", evt.AccountId);
|
||||||
|
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
|
||||||
|
await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == evt.AccountId)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
await db.ChatMessages
|
||||||
|
.Where(m => m.Sender.AccountId == evt.AccountId)
|
||||||
|
.ExecuteUpdateAsync(c => c.SetProperty(p => p.DeletedAt, now), stoppingToken);
|
||||||
|
|
||||||
|
await db.ChatReactions
|
||||||
|
.Where(r => r.Sender.AccountId == evt.AccountId)
|
||||||
|
.ExecuteUpdateAsync(c => c.SetProperty(p => p.DeletedAt, now), stoppingToken);
|
||||||
|
|
||||||
|
await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == evt.AccountId)
|
||||||
|
.ExecuteUpdateAsync(c => c.SetProperty(p => p.DeletedAt, now), stoppingToken);
|
||||||
|
|
||||||
|
await transaction.CommitAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken: stoppingToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error processing AccountDeleted");
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleWebSocketPackets(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
await foreach (var msg in nats.SubscribeAsync<byte[]>(
|
||||||
|
WebSocketPacketEvent.SubjectPrefix + "sphere", cancellationToken: stoppingToken))
|
||||||
|
{
|
||||||
|
logger.LogDebug("Handling websocket packet...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var evt = JsonSerializer.Deserialize<WebSocketPacketEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
|
||||||
|
if (evt == null) throw new ArgumentNullException(nameof(evt));
|
||||||
|
var packet = WebSocketPacket.FromBytes(evt.PacketBytes);
|
||||||
|
logger.LogInformation("Handling websocket packet... {Type}", packet.Type);
|
||||||
|
switch (packet.Type)
|
||||||
|
{
|
||||||
|
case "messages.read":
|
||||||
|
await HandleMessageRead(evt, packet);
|
||||||
|
break;
|
||||||
|
case "messages.typing":
|
||||||
|
await HandleMessageTyping(evt, packet);
|
||||||
|
break;
|
||||||
|
case "messages.subscribe":
|
||||||
|
await HandleMessageSubscribe(evt, packet);
|
||||||
|
break;
|
||||||
|
case "messages.unsubscribe":
|
||||||
|
await HandleMessageUnsubscribe(evt, packet);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error processing websocket packet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMessageRead(WebSocketPacketEvent evt, WebSocketPacket packet)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var cs = scope.ServiceProvider.GetRequiredService<ChatService>();
|
||||||
|
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||||
|
|
||||||
|
if (packet.Data == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "Mark message as read requires you to provide the ChatRoomId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestData = packet.GetData<Chat.ChatController.MarkMessageReadRequest>();
|
||||||
|
if (requestData == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "Invalid request data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await cs.ReadChatRoomAsync(requestData.ChatRoomId, evt.AccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMessageTyping(WebSocketPacketEvent evt, WebSocketPacket packet)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||||
|
|
||||||
|
if (packet.Data == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "messages.typing requires you to provide the ChatRoomId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
||||||
|
if (requestData == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "Invalid request data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var responsePacket = new WebSocketPacket
|
||||||
|
{
|
||||||
|
Type = "messages.typing",
|
||||||
|
Data = new
|
||||||
|
{
|
||||||
|
room_id = sender.ChatRoomId,
|
||||||
|
sender_id = sender.Id,
|
||||||
|
sender
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Broadcast typing indicator to subscribed room members only
|
||||||
|
var subscribedMemberIds = await crs.GetSubscribedMembers(requestData.ChatRoomId);
|
||||||
|
var roomMembers = await crs.ListRoomMembers(requestData.ChatRoomId);
|
||||||
|
|
||||||
|
// Filter to subscribed members excluding the current user
|
||||||
|
var subscribedMembers = roomMembers
|
||||||
|
.Where(m => subscribedMemberIds.Contains(m.Id) && m.AccountId != evt.AccountId)
|
||||||
|
.Select(m => m.AccountId.ToString())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (subscribedMembers.Count > 0)
|
||||||
|
{
|
||||||
|
var respRequest = new PushWebSocketPacketToUsersRequest { Packet = responsePacket.ToProtoValue() };
|
||||||
|
respRequest.UserIds.AddRange(subscribedMembers);
|
||||||
|
await pusher.PushWebSocketPacketToUsersAsync(respRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMessageSubscribe(WebSocketPacketEvent evt, WebSocketPacket packet)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||||
|
|
||||||
|
if (packet.Data == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "messages.subscribe requires you to provide the ChatRoomId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
||||||
|
if (requestData == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "Invalid request data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await crs.SubscribeChatRoom(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMessageUnsubscribe(WebSocketPacketEvent evt, WebSocketPacket packet)
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||||
|
|
||||||
|
if (packet.Data == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "messages.unsubscribe requires you to provide the ChatRoomId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
||||||
|
if (requestData == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "Invalid request data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await crs.UnsubscribeChatRoom(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleAccountStatusUpdates(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountStatusUpdatedEvent.Type,
|
||||||
|
cancellationToken: stoppingToken))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var evt =
|
||||||
|
GrpcTypeHelper.ConvertByteStringToObject<AccountStatusUpdatedEvent>(ByteString.CopyFrom(msg.Data));
|
||||||
|
if (evt == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
logger.LogInformation("Account status updated: {AccountId}", evt.AccountId);
|
||||||
|
|
||||||
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
var chatRoomService = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||||
|
|
||||||
|
// Get user's joined chat rooms
|
||||||
|
var userRooms = await db.ChatMembers
|
||||||
|
.Where(m => m.AccountId == evt.AccountId && m.JoinedAt != null && m.LeaveAt == null)
|
||||||
|
.Select(m => m.ChatRoomId)
|
||||||
|
.ToListAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
// Send WebSocket packet to subscribed users per room
|
||||||
|
foreach (var roomId in userRooms)
|
||||||
|
{
|
||||||
|
var members = await chatRoomService.ListRoomMembers(roomId);
|
||||||
|
var subscribedMemberIds = await chatRoomService.GetSubscribedMembers(roomId);
|
||||||
|
var subscribedUsers = members
|
||||||
|
.Where(m => subscribedMemberIds.Contains(m.Id))
|
||||||
|
.Select(m => m.AccountId.ToString())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (subscribedUsers.Count == 0) continue;
|
||||||
|
var packet = new WebSocketPacket
|
||||||
|
{
|
||||||
|
Type = "accounts.status.update",
|
||||||
|
Data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["status"] = evt.Status,
|
||||||
|
["chat_room_id"] = roomId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = new PushWebSocketPacketToUsersRequest
|
||||||
|
{
|
||||||
|
Packet = packet.ToProtoValue()
|
||||||
|
};
|
||||||
|
request.UserIds.AddRange(subscribedUsers);
|
||||||
|
|
||||||
|
await pusher.PushWebSocketPacketToUsersAsync(request, cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Sent status update for room {roomId} to {count} subscribed users", roomId,
|
||||||
|
subscribedUsers.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error processing AccountStatusUpdated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendErrorResponse(WebSocketPacketEvent evt, string message)
|
||||||
|
{
|
||||||
|
await pusher.PushWebSocketPacketToDeviceAsync(new PushWebSocketPacketToDeviceRequest
|
||||||
|
{
|
||||||
|
DeviceId = evt.DeviceId,
|
||||||
|
Packet = new WebSocketPacket
|
||||||
|
{
|
||||||
|
Type = "error",
|
||||||
|
ErrorMessage = message
|
||||||
|
}.ToProtoValue()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Messager.Chat;
|
||||||
|
using DysonNetwork.Messager.Chat.Realtime;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
|
|
||||||
@@ -8,7 +10,9 @@ namespace DysonNetwork.Messager.Startup;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
extension(IServiceCollection services)
|
||||||
|
{
|
||||||
|
public IServiceCollection AddAppServices()
|
||||||
{
|
{
|
||||||
services.AddDbContext<AppDatabase>();
|
services.AddDbContext<AppDatabase>();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
@@ -36,17 +40,22 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public IServiceCollection AddAppAuthentication()
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppBusinessServices(
|
public IServiceCollection AddAppBusinessServices(IConfiguration configuration
|
||||||
this IServiceCollection services,
|
|
||||||
IConfiguration configuration
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
services.AddScoped<ChatRoomService>();
|
||||||
|
services.AddScoped<ChatService>();
|
||||||
|
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
||||||
|
|
||||||
|
services.AddHostedService<BroadcastEventHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
8
DysonNetwork.Messager/Wallet/FundEmbed.cs
Normal file
8
DysonNetwork.Messager/Wallet/FundEmbed.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.Wallet;
|
||||||
|
|
||||||
|
public class FundEmbed
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
}
|
||||||
41
DysonNetwork.Messager/WebReader/EmbeddableBase.cs
Normal file
41
DysonNetwork.Messager/WebReader/EmbeddableBase.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The embeddable can be used in the post or messages' meta's embeds fields
|
||||||
|
/// To render a richer type of content.
|
||||||
|
///
|
||||||
|
/// A simple example of using link preview embed:
|
||||||
|
/// <code>
|
||||||
|
/// {
|
||||||
|
/// // ... post content
|
||||||
|
/// "meta": {
|
||||||
|
/// "embeds": [
|
||||||
|
/// {
|
||||||
|
/// "type": "link",
|
||||||
|
/// "title: "...",
|
||||||
|
/// /// ...
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </summary>
|
||||||
|
public abstract class EmbeddableBase
|
||||||
|
{
|
||||||
|
public abstract string Type { get; }
|
||||||
|
|
||||||
|
public static Dictionary<string, object> ToDictionary(dynamic input)
|
||||||
|
{
|
||||||
|
var jsonRaw = JsonSerializer.Serialize(
|
||||||
|
input,
|
||||||
|
GrpcTypeHelper.SerializerOptionsWithoutIgnore
|
||||||
|
);
|
||||||
|
return JsonSerializer.Deserialize<Dictionary<string, object>>(
|
||||||
|
jsonRaw,
|
||||||
|
GrpcTypeHelper.SerializerOptionsWithoutIgnore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
DysonNetwork.Messager/WebReader/LinkEmbed.cs
Normal file
55
DysonNetwork.Messager/WebReader/LinkEmbed.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link embed is a part of the embeddable implementations
|
||||||
|
/// It can be used in the post or messages' meta's embeds fields
|
||||||
|
/// </summary>
|
||||||
|
public class LinkEmbed : EmbeddableBase
|
||||||
|
{
|
||||||
|
public override string Type => "link";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The original URL that was processed
|
||||||
|
/// </summary>
|
||||||
|
public required string Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Title of the linked content (from OpenGraph og:title, meta title, or page title)
|
||||||
|
/// </summary>
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the linked content (from OpenGraph og:description or meta description)
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL to the thumbnail image (from OpenGraph og:image or other meta tags)
|
||||||
|
/// </summary>
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The favicon URL of the site
|
||||||
|
/// </summary>
|
||||||
|
public string? FaviconUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The site name (from OpenGraph og:site_name)
|
||||||
|
/// </summary>
|
||||||
|
public string? SiteName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of the content (from OpenGraph og:type)
|
||||||
|
/// </summary>
|
||||||
|
public string? ContentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Author of the content if available
|
||||||
|
/// </summary>
|
||||||
|
public string? Author { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Published date of the content if available
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PublishedDate { get; set; }
|
||||||
|
}
|
||||||
7
DysonNetwork.Messager/WebReader/ScrapedArticle.cs
Normal file
7
DysonNetwork.Messager/WebReader/ScrapedArticle.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
public class ScrapedArticle
|
||||||
|
{
|
||||||
|
public LinkEmbed LinkEmbed { get; set; } = null!;
|
||||||
|
public string? Content { get; set; }
|
||||||
|
}
|
||||||
110
DysonNetwork.Messager/WebReader/WebReaderController.cs
Normal file
110
DysonNetwork.Messager/WebReader/WebReaderController.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for web scraping and link preview services
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/scrap")]
|
||||||
|
[EnableRateLimiting("fixed")]
|
||||||
|
public class WebReaderController(WebReaderService reader, ILogger<WebReaderController> logger)
|
||||||
|
: ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a preview for the provided URL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL-encoded link to generate preview for</param>
|
||||||
|
/// <returns>Link preview data including title, description, and image</returns>
|
||||||
|
[HttpGet("link")]
|
||||||
|
public async Task<ActionResult<LinkEmbed>> ScrapLink([FromQuery] string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "URL parameter is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure URL is properly decoded
|
||||||
|
var decodedUrl = UrlDecoder.Decode(url);
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
if (!Uri.TryCreate(decodedUrl, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Invalid URL format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkEmbed = await reader.GetLinkPreviewAsync(decodedUrl);
|
||||||
|
return Ok(linkEmbed);
|
||||||
|
}
|
||||||
|
catch (WebReaderException ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Error scraping link: {Url}", url);
|
||||||
|
return BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error scraping link: {Url}", url);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||||
|
new { error = "An unexpected error occurred while processing the link" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force invalidates the cache for a specific URL
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("link/cache")]
|
||||||
|
[Authorize]
|
||||||
|
[AskPermission("cache.scrap")]
|
||||||
|
public async Task<IActionResult> InvalidateCache([FromQuery] string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "URL parameter is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await reader.InvalidateCacheForUrlAsync(url);
|
||||||
|
return Ok(new { message = "Cache invalidated for URL" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force invalidates all cached link previews
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("cache/all")]
|
||||||
|
[Authorize]
|
||||||
|
[AskPermission("cache.scrap")]
|
||||||
|
public async Task<IActionResult> InvalidateAllCache()
|
||||||
|
{
|
||||||
|
await reader.InvalidateAllCachedPreviewsAsync();
|
||||||
|
return Ok(new { message = "All link preview caches invalidated" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for URL decoding
|
||||||
|
/// </summary>
|
||||||
|
public static class UrlDecoder
|
||||||
|
{
|
||||||
|
public static string Decode(string url)
|
||||||
|
{
|
||||||
|
// First check if URL is already decoded
|
||||||
|
if (!url.Contains('%') && !url.Contains('+'))
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Net.WebUtility.UrlDecode(url);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If decoding fails, return the original string
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
DysonNetwork.Messager/WebReader/WebReaderException.cs
Normal file
15
DysonNetwork.Messager/WebReader/WebReaderException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when an error occurs during web reading operations
|
||||||
|
/// </summary>
|
||||||
|
public class WebReaderException : Exception
|
||||||
|
{
|
||||||
|
public WebReaderException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebReaderException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
367
DysonNetwork.Messager/WebReader/WebReaderService.cs
Normal file
367
DysonNetwork.Messager/WebReader/WebReaderService.cs
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Messager.WebReader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service is amin to providing scrapping service to the Solar Network.
|
||||||
|
/// Such as news feed, external articles and link preview.
|
||||||
|
/// </summary>
|
||||||
|
public class WebReaderService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
ILogger<WebReaderService> logger,
|
||||||
|
ICacheService cache
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private const string LinkPreviewCachePrefix = "scrap:preview:";
|
||||||
|
private const string LinkPreviewCacheGroup = "scrap:preview";
|
||||||
|
|
||||||
|
public async Task<ScrapedArticle> ScrapeArticleAsync(string url, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var linkEmbed = await GetLinkPreviewAsync(url, cancellationToken);
|
||||||
|
var content = await GetArticleContentAsync(url, cancellationToken);
|
||||||
|
return new ScrapedArticle
|
||||||
|
{
|
||||||
|
LinkEmbed = linkEmbed,
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetArticleContentAsync(string url, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var httpClient = httpClientFactory.CreateClient("WebReader");
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to scrap article content for URL: {Url}", url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(html);
|
||||||
|
var articleNode = doc.DocumentNode.SelectSingleNode("//article");
|
||||||
|
return articleNode?.InnerHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a link preview embed from a URL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL to generate the preview for</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="bypassCache">If true, bypass cache and fetch fresh data</param>
|
||||||
|
/// <param name="cacheExpiry">Custom cache expiration time</param>
|
||||||
|
/// <returns>A LinkEmbed object containing the preview data</returns>
|
||||||
|
public async Task<LinkEmbed> GetLinkPreviewAsync(
|
||||||
|
string url,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
TimeSpan? cacheExpiry = null,
|
||||||
|
bool bypassCache = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Ensure URL is valid
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(@"Invalid URL format", nameof(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get from cache if not bypassing
|
||||||
|
if (!bypassCache)
|
||||||
|
{
|
||||||
|
var cachedPreview = await GetCachedLinkPreview(url);
|
||||||
|
if (cachedPreview is not null)
|
||||||
|
return cachedPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss or bypass, fetch fresh data
|
||||||
|
logger.LogDebug("Fetching fresh link preview for URL: {Url}", url);
|
||||||
|
var httpClient = httpClientFactory.CreateClient("WebReader");
|
||||||
|
httpClient.MaxResponseContentBufferSize =
|
||||||
|
10 * 1024 * 1024; // 10MB, prevent scrap some directly accessible files
|
||||||
|
httpClient.Timeout = TimeSpan.FromSeconds(3);
|
||||||
|
// Setting UA to facebook's bot to get the opengraph.
|
||||||
|
httpClient.DefaultRequestHeaders.Add("User-Agent", "facebookexternalhit/1.1");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var contentType = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (contentType == null || !contentType.StartsWith("text/html"))
|
||||||
|
{
|
||||||
|
logger.LogWarning("URL is not an HTML page: {Url}, ContentType: {ContentType}", url, contentType);
|
||||||
|
var nonHtmlEmbed = new LinkEmbed
|
||||||
|
{
|
||||||
|
Url = url,
|
||||||
|
Title = uri.Host,
|
||||||
|
ContentType = contentType
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache non-HTML responses too
|
||||||
|
await CacheLinkPreview(nonHtmlEmbed, url, cacheExpiry);
|
||||||
|
return nonHtmlEmbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var linkEmbed = await ExtractLinkData(url, html, uri);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
await CacheLinkPreview(linkEmbed, url, cacheExpiry);
|
||||||
|
|
||||||
|
return linkEmbed;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to fetch URL: {Url}", url);
|
||||||
|
throw new WebReaderException($"Failed to fetch URL: {url}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<LinkEmbed> ExtractLinkData(string url, string html, Uri uri)
|
||||||
|
{
|
||||||
|
var embed = new LinkEmbed
|
||||||
|
{
|
||||||
|
Url = url
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure AngleSharp context
|
||||||
|
var config = Configuration.Default;
|
||||||
|
var context = BrowsingContext.New(config);
|
||||||
|
var document = await context.OpenAsync(req => req.Content(html));
|
||||||
|
|
||||||
|
// Extract OpenGraph tags
|
||||||
|
var ogTitle = GetMetaTagContent(document, "og:title");
|
||||||
|
var ogDescription = GetMetaTagContent(document, "og:description");
|
||||||
|
var ogImage = GetMetaTagContent(document, "og:image");
|
||||||
|
var ogSiteName = GetMetaTagContent(document, "og:site_name");
|
||||||
|
var ogType = GetMetaTagContent(document, "og:type");
|
||||||
|
|
||||||
|
// Extract Twitter card tags as fallback
|
||||||
|
var twitterTitle = GetMetaTagContent(document, "twitter:title");
|
||||||
|
var twitterDescription = GetMetaTagContent(document, "twitter:description");
|
||||||
|
var twitterImage = GetMetaTagContent(document, "twitter:image");
|
||||||
|
|
||||||
|
// Extract standard meta tags as final fallback
|
||||||
|
var metaTitle = GetMetaTagContent(document, "title") ??
|
||||||
|
GetMetaContent(document, "title");
|
||||||
|
var metaDescription = GetMetaTagContent(document, "description");
|
||||||
|
|
||||||
|
// Extract page title
|
||||||
|
var pageTitle = document.Title?.Trim();
|
||||||
|
|
||||||
|
// Extract publish date
|
||||||
|
var publishedTime = GetMetaTagContent(document, "article:published_time") ??
|
||||||
|
GetMetaTagContent(document, "datePublished") ??
|
||||||
|
GetMetaTagContent(document, "pubdate");
|
||||||
|
|
||||||
|
// Extract author
|
||||||
|
var author = GetMetaTagContent(document, "author") ??
|
||||||
|
GetMetaTagContent(document, "article:author");
|
||||||
|
|
||||||
|
// Extract favicon
|
||||||
|
var faviconUrl = GetFaviconUrl(document, uri);
|
||||||
|
|
||||||
|
// Populate the embed with the data, prioritizing OpenGraph
|
||||||
|
embed.Title = ogTitle ?? twitterTitle ?? metaTitle ?? pageTitle ?? uri.Host;
|
||||||
|
embed.Description = ogDescription ?? twitterDescription ?? metaDescription;
|
||||||
|
embed.ImageUrl = ResolveRelativeUrl(ogImage ?? twitterImage, uri);
|
||||||
|
embed.SiteName = ogSiteName ?? uri.Host;
|
||||||
|
embed.ContentType = ogType;
|
||||||
|
embed.FaviconUrl = faviconUrl;
|
||||||
|
embed.Author = author;
|
||||||
|
|
||||||
|
// Parse and set published date
|
||||||
|
if (!string.IsNullOrEmpty(publishedTime) &&
|
||||||
|
DateTime.TryParse(publishedTime, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal,
|
||||||
|
out DateTime parsedDate))
|
||||||
|
{
|
||||||
|
embed.PublishedDate = parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetMetaTagContent(IDocument doc, string property)
|
||||||
|
{
|
||||||
|
// Check for OpenGraph/Twitter style meta tags
|
||||||
|
var node = doc.QuerySelector($"meta[property='{property}'][content]")
|
||||||
|
?? doc.QuerySelector($"meta[name='{property}'][content]");
|
||||||
|
|
||||||
|
return node?.GetAttribute("content")?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetMetaContent(IDocument doc, string name)
|
||||||
|
{
|
||||||
|
var node = doc.QuerySelector($"meta[name='{name}'][content]");
|
||||||
|
return node?.GetAttribute("content")?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetFaviconUrl(IDocument doc, Uri baseUri)
|
||||||
|
{
|
||||||
|
// Look for apple-touch-icon first as it's typically higher quality
|
||||||
|
var appleIconNode = doc.QuerySelector("link[rel='apple-touch-icon'][href]");
|
||||||
|
if (appleIconNode != null)
|
||||||
|
{
|
||||||
|
return ResolveRelativeUrl(appleIconNode.GetAttribute("href"), baseUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for standard favicon
|
||||||
|
var faviconNode = doc.QuerySelector("link[rel='icon'][href]") ??
|
||||||
|
doc.QuerySelector("link[rel='shortcut icon'][href]");
|
||||||
|
|
||||||
|
return faviconNode != null
|
||||||
|
? ResolveRelativeUrl(faviconNode.GetAttribute("href"), baseUri)
|
||||||
|
: new Uri(baseUri, "/favicon.ico").ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveRelativeUrl(string? url, Uri baseUri)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
return url; // Already absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
return Uri.TryCreate(baseUri, url, out var absoluteUri) ? absoluteUri.ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a hash-based cache key for a URL
|
||||||
|
/// </summary>
|
||||||
|
private string GenerateUrlCacheKey(string url)
|
||||||
|
{
|
||||||
|
// Normalize the URL first
|
||||||
|
var normalizedUrl = NormalizeUrl(url);
|
||||||
|
|
||||||
|
// Create SHA256 hash of the normalized URL
|
||||||
|
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||||
|
var urlBytes = System.Text.Encoding.UTF8.GetBytes(normalizedUrl);
|
||||||
|
var hashBytes = sha256.ComputeHash(urlBytes);
|
||||||
|
|
||||||
|
// Convert to hex string
|
||||||
|
var hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
|
||||||
|
// Return prefixed key
|
||||||
|
return $"{LinkPreviewCachePrefix}{hashString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalize URL by trimming trailing slashes but preserving query parameters
|
||||||
|
/// </summary>
|
||||||
|
private string NormalizeUrl(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// First ensure we have a valid URI
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||||
|
return url.TrimEnd('/');
|
||||||
|
|
||||||
|
// Rebuild the URL without trailing slashes but with query parameters
|
||||||
|
var scheme = uri.Scheme;
|
||||||
|
var host = uri.Host;
|
||||||
|
var port = uri.IsDefaultPort ? string.Empty : $":{uri.Port}";
|
||||||
|
var path = uri.AbsolutePath.TrimEnd('/');
|
||||||
|
var query = uri.Query;
|
||||||
|
|
||||||
|
return $"{scheme}://{host}{port}{path}{query}".ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache a link preview
|
||||||
|
/// </summary>
|
||||||
|
private async Task CacheLinkPreview(LinkEmbed? linkEmbed, string url, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
if (linkEmbed == null || string.IsNullOrEmpty(url))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheKey = GenerateUrlCacheKey(url);
|
||||||
|
var expiryTime = expiry ?? TimeSpan.FromHours(24);
|
||||||
|
|
||||||
|
await cache.SetWithGroupsAsync(
|
||||||
|
cacheKey,
|
||||||
|
linkEmbed,
|
||||||
|
[LinkPreviewCacheGroup],
|
||||||
|
expiryTime);
|
||||||
|
|
||||||
|
logger.LogDebug("Cached link preview for URL: {Url} with key: {CacheKey}", url, cacheKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log but don't throw - caching failures shouldn't break the main functionality
|
||||||
|
logger.LogWarning(ex, "Failed to cache link preview for URL: {Url}", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to get a cached link preview
|
||||||
|
/// </summary>
|
||||||
|
private async Task<LinkEmbed?> GetCachedLinkPreview(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheKey = GenerateUrlCacheKey(url);
|
||||||
|
var cachedPreview = await cache.GetAsync<LinkEmbed>(cacheKey);
|
||||||
|
|
||||||
|
if (cachedPreview is not null)
|
||||||
|
logger.LogDebug("Retrieved cached link preview for URL: {Url}", url);
|
||||||
|
|
||||||
|
return cachedPreview;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to retrieve cached link preview for URL: {Url}", url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate cache for a specific URL
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvalidateCacheForUrlAsync(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheKey = GenerateUrlCacheKey(url);
|
||||||
|
await cache.RemoveAsync(cacheKey);
|
||||||
|
logger.LogDebug("Invalidated cache for URL: {Url} with key: {CacheKey}", url, cacheKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to invalidate cache for URL: {Url}", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate all cached link previews
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvalidateAllCachedPreviewsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await cache.RemoveGroupAsync(LinkPreviewCacheGroup);
|
||||||
|
logger.LogInformation("Invalidated all cached link previews");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to invalidate all cached link previews");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,8 @@ public class AccountRewindService(
|
|||||||
var rewindEventTasks = new List<Task<RewindEvent>>
|
var rewindEventTasks = new List<Task<RewindEvent>>
|
||||||
{
|
{
|
||||||
passRewindSrv.CreateRewindEvent(accountId, currentYear),
|
passRewindSrv.CreateRewindEvent(accountId, currentYear),
|
||||||
CreateRewindServiceClient("sphere").GetRewindEventAsync(rewindRequest).ResponseAsync
|
CreateRewindServiceClient("sphere").GetRewindEventAsync(rewindRequest).ResponseAsync,
|
||||||
|
CreateRewindServiceClient("messager").GetRewindEventAsync(rewindRequest).ResponseAsync
|
||||||
};
|
};
|
||||||
var rewindEvents = await Task.WhenAll(rewindEventTasks);
|
var rewindEvents = await Task.WhenAll(rewindEventTasks);
|
||||||
|
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ public class SnAccount : ModelBase
|
|||||||
public Guid? AutomatedId { get; set; }
|
public Guid? AutomatedId { get; set; }
|
||||||
|
|
||||||
public SnAccountProfile Profile { get; set; } = null!;
|
public SnAccountProfile Profile { get; set; } = null!;
|
||||||
public ICollection<SnAccountContact> Contacts { get; set; } = [];
|
public List<SnAccountContact> Contacts { get; set; } = [];
|
||||||
public ICollection<SnAccountBadge> Badges { get; set; } = [];
|
public List<SnAccountBadge> Badges { get; set; } = [];
|
||||||
|
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAccountAuthFactor> AuthFactors { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAccountAuthFactor> AuthFactors { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAccountConnection> Connections { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAccountConnection> Connections { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAuthSession> Sessions { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAuthSession> Sessions { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAuthChallenge> Challenges { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAuthChallenge> Challenges { get; set; } = [];
|
||||||
|
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAccountRelationship> OutgoingRelationships { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAccountRelationship> OutgoingRelationships { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnAccountRelationship> IncomingRelationships { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnAccountRelationship> IncomingRelationships { get; set; } = [];
|
||||||
|
|
||||||
[NotMapped] public SnSubscriptionReferenceObject? PerkSubscription { get; set; }
|
[NotMapped] public SnSubscriptionReferenceObject? PerkSubscription { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public class SnCheckInResult : ModelBase
|
|||||||
public int? RewardExperience { get; set; }
|
public int? RewardExperience { get; set; }
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")]
|
[Column(TypeName = "jsonb")]
|
||||||
public ICollection<CheckInFortuneTip> Tips { get; set; } = new List<CheckInFortuneTip>();
|
public List<CheckInFortuneTip> Tips { get; set; } = new List<CheckInFortuneTip>();
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public SnAccount Account { get; set; } = null!;
|
public SnAccount Account { get; set; } = null!;
|
||||||
@@ -135,7 +135,7 @@ public class DailyEventResponse
|
|||||||
{
|
{
|
||||||
public Instant Date { get; set; }
|
public Instant Date { get; set; }
|
||||||
public SnCheckInResult? CheckInResult { get; set; }
|
public SnCheckInResult? CheckInResult { get; set; }
|
||||||
public ICollection<SnAccountStatus> Statuses { get; set; } = new List<SnAccountStatus>();
|
public List<SnAccountStatus> Statuses { get; set; } = new List<SnAccountStatus>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PresenceType
|
public enum PresenceType
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class SnChatMessage : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Attachments { get; set; } = [];
|
||||||
|
|
||||||
public ICollection<SnChatMessageReaction> Reactions { get; set; } = new List<SnChatMessageReaction>();
|
public List<SnChatReaction> Reactions { get; set; } = new();
|
||||||
|
|
||||||
public Guid? RepliedMessageId { get; set; }
|
public Guid? RepliedMessageId { get; set; }
|
||||||
public SnChatMessage? RepliedMessage { get; set; }
|
public SnChatMessage? RepliedMessage { get; set; }
|
||||||
@@ -66,7 +66,7 @@ public enum MessageReactionAttitude
|
|||||||
Negative,
|
Negative,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnChatMessageReaction : ModelBase
|
public class SnChatReaction : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public Guid MessageId { get; set; }
|
public Guid MessageId { get; set; }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class SnChatRoom : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
[IgnoreMember]
|
[IgnoreMember]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ICollection<SnChatMember> Members { get; set; } = new List<SnChatMember>();
|
public List<SnChatMember> Members { get; set; } = new List<SnChatMember>();
|
||||||
|
|
||||||
public Guid? AccountId { get; set; }
|
public Guid? AccountId { get; set; }
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ public class SnChatRoom : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
[JsonPropertyName("members")]
|
[JsonPropertyName("members")]
|
||||||
public ICollection<ChatMemberTransmissionObject> DirectMembers { get; set; } =
|
public List<ChatMemberTransmissionObject> DirectMembers { get; set; } =
|
||||||
new List<ChatMemberTransmissionObject>();
|
new List<ChatMemberTransmissionObject>();
|
||||||
|
|
||||||
public string ResourceIdentifier => $"chatroom:{Id}";
|
public string ResourceIdentifier => $"chatroom:{Id}";
|
||||||
@@ -81,30 +81,22 @@ public class SnChatMember : ModelBase
|
|||||||
public SnChatRoom ChatRoom { get; set; } = null!;
|
public SnChatRoom ChatRoom { get; set; } = null!;
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped] public SnAccount? Account { get; set; }
|
||||||
public SnAccount? Account { get; set; }
|
[NotMapped] public SnAccountStatus? Status { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[MaxLength(1024)] public string? Nick { get; set; }
|
||||||
public SnAccountStatus? Status { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(1024)]
|
|
||||||
public string? Nick { get; set; }
|
|
||||||
|
|
||||||
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
|
public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All;
|
||||||
public Instant? LastReadAt { get; set; }
|
public Instant? LastReadAt { get; set; }
|
||||||
public Instant? JoinedAt { get; set; }
|
public Instant? JoinedAt { get; set; }
|
||||||
public Instant? LeaveAt { get; set; }
|
public Instant? LeaveAt { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public List<SnChatMessage> Messages { get; set; } = [];
|
||||||
|
[JsonIgnore] public List<SnChatReaction> Reactions { get; set; } = [];
|
||||||
|
|
||||||
public Guid? InvitedById { get; set; }
|
public Guid? InvitedById { get; set; }
|
||||||
public SnChatMember? InvitedBy { get; set; }
|
public SnChatMember? InvitedBy { get; set; }
|
||||||
|
|
||||||
// Backwards support field
|
|
||||||
[NotMapped]
|
|
||||||
public int Role { get; } = 0;
|
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
public bool IsBot { get; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The break time is the user doesn't receive any message from this member for a while.
|
/// The break time is the user doesn't receive any message from this member for a while.
|
||||||
/// Expect mentioned him or her.
|
/// Expect mentioned him or her.
|
||||||
@@ -147,13 +139,6 @@ public class ChatMemberTransmissionObject : ModelBase
|
|||||||
public Instant? TimeoutUntil { get; set; }
|
public Instant? TimeoutUntil { get; set; }
|
||||||
public ChatTimeoutCause? TimeoutCause { get; set; }
|
public ChatTimeoutCause? TimeoutCause { get; set; }
|
||||||
|
|
||||||
// Backwards support field
|
|
||||||
[NotMapped]
|
|
||||||
public int Role { get; } = 0;
|
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
public bool IsBot { get; } = false;
|
|
||||||
|
|
||||||
public static ChatMemberTransmissionObject FromEntity(SnChatMember member)
|
public static ChatMemberTransmissionObject FromEntity(SnChatMember member)
|
||||||
{
|
{
|
||||||
return new ChatMemberTransmissionObject
|
return new ChatMemberTransmissionObject
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class SnCloudFile : ModelBase, ICloudFile, IIdentifiedResource
|
|||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? FastUploadLink { get; set; }
|
public string? FastUploadLink { get; set; }
|
||||||
|
|
||||||
public ICollection<SnCloudFileReference> References { get; set; } = new List<SnCloudFileReference>();
|
public List<SnCloudFileReference> References { get; set; } = new List<SnCloudFileReference>();
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class SnCustomApp : ModelBase, IIdentifiedResource
|
|||||||
[Column(TypeName = "jsonb")] public SnCustomAppOauthConfig? OauthConfig { get; set; }
|
[Column(TypeName = "jsonb")] public SnCustomAppOauthConfig? OauthConfig { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public SnCustomAppLinks? Links { get; set; }
|
[Column(TypeName = "jsonb")] public SnCustomAppLinks? Links { get; set; }
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<SnCustomAppSecret> Secrets { get; set; } = new List<SnCustomAppSecret>();
|
[JsonIgnore] public List<SnCustomAppSecret> Secrets { get; set; } = new List<SnCustomAppSecret>();
|
||||||
|
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
public SnDevProject Project { get; set; } = null!;
|
public SnDevProject Project { get; set; } = null!;
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ public class SnFediverseActor : ModelBase
|
|||||||
public Guid InstanceId { get; set; }
|
public Guid InstanceId { get; set; }
|
||||||
public SnFediverseInstance Instance { get; set; } = null!;
|
public SnFediverseInstance Instance { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<SnFediverseRelationship> FollowingRelationships { get; set; } = [];
|
[JsonIgnore] public List<SnFediverseRelationship> FollowingRelationships { get; set; } = [];
|
||||||
[JsonIgnore] public ICollection<SnFediverseRelationship> FollowerRelationships { get; set; } = [];
|
[JsonIgnore] public List<SnFediverseRelationship> FollowerRelationships { get; set; } = [];
|
||||||
|
|
||||||
public Instant? LastFetchedAt { get; set; }
|
public Instant? LastFetchedAt { get; set; }
|
||||||
public Instant? LastActivityAt { get; set; }
|
public Instant? LastActivityAt { get; set; }
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class SnFediverseInstance : ModelBase
|
|||||||
public bool IsSilenced { get; set; } = false;
|
public bool IsSilenced { get; set; } = false;
|
||||||
|
|
||||||
[MaxLength(2048)] public string? BlockReason { get; set; }
|
[MaxLength(2048)] public string? BlockReason { get; set; }
|
||||||
[JsonIgnore] public ICollection<SnFediverseActor> Actors { get; set; } = [];
|
[JsonIgnore] public List<SnFediverseActor> Actors { get; set; } = [];
|
||||||
|
|
||||||
public Instant? LastFetchedAt { get; set; }
|
public Instant? LastFetchedAt { get; set; }
|
||||||
public Instant? LastActivityAt { get; set; }
|
public Instant? LastActivityAt { get; set; }
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ public class SnPermissionGroup : ModelBase
|
|||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
[MaxLength(1024)] public string Key { get; set; } = null!;
|
[MaxLength(1024)] public string Key { get; set; } = null!;
|
||||||
|
|
||||||
public ICollection<SnPermissionNode> Nodes { get; set; } = [];
|
public List<SnPermissionNode> Nodes { get; set; } = [];
|
||||||
[JsonIgnore] public ICollection<SnPermissionGroupMember> Members { get; set; } = [];
|
[JsonIgnore] public List<SnPermissionGroupMember> Members { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnPermissionGroupMember : ModelBase
|
public class SnPermissionGroupMember : ModelBase
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Shared.Models;
|
||||||
@@ -19,6 +21,60 @@ public class SnPoll : ModelBase
|
|||||||
|
|
||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
[JsonIgnore] public SnPublisher? Publisher { get; set; }
|
[JsonIgnore] public SnPublisher? Publisher { get; set; }
|
||||||
|
|
||||||
|
public Poll ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new Poll
|
||||||
|
{
|
||||||
|
Id = Id.ToString(),
|
||||||
|
IsAnonymous = IsAnonymous,
|
||||||
|
PublisherId = PublisherId.ToString(),
|
||||||
|
Publisher = Publisher?.ToProtoValue(),
|
||||||
|
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Title != null)
|
||||||
|
proto.Title = Title;
|
||||||
|
|
||||||
|
if (Description != null)
|
||||||
|
proto.Description = Description;
|
||||||
|
|
||||||
|
if (EndedAt.HasValue)
|
||||||
|
proto.EndedAt = Timestamp.FromDateTimeOffset(EndedAt.Value.ToDateTimeOffset());
|
||||||
|
|
||||||
|
proto.Questions.AddRange(Questions.Select(q => q.ToProtoValue()));
|
||||||
|
|
||||||
|
if (DeletedAt.HasValue)
|
||||||
|
proto.DeletedAt = Timestamp.FromDateTimeOffset(DeletedAt.Value.ToDateTimeOffset());
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnPoll FromProtoValue(Poll proto)
|
||||||
|
{
|
||||||
|
var poll = new SnPoll
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Title = proto.Title != null ? proto.Title : null,
|
||||||
|
Description = proto.Description != null ? proto.Description : null,
|
||||||
|
IsAnonymous = proto.IsAnonymous,
|
||||||
|
PublisherId = Guid.Parse(proto.PublisherId),
|
||||||
|
Publisher = proto.Publisher != null ? SnPublisher.FromProtoValue(proto.Publisher) : null,
|
||||||
|
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proto.EndedAt != null)
|
||||||
|
poll.EndedAt = Instant.FromDateTimeOffset(proto.EndedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
poll.Questions.AddRange(proto.Questions.Select(SnPollQuestion.FromProtoValue));
|
||||||
|
|
||||||
|
if (proto.DeletedAt != null)
|
||||||
|
poll.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
return poll;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PollQuestionType
|
public enum PollQuestionType
|
||||||
@@ -44,6 +100,46 @@ public class SnPollQuestion : ModelBase
|
|||||||
|
|
||||||
public Guid PollId { get; set; }
|
public Guid PollId { get; set; }
|
||||||
[JsonIgnore] public SnPoll Poll { get; set; } = null!;
|
[JsonIgnore] public SnPoll Poll { get; set; } = null!;
|
||||||
|
|
||||||
|
public PollQuestion ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new PollQuestion
|
||||||
|
{
|
||||||
|
Id = Id.ToString(),
|
||||||
|
Type = (Proto.PollQuestionType)((int)Type + 1),
|
||||||
|
Title = Title,
|
||||||
|
Order = Order,
|
||||||
|
IsRequired = IsRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Description != null)
|
||||||
|
proto.Description = Description;
|
||||||
|
|
||||||
|
if (Options != null)
|
||||||
|
proto.Options.AddRange(Options.Select(o => o.ToProtoValue()));
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnPollQuestion FromProtoValue(PollQuestion proto)
|
||||||
|
{
|
||||||
|
var question = new SnPollQuestion
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Type = (PollQuestionType)((int)proto.Type - 1),
|
||||||
|
Title = proto.Title,
|
||||||
|
Order = proto.Order,
|
||||||
|
IsRequired = proto.IsRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proto.Description != null)
|
||||||
|
question.Description = proto.Description;
|
||||||
|
|
||||||
|
if (proto.Options.Count > 0)
|
||||||
|
question.Options = proto.Options.Select(SnPollOption.FromProtoValue).ToList();
|
||||||
|
|
||||||
|
return question;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnPollOption
|
public class SnPollOption
|
||||||
@@ -52,6 +148,32 @@ public class SnPollOption
|
|||||||
[Required][MaxLength(1024)] public string Label { get; set; } = null!;
|
[Required][MaxLength(1024)] public string Label { get; set; } = null!;
|
||||||
[MaxLength(4096)] public string? Description { get; set; }
|
[MaxLength(4096)] public string? Description { get; set; }
|
||||||
public int Order { get; set; } = 0;
|
public int Order { get; set; } = 0;
|
||||||
|
|
||||||
|
public PollOption ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new PollOption
|
||||||
|
{
|
||||||
|
Id = Id.ToString(),
|
||||||
|
Label = Label,
|
||||||
|
Order = Order,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Description != null)
|
||||||
|
proto.Description = Description;
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnPollOption FromProtoValue(PollOption proto)
|
||||||
|
{
|
||||||
|
return new SnPollOption
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Label = proto.Label,
|
||||||
|
Description = proto.Description != null ? proto.Description : null,
|
||||||
|
Order = proto.Order,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnPollAnswer : ModelBase
|
public class SnPollAnswer : ModelBase
|
||||||
@@ -63,4 +185,40 @@ public class SnPollAnswer : ModelBase
|
|||||||
public Guid PollId { get; set; }
|
public Guid PollId { get; set; }
|
||||||
[JsonIgnore] public SnPoll? Poll { get; set; }
|
[JsonIgnore] public SnPoll? Poll { get; set; }
|
||||||
[NotMapped] public SnAccount? Account { get; set; }
|
[NotMapped] public SnAccount? Account { get; set; }
|
||||||
|
|
||||||
|
public PollAnswer ToProtoValue()
|
||||||
|
{
|
||||||
|
var proto = new PollAnswer
|
||||||
|
{
|
||||||
|
Id = Id.ToString(),
|
||||||
|
Answer = GrpcTypeHelper.ConvertObjectToByteString(Answer),
|
||||||
|
AccountId = AccountId.ToString(),
|
||||||
|
PollId = PollId.ToString(),
|
||||||
|
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (DeletedAt.HasValue)
|
||||||
|
proto.DeletedAt = Timestamp.FromDateTimeOffset(DeletedAt.Value.ToDateTimeOffset());
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SnPollAnswer FromProtoValue(PollAnswer proto)
|
||||||
|
{
|
||||||
|
var answer = new SnPollAnswer
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Answer = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, JsonElement>>(proto.Answer),
|
||||||
|
AccountId = Guid.Parse(proto.AccountId),
|
||||||
|
PollId = Guid.Parse(proto.PollId),
|
||||||
|
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proto.DeletedAt != null)
|
||||||
|
answer.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
|||||||
[MaxLength(8192)] [JsonIgnore] public string? PrivateKeyPem { get; set; }
|
[MaxLength(8192)] [JsonIgnore] public string? PrivateKeyPem { get; set; }
|
||||||
[MaxLength(8192)] public string? PublicKeyPem { get; set; }
|
[MaxLength(8192)] public string? PublicKeyPem { get; set; }
|
||||||
|
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnPost> Posts { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnPost> Posts { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnPoll> Polls { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnPoll> Polls { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnPostCollection> Collections { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnPublisherMember> Members { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnPublisherMember> Members { get; set; } = [];
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnPublisherFeature> Features { get; set; } = [];
|
[IgnoreMember] [JsonIgnore] public List<SnPublisherFeature> Features { get; set; } = [];
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ICollection<SnPublisherSubscription> Subscriptions { get; set; } = [];
|
public List<SnPublisherSubscription> Subscriptions { get; set; } = [];
|
||||||
|
|
||||||
public Guid? AccountId { get; set; }
|
public Guid? AccountId { get; set; }
|
||||||
public Guid? RealmId { get; set; }
|
public Guid? RealmId { get; set; }
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class SnRealm : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
|
[Column(TypeName = "jsonb")] public SnVerificationMark? Verification { get; set; }
|
||||||
|
|
||||||
[IgnoreMember] [JsonIgnore] public ICollection<SnRealmMember> Members { get; set; } = new List<SnRealmMember>();
|
[IgnoreMember] [JsonIgnore] public List<SnRealmMember> Members { get; set; } = new List<SnRealmMember>();
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class SnWallet : ModelBase
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
public ICollection<SnWalletPocket> Pockets { get; set; } = new List<SnWalletPocket>();
|
public List<SnWalletPocket> Pockets { get; set; } = new List<SnWalletPocket>();
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public SnAccount Account { get; set; } = null!;
|
public SnAccount Account { get; set; } = null!;
|
||||||
@@ -98,7 +98,7 @@ public class SnWalletFund : ModelBase
|
|||||||
public SnAccount CreatorAccount { get; set; } = null!;
|
public SnAccount CreatorAccount { get; set; } = null!;
|
||||||
|
|
||||||
// Recipients
|
// Recipients
|
||||||
public ICollection<SnWalletFundRecipient> Recipients { get; set; } = new List<SnWalletFundRecipient>();
|
public List<SnWalletFundRecipient> Recipients { get; set; } = new List<SnWalletFundRecipient>();
|
||||||
|
|
||||||
// Expiration
|
// Expiration
|
||||||
public Instant ExpiredAt { get; set; }
|
public Instant ExpiredAt { get; set; }
|
||||||
|
|||||||
133
DysonNetwork.Shared/Proto/poll.proto
Normal file
133
DysonNetwork.Shared/Proto/poll.proto
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package proto;
|
||||||
|
|
||||||
|
option csharp_namespace = "DysonNetwork.Shared.Proto";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "google/protobuf/wrappers.proto";
|
||||||
|
import "publisher.proto";
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum PollQuestionType {
|
||||||
|
POLL_QUESTION_TYPE_UNSPECIFIED = 0;
|
||||||
|
SINGLE_CHOICE = 1;
|
||||||
|
MULTIPLE_CHOICE = 2;
|
||||||
|
YES_NO = 3;
|
||||||
|
RATING = 4;
|
||||||
|
FREE_TEXT = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
|
||||||
|
message PollOption {
|
||||||
|
string id = 1;
|
||||||
|
string label = 2;
|
||||||
|
google.protobuf.StringValue description = 3;
|
||||||
|
int32 order = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PollQuestion {
|
||||||
|
string id = 1;
|
||||||
|
PollQuestionType type = 2;
|
||||||
|
repeated PollOption options = 3;
|
||||||
|
string title = 4;
|
||||||
|
google.protobuf.StringValue description = 5;
|
||||||
|
int32 order = 6;
|
||||||
|
bool is_required = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Poll {
|
||||||
|
string id = 1;
|
||||||
|
google.protobuf.StringValue title = 2;
|
||||||
|
google.protobuf.StringValue description = 3;
|
||||||
|
optional google.protobuf.Timestamp ended_at = 4;
|
||||||
|
bool is_anonymous = 5;
|
||||||
|
|
||||||
|
string publisher_id = 6;
|
||||||
|
optional Publisher publisher = 7;
|
||||||
|
|
||||||
|
repeated PollQuestion questions = 8;
|
||||||
|
|
||||||
|
google.protobuf.Timestamp created_at = 9;
|
||||||
|
google.protobuf.Timestamp updated_at = 10;
|
||||||
|
optional google.protobuf.Timestamp deleted_at = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PollAnswer {
|
||||||
|
string id = 1;
|
||||||
|
bytes answer = 2; // Dictionary<string, JsonElement>
|
||||||
|
|
||||||
|
string account_id = 3;
|
||||||
|
string poll_id = 4;
|
||||||
|
|
||||||
|
google.protobuf.Timestamp created_at = 5;
|
||||||
|
google.protobuf.Timestamp updated_at = 6;
|
||||||
|
optional google.protobuf.Timestamp deleted_at = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// Request/Response Messages
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
message GetPollRequest {
|
||||||
|
oneof identifier {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollBatchRequest {
|
||||||
|
repeated string ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollBatchResponse {
|
||||||
|
repeated Poll polls = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPollsRequest {
|
||||||
|
google.protobuf.StringValue publisher_id = 1;
|
||||||
|
int32 page_size = 2;
|
||||||
|
string page_token = 3;
|
||||||
|
google.protobuf.StringValue order_by = 4;
|
||||||
|
bool order_desc = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPollsResponse {
|
||||||
|
repeated Poll polls = 1;
|
||||||
|
string next_page_token = 2;
|
||||||
|
int32 total_size = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollAnswerRequest {
|
||||||
|
string poll_id = 1;
|
||||||
|
string account_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollStatsRequest {
|
||||||
|
string poll_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollStatsResponse {
|
||||||
|
map<string, string> stats = 1; // Question ID -> JSON string of stats (option_id -> count)
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollQuestionStatsRequest {
|
||||||
|
string question_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPollQuestionStatsResponse {
|
||||||
|
map<string, int32> stats = 1; // Option ID -> count
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// Service Definitions
|
||||||
|
// ====================================
|
||||||
|
|
||||||
|
service PollService {
|
||||||
|
rpc GetPoll(GetPollRequest) returns (Poll);
|
||||||
|
rpc GetPollBatch(GetPollBatchRequest) returns (GetPollBatchResponse);
|
||||||
|
rpc ListPolls(ListPollsRequest) returns (ListPollsResponse);
|
||||||
|
rpc GetPollAnswer(GetPollAnswerRequest) returns (PollAnswer);
|
||||||
|
rpc GetPollStats(GetPollStatsRequest) returns (GetPollStatsResponse);
|
||||||
|
rpc GetPollQuestionStats(GetPollQuestionStatsRequest) returns (GetPollQuestionStatsResponse);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import "google/protobuf/wrappers.proto";
|
|||||||
import "file.proto";
|
import "file.proto";
|
||||||
import "realm.proto";
|
import "realm.proto";
|
||||||
import "publisher.proto";
|
import "publisher.proto";
|
||||||
|
import "account.proto";
|
||||||
|
|
||||||
// Enums
|
// Enums
|
||||||
enum PostType {
|
enum PostType {
|
||||||
@@ -320,5 +321,3 @@ service PostService {
|
|||||||
// List posts with filters
|
// List posts with filters
|
||||||
rpc ListPosts(ListPostsRequest) returns (ListPostsResponse);
|
rpc ListPosts(ListPostsRequest) returns (ListPostsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
import 'account.proto';
|
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ public static class ServiceInjectionHelper
|
|||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddGrpcClient<PollService.PollServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
|
);
|
||||||
services.AddSingleton<RemotePublisherService>();
|
services.AddSingleton<RemotePublisherService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class AppDatabase(
|
|||||||
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
public DbSet<SnChatMember> ChatMembers { get; set; } = null!;
|
||||||
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
public DbSet<SnChatMessage> ChatMessages { get; set; } = null!;
|
||||||
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
public DbSet<SnRealtimeCall> ChatRealtimeCall { get; set; } = null!;
|
||||||
public DbSet<SnChatMessageReaction> ChatReactions { get; set; } = null!;
|
public DbSet<SnChatReaction> ChatReactions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<SnSticker> Stickers { get; set; } = null!;
|
public DbSet<SnSticker> Stickers { get; set; } = null!;
|
||||||
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
|
public DbSet<StickerPack> StickerPacks { get; set; } = null!;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using PollQuestionType = DysonNetwork.Shared.Models.PollQuestionType;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Poll;
|
namespace DysonNetwork.Sphere.Poll;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ namespace DysonNetwork.Sphere.Poll;
|
|||||||
[Route("/api/polls")]
|
[Route("/api/polls")]
|
||||||
public class PollController(
|
public class PollController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PollService polls,
|
Poll.PollService polls,
|
||||||
Publisher.PublisherService pub,
|
Publisher.PublisherService pub,
|
||||||
RemoteAccountService remoteAccountsHelper
|
RemoteAccountService remoteAccountsHelper
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
|
|||||||
158
DysonNetwork.Sphere/Poll/PollServiceGrpc.cs
Normal file
158
DysonNetwork.Sphere/Poll/PollServiceGrpc.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.Json;
|
||||||
|
using PollQuestionType = DysonNetwork.Shared.Proto.PollQuestionType;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Poll;
|
||||||
|
|
||||||
|
public class PollServiceGrpc(AppDatabase db, PollService ps) : Shared.Proto.PollService.PollServiceBase
|
||||||
|
{
|
||||||
|
public override async Task<Shared.Proto.Poll> GetPoll(GetPollRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(request.Id, out var id))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid poll id"));
|
||||||
|
|
||||||
|
var poll = await db.Polls
|
||||||
|
.Include(p => p.Publisher)
|
||||||
|
.Include(p => p.Questions)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
|
|
||||||
|
if (poll == null) throw new RpcException(new Status(StatusCode.NotFound, "poll not found"));
|
||||||
|
|
||||||
|
return poll.ToProtoValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetPollBatchResponse> GetPollBatch(GetPollBatchRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var ids = request.Ids
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out _))
|
||||||
|
.Select(Guid.Parse)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ids.Count == 0) return new GetPollBatchResponse();
|
||||||
|
|
||||||
|
var polls = await db.Polls
|
||||||
|
.Include(p => p.Publisher)
|
||||||
|
.Include(p => p.Questions)
|
||||||
|
.Where(p => ids.Contains(p.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var resp = new GetPollBatchResponse();
|
||||||
|
resp.Polls.AddRange(polls.Select(p => p.ToProtoValue()));
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ListPollsResponse> ListPolls(ListPollsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var query = db.Polls
|
||||||
|
.Include(p => p.Publisher)
|
||||||
|
.Include(p => p.Questions)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
|
||||||
|
query = query.Where(p => p.PublisherId == pid);
|
||||||
|
|
||||||
|
var totalSize = await query.CountAsync();
|
||||||
|
|
||||||
|
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
|
||||||
|
var pageToken = request.PageToken;
|
||||||
|
var offset = string.IsNullOrEmpty(pageToken) ? 0 : int.Parse(pageToken);
|
||||||
|
|
||||||
|
IOrderedQueryable<SnPoll> orderedQuery;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.OrderBy))
|
||||||
|
{
|
||||||
|
switch (request.OrderBy)
|
||||||
|
{
|
||||||
|
case "title":
|
||||||
|
orderedQuery = request.OrderDesc
|
||||||
|
? query.OrderByDescending(q => q.Title ?? string.Empty)
|
||||||
|
: query.OrderBy(q => q.Title ?? string.Empty);
|
||||||
|
break;
|
||||||
|
case "ended_at":
|
||||||
|
orderedQuery = request.OrderDesc
|
||||||
|
? query.OrderByDescending(q => q.EndedAt)
|
||||||
|
: query.OrderBy(q => q.EndedAt);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
orderedQuery = request.OrderDesc
|
||||||
|
? query.OrderByDescending(q => q.CreatedAt)
|
||||||
|
: query.OrderBy(q => q.CreatedAt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
orderedQuery = request.OrderDesc
|
||||||
|
? query.OrderByDescending(q => q.CreatedAt)
|
||||||
|
: query.OrderBy(q => q.CreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var polls = await orderedQuery
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var nextToken = offset + pageSize < totalSize ? (offset + pageSize).ToString() : string.Empty;
|
||||||
|
|
||||||
|
var resp = new ListPollsResponse();
|
||||||
|
resp.Polls.AddRange(polls.Select(p => p.ToProtoValue()));
|
||||||
|
resp.NextPageToken = nextToken;
|
||||||
|
resp.TotalSize = totalSize;
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PollAnswer> GetPollAnswer(GetPollAnswerRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(request.PollId, out var pollId))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid poll id"));
|
||||||
|
|
||||||
|
if (!Guid.TryParse(request.AccountId, out var accountId))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid account id"));
|
||||||
|
|
||||||
|
var answer = await ps.GetPollAnswer(pollId, accountId);
|
||||||
|
|
||||||
|
if (answer == null)
|
||||||
|
throw new RpcException(new Status(StatusCode.NotFound, "answer not found"));
|
||||||
|
|
||||||
|
return answer.ToProtoValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetPollStatsResponse> GetPollStats(GetPollStatsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(request.PollId, out var pollId))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid poll id"));
|
||||||
|
|
||||||
|
var stats = await ps.GetPollStats(pollId);
|
||||||
|
|
||||||
|
var resp = new GetPollStatsResponse();
|
||||||
|
foreach (var stat in stats)
|
||||||
|
{
|
||||||
|
var statsJson = JsonSerializer.Serialize(stat.Value);
|
||||||
|
resp.Stats[stat.Key.ToString()] = statsJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetPollQuestionStatsResponse> GetPollQuestionStats(GetPollQuestionStatsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(request.QuestionId, out var questionId))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid question id"));
|
||||||
|
|
||||||
|
var stats = await ps.GetPollQuestionStats(questionId);
|
||||||
|
|
||||||
|
var resp = new GetPollQuestionStatsResponse();
|
||||||
|
foreach (var stat in stats)
|
||||||
|
{
|
||||||
|
resp.Stats[stat.Key] = stat.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ using Swashbuckle.AspNetCore.Annotations;
|
|||||||
using PostType = DysonNetwork.Shared.Models.PostType;
|
using PostType = DysonNetwork.Shared.Models.PostType;
|
||||||
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
|
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
|
||||||
using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService;
|
using PublisherService = DysonNetwork.Sphere.Publisher.PublisherService;
|
||||||
|
using PollsService = DysonNetwork.Sphere.Poll.PollService;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Post;
|
namespace DysonNetwork.Sphere.Post;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ public class PostActionController(
|
|||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
ActionLogService.ActionLogServiceClient als,
|
ActionLogService.ActionLogServiceClient als,
|
||||||
PaymentService.PaymentServiceClient payments,
|
PaymentService.PaymentServiceClient payments,
|
||||||
PollService polls,
|
PollsService polls,
|
||||||
RemoteRealmService rs
|
RemoteRealmService rs
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System.Globalization;
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Sphere.Chat;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using JiebaNet.Segmenter;
|
using JiebaNet.Segmenter;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -14,7 +13,6 @@ namespace DysonNetwork.Sphere.Rewind;
|
|||||||
public class SphereRewindServiceGrpc(
|
public class SphereRewindServiceGrpc(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RemoteAccountService remoteAccounts,
|
RemoteAccountService remoteAccounts,
|
||||||
ChatRoomService crs,
|
|
||||||
Publisher.PublisherService ps
|
Publisher.PublisherService ps
|
||||||
) : RewindService.RewindServiceBase
|
) : RewindService.RewindServiceBase
|
||||||
{
|
{
|
||||||
@@ -101,116 +99,6 @@ public class SphereRewindServiceGrpc(
|
|||||||
.Take(100)
|
.Take(100)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Chat data
|
|
||||||
var messagesQuery = db
|
|
||||||
.ChatMessages.Include(m => m.Sender)
|
|
||||||
.Include(m => m.ChatRoom)
|
|
||||||
.Where(m => m.CreatedAt >= startDate && m.CreatedAt < endDate)
|
|
||||||
.Where(m => m.Sender.AccountId == accountId)
|
|
||||||
.AsQueryable();
|
|
||||||
var mostMessagedChatInfo = await messagesQuery
|
|
||||||
.Where(m => m.ChatRoom.Type == ChatRoomType.Group)
|
|
||||||
.GroupBy(m => m.ChatRoomId)
|
|
||||||
.OrderByDescending(g => g.Count())
|
|
||||||
.Select(g => new { ChatRoom = g.First().ChatRoom, MessageCount = g.Count() })
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
var mostMessagedChat = mostMessagedChatInfo?.ChatRoom;
|
|
||||||
var mostMessagedDirectChatInfo = await messagesQuery
|
|
||||||
.Where(m => m.ChatRoom.Type == ChatRoomType.DirectMessage)
|
|
||||||
.GroupBy(m => m.ChatRoomId)
|
|
||||||
.OrderByDescending(g => g.Count())
|
|
||||||
.Select(g => new { ChatRoom = g.First().ChatRoom, MessageCount = g.Count() })
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
var mostMessagedDirectChat = mostMessagedDirectChatInfo is not null
|
|
||||||
? await crs.LoadDirectMessageMembers(mostMessagedDirectChatInfo.ChatRoom, accountId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Call data
|
|
||||||
var callQuery = db
|
|
||||||
.ChatRealtimeCall.Include(c => c.Sender)
|
|
||||||
.Include(c => c.Room)
|
|
||||||
.Where(c => c.CreatedAt >= startDate && c.CreatedAt < endDate)
|
|
||||||
.Where(c => c.Sender.AccountId == accountId)
|
|
||||||
.AsQueryable();
|
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
var groupCallRecords = await callQuery
|
|
||||||
.Where(c => c.Room.Type == ChatRoomType.Group)
|
|
||||||
.Select(c => new
|
|
||||||
{
|
|
||||||
c.RoomId,
|
|
||||||
c.CreatedAt,
|
|
||||||
c.EndedAt,
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
var callDurations = groupCallRecords
|
|
||||||
.Select(c => new { c.RoomId, Duration = (c.EndedAt ?? now).Minus(c.CreatedAt).Seconds })
|
|
||||||
.ToList();
|
|
||||||
var mostCalledRoomInfo = callDurations
|
|
||||||
.GroupBy(c => c.RoomId)
|
|
||||||
.Select(g => new { RoomId = g.Key, TotalDuration = g.Sum(c => c.Duration) })
|
|
||||||
.OrderByDescending(g => g.TotalDuration)
|
|
||||||
.FirstOrDefault();
|
|
||||||
var mostCalledRoom =
|
|
||||||
mostCalledRoomInfo != null && mostCalledRoomInfo.RoomId != Guid.Empty
|
|
||||||
? await db.ChatRooms.FindAsync(mostCalledRoomInfo.RoomId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
List<SnAccount>? mostCalledChatTopMembers = null;
|
|
||||||
if (mostCalledRoom != null)
|
|
||||||
mostCalledChatTopMembers = await crs.GetTopActiveMembers(
|
|
||||||
mostCalledRoom.Id,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
|
|
||||||
var directCallRecords = await callQuery
|
|
||||||
.Where(c => c.Room.Type == ChatRoomType.DirectMessage)
|
|
||||||
.Select(c => new
|
|
||||||
{
|
|
||||||
c.RoomId,
|
|
||||||
c.CreatedAt,
|
|
||||||
c.EndedAt,
|
|
||||||
c.Room,
|
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
var directCallDurations = directCallRecords
|
|
||||||
.Select(c => new
|
|
||||||
{
|
|
||||||
c.RoomId,
|
|
||||||
c.Room,
|
|
||||||
Duration = (c.EndedAt ?? now).Minus(c.CreatedAt).Seconds,
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
var mostCalledDirectRooms = directCallDurations
|
|
||||||
.GroupBy(c => c.RoomId)
|
|
||||||
.Select(g => new { ChatRoom = g.First().Room, TotalDuration = g.Sum(c => c.Duration) })
|
|
||||||
.OrderByDescending(g => g.TotalDuration)
|
|
||||||
.Take(3)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var accountIds = new List<Guid>();
|
|
||||||
foreach (var item in mostCalledDirectRooms)
|
|
||||||
{
|
|
||||||
var room = await crs.LoadDirectMessageMembers(item.ChatRoom, accountId);
|
|
||||||
var otherMember = room.DirectMembers.FirstOrDefault(m => m.AccountId != accountId);
|
|
||||||
if (otherMember != null)
|
|
||||||
accountIds.Add(otherMember.AccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var accounts = await remoteAccounts.GetAccountBatch(accountIds);
|
|
||||||
var mostCalledAccounts = accounts
|
|
||||||
.Zip(
|
|
||||||
mostCalledDirectRooms,
|
|
||||||
(account, room) =>
|
|
||||||
new Dictionary<string, object?>
|
|
||||||
{
|
|
||||||
["account"] = account,
|
|
||||||
["duration"] = room.TotalDuration,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var data = new Dictionary<string, object?>
|
var data = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["total_post_count"] = postTotalCount,
|
["total_post_count"] = postTotalCount,
|
||||||
@@ -244,27 +132,6 @@ public class SphereRewindServiceGrpc(
|
|||||||
["upvote_counts"] = mostLovedAudienceClue.ReactionCount,
|
["upvote_counts"] = mostLovedAudienceClue.ReactionCount,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
["most_messaged_chat"] = mostMessagedChatInfo is not null
|
|
||||||
? new Dictionary<string, object?>
|
|
||||||
{
|
|
||||||
["chat"] = mostMessagedChat,
|
|
||||||
["message_counts"] = mostMessagedChatInfo.MessageCount,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
["most_messaged_direct_chat"] = mostMessagedDirectChatInfo is not null
|
|
||||||
? new Dictionary<string, object?>
|
|
||||||
{
|
|
||||||
["chat"] = mostMessagedDirectChat,
|
|
||||||
["message_counts"] = mostMessagedDirectChatInfo.MessageCount,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
["most_called_chat"] = new Dictionary<string, object?>
|
|
||||||
{
|
|
||||||
["chat"] = mostCalledRoom,
|
|
||||||
["duration"] = mostCalledRoomInfo?.TotalDuration,
|
|
||||||
},
|
|
||||||
["most_called_chat_top_members"] = mostCalledChatTopMembers,
|
|
||||||
["most_called_accounts"] = mostCalledAccounts,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return new RewindEvent
|
return new RewindEvent
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
|
using DysonNetwork.Sphere.Poll;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using DysonNetwork.Sphere.Rewind;
|
using DysonNetwork.Sphere.Rewind;
|
||||||
@@ -23,6 +24,7 @@ public static class ApplicationConfiguration
|
|||||||
|
|
||||||
// Map gRPC services
|
// Map gRPC services
|
||||||
app.MapGrpcService<PostServiceGrpc>();
|
app.MapGrpcService<PostServiceGrpc>();
|
||||||
|
app.MapGrpcService<PollServiceGrpc>();
|
||||||
app.MapGrpcService<PublisherServiceGrpc>();
|
app.MapGrpcService<PublisherServiceGrpc>();
|
||||||
app.MapGrpcService<SphereRewindServiceGrpc>();
|
app.MapGrpcService<SphereRewindServiceGrpc>();
|
||||||
app.MapGrpcReflectionService();
|
app.MapGrpcReflectionService();
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Queue;
|
using DysonNetwork.Shared.Queue;
|
||||||
using DysonNetwork.Sphere.Chat;
|
|
||||||
using DysonNetwork.Sphere.Post;
|
|
||||||
using Google.Protobuf;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
using NATS.Client.JetStream.Models;
|
using NATS.Client.JetStream.Models;
|
||||||
@@ -39,10 +35,8 @@ public class BroadcastEventHandler(
|
|||||||
{
|
{
|
||||||
var paymentTask = HandlePaymentOrders(stoppingToken);
|
var paymentTask = HandlePaymentOrders(stoppingToken);
|
||||||
var accountTask = HandleAccountDeletions(stoppingToken);
|
var accountTask = HandleAccountDeletions(stoppingToken);
|
||||||
var websocketTask = HandleWebSocketPackets(stoppingToken);
|
|
||||||
var accountStatusTask = HandleAccountStatusUpdates(stoppingToken);
|
|
||||||
|
|
||||||
await Task.WhenAll(paymentTask, accountTask, websocketTask, accountStatusTask);
|
await Task.WhenAll(paymentTask, accountTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandlePaymentOrders(CancellationToken stoppingToken)
|
private async Task HandlePaymentOrders(CancellationToken stoppingToken)
|
||||||
@@ -94,6 +88,7 @@ public class BroadcastEventHandler(
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// ignore
|
// ignore
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,242 +164,6 @@ public class BroadcastEventHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleWebSocketPackets(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await foreach (var msg in nats.SubscribeAsync<byte[]>(
|
|
||||||
WebSocketPacketEvent.SubjectPrefix + "sphere", cancellationToken: stoppingToken))
|
|
||||||
{
|
|
||||||
logger.LogDebug("Handling websocket packet...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var evt = JsonSerializer.Deserialize<WebSocketPacketEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
|
|
||||||
if (evt == null) throw new ArgumentNullException(nameof(evt));
|
|
||||||
var packet = WebSocketPacket.FromBytes(evt.PacketBytes);
|
|
||||||
logger.LogInformation("Handling websocket packet... {Type}", packet.Type);
|
|
||||||
switch (packet.Type)
|
|
||||||
{
|
|
||||||
case "messages.read":
|
|
||||||
await HandleMessageRead(evt, packet);
|
|
||||||
break;
|
|
||||||
case "messages.typing":
|
|
||||||
await HandleMessageTyping(evt, packet);
|
|
||||||
break;
|
|
||||||
case "messages.subscribe":
|
|
||||||
await HandleMessageSubscribe(evt, packet);
|
|
||||||
break;
|
|
||||||
case "messages.unsubscribe":
|
|
||||||
await HandleMessageUnsubscribe(evt, packet);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error processing websocket packet");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleMessageRead(WebSocketPacketEvent evt, WebSocketPacket packet)
|
|
||||||
{
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
|
||||||
var cs = scope.ServiceProvider.GetRequiredService<ChatService>();
|
|
||||||
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
|
||||||
|
|
||||||
if (packet.Data == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "Mark message as read requires you to provide the ChatRoomId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData = packet.GetData<Chat.ChatController.MarkMessageReadRequest>();
|
|
||||||
if (requestData == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "Invalid request data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
|
||||||
if (sender == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await cs.ReadChatRoomAsync(requestData.ChatRoomId, evt.AccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleMessageTyping(WebSocketPacketEvent evt, WebSocketPacket packet)
|
|
||||||
{
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
|
||||||
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
|
||||||
|
|
||||||
if (packet.Data == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "messages.typing requires you to provide the ChatRoomId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
|
||||||
if (requestData == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "Invalid request data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
|
||||||
if (sender == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var responsePacket = new WebSocketPacket
|
|
||||||
{
|
|
||||||
Type = "messages.typing",
|
|
||||||
Data = new
|
|
||||||
{
|
|
||||||
room_id = sender.ChatRoomId,
|
|
||||||
sender_id = sender.Id,
|
|
||||||
sender
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Broadcast typing indicator to subscribed room members only
|
|
||||||
var subscribedMemberIds = await crs.GetSubscribedMembers(requestData.ChatRoomId);
|
|
||||||
var roomMembers = await crs.ListRoomMembers(requestData.ChatRoomId);
|
|
||||||
|
|
||||||
// Filter to subscribed members excluding the current user
|
|
||||||
var subscribedMembers = roomMembers
|
|
||||||
.Where(m => subscribedMemberIds.Contains(m.Id) && m.AccountId != evt.AccountId)
|
|
||||||
.Select(m => m.AccountId.ToString())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (subscribedMembers.Count > 0)
|
|
||||||
{
|
|
||||||
var respRequest = new PushWebSocketPacketToUsersRequest { Packet = responsePacket.ToProtoValue() };
|
|
||||||
respRequest.UserIds.AddRange(subscribedMembers);
|
|
||||||
await pusher.PushWebSocketPacketToUsersAsync(respRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleMessageSubscribe(WebSocketPacketEvent evt, WebSocketPacket packet)
|
|
||||||
{
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
|
||||||
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
|
||||||
|
|
||||||
if (packet.Data == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "messages.subscribe requires you to provide the ChatRoomId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
|
||||||
if (requestData == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "Invalid request data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
|
||||||
if (sender == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await crs.SubscribeChatRoom(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleMessageUnsubscribe(WebSocketPacketEvent evt, WebSocketPacket packet)
|
|
||||||
{
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
|
||||||
var crs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
|
||||||
|
|
||||||
if (packet.Data == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "messages.unsubscribe requires you to provide the ChatRoomId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData = packet.GetData<Chat.ChatController.ChatRoomWsUniversalRequest>();
|
|
||||||
if (requestData == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "Invalid request data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sender = await crs.GetRoomMember(evt.AccountId, requestData.ChatRoomId);
|
|
||||||
if (sender == null)
|
|
||||||
{
|
|
||||||
await SendErrorResponse(evt, "User is not a member of the chat room.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await crs.UnsubscribeChatRoom(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleAccountStatusUpdates(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountStatusUpdatedEvent.Type, cancellationToken: stoppingToken))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var evt = GrpcTypeHelper.ConvertByteStringToObject<AccountStatusUpdatedEvent>(ByteString.CopyFrom(msg.Data));
|
|
||||||
if (evt == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
logger.LogInformation("Account status updated: {AccountId}", evt.AccountId);
|
|
||||||
|
|
||||||
await using var scope = serviceProvider.CreateAsyncScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
|
||||||
var chatRoomService = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
|
||||||
|
|
||||||
// Get user's joined chat rooms
|
|
||||||
var userRooms = await db.ChatMembers
|
|
||||||
.Where(m => m.AccountId == evt.AccountId && m.JoinedAt != null && m.LeaveAt == null)
|
|
||||||
.Select(m => m.ChatRoomId)
|
|
||||||
.ToListAsync(cancellationToken: stoppingToken);
|
|
||||||
|
|
||||||
// Send WebSocket packet to subscribed users per room
|
|
||||||
foreach (var roomId in userRooms)
|
|
||||||
{
|
|
||||||
var members = await chatRoomService.ListRoomMembers(roomId);
|
|
||||||
var subscribedMemberIds = await chatRoomService.GetSubscribedMembers(roomId);
|
|
||||||
var subscribedUsers = members
|
|
||||||
.Where(m => subscribedMemberIds.Contains(m.Id))
|
|
||||||
.Select(m => m.AccountId.ToString())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (subscribedUsers.Count == 0) continue;
|
|
||||||
var packet = new WebSocketPacket
|
|
||||||
{
|
|
||||||
Type = "accounts.status.update",
|
|
||||||
Data = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["status"] = evt.Status,
|
|
||||||
["chat_room_id"] = roomId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var request = new PushWebSocketPacketToUsersRequest
|
|
||||||
{
|
|
||||||
Packet = packet.ToProtoValue()
|
|
||||||
};
|
|
||||||
request.UserIds.AddRange(subscribedUsers);
|
|
||||||
|
|
||||||
await pusher.PushWebSocketPacketToUsersAsync(request, cancellationToken: stoppingToken);
|
|
||||||
|
|
||||||
logger.LogInformation("Sent status update for room {roomId} to {count} subscribed users", roomId, subscribedUsers.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error processing AccountStatusUpdated");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendErrorResponse(WebSocketPacketEvent evt, string message)
|
private async Task SendErrorResponse(WebSocketPacketEvent evt, string message)
|
||||||
{
|
{
|
||||||
await pusher.PushWebSocketPacketToDeviceAsync(new PushWebSocketPacketToDeviceRequest
|
await pusher.PushWebSocketPacketToDeviceAsync(new PushWebSocketPacketToDeviceRequest
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ using DysonNetwork.Shared.Cache;
|
|||||||
using DysonNetwork.Shared.Geometry;
|
using DysonNetwork.Shared.Geometry;
|
||||||
using DysonNetwork.Sphere.ActivityPub;
|
using DysonNetwork.Sphere.ActivityPub;
|
||||||
using DysonNetwork.Sphere.Autocompletion;
|
using DysonNetwork.Sphere.Autocompletion;
|
||||||
using DysonNetwork.Sphere.Chat;
|
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
|
||||||
using DysonNetwork.Sphere.Discovery;
|
using DysonNetwork.Sphere.Discovery;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Poll;
|
using DysonNetwork.Sphere.Poll;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using DysonNetwork.Sphere.Sticker;
|
|
||||||
using DysonNetwork.Sphere.Timeline;
|
using DysonNetwork.Sphere.Timeline;
|
||||||
using DysonNetwork.Sphere.Translation;
|
using DysonNetwork.Sphere.Translation;
|
||||||
using DysonNetwork.Sphere.WebReader;
|
using DysonNetwork.Sphere.WebReader;
|
||||||
@@ -23,7 +20,9 @@ namespace DysonNetwork.Sphere.Startup;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
extension(IServiceCollection services)
|
||||||
|
{
|
||||||
|
public IServiceCollection AddAppServices()
|
||||||
{
|
{
|
||||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
|
||||||
@@ -70,13 +69,13 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
public IServiceCollection AddAppAuthentication()
|
||||||
{
|
{
|
||||||
services.AddAuthorization();
|
services.AddAuthorization();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services)
|
public IServiceCollection AddAppFlushHandlers()
|
||||||
{
|
{
|
||||||
services.AddSingleton<FlushBufferService>();
|
services.AddSingleton<FlushBufferService>();
|
||||||
services.AddScoped<PostViewFlushHandler>();
|
services.AddScoped<PostViewFlushHandler>();
|
||||||
@@ -84,9 +83,7 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddAppBusinessServices(
|
public IServiceCollection AddAppBusinessServices(IConfiguration configuration
|
||||||
this IServiceCollection services,
|
|
||||||
IConfiguration configuration
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
services.Configure<GeoOptions>(configuration.GetSection("GeoIP"));
|
services.Configure<GeoOptions>(configuration.GetSection("GeoIP"));
|
||||||
@@ -96,10 +93,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<PublisherSubscriptionService>();
|
services.AddScoped<PublisherSubscriptionService>();
|
||||||
services.AddScoped<TimelineService>();
|
services.AddScoped<TimelineService>();
|
||||||
services.AddScoped<PostService>();
|
services.AddScoped<PostService>();
|
||||||
services.AddScoped<ChatRoomService>();
|
|
||||||
services.AddScoped<ChatService>();
|
|
||||||
services.AddScoped<StickerService>();
|
|
||||||
services.AddScoped<IRealtimeService, LiveKitRealtimeService>();
|
|
||||||
services.AddScoped<WebReaderService>();
|
services.AddScoped<WebReaderService>();
|
||||||
services.AddScoped<WebFeedService>();
|
services.AddScoped<WebFeedService>();
|
||||||
services.AddScoped<DiscoveryService>();
|
services.AddScoped<DiscoveryService>();
|
||||||
@@ -124,3 +117,4 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class WebFeed : ModelBase
|
|||||||
public Guid PublisherId { get; set; }
|
public Guid PublisherId { get; set; }
|
||||||
public SnPublisher Publisher { get; set; } = null!;
|
public SnPublisher Publisher { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
[JsonIgnore] public List<WebArticle> Articles { get; set; } = new List<WebArticle>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WebFeedSubscription : ModelBase
|
public class WebFeedSubscription : ModelBase
|
||||||
|
|||||||
Reference in New Issue
Block a user