Implement server-side chat and chat command framework

This commit is contained in:
NGnius (Graham) 2021-08-13 21:26:48 -04:00
parent 476d6382da
commit b8a8a535f1
17 changed files with 783 additions and 7 deletions

View file

@ -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;

View file

@ -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());
}
}
}

View file

@ -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<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(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<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(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<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(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;
}
}
}

View file

@ -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<string> bans = new List<string>(CLre.Config.bans);
if (!bans.Contains(publicId))
{
bans.Add(publicId);
CLre.Config.bans = bans.ToArray();
}
return DisconnectPlayerById(publicId);
}
public bool BanPlayerByName(string name)
{
List<string> bans = new List<string>(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
}
}
}

View file

@ -5,6 +5,7 @@ using GameServer;
using HarmonyLib;
using NetworkFramework.Server;
using Svelto.Context;
using User.Server;
namespace CLre_server.API.MainServer
{
@ -18,11 +19,16 @@ 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<StartingEventArgs> 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();
}
}

View file

@ -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<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(accountGroup);
List<AccountIdServerNode> list = new List<AccountIdServerNode>();
foreach (var a in accounts)
{
list.Add(a);
}
return list.ToArray();
}
}
}

View file

@ -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<AuthenticationResponse>(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;
}
}

View file

@ -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.");
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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<ChatCommandAttribute, CommandHandler> _handlers;
internal static List<Assembly> _assembliesToCheck = new List<Assembly>(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<ChatCommandAttribute, CommandHandler>();
foreach (Assembly asm in _assembliesToCheck.ToArray())
{
foreach (Type t in asm.GetTypes())
{
foreach (MethodInfo m in t.GetMethods(AccessTools.all))
{
ChatCommandAttribute attr = m.GetCustomAttribute<ChatCommandAttribute>();
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));
}
}
}
}
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<ChatCommandAttribute, ChatConnectionEngine.CommandHandler> _handlers;
public ChatListener(Dictionary<ChatCommandAttribute, ChatConnectionEngine.CommandHandler> 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();
}
}
}

View file

@ -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<string> moderators = new List<string>(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<string> moderators = new List<string>(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);
}
}
}

View file

@ -23,7 +23,8 @@ namespace CLre_server.WebStatus
if (WebServer.MainInstance != null && WebServer.MainInstance.IsRunning)
{
// Web server is already running
// TODO
}
}

View file

@ -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}/");