mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-05 04:41:27 +00:00
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
This commit is contained in:
parent
98b370b106
commit
aea66b4a74
33 changed files with 712 additions and 199 deletions
30
Documentation/Errors.md
Normal file
30
Documentation/Errors.md
Normal file
|
@ -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.
|
|
@ -60,4 +60,7 @@
|
|||
<data name="announcements" xml:space="preserve">
|
||||
<value>Announcements</value>
|
||||
</data>
|
||||
<data name="notifications" xml:space="preserve">
|
||||
<value>Notifications</value>
|
||||
</data>
|
||||
</root>
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
@ -71,7 +73,41 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
|
|||
}
|
||||
|
||||
[HttpGet("notification")]
|
||||
public IActionResult Notification() => this.Ok();
|
||||
[Produces("text/xml")]
|
||||
public async Task<IActionResult> Notification()
|
||||
{
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
List<NotificationEntity> 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(),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters chat messages sent by a user.
|
||||
|
|
|
@ -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,6 +190,8 @@ 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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -242,8 +271,10 @@ public class PublishController : ControllerBase
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,9 @@ public class AdminUserController : ControllerBase
|
|||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
|
@ -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<IActionResult> 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<IActionResult> 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}");
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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<WebsiteAnnouncementEntity> Announcements { get; set; } = new();
|
||||
public string Error { get; set; } = "";
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
this.Announcements = await this.Database.WebsiteAnnouncements
|
||||
.Include(a => a.Publisher)
|
||||
.OrderByDescending(a => a.AnnouncementId)
|
||||
.ToListAsync();
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> 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<IActionResult> 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();
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
@ -103,7 +109,9 @@
|
|||
|
||||
@if (!Model.IsMobile)
|
||||
{
|
||||
<span class="ui inline @navigationItem.CustomColor text">@Model.Translate(navigationItem.Name)</span>
|
||||
<span class="ui inline @navigationItem.CustomColor text">
|
||||
@Model.Translate(navigationItem.Name)
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#nullable enable
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Database;
|
||||
using LBPUnion.ProjectLighthouse.Localization;
|
||||
|
@ -26,6 +25,7 @@ public class BaseLayout : PageModel
|
|||
public string Title = string.Empty;
|
||||
|
||||
private UserEntity? user;
|
||||
|
||||
public BaseLayout(DatabaseContext database)
|
||||
{
|
||||
this.Database = database;
|
||||
|
@ -33,8 +33,6 @@ public class BaseLayout : PageModel
|
|||
this.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderUsers, "/users/0", "user friends"));
|
||||
this.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderPhotos, "/photos/0", "camera"));
|
||||
this.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderSlots, "/slots/0", "globe americas"));
|
||||
|
||||
this.NavigationItemsRight.Add(new PageNavigationItem(GeneralStrings.Announcements, "/announce", "bullhorn"));
|
||||
}
|
||||
|
||||
public new UserEntity? User {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
@page "/announce"
|
||||
@page "/notifications"
|
||||
@using LBPUnion.ProjectLighthouse.Localization.StringLists
|
||||
@using LBPUnion.ProjectLighthouse.Types.Entities.Notifications
|
||||
@using LBPUnion.ProjectLighthouse.Types.Entities.Website
|
||||
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.AnnouncePage
|
||||
@using LBPUnion.ProjectLighthouse.Types.Notifications
|
||||
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.NotificationsPage
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@{
|
||||
Layout = "Layouts/BaseLayout";
|
||||
Model.Title = Model.Translate(GeneralStrings.Announcements);
|
||||
Model.Title = Model.Translate(GeneralStrings.Notifications);
|
||||
}
|
||||
|
||||
@if (Model.User != null && Model.User.IsAdmin)
|
||||
|
@ -17,7 +19,7 @@
|
|||
{
|
||||
@await Html.PartialAsync("Partials/ErrorModalPartial", (Model.Translate(GeneralStrings.Error), Model.Error), ViewData)
|
||||
}
|
||||
<form id="form" method="POST" class="ui form center aligned" action="/announce">
|
||||
<form id="form" method="POST" class="ui form center aligned" action="/notifications">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="field">
|
||||
<label style="text-align: left" for="title">Announcement Title</label>
|
||||
|
@ -32,7 +34,7 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@if (Model.Announcements.Any())
|
||||
@if (Model.Announcements.Count > 0)
|
||||
{
|
||||
@foreach (WebsiteAnnouncementEntity announcement in Model.Announcements)
|
||||
{
|
||||
|
@ -40,7 +42,7 @@
|
|||
<div>
|
||||
<h3>@announcement.Title</h3>
|
||||
<div style="padding-bottom: 2em;">
|
||||
<span style="white-space: pre-line; ">@announcement.Content</span>
|
||||
<span style="white-space: pre-line">@announcement.Content</span>
|
||||
</div>
|
||||
@if (announcement.Publisher != null)
|
||||
{
|
||||
|
@ -58,6 +60,7 @@
|
|||
@Html.AntiForgeryToken()
|
||||
<button
|
||||
asp-page-handler="delete"
|
||||
asp-route-type="announcement"
|
||||
asp-route-id="@announcement.AnnouncementId"
|
||||
onclick="return confirm('Are you sure you want to delete this announcement?')"
|
||||
class="ui red icon button"
|
||||
|
@ -69,9 +72,45 @@
|
|||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Notifications.Count > 0)
|
||||
{
|
||||
@foreach (NotificationEntity notification in Model.Notifications)
|
||||
{
|
||||
<div class="ui blue segment" style="position: relative;">
|
||||
<div>
|
||||
<div>
|
||||
<i class="bell icon"></i>
|
||||
<span style="white-space: pre-line">@notification.Text</span>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<button
|
||||
asp-page-handler="delete"
|
||||
asp-route-type="notification"
|
||||
asp-route-id="@notification.Id"
|
||||
onclick="return confirm('Are you sure you want to mark this notification as read?')"
|
||||
class="ui green icon button"
|
||||
style="position: absolute; right: 0.5em; top: 0.5em">
|
||||
<i class="check icon"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (Model.User == null)
|
||||
{
|
||||
<div class="ui blue segment" style="position: relative;">
|
||||
<p>You need to <a href="/login">log in</a> to view your notifications.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="ui blue segment" style="position: relative;">
|
||||
<p>There are no announcements to display.</p>
|
||||
<p>You don't have any new notifications.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Database;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Notifications;
|
||||
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 NotificationsPage : BaseLayout
|
||||
{
|
||||
public NotificationsPage(DatabaseContext database) : base(database)
|
||||
{ }
|
||||
|
||||
public List<WebsiteAnnouncementEntity> Announcements { get; set; } = new();
|
||||
public List<NotificationEntity> Notifications { get; set; } = new();
|
||||
public string Error { get; set; } = "";
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
this.Announcements = await this.Database.WebsiteAnnouncements
|
||||
.Include(a => a.Publisher)
|
||||
.OrderByDescending(a => a.AnnouncementId)
|
||||
.ToListAsync();
|
||||
|
||||
if (this.User == null) return this.Page();
|
||||
|
||||
this.Notifications = await this.Database.Notifications
|
||||
.Where(n => n.UserId == this.User.UserId)
|
||||
.Where(n => !n.IsDismissed)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> 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,
|
||||
};
|
||||
|
||||
this.Database.WebsiteAnnouncements.Add(announcement);
|
||||
await this.Database.SaveChangesAsync();
|
||||
|
||||
if (!DiscordConfiguration.Instance.DiscordIntegrationEnabled) return this.RedirectToPage();
|
||||
|
||||
string truncatedAnnouncement = content.Length > 250
|
||||
? content[..250] + $"... [read more]({ServerConfiguration.Instance.ExternalUrl}/notifications)"
|
||||
: content;
|
||||
|
||||
await WebhookHelper.SendWebhook($":mega: {title}", truncatedAnnouncement);
|
||||
|
||||
return this.RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDelete(string type, int id)
|
||||
{
|
||||
UserEntity? user = this.Database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.BadRequest();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "announcement":
|
||||
{
|
||||
WebsiteAnnouncementEntity? announcement = await this.Database.WebsiteAnnouncements
|
||||
.Where(a => a.AnnouncementId == id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (announcement == null || !user.IsAdmin) return this.BadRequest();
|
||||
|
||||
this.Database.WebsiteAnnouncements.Remove(announcement);
|
||||
await this.Database.SaveChangesAsync();
|
||||
|
||||
break;
|
||||
}
|
||||
case "notification":
|
||||
{
|
||||
NotificationEntity? notification = await this.Database.Notifications
|
||||
.Where(n => n.Id == id)
|
||||
.Where(n => !n.IsDismissed)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (notification == null || notification.UserId != user.UserId) return this.BadRequest();
|
||||
|
||||
notification.IsDismissed = true;
|
||||
|
||||
await this.Database.SaveChangesAsync();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.RedirectToPage();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity
|
||||
|
||||
<form method="post" action="/admin/user/@Model.UserId/sendNotification">
|
||||
@Html.AntiForgeryToken()
|
||||
<span class="ui left action input">
|
||||
<button type="submit" class="ui blue button">
|
||||
<i class="comment icon"></i>
|
||||
<span>Send Notification</span>
|
||||
</button>
|
||||
<textarea name="notificationContent" placeholder="Notification Content" rows="1" cols="100" style="width: 100%;"></textarea>
|
||||
</span>
|
||||
</form>
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<form method="post" action="/admin/user/@Model.UserId/setGrantedSlots">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="ui left action input">
|
||||
<span class="ui left action input">
|
||||
<button type="submit" class="ui blue button">
|
||||
<i class="pencil icon"></i>
|
||||
<span>Set Granted Slots</span>
|
||||
</button>
|
||||
<input type="text" name="grantedSlotCount" placeholder="Granted Slots" value="@Model.AdminGrantedSlots">
|
||||
</div>
|
||||
</span>
|
||||
</form>
|
|
@ -314,37 +314,31 @@ else
|
|||
|
||||
@if (!Model.ProfileUser.IsBanned)
|
||||
{
|
||||
<div>
|
||||
<a class="ui red button" href="/moderation/newCase?type=@((int)CaseType.UserBan)&affectedId=@Model.ProfileUser.UserId">
|
||||
<i class="ban icon"></i>
|
||||
<span>Ban User</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ui fitted hidden divider"></div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipePlanets">
|
||||
<i class="trash alternate icon"></i>
|
||||
<span>Wipe Earth Decorations</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ui fitted hidden divider"></div>
|
||||
|
||||
@if (!Model.CommentsDisabledByModerator)
|
||||
{
|
||||
<div>
|
||||
<a class="ui yellow button" href="/moderation/newCase?type=@((int)CaseType.UserDisableComments)&affectedId=@Model.ProfileUser.UserId">
|
||||
<i class="lock icon"></i>
|
||||
<span>Forcibly Disable Comments</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ui fitted hidden divider"></div>
|
||||
}
|
||||
|
||||
@if (Model.User.IsAdmin)
|
||||
{
|
||||
<div class="ui divider"></div>
|
||||
@await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser)
|
||||
<div class="ui fitted hidden divider" style="margin-bottom: 3px;"></div>
|
||||
@await Html.PartialAsync("Partials/AdminSendNotificationPartial", Model.ProfileUser)
|
||||
}
|
||||
</div>
|
||||
@if (isMobile)
|
||||
|
|
|
@ -107,19 +107,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
|
|||
Assert.Equal(expected, announceMsg);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Notification_ShouldReturn_Empty()
|
||||
{
|
||||
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
|
||||
|
||||
MessageController messageController = new(dbMock);
|
||||
messageController.SetupTestController();
|
||||
|
||||
IActionResult result = messageController.Notification();
|
||||
|
||||
Assert.IsType<OkResult>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filter_ShouldNotCensor_WhenCensorDisabled()
|
||||
{
|
||||
|
|
|
@ -63,6 +63,10 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
case CaseType.UserDisableComments:
|
||||
{
|
||||
user!.CommentsEnabled = true;
|
||||
|
||||
await database.SendNotification(user.UserId,
|
||||
"Your profile comments have been re-enabled by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -70,11 +74,19 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
{
|
||||
slot!.Hidden = false;
|
||||
slot.HiddenReason = "";
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"Your level, {slot.Name}, is no longer hidden by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
case CaseType.LevelDisableComments:
|
||||
{
|
||||
slot!.CommentsEnabled = true;
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"The comments on your level, {slot.Name}, have been re-enabled by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
case CaseType.LevelLock:
|
||||
|
@ -82,6 +94,10 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
slot!.InitiallyLocked = false;
|
||||
slot.LockedByModerator = false;
|
||||
slot.LockedReason = "";
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"Your level, {slot.Name}, is no longer locked by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
|
@ -113,6 +129,10 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
case CaseType.UserDisableComments:
|
||||
{
|
||||
user!.CommentsEnabled = false;
|
||||
|
||||
await database.SendNotification(user.UserId,
|
||||
"Your profile comments have been disabled by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -121,11 +141,18 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
slot!.Hidden = true;
|
||||
slot.HiddenReason = @case.Reason;
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"Your level, {slot.Name}, has been hidden by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
case CaseType.LevelDisableComments:
|
||||
{
|
||||
slot!.CommentsEnabled = false;
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"The comments on your level, {slot.Name}, have been disabled by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
case CaseType.LevelLock:
|
||||
|
@ -133,6 +160,10 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
slot!.InitiallyLocked = true;
|
||||
slot.LockedByModerator = true;
|
||||
slot.LockedReason = @case.Reason;
|
||||
|
||||
await database.SendNotification(slot.CreatorId,
|
||||
$"Your level, {slot.Name}, has been locked by a moderator.");
|
||||
|
||||
break;
|
||||
}
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
|
||||
|
||||
public class NotificationConfiguration
|
||||
{
|
||||
public bool ShowServerNameInText { get; set; } = true;
|
||||
public bool ShowTimestampInText { get; set; } = false;
|
||||
}
|
|
@ -11,7 +11,7 @@ public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
|
|||
// This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly.
|
||||
// If you are modifying anything here, this value MUST be incremented.
|
||||
// Thanks for listening~
|
||||
public override int ConfigVersion { get; set; } = 23;
|
||||
public override int ConfigVersion { get; set; } = 24;
|
||||
|
||||
public override string ConfigName { get; set; } = "lighthouse.yml";
|
||||
public string WebsiteListenUrl { get; set; } = "http://localhost:10060";
|
||||
|
@ -43,6 +43,7 @@ public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
|
|||
public RateLimitConfiguration RateLimitConfiguration { get; set; } = new();
|
||||
public TwoFactorConfiguration TwoFactorConfiguration { get; set; } = new();
|
||||
public RichPresenceConfiguration RichPresenceConfiguration { get; set; } = new();
|
||||
public NotificationConfiguration NotificationConfiguration { get; set; } = new();
|
||||
|
||||
public override ConfigurationBase<ServerConfiguration> Deserialize(IDeserializer deserializer, string text) => deserializer.Deserialize<ServerConfiguration>(text);
|
||||
}
|
|
@ -11,7 +11,6 @@ namespace LBPUnion.ProjectLighthouse.Database;
|
|||
|
||||
public partial class DatabaseContext
|
||||
{
|
||||
|
||||
public async Task<string> UsernameFromGameToken(GameTokenEntity? token)
|
||||
{
|
||||
if (token == null) return "";
|
||||
|
|
55
ProjectLighthouse/Database/DatabaseContext.Notifications.cs
Normal file
55
ProjectLighthouse/Database/DatabaseContext.Notifications.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Notifications;
|
||||
using LBPUnion.ProjectLighthouse.Types.Notifications;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Database;
|
||||
|
||||
public partial class DatabaseContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a notification to a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID of the target user.</param>
|
||||
/// <param name="text">The message to send.</param>
|
||||
/// <param name="type">The <see cref="NotificationType"/> for the notification. Defaults to <c>ModerationNotification</c>.</param>
|
||||
public async Task SendNotification
|
||||
(int userId, string text, NotificationType type = NotificationType.ModerationNotification)
|
||||
{
|
||||
if (!await this.Users.AnyAsync(u => u.UserId == userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length > 2048)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new(text);
|
||||
|
||||
// Prepend server name to notification text if enabled
|
||||
if (ServerConfiguration.Instance.NotificationConfiguration.ShowServerNameInText)
|
||||
{
|
||||
builder.Insert(0, $"[{ServerConfiguration.Instance.Customization.ServerName}] ");
|
||||
}
|
||||
// Prepend timestamp to notification text if enabled
|
||||
if (ServerConfiguration.Instance.NotificationConfiguration.ShowTimestampInText)
|
||||
{
|
||||
builder.Insert(0, $"[{DateTime.Now:g}] ");
|
||||
}
|
||||
|
||||
NotificationEntity notification = new()
|
||||
{
|
||||
UserId = userId,
|
||||
Type = type,
|
||||
Text = builder.ToString(),
|
||||
};
|
||||
|
||||
this.Notifications.Add(notification);
|
||||
await this.SaveChangesAsync();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
|
|||
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Maintenance;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Moderation;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Notifications;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Website;
|
||||
|
@ -59,6 +60,10 @@ public partial class DatabaseContext : DbContext
|
|||
public DbSet<GriefReportEntity> Reports { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Notifications
|
||||
public DbSet<NotificationEntity> Notifications { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Misc
|
||||
public DbSet<CompletedMigrationEntity> CompletedMigrations { get; set; }
|
||||
#endregion
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
using LBPUnion.ProjectLighthouse.Database;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ProjectLighthouse.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20231027215257_AddNotificationEntity")]
|
||||
public partial class AddNotificationEntity : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
Type = table.Column<int>(type: "int", nullable: false),
|
||||
Text = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsDismissed = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notifications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Notifications_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_UserId",
|
||||
table: "Notifications",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notifications");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ namespace ProjectLighthouse.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.10")
|
||||
.HasAnnotation("ProductVersion", "7.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b =>
|
||||
|
@ -598,6 +598,31 @@ namespace ProjectLighthouse.Migrations
|
|||
b.ToTable("Cases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Notifications.NotificationEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsDismissed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.BlockedProfileEntity", b =>
|
||||
{
|
||||
b.Property<int>("BlockedProfileId")
|
||||
|
@ -1308,6 +1333,17 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Navigation("Dismisser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Notifications.NotificationEntity", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.BlockedProfileEntity", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "BlockedUser")
|
||||
|
|
|
@ -32,6 +32,12 @@ public class LbpOutputFormatter : TextOutputFormatter
|
|||
{
|
||||
if (context.Object is not ILbpSerializable o) return;
|
||||
|
||||
if (o is LbpCustomXml customXml)
|
||||
{
|
||||
await context.HttpContext.Response.WriteAsync(customXml.Content);
|
||||
return;
|
||||
}
|
||||
|
||||
string serialized = LighthouseSerializer.Serialize(context.HttpContext.RequestServices, o);
|
||||
|
||||
await context.HttpContext.Response.WriteAsync(serialized);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
|
||||
using LBPUnion.ProjectLighthouse.Types.Notifications;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types.Entities.Notifications;
|
||||
|
||||
public class NotificationEntity
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
|
||||
#nullable enable
|
||||
|
||||
[ForeignKey(nameof (UserId))]
|
||||
public UserEntity? User { get; set; }
|
||||
|
||||
#nullable disable
|
||||
|
||||
public NotificationType Type { get; set; } = NotificationType.ModerationNotification;
|
||||
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
public bool IsDismissed { get; set; }
|
||||
}
|
|
@ -19,6 +19,4 @@ public class WebsiteAnnouncementEntity
|
|||
|
||||
[ForeignKey(nameof(PublisherId))]
|
||||
public UserEntity? Publisher { get; set; }
|
||||
|
||||
#nullable disable
|
||||
}
|
37
ProjectLighthouse/Types/Notifications/NotificationType.cs
Normal file
37
ProjectLighthouse/Types/Notifications/NotificationType.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types.Notifications;
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("mmPick")]
|
||||
MMPick,
|
||||
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("playsOnSlot")]
|
||||
PlaysOnSlot,
|
||||
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("top100Hottest")]
|
||||
Top100Hottest,
|
||||
|
||||
/// <summary>
|
||||
/// Displays a moderation notification upon login and in LBP Messages.
|
||||
/// </summary>
|
||||
[XmlEnum("moderationNotification")]
|
||||
ModerationNotification,
|
||||
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("commentOnSlot")]
|
||||
CommentOnSlot,
|
||||
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("heartsOnSlot")]
|
||||
HeartsOnSlot,
|
||||
|
||||
[Obsolete("This notification type is ignored by the game and does nothing.")]
|
||||
[XmlEnum("heartedAsAuthor")]
|
||||
HeartedAsAuthor,
|
||||
}
|
21
ProjectLighthouse/Types/Serialization/GameNotification.cs
Normal file
21
ProjectLighthouse/Types/Serialization/GameNotification.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Xml.Serialization;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Notifications;
|
||||
using LBPUnion.ProjectLighthouse.Types.Notifications;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
|
||||
|
||||
[XmlRoot("notification")]
|
||||
public class GameNotification : ILbpSerializable
|
||||
{
|
||||
[XmlAttribute("type")]
|
||||
public NotificationType Type { get; set; }
|
||||
|
||||
[XmlElement("text")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
public static GameNotification CreateFromEntity(NotificationEntity notification) => new()
|
||||
{
|
||||
Type = notification.Type,
|
||||
Text = notification.Text,
|
||||
};
|
||||
}
|
6
ProjectLighthouse/Types/Serialization/LbpCustomXml.cs
Normal file
6
ProjectLighthouse/Types/Serialization/LbpCustomXml.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
|
||||
|
||||
public class LbpCustomXml : ILbpSerializable
|
||||
{
|
||||
public required string Content { get; init; }
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue