Merge pull request #84 from Moonlight-Panel/RewriteSupportChatBackend
Rewritten support chat backend. Added discord notifications
This commit is contained in:
@@ -45,6 +45,7 @@ public class DataContext : DbContext
|
||||
public DbSet<CloudPanel> CloudPanels { get; set; }
|
||||
public DbSet<MySqlDatabase> Databases { get; set; }
|
||||
public DbSet<WebSpace> WebSpaces { get; set; }
|
||||
public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
21
Moonlight/App/Database/Entities/SupportChatMessage.cs
Normal file
21
Moonlight/App/Database/Entities/SupportChatMessage.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Moonlight.App.Models.Misc;
|
||||
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
|
||||
public class SupportChatMessage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Content { get; set; } = "";
|
||||
public string Attachment { get; set; } = "";
|
||||
|
||||
public User? Sender { get; set; }
|
||||
public User Recipient { get; set; }
|
||||
|
||||
public bool IsQuestion { get; set; } = false;
|
||||
public QuestionType QuestionType { get; set; }
|
||||
public string Answer { get; set; } = "";
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
1087
Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs
generated
Normal file
1087
Moonlight/App/Database/Migrations/20230420213846_AddedNewSupportChatModels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedNewSupportChatModels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Websites");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PleskServers");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SupportChatMessages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Content = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Attachment = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SenderId = table.Column<int>(type: "int", nullable: true),
|
||||
RecipientId = table.Column<int>(type: "int", nullable: false),
|
||||
IsQuestion = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
QuestionType = table.Column<int>(type: "int", nullable: false),
|
||||
Answer = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SupportChatMessages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupportChatMessages_Users_RecipientId",
|
||||
column: x => x.RecipientId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SupportChatMessages_Users_SenderId",
|
||||
column: x => x.SenderId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupportChatMessages_RecipientId",
|
||||
table: "SupportChatMessages",
|
||||
column: "RecipientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupportChatMessages_SenderId",
|
||||
table: "SupportChatMessages",
|
||||
column: "SenderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SupportChatMessages");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PleskServers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
ApiKey = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ApiUrl = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PleskServers", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Websites",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
OwnerId = table.Column<int>(type: "int", nullable: false),
|
||||
PleskServerId = table.Column<int>(type: "int", nullable: false),
|
||||
BaseDomain = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
FtpLogin = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
FtpPassword = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PleskId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Websites", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Websites_PleskServers_PleskServerId",
|
||||
column: x => x.PleskServerId,
|
||||
principalTable: "PleskServers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Websites_Users_OwnerId",
|
||||
column: x => x.OwnerId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Websites_OwnerId",
|
||||
table: "Websites",
|
||||
column: "OwnerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Websites_PleskServerId",
|
||||
table: "Websites",
|
||||
column: "PleskServerId");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,29 +453,6 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.ToTable("NotificationClients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("ApiUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PleskServers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -673,6 +650,51 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.ToTable("Subscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Answer")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Attachment")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsQuestion")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int>("QuestionType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RecipientId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("SenderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RecipientId");
|
||||
|
||||
b.HasIndex("SenderId");
|
||||
|
||||
b.ToTable("SupportChatMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -836,42 +858,6 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.ToTable("WebSpaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("BaseDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("FtpLogin")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("FtpPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("OwnerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PleskId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PleskServerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("PleskServerId");
|
||||
|
||||
b.ToTable("Websites");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
||||
@@ -1007,6 +993,23 @@ namespace Moonlight.App.Database.Migrations
|
||||
.HasForeignKey("ServerId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
|
||||
.WithMany()
|
||||
.HasForeignKey("RecipientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
|
||||
.WithMany()
|
||||
.HasForeignKey("SenderId");
|
||||
|
||||
b.Navigation("Recipient");
|
||||
|
||||
b.Navigation("Sender");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
|
||||
@@ -1050,25 +1053,6 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer")
|
||||
.WithMany()
|
||||
.HasForeignKey("PleskServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
|
||||
b.Navigation("PleskServer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
@@ -80,7 +80,15 @@ public class EventSystem
|
||||
|
||||
var del = (Delegate)subscriber.Action;
|
||||
|
||||
try
|
||||
{
|
||||
((Task)del.DynamicInvoke(data)!).Wait();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn($"Error emitting '{subscriber.Id} on {subscriber.Handle}'");
|
||||
Logger.Warn(e);
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
|
||||
101
Moonlight/App/Services/DiscordNotificationService.cs
Normal file
101
Moonlight/App/Services/DiscordNotificationService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Discord;
|
||||
using Discord.Webhook;
|
||||
using Logging.Net;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
|
||||
namespace Moonlight.App.Services;
|
||||
|
||||
public class DiscordNotificationService
|
||||
{
|
||||
private readonly EventSystem Event;
|
||||
private readonly ResourceService ResourceService;
|
||||
private readonly DiscordWebhookClient Client;
|
||||
private readonly string AppUrl;
|
||||
|
||||
public DiscordNotificationService(
|
||||
EventSystem eventSystem,
|
||||
ConfigService configService,
|
||||
ResourceService resourceService)
|
||||
{
|
||||
Event = eventSystem;
|
||||
ResourceService = resourceService;
|
||||
|
||||
var config = configService.GetSection("Moonlight").GetSection("DiscordNotifications");
|
||||
|
||||
if (config.GetValue<bool>("Enable"))
|
||||
{
|
||||
Logger.Info("Discord notifications enabled");
|
||||
|
||||
Client = new(config.GetValue<string>("WebHook"));
|
||||
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
|
||||
|
||||
Event.On<User>("supportChat.new", this, OnNewSupportChat);
|
||||
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);
|
||||
Event.On<User>("supportChat.close", this, OnSupportChatClose);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info("Discord notifications disabled");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSupportChatClose(User user)
|
||||
{
|
||||
await SendNotification("", builder =>
|
||||
{
|
||||
builder.Title = "A new support chat has been marked as closed";
|
||||
builder.Color = Color.Red;
|
||||
builder.AddField("Email", user.Email);
|
||||
builder.AddField("Firstname", user.FirstName);
|
||||
builder.AddField("Lastname", user.LastName);
|
||||
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnSupportChatMessage(SupportChatMessage message)
|
||||
{
|
||||
if(message.Sender == null)
|
||||
return;
|
||||
|
||||
await SendNotification("", builder =>
|
||||
{
|
||||
builder.Title = "New message in support chat";
|
||||
builder.Color = Color.Blue;
|
||||
builder.AddField("Message", message.Content);
|
||||
builder.Author = new EmbedAuthorBuilder()
|
||||
.WithName($"{message.Sender.FirstName} {message.Sender.LastName}")
|
||||
.WithIconUrl(ResourceService.Avatar(message.Sender));
|
||||
builder.Url = $"{AppUrl}/admin/support/view/{message.Recipient.Id}";
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnNewSupportChat(User user)
|
||||
{
|
||||
await SendNotification("", builder =>
|
||||
{
|
||||
builder.Title = "A new support chat has been marked as active";
|
||||
builder.Color = Color.Green;
|
||||
builder.AddField("Email", user.Email);
|
||||
builder.AddField("Firstname", user.FirstName);
|
||||
builder.AddField("Lastname", user.LastName);
|
||||
builder.Url = $"{AppUrl}/admin/support/view/{user.Id}";
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendNotification(string content, Action<EmbedBuilder>? embed = null)
|
||||
{
|
||||
Logger.Debug(Client);
|
||||
|
||||
var e = new EmbedBuilder();
|
||||
embed?.Invoke(e);
|
||||
|
||||
await Client.SendMessageAsync(
|
||||
content,
|
||||
false,
|
||||
new []{e.Build()},
|
||||
"Moonlight Notification",
|
||||
ResourceService.Image("logo.svg")
|
||||
);
|
||||
}
|
||||
}
|
||||
127
Moonlight/App/Services/SupportChat/SupportChatAdminService.cs
Normal file
127
Moonlight/App/Services/SupportChat/SupportChatAdminService.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
|
||||
namespace Moonlight.App.Services.SupportChat;
|
||||
|
||||
public class SupportChatAdminService
|
||||
{
|
||||
private readonly EventSystem Event;
|
||||
private readonly IdentityService IdentityService;
|
||||
private readonly SupportChatServerService ServerService;
|
||||
|
||||
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||
|
||||
private User? User;
|
||||
private User Recipient = null!;
|
||||
private readonly List<User> TypingUsers = new();
|
||||
|
||||
public SupportChatAdminService(
|
||||
EventSystem eventSystem,
|
||||
SupportChatServerService serverService,
|
||||
IdentityService identityService)
|
||||
{
|
||||
Event = eventSystem;
|
||||
ServerService = serverService;
|
||||
IdentityService = identityService;
|
||||
}
|
||||
|
||||
public async Task Start(User recipient)
|
||||
{
|
||||
User = await IdentityService.Get();
|
||||
Recipient = recipient;
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
await Event.On<SupportChatMessage>($"supportChat.{Recipient.Id}.message", this, async message =>
|
||||
{
|
||||
if(OnMessage != null)
|
||||
await OnMessage.Invoke(message);
|
||||
});
|
||||
|
||||
await Event.On<User>($"supportChat.{Recipient.Id}.typing", this, async user =>
|
||||
{
|
||||
await HandleTyping(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SupportChatMessage[]> GetMessages()
|
||||
{
|
||||
if (User == null)
|
||||
return Array.Empty<SupportChatMessage>();
|
||||
|
||||
return await ServerService.GetMessages(Recipient);
|
||||
}
|
||||
|
||||
public async Task SendMessage(string content)
|
||||
{
|
||||
if (User != null)
|
||||
{
|
||||
await ServerService.SendMessage(Recipient, content, User);
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleTyping(User user)
|
||||
{
|
||||
lock (TypingUsers)
|
||||
{
|
||||
if (!TypingUsers.Contains(user))
|
||||
{
|
||||
TypingUsers.Add(user);
|
||||
|
||||
if (OnTypingChanged != null)
|
||||
{
|
||||
OnTypingChanged.Invoke(
|
||||
TypingUsers
|
||||
.Where(x => x.Id != User!.Id)
|
||||
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
if (TypingUsers.Contains(user))
|
||||
{
|
||||
TypingUsers.Remove(user);
|
||||
|
||||
if (OnTypingChanged != null)
|
||||
{
|
||||
await OnTypingChanged.Invoke(
|
||||
TypingUsers
|
||||
.Where(x => x.Id != User!.Id)
|
||||
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SendTyping()
|
||||
{
|
||||
await Event.Emit($"supportChat.{Recipient.Id}.typing", User);
|
||||
}
|
||||
|
||||
public async Task Close()
|
||||
{
|
||||
await ServerService.CloseChat(Recipient);
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
if (User != null)
|
||||
{
|
||||
await Event.Off($"supportChat.{Recipient.Id}.message", this);
|
||||
await Event.Off($"supportChat.{Recipient.Id}.typing", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
Moonlight/App/Services/SupportChat/SupportChatClientService.cs
Normal file
121
Moonlight/App/Services/SupportChat/SupportChatClientService.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Logging.Net;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
|
||||
namespace Moonlight.App.Services.SupportChat;
|
||||
|
||||
public class SupportChatClientService : IDisposable
|
||||
{
|
||||
private readonly EventSystem Event;
|
||||
private readonly IdentityService IdentityService;
|
||||
private readonly SupportChatServerService ServerService;
|
||||
|
||||
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
|
||||
public Func<string[], Task>? OnTypingChanged { get; set; }
|
||||
|
||||
private User? User;
|
||||
private readonly List<User> TypingUsers = new();
|
||||
|
||||
public SupportChatClientService(
|
||||
EventSystem eventSystem,
|
||||
SupportChatServerService serverService,
|
||||
IdentityService identityService)
|
||||
{
|
||||
Event = eventSystem;
|
||||
ServerService = serverService;
|
||||
IdentityService = identityService;
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
User = await IdentityService.Get();
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
await Event.On<SupportChatMessage>($"supportChat.{User.Id}.message", this, async message =>
|
||||
{
|
||||
if(OnMessage != null)
|
||||
await OnMessage.Invoke(message);
|
||||
});
|
||||
|
||||
await Event.On<User>($"supportChat.{User.Id}.typing", this, async user =>
|
||||
{
|
||||
await HandleTyping(user);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SupportChatMessage[]> GetMessages()
|
||||
{
|
||||
if (User == null)
|
||||
return Array.Empty<SupportChatMessage>();
|
||||
|
||||
return await ServerService.GetMessages(User);
|
||||
}
|
||||
|
||||
public async Task SendMessage(string content)
|
||||
{
|
||||
if (User != null)
|
||||
{
|
||||
await ServerService.SendMessage(User, content, User);
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleTyping(User user)
|
||||
{
|
||||
lock (TypingUsers)
|
||||
{
|
||||
if (!TypingUsers.Contains(user))
|
||||
{
|
||||
TypingUsers.Add(user);
|
||||
|
||||
if (OnTypingChanged != null)
|
||||
{
|
||||
OnTypingChanged.Invoke(
|
||||
TypingUsers
|
||||
.Where(x => x.Id != User!.Id)
|
||||
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
if (TypingUsers.Contains(user))
|
||||
{
|
||||
TypingUsers.Remove(user);
|
||||
|
||||
if (OnTypingChanged != null)
|
||||
{
|
||||
await OnTypingChanged.Invoke(
|
||||
TypingUsers
|
||||
.Where(x => x.Id != User!.Id)
|
||||
.Select(x => $"{x.FirstName} {x.LastName}")
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SendTyping()
|
||||
{
|
||||
await Event.Emit($"supportChat.{User!.Id}.typing", User);
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
if (User != null)
|
||||
{
|
||||
await Event.Off($"supportChat.{User.Id}.message", this);
|
||||
await Event.Off($"supportChat.{User.Id}.typing", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
Moonlight/App/Services/SupportChat/SupportChatServerService.cs
Normal file
144
Moonlight/App/Services/SupportChat/SupportChatServerService.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.SupportChat;
|
||||
|
||||
public class SupportChatServerService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
private readonly DateTimeService DateTimeService;
|
||||
private readonly EventSystem Event;
|
||||
|
||||
public SupportChatServerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
DateTimeService dateTimeService,
|
||||
EventSystem eventSystem)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
DateTimeService = dateTimeService;
|
||||
Event = eventSystem;
|
||||
}
|
||||
|
||||
public Task<SupportChatMessage[]> GetMessages(User recipient)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||
|
||||
var messages = msgRepo
|
||||
.Get()
|
||||
.Include(x => x.Recipient)
|
||||
.Include(x => x.Sender)
|
||||
.Where(x => x.Recipient.Id == recipient.Id)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.AsEnumerable()
|
||||
.Take(50)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult(messages);
|
||||
}
|
||||
|
||||
public async Task SendMessage(User recipient, string content, User? sender, string? attachment = null)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
|
||||
var message = new SupportChatMessage()
|
||||
{
|
||||
CreatedAt = DateTimeService.GetCurrent(),
|
||||
IsQuestion = false,
|
||||
Sender = sender == null ? null : userRepo.Get().First(x => x.Id == sender.Id),
|
||||
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||
Answer = "",
|
||||
Attachment = attachment ?? "",
|
||||
Content = content,
|
||||
UpdatedAt = DateTimeService.GetCurrent()
|
||||
};
|
||||
|
||||
var finalMessage = msgRepo.Add(message);
|
||||
|
||||
await Event.Emit($"supportChat.{recipient.Id}.message", finalMessage);
|
||||
await Event.Emit("supportChat.message", finalMessage);
|
||||
|
||||
if (!userRepo.Get().First(x => x.Id == recipient.Id).SupportPending)
|
||||
{
|
||||
var ticketStart = new SupportChatMessage()
|
||||
{
|
||||
CreatedAt = DateTimeService.GetCurrent(),
|
||||
IsQuestion = false,
|
||||
Sender = null,
|
||||
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||
Answer = "",
|
||||
Attachment = "",
|
||||
Content = "Support ticket open", //TODO: Config
|
||||
UpdatedAt = DateTimeService.GetCurrent()
|
||||
};
|
||||
|
||||
var ticketStartFinal = msgRepo.Add(ticketStart);
|
||||
|
||||
var user = userRepo.Get().First(x => x.Id == recipient.Id);
|
||||
user.SupportPending = true;
|
||||
userRepo.Update(user);
|
||||
|
||||
await Event.Emit($"supportChat.{recipient.Id}.message", ticketStartFinal);
|
||||
await Event.Emit("supportChat.message", ticketStartFinal);
|
||||
await Event.Emit("supportChat.new", recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Dictionary<User, SupportChatMessage?>> GetOpenChats()
|
||||
{
|
||||
var result = new Dictionary<User, SupportChatMessage?>();
|
||||
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||
|
||||
foreach (var user in userRepo.Get().Where(x => x.SupportPending).ToArray())
|
||||
{
|
||||
var lastMessage = msgRepo
|
||||
.Get()
|
||||
.Include(x => x.Recipient)
|
||||
.Include(x => x.Sender)
|
||||
.Where(x => x.Recipient.Id == user.Id)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.AsEnumerable()
|
||||
.FirstOrDefault();
|
||||
|
||||
result.Add(user, lastMessage);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public async Task CloseChat(User recipient)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var msgRepo = scope.ServiceProvider.GetRequiredService<Repository<SupportChatMessage>>();
|
||||
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
|
||||
|
||||
var ticketEnd = new SupportChatMessage()
|
||||
{
|
||||
CreatedAt = DateTimeService.GetCurrent(),
|
||||
IsQuestion = false,
|
||||
Sender = null,
|
||||
Recipient = userRepo.Get().First(x => x.Id == recipient.Id),
|
||||
Answer = "",
|
||||
Attachment = "",
|
||||
Content = "Support ticket closed", //TODO: Config
|
||||
UpdatedAt = DateTimeService.GetCurrent()
|
||||
};
|
||||
|
||||
var ticketEndFinal = msgRepo.Add(ticketEnd);
|
||||
|
||||
var user = userRepo.Get().First(x => x.Id == recipient.Id);
|
||||
user.SupportPending = false;
|
||||
userRepo.Update(user);
|
||||
|
||||
await Event.Emit($"supportChat.{recipient.Id}.message", ticketEndFinal);
|
||||
await Event.Emit("supportChat.message", ticketEndFinal);
|
||||
await Event.Emit("supportChat.close", recipient);
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||
<PackageReference Include="CloudFlare.Client" Version="6.1.4" />
|
||||
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" />
|
||||
<PackageReference Include="Discord.Net" Version="3.9.0" />
|
||||
<PackageReference Include="DiscordWebhooks" Version="1.0.4" />
|
||||
<PackageReference Include="Discord.Net" Version="3.10.0" />
|
||||
<PackageReference Include="Discord.Net.Webhook" Version="3.10.0" />
|
||||
<PackageReference Include="FluentFTP" Version="46.0.2" />
|
||||
<PackageReference Include="GravatarSharp.Core" Version="1.0.1.2" />
|
||||
<PackageReference Include="JWT" Version="10.0.2" />
|
||||
|
||||
@@ -20,6 +20,7 @@ using Moonlight.App.Services.OAuth2;
|
||||
using Moonlight.App.Services.Sessions;
|
||||
using Moonlight.App.Services.Statistics;
|
||||
using Moonlight.App.Services.Support;
|
||||
using Moonlight.App.Services.SupportChat;
|
||||
|
||||
namespace Moonlight
|
||||
{
|
||||
@@ -124,11 +125,16 @@ namespace Moonlight
|
||||
builder.Services.AddScoped<MailService>();
|
||||
builder.Services.AddSingleton<TrashMailDetectorService>();
|
||||
|
||||
// Support
|
||||
// Support TODO: Remove
|
||||
builder.Services.AddSingleton<SupportServerService>();
|
||||
builder.Services.AddScoped<SupportAdminService>();
|
||||
builder.Services.AddScoped<SupportClientService>();
|
||||
|
||||
// Support chat
|
||||
builder.Services.AddSingleton<SupportChatServerService>();
|
||||
builder.Services.AddScoped<SupportChatClientService>();
|
||||
builder.Services.AddScoped<SupportChatAdminService>();
|
||||
|
||||
// Helpers
|
||||
builder.Services.AddSingleton<SmartTranslateHelper>();
|
||||
builder.Services.AddScoped<WingsApiHelper>();
|
||||
@@ -143,6 +149,7 @@ namespace Moonlight
|
||||
// Background services
|
||||
builder.Services.AddSingleton<DiscordBotService>();
|
||||
builder.Services.AddSingleton<StatisticsCaptureService>();
|
||||
builder.Services.AddSingleton<DiscordNotificationService>();
|
||||
|
||||
// Third party services
|
||||
builder.Services.AddBlazorTable();
|
||||
@@ -176,6 +183,7 @@ namespace Moonlight
|
||||
_ = app.Services.GetRequiredService<CleanupService>();
|
||||
_ = app.Services.GetRequiredService<DiscordBotService>();
|
||||
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
|
||||
_ = app.Services.GetRequiredService<DiscordNotificationService>();
|
||||
|
||||
// Discord bot service
|
||||
//var discordBotService = app.Services.GetRequiredService<DiscordBotService>();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
@page "/admin/support"
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Database
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Events
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
|
||||
@inject SupportChatServerService ServerService
|
||||
@inject EventSystem Event
|
||||
|
||||
@inject SupportMessageRepository SupportMessageRepository
|
||||
@inject ConfigService ConfigService
|
||||
@inject MessageService MessageService
|
||||
@implements IDisposable
|
||||
|
||||
<OnlyAdmin>
|
||||
@@ -18,13 +16,13 @@
|
||||
<div class="flex-lg-row-fluid me-xl-15 mb-20 mb-xl-0">
|
||||
<div class="mb-0">
|
||||
<h1 class="text-dark mb-6">
|
||||
<TL>Open tickets</TL>
|
||||
<TL>Open chats</TL>
|
||||
</h1>
|
||||
<div class="separator"></div>
|
||||
<div class="mb-5">
|
||||
@if (Users.Any())
|
||||
@if (OpenChats.Any())
|
||||
{
|
||||
foreach (var user in Users)
|
||||
foreach (var chat in OpenChats)
|
||||
{
|
||||
<div class="d-flex mt-3 mb-3 ms-2 me-2">
|
||||
<table>
|
||||
@@ -35,25 +33,21 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/support/view/@(user.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
||||
@(user.FirstName) @(user.LastName)
|
||||
<a href="/admin/support/view/@(chat.Key.Id)" class="text-dark text-hover-primary fs-4 me-3 fw-semibold">
|
||||
@(chat.Key.FirstName) @(chat.Key.LastName)
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-muted fw-semibold fs-6">
|
||||
@{
|
||||
var lastMessage = MessageCache.ContainsKey(user) ? MessageCache[user] : null;
|
||||
}
|
||||
|
||||
@if (lastMessage == null)
|
||||
@if (chat.Value == null)
|
||||
{
|
||||
<TL>No message sent yet</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(lastMessage.Message)
|
||||
@(chat.Value.Content)
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
@@ -66,7 +60,7 @@
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<TL>No support ticket is currently open</TL>
|
||||
<TL>No support chat is currently open</TL>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -80,53 +74,28 @@
|
||||
|
||||
@code
|
||||
{
|
||||
private User[] Users;
|
||||
private Dictionary<User, SupportMessage?> MessageCache;
|
||||
|
||||
private LazyLoader? LazyLoader;
|
||||
private Dictionary<User, SupportChatMessage?> OpenChats = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
MessageCache = new();
|
||||
await Event.On<User>("supportChat.new", this, async user =>
|
||||
{
|
||||
//TODO: Play sound or smth. Add a config option
|
||||
|
||||
MessageService.Subscribe<Index, User>("support.new", this, async user =>
|
||||
{
|
||||
if (LazyLoader != null)
|
||||
await LazyLoader.Reload();
|
||||
});
|
||||
OpenChats = await ServerService.GetOpenChats();
|
||||
|
||||
MessageService.Subscribe<Index, User>("support.close", this, async user =>
|
||||
{
|
||||
if (LazyLoader != null)
|
||||
await LazyLoader.Reload();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private Task Load(LazyLoader arg)
|
||||
private async Task Load(LazyLoader arg) // Only for initial load
|
||||
{
|
||||
// We dont want cache here
|
||||
Users = (new UserRepository(new DataContext(ConfigService)))
|
||||
.Get()
|
||||
.Where(x => x.SupportPending)
|
||||
.ToArray();
|
||||
|
||||
foreach (var user in Users)
|
||||
{
|
||||
var lastMessage = SupportMessageRepository
|
||||
.Get()
|
||||
.Include(x => x.Recipient)
|
||||
.OrderByDescending(x => x.Id)
|
||||
.FirstOrDefault(x => x.Recipient!.Id == user.Id);
|
||||
|
||||
MessageCache.Add(user, lastMessage);
|
||||
OpenChats = await ServerService.GetOpenChats();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public async void Dispose()
|
||||
{
|
||||
MessageService.Unsubscribe("support.new", this);
|
||||
MessageService.Unsubscribe("support.close", this);
|
||||
await Event.Off("supportChat.new", this);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
@page "/admin/support/view/{Id:int}"
|
||||
@using Moonlight.App.Services.Support
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
|
||||
@inject SupportAdminService SupportAdminService
|
||||
@inject SupportChatAdminService AdminService
|
||||
@inject UserRepository UserRepository
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject ResourceService ResourceService
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.IsSystem || message.IsSupport)
|
||||
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||
{
|
||||
<div class="d-flex justify-content-end mb-10 ">
|
||||
<div class="d-flex flex-column align-items-end">
|
||||
@@ -37,13 +37,13 @@
|
||||
<div class="me-3">
|
||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
||||
@if (message.IsSupport && !message.IsSystem)
|
||||
@if (message.Sender != null)
|
||||
{
|
||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>System</span>
|
||||
<span><TL>System</TL></span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
@@ -53,13 +53,13 @@
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||
@if (message.IsSystem)
|
||||
@if (message.Sender == null)
|
||||
{
|
||||
<TL>@(message.Message)</TL>
|
||||
<TL>@(message.Content)</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(message.Message)
|
||||
@(message.Content)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,14 +75,14 @@
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
</a>
|
||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||
@(message.Message)
|
||||
@(message.Content)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,11 +92,7 @@
|
||||
</LazyLoader>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@{
|
||||
var typingUsers = SupportAdminService.GetTypingUsers();
|
||||
}
|
||||
|
||||
@if (typingUsers.Any())
|
||||
@if (Typing.Any())
|
||||
{
|
||||
<span class="mb-5 fs-5 d-flex flex-row">
|
||||
<div class="wave me-1">
|
||||
@@ -104,16 +100,16 @@
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px"></div>
|
||||
</div>
|
||||
@if (typingUsers.Length > 1)
|
||||
@if (Typing.Length > 1)
|
||||
{
|
||||
<span>
|
||||
@(typingUsers.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>
|
||||
@(typingUsers.First()) <TL>is typing</TL>
|
||||
@(Typing.First()) <TL>is typing</TL>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
@@ -183,9 +179,11 @@
|
||||
public int Id { get; set; }
|
||||
|
||||
private User? User;
|
||||
private SupportMessage[] Messages;
|
||||
private string Content = "";
|
||||
|
||||
private SupportChatMessage[] Messages = Array.Empty<SupportChatMessage>();
|
||||
private string[] Typing = Array.Empty<string>();
|
||||
|
||||
private string Content = "";
|
||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
@@ -196,55 +194,54 @@
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
SupportAdminService.OnNewMessage += OnNewMessage;
|
||||
SupportAdminService.OnUpdateTyping += OnUpdateTyping;
|
||||
AdminService.OnMessage += OnMessage;
|
||||
AdminService.OnTypingChanged += OnTypingChanged;
|
||||
|
||||
await SupportAdminService.Start(User);
|
||||
await AdminService.Start(User);
|
||||
}
|
||||
}
|
||||
|
||||
#region Message handling
|
||||
|
||||
private async void OnNewMessage(object? sender, SupportMessage e)
|
||||
{
|
||||
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LoadMessages(LazyLoader arg)
|
||||
{
|
||||
Messages = (await SupportAdminService.GetMessages()).Reverse().ToArray();
|
||||
Messages = await AdminService.GetMessages();
|
||||
}
|
||||
|
||||
#endregion
|
||||
private async Task OnTypingChanged(string[] typing)
|
||||
{
|
||||
Typing = typing;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnMessage(SupportChatMessage arg)
|
||||
{
|
||||
Messages = await AdminService.GetMessages();
|
||||
|
||||
//TODO: Sound when message from system or admin
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
await SupportAdminService.SendMessage(Content);
|
||||
await AdminService.SendMessage(Content);
|
||||
Content = "";
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CloseTicket()
|
||||
{
|
||||
await SupportAdminService.Close();
|
||||
await AdminService.Close();
|
||||
}
|
||||
|
||||
#region Typing
|
||||
|
||||
private async Task OnTyping()
|
||||
{
|
||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||
{
|
||||
LastTypingTimestamp = DateTime.UtcNow;
|
||||
|
||||
await SupportAdminService.TriggerTyping();
|
||||
await AdminService.SendTyping();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUpdateTyping(object? sender, EventArgs e)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
@using Moonlight.App.Services
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Services.Support
|
||||
@using Moonlight.App.Services.SupportChat
|
||||
|
||||
@inject ResourceService ResourceService
|
||||
@inject SupportClientService SupportClientService
|
||||
@inject SupportChatClientService ClientService
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.IsSystem || message.IsSupport)
|
||||
if (message.Sender == null || message.Sender.Id != User.Id)
|
||||
{
|
||||
<div class="d-flex justify-content-start mb-10 ">
|
||||
<div class="d-flex flex-column align-items-start">
|
||||
@@ -26,13 +26,13 @@
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
@if (message.IsSupport && !message.IsSystem)
|
||||
@if (message.Sender != null)
|
||||
{
|
||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>System</span>
|
||||
<span><TL>System</TL></span>
|
||||
}
|
||||
</a>
|
||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||
@@ -40,13 +40,13 @@
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
|
||||
@if (message.IsSystem)
|
||||
@if (message.Sender == null)
|
||||
{
|
||||
<TL>@(message.Message)</TL>
|
||||
<TL>@(message.Content)</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
@(message.Message)
|
||||
@(message.Content)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="me-3">
|
||||
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
|
||||
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
|
||||
<span>@(message.Sender.FirstName) @(message.Sender.LastName)</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="symbol symbol-35px symbol-circle ">
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
|
||||
@(message.Message)
|
||||
@(message.Content)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
|
||||
<span>System</span>
|
||||
<span><TL>System</TL></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,11 +97,7 @@
|
||||
</LazyLoader>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@{
|
||||
var typingUsers = SupportClientService.GetTypingUsers();
|
||||
}
|
||||
|
||||
@if (typingUsers.Any())
|
||||
@if (Typing.Any())
|
||||
{
|
||||
<span class="mb-5 fs-5 d-flex flex-row">
|
||||
<div class="wave me-1">
|
||||
@@ -109,13 +105,13 @@
|
||||
<div class="dot h-5px w-5px" style="margin-right: 1px;"></div>
|
||||
<div class="dot h-5px w-5px"></div>
|
||||
</div>
|
||||
@if (typingUsers.Length > 1)
|
||||
@if (Typing.Length > 1)
|
||||
{
|
||||
<span>@(typingUsers.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL></span>
|
||||
<span>@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(typingUsers.First()) <TL>is typing</TL></span>
|
||||
<span>@(Typing.First()) <TL>is typing</TL></span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@@ -149,47 +145,51 @@
|
||||
|
||||
@code
|
||||
{
|
||||
private SupportMessage[] Messages;
|
||||
private string Content = "";
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private SupportChatMessage[] Messages = Array.Empty<SupportChatMessage>();
|
||||
private string[] Typing = Array.Empty<string>();
|
||||
|
||||
private string Content = "";
|
||||
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Starting chat client");
|
||||
|
||||
SupportClientService.OnNewMessage += OnNewMessage;
|
||||
SupportClientService.OnUpdateTyping += OnUpdateTyping;
|
||||
ClientService.OnMessage += OnMessage;
|
||||
ClientService.OnTypingChanged += OnTypingChanged;
|
||||
|
||||
await SupportClientService.Start();
|
||||
}
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
await SupportClientService.SendMessage(Content);
|
||||
Content = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
#region Message handling
|
||||
|
||||
private async void OnNewMessage(object? sender, SupportMessage e)
|
||||
{
|
||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await ClientService.Start();
|
||||
}
|
||||
|
||||
private async Task LoadMessages(LazyLoader arg)
|
||||
{
|
||||
Messages = (await SupportClientService.GetMessages()).Reverse().ToArray();
|
||||
Messages = await ClientService.GetMessages();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Typing
|
||||
|
||||
private async void OnUpdateTyping(object? sender, EventArgs e)
|
||||
private async Task OnTypingChanged(string[] typing)
|
||||
{
|
||||
Typing = typing;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnMessage(SupportChatMessage message)
|
||||
{
|
||||
Messages = await ClientService.GetMessages();
|
||||
|
||||
//TODO: Sound when message from system or admin
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
await ClientService.SendMessage(Content);
|
||||
Content = "";
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -198,10 +198,7 @@
|
||||
if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5)
|
||||
{
|
||||
LastTypingTimestamp = DateTime.UtcNow;
|
||||
|
||||
await SupportClientService.TriggerTyping();
|
||||
await ClientService.SendTyping();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user