From aea66b4a74e4abce754747f32c6d03fd68ec8fe5 Mon Sep 17 00:00:00 2001 From: sudokoko Date: Sun, 29 Oct 2023 16:27:41 -0400 Subject: [PATCH] Implement in-game and website notifications (#932) * Implement notifications logic, basic calls, and admin command * Remove unnecessary code * Add ability to stack notifications and return manually created XML * Remove test that is no longer needed and is causing failures * Apply suggestions from code review * Merge notifications with existing announcements page * Order notifications by descending ID instead of ascending ID * Move notification send task to moderation options under user Also restyles the buttons to line up next to each other like in the slot pages. * Style/position fixes with granted slots/notification partials * Fix incorrect form POST route * Prevent notification text area from breaking out of container * Actually use builder result for notification text * Minor restructuring of the notifications page * Add notifications for team picks, publish issues, and moderation * Mark notifications as dismissed instead of deleting them * Add XMLdoc to SendNotification method * Fix incorrect URL in announcements webhook * Remove unnecessary inline style from granted slots partial * Apply suggestions from code review * Apply first batch of suggestions from code review * Apply second batch of suggestions from code review * Change notification icon depending on if user has unread notifications * Show unread notification icon if there is an announcement posted * Remove "potential" wording from definitive fixes in error docs * Remove "Error code:" from publish notifications * Send notification if user tries to unlock a mod-locked level * Change notification timestamp format to include date * Add clarification to level mod-lock notification message * Change team pick notifications to moderation notifications Apparently the MMPick type doesn't show a visual notification. * Apply suggestions from code review * Add obsolete to notification types that display nothing in-game * Remove unused imports and remove icon switch case in favor of bell icon * Last minute fixes * Send notification upon earth wipe and clarify moderation case notifications * Add check for empty/too long notification text --- Documentation/Errors.md | 30 +++++ ProjectLighthouse.Localization/General.resx | 5 +- .../StringLists/GeneralStrings.cs | 3 +- .../Controllers/MessageController.cs | 44 ++++++- .../Controllers/Slots/PublishController.cs | 59 +++++++-- .../Controllers/Admin/AdminUserController.cs | 13 +- .../Moderator/ModerationSlotController.cs | 13 +- .../Admin/AdminSendNotificationPage.cshtml | 9 ++ .../Admin/AdminSendNotificationPage.cshtml.cs | 39 ++++++ .../Admin/AdminSetGrantedSlotsPage.cshtml | 2 +- .../Pages/AnnouncePage.cshtml.cs | 83 ------------- .../Pages/Layouts/BaseLayout.cshtml | 52 ++++---- .../Pages/Layouts/BaseLayout.cshtml.cs | 8 +- ...cePage.cshtml => NotificationsPage.cshtml} | 57 +++++++-- .../Pages/NotificationsPage.cshtml.cs | 112 ++++++++++++++++++ .../AdminSendNotificationPartial.cshtml | 12 ++ .../AdminSetGrantedSlotsFormPartial.cshtml | 4 +- .../Pages/UserPage.cshtml | 48 ++++---- .../Controllers/MessageControllerTests.cs | 15 +-- .../RepeatingTasks/PerformCaseActionsTask.cs | 39 +++++- .../NotificationConfiguration.cs | 7 ++ .../Configuration/ServerConfiguration.cs | 3 +- .../Database/DatabaseContext.GameTokens.cs | 1 - .../Database/DatabaseContext.Notifications.cs | 55 +++++++++ ProjectLighthouse/Database/DatabaseContext.cs | 7 +- .../20231027215257_AddNotificationEntity.cs | 52 ++++++++ .../Migrations/DatabaseModelSnapshot.cs | 38 +++++- .../Serialization/LbpOutputFormatter.cs | 8 +- .../Notifications/NotificationEntity.cs | 27 +++++ .../Website/WebsiteAnnouncementEntity.cs | 2 - .../Types/Notifications/NotificationType.cs | 37 ++++++ .../Types/Serialization/GameNotification.cs | 21 ++++ .../Types/Serialization/LbpCustomXml.cs | 6 + 33 files changed, 712 insertions(+), 199 deletions(-) create mode 100644 Documentation/Errors.md create mode 100644 ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml create mode 100644 ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml.cs delete mode 100644 ProjectLighthouse.Servers.Website/Pages/AnnouncePage.cshtml.cs rename ProjectLighthouse.Servers.Website/Pages/{AnnouncePage.cshtml => NotificationsPage.cshtml} (58%) create mode 100644 ProjectLighthouse.Servers.Website/Pages/NotificationsPage.cshtml.cs create mode 100644 ProjectLighthouse.Servers.Website/Pages/Partials/AdminSendNotificationPartial.cshtml create mode 100644 ProjectLighthouse/Configuration/ConfigurationCategories/NotificationConfiguration.cs create mode 100644 ProjectLighthouse/Database/DatabaseContext.Notifications.cs create mode 100644 ProjectLighthouse/Migrations/20231027215257_AddNotificationEntity.cs create mode 100644 ProjectLighthouse/Types/Entities/Notifications/NotificationEntity.cs create mode 100644 ProjectLighthouse/Types/Notifications/NotificationType.cs create mode 100644 ProjectLighthouse/Types/Serialization/GameNotification.cs create mode 100644 ProjectLighthouse/Types/Serialization/LbpCustomXml.cs diff --git a/Documentation/Errors.md b/Documentation/Errors.md new file mode 100644 index 00000000..316b8a1d --- /dev/null +++ b/Documentation/Errors.md @@ -0,0 +1,30 @@ + +# Errors + +Here's a list of error codes, as well as their explanations and potential fixes, that are displayed within in-game and +website notifications to indicate what went wrong. + +## Level Publishing + +- `LH-PUB-0001`: The level failed to publish because the slot is null. + - **Note:** The slot name will not be displayed in the notification if this error occurs. +- `LH-PUB-0002`: The level failed to publish because the slot does not include a `rootLevel`. +- `LH-PUB-0003`: The level failed to publish because the resource list is null. +- `LH-PUB-0004`: The level failed to publish because the level name is too long. + - **Fix:** Shorten the level name to something below 64 characters. +- `LH-PUB-0005`: The level failed to publish because the level description is too long. + - **Fix:** Shorten the level description to something below 512 characters. +- `LH-PUB-0006`: The level failed to publish because the server is missing resources required by the level. + - **Potential Fix:** Remove any resources that are not available on the server from the level. +- `LH-PUB-0007`: The level failed to publish because the root level is not a valid level. +- `LH-PUB-0008`: The level failed to publish because the root level is not an LBP3 Adventure level. +- `LH-PUB-0009`: The level failed to publish because the the user has reached their level publishing limit. + - **Fix:** Delete some of your previously published levels to make room for new ones. + +## Level Republishing + +- `LH-REP-0001`: The level failed to republish because the old slot does not exist. +- `LH-REP-0002`: The level failed to republish because the original publisher is not the current publisher. + - **Potential Fix:** Copying the level to another slot on your moon typically fixes this issue. +- `LH-REP-0003`: The level could not be unlocked because it was locked by a moderator. + - **Potential Fix:** Ask a server administrator/moderator to unlock the level for you. \ No newline at end of file diff --git a/ProjectLighthouse.Localization/General.resx b/ProjectLighthouse.Localization/General.resx index c92e5f04..41f4ff69 100644 --- a/ProjectLighthouse.Localization/General.resx +++ b/ProjectLighthouse.Localization/General.resx @@ -3,7 +3,7 @@ - + @@ -60,4 +60,7 @@ Announcements + + Notifications + \ No newline at end of file diff --git a/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs b/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs index 9ba858d6..de05e8f8 100644 --- a/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs +++ b/ProjectLighthouse.Localization/StringLists/GeneralStrings.cs @@ -16,6 +16,7 @@ public static class GeneralStrings public static readonly TranslatableString RecentActivity = create("recent_activity"); public static readonly TranslatableString Soon = create("soon"); public static readonly TranslatableString Announcements = create("announcements"); - + public static readonly TranslatableString Notifications = create("notifications"); + private static TranslatableString create(string key) => new(TranslationAreas.General, key); } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs index 544eb7ad..22abd9de 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs @@ -1,14 +1,16 @@ -#nullable enable using System.Text; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Serialization; +using LBPUnion.ProjectLighthouse.Types.Entities.Notifications; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Mail; +using LBPUnion.ProjectLighthouse.Types.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -66,12 +68,46 @@ along with this program. If not, see ."; $"token.ExpiresAt: {token.ExpiresAt.ToString()}\n" + "---DEBUG INFO---"); #endif - + return this.Ok(announceText.ToString()); } [HttpGet("notification")] - public IActionResult Notification() => this.Ok(); + [Produces("text/xml")] + public async Task Notification() + { + GameTokenEntity token = this.GetToken(); + + List notifications = await this.database.Notifications + .Where(n => n.UserId == token.UserId) + .Where(n => !n.IsDismissed) + .OrderByDescending(n => n.Id) + .ToListAsync(); + + // We don't need to do any more work if there are no unconverted notifications to begin with. + if (notifications.Count == 0) return this.Ok(); + + StringBuilder builder = new(); + + // ReSharper disable once ForCanBeConvertedToForeach + // Suppressing this because we need to modify the list while iterating over it. + for (int i = 0; i < notifications.Count; i++) + { + NotificationEntity n = notifications[i]; + + builder.AppendLine(LighthouseSerializer.Serialize(this.HttpContext.RequestServices, + GameNotification.CreateFromEntity(n))); + + n.IsDismissed = true; + } + + await this.database.SaveChangesAsync(); + + return this.Ok(new LbpCustomXml + { + Content = builder.ToString(), + }); + } /// /// Filters chat messages sent by a user. @@ -104,7 +140,7 @@ along with this program. If not, see ."; } string username = await this.database.UsernameFromGameToken(token); - + string filteredText = CensorHelper.FilterMessage(message); if (ServerConfiguration.Instance.LogChatMessages) Logger.Info($"{username}: \"{message}\"", LogArea.Filter); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index e29f3fe0..5bdb53a6 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; @@ -47,12 +46,16 @@ public class PublishController : ControllerBase if (slot == null) { Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish); + await this.database.SendNotification(user.UserId, + "Your level failed to publish. (LH-PUB-0001)"); return this.BadRequest(); // if the level cant be parsed then it obviously cant be uploaded } if (string.IsNullOrEmpty(slot.RootLevel)) { Logger.Warn("Rejecting level upload, slot does not include rootLevel", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0002)"); return this.BadRequest(); } @@ -61,6 +64,8 @@ public class PublishController : ControllerBase if (slot.Resources == null) { Logger.Warn("Rejecting level upload, resource list is null", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0003)"); return this.BadRequest(); } @@ -73,11 +78,15 @@ public class PublishController : ControllerBase if (oldSlot == null) { Logger.Warn("Rejecting level republish, could not find old slot", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to republish. (LH-REP-0001)"); return this.NotFound(); } if (oldSlot.CreatorId != user.UserId) { Logger.Warn("Rejecting level republish, old slot's creator is not publishing user", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to republish because you are not the original publisher. (LH-REP-0002)"); return this.BadRequest(); } } @@ -111,36 +120,48 @@ public class PublishController : ControllerBase if (slot == null) { Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish); + await this.database.SendNotification(user.UserId, + "Your level failed to publish. (LH-PUB-0001)"); return this.BadRequest(); } if (slot.Resources?.Length == 0) { Logger.Warn("Rejecting level upload, resource list is null", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0003)"); return this.BadRequest(); } // Yes Rider, this isn't null Debug.Assert(slot.Resources != null, "slot.ResourceList != null"); - slot.Description = CensorHelper.FilterMessage(slot.Description); - - if (slot.Description.Length > 512) - { - Logger.Warn($"Rejecting level upload, description too long ({slot.Description.Length} characters)", LogArea.Publish); - return this.BadRequest(); - } - slot.Name = CensorHelper.FilterMessage(slot.Name); if (slot.Name.Length > 64) { - Logger.Warn($"Rejecting level upload, title too long ({slot.Name.Length} characters)", LogArea.Publish); + Logger.Warn($"Rejecting level upload, title too long ({slot.Name.Length} characters)", + LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish because the name is too long, {slot.Name.Length} characters. (LH-PUB-0004)"); + return this.BadRequest(); + } + + slot.Description = CensorHelper.FilterMessage(slot.Description); + + if (slot.Description.Length > 512) + { + Logger.Warn($"Rejecting level upload, description too long ({slot.Description.Length} characters)", + LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish because the description is too long, {slot.Description.Length} characters. (LH-PUB-0005)"); return this.BadRequest(); } if (slot.Resources.Any(resource => !FileHelper.ResourceExists(resource))) { Logger.Warn("Rejecting level upload, missing resource(s)", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish because the server is missing resources. (LH-PUB-0006)"); return this.BadRequest(); } @@ -149,6 +170,8 @@ public class PublishController : ControllerBase if (rootLevel == null) { Logger.Warn("Rejecting level upload, unable to find rootLevel", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0002)"); return this.BadRequest(); } @@ -157,6 +180,8 @@ public class PublishController : ControllerBase if (rootLevel.FileType != LbpFileType.Level) { Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0007)"); return this.BadRequest(); } } @@ -165,8 +190,10 @@ public class PublishController : ControllerBase if (rootLevel.FileType != LbpFileType.Adventure) { Logger.Warn("Rejecting level upload, rootLevel is not a LBP 3 Adventure", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish. (LH-PUB-0008)"); return this.BadRequest(); - } + } } GameVersion slotVersion = FileHelper.ParseLevelVersion(rootLevel); @@ -193,6 +220,8 @@ public class PublishController : ControllerBase if (oldSlot.CreatorId != user.UserId) { Logger.Warn("Rejecting level republish, old level not owned by current user", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to republish because you are not the original publisher. (LH-REP-0002)"); return this.BadRequest(); } @@ -240,10 +269,12 @@ public class PublishController : ControllerBase oldSlot.MinimumPlayers = Math.Clamp(slot.MinimumPlayers, 1, 4); oldSlot.MaximumPlayers = Math.Clamp(slot.MaximumPlayers, 1, 4); - + // Check if the level has been locked by a moderator to avoid unlocking it - if (oldSlot.LockedByModerator) + if (oldSlot.LockedByModerator && !slot.InitiallyLocked) { + await this.database.SendNotification(user.UserId, + $"{slot.Name} will not be unlocked because it has been locked by a moderator. (LH-REP-0003)"); oldSlot.InitiallyLocked = true; } @@ -256,6 +287,8 @@ public class PublishController : ControllerBase if (usedSlots > user.EntitledSlots) { Logger.Warn("Rejecting level upload, too many published slots", LogArea.Publish); + await this.database.SendNotification(user.UserId, + $"{slot.Name} failed to publish because you have reached the maximum number of levels on your earth. (LH-PUB-0009)"); return this.BadRequest(); } diff --git a/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs b/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs index 0fa327d8..84b024ff 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs @@ -32,7 +32,7 @@ public class AdminUserController : ControllerBase UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id); if (targetedUser == null) return this.NotFound(); - + string[] hashes = { targetedUser.PlanetHashLBP2, targetedUser.PlanetHashLBP3, @@ -44,7 +44,7 @@ public class AdminUserController : ControllerBase { // Don't try to remove empty hashes. That's a horrible idea. if (string.IsNullOrWhiteSpace(hash)) continue; - + // Find users with a matching hash List users = await this.database.Users .Where(u => u.PlanetHashLBP2 == hash || @@ -54,7 +54,7 @@ public class AdminUserController : ControllerBase // We should match at least the targeted user... System.Diagnostics.Debug.Assert(users.Count != 0); - + // Reset each users' hash. foreach (UserEntity userWithPlanet in users) { @@ -63,7 +63,7 @@ public class AdminUserController : ControllerBase userWithPlanet.PlanetHashLBPVita = ""; Logger.Success($"Deleted planets for {userWithPlanet.Username} (id:{userWithPlanet.UserId})", LogArea.Admin); } - + // And finally, attempt to remove the resource from the filesystem. We don't want that taking up space. try { @@ -82,7 +82,10 @@ public class AdminUserController : ControllerBase Logger.Error($"Failed to delete planet resource {hash}\n{e}", LogArea.Admin); } } - + + await this.database.SendNotification(targetedUser.UserId, + "Your earth decorations have been reset by a moderator."); + await this.database.SaveChangesAsync(); return this.Redirect($"/user/{targetedUser.UserId}"); diff --git a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationSlotController.cs b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationSlotController.cs index 383979a6..27aabd01 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationSlotController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Moderator/ModerationSlotController.cs @@ -1,4 +1,3 @@ -#nullable enable using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; @@ -34,6 +33,10 @@ public class ModerationSlotController : ControllerBase // Send webhook with slot.Name and slot.Creator.Username await WebhookHelper.SendWebhook("New Team Pick!", $"The level [**{slot.Name}**]({ServerConfiguration.Instance.ExternalUrl}/slot/{slot.SlotId}) by **{slot.Creator?.Username}** has been team picked"); + // Send a notification to the creator + await this.database.SendNotification(slot.CreatorId, + $"Your level, {slot.Name}, has been team picked!"); + await this.database.SaveChangesAsync(); return this.Redirect("~/slot/" + id); @@ -50,6 +53,10 @@ public class ModerationSlotController : ControllerBase slot.TeamPick = false; + // Send a notification to the creator + await this.database.SendNotification(slot.CreatorId, + $"Your level, {slot.Name}, is no longer team picked."); + await this.database.SaveChangesAsync(); return this.Redirect("~/slot/" + id); @@ -64,6 +71,10 @@ public class ModerationSlotController : ControllerBase SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == id); if (slot == null) return this.Ok(); + // Send a notification to the creator + await this.database.SendNotification(slot.CreatorId, + $"Your level, {slot.Name}, has been deleted by a moderator."); + await this.database.RemoveSlot(slot); return this.Redirect("~/slots/0"); diff --git a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml new file mode 100644 index 00000000..3664a4b7 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml @@ -0,0 +1,9 @@ +@page "/admin/user/{id:int}/sendNotification" +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminSendNotificationPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = $"Send notification to {Model.TargetedUser!.Username}"; +} + +@await Html.PartialAsync("Partials/AdminSendNotificationPartial", Model.TargetedUser) \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml.cs new file mode 100644 index 00000000..c8713000 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSendNotificationPage.cshtml.cs @@ -0,0 +1,39 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin; + +public class AdminSendNotificationPage : BaseLayout +{ + public AdminSendNotificationPage(DatabaseContext database) : base(database) + { } + + public UserEntity? TargetedUser; + + public async Task OnGet([FromRoute] int id) + { + UserEntity? user = this.Database.UserFromWebRequest(this.Request); + if (user == null || !user.IsAdmin) return this.NotFound(); + + this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id); + if (this.TargetedUser == null) return this.NotFound(); + + return this.Page(); + } + + public async Task OnPost([FromRoute] int id, [FromForm] string notificationContent) + { + UserEntity? user = this.Database.UserFromWebRequest(this.Request); + if (user == null || !user.IsAdmin) return this.NotFound(); + + this.TargetedUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == id); + if (this.TargetedUser == null) return this.NotFound(); + + await this.Database.SendNotification(this.TargetedUser.UserId, notificationContent); + + return this.Redirect($"/user/{this.TargetedUser.UserId}"); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSetGrantedSlotsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSetGrantedSlotsPage.cshtml index fe2ac408..9be780cf 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSetGrantedSlotsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Admin/AdminSetGrantedSlotsPage.cshtml @@ -3,7 +3,7 @@ @{ Layout = "Layouts/BaseLayout"; - Model.Title = "Set granted slots for " + Model.TargetedUser!.Username; + Model.Title = $"Set granted slots for {Model.TargetedUser!.Username}"; } @await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.TargetedUser) \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/AnnouncePage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/AnnouncePage.cshtml.cs deleted file mode 100644 index 41b554c6..00000000 --- a/ProjectLighthouse.Servers.Website/Pages/AnnouncePage.cshtml.cs +++ /dev/null @@ -1,83 +0,0 @@ -#nullable enable - -using LBPUnion.ProjectLighthouse.Configuration; -using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Entities.Website; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; - -public class AnnouncePage : BaseLayout -{ - public AnnouncePage(DatabaseContext database) : base(database) - { } - - public List Announcements { get; set; } = new(); - public string Error { get; set; } = ""; - - public async Task OnGet() - { - this.Announcements = await this.Database.WebsiteAnnouncements - .Include(a => a.Publisher) - .OrderByDescending(a => a.AnnouncementId) - .ToListAsync(); - - return this.Page(); - } - - public async Task OnPost([FromForm] string title, [FromForm] string content) - { - UserEntity? user = this.Database.UserFromWebRequest(this.Request); - if (user == null) return this.BadRequest(); - if (!user.IsAdmin) return this.BadRequest(); - - if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) - { - this.Error = "Invalid form data, please ensure all fields are filled out."; - return this.Page(); - } - - WebsiteAnnouncementEntity announcement = new() - { - Title = title.Trim(), - Content = content.Trim(), - PublisherId = user.UserId, - }; - - await this.Database.WebsiteAnnouncements.AddAsync(announcement); - await this.Database.SaveChangesAsync(); - - if (DiscordConfiguration.Instance.DiscordIntegrationEnabled) - { - string truncatedAnnouncement = content.Length > 250 - ? content[..250] + $"... [read more]({ServerConfiguration.Instance.ExternalUrl}/announce)" - : content; - - await WebhookHelper.SendWebhook($":mega: {title}", truncatedAnnouncement); - } - - return this.RedirectToPage(); - } - - public async Task OnPostDelete(int id) - { - UserEntity? user = this.Database.UserFromWebRequest(this.Request); - if (user == null) return this.BadRequest(); - if (!user.IsAdmin) return this.BadRequest(); - - WebsiteAnnouncementEntity? announcement = await this.Database.WebsiteAnnouncements - .Where(a => a.AnnouncementId == id) - .FirstOrDefaultAsync(); - - if (announcement == null) return this.BadRequest(); - - this.Database.WebsiteAnnouncements.Remove(announcement); - await this.Database.SaveChangesAsync(); - - return this.RedirectToPage(); - } -} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml index 4f73184e..614abfa3 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml @@ -3,24 +3,30 @@ @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Servers.Website.Types +@using Microsoft.EntityFrameworkCore @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts.BaseLayout @{ if (Model.User == null) { - if (ServerConfiguration.Instance.Authentication.RegistrationEnabled) - { - Model.NavigationItemsRight.Add(new PageNavigationItem(BaseLayoutStrings.HeaderLoginRegister, "/login", "sign in")); - } - else - { - Model.NavigationItemsRight.Add(new PageNavigationItem(BaseLayoutStrings.HeaderLogin, "/login", "sign in")); - } + Model.NavigationItemsRight.Add(ServerConfiguration.Instance.Authentication.RegistrationEnabled + ? new PageNavigationItem(BaseLayoutStrings.HeaderLoginRegister, "/login", "sign in") + : new PageNavigationItem(BaseLayoutStrings.HeaderLogin, "/login", "sign in")); } else { Model.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderAuthentication, "/authentication", "key")); + @if (await Model.Database.Notifications.AnyAsync(n => n.UserId == Model.User.UserId && !n.IsDismissed) || + await Model.Database.WebsiteAnnouncements.AnyAsync()) + { + Model.NavigationItemsRight.Add(new PageNavigationItem(GeneralStrings.Notifications, "/notifications", "bell")); + } + else + { + Model.NavigationItemsRight.Add(new PageNavigationItem(GeneralStrings.Notifications, "/notifications", "bell outline")); + } + @if (Model.User.IsAdmin) { Model.NavigationItemsRight.Add(new PageNavigationItem(BaseLayoutStrings.HeaderAdminPanel, "/admin", "wrench", "yellow")); @@ -33,8 +39,8 @@ Model.IsMobile = Model.Request.IsMobile(); - string title = Model.Title == string.Empty - ? ServerConfiguration.Instance.Customization.ServerName + string title = Model.Title == string.Empty + ? ServerConfiguration.Instance.Customization.ServerName : $"{Model.Title} - {ServerConfiguration.Instance.Customization.ServerName}"; } @@ -89,7 +95,7 @@ @@ -103,7 +109,9 @@ @if (!Model.IsMobile) { - @Model.Translate(navigationItem.Name) + + @Model.Translate(navigationItem.Name) + } } @@ -122,11 +130,11 @@ } } - + @if (Model.User != null) { - @@ -157,11 +165,11 @@ @Model.Translate(BaseLayoutStrings.LicenseWarnTitle)

- @Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1, + @Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1, "GNU Affero General Public License v3.0"))

- @Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2, + @Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2, "git status", "", ""))

@@ -239,18 +247,18 @@