Implement server-side chat and chat command framework
This commit is contained in:
parent
476d6382da
commit
b8a8a535f1
17 changed files with 783 additions and 7 deletions
|
@ -18,7 +18,7 @@ namespace CLre
|
||||||
{
|
{
|
||||||
public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name;
|
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;
|
internal static Harmony harmonyInstance = null;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace CLre_server.API.Config
|
namespace CLre_server.API.Config
|
||||||
{
|
{
|
||||||
|
@ -9,6 +10,11 @@ namespace CLre_server.API.Config
|
||||||
public bool clre_clients_only;
|
public bool clre_clients_only;
|
||||||
public bool web_server;
|
public bool web_server;
|
||||||
public bool terrain_exclusion_zone;
|
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()
|
public static CLreConfig Default()
|
||||||
{
|
{
|
||||||
|
@ -17,6 +23,11 @@ namespace CLre_server.API.Config
|
||||||
clre_clients_only = false,
|
clre_clients_only = false,
|
||||||
web_server = false,
|
web_server = false,
|
||||||
terrain_exclusion_zone = 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);
|
return UnityEngine.JsonUtility.ToJson(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ToFile(string path)
|
||||||
|
{
|
||||||
|
File.WriteAllText(path, this.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
68
CLre_server/API/MainServer/ModerationEngine.cs
Normal file
68
CLre_server/API/MainServer/ModerationEngine.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
CLre_server/API/MainServer/Moderator.cs
Normal file
139
CLre_server/API/MainServer/Moderator.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using GameServer;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using NetworkFramework.Server;
|
using NetworkFramework.Server;
|
||||||
using Svelto.Context;
|
using Svelto.Context;
|
||||||
|
using User.Server;
|
||||||
|
|
||||||
namespace CLre_server.API.MainServer
|
namespace CLre_server.API.MainServer
|
||||||
{
|
{
|
||||||
|
@ -18,10 +19,15 @@ namespace CLre_server.API.MainServer
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_instance == null) _instance = new Server();
|
if (_instance == null) Init();
|
||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void Init()
|
||||||
|
{
|
||||||
|
if (_instance == null) _instance = new Server();
|
||||||
|
}
|
||||||
|
|
||||||
// instance events
|
// instance events
|
||||||
public event EventHandler<StartingEventArgs> InitStart
|
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()
|
private Server()
|
||||||
{
|
{
|
||||||
new ServerReadyEngine();
|
_serverReadyEngine = new ServerReadyEngine();
|
||||||
|
_serverDatabaseQueryEngine = new ServerDatabaseQueryEngine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using CLre_server.API.Engines;
|
using CLre_server.API.Engines;
|
||||||
using Game.DataLoader;
|
using Game.DataLoader;
|
||||||
using GameServer;
|
using GameServer;
|
||||||
|
using HarmonyLib;
|
||||||
using Svelto.Context;
|
using Svelto.Context;
|
||||||
|
using Svelto.DataStructures;
|
||||||
using Svelto.ECS;
|
using Svelto.ECS;
|
||||||
|
using User.Server;
|
||||||
|
|
||||||
namespace CLre_server.API.MainServer
|
namespace CLre_server.API.MainServer
|
||||||
{
|
{
|
||||||
|
@ -45,4 +51,25 @@ namespace CLre_server.API.MainServer
|
||||||
if (serverFrameworkDestroyed != null) serverFrameworkDestroyed(this, new StopEventArgs{});
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
61
CLre_server/API/Utility/CardLifeUserAuthentication.cs
Normal file
61
CLre_server/API/Utility/CardLifeUserAuthentication.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,15 +18,18 @@ namespace CLre_server
|
||||||
{
|
{
|
||||||
public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name;
|
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;
|
internal static Harmony harmonyInstance = null;
|
||||||
|
|
||||||
|
private const string CONFIG_PATH = "CLre_server.json";
|
||||||
|
|
||||||
public static CLreConfig Config = CLreConfig.Default();
|
public static CLreConfig Config = CLreConfig.Default();
|
||||||
|
|
||||||
// called when Cardlife shuts down
|
// called when Cardlife shuts down
|
||||||
public override void OnApplicationQuit()
|
public override void OnApplicationQuit()
|
||||||
{
|
{
|
||||||
|
Config.ToFile(CONFIG_PATH);
|
||||||
WebServer.Deinit();
|
WebServer.Deinit();
|
||||||
harmonyInstance.UnpatchAll();
|
harmonyInstance.UnpatchAll();
|
||||||
}
|
}
|
||||||
|
@ -88,11 +91,14 @@ namespace CLre_server
|
||||||
API.MainServer.Server.Instance.InitComplete += (_, __) => API.Utility.Logging.MetaLog("(!) Server successfully initialised");
|
API.MainServer.Server.Instance.InitComplete += (_, __) => API.Utility.Logging.MetaLog("(!) Server successfully initialised");
|
||||||
#endif
|
#endif
|
||||||
// try to load config file
|
// try to load config file
|
||||||
Config = CLreConfig.FromFileSafely("CLre_server.json");
|
Config = CLreConfig.FromFileSafely(CONFIG_PATH);
|
||||||
// init config-dependent functionality
|
// init config-dependent functionality
|
||||||
WebServer.Init();
|
WebServer.Init();
|
||||||
API.Synergy.CLreEnforcer.Init();
|
API.Synergy.CLreEnforcer.Init();
|
||||||
Tweaks.TerrainModificationExclusionZone.Init();
|
Tweaks.TerrainModificationExclusionZone.Init();
|
||||||
|
Tweaks.Chat.ChatHandler.Init();
|
||||||
|
API.MainServer.Server.Init();
|
||||||
|
API.MainServer.Moderator.Init();
|
||||||
// Log info
|
// Log info
|
||||||
API.Utility.Logging.MetaLog($"{Name} init complete.");
|
API.Utility.Logging.MetaLog($"{Name} init complete.");
|
||||||
}
|
}
|
||||||
|
|
60
CLre_server/Tweaks/Chat/Attributes.cs
Normal file
60
CLre_server/Tweaks/Chat/Attributes.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
CLre_server/Tweaks/Chat/AuthenticationEngine.cs
Normal file
21
CLre_server/Tweaks/Chat/AuthenticationEngine.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
CLre_server/Tweaks/Chat/ChatConfig.cs
Normal file
12
CLre_server/Tweaks/Chat/ChatConfig.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
88
CLre_server/Tweaks/Chat/ChatConnectionEngine.cs
Normal file
88
CLre_server/Tweaks/Chat/ChatConnectionEngine.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
CLre_server/Tweaks/Chat/ChatHandler.cs
Normal file
33
CLre_server/Tweaks/Chat/ChatHandler.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
CLre_server/Tweaks/Chat/ChatListener.cs
Normal file
116
CLre_server/Tweaks/Chat/ChatListener.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
CLre_server/Tweaks/Chat/ModeratorCommands.cs
Normal file
111
CLre_server/Tweaks/Chat/ModeratorCommands.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,8 @@ namespace CLre_server.WebStatus
|
||||||
|
|
||||||
if (WebServer.MainInstance != null && WebServer.MainInstance.IsRunning)
|
if (WebServer.MainInstance != null && WebServer.MainInstance.IsRunning)
|
||||||
{
|
{
|
||||||
|
// Web server is already running
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace CLre_server.WebStatus
|
||||||
{
|
{
|
||||||
if (!HttpListener.IsSupported)
|
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 = new HttpListener();
|
||||||
_httpListener.Prefixes.Add($"http://{ip}:{port}/");
|
_httpListener.Prefixes.Add($"http://{ip}:{port}/");
|
||||||
|
|
Loading…
Reference in a new issue