diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs index 272abf0df..16197b9cf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs @@ -5,8 +5,11 @@ public interface IBotSharpFileService string GetDirectory(string conversationId); IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2); IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false); - string? GetMessageFile(string conversationId, string messageId, string fileName); - void SaveMessageFiles(string conversationId, string messageId, List files); + string GetMessageFile(string conversationId, string messageId, string fileName); + bool SaveMessageFiles(string conversationId, string messageId, List files); + + string GetUserAvatar(); + bool SaveUserAvatar(BotSharpFile file); /// /// Delete files under messages diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs new file mode 100644 index 000000000..f12ecb608 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs @@ -0,0 +1,188 @@ +using Microsoft.AspNetCore.StaticFiles; +using System.IO; +using System.Threading; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 1) + { + var files = new List(); + if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) + { + return files; + } + + if (offset <= 0) + { + offset = MIN_OFFSET; + } + else if (offset > MAX_OFFSET) + { + offset = MAX_OFFSET; + } + + var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); + files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); + return files; + } + + public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) + { + var files = new List(); + if (messageIds.IsNullOrEmpty()) return files; + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + continue; + } + + foreach (var file in Directory.GetFiles(dir)) + { + var contentType = GetFileContentType(file); + if (imageOnly && !_allowedTypes.Contains(contentType)) + { + continue; + } + + var fileName = Path.GetFileNameWithoutExtension(file); + var extension = Path.GetExtension(file); + var fileType = extension.Substring(1); + + var model = new MessageFileModel() + { + MessageId = messageId, + FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", + FileStorageUrl = file, + FileName = fileName, + FileType = fileType, + ContentType = contentType + }; + files.Add(model); + } + } + + return files; + } + + public string GetMessageFile(string conversationId, string messageId, string fileName) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + return string.Empty; + } + + var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); + return found; + } + + public bool SaveMessageFiles(string conversationId, string messageId, List files) + { + if (files.IsNullOrEmpty()) return false; + + var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); + if (!ExistDirectory(dir)) return false; + + try + { + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + if (string.IsNullOrEmpty(file.FileData)) + { + continue; + } + + var (_, bytes) = GetFileInfoFromData(file.FileData); + var fileType = Path.GetExtension(file.FileName); + var fileName = $"{i + 1}{fileType}"; + Thread.Sleep(100); + File.WriteAllBytes(Path.Combine(dir, fileName), bytes); + } + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving conversation files: {ex.Message}"); + return false; + } + } + + + + public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) + { + if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; + + if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) + { + var prevDir = GetConversationFileDirectory(conversationId, targetMessageId); + var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId); + + if (ExistDirectory(prevDir)) + { + if (ExistDirectory(newDir)) + { + Directory.Delete(newDir, true); + } + + Directory.Move(prevDir, newDir); + } + } + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (string.IsNullOrEmpty(dir)) continue; + + Thread.Sleep(100); + Directory.Delete(dir, true); + } + + return true; + } + + public bool DeleteConversationFiles(IEnumerable conversationIds) + { + if (conversationIds.IsNullOrEmpty()) return false; + + foreach (var conversationId in conversationIds) + { + var convDir = FindConversationDirectory(conversationId); + if (!ExistDirectory(convDir)) continue; + + Directory.Delete(convDir, true); + } + return true; + } + + #region Private methods + private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + + private string? FindConversationDirectory(string conversationId) + { + if (string.IsNullOrEmpty(conversationId)) return null; + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId); + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs new file mode 100644 index 000000000..b6a879935 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs @@ -0,0 +1,65 @@ +using System.IO; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public string GetUserAvatar() + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (!ExistDirectory(dir)) return string.Empty; + + var found = Directory.GetFiles(dir).FirstOrDefault() ?? string.Empty; + return found; + } + + public bool SaveUserAvatar(BotSharpFile file) + { + if (file == null || string.IsNullOrEmpty(file.FileData)) return false; + + try + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (string.IsNullOrEmpty(dir)) return false; + + if (Directory.Exists(dir)) + { + Directory.Delete(dir, true); + } + + dir = GetUserAvatarDir(user?.Id, createNewDir: true); + var (_, bytes) = GetFileInfoFromData(file.FileData); + File.WriteAllBytes(Path.Combine(dir, file.FileName), bytes); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving user avatar: {ex.Message}"); + return false; + } + } + + + #region Private methods + private string GetUserAvatarDir(string? userId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(userId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, USERS_FOLDER, userId, USER_AVATAR_FOLDER); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs index d7e961be4..76d26dbc9 100644 --- a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs @@ -1,28 +1,35 @@ using Microsoft.AspNetCore.StaticFiles; +using System; using System.IO; using System.Threading; namespace BotSharp.Core.Files; -public class BotSharpFileService : IBotSharpFileService +public partial class BotSharpFileService : IBotSharpFileService { private readonly BotSharpDatabaseSettings _dbSettings; private readonly IServiceProvider _services; + private readonly IUserIdentity _user; private readonly ILogger _logger; private readonly string _baseDir; private readonly IEnumerable _allowedTypes = new List { "image/png", "image/jpeg" }; private const string CONVERSATION_FOLDER = "conversations"; private const string FILE_FOLDER = "files"; + private const string USERS_FOLDER = "users"; + private const string USER_AVATAR_FOLDER = "avatar"; + private const int MIN_OFFSET = 1; private const int MAX_OFFSET = 5; public BotSharpFileService( BotSharpDatabaseSettings dbSettings, + IUserIdentity user, ILogger logger, IServiceProvider services) { _dbSettings = dbSettings; + _user = user; _logger = logger; _services = services; _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository); @@ -38,157 +45,6 @@ public string GetDirectory(string conversationId) return dir; } - public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2) - { - var files = new List(); - if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) - { - return files; - } - - if (offset <= 0) - { - offset = MIN_OFFSET; - } - else if (offset > MAX_OFFSET) - { - offset = MAX_OFFSET; - } - - var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); - files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); - return files; - } - - public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) - { - var files = new List(); - if (messageIds.IsNullOrEmpty()) return files; - - foreach (var messageId in messageIds) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) - { - continue; - } - - foreach (var file in Directory.GetFiles(dir)) - { - var contentType = GetFileContentType(file); - if (imageOnly && !_allowedTypes.Contains(contentType)) - { - continue; - } - - var fileName = Path.GetFileNameWithoutExtension(file); - var extension = Path.GetExtension(file); - var fileType = extension.Substring(1); - - var model = new MessageFileModel() - { - MessageId = messageId, - FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", - FileStorageUrl = file, - FileName = fileName, - FileType = fileType, - ContentType = contentType - }; - files.Add(model); - } - } - - return files; - } - - public string? GetMessageFile(string conversationId, string messageId, string fileName) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) - { - return null; - } - - var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); - return found; - } - - public void SaveMessageFiles(string conversationId, string messageId, List files) - { - if (files.IsNullOrEmpty()) return; - - var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); - if (string.IsNullOrEmpty(dir)) return; - - try - { - for (int i = 0; i < files.Count; i++) - { - var file = files[i]; - if (string.IsNullOrEmpty(file.FileData)) - { - continue; - } - - var (_, bytes) = GetFileInfoFromData(file.FileData); - var fileType = Path.GetExtension(file.FileName); - var fileName = $"{i + 1}{fileType}"; - Thread.Sleep(100); - File.WriteAllBytes(Path.Combine(dir, fileName), bytes); - } - } - catch (Exception ex) - { - _logger.LogError($"Error when saving conversation files: {ex.Message}"); - } - } - - public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) - { - if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; - - if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) - { - var prevDir = GetConversationFileDirectory(conversationId, targetMessageId); - var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId); - - if (Directory.Exists(prevDir)) - { - if (Directory.Exists(newDir)) - { - Directory.Delete(newDir, true); - } - - Directory.Move(prevDir, newDir); - } - } - - foreach ( var messageId in messageIds) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) continue; - - Thread.Sleep(100); - Directory.Delete(dir, true); - } - - return true; - } - - public bool DeleteConversationFiles(IEnumerable conversationIds) - { - if (conversationIds.IsNullOrEmpty()) return false; - - foreach (var conversationId in conversationIds) - { - var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) continue; - - Directory.Delete(convDir, true); - } - return true; - } - public (string, byte[]) GetFileInfoFromData(string data) { if (string.IsNullOrEmpty(data)) @@ -207,38 +63,6 @@ public bool DeleteConversationFiles(IEnumerable conversationIds) } #region Private methods - private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) - { - if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) - { - return string.Empty; - } - - var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); - if (!Directory.Exists(dir)) - { - if (createNewDir) - { - Directory.CreateDirectory(dir); - } - else - { - return string.Empty; - } - } - return dir; - } - - private string? FindConversationDirectory(string conversationId) - { - if (string.IsNullOrEmpty(conversationId)) return null; - - var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId); - if (!Directory.Exists(dir)) return null; - - return dir; - } - private string GetFileContentType(string filePath) { string contentType; @@ -250,5 +74,10 @@ private string GetFileContentType(string filePath) return contentType; } + + private bool ExistDirectory(string? dir) + { + return !string.IsNullOrEmpty(dir) && Directory.Exists(dir); + } #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs index cfae602ca..0c7fa2556 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs @@ -46,7 +46,7 @@ public IEnumerable GetMessageFiles([FromRoute] string conv } [HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")] - public async Task GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName) + public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName) { var fileService = _services.GetRequiredService(); var file = fileService.GetMessageFile(conversationId, messageId, fileName); @@ -54,7 +54,30 @@ public async Task GetMessageFile([FromRoute] string conversationI { return NotFound(); } + return BuildFileResult(file); + } + + [HttpPost("/user/avatar")] + public bool UploadUserAvatar([FromBody] BotSharpFile file) + { + var fileService = _services.GetRequiredService(); + return fileService.SaveUserAvatar(file); + } + + [HttpGet("/user/avatar")] + public IActionResult GetUserAvatar() + { + var fileService = _services.GetRequiredService(); + var file = fileService.GetUserAvatar(); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + return BuildFileResult(file); + } + private FileContentResult BuildFileResult(string file) + { using Stream stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); var bytes = new byte[stream.Length]; stream.Read(bytes, 0, (int)stream.Length); diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index eeb1a8a27..8d28cf035 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -19,6 +19,7 @@ public class UserViewModel public string Source { get; set; } [JsonPropertyName("external_id")] public string? ExternalId { get; set; } + public string Avatar { get; set; } = "/user/avatar"; [JsonPropertyName("create_date")] public DateTime CreateDate { get; set; } [JsonPropertyName("update_date")] @@ -47,7 +48,8 @@ public static UserViewModel FromUser(User user) Source = user.Source, ExternalId = user.ExternalId, CreateDate = user.CreatedTime, - UpdateDate = user.UpdatedTime + UpdateDate = user.UpdatedTime, + Avatar = "/user/avatar" }; } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index e20f86026..79cb803a9 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -14,13 +14,11 @@ public WebSocketsMiddleware(RequestDelegate next) public async Task Invoke(HttpContext httpContext) { - var request = httpContext.Request;; - var messageFileRegex = new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase); + var request = httpContext.Request; // web sockets cannot pass headers so we must take the access token from query param and // add it to the header before authentication middleware runs - if ((request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) - || messageFileRegex.IsMatch(request.Path.Value ?? string.Empty)) && + if ((VerifyChatHubRequest(request) || VerifyGetRequest(request)) && request.Query.TryGetValue("access_token", out var accessToken)) { request.Headers["Authorization"] = $"Bearer {accessToken}"; @@ -28,4 +26,20 @@ public async Task Invoke(HttpContext httpContext) await _next(httpContext); } + + private bool VerifyChatHubRequest(HttpRequest request) + { + return request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase); + } + + private bool VerifyGetRequest(HttpRequest request) + { + var regexes = new List + { + new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase), + new Regex(@"/user/avatar", RegexOptions.IgnoreCase) + }; + + return request.Method.IsEqualTo("GET") && regexes.Any(x => x.IsMatch(request.Path.Value ?? string.Empty)); + } }