add alert/notification functionality (for server connection events and messages)
This commit is contained in:
parent
ffb0e5cac1
commit
a44b4e9475
55
Application/Alerts/AlertExtensions.cs
Normal file
55
Application/Alerts/AlertExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
137
Application/Alerts/AlertManager.cs
Normal file
137
Application/Alerts/AlertManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ namespace IW4MAdmin.Application
|
||||
private readonly List<MessageToken> MessageTokens;
|
||||
private readonly ClientService ClientSvc;
|
||||
readonly PenaltyService PenaltySvc;
|
||||
private readonly IAlertManager _alertManager;
|
||||
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||
readonly IPageList PageList;
|
||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||
@ -82,13 +83,14 @@ namespace IW4MAdmin.Application
|
||||
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
|
||||
{
|
||||
MiddlewareActionHandler = actionHandler;
|
||||
_servers = new ConcurrentBag<Server>();
|
||||
MessageTokens = new List<MessageToken>();
|
||||
ClientSvc = clientService;
|
||||
PenaltySvc = penaltyService;
|
||||
_alertManager = alertManager;
|
||||
ConfigHandler = appConfigHandler;
|
||||
StartTime = DateTime.UtcNow;
|
||||
PageList = new PageList();
|
||||
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
|
||||
#endregion
|
||||
|
||||
_metaRegistration.Register();
|
||||
await _alertManager.Initialize();
|
||||
|
||||
#region CUSTOM_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 IAlertManager AlertManager => _alertManager;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Misc;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IAlertManager _alertManager;
|
||||
private const short MaxLength = 1024;
|
||||
|
||||
|
||||
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";
|
||||
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
||||
Alias = "om";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
RequiresTarget = true;
|
||||
|
||||
|
||||
_contextFactory = contextFactory;
|
||||
_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)
|
||||
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
|
||||
{
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
||||
|
||||
var newMessage = new EFInboxMessage()
|
||||
var newMessage = new EFInboxMessage
|
||||
{
|
||||
SourceClientId = gameEvent.Origin.ClientId,
|
||||
DestinationClientId = gameEvent.Target.ClientId,
|
||||
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
|
||||
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
|
||||
{
|
||||
context.Set<EFInboxMessage>().Add(newMessage);
|
||||
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,10 @@ using Serilog.Context;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using Data.Models;
|
||||
using Data.Models.Server;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Commands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using static Data.Models.Client.EFClient;
|
||||
|
||||
namespace IW4MAdmin
|
||||
@ -306,8 +308,16 @@ namespace IW4MAdmin
|
||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -318,7 +328,15 @@ namespace IW4MAdmin
|
||||
|
||||
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))
|
||||
|
@ -27,6 +27,7 @@ using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Helpers;
|
||||
using Integrations.Source.Extensions;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Extensions;
|
||||
using IW4MAdmin.Application.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
|
||||
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
||||
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
||||
.AddSingleton<IAlertManager, AlertManager>()
|
||||
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
||||
.AddSingleton(translationLookup)
|
||||
.AddDatabaseContextOptions(appConfig);
|
||||
|
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
33
SharedLibraryCore/Alerts/Alert.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ namespace SharedLibraryCore
|
||||
{
|
||||
public class BaseController : Controller
|
||||
{
|
||||
protected readonly IAlertManager AlertManager;
|
||||
|
||||
/// <summary>
|
||||
/// life span in months
|
||||
/// </summary>
|
||||
@ -34,6 +36,7 @@ namespace SharedLibraryCore
|
||||
|
||||
public BaseController(IManager manager)
|
||||
{
|
||||
AlertManager = manager.AlertManager;
|
||||
Manager = manager;
|
||||
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
|
||||
AppConfig = Manager.GetApplicationSettings().Configuration();
|
||||
@ -169,6 +172,7 @@ namespace SharedLibraryCore
|
||||
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
|
||||
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
|
||||
ViewBag.PermissionsSet = PermissionsSet;
|
||||
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Data.Models.Misc;
|
||||
using Newtonsoft.Json;
|
||||
using SharedLibraryCore.Configuration.Attributes;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
|
||||
{ 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]
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
|
||||
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
|
||||
|
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal 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; }
|
||||
|
||||
}
|
@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// event executed when event has finished executing
|
||||
/// </summary>
|
||||
event EventHandler<GameEvent> OnGameEventExecuted;
|
||||
|
||||
IAlertManager AlertManager { get; }
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ namespace WebfrontCore.Controllers
|
||||
private readonly string _unbanCommandName;
|
||||
private readonly string _sayCommandName;
|
||||
private readonly string _kickCommandName;
|
||||
private readonly string _offlineMessageCommandName;
|
||||
private readonly string _flagCommandName;
|
||||
private readonly string _unflagCommandName;
|
||||
private readonly string _setLevelCommandName;
|
||||
@ -64,6 +65,9 @@ namespace WebfrontCore.Controllers
|
||||
case nameof(SetLevelCommand):
|
||||
_setLevelCommandName = cmd.Name;
|
||||
break;
|
||||
case "OfflineMessageCommand":
|
||||
_offlineMessageCommandName = cmd.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,7 +217,7 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
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()
|
||||
@ -327,26 +331,27 @@ namespace WebfrontCore.Controllers
|
||||
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
|
||||
{
|
||||
ViewBag.First = request.Offset == 0;
|
||||
|
||||
|
||||
if (request.Count > 20)
|
||||
{
|
||||
request.Count = 20;
|
||||
}
|
||||
|
||||
|
||||
var clients = await Manager.GetClientService().GetRecentClients(request);
|
||||
|
||||
return request.Offset == 0
|
||||
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
|
||||
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
|
||||
}
|
||||
|
||||
|
||||
public IActionResult RecentReportsForm()
|
||||
{
|
||||
var serverInfo = Manager.GetServers().Select(server =>
|
||||
new ServerInfo
|
||||
{
|
||||
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);
|
||||
@ -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
|
||||
.Concat(_appConfig.GlobalRules)
|
||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||
|
@ -4,7 +4,7 @@
|
||||
Layout = null;
|
||||
}
|
||||
<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"/>
|
||||
}
|
||||
@ -55,12 +55,12 @@
|
||||
<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"/>
|
||||
}
|
||||
<div class="ml-auto">
|
||||
<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>
|
||||
</form>
|
||||
|
@ -279,6 +279,18 @@
|
||||
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
|
||||
{
|
||||
@ -336,6 +348,7 @@
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||
|
||||
|
65
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal file
65
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal 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">•</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>
|
@ -119,11 +119,13 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i class="oi oi-moon"></i>
|
||||
</div>
|
||||
<div class="d-none d-md-block ">
|
||||
|
||||
<partial name="_SearchResourceForm"/>
|
||||
</div>
|
||||
<div class="d-flex d-lg-none">
|
||||
|
Loading…
Reference in New Issue
Block a user