Added multi line feature and file upload in the support chat. Started working on snippets

This commit is contained in:
Marcel Baumgartner
2023-05-02 23:58:44 +02:00
parent 825b7be86d
commit af9916d563
10 changed files with 368 additions and 50 deletions

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Database.Entities;
public class SupportChatSnippets
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Description { get; set; } = "";
}

View File

@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
using Moonlight.App.Services.LogServices;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -11,10 +12,13 @@ namespace Moonlight.App.Http.Controllers.Api.Moonlight;
public class ResourcesController : Controller
{
private readonly SecurityLogService SecurityLogService;
private readonly BucketService BucketService;
public ResourcesController(SecurityLogService securityLogService)
public ResourcesController(SecurityLogService securityLogService,
BucketService bucketService)
{
SecurityLogService = securityLogService;
BucketService = bucketService;
}
[HttpGet("images/{name}")]
@@ -26,6 +30,7 @@ public class ResourcesController : Controller
{
x.Add<string>(name);
});
return NotFound();
}
@@ -38,4 +43,33 @@ public class ResourcesController : Controller
return NotFound();
}
[HttpGet("bucket/{bucket}/{name}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
return NotFound();
}
try
{
var fs = await BucketService.GetFile(bucket, name);
return File(fs, MimeTypes.GetMimeType(name), name);
}
catch (FileNotFoundException)
{
return NotFound();
}
catch (Exception)
{
return Problem();
}
}
}

View File

@@ -0,0 +1,68 @@
using Logging.Net;
using Moonlight.App.Helpers;
namespace Moonlight.App.Services;
public class BucketService
{
private string BucketPath;
public BucketService()
{
BucketPath = PathBuilder.Dir("storage", "uploads");
}
public Task<string[]> GetBuckets()
{
var buckets = Directory.GetDirectories(BucketPath)
.Select(x =>
x.Replace(BucketPath, "").TrimEnd('/')
)
.ToArray();
return Task.FromResult(buckets);
}
private Task EnsureBucket(string name)
{
Directory.CreateDirectory(BucketPath + name);
return Task.CompletedTask;
}
public async Task<string> StoreFile(string bucket, Stream dataStream, string? name = null)
{
await EnsureBucket(bucket);
var extension = "";
if (name != null)
extension = Path.GetExtension(name);
var fileName = Path.GetRandomFileName() + extension; //TODO: Add check for existing file
var filePath = BucketPath + PathBuilder.File(bucket, fileName);
var fileStream = File.Create(filePath);
await dataStream.CopyToAsync(fileStream);
await fileStream.FlushAsync();
fileStream.Close();
return fileName;
}
public Task<Stream> GetFile(string bucket, string file)
{
var filePath = BucketPath + PathBuilder.File(bucket, file);
if (File.Exists(filePath))
{
var stream = File.Open(filePath, FileMode.Open);
return Task.FromResult<Stream>(stream);
}
else
throw new FileNotFoundException();
}
}

View File

@@ -20,4 +20,9 @@ public class ResourceService
{
return $"{AppUrl}/api/moonlight/avatar/{user.Id}";
}
public string BucketItem(string bucket, string name)
{
return $"{AppUrl}/api/moonlight/resources/bucket/{bucket}/{name}";
}
}

View File

@@ -1,14 +1,16 @@
using Moonlight.App.Database.Entities;
using Microsoft.AspNetCore.Components.Forms;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Services.SupportChat;
public class SupportChatAdminService
public class SupportChatAdminService : IDisposable
{
private readonly EventSystem Event;
private readonly IdentityService IdentityService;
private readonly SupportChatServerService ServerService;
private readonly BucketService BucketService;
public Func<SupportChatMessage, Task>? OnMessage { get; set; }
public Func<string[], Task>? OnTypingChanged { get; set; }
@@ -20,11 +22,13 @@ public class SupportChatAdminService
public SupportChatAdminService(
EventSystem eventSystem,
SupportChatServerService serverService,
IdentityService identityService)
IdentityService identityService,
BucketService bucketService)
{
Event = eventSystem;
ServerService = serverService;
IdentityService = identityService;
BucketService = bucketService;
}
public async Task Start(User recipient)
@@ -60,11 +64,21 @@ public class SupportChatAdminService
return await ServerService.GetMessages(Recipient);
}
public async Task<SupportChatMessage> SendMessage(string content)
public async Task<SupportChatMessage> SendMessage(string content, IBrowserFile? browserFile = null)
{
if (User != null)
{
return await ServerService.SendMessage(Recipient, content, User);
string? attachment = null;
if (browserFile != null)
{
attachment = await BucketService.StoreFile(
"supportChat",
browserFile.OpenReadStream(1024 * 1024 * 5),
browserFile.Name);
}
return await ServerService.SendMessage(Recipient, content, User, attachment);
}
return null!;

View File

@@ -1,4 +1,5 @@
using Logging.Net;
using Microsoft.AspNetCore.Components.Forms;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Services.Sessions;
@@ -8,6 +9,7 @@ namespace Moonlight.App.Services.SupportChat;
public class SupportChatClientService : IDisposable
{
private readonly EventSystem Event;
private readonly BucketService BucketService;
private readonly IdentityService IdentityService;
private readonly SupportChatServerService ServerService;
@@ -20,11 +22,13 @@ public class SupportChatClientService : IDisposable
public SupportChatClientService(
EventSystem eventSystem,
SupportChatServerService serverService,
IdentityService identityService)
IdentityService identityService,
BucketService bucketService)
{
Event = eventSystem;
ServerService = serverService;
IdentityService = identityService;
BucketService = bucketService;
}
public async Task Start()
@@ -59,11 +63,21 @@ public class SupportChatClientService : IDisposable
return await ServerService.GetMessages(User);
}
public async Task<SupportChatMessage> SendMessage(string content)
public async Task<SupportChatMessage> SendMessage(string content, IBrowserFile? browserFile = null)
{
if (User != null)
{
return await ServerService.SendMessage(User, content, User);
string? attachment = null;
if (browserFile != null)
{
attachment = await BucketService.StoreFile(
"supportChat",
browserFile.OpenReadStream(1024 * 1024 * 5),
browserFile.Name);
}
return await ServerService.SendMessage(User, content, User, attachment);
}
return null!;

View File

@@ -108,6 +108,7 @@ namespace Moonlight
builder.Services.AddScoped<FileDownloadService>();
builder.Services.AddScoped<ForgeService>();
builder.Services.AddScoped<FabricService>();
builder.Services.AddSingleton<BucketService>();
builder.Services.AddScoped<GoogleOAuth2Service>();
builder.Services.AddScoped<DiscordOAuth2Service>();

View File

@@ -0,0 +1,61 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Logging.Net
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden=""/>
<label for="fileUpload" class="btn btn-primary me-3">
@if (SelectedFile != null)
{
<div class="input-group">
<input type="text" class="form-control" value="@(SelectedFile.Name)">
<button class="btn btn-danger" type="button" @onclick="RemoveSelection">
<i class="bx bx-md bx-x"></i>
</button>
</div>
}
else
{
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
}
</label>
@code
{
public IBrowserFile? SelectedFile { get; set; }
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
if (arg.FileCount > 0)
{
if (arg.File.Size < 1024 * 1024 * 5)
{
SelectedFile = arg.File;
await InvokeAsync(StateHasChanged);
return;
}
await ToastService.Error(SmartTranslateService.Translate("The uploaded file should not be bigger than 5MB"));
}
SelectedFile = null;
await InvokeAsync(StateHasChanged);
}
public async Task RemoveSelection()
{
SelectedFile = null;
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -4,12 +4,15 @@
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.App.Services.SupportChat
@using System.Text.RegularExpressions
@inject SupportChatAdminService AdminService
@inject UserRepository UserRepository
@inject SmartTranslateService SmartTranslateService
@inject ResourceService ResourceService
@implements IDisposable
<OnlyAdmin>
<LazyLoader Load="Load">
@if (User == null)
@@ -43,7 +46,9 @@
}
else
{
<span><TL>System</TL></span>
<span>
<TL>System</TL>
</span>
}
</a>
</div>
@@ -59,7 +64,26 @@
}
else
{
@(message.Content)
foreach (var line in message.Content.Split("\n"))
{
@(line)<br/>
}
if (message.Attachment != "")
{
<div class="mt-3">
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
{
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
}
else
{
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
<i class="me-2 bx bx-download"></i> @(message.Attachment)
</a>
}
</div>
}
}
</div>
</div>
@@ -82,7 +106,26 @@
</div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
@(message.Content)
@foreach (var line in message.Content.Split("\n"))
{
@(line)<br/>
}
@if (message.Attachment != "")
{
<div class="mt-3">
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
{
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
}
else
{
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
<i class="me-2 bx bx-download"></i> @(message.Attachment)
</a>
}
</div>
}
</div>
</div>
</div>
@@ -118,11 +161,9 @@
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<!--<td class="align-top">
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
<i class="bx bx-upload fs-3"></i>
</button>
</td>-->
<td class="align-top">
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
</textarea>
@@ -186,6 +227,8 @@
private string Content = "";
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
private SmartFileSelect SmartFileSelect;
private async Task Load(LazyLoader arg)
{
User = UserRepository
@@ -224,12 +267,17 @@
private async Task Send()
{
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
Content = "File upload";
if (string.IsNullOrEmpty(Content))
return;
var message = await AdminService.SendMessage(Content);
var message = await AdminService.SendMessage(Content, SmartFileSelect.SelectedFile);
Content = "";
await SmartFileSelect.RemoveSelection();
Messages.Insert(0, message);
await InvokeAsync(StateHasChanged);
@@ -249,4 +297,9 @@
await AdminService.SendTyping();
}
}
public void Dispose()
{
AdminService?.Dispose();
}
}

View File

@@ -4,18 +4,21 @@
@using Moonlight.App.Helpers
@using Moonlight.App.Services.SupportChat
@using Logging.Net
@using System.Text.RegularExpressions
@inject ResourceService ResourceService
@inject SupportChatClientService ClientService
@inject SmartTranslateService SmartTranslateService
@implements IDisposable
<LazyLoader Load="Load">
<div class="row">
<div class="card">
<div class="card-body">
<LazyLoader Load="LoadMessages">
<div class="scroll-y me-n5 pe-5" style="max-height: 55vh; display: flex; flex-direction: column-reverse;">
@foreach (var message in Messages)
@foreach (var message in Messages.ToArray())
{
if (message.Sender == null || message.Sender.Id != User.Id)
{
@@ -33,7 +36,9 @@
}
else
{
<span><TL>System</TL></span>
<span>
<TL>System</TL>
</span>
}
</a>
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
@@ -47,7 +52,26 @@
}
else
{
@(message.Content)
foreach (var line in message.Content.Split("\n"))
{
@(line)<br/>
}
if (message.Attachment != "")
{
<div class="mt-3">
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
{
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
}
else
{
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
<i class="me-2 bx bx-download"></i> @(message.Attachment)
</a>
}
</div>
}
}
</div>
</div>
@@ -70,7 +94,26 @@
</div>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@(message.Content)
@foreach (var line in message.Content.Split("\n"))
{
@(line)<br/>
}
@if (message.Attachment != "")
{
<div class="mt-3">
@if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$"))
{
<img src="@(ResourceService.BucketItem("supportChat", message.Attachment))" class="img-fluid" alt="Attachment"/>
}
else
{
<a class="btn btn-secondary" href="@(ResourceService.BucketItem("supportChat", message.Attachment))">
<i class="me-2 bx bx-download"></i> @(message.Attachment)
</a>
}
</div>
}
</div>
</div>
</div>
@@ -84,7 +127,9 @@
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
<span><TL>System</TL></span>
<span>
<TL>System</TL>
</span>
</a>
</div>
</div>
@@ -108,11 +153,15 @@
</div>
@if (Typing.Length > 1)
{
<span>@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL></span>
<span>
@(Typing.Aggregate((current, next) => current + ", " + next)) <TL>are typing</TL>
</span>
}
else
{
<span>@(Typing.First()) <TL>is typing</TL></span>
<span>
@(Typing.First()) <TL>is typing</TL>
</span>
}
</span>
}
@@ -120,11 +169,9 @@
<div class="d-flex flex-stack">
<table class="w-100">
<tr>
<!--<td class="align-top">
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
<i class="bx bx-upload fs-3"></i>
</button>
</td>-->
<td class="align-top">
<SmartFileSelect @ref="SmartFileSelect"></SmartFileSelect>
</td>
<td class="w-100">
<textarea @bind="Content" @oninput="OnTyping" class="form-control mb-3 form-control-flush" rows="1" placeholder="Type a message">
</textarea>
@@ -155,6 +202,8 @@
private string Content = "";
private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10);
private SmartFileSelect SmartFileSelect;
private async Task Load(LazyLoader lazyLoader)
{
await lazyLoader.SetText("Starting chat client");
@@ -188,12 +237,14 @@
private async Task Send()
{
if(string.IsNullOrEmpty(Content))
return;
if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content))
Content = "File upload";
var message = await ClientService.SendMessage(Content);
var message = await ClientService.SendMessage(Content, SmartFileSelect.SelectedFile);
Content = "";
await SmartFileSelect.RemoveSelection();
Messages.Insert(0, message);
await InvokeAsync(StateHasChanged);
@@ -207,4 +258,13 @@
await ClientService.SendTyping();
}
}
public void Dispose()
{
ClientService?.Dispose();
}
private void OnFileChange(InputFileChangeEventArgs obj)
{
}
}