Add core models, DTOs, and services for EchoHub chat application

- Created User, Channel, Message, and ServerInfo models with necessary properties.
- Added DTOs for user profiles, server information, and message handling.
- Implemented JWT authentication service for user login and token generation.
- Developed Entity Framework Core DbContext for database interactions.
- Introduced SignalR ChatHub for real-time messaging and presence tracking.
- Implemented file storage and image to ASCII conversion services.
- Set up CORS and middleware for API endpoints.
- Created launch settings and development configuration for server and web projects.
This commit is contained in:
HueByte
2026-02-18 13:52:19 +01:00
parent 20a341947a
commit cfa8b90d04
47 changed files with 4596 additions and 0 deletions
@@ -0,0 +1,46 @@
namespace EchoHub.Server.Services;
public class FileStorageService
{
private readonly string _storagePath;
public FileStorageService(IConfiguration configuration)
{
_storagePath = configuration["Storage:Path"] ?? "./uploads";
if (!Directory.Exists(_storagePath))
{
Directory.CreateDirectory(_storagePath);
}
}
public async Task<(string fileId, string filePath)> SaveFileAsync(Stream stream, string fileName)
{
var fileId = Guid.NewGuid().ToString();
var extension = Path.GetExtension(fileName);
var storedFileName = $"{fileId}{extension}";
var filePath = Path.Combine(_storagePath, storedFileName);
using var fileStream = File.Create(filePath);
await stream.CopyToAsync(fileStream);
return (fileId, filePath);
}
public string? GetFilePath(string fileId)
{
var files = Directory.GetFiles(_storagePath, $"{fileId}.*");
return files.Length > 0 ? files[0] : null;
}
public void DeleteFile(string fileId)
{
var filePath = GetFilePath(fileId);
if (filePath is not null && File.Exists(filePath))
{
File.Delete(filePath);
}
}
}
@@ -0,0 +1,41 @@
using System.Text;
using EchoHub.Core.Constants;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace EchoHub.Server.Services;
public class ImageToAsciiService
{
private static readonly char[] AsciiChars = " .:-=+*#%@".ToCharArray();
public string ConvertToAscii(Stream imageStream, int width = HubConstants.AsciiArtWidth, int height = HubConstants.AsciiArtHeight)
{
using var image = Image.Load<Rgba32>(imageStream);
image.Mutate(x => x.Resize(width, height));
var sb = new StringBuilder();
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
var pixel = image[x, y];
var brightness = 0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B;
// Map brightness (0-255) to ASCII char index (inverted: dark pixels get dense chars)
var index = (int)((brightness / 255.0) * (AsciiChars.Length - 1));
sb.Append(AsciiChars[index]);
}
if (y < image.Height - 1)
{
sb.AppendLine();
}
}
return sb.ToString();
}
}
@@ -0,0 +1,113 @@
using System.Collections.Concurrent;
namespace EchoHub.Server.Services;
public class PresenceTracker
{
private readonly ConcurrentDictionary<string, (Guid userId, string username)> _connections = new();
private readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
private readonly ConcurrentDictionary<string, HashSet<string>> _userChannels = new();
private readonly object _lock = new();
public void UserConnected(string connectionId, Guid userId, string username)
{
_connections[connectionId] = (userId, username);
lock (_lock)
{
if (!_userConnections.TryGetValue(username, out var connections))
{
connections = new HashSet<string>();
_userConnections[username] = connections;
}
connections.Add(connectionId);
}
}
public string? UserDisconnected(string connectionId)
{
if (!_connections.TryRemove(connectionId, out var userInfo))
return null;
var username = userInfo.username;
lock (_lock)
{
if (_userConnections.TryGetValue(username, out var connections))
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_userConnections.TryRemove(username, out _);
_userChannels.TryRemove(username, out _);
}
}
}
return username;
}
public void JoinChannel(string username, string channelName)
{
lock (_lock)
{
if (!_userChannels.TryGetValue(username, out var channels))
{
channels = new HashSet<string>();
_userChannels[username] = channels;
}
channels.Add(channelName);
}
}
public void LeaveChannel(string username, string channelName)
{
lock (_lock)
{
if (_userChannels.TryGetValue(username, out var channels))
{
channels.Remove(channelName);
}
}
}
public List<string> GetOnlineUsersInChannel(string channelName)
{
var users = new List<string>();
lock (_lock)
{
foreach (var (username, channels) in _userChannels)
{
if (channels.Contains(channelName))
{
users.Add(username);
}
}
}
return users;
}
public List<string> GetChannelsForUser(string username)
{
lock (_lock)
{
if (_userChannels.TryGetValue(username, out var channels))
{
return channels.ToList();
}
}
return [];
}
public bool IsOnline(string username)
{
return _userConnections.TryGetValue(username, out var connections) && connections.Count > 0;
}
}