diff --git a/qtc-net-server/Controllers/AuthController.cs b/qtc-net-server/Controllers/AuthController.cs index d1e3172..e5e7fc9 100644 --- a/qtc-net-server/Controllers/AuthController.cs +++ b/qtc-net-server/Controllers/AuthController.cs @@ -101,6 +101,15 @@ namespace qtc_api.Controllers var token = await _tokenService.GenerateAccessTokenAndRefreshToken(dbUser.Data, true, request.RememberMe); + // check if the new user tag can be removed (14 days or 2 weeks) + var now = DateTime.UtcNow; + if(now.Date > dbUser.Data.CreatedAt.Date.AddDays(14) && dbUser.Data.Tags.Contains("new_user")) + { + List _tagsList = [.. dbUser.Data.Tags]; + _tagsList.Remove("new_user"); + dbUser.Data.Tags = [.. _tagsList]; + } + await dataContext.SaveChangesAsync(); return Ok(token); } diff --git a/qtc-net-server/Controllers/TagController.cs b/qtc-net-server/Controllers/TagController.cs new file mode 100644 index 0000000..270b74a --- /dev/null +++ b/qtc-net-server/Controllers/TagController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Razor.TagHelpers; +using qtc_api.Services.BucketService; + +namespace qtc_api.Controllers +{ + [ApiController] + [Route("api/tags")] + public class TagController : ControllerBase + { + private readonly IBucketService _bucketService; + public TagController(IBucketService bucketService) + { + _bucketService = bucketService; + } + + [HttpGet] + [Route("{tagName}")] + [Authorize] + public async Task GetTagPicBytes(string tagName) + { + var _s3Res = await _bucketService.GetTagImageBytes(tagName); + if (_s3Res != null) + return new FileContentResult(_s3Res, "image/jpeg"); + else + { + return NotFound(); // TODO - code the legacy way of finding images here (mostly for self-hosters) + } + } + } +} diff --git a/qtc-net-server/Controllers/UsersController.cs b/qtc-net-server/Controllers/UsersController.cs index d49f6a7..ad6de22 100644 --- a/qtc-net-server/Controllers/UsersController.cs +++ b/qtc-net-server/Controllers/UsersController.cs @@ -73,10 +73,15 @@ namespace qtc_api.Controllers if(id != null && id == user.Id) { var updatedUser = await _userService.UpdateUserInfo(user); + if (updatedUser.Success && updatedUser.Data != null) + { + await _chatGWContext.Clients.All.SendAsync("RefreshUserLists"); + await _chatGWContext.Clients.All.SendAsync("RefreshContactsList"); + await _chatGWContext.Clients.User(updatedUser.Data.Id).SendAsync("UpdateCurrentUser"); - await _chatGWContext.Clients.All.SendAsync("RefreshUserLists"); - - return Ok(updatedUser); + return Ok(updatedUser); + } + else return Ok(); } else { return Unauthorized("You are not authorized to edit that user."); @@ -116,6 +121,7 @@ namespace qtc_api.Controllers stream.CopyTo(ms); await _cache.SetImageAsync(recordId, ms.ToArray(), TimeSpan.FromHours(1)); + await _chatGWContext.Clients.User(id).SendAsync("UpdateCurrentUser"); return Ok(response); } else { @@ -185,8 +191,12 @@ namespace qtc_api.Controllers { IEnumerable claims = identity.Claims; var id = claims.First().Value; - - if (id != null) return Ok(await _userService.AddCurrencyToUser(id, amount, isSpinClaim)); + var _res = await _userService.AddCurrencyToUser(id, amount, isSpinClaim); + if(_res.Success) + { + await _chatGWContext.Clients.User(id).SendAsync("UpdateCurrentUser"); + return Ok(_res); + } else return Ok(new ServiceResponse { Success = false, Message = "Identity Has No User ID" }); } else return Ok(new ServiceResponse { Success = false, Message = "No Identity" }); diff --git a/qtc-net-server/Dtos/User/UserInformationDto.cs b/qtc-net-server/Dtos/User/UserInformationDto.cs index 0e7fc03..e213521 100644 --- a/qtc-net-server/Dtos/User/UserInformationDto.cs +++ b/qtc-net-server/Dtos/User/UserInformationDto.cs @@ -13,5 +13,7 @@ public int Status { get; set; } = 0; public int CurrencyAmount { get; set; } = 0; public int ProfileCosmeticId { get; set; } = 0; + public string TextStatus { get; set; } = string.Empty; + public string[] Tags { get; set; } = []; } } diff --git a/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs index 461addd..d61a5ad 100644 --- a/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs +++ b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs @@ -7,5 +7,6 @@ public string Bio { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } = new DateTime(); public int ProfileCosmeticId { get; set; } = 0; + public string TextStatus { get; set; } = string.Empty; } } diff --git a/qtc-net-server/Hubs/ChatHub.cs b/qtc-net-server/Hubs/ChatHub.cs index bf25d24..0af5c9a 100644 --- a/qtc-net-server/Hubs/ChatHub.cs +++ b/qtc-net-server/Hubs/ChatHub.cs @@ -1,4 +1,5 @@ -using qtc_api.Services.RoomService; +using Microsoft.AspNetCore.Identity; +using qtc_api.Services.RoomService; using System.Text.Json; namespace qtc_api.Hubs @@ -9,17 +10,25 @@ namespace qtc_api.Hubs private IRoomService _roomService; private ILogger _logger; private DataContext _dataContext; + private ServerConfig _serverConfig; - private static readonly Dictionary> GroupUsers = []; - private static readonly Dictionary> ConnectedUsers = []; - private static readonly List OnlineUsers = []; + public static readonly Dictionary> GroupUsers = []; + public static readonly Dictionary> ConnectedUsers = []; + public static readonly List OnlineUsers = []; - public ChatHub(IUserService userService, IRoomService roomService, ILogger logger, DataContext dataContext) + private static User ServerUser = new() + { + Id = "0", + Username = "Server", + }; + + public ChatHub(IUserService userService, IRoomService roomService, ILogger logger, DataContext dataContext, ServerConfig serverConfig) { _userService = userService; _roomService = roomService; _logger = logger; _dataContext = dataContext; + _serverConfig = serverConfig; } public async override Task OnConnectedAsync() @@ -52,6 +61,7 @@ namespace qtc_api.Hubs if (user.Data.Status == 0) await UpdateStatusAsync(user.Data, 1); await Clients.Client(Context.ConnectionId).SendAsync("RefreshRoomList"); + await Clients.Client(Context.ConnectionId).SendAsync("ReceiveServerConfig", _serverConfig); } } } @@ -117,6 +127,7 @@ namespace qtc_api.Hubs await Clients.All.SendAsync("RefreshUserLists"); await Clients.All.SendAsync("RefreshContactsList"); Log($"Status Was Set To {res.Data.Status} On User {user.Username}"); + await Clients.Client(Context.ConnectionId).SendAsync("UpdateCurrentUser"); } else Log($"Something Went Wrong Setting The Status On User {user.Username}"); @@ -128,12 +139,12 @@ namespace qtc_api.Hubs { await Groups.AddToGroupAsync(Context.ConnectionId, room.Id); - await Clients.Group(room.Id).SendAsync("RoomMessage", $"Server: User {user.Username} Has Joined {room.Name}"); + await Clients.Group(room.Id).SendAsync("RoomMessage", ServerUser, $"User {user.Username} Has Joined {room.Name}", room); if (!GroupUsers.TryGetValue(room.Id, out _)) { GroupUsers.Add(room.Id, new List()); } GroupUsers[room.Id].Add(user); - await Clients.Group(room.Id).SendAsync("RoomUserList", GroupUsers[room.Id]); + await Clients.Group(room.Id).SendAsync("RoomUserList", room, GroupUsers[room.Id]); Log($"User {user.Username} Has Joined {room.Name}"); user.CurrentRoomId = room.Id; @@ -157,7 +168,7 @@ namespace qtc_api.Hubs { await Groups.AddToGroupAsync(Context.ConnectionId, room.Data.Id); - await Clients.Group(room.Data.Id).SendAsync("GuestJoin", username); + await Clients.Group(room.Data.Id).SendAsync("RoomMessage", ServerUser, $"Guest User {username} Has Joined {room.Data.Name}", room); room.Data.UserCount += 1; await _dataContext.SaveChangesAsync(); @@ -172,11 +183,11 @@ namespace qtc_api.Hubs { await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id); - await Clients.Group(room.Id).SendAsync("RoomMessage", $"Server: User {user.Username} Has Left {room.Name}"); + await Clients.Group(room.Id).SendAsync("RoomMessage", ServerUser, $"User {user.Username} Has Left {room.Name}", room); if (GroupUsers.TryGetValue(room.Id, out _)) GroupUsers[room.Id].Remove(GroupUsers[room.Id].FirstOrDefault(e => e.Id == user.Id)!); - await Clients.Group(room.Id).SendAsync("RoomUserList", GroupUsers[room.Id]); + await Clients.Group(room.Id).SendAsync("RoomUserList", room, GroupUsers[room.Id]); Log($"User {user.Username} Has Left {room.Name}"); user.CurrentRoomId = string.Empty; @@ -194,7 +205,7 @@ namespace qtc_api.Hubs [Authorize] public async Task SendMessageAsync(User user, Message message, Room room) { - await Clients.Group(room.Id).SendAsync("RoomMessage", $"{user.Username}: {message.Content}"); + await Clients.Group(room.Id).SendAsync("RoomMessage", user, $"{message.Content}", room); } [HubMethodName("SendDirectMessage")] @@ -207,7 +218,7 @@ namespace qtc_api.Hubs var connection = value.FirstOrDefault(); if(connection != null) { - UserInformationDto userInformationDto = new UserInformationDto { Id = user.Id, Username = user.Username, Bio = user.Bio, Role = user.Role, Status = user.Status, CreatedAt = user.CreatedAt, DateOfBirth = user.DateOfBirth, ProfilePicture = user.ProfilePicture }; + UserInformationDto userInformationDto = new UserInformationDto { Id = user.Id, Username = user.Username, Bio = user.Bio, Role = user.Role, Status = user.Status, CreatedAt = user.CreatedAt, DateOfBirth = user.DateOfBirth, ProfilePicture = user.ProfilePicture, ProfileCosmeticId = user.ActiveProfileCosmetic }; await Clients.Client(connection).SendAsync("ReceiveDirectMessage", message, userInformationDto); return; } diff --git a/qtc-net-server/Models/User.cs b/qtc-net-server/Models/User.cs index 178b2db..3098e7c 100644 --- a/qtc-net-server/Models/User.cs +++ b/qtc-net-server/Models/User.cs @@ -19,6 +19,8 @@ public int ActiveProfileCosmetic { get; set; } = 0; public string CurrentRoomId { get; set; } = string.Empty; public DateTime LastLogin { get; set; } + public string TextStatus { get; set; } = string.Empty; + public string[] Tags { get; set; } = []; public virtual IEnumerable? RefreshTokens { get; } public virtual IEnumerable? ContactsMade { get; } diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index 00b6e58..778003f 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -19,6 +19,7 @@ using qtc_api.Services.GameRoomService; using qtc_api.Services.StoreService; using qtc_api.Services.EmailService; using qtc_api.Services.BucketService; +using System.Text.Json; var builder = WebApplication.CreateBuilder(args); @@ -68,6 +69,28 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(provider => GetServerConfig()); + +ServerConfig GetServerConfig() +{ + if (!File.Exists("./Resources/ServerConfig.json")) + throw new Exception("Server Config Doesn't Exist. Please Refer To The Repo For Examples."); + else + { + try + { + var _jsonText = File.ReadAllText("./Resources/ServerConfig.json"); + ServerConfig? _config = JsonSerializer.Deserialize(_jsonText); + if (_config != null) + return _config; + else throw new Exception("Invalid Server Config. Please Refer To The Repo For Examples."); + } + catch (JsonException) + { + throw new Exception("Invalid Server Config. Please Refer To The Repo For Examples."); + } + } +} builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/qtc-net-server/Services/BucketService/BucketService.cs b/qtc-net-server/Services/BucketService/BucketService.cs index 3dcf41f..767b1f7 100644 --- a/qtc-net-server/Services/BucketService/BucketService.cs +++ b/qtc-net-server/Services/BucketService/BucketService.cs @@ -11,6 +11,7 @@ namespace qtc_api.Services.BucketService public string AccessKey { get; private set; } public string SecretKey { get; private set; } public string ProfileImagesBucketName { get; private set; } + public string CDNBucketName { get; private set; } public string ImagesBucketName { get; private set; } private static AmazonS3Client S3Client; @@ -27,6 +28,7 @@ namespace qtc_api.Services.BucketService var accessKey = _config["S3Config:S3AccessKey"]; var secretKey = _config["S3Config:S3SecretKey"]; var profileImagesBucket = _config["S3Config:S3ProfileImagesBucket"]; + var cdnBucket = _config["S3Config:S3CDNBucket"]; var imagesBucket = _config["S3Config:S3ImagesBucket"]; var enabled = _config.GetValue("S3Config:S3Enabled"); @@ -34,6 +36,7 @@ namespace qtc_api.Services.BucketService if (accessKey != null) AccessKey = accessKey; if (secretKey != null) SecretKey = secretKey; if (profileImagesBucket != null) ProfileImagesBucketName = profileImagesBucket; + if (cdnBucket != null) CDNBucketName = cdnBucket; if (imagesBucket != null) ImagesBucketName = imagesBucket; Enabled = enabled; @@ -89,12 +92,37 @@ namespace qtc_api.Services.BucketService if (response != null) { - using (var stream = response.ResponseStream) - using (var ms = new MemoryStream()) - { - stream.CopyTo(ms); - return ms.ToArray(); - } + using var stream = response.ResponseStream; + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + else return default; + } catch (AmazonS3Exception ex) + { + _logger.LogError($"S3 Bucket Threw An Exception\n{ex.Message}\n{ex.StackTrace}"); + return default; + } + } + + public async Task GetTagImageBytes(string tagName) + { + if (!Enabled) + { + _logger.LogWarning("Not Using S3 Bucket. Performance May Be Degraded."); + return default; + } + + try + { + var response = await S3Client.GetObjectAsync(CDNBucketName, $@"tags/{tagName}.png"); + + if (response != null) + { + using var stream = response.ResponseStream; + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); } else return default; } catch (AmazonS3Exception ex) diff --git a/qtc-net-server/Services/BucketService/IBucketService.cs b/qtc-net-server/Services/BucketService/IBucketService.cs index eaa0d72..cf5276f 100644 --- a/qtc-net-server/Services/BucketService/IBucketService.cs +++ b/qtc-net-server/Services/BucketService/IBucketService.cs @@ -9,6 +9,7 @@ public string ImagesBucketName { get; } public Task PutProfileImage(string userId, string imageName, byte[] imageBytes); public Task GetProfileImageBytes(string userId, string imageName); + public Task GetTagImageBytes(string tagName); public Task DeleteProfileImage(string userId, string imageName); } } diff --git a/qtc-net-server/Services/UserService/UserService.cs b/qtc-net-server/Services/UserService/UserService.cs index 9930ffb..c29a54e 100644 --- a/qtc-net-server/Services/UserService/UserService.cs +++ b/qtc-net-server/Services/UserService/UserService.cs @@ -35,6 +35,9 @@ namespace qtc_api.Services.UserService user.Role = _configuration["Jwt:DefaultUserRole"]!; user.CreatedAt = DateTime.UtcNow; + // add new user tag + user.Tags = ["new_user"]; + try { var cUser = userList.FirstOrDefault(x => x.Email == user.Email); @@ -174,19 +177,30 @@ namespace qtc_api.Services.UserService if(user != null) { - var dto = new UserInformationDto(); + var dto = new UserInformationDto + { + Id = user.Id, + Username = user.Username, + ProfilePicture = user.ProfilePicture, + Role = user.Role, + Bio = user.Bio, + DateOfBirth = user.DateOfBirth, + CreatedAt = user.CreatedAt, + Status = user.Status, + CurrencyAmount = user.CurrencyAmount, + ProfileCosmeticId = user.ActiveProfileCosmetic, + LastLogin = user.LastLogin, + TextStatus = user.TextStatus, + Tags = user.Tags + }; - dto.Id = user.Id; - dto.Username = user.Username; - dto.ProfilePicture = user.ProfilePicture; - dto.Role = user.Role; - dto.Bio = user.Bio; - dto.DateOfBirth = user.DateOfBirth; - dto.CreatedAt = user.CreatedAt; - dto.Status = user.Status; - dto.CurrencyAmount = user.CurrencyAmount; - dto.ProfileCosmeticId = user.ActiveProfileCosmetic; - dto.LastLogin = user.LastLogin; + // always add admin tag if user is admin + if(user.Role == "Admin") + { + List _tagList = [.. dto.Tags]; + _tagList.Add("admin"); + dto.Tags = [.. _tagList]; + } serviceResponse.Success = true; serviceResponse.Data = dto; @@ -219,21 +233,24 @@ namespace qtc_api.Services.UserService dbUser.Bio = request.Bio; dbUser.DateOfBirth = request.DateOfBirth; dbUser.ActiveProfileCosmetic = request.ProfileCosmeticId; + dbUser.TextStatus = request.TextStatus; await _dataContext.SaveChangesAsync(); - var infoDto = new UserInformationDto(); - - infoDto.Id = dbUser.Id; - infoDto.Username = request.Username; - infoDto.ProfilePicture = dbUser.ProfilePicture; - infoDto.Bio = request.Bio; - infoDto.Role = dbUser.Role; - infoDto.DateOfBirth = request.DateOfBirth; - infoDto.CreatedAt = dbUser.CreatedAt; - infoDto.Status = dbUser.Status; - infoDto.CurrencyAmount = dbUser.CurrencyAmount; - infoDto.ProfileCosmeticId = request.ProfileCosmeticId; + var infoDto = new UserInformationDto + { + Id = dbUser.Id, + Username = request.Username, + ProfilePicture = dbUser.ProfilePicture, + Bio = request.Bio, + Role = dbUser.Role, + DateOfBirth = request.DateOfBirth, + CreatedAt = dbUser.CreatedAt, + Status = dbUser.Status, + CurrencyAmount = dbUser.CurrencyAmount, + ProfileCosmeticId = request.ProfileCosmeticId, + TextStatus = request.TextStatus + }; serviceResponse.Success = true; serviceResponse.Data = infoDto; diff --git a/qtc-net-server/appsettings.json b/qtc-net-server/appsettings.json index 3e468d0..35e4a46 100644 --- a/qtc-net-server/appsettings.json +++ b/qtc-net-server/appsettings.json @@ -20,6 +20,7 @@ "S3AccessKey": "", "S3SecretKey": "", "S3ProfileImagesBucket": "qtcnet-profileimages", + "S3CDNBucket": "qtcnet-cdn", "S3ImagesBucket": "qtcnet-images" }, "Logging": {