diff --git a/src/Application/ActivityLogs/Models/ActivityLogMetadataEnrichedViewModel.cs b/src/Application/ActivityLogs/Models/ActivityLogMetadataEnrichedViewModel.cs new file mode 100644 index 000000000..390efbc39 --- /dev/null +++ b/src/Application/ActivityLogs/Models/ActivityLogMetadataEnrichedViewModel.cs @@ -0,0 +1,12 @@ +using Crpg.Application.Characters.Models; +using Crpg.Application.Clans.Models; +using Crpg.Application.Users.Models; + +namespace Crpg.Application.ActivityLogs.Models; + +public record ActivityLogMetadataEnrichedViewModel +{ + public IList Clans { get; init; } = Array.Empty(); + public IList Users { get; init; } = Array.Empty(); + public IList Characters { get; init; } = Array.Empty(); +} diff --git a/src/Application/ActivityLogs/Models/ActivityLogWithDictViewModel.cs b/src/Application/ActivityLogs/Models/ActivityLogWithDictViewModel.cs new file mode 100644 index 000000000..7592850d9 --- /dev/null +++ b/src/Application/ActivityLogs/Models/ActivityLogWithDictViewModel.cs @@ -0,0 +1,7 @@ +using Crpg.Application.ActivityLogs.Models; + +public record ActivityLogWithDictViewModel +{ + public IList ActivityLogs { get; init; } = Array.Empty(); + public ActivityLogMetadataEnrichedViewModel Dict { get; init; } = new(); +} diff --git a/src/Application/ActivityLogs/Queries/GetActivityLogsQuery.cs b/src/Application/ActivityLogs/Queries/GetActivityLogsQuery.cs index e82f9ffda..fba54ff2d 100644 --- a/src/Application/ActivityLogs/Queries/GetActivityLogsQuery.cs +++ b/src/Application/ActivityLogs/Queries/GetActivityLogsQuery.cs @@ -1,15 +1,19 @@ using AutoMapper; using Crpg.Application.ActivityLogs.Models; +using Crpg.Application.Characters.Models; +using Crpg.Application.Clans.Models; using Crpg.Application.Common.Interfaces; using Crpg.Application.Common.Mediator; using Crpg.Application.Common.Results; +using Crpg.Application.Common.Services; +using Crpg.Application.Users.Models; using Crpg.Domain.Entities.ActivityLogs; using FluentValidation; using Microsoft.EntityFrameworkCore; namespace Crpg.Application.ActivityLogs.Queries; -public record GetActivityLogsQuery : IMediatorRequest> +public record GetActivityLogsQuery : IMediatorRequest { public DateTime From { get; init; } public DateTime To { get; init; } @@ -25,18 +29,20 @@ public Validator() } } - internal class Handler : IMediatorRequestHandler> + internal class Handler : IMediatorRequestHandler { private readonly ICrpgDbContext _db; private readonly IMapper _mapper; + private readonly IActivityLogService _activityLogService; - public Handler(ICrpgDbContext db, IMapper mapper) + public Handler(ICrpgDbContext db, IMapper mapper, IActivityLogService activityLogService) { _db = db; _mapper = mapper; + _activityLogService = activityLogService; } - public async Task>> Handle(GetActivityLogsQuery req, + public async Task> Handle(GetActivityLogsQuery req, CancellationToken cancellationToken) { var activityLogs = await _db.ActivityLogs @@ -48,8 +54,24 @@ public async Task>> Handle(GetActivityLogsQue && (req.Types.Length == 0 || req.Types.Contains(l.Type))) .OrderByDescending(l => l.CreatedAt) .Take(1000) - .ToArrayAsync(cancellationToken); - return new(_mapper.Map>(activityLogs)); + .ToListAsync(cancellationToken); + + var entitiesFromMetadata = _activityLogService.ExtractEntitiesFromMetadata(activityLogs); + + var clans = await _db.Clans.Where(c => entitiesFromMetadata.clansIds.Contains(c.Id)).ToArrayAsync(); + var users = await _db.Users.Where(u => entitiesFromMetadata.usersIds.Contains(u.Id)).ToArrayAsync(); + var characters = await _db.Characters.Where(c => entitiesFromMetadata.charactersIds.Contains(c.Id)).ToArrayAsync(); + + return new(new ActivityLogWithDictViewModel() + { + ActivityLogs = _mapper.Map>(activityLogs), + Dict = new() + { + Clans = _mapper.Map>(clans), + Users = _mapper.Map>(users), + Characters = _mapper.Map>(characters), + }, + }); } } } diff --git a/src/Application/Characters/Commands/RewardCharacterCommand.cs b/src/Application/Characters/Commands/RewardCharacterCommand.cs index 08a1bc0b4..6c1fdcf6e 100644 --- a/src/Application/Characters/Commands/RewardCharacterCommand.cs +++ b/src/Application/Characters/Commands/RewardCharacterCommand.cs @@ -35,6 +35,7 @@ internal class Handler : IMediatorRequestHandler> Handle(RewardCharacterCommand req, _characterService.GiveExperience(character, req.Experience, useExperienceMultiplier: false); } - _db.ActivityLogs.Add(_activityLogService.CreateCharacterRewardedLog(req.UserId, req.ActorUserId, req.CharacterId, req.Experience)); + var activityLog = _activityLogService.CreateCharacterRewardedLog(req.UserId, req.ActorUserId, req.CharacterId, req.Experience); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateCharacterRewardedToUserNotification(req.UserId, activityLog.Id)); await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("Character '{0}' rewarded", req.CharacterId); diff --git a/src/Application/Characters/Models/CharacterPublicCompetitiveViewModel.cs b/src/Application/Characters/Models/CharacterPublicCompetitiveViewModel.cs new file mode 100644 index 000000000..b42c4d160 --- /dev/null +++ b/src/Application/Characters/Models/CharacterPublicCompetitiveViewModel.cs @@ -0,0 +1,14 @@ +using Crpg.Application.Common.Mappings; +using Crpg.Application.Users.Models; +using Crpg.Domain.Entities.Characters; + +namespace Crpg.Application.Characters.Models; + +public record CharacterPublicCompetitiveViewModel : IMapFrom +{ + public int Id { get; init; } + public int Level { get; init; } + public CharacterClass Class { get; init; } + public CharacterRatingViewModel Rating { get; init; } = new(); + public UserPublicViewModel User { get; init; } = new(); +} diff --git a/src/Application/Characters/Models/CharacterPublicViewModel.cs b/src/Application/Characters/Models/CharacterPublicViewModel.cs index df7eb5f90..46355f67e 100644 --- a/src/Application/Characters/Models/CharacterPublicViewModel.cs +++ b/src/Application/Characters/Models/CharacterPublicViewModel.cs @@ -1,14 +1,12 @@ using Crpg.Application.Common.Mappings; -using Crpg.Application.Users.Models; using Crpg.Domain.Entities.Characters; namespace Crpg.Application.Characters.Models; public record CharacterPublicViewModel : IMapFrom { - public int Id { get; init; } - public int Level { get; init; } - public CharacterClass Class { get; init; } - public CharacterRatingViewModel Rating { get; init; } = new(); - public UserPublicViewModel User { get; init; } = new(); + public int Id { get; init; } + public int Level { get; init; } + public string Name { get; init; } = string.Empty; + public CharacterClass Class { get; init; } } diff --git a/src/Application/Characters/Queries/GetLeaderboardQuery.cs b/src/Application/Characters/Queries/GetLeaderboardQuery.cs index 7a64d80a8..b303b0108 100644 --- a/src/Application/Characters/Queries/GetLeaderboardQuery.cs +++ b/src/Application/Characters/Queries/GetLeaderboardQuery.cs @@ -11,12 +11,12 @@ namespace Crpg.Application.Characters.Queries; -public record GetLeaderboardQuery : IMediatorRequest> +public record GetLeaderboardQuery : IMediatorRequest> { public Region? Region { get; set; } public CharacterClass? CharacterClass { get; set; } - internal class Handler : IMediatorRequestHandler> + internal class Handler : IMediatorRequestHandler> { private readonly ICrpgDbContext _db; private readonly IMapper _mapper; @@ -29,11 +29,11 @@ public Handler(ICrpgDbContext db, IMapper mapper, IMemoryCache cache) _cache = cache; } - public async Task>> Handle(GetLeaderboardQuery req, CancellationToken cancellationToken) + public async Task>> Handle(GetLeaderboardQuery req, CancellationToken cancellationToken) { string cacheKey = GetCacheKey(req); - if (_cache.TryGetValue(cacheKey, out IList? results) == false) + if (_cache.TryGetValue(cacheKey, out IList? results) == false) { // Todo: use DistinctBy here when EfCore implements it (does not work for now: https://github.com/dotnet/efcore/issues/27470 ) var topRatedCharactersByRegion = await _db.Characters @@ -42,10 +42,10 @@ public async Task>> Handle(GetLeaderboard .Where(c => (req.Region == null || req.Region == c.User!.Region) && (req.CharacterClass == null || req.CharacterClass == c.Class)) .Take(500) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(_mapper.ConfigurationProvider) .ToArrayAsync(cancellationToken); - IList data = topRatedCharactersByRegion.DistinctBy(c => c.User.Id).Take(50).ToList(); + IList data = topRatedCharactersByRegion.DistinctBy(c => c.User.Id).Take(50).ToList(); var cacheOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(1)); diff --git a/src/Application/Clans/Commands/Armory/AddItemToClanArmoryCommand.cs b/src/Application/Clans/Commands/Armory/AddItemToClanArmoryCommand.cs index afc569c32..97c8f9112 100644 --- a/src/Application/Clans/Commands/Armory/AddItemToClanArmoryCommand.cs +++ b/src/Application/Clans/Commands/Armory/AddItemToClanArmoryCommand.cs @@ -22,12 +22,10 @@ internal class Handler : IMediatorRequestHandler> Handle(AddItemToClanArmoryCom return new(result.Errors); } - _db.ActivityLogs.Add(_activityLogService.CreateAddItemToClanArmory(user.Id, clan.Id, req.UserItemId)); - await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' added item '{1}' to the armory '{2}'", req.UserId, req.UserItemId, req.ClanId); diff --git a/src/Application/Clans/Commands/Armory/BorrowItemFromClanArmoryCommand.cs b/src/Application/Clans/Commands/Armory/BorrowItemFromClanArmoryCommand.cs index 88923504d..abcbc4794 100644 --- a/src/Application/Clans/Commands/Armory/BorrowItemFromClanArmoryCommand.cs +++ b/src/Application/Clans/Commands/Armory/BorrowItemFromClanArmoryCommand.cs @@ -22,12 +22,13 @@ internal class Handler : IMediatorRequestHandler> Handle(BorrowItemFrom return new(result.Errors); } - _db.ActivityLogs.Add(_activityLogService.CreateBorrowItemFromClanArmory(user.Id, clan.Id, req.UserItemId)); - await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' borrowed item '{1}' from the armory '{2}'", req.UserId, req.UserItemId, req.ClanId); diff --git a/src/Application/Clans/Commands/Armory/RemoveItemFromClanArmoryCommand.cs b/src/Application/Clans/Commands/Armory/RemoveItemFromClanArmoryCommand.cs index b12a274f9..f5a2066de 100644 --- a/src/Application/Clans/Commands/Armory/RemoveItemFromClanArmoryCommand.cs +++ b/src/Application/Clans/Commands/Armory/RemoveItemFromClanArmoryCommand.cs @@ -19,12 +19,10 @@ internal class Handler : IMediatorRequestHandler(); private readonly ICrpgDbContext _db; - private readonly IActivityLogService _activityLogService; private readonly IClanService _clanService; - public Handler(ICrpgDbContext db, IActivityLogService activityLogService, IClanService clanService) + public Handler(ICrpgDbContext db, IClanService clanService) { - _activityLogService = activityLogService; _db = db; _clanService = clanService; } @@ -54,8 +52,6 @@ public async Task Handle(RemoveItemFromClanArmoryCommand req, Cancellati return new(result.Errors); } - _db.ActivityLogs.Add(_activityLogService.CreateRemoveItemFromClanArmory(user.Id, clan.Id, req.UserItemId)); - await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' removed item '{1}' from the armory '{2}'", req.UserId, req.UserItemId, req.ClanId); diff --git a/src/Application/Clans/Commands/Armory/ReturnItemToClanArmoryCommand.cs b/src/Application/Clans/Commands/Armory/ReturnItemToClanArmoryCommand.cs index 927402542..2c24f62f0 100644 --- a/src/Application/Clans/Commands/Armory/ReturnItemToClanArmoryCommand.cs +++ b/src/Application/Clans/Commands/Armory/ReturnItemToClanArmoryCommand.cs @@ -19,12 +19,10 @@ internal class Handler : IMediatorRequestHandler private static readonly ILogger Logger = LoggerFactory.CreateLogger(); private readonly ICrpgDbContext _db; - private readonly IActivityLogService _activityLogService; private readonly IClanService _clanService; - public Handler(ICrpgDbContext db, IActivityLogService activityLogService, IClanService clanService) + public Handler(ICrpgDbContext db, IClanService clanService) { - _activityLogService = activityLogService; _db = db; _clanService = clanService; } @@ -54,8 +52,6 @@ public async Task Handle(ReturnItemToClanArmoryCommand req, Cancellation return new(result.Errors); } - _db.ActivityLogs.Add(_activityLogService.CreateReturnItemToClanArmory(user.Id, clan.Id, req.UserItemId)); - await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' returned item '{1}' to the armory '{2}'", req.UserId, req.UserItemId, req.ClanId); diff --git a/src/Application/Clans/Commands/Armory/ReturnUnusedItemsToClanArmoryCommand.cs b/src/Application/Clans/Commands/Armory/ReturnUnusedItemsToClanArmoryCommand.cs index b3b7afe25..9632cb6de 100644 --- a/src/Application/Clans/Commands/Armory/ReturnUnusedItemsToClanArmoryCommand.cs +++ b/src/Application/Clans/Commands/Armory/ReturnUnusedItemsToClanArmoryCommand.cs @@ -1,6 +1,7 @@ using Crpg.Application.Common.Interfaces; using Crpg.Application.Common.Mediator; using Crpg.Application.Common.Results; +using Crpg.Application.Common.Services; using Crpg.Sdk.Abstractions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -16,11 +17,15 @@ internal class Handler : IMediatorRequestHandler Handle(ReturnUnusedItemsToClanArmoryCommand req, CancellationToken cancellationToken) @@ -40,6 +45,12 @@ public async Task Handle(ReturnUnusedItemsToClanArmoryCommand req, Cance var equipped = u.ClanMembership!.ArmoryBorrowedItems.SelectMany(bi => bi.UserItem!.EquippedItems); _db.EquippedItems.RemoveRange(equipped); _db.ClanArmoryBorrowedItems.RemoveRange(u.ClanMembership!.ArmoryBorrowedItems); + foreach (var bi in u.ClanMembership!.ArmoryBorrowedItems) + { + var activityLog = _activityLogService.CreateReturnItemToClanArmoryLog(bi.UserItem!.UserId, u.ClanMembership.ClanId, bi.UserItem!); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateClanArmoryRemoveItemToBorrowerNotification(u.Id, activityLog.Id)); + } } await _db.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Clans/Commands/CreateClanCommand.cs b/src/Application/Clans/Commands/CreateClanCommand.cs index 70aa76a3c..7763cc8cd 100644 --- a/src/Application/Clans/Commands/CreateClanCommand.cs +++ b/src/Application/Clans/Commands/CreateClanCommand.cs @@ -5,6 +5,7 @@ using Crpg.Application.Common.Interfaces; using Crpg.Application.Common.Mediator; using Crpg.Application.Common.Results; +using Crpg.Application.Common.Services; using Crpg.Domain.Entities; using Crpg.Domain.Entities.Clans; using FluentValidation; @@ -73,10 +74,13 @@ internal class Handler : IMediatorRequestHandler> Handle(CreateClanCommand req, CancellationToken cancellationToken) @@ -126,6 +130,7 @@ public async Task> Handle(CreateClanCommand req, Cancellat }; _db.Clans.Add(clan); + _db.ActivityLogs.Add(_activityLogService.CreateClanCreatedLog(req.UserId, clan.Id)); await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' created clan '[{1}] {2}' ({3})", req.UserId, req.Tag, req.Name, clan.Id); return new(_mapper.Map(clan)); diff --git a/src/Application/Clans/Commands/InviteClanMemberCommand.cs b/src/Application/Clans/Commands/InviteClanMemberCommand.cs index a287f8ff8..ccb84396c 100644 --- a/src/Application/Clans/Commands/InviteClanMemberCommand.cs +++ b/src/Application/Clans/Commands/InviteClanMemberCommand.cs @@ -28,12 +28,16 @@ internal class Handler : IMediatorRequestHandler> Handle(InviteClanMemberCommand req, CancellationToken cancellationToken) @@ -81,7 +85,23 @@ private async Task> RequestToJoinClan(User user, Status = ClanInvitationStatus.Pending, }; _db.ClanInvitations.Add(invitation); + + var createClanInvitationActivityLog = _activityLogService.CreateClanApplicationCreatedLog(user.Id, clanId); + _db.ActivityLogs.Add(createClanInvitationActivityLog); + + _db.UserNotifications.Add(_userNotificationService.CreateClanApplicationCreatedToUserNotification(user.Id, createClanInvitationActivityLog.Id)); + + var clanOfficers = await _clanService.GetClanOfficers(_db, clanId, cancellationToken); + if (clanOfficers.Errors == null && clanOfficers.Data != null) + { + foreach (var officer in clanOfficers.Data) + { + _db.UserNotifications.Add(_userNotificationService.CreateClanApplicationCreatedToOfficersNotification(officer.UserId, createClanInvitationActivityLog.Id)); + } + } + await _db.SaveChangesAsync(cancellationToken); + Logger.LogInformation("User '{0}' requested to join clan '{1}'", user.Id, clanId); return new(_mapper.Map(invitation)); } diff --git a/src/Application/Clans/Commands/KickClanMemberCommand.cs b/src/Application/Clans/Commands/KickClanMemberCommand.cs index 6b66c2011..8db1ecf7c 100644 --- a/src/Application/Clans/Commands/KickClanMemberCommand.cs +++ b/src/Application/Clans/Commands/KickClanMemberCommand.cs @@ -20,11 +20,15 @@ internal class Handler : IMediatorRequestHandler private readonly ICrpgDbContext _db; private readonly IClanService _clanService; + private readonly IActivityLogService _activityLogService; + private readonly IUserNotificationService _userNotificationService; - public Handler(ICrpgDbContext db, IClanService clanService) + public Handler(ICrpgDbContext db, IClanService clanService, IActivityLogService activityLogService, IUserNotificationService userNotificationService) { _db = db; _clanService = clanService; + _activityLogService = activityLogService; + _userNotificationService = userNotificationService; } public async Task Handle(KickClanMemberCommand req, CancellationToken cancellationToken) @@ -63,6 +67,11 @@ public async Task Handle(KickClanMemberCommand req, CancellationToken ca } _db.ClanMembers.Remove(kickedUser.ClanMembership); + + var clanMemberKickedActivityLog = _activityLogService.CreateClanMemberKickedLog(req.KickedUserId, req.ClanId, req.UserId); + _db.ActivityLogs.Add(clanMemberKickedActivityLog); + _db.UserNotifications.Add(_userNotificationService.CreateClanMemberKickedToExMemberNotification(req.KickedUserId, clanMemberKickedActivityLog.Id)); + await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' kicked user '{1}' out of clan '{2}'", req.UserId, req.KickedUserId, req.ClanId); diff --git a/src/Application/Clans/Commands/RespondClanInvitationCommand.cs b/src/Application/Clans/Commands/RespondClanInvitationCommand.cs index 5ef30a461..b2287dce2 100644 --- a/src/Application/Clans/Commands/RespondClanInvitationCommand.cs +++ b/src/Application/Clans/Commands/RespondClanInvitationCommand.cs @@ -26,12 +26,16 @@ internal class Handler : IMediatorRequestHandler> Handle(RespondClanInvitationCommand req, CancellationToken cancellationToken) @@ -103,6 +107,10 @@ public async Task> Handle(RespondClanInvitationC } else // Request { + var activityLog = _activityLogService.CreateClanApplicationDeclinedLog(user.Id, invitation.ClanId); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateClanApplicationDeclinedToUserNotification(user.Id, activityLog.Id)); + await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' declined request to join '{1}' from user '{2}' to join clan '{3}'", inviter.Id, invitation.Id, invitee.Id, invitation.ClanId); } @@ -129,30 +137,32 @@ public async Task> Handle(RespondClanInvitationC invitation.Status = ClanInvitationStatus.Accepted; await _db.SaveChangesAsync(cancellationToken); - if (oldClanId == null) + + if (invitation.Type == ClanInvitationType.Offer) // TODO: implement offer ui { - if (invitation.Type == ClanInvitationType.Offer) + if (oldClanId == null) { - Logger.LogInformation("User '{0}' accepted invitation '{1}' from user '{2}' to join clan '{3}'", - invitee.Id, invitation.Id, inviter.Id, invitation.ClanId); + Logger.LogInformation("User '{0}' accepted invitation '{1}' from user '{2}' to join clan '{3}'", invitee.Id, invitation.Id, inviter.Id, invitation.ClanId); } - else // Request + else { - Logger.LogInformation("User '{0}' accepted request '{1}' from user '{2}' to join clan '{3}'", - inviter.Id, invitation.Id, invitee.Id, invitation.ClanId); + Logger.LogInformation("User '{0}' left clan '{1}' and accepted invitation '{2}' from user '{3}' to join clan '{4}'", invitee.Id, oldClanId, invitation.Id, inviter.Id, invitation.ClanId); } } - else + else // Request { - if (invitation.Type == ClanInvitationType.Offer) + var invitationAcceptedActivityLog = _activityLogService.CreateClanApplicationAcceptedLog(user.Id, invitation.ClanId); + _db.ActivityLogs.Add(invitationAcceptedActivityLog); + _db.UserNotifications.Add(_userNotificationService.CreateClanApplicationAcceptedToUserNotification(invitee.Id, invitationAcceptedActivityLog.Id)); + await _db.SaveChangesAsync(cancellationToken); + + if (oldClanId == null) { - Logger.LogInformation("User '{0}' left clan '{1}' and accepted invitation '{2}' from user '{3}' to join clan '{4}'", - invitee.Id, oldClanId, invitation.Id, inviter.Id, invitation.ClanId); + Logger.LogInformation("User '{0}' accepted request '{1}' from user '{2}' to join clan '{3}'", inviter.Id, invitation.Id, invitee.Id, invitation.ClanId); } else { - Logger.LogInformation("User '{0}' accepted request '{1}' from user '{2}' to join left clan '{3}' for clan '{4}'", - inviter.Id, invitation.Id, invitee.Id, oldClanId, invitation.ClanId); + Logger.LogInformation("User '{0}' accepted request '{1}' from user '{2}' to join left clan '{3}' for clan '{4}'", inviter.Id, invitation.Id, invitee.Id, oldClanId, invitation.ClanId); } } diff --git a/src/Application/Clans/Commands/UpdateClanMemberCommand.cs b/src/Application/Clans/Commands/UpdateClanMemberCommand.cs index 5d8e2281b..b2b07cecc 100644 --- a/src/Application/Clans/Commands/UpdateClanMemberCommand.cs +++ b/src/Application/Clans/Commands/UpdateClanMemberCommand.cs @@ -34,12 +34,16 @@ internal class Handler : IMediatorRequestHandler> Handle(UpdateClanMemberCommand req, CancellationToken cancellationToken) @@ -70,12 +74,16 @@ public async Task> Handle(UpdateClanMemberCommand re user.ClanMembership.Role)); } + var oldRole = toUpdateUser.ClanMembership!.Role; toUpdateUser.ClanMembership!.Role = req.Role; if (req.Role == ClanMemberRole.Leader) // If user is giving their leader role. { user.ClanMembership.Role = ClanMemberRole.Officer; } + var activityLog = _activityLogService.CreateClanMemberRoleChangeLog(toUpdateUser.Id, req.ClanId, req.UserId, oldRole, req.Role); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateClanMemberRoleChangedToUserNotification(toUpdateUser.Id, activityLog.Id)); await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' updated member '{1}' from clan '{2}'", req.UserId, req.MemberId, req.ClanId); diff --git a/src/Application/Common/Interfaces/ICrpgDbContext.cs b/src/Application/Common/Interfaces/ICrpgDbContext.cs index 6c50bbeb3..0c1d722f0 100644 --- a/src/Application/Common/Interfaces/ICrpgDbContext.cs +++ b/src/Application/Common/Interfaces/ICrpgDbContext.cs @@ -5,6 +5,7 @@ using Crpg.Domain.Entities.GameServers; using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Limitations; +using Crpg.Domain.Entities.Notification; using Crpg.Domain.Entities.Parties; using Crpg.Domain.Entities.Restrictions; using Crpg.Domain.Entities.Settlements; @@ -20,6 +21,7 @@ public interface ICrpgDbContext DbSet Characters { get; } DbSet Items { get; } DbSet UserItems { get; } + DbSet UserNotifications { get; } DbSet PersonalItems { get; } DbSet EquippedItems { get; } DbSet CharacterLimitations { get; } diff --git a/src/Application/Common/Results/CommonErrors.cs b/src/Application/Common/Results/CommonErrors.cs index 6afcb7e6b..760e519c9 100644 --- a/src/Application/Common/Results/CommonErrors.cs +++ b/src/Application/Common/Results/CommonErrors.cs @@ -115,6 +115,12 @@ public static Error ClanMemberRoleNotMet(int userId, ClanMemberRole expectedRole Detail = $"Clan with id '{clanId}' was not found", }; + public static Error ClanLeaderNotFound(int clanId) => new(ErrorType.NotFound, ErrorCode.ClanLeaderFound) + { + Title = "Clan leader was not found", + Detail = $"Clan leader with clanId '{clanId}' was not found", + }; + public static Error ClanTagAlreadyUsed(string clanTag) => new(ErrorType.Validation, ErrorCode.ClanTagAlreadyUsed) { Title = "Clan tag is already used", @@ -363,4 +369,10 @@ public static Error UserItemMaxRankReached(int userItemId, int maxRank) => Title = "Personal item already exist", Detail = $"User with id '{userId}' is already the owner of a personal item '{itemId}'", }; + + public static Error UserNotificationNotFound(int userId, int userNotificationId) => new(ErrorType.NotFound, ErrorCode.UserNotificationNotFound) + { + Title = "User notification was not found", + Detail = $"User notification with id '{userNotificationId}' was not found", + }; } diff --git a/src/Application/Common/Results/ErrorCode.cs b/src/Application/Common/Results/ErrorCode.cs index 36ecb291d..518b94e6d 100644 --- a/src/Application/Common/Results/ErrorCode.cs +++ b/src/Application/Common/Results/ErrorCode.cs @@ -23,6 +23,7 @@ public enum ErrorCode ClanNameAlreadyUsed, ClanNeedLeader, ClanNotFound, + ClanLeaderFound, ClanTagAlreadyUsed, Conflict, FighterNotACommander, @@ -63,6 +64,7 @@ public enum ErrorCode UserItemNotFound, UserNotAClanMember, UserNotFound, + UserNotificationNotFound, UserNotInAClan, UserRoleNotMet, PersonalItemAlreadyExist, diff --git a/src/Application/Common/Services/IActivityLogService.cs b/src/Application/Common/Services/IActivityLogService.cs index 637f62567..0dd17b2f9 100644 --- a/src/Application/Common/Services/IActivityLogService.cs +++ b/src/Application/Common/Services/IActivityLogService.cs @@ -1,8 +1,18 @@ using Crpg.Domain.Entities.ActivityLogs; +using Crpg.Domain.Entities.Clans; +using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Servers; namespace Crpg.Application.Common.Services; +// TODO: +public record EntitiesFromMetadata +{ + public IList clansIds = new List(); + public IList usersIds = new List(); + public IList charactersIds = new List(); +} + internal interface IActivityLogService { ActivityLog CreateUserCreatedLog(int userId); @@ -14,6 +24,7 @@ internal interface IActivityLogService ActivityLog CreateItemBrokeLog(int userId, string itemId); ActivityLog CreateItemReforgedLog(int userId, string itemId, int heirloomPoints, int price); ActivityLog CreateItemRepairedLog(int userId, string itemId, int price); + ActivityLog CreateItemReturnedLog(int userId, string itemId, int refundedHeirloomPoints, int refundedGold); ActivityLog CreateItemUpgradedLog(int userId, string itemId, int heirloomPoints); ActivityLog CreateCharacterCreatedLog(int userId, int characterId); ActivityLog CreateCharacterDeletedLog(int userId, int characterId, int generation, int level); @@ -21,15 +32,61 @@ internal interface IActivityLogService ActivityLog CreateCharacterRespecializedLog(int userId, int characterId, int price); ActivityLog CreateCharacterRetiredLog(int userId, int characterId, int level); ActivityLog CreateCharacterRewardedLog(int userId, int actorUserId, int characterId, int experience); - ActivityLog CreateAddItemToClanArmory(int userId, int clanId, int userItemId); - ActivityLog CreateRemoveItemFromClanArmory(int userId, int clanId, int userItemId); - ActivityLog CreateBorrowItemFromClanArmory(int userId, int clanId, int userItemId); - ActivityLog CreateReturnItemToClanArmory(int userId, int clanId, int userItemId); + ActivityLog CreateClanCreatedLog(int userId, int clanId); + ActivityLog CreateClanDeletedLog(int userId, int clanId); + ActivityLog CreateClanApplicationCreatedLog(int userId, int clanId); + ActivityLog CreateClanApplicationDeclinedLog(int userId, int clanId); + ActivityLog CreateClanApplicationAcceptedLog(int userId, int clanId); + ActivityLog CreateClanMemberRoleChangeLog(int userId, int clanId, int actorUserId, ClanMemberRole oldClanMemberRole, ClanMemberRole newClanMemberRole); + ActivityLog CreateClanMemberLeavedLog(int userId, int clanId); + ActivityLog CreateClanMemberKickedLog(int userId, int clanId, int actorUserId); + ActivityLog CreateAddItemToClanArmoryLog(int userId, int clanId, UserItem userItem); + ActivityLog CreateRemoveItemFromClanArmoryLog(int userId, int clanId, UserItem userItem); + ActivityLog CreateBorrowItemFromClanArmoryLog(int userId, int clanId, UserItem userItem); + ActivityLog CreateReturnItemToClanArmoryLog(int userId, int clanId, UserItem userItem); ActivityLog CreateCharacterEarnedLog(int userId, int characterId, GameMode gameMode, int experience, int gold); + EntitiesFromMetadata ExtractEntitiesFromMetadata(List activityLogs); } internal class ActivityLogService : IActivityLogService { + public EntitiesFromMetadata ExtractEntitiesFromMetadata(List activityLogs) + { + EntitiesFromMetadata output = new() { usersIds = new List() }; + + foreach (var al in activityLogs) + { + foreach (var md in al.Metadata) + { + if (md.Key == "clanId") + { + if (!output.clansIds.Contains(Convert.ToInt32(md.Value))) + { + output.clansIds.Add(Convert.ToInt32(md.Value)); + } + } + + if (md.Key == "userId" || md.Key == "actorUserId") + { + if (!output.usersIds.Contains(Convert.ToInt32(md.Value))) + { + output.usersIds.Add(Convert.ToInt32(md.Value)); + } + } + + if (md.Key == "characterId") + { + if (!output.charactersIds.Contains(Convert.ToInt32(md.Value))) + { + output.charactersIds.Add(Convert.ToInt32(md.Value)); + } + } + } + } + + return output; + } + public ActivityLog CreateUserCreatedLog(int userId) { return CreateLog(ActivityLogType.UserCreated, userId); @@ -114,6 +171,16 @@ public ActivityLog CreateItemUpgradedLog(int userId, string itemId, int heirloom }); } + public ActivityLog CreateItemReturnedLog(int userId, string itemId, int refundedHeirloomPoints, int refundedGold) + { + return CreateLog(ActivityLogType.ItemReturned, userId, new ActivityLogMetadata[] + { + new("itemId", itemId), + new("refundedHeirloomPoints", refundedHeirloomPoints.ToString()), + new("refundedGold", refundedGold.ToString()), + }); + } + public ActivityLog CreateCharacterCreatedLog(int userId, int characterId) { return CreateLog(ActivityLogType.CharacterCreated, userId, new ActivityLogMetadata[] @@ -168,39 +235,111 @@ public ActivityLog CreateCharacterRewardedLog(int userId, int actorUserId, int c }); } - public ActivityLog CreateAddItemToClanArmory(int userId, int clanId, int userItemId) + public ActivityLog CreateClanCreatedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanCreated, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateClanDeletedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanDeleted, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateClanMemberRoleChangeLog(int userId, int clanId, int actorUserId, ClanMemberRole oldClanMemberRole, ClanMemberRole newClanMemberRole) + { + return CreateLog(ActivityLogType.ClanMemberRoleEdited, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + new("actorUserId", actorUserId.ToString()), + new("oldClanMemberRole", oldClanMemberRole.ToString()), + new("newClanMemberRole", newClanMemberRole.ToString()), + }); + } + + public ActivityLog CreateClanMemberKickedLog(int userId, int clanId, int actorUserId) + { + return CreateLog(ActivityLogType.ClanMemberKicked, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + new("actorUserId", actorUserId.ToString()), + }); + } + + public ActivityLog CreateClanMemberLeavedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanMemberLeaved, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateClanApplicationCreatedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanApplicationCreated, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateClanApplicationDeclinedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanApplicationDeclined, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateClanApplicationAcceptedLog(int userId, int clanId) + { + return CreateLog(ActivityLogType.ClanApplicationAccepted, userId, new ActivityLogMetadata[] + { + new("clanId", clanId.ToString()), + }); + } + + public ActivityLog CreateAddItemToClanArmoryLog(int userId, int clanId, UserItem userItem) { return CreateLog(ActivityLogType.ClanArmoryAddItem, userId, new ActivityLogMetadata[] { - new("userItemId", userId.ToString()), new("clanId", clanId.ToString()), + new("userItemId", userItem.Id.ToString()), + new("itemId", userItem.ItemId), }); } - public ActivityLog CreateRemoveItemFromClanArmory(int userId, int clanId, int userItemId) + public ActivityLog CreateRemoveItemFromClanArmoryLog(int userId, int clanId, UserItem userItem) { return CreateLog(ActivityLogType.ClanArmoryRemoveItem, userId, new ActivityLogMetadata[] { - new("userItemId", userId.ToString()), new("clanId", clanId.ToString()), + new("userItemId", userItem.Id.ToString()), + new("itemId", userItem.ItemId), }); } - public ActivityLog CreateBorrowItemFromClanArmory(int userId, int clanId, int userItemId) + public ActivityLog CreateBorrowItemFromClanArmoryLog(int userId, int clanId, UserItem userItem) { return CreateLog(ActivityLogType.ClanArmoryBorrowItem, userId, new ActivityLogMetadata[] { - new("userItemId", userId.ToString()), new("clanId", clanId.ToString()), + new("userItemId", userItem.Id.ToString()), + new("itemId", userItem.ItemId), }); } - public ActivityLog CreateReturnItemToClanArmory(int userId, int clanId, int userItemId) + public ActivityLog CreateReturnItemToClanArmoryLog(int userId, int clanId, UserItem userItem) { return CreateLog(ActivityLogType.ClanArmoryReturnItem, userId, new ActivityLogMetadata[] { - new("userItemId", userId.ToString()), new("clanId", clanId.ToString()), + new("userItemId", userItem.Id.ToString()), + new("itemId", userItem.ItemId), }); } diff --git a/src/Application/Common/Services/IClanService.cs b/src/Application/Common/Services/IClanService.cs index 5202b9741..752bb4ad6 100644 --- a/src/Application/Common/Services/IClanService.cs +++ b/src/Application/Common/Services/IClanService.cs @@ -10,10 +10,11 @@ namespace Crpg.Application.Common.Services; internal interface IClanService { Task> GetClanMember(ICrpgDbContext db, int userId, int clanId, CancellationToken cancellationToken); + Task> GetClanLeader(ICrpgDbContext db, int clanId, CancellationToken cancellationToken); + Task>> GetClanOfficers(ICrpgDbContext db, int clanId, CancellationToken cancellationToken); Error? CheckClanMembership(User user, int clanId); Task> JoinClan(ICrpgDbContext db, User user, int clanId, CancellationToken cancellationToken); Task LeaveClan(ICrpgDbContext db, ClanMember member, CancellationToken cancellationToken); - Task> AddArmoryItem(ICrpgDbContext db, Clan clan, User user, int userItemId, CancellationToken cancellationToken = default); Task RemoveArmoryItem(ICrpgDbContext db, Clan clan, User user, int userItemId, CancellationToken cancellationToken = default); Task> BorrowArmoryItem(ICrpgDbContext db, Clan clan, User user, int userItemId, CancellationToken cancellationToken = default); @@ -22,6 +23,15 @@ internal interface IClanService internal class ClanService : IClanService { + private readonly IActivityLogService _activityLogService; + private readonly IUserNotificationService _userNotificationService; + + public ClanService(IActivityLogService activityLogService, IUserNotificationService userNotificationService) + { + _activityLogService = activityLogService; + _userNotificationService = userNotificationService; + } + public async Task> GetClanMember(ICrpgDbContext db, int userId, int clanId, CancellationToken cancellationToken) { var user = await db.Users @@ -36,6 +46,30 @@ public async Task> GetClanMember(ICrpgDbContext db, int userId, int return error != null ? new(error) : new(user); } + public async Task> GetClanLeader(ICrpgDbContext db, int clanId, CancellationToken cancellationToken) + { + var clanMember = await db.ClanMembers + .Include(cm => cm.User) + .FirstOrDefaultAsync(cm => cm.ClanId == clanId && cm.Role == ClanMemberRole.Leader, cancellationToken); + if (clanMember == null) + { + return new(CommonErrors.ClanLeaderNotFound(clanId)); + } + + return new(clanMember); + } + + public async Task>> GetClanOfficers(ICrpgDbContext db, int clanId, CancellationToken cancellationToken) + { + ClanMemberRole[] officersRoles = { ClanMemberRole.Officer, ClanMemberRole.Leader }; + var clanOfficers = await db.ClanMembers + .Include(cm => cm.User) + .Where(cm => cm.ClanId == clanId && officersRoles.Contains(cm.Role)) + .ToArrayAsync(cancellationToken); + + return new(clanOfficers); + } + public Error? CheckClanMembership(User user, int clanId) { if (user.ClanMembership == null) @@ -96,16 +130,23 @@ await db.Entry(member) } db.Clans.Remove(member.Clan); + + db.ActivityLogs.Add(_activityLogService.CreateClanDeletedLog(member.UserId, member.ClanId)); } await db.Entry(member) .Collection(cm => cm.ArmoryItems) - .Query().Include(ci => ci.BorrowedItem!).ThenInclude(bi => bi.UserItem!).ThenInclude(ui => ui.EquippedItems) + .Query() + .Include(ci => ci.BorrowedItem!) + .ThenInclude(bi => bi.UserItem!) + .ThenInclude(ui => ui.EquippedItems) .LoadAsync(); await db.Entry(member) .Collection(cm => cm.ArmoryBorrowedItems) - .Query().Include(bi => bi.UserItem!).ThenInclude(ui => ui.EquippedItems) + .Query() + .Include(bi => bi.UserItem!) + .ThenInclude(ui => ui.EquippedItems) .LoadAsync(); db.EquippedItems.RemoveRange(member.ArmoryItems.SelectMany(ci => ci.BorrowedItem != null ? ci.BorrowedItem.UserItem!.EquippedItems : new())); @@ -115,6 +156,19 @@ await db.Entry(member) db.ClanArmoryItems.RemoveRange(member.ArmoryItems); db.ClanMembers.Remove(member); + + var clanMemberLeavedActivityLog = _activityLogService.CreateClanMemberLeavedLog(member.UserId, member.ClanId); + db.ActivityLogs.Add(clanMemberLeavedActivityLog); + + if (member.Role != ClanMemberRole.Leader) + { + var clanLeaderRes = await GetClanLeader(db, member.ClanId, cancellationToken); + if (clanLeaderRes.Errors == null) + { + db.UserNotifications.Add(_userNotificationService.CreateClanMemberLeavedToLeaderNotification(clanLeaderRes.Data!.UserId, clanMemberLeavedActivityLog.Id)); + } + } + return Result.NoErrors; } @@ -159,6 +213,7 @@ await db.Entry(user) var armoryItem = new ClanArmoryItem { LenderClanId = clan.Id, UserItemId = userItem.Id, LenderUserId = user.Id }; db.ClanArmoryItems.Add(armoryItem); + db.ActivityLogs.Add(_activityLogService.CreateAddItemToClanArmoryLog(user.Id, clan.Id, userItem)); return new(armoryItem); } @@ -194,6 +249,14 @@ await db.Entry(user) db.ClanArmoryItems.Remove(userItem.ClanArmoryItem); + var activityLog = _activityLogService.CreateRemoveItemFromClanArmoryLog(user.Id, clan.Id, userItem); + db.ActivityLogs.Add(activityLog); + + if (userItem.ClanArmoryBorrowedItem != null) + { + db.UserNotifications.Add(_userNotificationService.CreateClanArmoryRemoveItemToBorrowerNotification(userItem.UserId, activityLog.Id)); + } + return Result.NoErrors; } @@ -201,11 +264,11 @@ public async Task> BorrowArmoryItem(ICrpgDbContex { await db.Entry(user) .Reference(u => u.ClanMembership) - .LoadAsync(); + .LoadAsync(cancellationToken); await db.Entry(user) .Collection(u => u.Items) - .LoadAsync(); + .LoadAsync(cancellationToken); var errors = CheckClanMembership(user, clan.Id); if (errors != null) @@ -236,6 +299,10 @@ await db.Entry(user) var borrowedItem = new ClanArmoryBorrowedItem { BorrowerClanId = clan.Id, UserItemId = armoryItem.UserItemId, BorrowerUserId = user.Id }; db.ClanArmoryBorrowedItems.Add(borrowedItem); + var activityLog = _activityLogService.CreateBorrowItemFromClanArmoryLog(user.Id, clan.Id, armoryItem.UserItem!); + db.ActivityLogs.Add(activityLog); + db.UserNotifications.Add(_userNotificationService.CreateClanArmoryBorrowItemToLenderNotification(armoryItem.LenderUserId, activityLog.Id)); + return new(borrowedItem); } @@ -243,7 +310,7 @@ public async Task ReturnArmoryItem(ICrpgDbContext db, Clan clan, User us { await db.Entry(user) .Reference(u => u.ClanMembership) - .LoadAsync(); + .LoadAsync(cancellationToken); var errors = CheckClanMembership(user, clan.Id); if (errors != null) @@ -254,8 +321,8 @@ await db.Entry(user) var borrowedItem = await db.ClanArmoryBorrowedItems .Where(bi => bi.UserItemId == userItemId - && (bi.BorrowerUserId == user.Id || user.ClanMembership!.Role == ClanMemberRole.Leader) // force return by clan leader - && bi.BorrowerClanId == clan.Id) + && bi.BorrowerClanId == clan.Id + && (bi.BorrowerUserId == user.Id || user.ClanMembership!.Role == ClanMemberRole.Leader)) // force return by clan leader .Include(bi => bi.UserItem!).ThenInclude(ui => ui.EquippedItems) .FirstOrDefaultAsync(cancellationToken); if (borrowedItem == null) @@ -265,6 +332,9 @@ await db.Entry(user) db.EquippedItems.RemoveRange(borrowedItem.UserItem!.EquippedItems); db.ClanArmoryBorrowedItems.Remove(borrowedItem); + var activityLog = _activityLogService.CreateReturnItemToClanArmoryLog(user.Id, clan.Id, borrowedItem.UserItem); + db.ActivityLogs.Add(activityLog); + db.UserNotifications.Add(_userNotificationService.CreateClanArmoryRemoveItemToBorrowerNotification(borrowedItem.BorrowerUserId, activityLog.Id)); return Result.NoErrors; } diff --git a/src/Application/Common/Services/IUserNotificationService.cs b/src/Application/Common/Services/IUserNotificationService.cs new file mode 100644 index 000000000..0055b61ca --- /dev/null +++ b/src/Application/Common/Services/IUserNotificationService.cs @@ -0,0 +1,92 @@ +using Crpg.Domain.Entities.Notification; + +namespace Crpg.Application.Common.Services; + +internal interface IUserNotificationService +{ + UserNotification CreateItemReturnedToUserNotification(int userId, int activityLogId); + UserNotification CreateClanApplicationCreatedToUserNotification(int userId, int activityLogId); + UserNotification CreateClanApplicationCreatedToOfficersNotification(int userId, int activityLogId); + UserNotification CreateClanApplicationAcceptedToUserNotification(int userId, int activityLogId); + UserNotification CreateClanApplicationDeclinedToUserNotification(int userId, int activityLogId); + UserNotification CreateClanMemberRoleChangedToUserNotification(int userId, int activityLogId); + UserNotification CreateClanMemberLeavedToLeaderNotification(int userId, int activityLogId); + UserNotification CreateClanMemberKickedToExMemberNotification(int userId, int activityLogId); + UserNotification CreateClanArmoryBorrowItemToLenderNotification(int userId, int activityLogId); + UserNotification CreateClanArmoryRemoveItemToBorrowerNotification(int userId, int activityLogId); + UserNotification CreateUserRewardedToUserNotification(int userId, int activityLogId); + UserNotification CreateCharacterRewardedToUserNotification(int userId, int activityLogId); +} + +internal class UserNotificationService : IUserNotificationService +{ + public UserNotification CreateItemReturnedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ItemReturned, userId, activityLogId); + } + + public UserNotification CreateClanMemberRoleChangedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanMemberRoleChangedToUser, userId, activityLogId); + } + + public UserNotification CreateClanMemberLeavedToLeaderNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanMemberLeavedToLeader, userId, activityLogId); + } + + public UserNotification CreateClanMemberKickedToExMemberNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanMemberKickedToExMember, userId, activityLogId); + } + + public UserNotification CreateClanApplicationCreatedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanApplicationCreatedToUser, userId, activityLogId); + } + + public UserNotification CreateClanApplicationCreatedToOfficersNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanApplicationCreatedToOfficers, userId, activityLogId); + } + + public UserNotification CreateClanApplicationAcceptedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanApplicationAcceptedToUser, userId, activityLogId); + } + + public UserNotification CreateClanApplicationDeclinedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanApplicationDeclinedToUser, userId, activityLogId); + } + + public UserNotification CreateClanArmoryBorrowItemToLenderNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanArmoryBorrowItemToLender, userId, activityLogId); + } + + public UserNotification CreateClanArmoryRemoveItemToBorrowerNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.ClanArmoryRemoveItemToBorrower, userId, activityLogId); + } + + public UserNotification CreateUserRewardedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.UserRewardedToUser, userId, activityLogId); + } + + public UserNotification CreateCharacterRewardedToUserNotification(int userId, int activityLogId) + { + return CreateNotification(NotificationType.CharacterRewardedToUser, userId, activityLogId); + } + + private UserNotification CreateNotification(NotificationType type, int userId, int activityLogId) + { + return new UserNotification + { + Type = type, + UserId = userId, + ActivityLogId = activityLogId, + }; + } +} diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index db3b3bace..fafc36bf8 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -33,6 +33,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(CreateGeoIpService()) diff --git a/src/Application/Notifications/Commands/DeleteAllUserNotificationsCommand.cs b/src/Application/Notifications/Commands/DeleteAllUserNotificationsCommand.cs new file mode 100644 index 000000000..6fa84f5da --- /dev/null +++ b/src/Application/Notifications/Commands/DeleteAllUserNotificationsCommand.cs @@ -0,0 +1,38 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using LoggerFactory = Crpg.Logging.LoggerFactory; + +namespace Crpg.Application.Notifications.Commands; + +public record DeleteAllUserNotificationsCommand : IMediatorRequest +{ + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private static readonly ILogger Logger = LoggerFactory.CreateLogger(); + + private readonly ICrpgDbContext _db; + + public Handler(ICrpgDbContext db) + { + _db = db; + } + + public async Task Handle(DeleteAllUserNotificationsCommand req, CancellationToken cancellationToken) + { + var userNotifications = await _db.UserNotifications + .Where(un => un.UserId == req.UserId) + .ToArrayAsync(cancellationToken); + + _db.UserNotifications.RemoveRange(userNotifications); + + await _db.SaveChangesAsync(cancellationToken); + Logger.LogInformation("User '{0}' delete all notifications", req.UserId); + return new Result(); + } + } +} diff --git a/src/Application/Notifications/Commands/DeleteOldUserNotificationsCommand.cs b/src/Application/Notifications/Commands/DeleteOldUserNotificationsCommand.cs new file mode 100644 index 000000000..443b06c3a --- /dev/null +++ b/src/Application/Notifications/Commands/DeleteOldUserNotificationsCommand.cs @@ -0,0 +1,44 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Sdk.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using LoggerFactory = Crpg.Logging.LoggerFactory; + +namespace Crpg.Application.Notifications.Commands; + +public record DeleteOldUserNotificationsCommand : IMediatorRequest +{ + internal class Handler : IMediatorRequestHandler + { + private static readonly ILogger Logger = LoggerFactory.CreateLogger(); + private static readonly TimeSpan LogRetention = TimeSpan.FromDays(30); + + private readonly ICrpgDbContext _db; + private readonly IDateTime _dateTime; + + public Handler(ICrpgDbContext db, IDateTime dateTime) + { + _db = db; + _dateTime = dateTime; + } + + public async Task Handle(DeleteOldUserNotificationsCommand req, CancellationToken cancellationToken) + { + var limit = _dateTime.UtcNow - LogRetention; + var userNotifications = await _db.UserNotifications + .Where(l => l.CreatedAt < limit) + .ToArrayAsync(cancellationToken); + + // ExecuteDelete can't be used because it is not supported by the in-memory provider which is used in our + // tests (https://github.com/dotnet/efcore/issues/30185). + _db.UserNotifications.RemoveRange(userNotifications); + await _db.SaveChangesAsync(cancellationToken); + + Logger.LogInformation("{0} old user notifications were cleaned out", userNotifications.Length); + + return Result.NoErrors; + } + } +} diff --git a/src/Application/Notifications/Commands/DeleteUserNotificationCommand.cs b/src/Application/Notifications/Commands/DeleteUserNotificationCommand.cs new file mode 100644 index 000000000..a4778f1b4 --- /dev/null +++ b/src/Application/Notifications/Commands/DeleteUserNotificationCommand.cs @@ -0,0 +1,43 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using LoggerFactory = Crpg.Logging.LoggerFactory; + +namespace Crpg.Application.Notifications.Commands; + +public record DeleteUserNotificationCommand : IMediatorRequest +{ + public int UserNotificationId { get; init; } + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private static readonly ILogger Logger = LoggerFactory.CreateLogger(); + + private readonly ICrpgDbContext _db; + + public Handler(ICrpgDbContext db) + { + _db = db; + } + + public async Task Handle(DeleteUserNotificationCommand req, CancellationToken cancellationToken) + { + var userNotification = await _db.UserNotifications + .FirstOrDefaultAsync(un => un.Id == req.UserNotificationId && un.UserId == req.UserId, cancellationToken); + + if (userNotification == null) + { + return new(CommonErrors.UserNotificationNotFound(req.UserId, req.UserNotificationId)); + } + + _db.UserNotifications.Remove(userNotification); + + await _db.SaveChangesAsync(cancellationToken); + Logger.LogInformation("User '{0}' delete the notification '{1}'", req.UserId, req.UserNotificationId); + return new Result(); + } + } +} diff --git a/src/Application/Notifications/Commands/ReadAllUserNotificationCommand.cs b/src/Application/Notifications/Commands/ReadAllUserNotificationCommand.cs new file mode 100644 index 000000000..cff69b58c --- /dev/null +++ b/src/Application/Notifications/Commands/ReadAllUserNotificationCommand.cs @@ -0,0 +1,45 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using LoggerFactory = Crpg.Logging.LoggerFactory; + +namespace Crpg.Application.Notifications.Commands; + +public record ReadAllUserNotificationCommand : IMediatorRequest +{ + public int UserNotificationId { get; init; } + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private static readonly ILogger Logger = LoggerFactory.CreateLogger(); + + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task Handle(ReadAllUserNotificationCommand req, CancellationToken cancellationToken) + { + var userNotifications = await _db.UserNotifications + .Where(un => un.UserId == req.UserId) + .ToArrayAsync(cancellationToken); + + foreach (var userNotification in userNotifications) + { + userNotification.State = Domain.Entities.Notification.NotificationState.Read; + } + + await _db.SaveChangesAsync(cancellationToken); + Logger.LogInformation("User '{0}' updated the notification '{1}'", req.UserId, req.UserNotificationId); + return new Result(); + } + } +} diff --git a/src/Application/Notifications/Commands/ReadUserNotificationCommand.cs b/src/Application/Notifications/Commands/ReadUserNotificationCommand.cs new file mode 100644 index 000000000..5116553db --- /dev/null +++ b/src/Application/Notifications/Commands/ReadUserNotificationCommand.cs @@ -0,0 +1,47 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Notifications.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using LoggerFactory = Crpg.Logging.LoggerFactory; + +namespace Crpg.Application.Notifications.Commands; + +public record ReadUserNotificationCommand : IMediatorRequest +{ + public int UserNotificationId { get; init; } + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private static readonly ILogger Logger = LoggerFactory.CreateLogger(); + + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(ReadUserNotificationCommand req, CancellationToken cancellationToken) + { + var userNotification = await _db.UserNotifications + .FirstOrDefaultAsync(un => un.Id == req.UserNotificationId && un.UserId == req.UserId, cancellationToken); + + if (userNotification == null) + { + return new(CommonErrors.UserNotificationNotFound(req.UserId, req.UserNotificationId)); + } + + userNotification.State = Domain.Entities.Notification.NotificationState.Read; + + await _db.SaveChangesAsync(cancellationToken); + Logger.LogInformation("User '{0}' read the notification '{1}'", req.UserId, req.UserNotificationId); + return new(_mapper.Map(userNotification)); + } + } +} diff --git a/src/Application/Notifications/Models/UserNotificationViewModel.cs b/src/Application/Notifications/Models/UserNotificationViewModel.cs new file mode 100644 index 000000000..aab1a5095 --- /dev/null +++ b/src/Application/Notifications/Models/UserNotificationViewModel.cs @@ -0,0 +1,14 @@ +using Crpg.Application.ActivityLogs.Models; +using Crpg.Application.Common.Mappings; +using Crpg.Domain.Entities.Notification; + +namespace Crpg.Application.Notifications.Models; + +public record UserNotificationViewModel : IMapFrom +{ + public int Id { get; init; } + public NotificationState State { get; init; } + public NotificationType Type { get; init; } + public ActivityLogViewModel? ActivityLog { get; init; } + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/Notifications/Models/UserNotificationsWithDictViewModel.cs b/src/Application/Notifications/Models/UserNotificationsWithDictViewModel.cs new file mode 100644 index 000000000..e97b069cd --- /dev/null +++ b/src/Application/Notifications/Models/UserNotificationsWithDictViewModel.cs @@ -0,0 +1,9 @@ +using Crpg.Application.ActivityLogs.Models; + +namespace Crpg.Application.Notifications.Models; + +public record UserNotificationsWithDictViewModel +{ + public IList Notifications { get; init; } = Array.Empty(); + public ActivityLogMetadataEnrichedViewModel Dict { get; init; } = new(); +} diff --git a/src/Application/Notifications/Queries/GetUserNotificationsQuery.cs b/src/Application/Notifications/Queries/GetUserNotificationsQuery.cs new file mode 100644 index 000000000..b47dc99dc --- /dev/null +++ b/src/Application/Notifications/Queries/GetUserNotificationsQuery.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using Crpg.Application.Characters.Models; +using Crpg.Application.Clans.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Common.Services; +using Crpg.Application.Notifications.Models; +using Crpg.Application.Users.Models; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Notifications.Queries; + +public record GetUserNotificationsQuery : IMediatorRequest +{ + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + private readonly IActivityLogService _activityLogService; + + public Handler(ICrpgDbContext db, IMapper mapper, IActivityLogService activityLogService) + { + _db = db; + _mapper = mapper; + _activityLogService = activityLogService; + } + + // TODO: all + pagination/filters (by date? by type?) + public async Task> Handle(GetUserNotificationsQuery req, + CancellationToken cancellationToken) + { + var userNotifications = await _db.UserNotifications + .Include(un => un.ActivityLog) + .ThenInclude(al => al!.Metadata) + .Where(un => un.UserId == req.UserId) + .OrderByDescending(un => un.CreatedAt) + .Take(1000) // TODO: + .ToArrayAsync(cancellationToken); + + var entitiesFromMetadata = _activityLogService.ExtractEntitiesFromMetadata(userNotifications.Select(un => un.ActivityLog!).ToList()); + + var clans = await _db.Clans.Where(c => entitiesFromMetadata.clansIds.Contains(c.Id)).ToArrayAsync(); + var users = await _db.Users.Where(u => entitiesFromMetadata.usersIds.Contains(u.Id)).ToArrayAsync(); + var characters = await _db.Characters.Where(c => entitiesFromMetadata.charactersIds.Contains(c.Id)).ToArrayAsync(); + + return new(new UserNotificationsWithDictViewModel() + { + Notifications = _mapper.Map>(userNotifications), + Dict = new() + { + Clans = _mapper.Map>(clans), + Users = _mapper.Map>(users), + Characters = _mapper.Map>(characters), + }, + }); + } + } +} diff --git a/src/Application/System/Commands/SeedDataCommand.cs b/src/Application/System/Commands/SeedDataCommand.cs index fc3143e1c..43efbbff6 100644 --- a/src/Application/System/Commands/SeedDataCommand.cs +++ b/src/Application/System/Commands/SeedDataCommand.cs @@ -10,6 +10,7 @@ using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Limitations; +using Crpg.Domain.Entities.Notification; using Crpg.Domain.Entities.Parties; using Crpg.Domain.Entities.Restrictions; using Crpg.Domain.Entities.Servers; @@ -37,12 +38,14 @@ internal class Handler : IMediatorRequestHandler private readonly IApplicationEnvironment _appEnv; private readonly ICharacterService _characterService; private readonly IExperienceTable _experienceTable; + private readonly IActivityLogService _activityLogService; + private readonly IUserNotificationService _userNotificationService; private readonly IStrategusMap _strategusMap; private readonly ISettlementsSource _settlementsSource; public Handler(ICrpgDbContext db, IItemsSource itemsSource, IApplicationEnvironment appEnv, ICharacterService characterService, IExperienceTable experienceTable, IStrategusMap strategusMap, - ISettlementsSource settlementsSource) + ISettlementsSource settlementsSource, IActivityLogService activityLogService, IUserNotificationService userNotificationService) { _db = db; _itemsSource = itemsSource; @@ -51,6 +54,8 @@ public Handler(ICrpgDbContext db, IItemsSource itemsSource, IApplicationEnvironm _experienceTable = experienceTable; _strategusMap = strategusMap; _settlementsSource = settlementsSource; + _activityLogService = activityLogService; + _userNotificationService = userNotificationService; } public async Task Handle(SeedDataCommand request, CancellationToken cancellationToken) @@ -970,260 +975,6 @@ private async Task AddDevelopmentData() } } - ActivityLog activityLogUserCreated1 = new() - { - Type = ActivityLogType.UserCreated, - User = namidaka, - Metadata = { }, - }; - ActivityLog activityLogUserDeleted1 = new() - { - Type = ActivityLogType.UserDeleted, - User = namidaka, - Metadata = { }, - }; - ActivityLog activityLogUserRenamed1 = new() - { - Type = ActivityLogType.UserRenamed, - User = namidaka, - Metadata = - { - new("newName", "Salt"), - new("oldName", "Duke Salt of Savoy"), - }, - }; - ActivityLog activityLogUserReward1 = new() - { - Type = ActivityLogType.UserRewarded, - User = namidaka, - Metadata = - { - new("gold", "120000"), - new("heirloomPoints", "3"), - new("itemId", "crpg_ba_bolzanogreathelmet_h2"), - }, - }; - ActivityLog activityLogItemBought1 = new() - { - Type = ActivityLogType.ItemBought, - User = namidaka, - Metadata = - { - new("itemId", "crpg_northern_round_shield"), - new("price", "12000"), - }, - }; - ActivityLog activityLogItemSold1 = new() - { - Type = ActivityLogType.ItemSold, - User = namidaka, - Metadata = - { - new("itemId", "crpg_northern_round_shield"), - new("price", "12000"), - }, - }; - ActivityLog activityLogItemBroke1 = new() - { - Type = ActivityLogType.ItemBroke, - User = namidaka, - Metadata = - { - new("itemId", "crpg_northern_round_shield"), - }, - }; - ActivityLog activityLogItemUpgraded1 = new() - { - Type = ActivityLogType.ItemUpgraded, - User = namidaka, - Metadata = - { - new("itemId", "crpg_northern_round_shield"), - new("price", "1000"), - new("heirloomPoints", "1"), - }, - }; - ActivityLog activityLogCharacterCreated1 = new() - { - Type = ActivityLogType.CharacterCreated, - User = namidaka, - Metadata = - { - new("characterId", "123"), - }, - }; - ActivityLog activityLogCharacterDeleted1 = new() - { - Type = ActivityLogType.CharacterDeleted, - User = namidaka, - Metadata = - { - new("characterId", "123"), - new("generation", "13"), - new("level", "36"), - }, - }; - ActivityLog activityLogCharacterRespecialized1 = new() - { - Type = ActivityLogType.CharacterRespecialized, - User = namidaka, - Metadata = - { - new("characterId", "123"), - new("price", "120000"), - }, - }; - ActivityLog activityLogCharacterRetired1 = new() - { - Type = ActivityLogType.CharacterRetired, - User = namidaka, - Metadata = - { - new("characterId", "123"), - new("level", "34"), - }, - }; - ActivityLog activityLogCharacterRewarded1 = new() - { - Type = ActivityLogType.CharacterRewarded, - User = namidaka, - Metadata = - { - new("characterId", "123"), - new("experience", "1000000"), - }, - }; - ActivityLog activityLogServerJoined1 = new() - { - Type = ActivityLogType.ServerJoined, - User = namidaka, - Metadata = { }, - }; - ActivityLog activityLogChatMessageSent1 = new() - { - Type = ActivityLogType.ChatMessageSent, - User = namidaka, - Metadata = - { - new("message", "Fluttershy is best"), - new("instance", "crpg01a"), - }, - }; - ActivityLog activityLogChatMessageSent2 = new() - { - Type = ActivityLogType.ChatMessageSent, - User = takeo, - Metadata = - { - new("message", "No, Rarity the best"), - new("instance", "crpg01a"), - }, - }; - ActivityLog activityLogChatMessageSent3 = new() - { - Type = ActivityLogType.ChatMessageSent, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-3), - Metadata = - { - new("message", "Do you get it?"), - new("instance", "crpg01a"), - }, - }; - ActivityLog activityLogTeamHit1 = new() - { - Type = ActivityLogType.TeamHit, - User = namidaka, - CreatedAt = DateTime.UtcNow.AddMinutes(+3), - Metadata = - { - new("targetUserId", "1"), - new("damage", "123"), - new("instance", "crpg01a"), - }, - }; - ActivityLog activityLogTeamHit2 = new() - { - Type = ActivityLogType.TeamHit, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-1), - Metadata = - { - new("targetUserId", "2"), - new("damage", "18"), - new("instance", "crpg01a"), - }, - }; - ActivityLog activityLogClanArmoryAddItem = new() - { - Type = ActivityLogType.ClanArmoryAddItem, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-1), - Metadata = - { - new("clanId", "2"), - new("userItemId", "1"), - }, - }; - ActivityLog activityLogClanArmoryRemoveItem = new() - { - Type = ActivityLogType.ClanArmoryRemoveItem, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-1), - Metadata = - { - new("clanId", "2"), - new("userItemId", "1"), - }, - }; - ActivityLog activityLogClanArmoryReturnItem = new() - { - Type = ActivityLogType.ClanArmoryReturnItem, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-1), - Metadata = - { - new("clanId", "2"), - new("userItemId", "1"), - }, - }; - ActivityLog activityLogClanArmoryBorrowItem = new() - { - Type = ActivityLogType.ClanArmoryBorrowItem, - User = takeo, - CreatedAt = DateTime.UtcNow.AddMinutes(-1), - Metadata = - { - new("clanId", "2"), - new("userItemId", "1"), - }, - }; - - ActivityLog[] newActivityLogCharacterEarned = - { - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-1), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "122000"), new("gold", "1244") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-12), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "7000"), new("gold", "989") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-15), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "32000"), new("gold", "-900") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-25), Metadata = { new("characterId", orleCharacter1.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "32000"), new("gold", "1989") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-35), Metadata = { new("characterId", orleCharacter1.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "322000"), new("gold", "989") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-11), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "1400"), new("gold", "1244") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-23), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "200"), new("gold", "-12") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-17), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "993310"), new("gold", "133") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-111), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "122234"), new("gold", "-1222") } }, - new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-112), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "3111"), new("gold", "-122") } }, - }; - - ActivityLog[] newActivityLogs = - { - activityLogUserCreated1, activityLogUserDeleted1, activityLogUserRenamed1, activityLogUserReward1, activityLogItemBought1, - activityLogItemSold1, activityLogItemBroke1, activityLogItemUpgraded1, activityLogCharacterCreated1, activityLogCharacterDeleted1, - activityLogCharacterRespecialized1, activityLogCharacterRetired1, activityLogCharacterRewarded1, activityLogServerJoined1, - activityLogChatMessageSent1, activityLogChatMessageSent2, activityLogChatMessageSent3, activityLogTeamHit1, activityLogTeamHit2, activityLogClanArmoryAddItem, activityLogClanArmoryRemoveItem, activityLogClanArmoryReturnItem, activityLogClanArmoryBorrowItem, - }; - - _db.ActivityLogs.RemoveRange(await _db.ActivityLogs.ToArrayAsync()); - _db.ActivityLogs.AddRange(newActivityLogs.Concat(newActivityLogCharacterEarned)); - Clan pecores = new() { Tag = "PEC", @@ -1246,7 +997,6 @@ private async Task AddDevelopmentData() }; ClanMember droobMember = new() { User = droob, Clan = droobClan, Role = ClanMemberRole.Leader, }; - ClanMember takeoMember = new() { User = takeo, Clan = pecores, Role = ClanMemberRole.Officer, }; ClanMember orleMember = new() { User = orle, Clan = pecores, Role = ClanMemberRole.Leader, }; ClanMember elmarykMember = new() { User = elmaryk, Clan = pecores, Role = ClanMemberRole.Officer, }; @@ -1284,25 +1034,31 @@ private async Task AddDevelopmentData() foreach (var newClanArmoryItem in newClanArmoryItems) { - // TODO: check if exist - // pecores.ArmoryItems.Add(newClanArmoryItem); + if (!pecores.ArmoryItems.Contains(newClanArmoryItem)) + { + pecores.ArmoryItems.Add(newClanArmoryItem); + } } - ClanArmoryBorrowedItem orleBorrowedItem1 = new() { UserItem = laHirekItem2, Borrower = orleMember, }; - ClanArmoryBorrowedItem orleBorrowedItem2 = new() { UserItem = laHirekItem3, Borrower = orleMember, }; - ClanArmoryBorrowedItem elmarykBorrowedItem1 = new() { UserItem = orleItem1, Borrower = elmarykMember, }; - ClanArmoryBorrowedItem elmarykBorrowedItem2 = new() { UserItem = takeoItem1, Borrower = elmarykMember, }; - ClanArmoryBorrowedItem laHireBorrowedItem1 = new() { UserItem = takeoItem2, Borrower = laHireMember, }; + ClanArmoryBorrowedItem orleBorrowedItem1 = new() { UserItem = laHireClanArmoryItem2.UserItem, Borrower = orleMember, }; + ClanArmoryBorrowedItem orleBorrowedItem2 = new() { UserItem = laHireClanArmoryItem3.UserItem, Borrower = orleMember, }; + ClanArmoryBorrowedItem elmarykBorrowedItem1 = new() { UserItem = orleClanArmoryItem1.UserItem, Borrower = elmarykMember, }; + ClanArmoryBorrowedItem elmarykBorrowedItem2 = new() { UserItem = takeoClanArmoryItem1.UserItem, Borrower = elmarykMember, }; + ClanArmoryBorrowedItem laHireBorrowedItem1 = new() { UserItem = takeoClanArmoryItem2.UserItem, Borrower = laHireMember, }; + ClanArmoryBorrowedItem laHireBorrowedItem2 = new() { UserItem = orleClanArmoryItem15.UserItem, Borrower = laHireMember, }; ClanArmoryBorrowedItem[] newClanArmoryBorrowedItems = { orleBorrowedItem1, orleBorrowedItem2, elmarykBorrowedItem1, elmarykBorrowedItem2, laHireBorrowedItem1, + laHireBorrowedItem2, }; foreach (var newClanArmoryBorrowedItem in newClanArmoryBorrowedItems) { - // TODO: check if exist - // pecores.ArmoryBorrowedItems.Add(newClanArmoryBorrowedItem); + if (!pecores.ArmoryBorrowedItems.Contains(newClanArmoryBorrowedItem)) + { + pecores.ArmoryBorrowedItems.Add(newClanArmoryBorrowedItem); + } } Clan ats = new() @@ -1498,8 +1254,8 @@ private async Task AddDevelopmentData() Status = ClanInvitationStatus.Pending, }; ClanInvitation[] newClanInvitations = { schumetzqRequestForPecores, victorhh888MemberRequestForPecores, neostralieOfferToBrygganForPecores }; - var existingClanInvitations = - await _db.ClanInvitations.ToDictionaryAsync(i => (i.InviteeId, i.InviterId)); + + var existingClanInvitations = await _db.ClanInvitations.ToDictionaryAsync(i => (i.InviteeId, i.InviterId)); foreach (var newClanInvitation in newClanInvitations) { if (!existingClanInvitations.ContainsKey((newClanInvitation.Invitee!.Id, newClanInvitation.Inviter!.Id))) @@ -1508,6 +1264,101 @@ private async Task AddDevelopmentData() } } + var activityLogUserCreated1 = _activityLogService.CreateUserCreatedLog(orle.Id); + var activityLogUserDeleted1 = _activityLogService.CreateUserDeletedLog(orle.Id); + var activityLogUserRenamed1 = _activityLogService.CreateUserRenamedLog(orle.Id, "Salt", "Duke Salt of Savoy"); + var activityLogUserRewarded1 = _activityLogService.CreateUserRewardedLog(orle.Id, namidaka.Id, 120000, 3, orleItem1.ItemId); + activityLogUserRewarded1.CreatedAt = DateTime.UtcNow.AddDays(-1); + var activityLogUserRewarded2 = _activityLogService.CreateUserRewardedLog(orle.Id, namidaka.Id, 120000, 0, string.Empty); + + var activityLogItemBought1 = _activityLogService.CreateItemBoughtLog(orle.Id, orleItem1.ItemId, 12000); + var activityLogItemSold1 = _activityLogService.CreateItemSoldLog(orle.Id, orleItem1.ItemId, 12000); + var activityLogItemBroke1 = _activityLogService.CreateItemBrokeLog(orle.Id, orleItem1.ItemId); + var activityLogItemUpgraded1 = _activityLogService.CreateItemUpgradedLog(orle.Id, orleItem1.ItemId, 2); + var activityLogItemReturned1 = _activityLogService.CreateItemReturnedLog(orle.Id, "crpg_item_1", 1, 1900); + + var activityLogCharacterCreated1 = _activityLogService.CreateCharacterCreatedLog(orle.Id, orleCharacter0.Id); + var activityLogCharacterDeleted1 = _activityLogService.CreateCharacterDeletedLog(orle.Id, orleCharacter0.Id, 13, 36); + var activityLogCharacterRespecialized1 = _activityLogService.CreateCharacterRespecializedLog(orle.Id, orleCharacter0.Id, 120000); + var activityLogCharacterRetired1 = _activityLogService.CreateCharacterRetiredLog(orle.Id, orleCharacter0.Id, 34); + var activityLogCharacterRewarded1 = _activityLogService.CreateCharacterRewardedLog(orle.Id, takeo.Id, 5, 1000000); + + var activityLogServerJoined1 = new ActivityLog() { Type = ActivityLogType.ServerJoined, User = orle }; + var activityLogChatMessageSent1 = new ActivityLog() { Type = ActivityLogType.ChatMessageSent, User = orle, Metadata = { new("message", "Fluttershy is best"), new("instance", "crpg01a"), } }; + var activityLogChatMessageSent2 = new ActivityLog() { Type = ActivityLogType.ChatMessageSent, User = orle, Metadata = { new("message", "No, Rarity the best"), new("instance", "crpg01a"), }, }; + var activityLogChatMessageSent3 = new ActivityLog() { Type = ActivityLogType.ChatMessageSent, User = takeo, CreatedAt = DateTime.UtcNow.AddMinutes(-3), Metadata = { new("message", "Do you get it?"), new("instance", "crpg01a"), }, }; + var activityLogTeamHit1 = new ActivityLog() { Type = ActivityLogType.TeamHit, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(+3), Metadata = { new("targetUserId", "1"), new("damage", "123"), new("instance", "crpg01a"), }, }; + var activityLogTeamHit2 = new ActivityLog() { Type = ActivityLogType.TeamHit, User = takeo, CreatedAt = DateTime.UtcNow.AddMinutes(-1), }; + + ActivityLog[] newActivityLogCharacterEarned = + { + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-1), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "122000"), new("gold", "1244") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-12), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "7000"), new("gold", "989") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-15), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "32000"), new("gold", "-900") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-25), Metadata = { new("characterId", orleCharacter1.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "32000"), new("gold", "1989") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-35), Metadata = { new("characterId", orleCharacter1.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "322000"), new("gold", "989") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-11), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "1400"), new("gold", "1244") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-23), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "200"), new("gold", "-12") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-17), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGBattle"), new("experience", "993310"), new("gold", "133") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-111), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "122234"), new("gold", "-1222") } }, + new() { Type = ActivityLogType.CharacterEarned, User = orle, CreatedAt = DateTime.UtcNow.AddMinutes(-112), Metadata = { new("characterId", orleCharacter0.Id.ToString()), new("gameMode", "CRPGDTV"), new("experience", "3111"), new("gold", "-122") } }, + }; + + var activityLogClanApplicationCreated1 = _activityLogService.CreateClanApplicationCreatedLog(takeo.Id, 1); + var activityLogClanApplicationCreated2 = _activityLogService.CreateClanApplicationCreatedLog(namidaka.Id, 1); + var activityLogClanApplicationCreated3 = _activityLogService.CreateClanApplicationCreatedLog(orle.Id, 1); + var activityLogClanApplicationAccepted1 = _activityLogService.CreateClanApplicationAcceptedLog(orle.Id, 1); + var activityLogClanApplicationDeclined1 = _activityLogService.CreateClanApplicationDeclinedLog(orle.Id, 1); + var activityLogClanMemberRoleChange1 = _activityLogService.CreateClanMemberRoleChangeLog(orle.Id, 1, takeo.Id, ClanMemberRole.Officer, ClanMemberRole.Leader); + var activityLogClanMemberLeaved1 = _activityLogService.CreateClanMemberLeavedLog(orle.Id, 1); + var activityLogClanMemberKicked1 = _activityLogService.CreateClanMemberKickedLog(orle.Id, 1, takeo.Id); + var activityLogClanCreatedl = _activityLogService.CreateClanCreatedLog(orle.Id, 1); + var activityLogClanDeletedl = _activityLogService.CreateClanDeletedLog(orle.Id, 1); + var activityLogClanArmoryAddItem1 = _activityLogService.CreateAddItemToClanArmoryLog(takeo.Id, pecores.Id, takeoItem1); + var activityLogClanArmoryRemoveItem1 = _activityLogService.CreateRemoveItemFromClanArmoryLog(takeo.Id, pecores.Id, takeoItem1); + var activityLogClanArmoryReturnItem1 = _activityLogService.CreateReturnItemToClanArmoryLog(takeo.Id, pecores.Id, orleItem1); + var activityLogClanArmoryBorrowItem1 = _activityLogService.CreateBorrowItemFromClanArmoryLog(takeo.Id, pecores.Id, orleItem1); + + ActivityLog[] newActivityLogs = + { + activityLogUserCreated1, activityLogUserDeleted1, activityLogUserRenamed1, activityLogUserRewarded1, activityLogUserRewarded2, activityLogItemBought1, + activityLogItemSold1, activityLogItemBroke1, activityLogItemUpgraded1, activityLogCharacterCreated1, activityLogCharacterDeleted1, + activityLogCharacterRespecialized1, activityLogCharacterRetired1, activityLogCharacterRewarded1, activityLogServerJoined1, + activityLogChatMessageSent1, activityLogChatMessageSent2, activityLogChatMessageSent3, activityLogTeamHit1, activityLogTeamHit2, activityLogClanArmoryAddItem1, activityLogClanArmoryRemoveItem1, activityLogClanArmoryReturnItem1, activityLogClanArmoryBorrowItem1, activityLogClanArmoryBorrowItem1, activityLogClanApplicationCreated1, activityLogClanApplicationCreated2, activityLogClanApplicationCreated3, activityLogClanApplicationAccepted1, activityLogClanApplicationDeclined1, activityLogItemReturned1, activityLogClanMemberRoleChange1, activityLogClanMemberLeaved1, activityLogClanMemberKicked1, activityLogClanCreatedl, + activityLogClanDeletedl, + }; + + _db.ActivityLogs.RemoveRange(await _db.ActivityLogs.ToArrayAsync()); + _db.ActivityLogs.AddRange(newActivityLogs.Concat(newActivityLogCharacterEarned)); + + var orleNotificationClanApplicationCreatedToOfficers1 = _userNotificationService.CreateClanApplicationCreatedToOfficersNotification(orle.Id, activityLogClanApplicationCreated1.Id); + orleNotificationClanApplicationCreatedToOfficers1.CreatedAt = DateTime.UtcNow.AddMinutes(-112); + var orleNotificationClanApplicationCreatedToOfficers2 = _userNotificationService.CreateClanApplicationCreatedToOfficersNotification(orle.Id, activityLogClanApplicationCreated2.Id); + orleNotificationClanApplicationCreatedToOfficers2.State = NotificationState.Read; + var orleNotificationClanApplicationCreatedToOfficers3 = _userNotificationService.CreateClanApplicationCreatedToOfficersNotification(orle.Id, activityLogClanApplicationCreated3.Id); + var orleNotificatioUserRewardedToUser = _userNotificationService.CreateUserRewardedToUserNotification(orle.Id, activityLogUserRewarded1.Id); + var orleNotificatioUserRewardedToUser2 = _userNotificationService.CreateUserRewardedToUserNotification(orle.Id, activityLogUserRewarded2.Id); + + var orleNotificationClanApplicationAcceptedToUser = _userNotificationService.CreateClanApplicationAcceptedToUserNotification(orle.Id, activityLogClanApplicationAccepted1.Id); + var orleNotificationClanApplicationDeclinedToUser = _userNotificationService.CreateClanApplicationDeclinedToUserNotification(orle.Id, activityLogClanApplicationDeclined1.Id); + var orleNotificationClanApplicationCreatedToUser = _userNotificationService.CreateClanApplicationCreatedToUserNotification(orle.Id, activityLogClanApplicationCreated1.Id); + var orleNotificationItemReturned = _userNotificationService.CreateItemReturnedToUserNotification(orle.Id, activityLogItemReturned1.Id); + var orleNotificationClanMemberRoleChangedToUser = _userNotificationService.CreateClanMemberRoleChangedToUserNotification(orle.Id, activityLogClanMemberRoleChange1.Id); + var orleNotificationClanMemberLeavedToLeader = _userNotificationService.CreateClanMemberLeavedToLeaderNotification(orle.Id, activityLogClanMemberLeaved1.Id); + var orleNotificationClanMemberKickedToExMember = _userNotificationService.CreateClanMemberKickedToExMemberNotification(orle.Id, activityLogClanMemberKicked1.Id); + var orleNotificationCharacterRewardedToUser = _userNotificationService.CreateCharacterRewardedToUserNotification(orle.Id, activityLogCharacterRewarded1.Id); + var orleNotificationClanArmoryBorrowItemToLender = _userNotificationService.CreateClanArmoryBorrowItemToLenderNotification(orle.Id, activityLogClanArmoryBorrowItem1.Id); + var orleNotificationClanArmoryRemoveItemToBorrower = _userNotificationService.CreateClanArmoryRemoveItemToBorrowerNotification(orle.Id, activityLogClanArmoryRemoveItem1.Id); + + UserNotification[] userNotifications = + { + orleNotificationClanApplicationCreatedToOfficers1, orleNotificationClanApplicationCreatedToOfficers2, orleNotificationClanApplicationCreatedToOfficers3, orleNotificatioUserRewardedToUser, orleNotificatioUserRewardedToUser2, orleNotificationClanApplicationAcceptedToUser, + orleNotificationClanApplicationDeclinedToUser, orleNotificationClanApplicationCreatedToUser, orleNotificationItemReturned, orleNotificationClanMemberRoleChangedToUser, orleNotificationClanMemberLeavedToLeader, + orleNotificationClanMemberKickedToExMember, orleNotificationCharacterRewardedToUser, orleNotificationClanArmoryBorrowItemToLender, orleNotificationClanArmoryRemoveItemToBorrower, + }; + _db.UserNotifications.RemoveRange(await _db.UserNotifications.ToArrayAsync()); + _db.UserNotifications.AddRange(userNotifications); + Task GetSettlementByName(string name) => _db.Settlements.FirstAsync(s => s.Name == name && s.Region == Region.Eu); var epicrotea = await GetSettlementByName("Epicrotea"); @@ -2191,6 +2042,9 @@ private async Task CreateOrUpdateItems(CancellationToken cancellationToken) } _db.UserItems.Remove(userItem); + var activityLog = _activityLogService.CreateItemReturnedLog(userItem.User!.Id, userItem.Item!.Id, userItem.Item!.Rank, userItem.Item!.Price); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateItemReturnedToUserNotification(userItem.User!.Id, activityLog.Id)); } var itemsToDelete = dbItemsById.Values.Where(i => i.Id == dbItem.Id).ToArray(); diff --git a/src/Application/Users/Commands/RewardUserCommand.cs b/src/Application/Users/Commands/RewardUserCommand.cs index 9fd20dd41..91948e095 100644 --- a/src/Application/Users/Commands/RewardUserCommand.cs +++ b/src/Application/Users/Commands/RewardUserCommand.cs @@ -23,15 +23,17 @@ internal class Handler : IMediatorRequestHandler(); - private readonly IActivityLogService _activityLogService; private readonly ICrpgDbContext _db; private readonly IMapper _mapper; + private readonly IActivityLogService _activityLogService; + private readonly IUserNotificationService _userNotificationService; - public Handler(IActivityLogService activityLogService, ICrpgDbContext db, IMapper mapper) + public Handler(ICrpgDbContext db, IMapper mapper, IActivityLogService activityLogService, IUserNotificationService userNotificationService) { - _activityLogService = activityLogService; _db = db; _mapper = mapper; + _activityLogService = activityLogService; + _userNotificationService = userNotificationService; } public async Task> Handle(RewardUserCommand req, CancellationToken cancellationToken) @@ -72,7 +74,9 @@ public async Task> Handle(RewardUserCommand req, Cancellat }); } - _db.ActivityLogs.Add(_activityLogService.CreateUserRewardedLog(req.UserId, req.ActorUserId, req.Gold, req.HeirloomPoints, req.ItemId)); + var activityLog = _activityLogService.CreateUserRewardedLog(req.UserId, req.ActorUserId, req.Gold, req.HeirloomPoints, req.ItemId); + _db.ActivityLogs.Add(activityLog); + _db.UserNotifications.Add(_userNotificationService.CreateUserRewardedToUserNotification(req.UserId, activityLog.Id)); await _db.SaveChangesAsync(cancellationToken); Logger.LogInformation("User '{0}' rewarded", req.UserId); diff --git a/src/Application/Users/Models/UserViewModel.cs b/src/Application/Users/Models/UserViewModel.cs index 166673b22..85a03dc1d 100644 --- a/src/Application/Users/Models/UserViewModel.cs +++ b/src/Application/Users/Models/UserViewModel.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Crpg.Application.Common.Mappings; using Crpg.Domain.Entities; using Crpg.Domain.Entities.Users; @@ -18,4 +19,14 @@ public record UserViewModel : IMapFrom public bool IsDonor { get; init; } public Uri? Avatar { get; init; } public int? ActiveCharacterId { get; init; } + public int UnreadNotificationsCount { get; init; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(u => u.UnreadNotificationsCount, + opt => opt.MapFrom(u => u.Notifications + .Where(un => un.State == Domain.Entities.Notification.NotificationState.Unread) + .Count())); + } } diff --git a/src/Domain/Entities/ActivityLogs/ActivityLogType.cs b/src/Domain/Entities/ActivityLogs/ActivityLogType.cs index 840bb2318..8da8c1c66 100644 --- a/src/Domain/Entities/ActivityLogs/ActivityLogType.cs +++ b/src/Domain/Entities/ActivityLogs/ActivityLogType.cs @@ -12,6 +12,7 @@ public enum ActivityLogType ItemReforged, ItemRepaired, ItemUpgraded, + ItemReturned, CharacterCreated, CharacterDeleted, CharacterRatingReset, @@ -22,6 +23,14 @@ public enum ActivityLogType ServerJoined, ChatMessageSent, TeamHit, + ClanCreated, + ClanDeleted, + ClanApplicationCreated, + ClanApplicationDeclined, + ClanApplicationAccepted, + ClanMemberKicked, + ClanMemberLeaved, + ClanMemberRoleEdited, ClanArmoryAddItem, ClanArmoryRemoveItem, ClanArmoryReturnItem, diff --git a/src/Domain/Entities/Notifications/NotificationState.cs b/src/Domain/Entities/Notifications/NotificationState.cs new file mode 100644 index 000000000..490dc2c47 --- /dev/null +++ b/src/Domain/Entities/Notifications/NotificationState.cs @@ -0,0 +1,17 @@ +namespace Crpg.Domain.Entities.Notification; + +/// +/// Represents state of a . +/// +public enum NotificationState +{ + /// + /// Notification is not read by user yet. + /// + Unread = 0, + + /// + /// Notification is read by user. + /// + Read, +} diff --git a/src/Domain/Entities/Notifications/NotificationType.cs b/src/Domain/Entities/Notifications/NotificationType.cs new file mode 100644 index 000000000..52be19c76 --- /dev/null +++ b/src/Domain/Entities/Notifications/NotificationType.cs @@ -0,0 +1,17 @@ +namespace Crpg.Domain.Entities.Notification; + +public enum NotificationType +{ + UserRewardedToUser, + CharacterRewardedToUser, + ItemReturned, + ClanApplicationCreatedToUser, + ClanApplicationCreatedToOfficers, + ClanApplicationAcceptedToUser, + ClanApplicationDeclinedToUser, + ClanMemberRoleChangedToUser, + ClanMemberLeavedToLeader, + ClanMemberKickedToExMember, + ClanArmoryBorrowItemToLender, + ClanArmoryRemoveItemToBorrower, +} diff --git a/src/Domain/Entities/Notifications/UserNotification.cs b/src/Domain/Entities/Notifications/UserNotification.cs new file mode 100644 index 000000000..b7e57c393 --- /dev/null +++ b/src/Domain/Entities/Notifications/UserNotification.cs @@ -0,0 +1,18 @@ +using Crpg.Domain.Common; +using Crpg.Domain.Entities.ActivityLogs; +using Crpg.Domain.Entities.Users; + +namespace Crpg.Domain.Entities.Notification; + +public class UserNotification : AuditableEntity +{ + public int Id { get; set; } + public NotificationType Type { get; set; } + + public NotificationState State { get; set; } + public int UserId { get; set; } + public int ActivityLogId { get; set; } + + public User? User { get; set; } + public ActivityLog? ActivityLog { get; set; } +} diff --git a/src/Domain/Entities/Users/User.cs b/src/Domain/Entities/Users/User.cs index da2a4ed3f..060b0983a 100644 --- a/src/Domain/Entities/Users/User.cs +++ b/src/Domain/Entities/Users/User.cs @@ -2,6 +2,7 @@ using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Notification; using Crpg.Domain.Entities.Parties; using Crpg.Domain.Entities.Restrictions; @@ -52,4 +53,5 @@ public class User : AuditableEntity public IList Restrictions { get; set; } = new List(); public ClanMember? ClanMembership { get; set; } public Party? Party { get; set; } + public IList Notifications { get; set; } = new List(); } diff --git a/src/Persistence/CrpgDbContext.cs b/src/Persistence/CrpgDbContext.cs index d8b4fb242..b0a9e7e55 100644 --- a/src/Persistence/CrpgDbContext.cs +++ b/src/Persistence/CrpgDbContext.cs @@ -9,6 +9,7 @@ using Crpg.Domain.Entities.GameServers; using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Limitations; +using Crpg.Domain.Entities.Notification; using Crpg.Domain.Entities.Parties; using Crpg.Domain.Entities.Restrictions; using Crpg.Domain.Entities.Servers; @@ -49,6 +50,8 @@ static CrpgDbContext() NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); + NpgsqlConnection.GlobalTypeMapper.MapEnum(); NpgsqlConnection.GlobalTypeMapper.MapEnum(); #pragma warning restore CS0618 } @@ -70,6 +73,7 @@ public CrpgDbContext( public DbSet Characters { get; set; } = default!; public DbSet Items { get; set; } = default!; public DbSet UserItems { get; set; } = default!; + public DbSet UserNotifications { get; set; } = default!; public DbSet PersonalItems { get; set; } = default!; public DbSet EquippedItems { get; set; } = default!; public DbSet CharacterLimitations { get; set; } = default!; @@ -154,6 +158,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); // Ensure that the PostGIS extension is installed. diff --git a/src/WebApi/Controllers/ActivityLogsController.cs b/src/WebApi/Controllers/ActivityLogsController.cs index 4de3b2fe7..7ca5c18f2 100644 --- a/src/WebApi/Controllers/ActivityLogsController.cs +++ b/src/WebApi/Controllers/ActivityLogsController.cs @@ -22,7 +22,7 @@ public class ActivityLogsController : BaseController /// Bad Request. [Authorize(Policy = ModeratorPolicy)] [HttpGet] - public async Task>>> GetActivityLogs( + public async Task>> GetActivityLogs( [FromQuery] DateTime from, [FromQuery] DateTime to, [FromQuery(Name = "userId[]")] int[]? userIds, diff --git a/src/WebApi/Controllers/LeaderboardController.cs b/src/WebApi/Controllers/LeaderboardController.cs index eb174f7de..3dde0c239 100644 --- a/src/WebApi/Controllers/LeaderboardController.cs +++ b/src/WebApi/Controllers/LeaderboardController.cs @@ -18,7 +18,7 @@ public class LeaderboardController : BaseController /// Ok. [HttpGet("leaderboard")] [ResponseCache(Duration = 1 * 60 * 1)] // 1 minutes - public Task>>> GetLeaderboard( + public Task>>> GetLeaderboard( [FromQuery] Region? region, [FromQuery] CharacterClass? characterClass) { diff --git a/src/WebApi/Controllers/UsersController.cs b/src/WebApi/Controllers/UsersController.cs index c142aa0dc..33d8ac08f 100644 --- a/src/WebApi/Controllers/UsersController.cs +++ b/src/WebApi/Controllers/UsersController.cs @@ -11,7 +11,9 @@ using Crpg.Application.Items.Queries; using Crpg.Application.Limitations.Models; using Crpg.Application.Limitations.Queries; -using Crpg.Application.Parties.Commands; +using Crpg.Application.Notifications.Commands; +using Crpg.Application.Notifications.Models; +using Crpg.Application.Notifications.Queries; using Crpg.Application.Restrictions.Models; using Crpg.Application.Restrictions.Queries; using Crpg.Application.Users.Commands; @@ -585,4 +587,58 @@ public Task RewardRecently() { return ResultToActionAsync(Mediator.Send(new RewardRecentUserCommand { })); } + + /// + /// Gets user's notifications. + /// + /// The user's notifications. + /// Ok. + [HttpGet("self/notifications")] + public Task>> GetUserNotifications() + { + GetUserNotificationsQuery req = new() { UserId = CurrentUser.User!.Id }; + return ResultToActionAsync(Mediator.Send(req)); + } + + /// + /// Read user's notification. + /// + /// The updated notification. + /// Read. + /// Bad Request. + /// Notification was not found. + [HttpPut("self/notifications/{id}")] + public Task>> UpdateUserNotification([FromRoute] int id) => + ResultToActionAsync(Mediator.Send(new ReadUserNotificationCommand { UserNotificationId = id, UserId = CurrentUser.User!.Id })); + + /// + /// Read all user's notifications. + /// + /// Read. + /// Bad Request. + [HttpPut("self/notifications/readAll")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task ReadAllUserNotifications() => + ResultToActionAsync(Mediator.Send(new ReadAllUserNotificationCommand { UserId = CurrentUser.User!.Id })); + + /// + /// Delete user's notification. + /// + /// Deleted. + /// Bad Request. + /// Notification was not found. + [HttpDelete("self/notifications/{id}")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task DeleteUserNotification([FromRoute] int id) => + ResultToActionAsync(Mediator.Send(new DeleteUserNotificationCommand { UserNotificationId = id, UserId = CurrentUser.User!.Id })); + + /// + /// Delete all user's notifications. + /// + /// Deleted. + /// Bad Request. + [HttpDelete("self/notifications/deleteAll")] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task DeleteAllUserNotifications() => + ResultToActionAsync(Mediator.Send(new DeleteAllUserNotificationsCommand { UserId = CurrentUser.User!.Id })); } diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 6a7dc31bf..9f3b75430 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -51,6 +51,7 @@ // .AddHostedService() Disable strategus for now. .AddHostedService() .AddHostedService() + .AddHostedService() .AddHostedService() .AddHttpContextAccessor() // Injects IHttpContextAccessor .AddScoped() diff --git a/src/WebApi/Workers/UserNotificationsCleanerWorker.cs b/src/WebApi/Workers/UserNotificationsCleanerWorker.cs new file mode 100644 index 000000000..9a4dcffc6 --- /dev/null +++ b/src/WebApi/Workers/UserNotificationsCleanerWorker.cs @@ -0,0 +1,36 @@ +using Crpg.Application.Notifications.Commands; +using MediatR; + +namespace Crpg.WebApi.Workers; + +internal class UserNotificationsCleanerWorker : BackgroundService +{ + private static readonly ILogger Logger = Logging.LoggerFactory.CreateLogger(); + + private readonly IServiceScopeFactory _serviceScopeFactory; + + public UserNotificationsCleanerWorker(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (true) + { + try + { + using var scope = _serviceScopeFactory.CreateScope(); + + var mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new DeleteOldUserNotificationsCommand(), stoppingToken); + } + catch (Exception e) + { + Logger.LogError(e, "An error occured while cleaning user notifications"); + } + + await Task.Delay(TimeSpan.FromHours(12), stoppingToken); + } + } +} diff --git a/src/WebUI/locales/en.yml b/src/WebUI/locales/en.yml index f8cc43548..53f3cd45b 100644 --- a/src/WebUI/locales/en.yml +++ b/src/WebUI/locales/en.yml @@ -183,6 +183,7 @@ shortcuts: setting: language: Language + notifications: Notifications settings: Settings logout: Logout @@ -971,6 +972,10 @@ user: navigate: Navigate to our {modMailLink} channel follow: Follow the instructions in that channel to reach out to us outro: Please note that at this time, we do not accept appeals through any platform other than Discord. + notifications: + title: Notifications + empty: you have no notifications yet + toAll: All Notifications banned: title: You are banned @@ -1434,26 +1439,49 @@ activityLog: UserCreated: 'The user was created' UserDeleted: 'The user was deleted' UserRenamed: 'The user {oldName} was renamed to {newName}' - UserRewarded: 'The user was rewarded with {gold} {heirloomPoints} {itemId} by user {actorUserId}' - ItemBought: 'User bought {itemId} for {price}' - ItemSold: 'User sold item {itemId} for {price}' - ItemBroke: 'The item {itemId} was broken' - ItemRepaired: 'The item {itemId} was repaired for {price}' - ItemUpgraded: 'The item {itemId} was upgraded for {price} {heirloomPoints}' - ItemReforged: 'The item {itemId} was reforged for {price}. {heirloomPoints} were received' - CharacterCreated: 'The character {characterId} was created' - CharacterDeleted: 'The character {characterId}, level {level}, generation {generation}, was deleted' - CharacterRespecialized: 'The character {characterId} was respecialized for {price}' - CharacterRetired: 'The character {characterId} was retired at level {level}' - CharacterRewarded: 'The character {characterId} was rewarded {experience} experience by user {actorUserId}' + UserRewarded: 'The user was rewarded with {gold} {heirloomPoints} {item} by user {actorUser}' + ItemBought: 'The user bought {item} for {price}' + ItemSold: 'The user sold item {item} for {price}' + ItemBroke: 'The item {item} was broken' + ItemRepaired: 'The item {item} was repaired for {price}' + ItemUpgraded: 'The item {item} was upgraded for {price} {heirloomPoints}' + ItemReforged: 'The item {item} was reforged for {price}. {heirloomPoints} were received' + ItemReturned: 'The item {item} has been returned. {refundedGold} {refundedHeirloomPoints} has been refunded' + CharacterCreated: 'The character {character} was created' + CharacterDeleted: 'The character {character}, level {level}, generation {generation}, was deleted' + CharacterRespecialized: 'The character {character} was respecialized for {price}' + CharacterRetired: 'The character {character} was retired at level {level}' + CharacterRewarded: 'The character {character} was rewarded {experience} experience by user {actorUser}' + CharacterEarned: 'The character {character} earned {experience} experience and {gold} gold in the {gameMode} game mode' ServerJoined: 'The user has joined the server' ChatMessageSent: 'Wrote a message: {message} {instance}' - TeamHit: 'Dealt {damage} damage to {targetUserId} {instance}' - ClanArmoryAddItem: 'Item {userItemId} added to clan {clanId} armory' - ClanArmoryRemoveItem: 'Item {userItemId} removed from clan {clanId} armory' - ClanArmoryReturnItem: 'Item {userItemId} was returned from clan {clanId} armory' - ClanArmoryBorrowItem: 'Item {userItemId} was borrowed from clan {clanId} armory' - CharacterEarned: 'The character {characterId} earned {experience} experience and {gold} gold in the {gameMode} game mode' + TeamHit: 'Dealt {damage} damage to {targetUser} {instance}' + ClanCreated: 'The Сlan {clan} was created' + ClanDeleted: 'The Сlan {clan} was deleted' + ClanApplicationCreated: 'The application to join {clan} has been created' + ClanApplicationDeclined: 'The application to join {clan} has been declined' + ClanApplicationAccepted: 'The application to join {clan} has been approved' + ClanMemberKicked: 'The user was kicked from the clan {clan}' + ClanMemberLeaved: 'The user has left the clan {clan}' + ClanMemberRoleEdited: 'The Role {oldClanMemberRole} in the clan {clan} has been changed to {newClanMemberRole}' + ClanArmoryAddItem: 'The Item {userItem} added to clan {clan} armory' + ClanArmoryRemoveItem: 'Item {userItem} removed from clan {clan} armory' + ClanArmoryReturnItem: 'Item {userItem} was returned from clan {clan} armory' + ClanArmoryBorrowItem: 'Item {userItem} was borrowed from clan {clan} armory' +notification: + tpl: + ItemReturned: 'The item {item} has been removed from your inventory. {refundedGold} {refundedHeirloomPoints} has been refunded' + ClanMemberRoleChangedToUser: 'Your role {oldClanMemberRole} in the clan {clan} has been changed to {newClanMemberRole}' + ClanMemberLeavedToLeader: '{user} has left your clan {clan}' + ClanMemberKickedToExMember: 'You were kicked out of clan {clan}' + ClanApplicationCreatedToUser: 'Your application for {clan} has been successfully created' + ClanApplicationCreatedToOfficers: '{user} has applied to join your clan {clan}' + ClanApplicationAcceptedToUser: 'Your application to join the {clan} has been approved' + ClanApplicationDeclinedToUser: 'Your application to join the {clan} has been declined' + ClanArmoryBorrowItemToLender: 'User {user} has borrowed your item {item} from the {clan} clan armory' + ClanArmoryRemoveItemToBorrower: 'User {user} has removed his item {item} from the {clan} clan armory' + UserRewardedToUser: 'You have been rewarded with {gold} {heirloomPoints} {item} by user {actorUser}' + CharacterRewardedToUser: 'Your character {character} has been rewarded {experience} experience by user {actorUser}' leaderboard: title: Leaderboard diff --git a/src/WebUI/src/assets/themes/oruga-tailwind/icons/carillon.ts b/src/WebUI/src/assets/themes/oruga-tailwind/icons/carillon.ts new file mode 100644 index 000000000..718358dfc --- /dev/null +++ b/src/WebUI/src/assets/themes/oruga-tailwind/icons/carillon.ts @@ -0,0 +1,11 @@ +export default { + prefix: 'crpg', + iconName: 'carillon', + icon: [ + 32, + 32, + [], + 'e001', + 'M14.503 1.773V1h1.647v.734a2.49 2.49 0 0 1 1.68 2.347c0 .41-.102.798-.281 1.14.912.318 1.604.91 2.266 1.85 1.082 1.538 1.967 4.103 2.944 7.774.857 3.22 2.035 4.802 2.894 5.94l.12.157C26.558 21.977 27 22.56 27 24.338c0 .276-.151.583-.683.974-.532.39-1.396.783-2.478 1.101-2.165.637-5.193 1-8.339 1-3.146-.002-6.172-.368-8.336-1.005-1.082-.319-1.946-.71-2.478-1.102-.532-.39-.686-.695-.686-.97 0-1.779.442-2.362 1.226-3.396l.12-.158c.86-1.138 2.036-2.72 2.892-5.94.976-3.67 1.863-6.236 2.944-7.773.61-.869 1.248-1.44 2.06-1.773a2.453 2.453 0 0 1-.323-1.215c0-1.047.663-1.95 1.584-2.308Zm.873 1.492a.8.8 0 0 1 .807.816.797.797 0 0 1-.807.813.797.797 0 0 1-.807-.813.8.8 0 0 1 .807-.816Zm.86 3.13c2.562.954 3.69 5.105 4.342 7.937.715 3.103 1.887 4.854 2.823 6.252.818 1.223 1.456 2.176 1.454 3.53-.251.21-.679.446-1.212.635-.673.238-1.488.414-2.275.54.164-1.766-.456-2.92-1.13-4.173-.708-1.318-1.475-2.746-1.453-5.111.046-4.85-1.279-7.995-2.966-9.494.144-.027.282-.065.416-.116Z', + ], +}; diff --git a/src/WebUI/src/components/app/ActivityLogMetadata.vue b/src/WebUI/src/components/app/ActivityLogMetadata.vue new file mode 100644 index 000000000..304c82ba2 --- /dev/null +++ b/src/WebUI/src/components/app/ActivityLogMetadata.vue @@ -0,0 +1,176 @@ + + + diff --git a/src/WebUI/src/components/app/Coin.vue b/src/WebUI/src/components/app/Coin.vue index 219782423..7dbd490e6 100644 --- a/src/WebUI/src/components/app/Coin.vue +++ b/src/WebUI/src/components/app/Coin.vue @@ -3,7 +3,7 @@ const { value = 0 } = defineProps<{ value?: number }>();