add alert/notification functionality (for server connection events and messages)

This commit is contained in:
RaidMax 2022-06-11 11:34:00 -05:00
parent ffb0e5cac1
commit a44b4e9475
16 changed files with 579 additions and 21 deletions

View File

@ -0,0 +1,55 @@
using System;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.Alerts;
public static class AlertExtensions
{
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
{
return new Alert.AlertState
{
RecipientId = client.ClientId,
Category = type ?? Alert.AlertCategory.Information
};
}
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
{
state.Category = category;
return state;
}
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
{
state.Type = type;
return state;
}
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
{
state.Message = message;
return state;
}
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
{
state.ExpiresAt = DateTime.Now.Add(expiration);
return state;
}
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
{
state.Source = source;
return state;
}
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
{
state.Source = client.Name.StripColors();
state.SourceId = client.ClientId;
return state;
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Alerts;
public class AlertManager : IAlertManager
{
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
public AlertManager(ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_states.TryAdd(0, new List<Alert.AlertState>());
}
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
public async Task Initialize()
{
foreach (var source in _staticSources)
{
var alerts = await source();
foreach (var alert in alerts)
{
AddAlert(alert);
}
}
}
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
lock (_states)
{
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
alerts = alerts.Concat(_states[0].Where(alert =>
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
}
if (_states.ContainsKey(client.ClientId))
{
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
if (matchingEvent is null)
{
continue;
}
items.Remove(matchingEvent);
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
}
public void AddAlert(Alert.AlertState alert)
{
lock (_states)
{
if (alert.RecipientId is null)
{
_states[0].Add(alert);
return;
}
if (!_states.ContainsKey(alert.RecipientId.Value))
{
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
}
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
{
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
}
_states[alert.RecipientId.Value].Add(alert);
PruneOldAlerts();
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
{
_staticSources.Add(alertSource);
}
private void PruneOldAlerts()
{
foreach (var value in _states.Values)
{
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
}
}
}

View File

@ -57,6 +57,7 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
@ -82,13 +83,14 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
#endregion #endregion
_metaRegistration.Register(); _metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -697,5 +700,6 @@ namespace IW4MAdmin.Application
} }
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
} }
} }

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Misc; using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024; private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout, public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout) IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{ {
Name = "offlinemessage"; Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om"; Alias = "om";
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId) if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.IsIngame) if (gameEvent.Target.IsIngame)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return; return;
} }
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString()); var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage() var newMessage = new EFInboxMessage
{ {
SourceClientId = gameEvent.Origin.ClientId, SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId, DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data, Message = gameEvent.Data,
}; };
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try try
{ {
context.Set<EFInboxMessage>().Add(newMessage); context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
} }
} }
} }
} }

View File

@ -24,8 +24,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models; using Data.Models;
using Data.Models.Server; using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -306,8 +308,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
Throttled = true; Throttled = true;
} }
@ -318,7 +328,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
if (!string.IsNullOrEmpty(CustomSayName)) if (!string.IsNullOrEmpty(CustomSayName))

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using Integrations.Source.Extensions; using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataCollector, ServerDataCollector>() .AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>() .AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -0,0 +1,33 @@
using System;
using Data.Models.Client;
namespace SharedLibraryCore.Alerts;
public class Alert
{
public enum AlertCategory
{
Information,
Warning,
Error,
Message,
}
public class AlertState
{
public Guid AlertId { get; } = Guid.NewGuid();
public AlertCategory Category { get; set; }
public DateTime OccuredAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
public string Message { get; set; }
public string Source { get; set; }
public int? RecipientId { get; set; }
public int? SourceId { get; set; }
public int? ReferenceId { get; set; }
public bool? Delivered { get; set; }
public bool? Consumed { get; set; }
public EFClient.Permission? MinimumPermission { get; set; }
public string Type { get; set; }
public static AlertState Build() => new();
}
}

View File

@ -20,6 +20,8 @@ namespace SharedLibraryCore
{ {
public class BaseController : Controller public class BaseController : Controller
{ {
protected readonly IAlertManager AlertManager;
/// <summary> /// <summary>
/// life span in months /// life span in months
/// </summary> /// </summary>
@ -34,6 +36,7 @@ namespace SharedLibraryCore
public BaseController(IManager manager) public BaseController(IManager manager)
{ {
AlertManager = manager.AlertManager;
Manager = manager; Manager = manager;
Localization ??= Utilities.CurrentLocalization.LocalizationIndex; Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
AppConfig = Manager.GetApplicationSettings().Configuration(); AppConfig = Manager.GetApplicationSettings().Configuration();
@ -169,6 +172,7 @@ namespace SharedLibraryCore
ViewBag.ReportCount = Manager.GetServers().Sum(server => ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24))); server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet; ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
base.OnActionExecuting(context); base.OnActionExecuting(context);
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Data.Models.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
{ Permission.Console.ToString(), new List<string> { "*" } } { Permission.Console.ToString(), new List<string> { "*" } }
}; };
public Dictionary<string, Permission> MinimumAlertPermissions { get; set; } = new()
{
{ nameof(EFInboxMessage), Permission.Trusted },
{ GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator },
{ GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator }
};
[ConfigurationIgnore] [ConfigurationIgnore]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new() public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces;
public interface IAlertManager
{
/// <summary>
/// Initializes the manager
/// </summary>
/// <returns></returns>
Task Initialize();
/// <summary>
/// Get all the alerts for given client
/// </summary>
/// <param name="client">client to retrieve alerts for</param>
/// <returns></returns>
IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client);
/// <summary>
/// Trigger a new alert
/// </summary>
/// <param name="alert">Alert to trigger</param>
void AddAlert(Alert.AlertState alert);
/// <summary>
/// Marks an alert as read and removes it from the manager
/// </summary>
/// <param name="alertId">Id of the alert to mark as read</param>
void MarkAlertAsRead(Guid alertId);
/// <summary>
/// Mark all alerts intended for the given recipientId as read
/// </summary>
/// <param name="recipientId">Identifier of the recipient</param>
void MarkAllAlertsAsRead(int recipientId);
/// <summary>
/// Registers a static (persistent) event source eg datastore that
/// gets initialized at startup
/// </summary>
/// <param name="alertSource">Source action</param>
void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource);
/// <summary>
/// Fires when an alert has been consumed (dimissed)
/// </summary>
EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
}

View File

@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing /// event executed when event has finished executing
/// </summary> /// </summary>
event EventHandler<GameEvent> OnGameEventExecuted; event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
} }
} }

View File

@ -24,6 +24,7 @@ namespace WebfrontCore.Controllers
private readonly string _unbanCommandName; private readonly string _unbanCommandName;
private readonly string _sayCommandName; private readonly string _sayCommandName;
private readonly string _kickCommandName; private readonly string _kickCommandName;
private readonly string _offlineMessageCommandName;
private readonly string _flagCommandName; private readonly string _flagCommandName;
private readonly string _unflagCommandName; private readonly string _unflagCommandName;
private readonly string _setLevelCommandName; private readonly string _setLevelCommandName;
@ -64,6 +65,9 @@ namespace WebfrontCore.Controllers
case nameof(SetLevelCommand): case nameof(SetLevelCommand):
_setLevelCommandName = cmd.Name; _setLevelCommandName = cmd.Name;
break; break;
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
} }
} }
} }
@ -213,7 +217,7 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> Login(int clientId, string password) public async Task<IActionResult> Login(int clientId, string password)
{ {
return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password})); return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password }));
} }
public IActionResult EditForm() public IActionResult EditForm()
@ -327,26 +331,27 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> RecentClientsForm(PaginationRequest request) public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
{ {
ViewBag.First = request.Offset == 0; ViewBag.First = request.Offset == 0;
if (request.Count > 20) if (request.Count > 20)
{ {
request.Count = 20; request.Count = 20;
} }
var clients = await Manager.GetClientService().GetRecentClients(request); var clients = await Manager.GetClientService().GetRecentClients(request);
return request.Offset == 0 return request.Offset == 0
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients) ? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients); : View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
} }
public IActionResult RecentReportsForm() public IActionResult RecentReportsForm()
{ {
var serverInfo = Manager.GetServers().Select(server => var serverInfo = Manager.GetServers().Select(server =>
new ServerInfo new ServerInfo
{ {
Name = server.Hostname, Name = server.Hostname,
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList() Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24)
.ToList()
}); });
return View("Partials/_Reports", serverInfo); return View("Partials/_Reports", serverInfo);
@ -473,6 +478,105 @@ namespace WebfrontCore.Controllers
})); }));
} }
public IActionResult DismissAlertForm(Guid id)
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss Alert?",
Inputs = new List<InputInfo>
{
new()
{
Name = "alertId",
Type = "hidden",
Value = id.ToString()
}
},
Action = nameof(DismissAlert),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAlert(Guid alertId)
{
AlertManager.MarkAlertAsRead(alertId);
return Json(new[]
{
new CommandResponseInfo
{
Response = "Alert dismissed"
}
});
}
public IActionResult DismissAllAlertsForm()
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss All Alerts?",
Inputs = new List<InputInfo>
{
new()
{
Name = "targetId",
Type = "hidden",
Value = Client.ClientId.ToString()
}
},
Action = nameof(DismissAllAlerts),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAllAlerts(int targetId)
{
AlertManager.MarkAllAlertsAsRead(targetId);
return Json(new[]
{
new CommandResponseInfo
{
Response = "Alerts dismissed"
}
});
}
public IActionResult OfflineMessageForm()
{
var info = new ActionInfo
{
ActionButtonLabel = "Send",
Name = "Compose Message",
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = "Message Content",
},
},
Action = "OfflineMessage",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> OfflineMessage(int targetId, string message)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules) .Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>())) .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))

View File

@ -4,7 +4,7 @@
Layout = null; Layout = null;
} }
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5> <h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
@if (Model.Inputs.Any()) @if (Model.Inputs.Any(input => input.Type != "hidden"))
{ {
<hr class="mb-10"/> <hr class="mb-10"/>
} }
@ -55,12 +55,12 @@
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden"> <input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
} }
} }
@if (Model.Inputs.Any()) @if (Model.Inputs.Any(input => input.Type != "hidden"))
{ {
<hr class="mb-10"/> <hr class="mb-10"/>
} }
<div class="ml-auto"> <div class="ml-auto">
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button> <button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
<a href="#" class="btn mr-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a> <a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
</div> </div>
</form> </form>

View File

@ -279,6 +279,18 @@
EntityId = Model.ClientId EntityId = Model.ClientId
}); });
} }
if (ViewBag.Authorized)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Message",
IsButton = true,
Reference = "OfflineMessage",
Icon = "oi oi-envelope-closed",
EntityId = Model.ClientId
});
}
menuItems.Items.Add(new SideContextMenuItem menuItems.Items.Add(new SideContextMenuItem
{ {
@ -336,6 +348,7 @@
EntityId = Model.ClientId EntityId = Model.ClientId
}); });
} }
} }
<partial name="_SideContextMenu" for="@menuItems"></partial> <partial name="_SideContextMenu" for="@menuItems"></partial>

View File

@ -0,0 +1,65 @@
@using SharedLibraryCore.Alerts
@using Humanizer
@model IEnumerable<SharedLibraryCore.Alerts.Alert.AlertState>
@{
Layout = null;
}
<div class="dropdown with-arrow" data-toggle="dropdown" id="alert-toggle" aria-haspopup="true" aria-expanded="false">
<div data-toggle="tooltip" data-title="@(Model.Any() ? "View Alerts" : "No Alerts")" data-placement="bottom">
<i class="oi oi-bell mt-5"></i>
</div>
@if (Model.Any())
{
<div class="position-absolute bg-danger rounded-circle ml-10" style="width: 0.5em;height: 0.5em;top: 0;"></div>
<div class="dropdown-menu dropdown-menu-right w-400" aria-labelledby="alert-toggle">
<div class="d-flex">
<h6 class="dropdown-header">@ViewBag.Alerts.Count Alerts</h6>
<i class="oi oi-circle-x font-size-12 ml-auto mr-10 text-danger align-self-center profile-action" data-action="DismissAllAlerts" data-action-id="@ViewBag.User.ClientId"></i>
</div>
<div class="dropdown-divider"></div>
@foreach (var alert in Model)
{
<div class="d-flex p-5 pl-10 pr-10">
<div class="align-self-center">
@if (alert.Category == Alert.AlertCategory.Error)
{
<i class="oi oi-warning text-danger font-size-12 mr-5"></i>
}
@if (alert.Category == Alert.AlertCategory.Warning)
{
<i class="oi oi-warning text-secondary font-size-12 mr-5"></i>
}
@if (alert.Category == Alert.AlertCategory.Information)
{
<i class="oi oi-circle-check font-size-12 mr-5 text-primary"></i>
}
@if (alert.Category == Alert.AlertCategory.Message)
{
<i class="oi oi-envelope-closed font-size-12 mr-5 text-primary"></i>
}
</div>
<div class="font-size-12 p-5">
<span>@alert.Message</span>
<div class="text-muted d-flex">
<span>@alert.OccuredAt.Humanize()</span>
@if (!string.IsNullOrEmpty(alert.Source))
{
<span class="ml-5 mr-5">&#8226;</span>
@if (alert.SourceId is null)
{
<div class="text-white font-weight-light">@alert.Source.StripColors()</div>
}
else
{
<a asp-controller="Client" asp-action="Profile" asp-route-id="@alert.SourceId" class="no-decoration">@alert.Source</a>
}
}
</div>
</div>
<i class="oi oi-circle-x font-size-12 ml-auto align-self-center profile-action" data-action="DismissAlert" data-action-id="@alert.AlertId"></i>
</div>
}
</div>
}
</div>

View File

@ -119,11 +119,13 @@
</div> </div>
<div class="d-flex ml-auto"> <div class="d-flex ml-auto">
<div class="align-self-center">
@await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts)
</div>
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom"> <div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
<i class="oi oi-moon"></i> <i class="oi oi-moon"></i>
</div> </div>
<div class="d-none d-md-block "> <div class="d-none d-md-block ">
<partial name="_SearchResourceForm"/> <partial name="_SearchResourceForm"/>
</div> </div>
<div class="d-flex d-lg-none"> <div class="d-flex d-lg-none">