using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using FehuNews.Admin; using FehuNews.Announcements; using FehuNews.Configuration; using FehuNews.Core; using FehuNews.Monitoring; using FehuNews.Remote; using FehuNews.Sinks; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace FehuNews { [BepInPlugin("forteca.fehunews", "FehuNews", "1.0.0")] public sealed class FehuNewsPlugin : BaseUnityPlugin { private Harmony _harmony; private FehuNewsConfig _config; private AnnouncementScheduler _announcements; private PlayerMonitor _players; private DayMonitor _days; private DiscordWebhookSink _discord; private RemoteAnnouncementServer _remoteAnnouncements; private FehuNewsCommands _commands; internal static FehuNewsPlugin Instance { get; private set; } internal IFehuEventPublisher Events { get; private set; } private void Awake() { //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Expected O, but got Unknown Instance = this; _config = new FehuNewsConfig(((BaseUnityPlugin)this).Config); FehuEventManager fehuEventManager = new FehuEventManager(_config, ((BaseUnityPlugin)this).Logger); fehuEventManager.AddSink(new LoggingSink(((BaseUnityPlugin)this).Logger)); fehuEventManager.AddSink(new InGameNotificationSink(_config)); _discord = new DiscordWebhookSink(_config, ((BaseUnityPlugin)this).Logger); fehuEventManager.AddSink(_discord); Events = fehuEventManager; _announcements = new AnnouncementScheduler(_config, Events, ((BaseUnityPlugin)this).Logger); _players = new PlayerMonitor(_config, Events, ((BaseUnityPlugin)this).Logger); _days = new DayMonitor(_config, Events, ((BaseUnityPlugin)this).Logger); _remoteAnnouncements = new RemoteAnnouncementServer(_config, Events, ((BaseUnityPlugin)this).Logger); _commands = new FehuNewsCommands(_config, Events, _announcements, _remoteAnnouncements, ((BaseUnityPlugin)this).Logger); _commands.Register(); _harmony = new Harmony("forteca.fehunews"); _harmony.PatchAll(); ValidateStartupConfiguration(); _remoteAnnouncements.Start(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"FehuNews 1.0.0 loaded as a server-side event framework."); } private void Update() { (Events as FehuEventManager)?.DrainQueuedEvents(); _announcements?.Tick(); _days?.Tick(); } private void OnDestroy() { ((BaseUnityPlugin)this).Logger.LogInfo((object)"FehuNews is unloading."); _remoteAnnouncements?.Dispose(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _discord?.Dispose(); (Events as FehuEventManager)?.Shutdown(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } internal void RefreshPlayers() { _players?.RefreshFromZNet(); } private void ValidateStartupConfiguration() { if (_config.EnableWebhook.Value && string.IsNullOrWhiteSpace(_config.WebhookUrl.Value)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Discord webhook is enabled but WebhookUrl is empty."); } if (_config.EnableRemoteAnnouncements.Value && string.IsNullOrWhiteSpace(_config.RemoteAuthToken.Value)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Remote announcements are enabled but RemoteAuthToken is empty; listener will not start."); } if (_config.Messages.Count == 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"No announcement messages are configured."); } } } internal static class PluginInfo { public const string Guid = "forteca.fehunews"; public const string Name = "FehuNews"; public const string Version = "1.0.0"; } } namespace FehuNews.Sinks { internal sealed class DiscordWebhookSink : IFehuEventSink, IDisposable { private readonly FehuNewsConfig _config; private readonly ManualLogSource _logger; private readonly HttpClient _httpClient; private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); public DiscordWebhookSink(FehuNewsConfig config, ManualLogSource logger) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown _config = config; _logger = logger; _httpClient = new HttpClient(); } public async Task PublishAsync(FehuEvent fehuEvent) { if (!_config.EnableWebhook.Value || string.IsNullOrWhiteSpace(_config.WebhookUrl.Value) || !IsEnabled(fehuEvent.Type)) { return; } await _sendLock.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); try { string payload = (_config.EmbedMode.Value ? BuildEmbedPayload(fehuEvent) : BuildPlainPayload(fehuEvent)); int attempts = Math.Max(1, _config.WebhookRetryCount.Value + 1); for (int attempt = 1; attempt <= attempts; attempt++) { using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(_config.WebhookTimeoutSeconds.Value))) { StringContent content = new StringContent(payload, Encoding.UTF8, "application/json"); try { int num; _ = num - 1; _ = 1; try { _logger.LogDebug((object)$"Sending Discord webhook for {fehuEvent.Type}, attempt {attempt}/{attempts}."); HttpResponseMessage response = await _httpClient.PostAsync(_config.WebhookUrl.Value, (HttpContent)(object)content, cts.Token).ConfigureAwait(continueOnCapturedContext: false); if (response.IsSuccessStatusCode) { _logger.LogDebug((object)$"Discord webhook delivered for {fehuEvent.Type}."); break; } string text = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); _logger.LogWarning((object)$"Discord webhook failed for {fehuEvent.Type}: {(int)response.StatusCode} {response.ReasonPhrase} {text}"); } catch (Exception ex) { _logger.LogWarning((object)$"Discord webhook request failed for {fehuEvent.Type} attempt {attempt}/{attempts}: {ex.Message}"); } } finally { ((IDisposable)content)?.Dispose(); } } if (attempt < attempts) { await Task.Delay(TimeSpan.FromMilliseconds(500 * attempt)).ConfigureAwait(continueOnCapturedContext: false); } } } finally { _sendLock.Release(); } } public void Dispose() { _sendLock.Dispose(); ((HttpMessageInvoker)_httpClient).Dispose(); } private bool IsEnabled(FehuEventType type) { switch (type) { case FehuEventType.PlayerJoined: case FehuEventType.FirstTimePlayerJoined: return _config.SendJoinEvents.Value; case FehuEventType.PlayerLeft: return _config.SendLeaveEvents.Value; case FehuEventType.PlayerDied: return _config.SendDeathEvents.Value; case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return _config.SendBossEvents.Value; case FehuEventType.NewDayStarted: case FehuEventType.DayMilestone: return _config.SendDayEvents.Value; case FehuEventType.ScheduledAnnouncement: case FehuEventType.ManualAdminAnnouncement: return _config.SendAnnouncementEvents.Value; case FehuEventType.RemoteAnnouncement: return _config.SendRemoteAnnouncementEvents.Value; case FehuEventType.ServerStarted: case FehuEventType.ServerStopping: case FehuEventType.WorldLoaded: case FehuEventType.WorldSaved: return _config.SendServerEvents.Value; case FehuEventType.TerritoryCaptured: case FehuEventType.TerritoryLost: case FehuEventType.ClanCreated: case FehuEventType.ClanDisbanded: case FehuEventType.ClanWarDeclared: case FehuEventType.AchievementUnlocked: case FehuEventType.EconomyTransaction: case FehuEventType.MarketPurchase: return true; default: return false; } } private static string BuildPlainPayload(FehuEvent fehuEvent) { return "{\"content\":\"" + JsonEscape("[Voice of Odin] " + fehuEvent.Title + "\n" + fehuEvent.Message) + "\"}"; } private static string BuildEmbedPayload(FehuEvent fehuEvent) { return "{\"username\":\"FehuNews\",\"embeds\":[{\"title\":\"" + JsonEscape("Voice of Odin") + "\",\"description\":\"" + JsonEscape(fehuEvent.Message) + "\",\"color\":12087123,\"footer\":{\"text\":\"" + JsonEscape(fehuEvent.Title + " | " + fehuEvent.Context.Source) + "\"},\"timestamp\":\"" + fehuEvent.CreatedUtc.ToString("o") + "\"}]}"; } private static string JsonEscape(string value) { if (value == null) { return string.Empty; } return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } } internal sealed class InGameNotificationSink : IFehuEventSink { private readonly FehuNewsConfig _config; public InGameNotificationSink(FehuNewsConfig config) { _config = config; } public Task PublishAsync(FehuEvent fehuEvent) { if (!IsEnabled(fehuEvent) || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZRoutedRpc.instance == null) { return Task.CompletedTask; } ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ShowMessage", new object[2] { 2, fehuEvent.Message }); return Task.CompletedTask; } private bool IsEnabled(FehuEvent fehuEvent) { switch (fehuEvent.Type) { case FehuEventType.ScheduledAnnouncement: return _config.EnableAnnouncements.Value; case FehuEventType.ManualAdminAnnouncement: case FehuEventType.RemoteAnnouncement: return true; case FehuEventType.PlayerJoined: case FehuEventType.FirstTimePlayerJoined: return _config.EnableJoinMessages.Value; case FehuEventType.PlayerLeft: return _config.EnableLeaveMessages.Value; case FehuEventType.PlayerDied: return _config.EnableDeathMessages.Value; case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return _config.EnableBossMessages.Value; case FehuEventType.NewDayStarted: case FehuEventType.DayMilestone: return _config.EnableDayMessages.Value; case FehuEventType.ServerStarted: case FehuEventType.ServerStopping: case FehuEventType.WorldLoaded: case FehuEventType.WorldSaved: return _config.EnableServerMessages.Value; default: return true; } } } internal sealed class LoggingSink : IFehuEventSink { private readonly ManualLogSource _logger; public LoggingSink(ManualLogSource logger) { _logger = logger; } public Task PublishAsync(FehuEvent fehuEvent) { _logger.LogInfo((object)$"Event logged: {fehuEvent.Type} | {fehuEvent.Message}"); return Task.CompletedTask; } } } namespace FehuNews.Remote { internal sealed class RemoteAnnouncementServer : IDisposable { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private readonly object _lock = new object(); private HttpListener _listener; private CancellationTokenSource _cts; private Task _listenTask; private DateTime _lastAcceptedUtc = DateTime.MinValue; public bool IsRunning { get { if (_listener != null) { return _listener.IsListening; } return false; } } public RemoteAnnouncementServer(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void Start() { if (!_config.EnableRemoteAnnouncements.Value) { _logger.LogInfo((object)"Remote announcements disabled."); return; } if (string.IsNullOrWhiteSpace(_config.RemoteAuthToken.Value)) { _logger.LogWarning((object)"Remote announcements enabled but RemoteAuthToken is empty; listener will not start."); return; } lock (_lock) { if (IsRunning) { return; } string text = $"http://{_config.RemoteBindAddress.Value}:{_config.RemotePort.Value}/"; _listener = new HttpListener(); _listener.Prefixes.Add(text); _cts = new CancellationTokenSource(); try { _listener.Start(); _listenTask = Task.Run(() => ListenLoopAsync(_cts.Token)); _logger.LogInfo((object)("Remote announcement listener started at " + text + "announcement.")); } catch (Exception ex) { _logger.LogWarning((object)("Failed to start remote announcement listener at " + text + ": " + ex.Message)); Stop(); } } } public void Restart() { Stop(); Start(); } public void Stop() { lock (_lock) { try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); } catch (Exception ex) { _logger.LogWarning((object)("Remote announcement listener stop failed: " + ex.Message)); } finally { _listener = null; _cts?.Dispose(); _cts = null; _listenTask = null; } } } public void Dispose() { Stop(); } private async Task ListenLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { HttpListenerContext context = null; try { context = await _listener.GetContextAsync().ConfigureAwait(continueOnCapturedContext: false); Task.Run(() => HandleRequestAsync(context, token), token); } catch (ObjectDisposedException) { break; } catch (HttpListenerException) { break; } catch (Exception ex3) { _logger.LogWarning((object)("Remote announcement listener error: " + ex3.Message)); context?.Response.Close(); } } } private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken token) { try { if (context.Request.HttpMethod != "POST" || context.Request.Url == null || context.Request.Url.AbsolutePath.TrimEnd(new char[1] { '/' }) != "/announcement") { await WriteResponseAsync(context.Response, 404, "Not Found").ConfigureAwait(continueOnCapturedContext: false); return; } if (!IsAuthorized(context.Request)) { _logger.LogWarning((object)$"Rejected unauthorized remote announcement from {context.Request.RemoteEndPoint}."); await WriteResponseAsync(context.Response, 401, "Unauthorized").ConfigureAwait(continueOnCapturedContext: false); return; } if (!TryRateLimit()) { _logger.LogWarning((object)$"Rate limited remote announcement from {context.Request.RemoteEndPoint}."); await WriteResponseAsync(context.Response, 429, "Rate limited").ConfigureAwait(continueOnCapturedContext: false); return; } string body; using (StreamReader reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8)) { body = await reader.ReadToEndAsync().ConfigureAwait(continueOnCapturedContext: false); } string text = ExtractMessage(body); if (string.IsNullOrWhiteSpace(text)) { await WriteResponseAsync(context.Response, 400, "Missing message").ConfigureAwait(continueOnCapturedContext: false); return; } if (text.Length > _config.RemoteMaxMessageLength.Value) { await WriteResponseAsync(context.Response, 400, "Message too long").ConfigureAwait(continueOnCapturedContext: false); return; } _logger.LogInfo((object)$"Accepted remote announcement from {context.Request.RemoteEndPoint}: {text}"); bool flag = _events.Publish(new FehuEvent(FehuEventType.RemoteAnnouncement, "Remote Announcement", text, "remote:" + text, null, new FehuEventContext("RemoteAnnouncement"))); await WriteResponseAsync(context.Response, flag ? 202 : 409, flag ? "Queued" : "Suppressed").ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex) { _logger.LogWarning((object)("Remote announcement request failed: " + ex.Message)); if (context.Response.OutputStream.CanWrite) { await WriteResponseAsync(context.Response, 500, "Internal Server Error").ConfigureAwait(continueOnCapturedContext: false); } } } private bool IsAuthorized(HttpListenerRequest request) { string value = _config.RemoteAuthToken.Value; string text = request.Headers["Authorization"] ?? string.Empty; string text2 = request.Headers["X-FehuNews-Token"] ?? string.Empty; if (text.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return ConstantTimeEquals(text.Substring("Bearer ".Length).Trim(), value); } return ConstantTimeEquals(text2.Trim(), value); } private bool TryRateLimit() { lock (_lock) { DateTime utcNow = DateTime.UtcNow; if (utcNow - _lastAcceptedUtc < TimeSpan.FromSeconds(_config.RemoteRateLimitSeconds.Value)) { return false; } _lastAcceptedUtc = utcNow; return true; } } private static string ExtractMessage(string body) { if (string.IsNullOrWhiteSpace(body)) { return string.Empty; } Match match = Regex.Match(body, "\"message\"\\s*:\\s*\"(?(?:\\\\.|[^\"])*)\"", RegexOptions.IgnoreCase); if (match.Success) { return Regex.Unescape(match.Groups["message"].Value).Trim(); } return body.Trim(); } private static async Task WriteResponseAsync(HttpListenerResponse response, int statusCode, string message) { byte[] bytes = Encoding.UTF8.GetBytes(message); response.StatusCode = statusCode; response.ContentType = "text/plain; charset=utf-8"; response.ContentLength64 = bytes.Length; await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(continueOnCapturedContext: false); response.Close(); } private static bool ConstantTimeEquals(string left, string right) { if (left == null || right == null) { return false; } int num = left.Length ^ right.Length; int num2 = Math.Min(left.Length, right.Length); for (int i = 0; i < num2; i++) { num |= left[i] ^ right[i]; } return num == 0; } } } namespace FehuNews.Patches { [HarmonyPatch(typeof(Character), "Start")] internal static class CharacterStartPatch { private static void Postfix(Character __instance) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) if (IsServerCharacter(__instance) && __instance.IsBoss()) { string hoverName = __instance.GetHoverName(); FehuNewsPlugin instance = FehuNewsPlugin.Instance; if (instance != null) { IFehuEventPublisher events = instance.Events; string message = hoverName + " has been summoned."; ZDOID zDOID = __instance.GetZDOID(); events.Publish(new FehuEvent(FehuEventType.BossSummoned, "Boss Summoned", message, "boss-summoned:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } } private static bool IsServerCharacter(Character character) { if ((Object)(object)character != (Object)null && (Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class CharacterDeathPatch { private static void Prefix(Character __instance) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZDOID zDOID; if (__instance.IsPlayer()) { string hoverName = __instance.GetHoverName(); FehuNewsPlugin instance = FehuNewsPlugin.Instance; if (instance != null) { IFehuEventPublisher events = instance.Events; string message = hoverName + " has fallen."; zDOID = __instance.GetZDOID(); events.Publish(new FehuEvent(FehuEventType.PlayerDied, "Player Died", message, "player-death:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } else if (__instance.IsBoss()) { string hoverName2 = __instance.GetHoverName(); FehuNewsPlugin instance2 = FehuNewsPlugin.Instance; if (instance2 != null) { IFehuEventPublisher events2 = instance2.Events; string message2 = hoverName2 + " has been defeated."; zDOID = __instance.GetZDOID(); events2.Publish(new FehuEvent(FehuEventType.BossDefeated, "Boss Defeated", message2, "boss-defeated:" + ((object)(ZDOID)(ref zDOID)).ToString())); } } } } [HarmonyPatch(typeof(ZNet), "Start")] internal static class ZNetStartPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.ServerStarted, "Server Started", "The realm is awake.", "server-started")); } } } [HarmonyPatch(typeof(ZNet), "WorldSetup")] internal static class ZNetWorldSetupPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.WorldLoaded, "World Loaded", "World " + __instance.GetWorldName() + " has loaded.", "world-loaded:" + __instance.GetWorldName())); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class ZNetOnDestroyPatch { private static void Prefix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.ServerStopping, "Server Stopping", "The realm is going quiet.", "server-stopping")); } } } [HarmonyPatch(typeof(ZNet), "UpdatePlayerList")] internal static class ZNetUpdatePlayerListPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.RefreshPlayers(); } } } [HarmonyPatch(typeof(ZNet), "SaveWorld")] internal static class ZNetSaveWorldPatch { private static void Postfix(ZNet __instance) { if ((Object)(object)__instance != (Object)null && __instance.IsServer()) { FehuNewsPlugin.Instance?.Events.Publish(new FehuEvent(FehuEventType.WorldSaved, "World Saved", "The world has been saved.", "world-saved")); } } } } namespace FehuNews.Monitoring { internal sealed class DayMonitor { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private int _lastDay = -1; public DayMonitor(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void Tick() { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)EnvMan.instance == (Object)null) { return; } int day = EnvMan.instance.GetDay(); if (day > 0 && day != _lastDay) { _lastDay = day; _logger.LogInfo((object)$"Detected new world day: {day}"); _events.Publish(new FehuEvent(FehuEventType.NewDayStarted, "New Day Started", $"Day {day} has dawned in the realm.", "day:" + day)); if (_config.Milestones.Contains(day)) { _events.Publish(new FehuEvent(FehuEventType.DayMilestone, "Day Milestone", $"The realm has endured {day} days.", "day-milestone:" + day)); } } } } internal sealed class PlayerMonitor { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private readonly Dictionary _knownOnlinePlayers = new Dictionary(StringComparer.Ordinal); public PlayerMonitor(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; } public void RefreshFromZNet() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { object obj = player; string fieldValue = GetFieldValue(obj, "m_characterID"); if (!string.IsNullOrWhiteSpace(fieldValue) && !fieldValue.EndsWith(":0", StringComparison.Ordinal) && !(fieldValue == "0") && !dictionary.ContainsKey(fieldValue)) { dictionary.Add(fieldValue, GetPlayerName(obj)); } } foreach (KeyValuePair item in dictionary) { if (!_knownOnlinePlayers.ContainsKey(item.Key)) { bool flag = !_config.KnownPlayerIds.Contains(item.Key); _knownOnlinePlayers[item.Key] = item.Value; _config.RememberPlayer(item.Key); FehuEventType type = (flag ? FehuEventType.FirstTimePlayerJoined : FehuEventType.PlayerJoined); string message = (flag ? ("The ravens announce that " + item.Value + " has entered the realm for the first time.") : ("The ravens announce that " + item.Value + " has entered the realm.")); _logger.LogInfo((object)$"Detected player join: {item.Value} ({item.Key}), firstTime={flag}"); _events.Publish(new FehuEvent(type, flag ? "First Time Player Joined" : "Player Joined", message, "join:" + item.Key)); } } KeyValuePair[] array = _knownOnlinePlayers.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; if (!dictionary.ContainsKey(keyValuePair.Key)) { _knownOnlinePlayers.Remove(keyValuePair.Key); _logger.LogInfo((object)("Detected player leave: " + keyValuePair.Value + " (" + keyValuePair.Key + ")")); _events.Publish(new FehuEvent(FehuEventType.PlayerLeft, "Player Left", keyValuePair.Value + " has left the realm.", "leave:" + keyValuePair.Key)); } } } private static string GetPlayerName(object player) { string fieldValue = GetFieldValue(player, "m_name"); if (!string.IsNullOrWhiteSpace(fieldValue)) { return fieldValue; } string fieldValue2 = GetFieldValue(player, "m_serverAssignedDisplayName"); if (!string.IsNullOrWhiteSpace(fieldValue2)) { return fieldValue2; } return "Unknown Viking"; } private static string GetFieldValue(object instance, string fieldName) { if (instance == null) { return string.Empty; } return (instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public)?.GetValue(instance))?.ToString() ?? string.Empty; } } } namespace FehuNews.Core { public enum FehuEventType { ServerStarted, ServerStopping, WorldLoaded, WorldSaved, PlayerJoined, PlayerLeft, FirstTimePlayerJoined, PlayerDied, ScheduledAnnouncement, ManualAdminAnnouncement, NewDayStarted, DayMilestone, BossSummoned, BossDefeated, RemoteAnnouncement, TerritoryCaptured, TerritoryLost, ClanCreated, ClanDisbanded, ClanWarDeclared, AchievementUnlocked, EconomyTransaction, MarketPurchase } public sealed class FehuEventContext { public string Source { get; } public string ActorName { get; } public string ActorId { get; } public string TargetName { get; } public string TargetId { get; } public IReadOnlyDictionary Metadata { get; } public FehuEventContext(string source = null, string actorName = null, string actorId = null, string targetName = null, string targetId = null, IReadOnlyDictionary metadata = null) { Source = (string.IsNullOrWhiteSpace(source) ? "FehuNews" : source); ActorName = actorName ?? string.Empty; ActorId = actorId ?? string.Empty; TargetName = targetName ?? string.Empty; TargetId = targetId ?? string.Empty; Metadata = metadata ?? new Dictionary(); } public static FehuEventContext Server(string source = null) { return new FehuEventContext(source ?? "Server"); } } public sealed class FehuEvent { public FehuEventType Type { get; } public string Title { get; } public string Message { get; } public string DedupeKey { get; } public IReadOnlyDictionary Data { get; } public FehuEventContext Context { get; } public DateTime CreatedUtc { get; } public FehuEvent(FehuEventType type, string title, string message, string dedupeKey = null, IReadOnlyDictionary data = null, FehuEventContext context = null) { Type = type; Title = (string.IsNullOrWhiteSpace(title) ? type.ToString() : title.Trim()); Message = (string.IsNullOrWhiteSpace(message) ? string.Empty : message.Trim()); DedupeKey = dedupeKey ?? (type.ToString() + ":" + message); Data = data ?? new Dictionary(); Context = context ?? FehuEventContext.Server(); CreatedUtc = DateTime.UtcNow; } } public sealed class FehuEventManager : IFehuEventPublisher { private static readonly object CurrentLock = new object(); private static FehuEventManager _current; private readonly object _lock = new object(); private readonly List _sinks = new List(); private readonly List _listeners = new List(); private readonly Dictionary _recentEvents = new Dictionary(StringComparer.Ordinal); private readonly Queue _pendingEvents = new Queue(); private readonly FehuNewsConfig _config; private readonly ManualLogSource _logger; private readonly int _mainThreadId; internal FehuEventManager(FehuNewsConfig config, ManualLogSource logger) { _config = config; _logger = logger; _mainThreadId = Thread.CurrentThread.ManagedThreadId; lock (CurrentLock) { _current = this; } } public static bool Publish(FehuEvent fehuEvent) { FehuEventManager current; lock (CurrentLock) { current = _current; } return current?.PublishInternal(fehuEvent) ?? false; } public static bool RegisterListener(IFehuEventListener listener) { FehuEventManager current; lock (CurrentLock) { current = _current; } if (current == null || listener == null) { return false; } current.AddListener(listener); return true; } public static bool UnregisterListener(IFehuEventListener listener) { FehuEventManager current; lock (CurrentLock) { current = _current; } if (current == null || listener == null) { return false; } current.RemoveListener(listener); return true; } bool IFehuEventPublisher.Publish(FehuEvent fehuEvent) { return PublishInternal(fehuEvent); } internal void AddSink(IFehuEventSink sink) { if (sink == null) { return; } lock (_lock) { _sinks.Add(sink); } } internal void AddListener(IFehuEventListener listener) { lock (_lock) { if (!_listeners.Contains(listener)) { _listeners.Add(listener); } } } internal void RemoveListener(IFehuEventListener listener) { lock (_lock) { _listeners.Remove(listener); } } internal void DrainQueuedEvents(int maxEvents = 25) { for (int i = 0; i < maxEvents; i++) { FehuEvent fehuEvent; lock (_lock) { if (_pendingEvents.Count == 0) { break; } fehuEvent = _pendingEvents.Dequeue(); } PublishOnMainThread(fehuEvent); } } internal void Shutdown() { lock (CurrentLock) { if (_current == this) { _current = null; } } lock (_lock) { _sinks.Clear(); _listeners.Clear(); _pendingEvents.Clear(); _recentEvents.Clear(); } } private bool PublishInternal(FehuEvent fehuEvent) { if (fehuEvent == null || string.IsNullOrWhiteSpace(fehuEvent.Message)) { _logger.LogWarning((object)"Rejected empty FehuNews event."); return false; } if (Thread.CurrentThread.ManagedThreadId != _mainThreadId) { lock (_lock) { if (_pendingEvents.Count >= _config.EventQueueLimit.Value) { _logger.LogWarning((object)$"Rejected event {fehuEvent.Type}: event queue limit reached."); return false; } _pendingEvents.Enqueue(fehuEvent); return true; } } return PublishOnMainThread(fehuEvent); } private bool PublishOnMainThread(FehuEvent fehuEvent) { TimeSpan cooldown = GetCooldown(fehuEvent.Type); if (IsDuplicate(fehuEvent.DedupeKey, cooldown)) { _logger.LogDebug((object)$"Suppressed duplicate event {fehuEvent.Type}: {fehuEvent.DedupeKey}"); return false; } IFehuEventSink[] array; IFehuEventListener[] array2; lock (_lock) { array = _sinks.ToArray(); array2 = _listeners.ToArray(); } _logger.LogInfo((object)$"Dispatching event {fehuEvent.Type}: {fehuEvent.Message}"); IFehuEventListener[] array3 = array2; foreach (IFehuEventListener fehuEventListener in array3) { try { fehuEventListener.OnFehuEvent(fehuEvent); } catch (Exception ex) { _logger.LogWarning((object)$"Event listener {fehuEventListener.GetType().Name} failed for {fehuEvent.Type}: {ex.Message}"); } } IFehuEventSink[] array4 = array; foreach (IFehuEventSink sink in array4) { PublishToSinkAsync(sink, fehuEvent); } return true; } private async Task PublishToSinkAsync(IFehuEventSink sink, FehuEvent fehuEvent) { try { await sink.PublishAsync(fehuEvent).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception arg) { _logger.LogWarning((object)$"Event sink {sink.GetType().Name} failed for {fehuEvent.Type}: {arg}"); } } private bool IsDuplicate(string key, TimeSpan cooldown) { DateTime utcNow = DateTime.UtcNow; PruneRecentEvents(utcNow); lock (_lock) { if (_recentEvents.TryGetValue(key, out var value) && utcNow - value < cooldown) { return true; } _recentEvents[key] = utcNow; return false; } } private void PruneRecentEvents(DateTime now) { lock (_lock) { if (_recentEvents.Count >= 512) { string[] array = (from pair in _recentEvents where now - pair.Value > TimeSpan.FromMinutes(30.0) select pair.Key).ToArray(); foreach (string key in array) { _recentEvents.Remove(key); } } } } private TimeSpan GetCooldown(FehuEventType type) { switch (type) { case FehuEventType.PlayerJoined: case FehuEventType.PlayerLeft: case FehuEventType.FirstTimePlayerJoined: return TimeSpan.FromSeconds(_config.PlayerEventCooldownSeconds.Value); case FehuEventType.PlayerDied: return TimeSpan.FromSeconds(_config.DeathEventCooldownSeconds.Value); case FehuEventType.WorldSaved: return TimeSpan.FromSeconds(_config.SaveEventCooldownSeconds.Value); case FehuEventType.BossSummoned: case FehuEventType.BossDefeated: return TimeSpan.FromSeconds(_config.BossEventCooldownSeconds.Value); case FehuEventType.ScheduledAnnouncement: case FehuEventType.ManualAdminAnnouncement: case FehuEventType.RemoteAnnouncement: return TimeSpan.FromSeconds(_config.AnnouncementCooldownSeconds.Value); default: return TimeSpan.FromSeconds(_config.DefaultEventCooldownSeconds.Value); } } } public interface IFehuEventListener { void OnFehuEvent(FehuEvent fehuEvent); } public interface IFehuEventPublisher { bool Publish(FehuEvent fehuEvent); } public interface IFehuEventSink { Task PublishAsync(FehuEvent fehuEvent); } } namespace FehuNews.Configuration { internal sealed class FehuNewsConfig { private readonly ConfigFile _configFile; public readonly ConfigEntry EnableAnnouncements; public readonly ConfigEntry AnnouncementIntervalMinutes; public readonly ConfigEntry AnnouncementMessages; public readonly ConfigEntry EnableWebhook; public readonly ConfigEntry WebhookUrl; public readonly ConfigEntry WebhookTimeoutSeconds; public readonly ConfigEntry WebhookRetryCount; public readonly ConfigEntry EnableJoinMessages; public readonly ConfigEntry EnableLeaveMessages; public readonly ConfigEntry EnableDeathMessages; public readonly ConfigEntry EnableBossMessages; public readonly ConfigEntry EnableDayMessages; public readonly ConfigEntry EnableServerMessages; public readonly ConfigEntry DayMilestones; public readonly ConfigEntry EmbedMode; public readonly ConfigEntry SendJoinEvents; public readonly ConfigEntry SendLeaveEvents; public readonly ConfigEntry SendDeathEvents; public readonly ConfigEntry SendBossEvents; public readonly ConfigEntry SendDayEvents; public readonly ConfigEntry SendAnnouncementEvents; public readonly ConfigEntry SendServerEvents; public readonly ConfigEntry SendRemoteAnnouncementEvents; public readonly ConfigEntry EnableRemoteAnnouncements; public readonly ConfigEntry RemoteBindAddress; public readonly ConfigEntry RemotePort; public readonly ConfigEntry RemoteAuthToken; public readonly ConfigEntry RemoteRateLimitSeconds; public readonly ConfigEntry RemoteMaxMessageLength; public readonly ConfigEntry DefaultEventCooldownSeconds; public readonly ConfigEntry PlayerEventCooldownSeconds; public readonly ConfigEntry DeathEventCooldownSeconds; public readonly ConfigEntry BossEventCooldownSeconds; public readonly ConfigEntry SaveEventCooldownSeconds; public readonly ConfigEntry AnnouncementCooldownSeconds; public readonly ConfigEntry EventQueueLimit; private readonly ConfigEntry _knownPlayerIds; public IReadOnlyList Messages => ParseMessages(AnnouncementMessages.Value); public ISet Milestones { get { int result; return new HashSet(from value in ParseStrings(DayMilestones.Value) select int.TryParse(value, out result) ? result : 0 into day where day > 0 select day); } } public ISet KnownPlayerIds => new HashSet(ParseStrings(_knownPlayerIds.Value), StringComparer.Ordinal); public FehuNewsConfig(ConfigFile configFile) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Expected O, but got Unknown //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Expected O, but got Unknown //IL_0318: Unknown result type (might be due to invalid IL or missing references) //IL_0322: Expected O, but got Unknown //IL_0369: Unknown result type (might be due to invalid IL or missing references) //IL_0373: Expected O, but got Unknown //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03a8: Expected O, but got Unknown //IL_03cf: Unknown result type (might be due to invalid IL or missing references) //IL_03d9: Expected O, but got Unknown //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_040b: Expected O, but got Unknown //IL_0433: Unknown result type (might be due to invalid IL or missing references) //IL_043d: Expected O, but got Unknown //IL_0465: Unknown result type (might be due to invalid IL or missing references) //IL_046f: Expected O, but got Unknown //IL_0497: Unknown result type (might be due to invalid IL or missing references) //IL_04a1: Expected O, but got Unknown //IL_04c9: Unknown result type (might be due to invalid IL or missing references) //IL_04d3: Expected O, but got Unknown //IL_04ff: Unknown result type (might be due to invalid IL or missing references) //IL_0509: Expected O, but got Unknown _configFile = configFile; EnableAnnouncements = configFile.Bind("Announcements", "EnableAnnouncements", true, "Enable scheduled server announcements."); AnnouncementIntervalMinutes = configFile.Bind("Announcements", "AnnouncementIntervalMinutes", 10, new ConfigDescription("Minutes between scheduled announcements.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 1440), Array.Empty())); AnnouncementMessages = configFile.Bind("Announcements", "AnnouncementMessages", "Welcome to the server!|Respect other players and their builds.|Have fun, Vikings!", "Messages separated by |, ;, real new lines, or literal \\n sequences."); EnableWebhook = configFile.Bind("Discord", "EnableWebhook", false, "Send FehuNews events to Discord."); WebhookUrl = configFile.Bind("Discord", "WebhookUrl", string.Empty, "Discord webhook URL."); EmbedMode = configFile.Bind("Discord", "EmbedMode", true, "Send Viking themed Discord embeds instead of plain text."); WebhookTimeoutSeconds = configFile.Bind("Discord", "WebhookTimeoutSeconds", 8, new ConfigDescription("Timeout for Discord webhook requests.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 60), Array.Empty())); WebhookRetryCount = configFile.Bind("Discord", "WebhookRetryCount", 2, new ConfigDescription("Number of retry attempts after a failed Discord webhook request.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 5), Array.Empty())); SendJoinEvents = configFile.Bind("Discord Events", "SendJoinEvents", true, "Send player join events to Discord."); SendLeaveEvents = configFile.Bind("Discord Events", "SendLeaveEvents", true, "Send player leave events to Discord."); SendDeathEvents = configFile.Bind("Discord Events", "SendDeathEvents", true, "Send player death events to Discord."); SendBossEvents = configFile.Bind("Discord Events", "SendBossEvents", true, "Send boss events to Discord."); SendDayEvents = configFile.Bind("Discord Events", "SendDayEvents", true, "Send day events to Discord."); SendAnnouncementEvents = configFile.Bind("Discord Events", "SendAnnouncementEvents", true, "Send announcement events to Discord."); SendServerEvents = configFile.Bind("Discord Events", "SendServerEvents", true, "Send server lifecycle events to Discord."); SendRemoteAnnouncementEvents = configFile.Bind("Discord Events", "SendRemoteAnnouncementEvents", true, "Send remote announcement events to Discord."); EnableJoinMessages = configFile.Bind("In-Game Messages", "EnableJoinMessages", true, "Show join messages in game."); EnableLeaveMessages = configFile.Bind("In-Game Messages", "EnableLeaveMessages", true, "Show leave messages in game."); EnableDeathMessages = configFile.Bind("In-Game Messages", "EnableDeathMessages", true, "Show death messages in game."); EnableBossMessages = configFile.Bind("In-Game Messages", "EnableBossMessages", true, "Show boss messages in game."); EnableDayMessages = configFile.Bind("In-Game Messages", "EnableDayMessages", true, "Show day and milestone messages in game."); EnableServerMessages = configFile.Bind("In-Game Messages", "EnableServerMessages", true, "Show server lifecycle messages in game."); EnableRemoteAnnouncements = configFile.Bind("Remote Announcements", "EnableRemoteAnnouncements", false, "Enable HTTP POST /announcement for authenticated remote announcements."); RemoteBindAddress = configFile.Bind("Remote Announcements", "RemoteBindAddress", "127.0.0.1", "HTTP bind address. Use 127.0.0.1 behind a reverse proxy/VPN for production."); RemotePort = configFile.Bind("Remote Announcements", "RemotePort", 8095, new ConfigDescription("HTTP listener port for remote announcements.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 65535), Array.Empty())); RemoteAuthToken = configFile.Bind("Remote Announcements", "RemoteAuthToken", string.Empty, "Bearer token required for remote announcements. Leave empty to disable listener startup."); RemoteRateLimitSeconds = configFile.Bind("Remote Announcements", "RemoteRateLimitSeconds", 5, new ConfigDescription("Minimum seconds between accepted remote announcements.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 3600), Array.Empty())); RemoteMaxMessageLength = configFile.Bind("Remote Announcements", "RemoteMaxMessageLength", 240, new ConfigDescription("Maximum accepted remote announcement message length.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 2000), Array.Empty())); DefaultEventCooldownSeconds = configFile.Bind("Anti-Spam", "DefaultEventCooldownSeconds", 5, new ConfigDescription("Default duplicate event suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); PlayerEventCooldownSeconds = configFile.Bind("Anti-Spam", "PlayerEventCooldownSeconds", 15, new ConfigDescription("Duplicate join/leave suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); DeathEventCooldownSeconds = configFile.Bind("Anti-Spam", "DeathEventCooldownSeconds", 20, new ConfigDescription("Duplicate player death suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); BossEventCooldownSeconds = configFile.Bind("Anti-Spam", "BossEventCooldownSeconds", 45, new ConfigDescription("Duplicate boss event suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); SaveEventCooldownSeconds = configFile.Bind("Anti-Spam", "SaveEventCooldownSeconds", 20, new ConfigDescription("Duplicate world save suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); AnnouncementCooldownSeconds = configFile.Bind("Anti-Spam", "AnnouncementCooldownSeconds", 10, new ConfigDescription("Duplicate announcement suppression cooldown.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 3600), Array.Empty())); EventQueueLimit = configFile.Bind("Anti-Spam", "EventQueueLimit", 256, new ConfigDescription("Maximum queued cross-thread events.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 5000), Array.Empty())); DayMilestones = configFile.Bind("World", "DayMilestones", "100|500|1000", "World day milestones separated by |, ;, commas, spaces, or new lines."); _knownPlayerIds = configFile.Bind("State", "KnownPlayerIds", string.Empty, "Internal FehuNews state for first-time join detection. Do not edit unless resetting history."); } public void RememberPlayer(string playerId) { if (string.IsNullOrWhiteSpace(playerId)) { return; } HashSet hashSet = new HashSet(KnownPlayerIds, StringComparer.Ordinal); if (hashSet.Add(playerId)) { _knownPlayerIds.Value = string.Join("|", hashSet.OrderBy((string value) => value, StringComparer.Ordinal)); _configFile.Save(); } } public void Reload() { _configFile.Reload(); } private static IReadOnlyList ParseStrings(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return Array.Empty(); } return (from value in raw.Replace("\\n", "\n").Split(new char[7] { '|', ';', ',', '\r', '\n', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries) select value.Trim() into value where !string.IsNullOrWhiteSpace(value) select value).ToArray(); } private static IReadOnlyList ParseMessages(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return Array.Empty(); } return (from value in raw.Replace("\\n", "\n").Split(new char[4] { '|', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) select value.Trim() into value where !string.IsNullOrWhiteSpace(value) select value).ToArray(); } } } namespace FehuNews.Announcements { internal sealed class AnnouncementScheduler { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly ManualLogSource _logger; private float _nextAnnouncementTime; private int _nextMessageIndex; public AnnouncementScheduler(FehuNewsConfig config, IFehuEventPublisher events, ManualLogSource logger) { _config = config; _events = events; _logger = logger; ResetTimer(); } public void ResetTimer() { _nextAnnouncementTime = Time.time + (float)_config.AnnouncementIntervalMinutes.Value * 60f; _logger.LogDebug((object)"Announcement timer reset."); } public void Tick() { if (!_config.EnableAnnouncements.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || Time.time < _nextAnnouncementTime) { return; } IReadOnlyList messages = _config.Messages; if (messages.Count == 0) { ResetTimer(); return; } if (_nextMessageIndex >= messages.Count) { _nextMessageIndex = 0; } string text = messages[_nextMessageIndex++]; _logger.LogInfo((object)("Scheduled announcement selected: " + text)); _events.Publish(new FehuEvent(FehuEventType.ScheduledAnnouncement, "Scheduled Announcement", text, "announcement:" + text)); ResetTimer(); } } } namespace FehuNews.Admin { internal sealed class FehuNewsCommands { private readonly FehuNewsConfig _config; private readonly IFehuEventPublisher _events; private readonly AnnouncementScheduler _announcements; private readonly RemoteAnnouncementServer _remote; private readonly ManualLogSource _logger; public FehuNewsCommands(FehuNewsConfig config, IFehuEventPublisher events, AnnouncementScheduler announcements, RemoteAnnouncementServer remote, ManualLogSource logger) { _config = config; _events = events; _announcements = announcements; _remote = remote; _logger = logger; } public void Register() { Type type = FindType("ConsoleCommand"); Type type2 = FindType("ConsoleEvent"); if (type == null || type2 == null) { _logger.LogWarning((object)"Could not register fehunews command because Valheim console command types were not found."); return; } MethodInfo method = GetType().GetMethod("HandleCommand", BindingFlags.Instance | BindingFlags.NonPublic); Delegate @delegate = Delegate.CreateDelegate(type2, this, method); ConstructorInfo constructorInfo = FindCommandConstructor(type, type2); if (constructorInfo == null) { _logger.LogWarning((object)"Could not register fehunews command because a compatible ConsoleCommand constructor was not found."); return; } object command = constructorInfo.Invoke(new object[3] { "fehunews", "FehuNews admin commands: help, send , reload, status, testwebhook", @delegate }); SetCommandFlag(command, "OnlyAdmin", value: true); SetCommandFlag(command, "OnlyServer", value: true); _logger.LogInfo((object)"Registered admin command: fehunews"); } private void HandleCommand(object args) { string propertyOrField = GetPropertyOrField(args, "FullLine"); string[] array = propertyOrField.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2) { ReplyHelp(args); return; } switch (array[1].ToLowerInvariant()) { case "help": ReplyHelp(args); break; case "send": { string text = ((propertyOrField.Length > "fehunews send ".Length) ? propertyOrField.Substring("fehunews send ".Length).Trim() : string.Empty); if (string.IsNullOrWhiteSpace(text)) { Reply(args, "Usage: fehunews send "); break; } _logger.LogInfo((object)("Manual admin announcement: " + text)); _events.Publish(new FehuEvent(FehuEventType.ManualAdminAnnouncement, "Manual Admin Announcement", text, "manual:" + text)); Reply(args, "FehuNews announcement sent."); break; } case "reload": _config.Reload(); _announcements.ResetTimer(); _remote.Restart(); _logger.LogInfo((object)"FehuNews configuration reloaded by admin command."); Reply(args, "FehuNews configuration reloaded."); break; case "status": Reply(args, $"FehuNews status: announcements={_config.EnableAnnouncements.Value}, interval={_config.AnnouncementIntervalMinutes.Value}m, messages={_config.Messages.Count}, webhook={_config.EnableWebhook.Value}, remote={_remote.IsRunning}"); break; case "testwebhook": if (!_config.EnableWebhook.Value || string.IsNullOrWhiteSpace(_config.WebhookUrl.Value)) { Reply(args, "Webhook is not enabled or WebhookUrl is empty."); break; } _logger.LogInfo((object)"Discord webhook test requested by admin command."); _events.Publish(new FehuEvent(FehuEventType.ManualAdminAnnouncement, "Webhook Test", "FehuNews webhook test: the ravens can reach Discord.", "testwebhook:" + DateTime.UtcNow.Ticks, null, new FehuEventContext("AdminCommand"))); Reply(args, "FehuNews webhook test queued."); break; default: Reply(args, "Unknown FehuNews command. Use: fehunews help"); break; } } private static void ReplyHelp(object args) { Reply(args, "FehuNews commands: fehunews send | fehunews reload | fehunews status | fehunews testwebhook | fehunews help"); } private static void Reply(object args, string message) { object memberValue = GetMemberValue(args, "Context"); (memberValue?.GetType().GetMethod("AddString", new Type[1] { typeof(string) }))?.Invoke(memberValue, new object[1] { message }); } private static void SetCommandFlag(object command, string name, bool value) { command.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public)?.SetValue(command, value); } private static Type FindType(string typeName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(typeName, throwOnError: false); if (type != null) { return type; } } return null; } private static ConstructorInfo FindCommandConstructor(Type commandType, Type eventType) { ConstructorInfo[] constructors = commandType.GetConstructors(BindingFlags.Instance | BindingFlags.Public); foreach (ConstructorInfo constructorInfo in constructors) { ParameterInfo[] parameters = constructorInfo.GetParameters(); if (parameters.Length == 3 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(string) && parameters[2].ParameterType == eventType) { return constructorInfo; } } return null; } private static string GetPropertyOrField(object instance, string name) { return GetMemberValue(instance, name)?.ToString() ?? string.Empty; } private static object GetMemberValue(object instance, string name) { if (instance == null) { return null; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public); if (property != null) { return property.GetValue(instance, null); } return type.GetField(name, BindingFlags.Instance | BindingFlags.Public)?.GetValue(instance); } } }