mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-04-19 19:14:51 +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.
|
|
@ -3,7 +3,7 @@
|
|||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
|
@ -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;
|
||||
|
@ -66,12 +68,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
|
|||
$"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<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.
|
||||
|
@ -104,7 +140,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
|
|||
}
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
|
||||
string filteredText = CensorHelper.FilterMessage(message);
|
||||
|
||||
if (ServerConfiguration.Instance.LogChatMessages) Logger.Info($"{username}: \"{message}\"", LogArea.Filter);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<UserEntity> 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}");
|
||||
|
|
|
@ -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"));
|
||||
|
@ -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 @@
|
|||
|
||||
<a class="item home-logo" href="/">
|
||||
<img src="~/logo-mono.png" alt="Home" class="logo-mono"/>
|
||||
<img src="~/@(ServerConfiguration.Instance.WebsiteConfiguration.PrideEventEnabled && DateTime.Now.Month == 6 ? "logo-pride.png" : "logo-color.png")"
|
||||
<img src="~/@(ServerConfiguration.Instance.WebsiteConfiguration.PrideEventEnabled && DateTime.Now.Month == 6 ? "logo-pride.png" : "logo-color.png")"
|
||||
alt="Home"
|
||||
class="logo-color"/>
|
||||
</a>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -122,11 +130,11 @@
|
|||
}
|
||||
</a>
|
||||
}
|
||||
|
||||
|
||||
@if (Model.User != null)
|
||||
{
|
||||
<a class="item" href="/user/@Model.User.UserId">
|
||||
<img src="/gameAssets/@Model.User.WebsiteAvatarHash"
|
||||
<img src="/gameAssets/@Model.User.WebsiteAvatarHash"
|
||||
onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'"
|
||||
alt=""
|
||||
class="lighthouse-avatar"/>
|
||||
|
@ -157,11 +165,11 @@
|
|||
<i class="warning icon"></i>
|
||||
<span style="font-size: 1.2rem;">@Model.Translate(BaseLayoutStrings.LicenseWarnTitle)</span>
|
||||
<p>
|
||||
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1,
|
||||
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1,
|
||||
"<a href=\"https://github.com/LBPUnion/project-lighthouse/blob/main/LICENSE\">GNU Affero General Public License v3.0</a>"))
|
||||
</p>
|
||||
<p>
|
||||
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2,
|
||||
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2,
|
||||
"<code>git status</code>", "<a href=\"https://github.com/LBPUnion/project-lighthouse/issues\">", "</a>"))
|
||||
</p>
|
||||
<p>
|
||||
|
@ -239,18 +247,18 @@
|
|||
|
||||
<script>
|
||||
const collapsible = document.getElementsByClassName("collapsible");
|
||||
|
||||
for (let i = 0; i < collapsible.length; i++)
|
||||
|
||||
for (let i = 0; i < collapsible.length; i++)
|
||||
{
|
||||
collapsible[i].addEventListener("click", function()
|
||||
collapsible[i].addEventListener("click", function()
|
||||
{
|
||||
this.classList.toggle("active");
|
||||
const content = this.nextElementSibling;
|
||||
if (content.style.display === "block")
|
||||
if (content.style.display === "block")
|
||||
{
|
||||
content.style.display = "none";
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
content.style.display = "block";
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -55,10 +53,10 @@ public class BaseLayout : PageModel
|
|||
if (this.language != null) return this.language;
|
||||
|
||||
if (this.User != null) return this.language = this.User.Language;
|
||||
|
||||
|
||||
IRequestCultureFeature? requestCulture = this.Request.HttpContext.Features.Get<IRequestCultureFeature>();
|
||||
if (requestCulture == null) return this.language = LocalizationManager.DefaultLang;
|
||||
|
||||
|
||||
return this.language = requestCulture.RequestCulture.UICulture.Name;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
<div class="ui blue segment" style="position: relative;">
|
||||
<p>There are no announcements to display.</p>
|
||||
</div>
|
||||
@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>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>
|
|
@ -141,7 +141,7 @@
|
|||
@if (isMobile)
|
||||
{
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
@ -241,7 +241,7 @@ else
|
|||
<p>
|
||||
<i>The user's privacy settings prevent you from viewing this page.</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -256,7 +256,7 @@ else
|
|||
@await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#levels", language, timeZone, isMobile, true)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="lh-content" id="lh-playlists">
|
||||
|
@ -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 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>
|
||||
}
|
||||
|
||||
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipePlanets">
|
||||
<i class="trash alternate icon"></i>
|
||||
<span>Wipe Earth Decorations</span>
|
||||
</a>
|
||||
|
||||
@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>
|
||||
<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>
|
||||
}
|
||||
|
||||
@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)
|
||||
|
@ -375,7 +369,7 @@ function setVisible(e){
|
|||
}
|
||||
// unhide content
|
||||
eTarget.style.display = "";
|
||||
|
||||
|
||||
e.classList.add("active");
|
||||
}
|
||||
|
||||
|
@ -394,7 +388,7 @@ if (selectedElement != null) {
|
|||
while (selectedElement != null && !selectedElement.classList.contains("lh-content")){
|
||||
selectedElement = selectedElement.parentElement;
|
||||
}
|
||||
|
||||
|
||||
let sidebarEle = document.querySelector("[target=" + selectedElement.id + "]")
|
||||
setVisible(sidebarEle);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
@ -194,7 +181,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>." + "\nuni
|
|||
public async Task Filter_ShouldSendEmail_WhenMailEnabled_AndEmailNotTaken()
|
||||
{
|
||||
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
|
||||
|
||||
|
||||
Mock<IMailService> mailMock = getMailServiceMock();
|
||||
|
||||
const string request = "/setemail unittest@unittest.com";
|
||||
|
|
|
@ -48,7 +48,7 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (@case.Expired || @case.Dismissed)
|
||||
{
|
||||
switch (@case.Type)
|
||||
|
@ -63,18 +63,30 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
case CaseType.LevelHide:
|
||||
{
|
||||
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();
|
||||
|
@ -105,7 +121,7 @@ public class PerformCaseActionsTask : IRepeatingTask
|
|||
{
|
||||
user!.PermissionLevel = PermissionLevel.Banned;
|
||||
user.BannedReason = @case.Reason;
|
||||
|
||||
|
||||
database.GameTokens.RemoveRange(database.GameTokens.Where(t => t.UserId == user.UserId));
|
||||
database.WebTokens.RemoveRange(database.WebTokens.Where(t => t.UserId == user.UserId));
|
||||
break;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -120,12 +140,19 @@ 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,10 +60,14 @@ 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
|
||||
|
||||
|
||||
#region Website
|
||||
public DbSet<WebsiteAnnouncementEntity> WebsiteAnnouncements { 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")
|
||||
|
|
|
@ -26,12 +26,18 @@ public class LbpOutputFormatter : TextOutputFormatter
|
|||
if (isSerializable) return base.CanWriteType(type);
|
||||
Logger.Warn($"Unable to serialize type '{type?.Name}' because it doesn't extend ISerializable: (fullType={type?.FullName}", LogArea.Serialization);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
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
Reference in a new issue