From b8a8a535f1b31cc0d0b1c99fcad8aaf639c5d719 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 13 Aug 2021 21:26:48 -0400 Subject: [PATCH] Implement server-side chat and chat command framework --- CLre/CLre.cs | 2 +- CLre_server/API/Config/CLreConfig.cs | 16 ++ .../API/MainServer/ModerationEngine.cs | 68 +++++++++ CLre_server/API/MainServer/Moderator.cs | 139 ++++++++++++++++++ CLre_server/API/MainServer/Server.cs | 21 ++- CLre_server/API/MainServer/ServerEngines.cs | 27 ++++ .../API/Utility/CardLifeUserAuthentication.cs | 61 ++++++++ CLre_server/CLre_server.cs | 10 +- CLre_server/Tweaks/Chat/Attributes.cs | 60 ++++++++ .../Tweaks/Chat/AuthenticationEngine.cs | 21 +++ CLre_server/Tweaks/Chat/ChatConfig.cs | 12 ++ .../Tweaks/Chat/ChatConnectionEngine.cs | 88 +++++++++++ CLre_server/Tweaks/Chat/ChatHandler.cs | 33 +++++ CLre_server/Tweaks/Chat/ChatListener.cs | 116 +++++++++++++++ CLre_server/Tweaks/Chat/ModeratorCommands.cs | 111 ++++++++++++++ CLre_server/WebStatus/Attributes.cs | 3 +- CLre_server/WebStatus/WebServer.cs | 2 +- 17 files changed, 783 insertions(+), 7 deletions(-) create mode 100644 CLre_server/API/MainServer/ModerationEngine.cs create mode 100644 CLre_server/API/MainServer/Moderator.cs create mode 100644 CLre_server/API/Utility/CardLifeUserAuthentication.cs create mode 100644 CLre_server/Tweaks/Chat/Attributes.cs create mode 100644 CLre_server/Tweaks/Chat/AuthenticationEngine.cs create mode 100644 CLre_server/Tweaks/Chat/ChatConfig.cs create mode 100644 CLre_server/Tweaks/Chat/ChatConnectionEngine.cs create mode 100644 CLre_server/Tweaks/Chat/ChatHandler.cs create mode 100644 CLre_server/Tweaks/Chat/ChatListener.cs create mode 100644 CLre_server/Tweaks/Chat/ModeratorCommands.cs diff --git a/CLre/CLre.cs b/CLre/CLre.cs index cf90897..582e742 100644 --- a/CLre/CLre.cs +++ b/CLre/CLre.cs @@ -18,7 +18,7 @@ namespace CLre { public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; - public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + public override string Version { get; } = "21Q3 " + Assembly.GetExecutingAssembly().GetName().Version.ToString(); internal static Harmony harmonyInstance = null; diff --git a/CLre_server/API/Config/CLreConfig.cs b/CLre_server/API/Config/CLreConfig.cs index 2922c71..9560d15 100644 --- a/CLre_server/API/Config/CLreConfig.cs +++ b/CLre_server/API/Config/CLreConfig.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; namespace CLre_server.API.Config { @@ -9,6 +10,11 @@ namespace CLre_server.API.Config public bool clre_clients_only; public bool web_server; public bool terrain_exclusion_zone; + public bool chat_commands; + public string email_address; + public string password; + public string[] bans; + public string[] moderators; public static CLreConfig Default() { @@ -17,6 +23,11 @@ namespace CLre_server.API.Config clre_clients_only = false, web_server = false, terrain_exclusion_zone = false, + chat_commands = false, + email_address = "email@address.com", + password = "s3cur3-password", + bans = new string[0], + moderators = new []{ "NGuiness", "NGnius", "Zhang"}, }; } @@ -60,5 +71,10 @@ namespace CLre_server.API.Config { return UnityEngine.JsonUtility.ToJson(this, true); } + + public void ToFile(string path) + { + File.WriteAllText(path, this.ToString()); + } } } \ No newline at end of file diff --git a/CLre_server/API/MainServer/ModerationEngine.cs b/CLre_server/API/MainServer/ModerationEngine.cs new file mode 100644 index 0000000..6221301 --- /dev/null +++ b/CLre_server/API/MainServer/ModerationEngine.cs @@ -0,0 +1,68 @@ +using System; +using System.Reflection; +using HarmonyLib; +using Svelto.Context; +using Svelto.DataStructures; +using Svelto.ECS; +using User.Server; + +namespace CLre_server.API.MainServer +{ + class ModerationEngine : Engines.ServerEnginePostBuild + { + public override void Ready() + { + } + + public int? FindConnectedPlayerById(string publicId) + { + FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup"); + ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null); + ReadOnlyCollectionStruct accounts = + entitiesDB.QueryEntityViews(accountGroup); + for (int i = 0; i < accounts.Count; i++) + { + if (accounts[i].accountId.publicId.ToString() == publicId) + { + return i; + } + } + + return null; + } + + public int? FindConnectedPlayerByName(string displayName) + { + FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup"); + ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null); + ReadOnlyCollectionStruct accounts = + entitiesDB.QueryEntityViews(accountGroup); + for (int i = 0; i < accounts.Count; i++) + { + if (String.Equals(accounts[i].accountId.displayName, displayName, StringComparison.InvariantCultureIgnoreCase)) + { + return i; + } + } + + return null; + } + + public Guid? FindConnectedPlayerGuidByName(string displayName) + { + FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup"); + ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null); + ReadOnlyCollectionStruct accounts = + entitiesDB.QueryEntityViews(accountGroup); + for (int i = 0; i < accounts.Count; i++) + { + if (String.Equals(accounts[i].accountId.displayName, displayName, StringComparison.InvariantCultureIgnoreCase)) + { + return accounts[i].accountId.publicId; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/CLre_server/API/MainServer/Moderator.cs b/CLre_server/API/MainServer/Moderator.cs new file mode 100644 index 0000000..580fc63 --- /dev/null +++ b/CLre_server/API/MainServer/Moderator.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GameNetworkLayer.Shared; + +namespace CLre_server.API.MainServer +{ + public class Moderator + { + private static Moderator _instance = null; + + public static Moderator Instance + { + get + { + if (_instance == null) Init(); + return _instance; + } + } + + internal static void Init() + { + if (_instance == null) _instance = new Moderator(); + } + + private ModerationEngine _moderationEngine; + + private Moderator() + { + _moderationEngine = new ModerationEngine(); + Server.Instance.PlayerConnect += (sender, args) => + { +#if DEBUG + Utility.Logging.MetaLog($"Player {args.PlayerId} is connecting, starting ban checker"); +#endif + CheckConnectingPlayerAsap(args.PlayerId).Run(); + }; + } + + public bool DisconnectPlayerById(string publicId) + { + int? playerId = _moderationEngine.FindConnectedPlayerById(publicId); + if (playerId.HasValue) + { + UserVerification.Instance.DisconnectPlayer(playerId.Value, NetworkDispatcherCode.GameDataVerificationFail); + return true; + } + + return false; + } + + public bool DisconnectPlayerByName(string name) + { + int? playerId = _moderationEngine.FindConnectedPlayerByName(name); + if (playerId.HasValue) + { + UserVerification.Instance.DisconnectPlayer(playerId.Value, NetworkDispatcherCode.GameDataVerificationFail); + return true; + } + + return false; + } + + public bool BanPlayerById(string publicId) + { + List bans = new List(CLre.Config.bans); + if (!bans.Contains(publicId)) + { + bans.Add(publicId); + CLre.Config.bans = bans.ToArray(); + } + return DisconnectPlayerById(publicId); + } + + public bool BanPlayerByName(string name) + { + List bans = new List(CLre.Config.bans); + if (!bans.Contains(name)) + { + bans.Add(name); + CLre.Config.bans = bans.ToArray(); + } + return DisconnectPlayerByName(name); + } + + public bool IsModerator(string name) + { + foreach (string modName in CLre.Config.moderators) + { + if (string.Compare(name, modName, StringComparison.InvariantCultureIgnoreCase) == 0) + { + return true; + } + } + + Guid? publicId = _moderationEngine.FindConnectedPlayerGuidByName(name); + if (publicId.HasValue) + { + foreach (string modGuid in CLre.Config.moderators) + { + if (modGuid == publicId.ToString()) + { + return true; + } + } + } + + return false; + } + + public IEnumerator CheckConnectingPlayerAsap(int playerId) + { + while (Server.Instance.Players.Length <= playerId) + { + yield return null; + yield return null; + yield return null; + yield return null; + } + var connector = Server.Instance.Players[playerId]; + if (CLre.Config.bans.Contains(connector.accountId.displayName) + || CLre.Config.bans.Contains(connector.accountId.publicId.ToString())) + { +#if DEBUG + Utility.Logging.MetaLog($"Banned player {connector.accountId.displayName} ({connector.accountId.publicId}) tried to connect, kicking"); +#endif + UserVerification.Instance.DisconnectPlayer(playerId, NetworkDispatcherCode.GameDataVerificationFail); + } +#if DEBUG + else + { + Utility.Logging.MetaLog($"Player {connector.accountId.displayName} ({connector.accountId.publicId}) is not banned, skipping auto-kick"); + } +#endif + } + } +} \ No newline at end of file diff --git a/CLre_server/API/MainServer/Server.cs b/CLre_server/API/MainServer/Server.cs index b260456..f06cdf4 100644 --- a/CLre_server/API/MainServer/Server.cs +++ b/CLre_server/API/MainServer/Server.cs @@ -5,6 +5,7 @@ using GameServer; using HarmonyLib; using NetworkFramework.Server; using Svelto.Context; +using User.Server; namespace CLre_server.API.MainServer { @@ -18,10 +19,15 @@ namespace CLre_server.API.MainServer { get { - if (_instance == null) _instance = new Server(); + if (_instance == null) Init(); return _instance; } } + + internal static void Init() + { + if (_instance == null) _instance = new Server(); + } // instance events public event EventHandler InitStart @@ -79,9 +85,20 @@ namespace CLre_server.API.MainServer } } + public AccountIdServerNode[] Players + { + get => _serverDatabaseQueryEngine.GetConnectedAccounts(); + } + + // fields + + private ServerDatabaseQueryEngine _serverDatabaseQueryEngine; + private ServerReadyEngine _serverReadyEngine; + private Server() { - new ServerReadyEngine(); + _serverReadyEngine = new ServerReadyEngine(); + _serverDatabaseQueryEngine = new ServerDatabaseQueryEngine(); } } diff --git a/CLre_server/API/MainServer/ServerEngines.cs b/CLre_server/API/MainServer/ServerEngines.cs index 85b11b3..571b2ef 100644 --- a/CLre_server/API/MainServer/ServerEngines.cs +++ b/CLre_server/API/MainServer/ServerEngines.cs @@ -1,10 +1,16 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using CLre_server.API.Engines; using Game.DataLoader; using GameServer; +using HarmonyLib; using Svelto.Context; +using Svelto.DataStructures; using Svelto.ECS; +using User.Server; namespace CLre_server.API.MainServer { @@ -45,4 +51,25 @@ namespace CLre_server.API.MainServer if (serverFrameworkDestroyed != null) serverFrameworkDestroyed(this, new StopEventArgs{}); } } + + class ServerDatabaseQueryEngine : ServerEnginePostBuild + { + public override void Ready() + { + } + + public AccountIdServerNode[] GetConnectedAccounts() + { + FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup"); + ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null); + ReadOnlyCollectionStruct accounts = + entitiesDB.QueryEntityViews(accountGroup); + List list = new List(); + foreach (var a in accounts) + { + list.Add(a); + } + return list.ToArray(); + } + } } \ No newline at end of file diff --git a/CLre_server/API/Utility/CardLifeUserAuthentication.cs b/CLre_server/API/Utility/CardLifeUserAuthentication.cs new file mode 100644 index 0000000..11baf6a --- /dev/null +++ b/CLre_server/API/Utility/CardLifeUserAuthentication.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace CLre_server.API.Utility +{ + public static class CardLifeUserAuthentication + { + [Serializable] + private struct AuthPayload + { + public string EmailAddress; + public string Password; + } + + private const string LOGIN_URL = "https://live-auth.cardlifegame.com/api/auth/authenticate"; + + public delegate void OnResponse(AuthenticationResponse data); + + public static IEnumerator Authenticate(string email, string password, OnResponse then) + { + UnityWebRequest req = new UnityWebRequest(LOGIN_URL, "POST"); + AuthPayload payload = new AuthPayload + { + EmailAddress = email, + Password = password, + }; + byte[] bytes = Encoding.UTF8.GetBytes(JsonUtility.ToJson(payload)); + req.uploadHandler = new UploadHandlerRaw(bytes); + req.downloadHandler = new DownloadHandlerBuffer(); + req.SetRequestHeader("Content-Type", "application/json"); + AsyncOperation op = req.SendWebRequest(); + while (!op.isDone) + { + yield return null; + } + + if (req.responseCode != 200) + { + Logging.LogError($"Authentication with email {email} returned code {req.responseCode} and was aborted. Response:\n{req.downloadHandler.text}"); + yield break; + } + + AuthenticationResponse resp = JsonUtility.FromJson(req.downloadHandler.text); + then(resp); + } + } + + [Serializable] + public struct AuthenticationResponse + { + public string PublicId; + public string EmailAddress; + public string DisplayName; + public bool Confirmed; + public string Token; + public uint ID; + } +} \ No newline at end of file diff --git a/CLre_server/CLre_server.cs b/CLre_server/CLre_server.cs index 9944e19..27c9375 100644 --- a/CLre_server/CLre_server.cs +++ b/CLre_server/CLre_server.cs @@ -18,15 +18,18 @@ namespace CLre_server { public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; - public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + public override string Version { get; } = "21Q3 " + Assembly.GetExecutingAssembly().GetName().Version.ToString(); internal static Harmony harmonyInstance = null; + + private const string CONFIG_PATH = "CLre_server.json"; public static CLreConfig Config = CLreConfig.Default(); // called when Cardlife shuts down public override void OnApplicationQuit() { + Config.ToFile(CONFIG_PATH); WebServer.Deinit(); harmonyInstance.UnpatchAll(); } @@ -88,11 +91,14 @@ namespace CLre_server API.MainServer.Server.Instance.InitComplete += (_, __) => API.Utility.Logging.MetaLog("(!) Server successfully initialised"); #endif // try to load config file - Config = CLreConfig.FromFileSafely("CLre_server.json"); + Config = CLreConfig.FromFileSafely(CONFIG_PATH); // init config-dependent functionality WebServer.Init(); API.Synergy.CLreEnforcer.Init(); Tweaks.TerrainModificationExclusionZone.Init(); + Tweaks.Chat.ChatHandler.Init(); + API.MainServer.Server.Init(); + API.MainServer.Moderator.Init(); // Log info API.Utility.Logging.MetaLog($"{Name} init complete."); } diff --git a/CLre_server/Tweaks/Chat/Attributes.cs b/CLre_server/Tweaks/Chat/Attributes.cs new file mode 100644 index 0000000..7483614 --- /dev/null +++ b/CLre_server/Tweaks/Chat/Attributes.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using System.Text.RegularExpressions; + + +namespace CLre_server.Tweaks.Chat +{ + public class Attributes + { + + } + + [System.AttributeUsage(System.AttributeTargets.Method)] + public class ChatCommandAttribute : System.Attribute + { + public readonly string Name; + private readonly Regex _pattern; + private readonly Regex _usernamePattern; + + public ChatCommandAttribute(string name, string pattern, + string usernamePattern = null, + RegexOptions options = RegexOptions.Compiled | RegexOptions.IgnoreCase) + { + this.Name = name; + this._pattern = new Regex(pattern, options); + this._usernamePattern = usernamePattern == null ? null : new Regex(pattern, options); + Assembly asm = Assembly.GetCallingAssembly(); + if (!ChatConnectionEngine._assembliesToCheck.Contains(asm)) + { + ChatConnectionEngine._assembliesToCheck.Add(asm); + } + + if (ChatHandler.IsAuthenticationReady && CLre.Config.chat_commands) + { + // Chat system is already started + // TODO + } + } + + public Regex GetPattern() + { + return _pattern; + } + + public Match RegexMatch(string sender, string message) + { + if (this._usernamePattern != null) + { + if (this._usernamePattern.IsMatch(sender)) + { + return _pattern.Match(message); + } + } + else + { + return _pattern.Match(message); + } + return System.Text.RegularExpressions.Match.Empty; + } + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/AuthenticationEngine.cs b/CLre_server/Tweaks/Chat/AuthenticationEngine.cs new file mode 100644 index 0000000..fffdbf0 --- /dev/null +++ b/CLre_server/Tweaks/Chat/AuthenticationEngine.cs @@ -0,0 +1,21 @@ +namespace CLre_server.Tweaks.Chat +{ + public class AuthenticationEngine: API.Engines.ServerEnginePostBuild + { + public API.Utility.AuthenticationResponse response = default; + public bool IsAuthenticated = false; + + public override void Ready() + { + API.Utility.CardLifeUserAuthentication.Authenticate( + CLre.Config.email_address, + CLre.Config.password, + (data) => + { + this.IsAuthenticated = true; + this.response = data; + API.Utility.Logging.Log("CLre chat credentials successfully authenticated"); + }).Run(); + } + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/ChatConfig.cs b/CLre_server/Tweaks/Chat/ChatConfig.cs new file mode 100644 index 0000000..859f328 --- /dev/null +++ b/CLre_server/Tweaks/Chat/ChatConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace CLre_server.Tweaks.Chat +{ + [Serializable] + public struct ChatConfig + { + public bool commands_enabled; + public string email_address; + public string password; + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/ChatConnectionEngine.cs b/CLre_server/Tweaks/Chat/ChatConnectionEngine.cs new file mode 100644 index 0000000..ba8ff29 --- /dev/null +++ b/CLre_server/Tweaks/Chat/ChatConnectionEngine.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using ExitGames.Client.Photon; +using ExitGames.Client.Photon.Chat; +using HarmonyLib; +using Svelto.Context; + +namespace CLre_server.Tweaks.Chat +{ + public class ChatConnectionEngine: API.Engines.ServerEnginePostBuild, IWaitForFrameworkInitialization, IWaitForFrameworkDestruction + { + private bool _running = false; + private ChatClient _chatClient; + private ChatListener _chatListener; + + public delegate void CommandHandler(Match messageMatch, ChatClient connection, string username); + + private Dictionary _handlers; + + internal static List _assembliesToCheck = new List(new []{typeof(CLre).Assembly}); + + public override void Ready() + { + _running = true; + } + + private IEnumerator connectify() + { + LoadHandlers(); // find & load commands + // wait for login to succeed (it may never) + while (!ChatHandler.IsAuthenticationReady && _running) + { + yield return null; + } + // login with authenticated credentials + // shout-out to however made an identical AuthenticationValues struct in the global namespace + ExitGames.Client.Photon.Chat.AuthenticationValues auth = new ExitGames.Client.Photon.Chat.AuthenticationValues(); + auth.AuthType = ExitGames.Client.Photon.Chat.CustomAuthenticationType.Custom; + auth.AddAuthParameter("publicId", ChatHandler.PublicId); + auth.AddAuthParameter("token", ChatHandler.Token); + auth.UserId = ChatHandler.PublicId; + _chatListener= new ChatListener(_handlers); + _chatClient = new ChatClient(_chatListener, ConnectionProtocol.Udp); + _chatListener._chatClient = _chatClient; + _chatClient.Connect(Game.Utilities.CardLifePhotonSettings.PhotonChatAppID, "1.0", auth); + // run forever (until server shutsdown) + while (_running) + { + _chatClient.Service(); + yield return null; + } + } + + public void OnFrameworkInitialized() + { + connectify().Run(); + } + + public void OnFrameworkDestroyed() + { + _running = false; + } + + private void LoadHandlers() + { + _handlers = new Dictionary(); + foreach (Assembly asm in _assembliesToCheck.ToArray()) + { + foreach (Type t in asm.GetTypes()) + { + foreach (MethodInfo m in t.GetMethods(AccessTools.all)) + { + ChatCommandAttribute attr = m.GetCustomAttribute(); + if (attr != null) + { + // TODO validate that method signature matches that of CommandHandler + API.Utility.Logging.MetaLog($"{t.FullName}:{m.Name} is handling {attr.Name}"); + _handlers.Add(attr, (CommandHandler) Delegate.CreateDelegate(typeof(CommandHandler), m)); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/ChatHandler.cs b/CLre_server/Tweaks/Chat/ChatHandler.cs new file mode 100644 index 0000000..6d666c2 --- /dev/null +++ b/CLre_server/Tweaks/Chat/ChatHandler.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace CLre_server.Tweaks.Chat +{ + public static class ChatHandler + { + private static AuthenticationEngine _chatAuthEngine = null; + private static ChatConnectionEngine _chatConnectionEngine = null; + + internal static bool IsAuthenticationReady + { + get => _chatAuthEngine.IsAuthenticated; + } + + internal static string PublicId + { + get => _chatAuthEngine.response.PublicId; + } + + internal static string Token + { + get => _chatAuthEngine.response.Token; + } + + public static void Init() + { + if (!CLre.Config.chat_commands) return; + _chatAuthEngine = new AuthenticationEngine(); + _chatConnectionEngine = new ChatConnectionEngine(); + } + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/ChatListener.cs b/CLre_server/Tweaks/Chat/ChatListener.cs new file mode 100644 index 0000000..98f3263 --- /dev/null +++ b/CLre_server/Tweaks/Chat/ChatListener.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using ExitGames.Client.Photon; +using ExitGames.Client.Photon.Chat; + +namespace CLre_server.Tweaks.Chat +{ + public class ChatListener: IChatClientListener + { + internal ChatClient _chatClient; + public static string ChatName; + private Dictionary _handlers; + + public ChatListener(Dictionary handlers) + { + _handlers = handlers; + } + + public void DebugReturn(DebugLevel level, string message) + { + API.Utility.Logging.Log($"ServerChatLog<{level}>: {message}"); + } + + public void OnDisconnected() + { + API.Utility.Logging.MetaLog("Chat disconnected"); + } + + public void OnConnected() + { + API.Utility.Logging.MetaLog("Chat connected"); + // autoconnect to server's chat room + Room room = PhotonNetwork.room; + ChatName = ""; + if (room != null) + { + ChatName = $"{room.Name}_chat_"; + } + else + { + return; + } + + bool result = _chatClient.Subscribe(new[] {ChatName}, 10); + API.Utility.Logging.MetaLog($"Subscribed to chat: {result}"); + } + + public void OnChatStateChange(ChatState state) + { + API.Utility.Logging.MetaLog($"Chat state changed to {state}"); + } + + public void OnGetMessages(string channelName, string[] senders, object[] messages) + { + if (channelName != ChatName) return; // just in case the server somehow gets subscribed to the wrong chat + for (int i = 0; i < senders.Length; i++) + { + string message = messages[i].ToString(); +#if DEBUG + API.Utility.Logging.MetaLog($"Chat: `{senders[i]}` said `{messages[i]}` in `{channelName}`"); +#endif + if (messages[i].ToString().ToLower() == "hello server") + { + _chatClient.PublishMessage(channelName, $"Hi {senders[i]}!"); + } + else if (messages[i].ToString().ToLower() == "hello world") + { + _chatClient.PublishMessage(channelName, $"Hello fellow programmer {senders[i]}!"); + } + + if (message.StartsWith("/")) + { + string command = message.Substring(1); + string username = stripUsernameTag(senders[i]); + foreach (ChatCommandAttribute cca in _handlers.Keys) + { + var match = cca.RegexMatch(senders[i], command); + if (match.Success) + { + _handlers[cca](match, _chatClient, username); + } + } + } + + } + } + + public void OnPrivateMessage(string sender, object message, string channelName) + { +#if DEBUG + API.Utility.Logging.MetaLog($"Chat (private): `{sender}` said `{message}` in `{channelName}`"); +#endif + } + + public void OnSubscribed(string[] channels, bool[] results) + { + API.Utility.Logging.MetaLog($"Subscribed"); + } + + public void OnUnsubscribed(string[] channels) + { + API.Utility.Logging.MetaLog($"Unsubscribed"); + } + + public void OnStatusUpdate(string user, int status, bool gotMessage, object message) + { + API.Utility.Logging.MetaLog($"Status update: {user}->{status} ({gotMessage}:{message})"); + } + + private string stripUsernameTag(string sender) + { + if (!sender.Contains("]")) return sender; + return sender.Split(']')[1].Trim(); + } + } +} \ No newline at end of file diff --git a/CLre_server/Tweaks/Chat/ModeratorCommands.cs b/CLre_server/Tweaks/Chat/ModeratorCommands.cs new file mode 100644 index 0000000..58cc388 --- /dev/null +++ b/CLre_server/Tweaks/Chat/ModeratorCommands.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using ExitGames.Client.Photon.Chat; + +namespace CLre_server.Tweaks.Chat +{ + public static class ModeratorCommands + { + [ChatCommand("KICK", "kick ([\\w\\d\\-_]+)")] + public static void KickInButt(Match messageMatch, ChatClient connection, string sender) + { + if (!API.MainServer.Moderator.Instance.IsModerator(sender)) + { + connection.PublishMessage(ChatListener.ChatName, "Ban failure: You're not a mod :("); + return; + } + string target = messageMatch.Groups[1].Value; +#if DEBUG + API.Utility.Logging.MetaLog($"/kick {target}"); +#endif + if (API.MainServer.Moderator.Instance.DisconnectPlayerById(target)) + { + connection.PublishMessage(ChatListener.ChatName, $"Adios {target}"); + } + else if (API.MainServer.Moderator.Instance.DisconnectPlayerByName(target)) + { + connection.PublishMessage(ChatListener.ChatName, $"Bye bye {target}"); + } + else + { + connection.PublishMessage(ChatListener.ChatName, "Kick failure: User not found :("); + } + } + + [ChatCommand("BAN", "ban ([\\w\\d\\-_]+)")] + public static void BanishIdiot(Match messageMatch, ChatClient connection, string sender) + { + if (!API.MainServer.Moderator.Instance.IsModerator(sender)) + { + connection.PublishMessage(ChatListener.ChatName, "Ban failure: You're not a mod :("); + return; + } + string target = messageMatch.Groups[1].Value; +#if DEBUG + API.Utility.Logging.MetaLog($"/ban {target}"); +#endif + if (API.MainServer.Moderator.Instance.BanPlayerById(target)) + { + connection.PublishMessage(ChatListener.ChatName, $"And {target} is no more!"); + } + else if (API.MainServer.Moderator.Instance.BanPlayerByName(target)) + { + connection.PublishMessage(ChatListener.ChatName, $"And {target} was never seen again..."); + } + else + { + connection.PublishMessage(ChatListener.ChatName, "Ban failure: User not found :("); + } + } + + [ChatCommand("(SH)OPIFY", "(mod|op) ([\\w\\d\\-_]+)")] + public static void GoPro(Match messageMatch, ChatClient connection, string sender) + { + if (!API.MainServer.Moderator.Instance.IsModerator(sender)) + { + connection.PublishMessage(ChatListener.ChatName, "Op failure: You're not a mod :("); + return; + } + string target = messageMatch.Groups[2].Value; +#if DEBUG + API.Utility.Logging.MetaLog($"/op {target}"); +#endif + List moderators = new List(CLre.Config.moderators); + moderators.Add(target); + CLre.Config.moderators = moderators.ToArray(); + connection.PublishMessage(ChatListener.ChatName, $"Promoted {target} to moderator"); + } + + [ChatCommand("De(SH)OPIFY", "(demod|deop) ([\\w\\d\\-_]+)")] + public static void AntiProton(Match messageMatch, ChatClient connection, string sender) + { + if (!API.MainServer.Moderator.Instance.IsModerator(sender)) + { + connection.PublishMessage(ChatListener.ChatName, "DeOp failure: You're not a mod :("); + return; + } + string target = messageMatch.Groups[2].Value; +#if DEBUG + API.Utility.Logging.MetaLog($"/deop {target}"); +#endif + List moderators = new List(CLre.Config.moderators); + if (moderators.Remove(target)) + { + CLre.Config.moderators = moderators.ToArray(); + connection.PublishMessage(ChatListener.ChatName, $"Demoted {target} to regular user"); + } + connection.PublishMessage(ChatListener.ChatName, "DeOp failure: User not found :("); + } + + [ChatCommand("ECHO", "echo (.+)$")] + public static void EchoEchoEcho(Match messageMatch, ChatClient connection, string sender) + { + string target = messageMatch.Groups[1].Value; +#if DEBUG + API.Utility.Logging.MetaLog($"/echo {target}"); +#endif + connection.PublishMessage(ChatListener.ChatName, target); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/Attributes.cs b/CLre_server/WebStatus/Attributes.cs index 706eb93..14681a3 100644 --- a/CLre_server/WebStatus/Attributes.cs +++ b/CLre_server/WebStatus/Attributes.cs @@ -23,7 +23,8 @@ namespace CLre_server.WebStatus if (WebServer.MainInstance != null && WebServer.MainInstance.IsRunning) { - + // Web server is already running + // TODO } } diff --git a/CLre_server/WebStatus/WebServer.cs b/CLre_server/WebStatus/WebServer.cs index c0e3614..f231e53 100644 --- a/CLre_server/WebStatus/WebServer.cs +++ b/CLre_server/WebStatus/WebServer.cs @@ -40,7 +40,7 @@ namespace CLre_server.WebStatus { if (!HttpListener.IsSupported) { - API.Utility.Logging.LogWarning("HTTP Server is unsupported on earlier Windows versions. It will fail to start."); + API.Utility.Logging.LogWarning("HTTP Server is unsupported on earlier Windows versions. CLre webserver will fail to start."); } _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://{ip}:{port}/");