2022-08-30 15:00:58 +00:00
|
|
|
|
using Fleck;
|
|
|
|
|
using SharpChat.Commands;
|
2023-02-08 23:53:42 +00:00
|
|
|
|
using SharpChat.Config;
|
2023-02-10 06:07:59 +00:00
|
|
|
|
using SharpChat.EventStorage;
|
2023-02-08 23:53:42 +00:00
|
|
|
|
using SharpChat.Misuzu;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
using SharpChat.Packet;
|
2023-02-16 21:16:06 +00:00
|
|
|
|
using SharpChat.PacketHandlers;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
2022-08-30 15:21:00 +00:00
|
|
|
|
using System.Net.Http;
|
2022-08-30 18:29:11 +00:00
|
|
|
|
using System.Threading;
|
2023-02-07 22:28:06 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
|
|
|
|
namespace SharpChat {
|
|
|
|
|
public class SockChatServer : IDisposable {
|
2023-02-08 23:53:42 +00:00
|
|
|
|
public const ushort DEFAULT_PORT = 6770;
|
|
|
|
|
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
|
|
|
|
|
public const int DEFAULT_MAX_CONNECTIONS = 5;
|
|
|
|
|
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
|
|
|
|
public bool IsDisposed { get; private set; }
|
|
|
|
|
|
|
|
|
|
public static ChatUser Bot { get; } = new ChatUser {
|
|
|
|
|
UserId = -1,
|
2023-02-08 03:17:07 +00:00
|
|
|
|
Username = "ChatBot",
|
2022-08-30 15:00:58 +00:00
|
|
|
|
Rank = 0,
|
|
|
|
|
Colour = new ChatColour(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public IWebSocketServer Server { get; }
|
|
|
|
|
public ChatContext Context { get; }
|
|
|
|
|
|
2023-02-06 20:14:50 +00:00
|
|
|
|
private readonly HttpClient HttpClient;
|
2023-02-08 23:53:42 +00:00
|
|
|
|
private readonly MisuzuClient Misuzu;
|
|
|
|
|
|
|
|
|
|
private readonly CachedValue<int> MaxMessageLength;
|
|
|
|
|
private readonly CachedValue<int> MaxConnections;
|
|
|
|
|
private readonly CachedValue<int> FloodKickLength;
|
2022-08-30 15:21:00 +00:00
|
|
|
|
|
2023-02-16 21:16:06 +00:00
|
|
|
|
private readonly List<IChatPacketHandler> GuestHandlers = new();
|
|
|
|
|
private readonly List<IChatPacketHandler> AuthedHandlers = new();
|
|
|
|
|
private readonly SendMessageHandler SendMessageHandler;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2022-08-30 18:29:11 +00:00
|
|
|
|
private bool IsShuttingDown = false;
|
|
|
|
|
|
2023-02-10 06:07:59 +00:00
|
|
|
|
private ChatChannel DefaultChannel { get; set; }
|
|
|
|
|
|
|
|
|
|
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
|
2023-02-08 23:53:42 +00:00
|
|
|
|
Logger.Write("Initialising Sock Chat server...");
|
|
|
|
|
|
|
|
|
|
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
|
|
|
|
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-08 23:53:42 +00:00
|
|
|
|
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
|
|
|
|
|
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
|
|
|
|
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
2022-08-30 18:29:11 +00:00
|
|
|
|
|
2023-02-10 06:07:59 +00:00
|
|
|
|
Context = new ChatContext(evtStore);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-08 23:53:42 +00:00
|
|
|
|
string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
|
|
|
|
|
|
|
|
|
|
foreach(string channelName in channelNames) {
|
|
|
|
|
ChatChannel channelInfo = new(channelName);
|
|
|
|
|
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
|
|
|
|
|
|
|
|
|
|
string tmp;
|
|
|
|
|
tmp = channelCfg.SafeReadValue("name", string.Empty);
|
|
|
|
|
if(!string.IsNullOrWhiteSpace(tmp))
|
|
|
|
|
channelInfo.Name = tmp;
|
|
|
|
|
|
|
|
|
|
channelInfo.Password = channelCfg.SafeReadValue("password", string.Empty);
|
|
|
|
|
channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-08 23:53:42 +00:00
|
|
|
|
Context.Channels.Add(channelInfo);
|
2023-02-10 06:07:59 +00:00
|
|
|
|
|
|
|
|
|
DefaultChannel ??= channelInfo;
|
2023-02-08 23:53:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:16:06 +00:00
|
|
|
|
GuestHandlers.Add(new AuthHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections));
|
|
|
|
|
|
|
|
|
|
AuthedHandlers.AddRange(new IChatPacketHandler[] {
|
|
|
|
|
new PingHandler(Misuzu),
|
|
|
|
|
SendMessageHandler = new SendMessageHandler(MaxMessageLength),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
SendMessageHandler.AddCommands(new IChatCommand[] {
|
2023-02-16 20:34:59 +00:00
|
|
|
|
new AFKCommand(),
|
|
|
|
|
new NickCommand(),
|
|
|
|
|
new WhisperCommand(),
|
|
|
|
|
new ActionCommand(),
|
|
|
|
|
new WhoCommand(),
|
|
|
|
|
new JoinChannelCommand(),
|
|
|
|
|
new CreateChannelCommand(),
|
|
|
|
|
new DeleteChannelCommand(),
|
|
|
|
|
new PasswordChannelCommand(),
|
|
|
|
|
new RankChannelCommand(),
|
|
|
|
|
new BroadcastCommand(),
|
|
|
|
|
new DeleteMessageCommand(),
|
|
|
|
|
new KickBanCommand(msz),
|
|
|
|
|
new PardonUserCommand(msz),
|
|
|
|
|
new PardonAddressCommand(msz),
|
|
|
|
|
new BanListCommand(msz),
|
|
|
|
|
new SilenceApplyCommand(),
|
|
|
|
|
new SilenceRevokeCommand(),
|
|
|
|
|
new RemoteAddressCommand(),
|
|
|
|
|
});
|
|
|
|
|
|
2023-02-08 23:53:42 +00:00
|
|
|
|
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
2023-02-08 03:17:07 +00:00
|
|
|
|
Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}");
|
2023-02-06 20:14:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 20:34:59 +00:00
|
|
|
|
public void Listen(ManualResetEvent waitHandle) {
|
|
|
|
|
if(waitHandle != null)
|
2023-02-16 21:16:06 +00:00
|
|
|
|
SendMessageHandler.AddCommand(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true)));
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
|
|
|
|
Server.Start(sock => {
|
2022-08-30 18:29:11 +00:00
|
|
|
|
if(IsShuttingDown || IsDisposed) {
|
|
|
|
|
sock.Close(1013);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-30 15:00:58 +00:00
|
|
|
|
sock.OnOpen = () => OnOpen(sock);
|
|
|
|
|
sock.OnClose = () => OnClose(sock);
|
|
|
|
|
sock.OnError = err => OnError(sock, err);
|
|
|
|
|
sock.OnMessage = msg => OnMessage(sock, msg);
|
|
|
|
|
});
|
2023-02-08 23:53:42 +00:00
|
|
|
|
|
|
|
|
|
Logger.Write("Listening...");
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
private void OnOpen(IWebSocketConnection sock) {
|
|
|
|
|
Logger.Write($"Connection opened from {sock.ConnectionInfo.ClientIpAddress}:{sock.ConnectionInfo.ClientPort}");
|
2023-02-10 06:07:59 +00:00
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
lock(Context.ConnectionsAccess) {
|
|
|
|
|
if(!Context.Connections.Any(x => x.Socket == sock))
|
|
|
|
|
Context.Connections.Add(new ChatConnection(sock));
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context.Update();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
private void OnClose(IWebSocketConnection sock) {
|
|
|
|
|
Logger.Write($"Connection closed from {sock.ConnectionInfo.ClientIpAddress}:{sock.ConnectionInfo.ClientPort}");
|
2023-02-10 06:07:59 +00:00
|
|
|
|
|
2023-02-16 22:33:48 +00:00
|
|
|
|
lock(Context.ConnectionsAccess) {
|
|
|
|
|
ChatConnection conn = Context.GetConnection(sock);
|
|
|
|
|
if(conn == null)
|
|
|
|
|
return;
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-16 22:33:48 +00:00
|
|
|
|
Context.Connections.Remove(conn);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-16 22:33:48 +00:00
|
|
|
|
if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User))
|
2023-02-17 19:02:35 +00:00
|
|
|
|
Context.HandleDisconnect(conn.User);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Context.Update();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
private void OnError(IWebSocketConnection sock, Exception ex) {
|
|
|
|
|
string connId;
|
|
|
|
|
lock(Context.ConnectionsAccess) {
|
|
|
|
|
ChatConnection conn = Context.GetConnection(sock);
|
|
|
|
|
connId = conn?.Id ?? new string('0', ChatConnection.ID_LENGTH);
|
2023-02-16 20:34:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
Logger.Write($"[{connId} {sock.ConnectionInfo.ClientIpAddress}] {ex}");
|
2022-08-30 15:00:58 +00:00
|
|
|
|
Context.Update();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
private void OnMessage(IWebSocketConnection sock, string msg) {
|
2022-08-30 15:00:58 +00:00
|
|
|
|
Context.Update();
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
ChatConnection conn;
|
|
|
|
|
lock(Context.ConnectionsAccess)
|
|
|
|
|
conn = Context.GetConnection(sock);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
if(conn == null) {
|
|
|
|
|
sock.Close();
|
2022-08-30 15:00:58 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:16:06 +00:00
|
|
|
|
// this doesn't affect non-authed connections?????
|
2023-02-16 21:25:41 +00:00
|
|
|
|
if(conn.User is not null && conn.User.HasFloodProtection) {
|
|
|
|
|
conn.User.RateLimiter.AddTimePoint();
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
if(conn.User.RateLimiter.State == ChatRateLimitState.Kick) {
|
2023-02-07 22:28:06 +00:00
|
|
|
|
Task.Run(async () => {
|
2023-02-08 23:53:42 +00:00
|
|
|
|
TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength);
|
2023-02-07 22:28:06 +00:00
|
|
|
|
|
2023-02-08 23:53:42 +00:00
|
|
|
|
await Misuzu.CreateBanAsync(
|
2023-02-16 21:25:41 +00:00
|
|
|
|
conn.User.UserId.ToString(), conn.RemoteAddress.ToString(),
|
2023-02-07 22:28:06 +00:00
|
|
|
|
string.Empty, "::1",
|
|
|
|
|
duration,
|
|
|
|
|
"Kicked from chat for flood protection."
|
|
|
|
|
);
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
Context.BanUser(conn.User, duration, UserDisconnectReason.Flood);
|
2023-02-07 22:28:06 +00:00
|
|
|
|
}).Wait();
|
2022-08-30 15:00:58 +00:00
|
|
|
|
return;
|
2023-02-16 21:25:41 +00:00
|
|
|
|
} else if(conn.User.RateLimiter.State == ChatRateLimitState.Warning)
|
2023-02-16 22:33:48 +00:00
|
|
|
|
Context.SendTo(conn.User, new FloodWarningPacket());
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
ChatPacketHandlerContext context = new(msg, Context, conn);
|
|
|
|
|
IChatPacketHandler handler = conn.User is null
|
2023-02-16 21:16:06 +00:00
|
|
|
|
? GuestHandlers.FirstOrDefault(h => h.IsMatch(context))
|
|
|
|
|
: AuthedHandlers.FirstOrDefault(h => h.IsMatch(context));
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-16 21:16:06 +00:00
|
|
|
|
handler?.Handle(context);
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 20:14:50 +00:00
|
|
|
|
~SockChatServer() {
|
|
|
|
|
DoDispose();
|
|
|
|
|
}
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-06 20:14:50 +00:00
|
|
|
|
public void Dispose() {
|
|
|
|
|
DoDispose();
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
2022-08-30 15:00:58 +00:00
|
|
|
|
|
2023-02-06 20:14:50 +00:00
|
|
|
|
private void DoDispose() {
|
2022-08-30 15:00:58 +00:00
|
|
|
|
if(IsDisposed)
|
|
|
|
|
return;
|
|
|
|
|
IsDisposed = true;
|
|
|
|
|
|
2023-02-16 21:25:41 +00:00
|
|
|
|
lock(Context.ConnectionsAccess)
|
|
|
|
|
foreach(ChatConnection conn in Context.Connections)
|
|
|
|
|
conn.Dispose();
|
2022-08-30 18:29:11 +00:00
|
|
|
|
|
2022-08-30 15:00:58 +00:00
|
|
|
|
Server?.Dispose();
|
2022-08-30 15:21:00 +00:00
|
|
|
|
HttpClient?.Dispose();
|
2022-08-30 15:00:58 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|