From 34e4e9b1a917f76063fa8bf3db5a9f9d93fb7fdd Mon Sep 17 00:00:00 2001 From: flashwave <me@flash.moe> Date: Sat, 26 Apr 2025 23:15:54 +0000 Subject: [PATCH] Switched to file namespace declarations. --- .editorconfig | 2 +- SharpChat.Flashii/FlashiiAuthResult.cs | 40 +- SharpChat.Flashii/FlashiiBanInfo.cs | 22 +- SharpChat.Flashii/FlashiiClient.cs | 386 +++++----- SharpChat.Flashii/FlashiiIPAddressBanInfo.cs | 12 +- SharpChat.Flashii/FlashiiRawBanInfo.cs | 40 +- SharpChat.Flashii/FlashiiUserBanInfo.cs | 16 +- SharpChat.SockChat/S2CPacket.cs | 8 +- .../S2CPackets/AuthFailS2CPacket.cs | 74 +- .../S2CPackets/AuthSuccessS2CPacket.cs | 68 +- .../S2CPackets/BanListS2CPacket.cs | 42 +- .../S2CPackets/ChannelCreateS2CPacket.cs | 32 +- .../S2CPackets/ChannelDeleteS2CPacket.cs | 20 +- .../S2CPackets/ChannelUpdateS2CPacket.cs | 38 +- .../S2CPackets/ChatMessageAddS2CPacket.cs | 72 +- .../S2CPackets/ChatMessageDeleteS2CPacket.cs | 16 +- .../S2CPackets/CommandResponseS2CPacket.cs | 150 ++-- .../S2CPackets/ContextChannelsS2CPacket.cs | 32 +- .../S2CPackets/ContextClearS2CPacket.cs | 30 +- .../S2CPackets/ContextUsersS2CPacket.cs | 56 +- .../S2CPackets/ForceDisconnectS2CPacket.cs | 32 +- .../S2CPackets/PongS2CPacket.cs | 10 +- .../UserChannelForceJoinS2CPacket.cs | 16 +- .../S2CPackets/UserChannelJoinS2CPacket.cs | 62 +- .../S2CPackets/UserChannelLeaveS2CPacket.cs | 20 +- .../S2CPackets/UserConnectS2CPacket.cs | 68 +- .../S2CPackets/UserDisconnectS2CPacket.cs | 88 +-- .../S2CPackets/UserUpdateS2CPacket.cs | 56 +- SharpChat/C2SPacketHandler.cs | 10 +- SharpChat/C2SPacketHandlerContext.cs | 30 +- .../C2SPacketHandlers/AuthC2SPacketHandler.cs | 170 ++--- .../C2SPacketHandlers/PingC2SPacketHandler.cs | 50 +- .../SendMessageC2SPacketHandler.cs | 128 ++-- SharpChat/Channel.cs | 64 +- SharpChat/ClientCommand.cs | 10 +- SharpChat/ClientCommandContext.cs | 84 +-- SharpChat/ClientCommands/AFKClientCommand.cs | 52 +- .../ClientCommands/ActionClientCommand.cs | 52 +- .../ClientCommands/BanListClientCommand.cs | 44 +- .../ClientCommands/BroadcastClientCommand.cs | 54 +- .../CreateChannelClientCommand.cs | 108 +-- .../DeleteChannelClientCommand.cs | 58 +- .../DeleteMessageClientCommand.cs | 63 +- .../JoinChannelClientCommand.cs | 34 +- .../ClientCommands/KickBanClientCommand.cs | 116 +-- SharpChat/ClientCommands/NickClientCommand.cs | 104 +-- .../PardonAddressClientCommand.cs | 62 +- .../ClientCommands/PardonUserClientCommand.cs | 76 +- .../PasswordChannelClientCommand.cs | 38 +- .../RankChannelClientCommand.cs | 42 +- .../RemoteAddressClientCommand.cs | 44 +- .../ShutdownRestartClientCommand.cs | 44 +- .../ClientCommands/WhisperClientCommand.cs | 72 +- SharpChat/ClientCommands/WhoClientCommand.cs | 100 +-- SharpChat/Connection.cs | 120 +-- SharpChat/Context.cs | 712 +++++++++--------- SharpChat/EventStorage/EventStorage.cs | 38 +- SharpChat/EventStorage/MariaDBEventStorage.cs | 226 +++--- .../MariaDBEventStorage_Database.cs | 152 ++-- .../MariaDBEventStorage_Migrations.cs | 154 ++-- SharpChat/EventStorage/StoredEventFlags.cs | 18 +- SharpChat/EventStorage/StoredEventInfo.cs | 40 +- SharpChat/EventStorage/VirtualEventStorage.cs | 114 +-- SharpChat/Events/ChatEvent.cs | 6 +- SharpChat/Events/MessageCreateEvent.cs | 60 +- SharpChat/Program.cs | 222 +++--- SharpChat/SharpChatWebSocketServer.cs | 272 +++---- .../S2CPackets/ContextMessageS2CPacket.cs | 190 ++--- SharpChat/SockChatServer.cs | 378 +++++----- SharpChat/User.cs | 114 +-- SharpChat/UserStatus.cs | 12 +- SharpChatCommon/Auth/AuthClient.cs | 10 +- SharpChatCommon/Auth/AuthFailedException.cs | 6 +- SharpChatCommon/Auth/AuthResult.cs | 16 +- SharpChatCommon/Bans/BanInfo.cs | 16 +- SharpChatCommon/Bans/BanKind.cs | 10 +- SharpChatCommon/Bans/BansClient.cs | 30 +- SharpChatCommon/Bans/IPAddressBanInfo.cs | 8 +- SharpChatCommon/Bans/UserBanInfo.cs | 12 +- SharpChatCommon/ColourInheritable.cs | 22 +- SharpChatCommon/ColourRgb.cs | 16 +- SharpChatCommon/Configuration/CachedValue.cs | 54 +- SharpChatCommon/Configuration/Config.cs | 48 +- .../Configuration/ConfigExceptions.cs | 14 +- SharpChatCommon/Configuration/ScopedConfig.cs | 50 +- SharpChatCommon/Configuration/StreamConfig.cs | 190 ++--- SharpChatCommon/Logger.cs | 48 +- SharpChatCommon/RNG.cs | 124 +-- SharpChatCommon/RateLimiter.cs | 56 +- SharpChatCommon/SharpInfo.cs | 48 +- SharpChatCommon/Snowflake/RandomSnowflake.cs | 18 +- .../Snowflake/SnowflakeGenerator.cs | 54 +- SharpChatCommon/UserPermissions.cs | 46 +- 93 files changed, 3470 insertions(+), 3471 deletions(-) diff --git a/.editorconfig b/.editorconfig index 4aa2717..0209688 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ csharp_indent_labels = one_less_than_current csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_primary_constructors = true:suggestion diff --git a/SharpChat.Flashii/FlashiiAuthResult.cs b/SharpChat.Flashii/FlashiiAuthResult.cs index 8bcc823..86a2645 100644 --- a/SharpChat.Flashii/FlashiiAuthResult.cs +++ b/SharpChat.Flashii/FlashiiAuthResult.cs @@ -1,31 +1,31 @@ using SharpChat.Auth; using System.Text.Json.Serialization; -namespace SharpChat.Flashii { - public class FlashiiAuthResult : AuthResult { - public string UserId => UserIdRaw.ToString(); - public string UserName => UserNameRaw ?? string.Empty; - public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); +namespace SharpChat.Flashii; - [JsonPropertyName("success")] - public bool Success { get; init; } +public class FlashiiAuthResult : AuthResult { + public string UserId => UserIdRaw.ToString(); + public string UserName => UserNameRaw ?? string.Empty; + public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); - [JsonPropertyName("reason")] - public string? Reason { get; init; } + [JsonPropertyName("success")] + public bool Success { get; init; } - [JsonPropertyName("user_id")] - public long UserIdRaw { get; init; } + [JsonPropertyName("reason")] + public string? Reason { get; init; } - [JsonPropertyName("username")] - public string? UserNameRaw { get; init; } + [JsonPropertyName("user_id")] + public long UserIdRaw { get; init; } - [JsonPropertyName("colour_raw")] - public int UserColourRaw { get; init; } + [JsonPropertyName("username")] + public string? UserNameRaw { get; init; } - [JsonPropertyName("hierarchy")] - public int UserRank { get; init; } + [JsonPropertyName("colour_raw")] + public int UserColourRaw { get; init; } - [JsonPropertyName("perms")] - public UserPermissions UserPermissions { get; init; } - } + [JsonPropertyName("hierarchy")] + public int UserRank { get; init; } + + [JsonPropertyName("perms")] + public UserPermissions UserPermissions { get; init; } } diff --git a/SharpChat.Flashii/FlashiiBanInfo.cs b/SharpChat.Flashii/FlashiiBanInfo.cs index ed6906c..51b3472 100644 --- a/SharpChat.Flashii/FlashiiBanInfo.cs +++ b/SharpChat.Flashii/FlashiiBanInfo.cs @@ -1,13 +1,13 @@ -using SharpChat.Bans; +using SharpChat.Bans; -namespace SharpChat.Flashii { - public abstract class FlashiiBanInfo( - BanKind kind, - FlashiiRawBanInfo rawBanInfo - ) : BanInfo { - public BanKind Kind { get; } = kind; - public bool IsPermanent { get; } = rawBanInfo.IsPermanent; - public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt; - public abstract override string ToString(); - } +namespace SharpChat.Flashii; + +public abstract class FlashiiBanInfo( + BanKind kind, + FlashiiRawBanInfo rawBanInfo +) : BanInfo { + public BanKind Kind { get; } = kind; + public bool IsPermanent { get; } = rawBanInfo.IsPermanent; + public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt; + public abstract override string ToString(); } diff --git a/SharpChat.Flashii/FlashiiClient.cs b/SharpChat.Flashii/FlashiiClient.cs index a749532..b4cd820 100644 --- a/SharpChat.Flashii/FlashiiClient.cs +++ b/SharpChat.Flashii/FlashiiClient.cs @@ -6,235 +6,235 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -namespace SharpChat.Flashii { - public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient { - private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat"; - private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL); +namespace SharpChat.Flashii; - private const string DEFAULT_SECRET_KEY = "woomy"; - private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY); +public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient { + private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat"; + private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL); - private string CreateStringSignature(string str) { - return CreateBufferSignature(Encoding.UTF8.GetBytes(str)); - } + private const string DEFAULT_SECRET_KEY = "woomy"; + private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY); - private string CreateBufferSignature(byte[] bytes) { - using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!)); - return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2"))); - } + private string CreateStringSignature(string str) { + return CreateBufferSignature(Encoding.UTF8.GetBytes(str)); + } - private const string AUTH_VERIFY_URL = "{0}/verify"; - private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}"; + private string CreateBufferSignature(byte[] bytes) { + using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!)); + return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2"))); + } - public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) { + private const string AUTH_VERIFY_URL = "{0}/verify"; + private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}"; + + public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) { + string remoteAddrStr = remoteAddr.ToString(); + + HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) { + Content = new FormUrlEncodedContent(new Dictionary<string, string> { + { "method", scheme }, + { "token", token }, + { "ipaddr", remoteAddrStr }, + }), + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) }, + }, + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream); + if(authResult?.Success != true) + throw new AuthFailedException(authResult?.Reason ?? "none"); + + return authResult; + } + + private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump"; + + public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) { + if(!entries.Any()) + return; + + string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + StringBuilder sb = new(); + sb.AppendFormat("bump#{0}", now); + + Dictionary<string, string> formData = new() { + { "t", now }, + }; + + foreach(var (remoteAddr, userId) in entries) { string remoteAddrStr = remoteAddr.ToString(); - - HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) { - Content = new FormUrlEncodedContent(new Dictionary<string, string> { - { "method", scheme }, - { "token", token }, - { "ipaddr", remoteAddrStr }, - }), - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) }, - }, - }; - - using HttpResponseMessage response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - using Stream stream = await response.Content.ReadAsStreamAsync(); - FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream); - if(authResult?.Success != true) - throw new AuthFailedException(authResult?.Reason ?? "none"); - - return authResult; + sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr); + formData.Add(string.Format("u[{0}]", userId), remoteAddrStr); } - private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump"; + HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) } + }, + Content = new FormUrlEncodedContent(formData), + }; - public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) { - if(!entries.Any()) - return; + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } - string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); - StringBuilder sb = new(); - sb.AppendFormat("bump#{0}", now); + private const string BANS_CREATE_URL = "{0}/bans/create"; + private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}"; - Dictionary<string, string> formData = new() { + public async Task BanCreateAsync( + BanKind kind, + TimeSpan duration, + IPAddress remoteAddr, + string? userId = null, + string? reason = null, + IPAddress? issuerRemoteAddr = null, + string? issuerUserId = null + ) { + if(duration <= TimeSpan.Zero || kind != BanKind.User) + return; + + issuerUserId ??= string.Empty; + userId ??= string.Empty; + reason ??= string.Empty; + issuerRemoteAddr ??= IPAddress.IPv6None; + + string isPerma = duration == TimeSpan.MaxValue ? "1" : "0"; + string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString(); + string remoteAddrStr = remoteAddr.ToString(); + string issuerRemoteAddrStr = issuerRemoteAddr.ToString(); + + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string sig = string.Format( + BANS_CREATE_SIG, + now, userId, remoteAddrStr, + issuerUserId, issuerRemoteAddrStr, + durationStr, isPerma, reason + ); + + HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + Content = new FormUrlEncodedContent(new Dictionary<string, string> { { "t", now }, - }; + { "ui", userId }, + { "ua", remoteAddrStr }, + { "mi", issuerUserId }, + { "ma", issuerRemoteAddrStr }, + { "d", durationStr }, + { "p", isPerma }, + { "r", reason }, + }), + }; - foreach(var (remoteAddr, userId) in entries) { - string remoteAddrStr = remoteAddr.ToString(); - sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr); - formData.Add(string.Format("u[{0}]", userId), remoteAddrStr); - } + using HttpResponseMessage response = await httpClient.SendAsync(request); - HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) { - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) } - }, - Content = new FormUrlEncodedContent(formData), - }; + response.EnsureSuccessStatusCode(); + } - using HttpResponseMessage response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - } + private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}"; + private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}"; - private const string BANS_CREATE_URL = "{0}/bans/create"; - private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}"; + public async Task<bool> BanRevokeAsync(BanInfo info) { + string type; + string target; - public async Task BanCreateAsync( - BanKind kind, - TimeSpan duration, - IPAddress remoteAddr, - string? userId = null, - string? reason = null, - IPAddress? issuerRemoteAddr = null, - string? issuerUserId = null - ) { - if(duration <= TimeSpan.Zero || kind != BanKind.User) - return; + if(info is UserBanInfo ubi) { + if(info.Kind != BanKind.User) + throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info)); - issuerUserId ??= string.Empty; - userId ??= string.Empty; - reason ??= string.Empty; - issuerRemoteAddr ??= IPAddress.IPv6None; + type = "user"; + target = ubi.UserId; + } else if(info is IPAddressBanInfo iabi) { + if(info.Kind != BanKind.IPAddress) + throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info)); - string isPerma = duration == TimeSpan.MaxValue ? "1" : "0"; - string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString(); - string remoteAddrStr = remoteAddr.ToString(); - string issuerRemoteAddrStr = issuerRemoteAddr.ToString(); + type = "addr"; + target = iabi.Address.ToString(); + } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info)); - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string sig = string.Format( - BANS_CREATE_SIG, - now, userId, remoteAddrStr, - issuerUserId, issuerRemoteAddrStr, - durationStr, isPerma, reason - ); + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now)); + string sig = string.Format(BANS_REVOKE_SIG, now, type, target); - HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) { - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sig) }, - }, - Content = new FormUrlEncodedContent(new Dictionary<string, string> { - { "t", now }, - { "ui", userId }, - { "ua", remoteAddrStr }, - { "mi", issuerUserId }, - { "ma", issuerRemoteAddrStr }, - { "d", durationStr }, - { "p", isPerma }, - { "r", reason }, - }), - }; + HttpRequestMessage request = new(HttpMethod.Delete, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; - using HttpResponseMessage response = await httpClient.SendAsync(request); + using HttpResponseMessage response = await httpClient.SendAsync(request); + if(response.StatusCode == HttpStatusCode.NotFound) + return false; - response.EnsureSuccessStatusCode(); - } + response.EnsureSuccessStatusCode(); - private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}"; - private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}"; + return response.StatusCode == HttpStatusCode.NoContent; + } - public async Task<bool> BanRevokeAsync(BanInfo info) { - string type; - string target; + private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}"; + private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}"; - if(info is UserBanInfo ubi) { - if(info.Kind != BanKind.User) - throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info)); + public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) { + userIdOrName ??= "0"; + remoteAddr ??= IPAddress.None; - type = "user"; - target = ubi.UserId; - } else if(info is IPAddressBanInfo iabi) { - if(info.Kind != BanKind.IPAddress) - throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info)); + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9'); + string remoteAddrStr = remoteAddr.ToString(); + string usingUserNameStr = usingUserName ? "1" : "0"; + string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr)); + string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr); - type = "addr"; - target = iabi.Address.ToString(); - } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info)); + HttpRequestMessage request = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now)); - string sig = string.Format(BANS_REVOKE_SIG, now, type, target); + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - HttpRequestMessage request = new(HttpMethod.Delete, url) { - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sig) }, - }, - }; + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream); + if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired) + return null; - using HttpResponseMessage response = await httpClient.SendAsync(request); - if(response.StatusCode == HttpStatusCode.NotFound) - return false; + return rawBanInfo.RemoteAddress is null or "::" + ? new FlashiiUserBanInfo(rawBanInfo) + : new FlashiiIPAddressBanInfo(rawBanInfo); + } - response.EnsureSuccessStatusCode(); + private const string BANS_LIST_URL = "{0}/bans/list?x={1}"; + private const string BANS_LIST_SIG = "list#{0}"; - return response.StatusCode == HttpStatusCode.NoContent; - } + public async Task<BanInfo[]> BanGetListAsync() { + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now)); + string sig = string.Format(BANS_LIST_SIG, now); - private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}"; - private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}"; + HttpRequestMessage request = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; - public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) { - userIdOrName ??= "0"; - remoteAddr ??= IPAddress.None; + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9'); - string remoteAddrStr = remoteAddr.ToString(); - string usingUserNameStr = usingUserName ? "1" : "0"; - string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr)); - string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr); + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream); + if(list is null || list.Length < 1) + return []; - HttpRequestMessage request = new(HttpMethod.Get, url) { - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sig) }, - }, - }; - - using HttpResponseMessage response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - using Stream stream = await response.Content.ReadAsStreamAsync(); - FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream); - if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired) - return null; - - return rawBanInfo.RemoteAddress is null or "::" - ? new FlashiiUserBanInfo(rawBanInfo) - : new FlashiiIPAddressBanInfo(rawBanInfo); - } - - private const string BANS_LIST_URL = "{0}/bans/list?x={1}"; - private const string BANS_LIST_SIG = "list#{0}"; - - public async Task<BanInfo[]> BanGetListAsync() { - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now)); - string sig = string.Format(BANS_LIST_SIG, now); - - HttpRequestMessage request = new(HttpMethod.Get, url) { - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sig) }, - }, - }; - - using HttpResponseMessage response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - using Stream stream = await response.Content.ReadAsStreamAsync(); - FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream); - if(list is null || list.Length < 1) - return []; - - return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => { - return (BanInfo)(b.RemoteAddress is null or "::" - ? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b)); - })]; - } + return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => { + return (BanInfo)(b.RemoteAddress is null or "::" + ? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b)); + })]; } } diff --git a/SharpChat.Flashii/FlashiiIPAddressBanInfo.cs b/SharpChat.Flashii/FlashiiIPAddressBanInfo.cs index 7bd6fc5..6c85b03 100644 --- a/SharpChat.Flashii/FlashiiIPAddressBanInfo.cs +++ b/SharpChat.Flashii/FlashiiIPAddressBanInfo.cs @@ -1,9 +1,9 @@ -using SharpChat.Bans; +using SharpChat.Bans; using System.Net; -namespace SharpChat.Flashii { - public class FlashiiIPAddressBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.IPAddress, rawBanInfo), IPAddressBanInfo { - public IPAddress Address { get; } = IPAddress.TryParse(rawBanInfo.RemoteAddress, out IPAddress? addr) && addr is not null ? addr : IPAddress.IPv6None; - public override string ToString() => Address.ToString(); - } +namespace SharpChat.Flashii; + +public class FlashiiIPAddressBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.IPAddress, rawBanInfo), IPAddressBanInfo { + public IPAddress Address { get; } = IPAddress.TryParse(rawBanInfo.RemoteAddress, out IPAddress? addr) && addr is not null ? addr : IPAddress.IPv6None; + public override string ToString() => Address.ToString(); } diff --git a/SharpChat.Flashii/FlashiiRawBanInfo.cs b/SharpChat.Flashii/FlashiiRawBanInfo.cs index 3263fde..5993341 100644 --- a/SharpChat.Flashii/FlashiiRawBanInfo.cs +++ b/SharpChat.Flashii/FlashiiRawBanInfo.cs @@ -1,29 +1,29 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; -namespace SharpChat.Flashii { - public class FlashiiRawBanInfo { - [JsonPropertyName("is_ban")] - public bool IsBanned { get; set; } +namespace SharpChat.Flashii; - [JsonPropertyName("user_id")] - public string? UserId { get; set; } +public class FlashiiRawBanInfo { + [JsonPropertyName("is_ban")] + public bool IsBanned { get; set; } - [JsonPropertyName("user_name")] - public string? UserName { get; set; } + [JsonPropertyName("user_id")] + public string? UserId { get; set; } - [JsonPropertyName("user_colour")] - public int UserColourRaw { get; set; } - public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); + [JsonPropertyName("user_name")] + public string? UserName { get; set; } - [JsonPropertyName("ip_addr")] - public string? RemoteAddress { get; set; } + [JsonPropertyName("user_colour")] + public int UserColourRaw { get; set; } + public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); - [JsonPropertyName("is_perma")] - public bool IsPermanent { get; set; } + [JsonPropertyName("ip_addr")] + public string? RemoteAddress { get; set; } - [JsonPropertyName("expires")] - public DateTimeOffset ExpiresAt { get; set; } + [JsonPropertyName("is_perma")] + public bool IsPermanent { get; set; } - public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; - } + [JsonPropertyName("expires")] + public DateTimeOffset ExpiresAt { get; set; } + + public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; } diff --git a/SharpChat.Flashii/FlashiiUserBanInfo.cs b/SharpChat.Flashii/FlashiiUserBanInfo.cs index 6124547..ffe7ea9 100644 --- a/SharpChat.Flashii/FlashiiUserBanInfo.cs +++ b/SharpChat.Flashii/FlashiiUserBanInfo.cs @@ -1,10 +1,10 @@ -using SharpChat.Bans; +using SharpChat.Bans; -namespace SharpChat.Flashii { - public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo { - public string UserId { get; } = rawBanInfo.UserId ?? string.Empty; - public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})"; - public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw); - public override string ToString() => UserName; - } +namespace SharpChat.Flashii; + +public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo { + public string UserId { get; } = rawBanInfo.UserId ?? string.Empty; + public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})"; + public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw); + public override string ToString() => UserName; } diff --git a/SharpChat.SockChat/S2CPacket.cs b/SharpChat.SockChat/S2CPacket.cs index f43f048..240d56b 100644 --- a/SharpChat.SockChat/S2CPacket.cs +++ b/SharpChat.SockChat/S2CPacket.cs @@ -1,5 +1,5 @@ -namespace SharpChat.SockChat { - public interface S2CPacket { - string Pack(); - } +namespace SharpChat.SockChat; + +public interface S2CPacket { + string Pack(); } diff --git a/SharpChat.SockChat/S2CPackets/AuthFailS2CPacket.cs b/SharpChat.SockChat/S2CPackets/AuthFailS2CPacket.cs index c6e9533..e9f0f89 100644 --- a/SharpChat.SockChat/S2CPackets/AuthFailS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/AuthFailS2CPacket.cs @@ -1,43 +1,43 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class AuthFailS2CPacket( - AuthFailS2CPacket.Reason reason, - DateTimeOffset? expiresAt = null - ) : S2CPacket { - public enum Reason { - AuthInvalid, - MaxSessions, - Banned, - Exception, +namespace SharpChat.SockChat.S2CPackets; + +public class AuthFailS2CPacket( + AuthFailS2CPacket.Reason reason, + DateTimeOffset? expiresAt = null +) : S2CPacket { + public enum Reason { + AuthInvalid, + MaxSessions, + Banned, + Exception, + } + + public string Pack() { + StringBuilder sb = new(); + + sb.Append("1\tn\t"); + + switch(reason) { + case Reason.AuthInvalid: + default: + sb.Append("authfail"); + break; + case Reason.Exception: + sb.Append("userfail"); + break; + case Reason.MaxSessions: + sb.Append("sockfail"); + break; + case Reason.Banned: + sb.Append("joinfail\t"); + if(expiresAt is null || expiresAt == DateTimeOffset.MaxValue) + sb.Append("-1"); + else + sb.Append(expiresAt.Value.ToUnixTimeSeconds()); + break; } - public string Pack() { - StringBuilder sb = new(); - - sb.Append("1\tn\t"); - - switch(reason) { - case Reason.AuthInvalid: - default: - sb.Append("authfail"); - break; - case Reason.Exception: - sb.Append("userfail"); - break; - case Reason.MaxSessions: - sb.Append("sockfail"); - break; - case Reason.Banned: - sb.Append("joinfail\t"); - if(expiresAt is null || expiresAt == DateTimeOffset.MaxValue) - sb.Append("-1"); - else - sb.Append(expiresAt.Value.ToUnixTimeSeconds()); - break; - } - - return sb.ToString(); - } + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/AuthSuccessS2CPacket.cs b/SharpChat.SockChat/S2CPackets/AuthSuccessS2CPacket.cs index 44bf7ce..caad0f8 100644 --- a/SharpChat.SockChat/S2CPackets/AuthSuccessS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/AuthSuccessS2CPacket.cs @@ -1,40 +1,40 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class AuthSuccessS2CPacket( - string userId, - string userName, - ColourInheritable userColour, - int userRank, - UserPermissions userPerms, - string channelName, - int maxMsgLength - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("1\ty\t"); - sb.Append(userId); - sb.Append('\t'); - sb.Append(userName); - sb.Append('\t'); - sb.Append(userColour); - sb.Append('\t'); - sb.Append(userRank); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); - sb.Append('\t'); - sb.Append(channelName); - sb.Append('\t'); - sb.Append(maxMsgLength); +public class AuthSuccessS2CPacket( + string userId, + string userName, + ColourInheritable userColour, + int userRank, + UserPermissions userPerms, + string channelName, + int maxMsgLength +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("1\ty\t"); + sb.Append(userId); + sb.Append('\t'); + sb.Append(userName); + sb.Append('\t'); + sb.Append(userColour); + sb.Append('\t'); + sb.Append(userRank); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + sb.Append('\t'); + sb.Append(channelName); + sb.Append('\t'); + sb.Append(maxMsgLength); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/BanListS2CPacket.cs b/SharpChat.SockChat/S2CPackets/BanListS2CPacket.cs index fb4fc6d..8f35c84 100644 --- a/SharpChat.SockChat/S2CPackets/BanListS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/BanListS2CPacket.cs @@ -1,29 +1,29 @@ using SharpChat.Bans; using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class BanListS2CPacket( - long msgId, - IEnumerable<BanListS2CPacket.Entry> entries - ) : S2CPacket { - public record Entry(BanKind type, string value); +namespace SharpChat.SockChat.S2CPackets; - public string Pack() { - StringBuilder sb = new(); +public class BanListS2CPacket( + long msgId, + IEnumerable<BanListS2CPacket.Entry> entries +) : S2CPacket { + public record Entry(BanKind type, string value); - sb.Append("2\t"); - sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); - sb.Append("\t-1\t0\fbanlist\f"); - sb.Append(string.Join(", ", entries.Select(entry => string.Format( - @"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>", - entry.type == BanKind.IPAddress ? "/unbanip" : "/unban", - entry.value - )))); - sb.Append('\t'); - sb.Append(msgId); - sb.Append("\t10010"); + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("2\t"); + sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); + sb.Append("\t-1\t0\fbanlist\f"); + sb.Append(string.Join(", ", entries.Select(entry => string.Format( + @"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>", + entry.type == BanKind.IPAddress ? "/unbanip" : "/unban", + entry.value + )))); + sb.Append('\t'); + sb.Append(msgId); + sb.Append("\t10010"); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ChannelCreateS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ChannelCreateS2CPacket.cs index 3618ed5..498d5e3 100644 --- a/SharpChat.SockChat/S2CPackets/ChannelCreateS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ChannelCreateS2CPacket.cs @@ -1,22 +1,22 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ChannelCreateS2CPacket( - string name, - bool hasPassword, - bool isTemporary - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("4\t0\t"); - sb.Append(name); - sb.Append('\t'); - sb.Append(hasPassword ? '1' : '0'); - sb.Append('\t'); - sb.Append(isTemporary ? '1' : '0'); +public class ChannelCreateS2CPacket( + string name, + bool hasPassword, + bool isTemporary +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("4\t0\t"); + sb.Append(name); + sb.Append('\t'); + sb.Append(hasPassword ? '1' : '0'); + sb.Append('\t'); + sb.Append(isTemporary ? '1' : '0'); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ChannelDeleteS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ChannelDeleteS2CPacket.cs index 9bcaa1b..b185d87 100644 --- a/SharpChat.SockChat/S2CPackets/ChannelDeleteS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ChannelDeleteS2CPacket.cs @@ -1,16 +1,16 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ChannelDeleteS2CPacket( - string channelName - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("4\t2\t"); - sb.Append(channelName); +public class ChannelDeleteS2CPacket( + string channelName +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("4\t2\t"); + sb.Append(channelName); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ChannelUpdateS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ChannelUpdateS2CPacket.cs index 4e7a6ed..25547fb 100644 --- a/SharpChat.SockChat/S2CPackets/ChannelUpdateS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ChannelUpdateS2CPacket.cs @@ -1,25 +1,25 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ChannelUpdateS2CPacket( - string previousName, - string newName, - bool hasPassword, - bool isTemporary - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("4\t1\t"); - sb.Append(previousName); - sb.Append('\t'); - sb.Append(newName); - sb.Append('\t'); - sb.Append(hasPassword ? '1' : '0'); - sb.Append('\t'); - sb.Append(isTemporary ? '1' : '0'); +public class ChannelUpdateS2CPacket( + string previousName, + string newName, + bool hasPassword, + bool isTemporary +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("4\t1\t"); + sb.Append(previousName); + sb.Append('\t'); + sb.Append(newName); + sb.Append('\t'); + sb.Append(hasPassword ? '1' : '0'); + sb.Append('\t'); + sb.Append(isTemporary ? '1' : '0'); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ChatMessageAddS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ChatMessageAddS2CPacket.cs index 4a1952d..09fbb56 100644 --- a/SharpChat.SockChat/S2CPackets/ChatMessageAddS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ChatMessageAddS2CPacket.cs @@ -1,48 +1,48 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ChatMessageAddS2CPacket( - long msgId, - DateTimeOffset created, - string userId, - string text, - bool isAction, - bool isPrivate - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("2\t"); +public class ChatMessageAddS2CPacket( + long msgId, + DateTimeOffset created, + string userId, + string text, + bool isAction, + bool isPrivate +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - sb.Append(created.ToUnixTimeSeconds()); - sb.Append('\t'); + sb.Append("2\t"); - sb.Append(userId); - sb.Append('\t'); + sb.Append(created.ToUnixTimeSeconds()); + sb.Append('\t'); - if(isAction) - sb.Append("<i>"); + sb.Append(userId); + sb.Append('\t'); - sb.Append( - text.Replace("<", "<") - .Replace(">", ">") - .Replace("\n", " <br/> ") - .Replace("\t", " ") - ); + if(isAction) + sb.Append("<i>"); - if(isAction) - sb.Append("</i>"); + sb.Append( + text.Replace("<", "<") + .Replace(">", ">") + .Replace("\n", " <br/> ") + .Replace("\t", " ") + ); - sb.Append('\t'); - sb.Append(msgId); - sb.AppendFormat( - "\t1{0}0{1}{2}", - isAction ? '1' : '0', - isAction ? '0' : '1', - isPrivate ? '1' : '0' - ); + if(isAction) + sb.Append("</i>"); - return sb.ToString(); - } + sb.Append('\t'); + sb.Append(msgId); + sb.AppendFormat( + "\t1{0}0{1}{2}", + isAction ? '1' : '0', + isAction ? '0' : '1', + isPrivate ? '1' : '0' + ); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ChatMessageDeleteS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ChatMessageDeleteS2CPacket.cs index 79a01a2..caf2573 100644 --- a/SharpChat.SockChat/S2CPackets/ChatMessageDeleteS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ChatMessageDeleteS2CPacket.cs @@ -1,14 +1,14 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ChatMessageDeleteS2CPacket(long eventId) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("6\t"); - sb.Append(eventId); +public class ChatMessageDeleteS2CPacket(long eventId) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("6\t"); + sb.Append(eventId); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/CommandResponseS2CPacket.cs b/SharpChat.SockChat/S2CPackets/CommandResponseS2CPacket.cs index bc671f1..fa72bb7 100644 --- a/SharpChat.SockChat/S2CPackets/CommandResponseS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/CommandResponseS2CPacket.cs @@ -1,85 +1,85 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class CommandResponseS2CPacket( - long msgId, - string stringId, - bool isError = true, - params object[] args - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - if(stringId == LCR.WELCOME) { - sb.Append("7\t1\t"); - sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); - sb.Append("\t-1\tChatBot\tinherit\t\t"); - } else { - sb.Append("2\t"); - sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); - sb.Append("\t-1\t"); +public class CommandResponseS2CPacket( + long msgId, + string stringId, + bool isError = true, + params object[] args +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); + + if(stringId == LCR.WELCOME) { + sb.Append("7\t1\t"); + sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); + sb.Append("\t-1\tChatBot\tinherit\t\t"); + } else { + sb.Append("2\t"); + sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); + sb.Append("\t-1\t"); + } + + sb.Append(isError ? '1' : '0'); + sb.Append('\f'); + sb.Append(stringId == LCR.WELCOME ? LCR.BROADCAST : stringId); + + if(args.Length > 0) + foreach(object arg in args) { + sb.Append('\f'); + sb.Append(arg); } - sb.Append(isError ? '1' : '0'); - sb.Append('\f'); - sb.Append(stringId == LCR.WELCOME ? LCR.BROADCAST : stringId); + sb.Append('\t'); - if(args.Length > 0) - foreach(object arg in args) { - sb.Append('\f'); - sb.Append(arg); - } + if(stringId == LCR.WELCOME) { + sb.Append(stringId); + sb.Append("\t0"); + } else + sb.Append(msgId); - sb.Append('\t'); + sb.Append("\t10010"); + /*sb.AppendFormat( + "\t1{0}0{1}{2}", + Flags.HasFlag(ChatMessageFlags.Action) ? '1' : '0', + Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1', + Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0' + );*/ - if(stringId == LCR.WELCOME) { - sb.Append(stringId); - sb.Append("\t0"); - } else - sb.Append(msgId); - - sb.Append("\t10010"); - /*sb.AppendFormat( - "\t1{0}0{1}{2}", - Flags.HasFlag(ChatMessageFlags.Action) ? '1' : '0', - Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1', - Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0' - );*/ - - return sb.ToString(); - } - } - - // Abbreviated class name because otherwise shit gets wide - public static class LCR { - public const string GENERIC_ERROR = "generr"; - public const string COMMAND_NOT_FOUND = "nocmd"; - public const string COMMAND_NOT_ALLOWED = "cmdna"; - public const string COMMAND_FORMAT_ERROR = "cmderr"; - public const string WELCOME = "welcome"; - public const string BROADCAST = "say"; - public const string IP_ADDRESS = "ipaddr"; - public const string USER_NOT_FOUND = "usernf"; - public const string NAME_IN_USE = "nameinuse"; - public const string CHANNEL_INSUFFICIENT_HIERARCHY = "ipchan"; - public const string CHANNEL_INVALID_PASSWORD = "ipwchan"; - public const string CHANNEL_NOT_FOUND = "nochan"; - public const string CHANNEL_ALREADY_EXISTS = "nischan"; - public const string CHANNEL_NAME_INVALID = "inchan"; - public const string CHANNEL_CREATED = "crchan"; - public const string CHANNEL_DELETE_FAILED = "ndchan"; - public const string CHANNEL_DELETED = "delchan"; - public const string CHANNEL_PASSWORD_CHANGED = "cpwdchan"; - public const string CHANNEL_HIERARCHY_CHANGED = "cprivchan"; - public const string USERS_LISTING_ERROR = "whoerr"; - public const string USERS_LISTING_CHANNEL = "whochan"; - public const string USERS_LISTING_SERVER = "who"; - public const string INSUFFICIENT_HIERARCHY = "rankerr"; - public const string MESSAGE_DELETE_ERROR = "delerr"; - public const string KICK_NOT_ALLOWED = "kickna"; - public const string USER_NOT_BANNED = "notban"; - public const string USER_UNBANNED = "unban"; - public const string FLOOD_WARN = "flwarn"; - public const string NICKNAME_CHANGE = "nick"; + return sb.ToString(); } } + +// Abbreviated class name because otherwise shit gets wide +public static class LCR { + public const string GENERIC_ERROR = "generr"; + public const string COMMAND_NOT_FOUND = "nocmd"; + public const string COMMAND_NOT_ALLOWED = "cmdna"; + public const string COMMAND_FORMAT_ERROR = "cmderr"; + public const string WELCOME = "welcome"; + public const string BROADCAST = "say"; + public const string IP_ADDRESS = "ipaddr"; + public const string USER_NOT_FOUND = "usernf"; + public const string NAME_IN_USE = "nameinuse"; + public const string CHANNEL_INSUFFICIENT_HIERARCHY = "ipchan"; + public const string CHANNEL_INVALID_PASSWORD = "ipwchan"; + public const string CHANNEL_NOT_FOUND = "nochan"; + public const string CHANNEL_ALREADY_EXISTS = "nischan"; + public const string CHANNEL_NAME_INVALID = "inchan"; + public const string CHANNEL_CREATED = "crchan"; + public const string CHANNEL_DELETE_FAILED = "ndchan"; + public const string CHANNEL_DELETED = "delchan"; + public const string CHANNEL_PASSWORD_CHANGED = "cpwdchan"; + public const string CHANNEL_HIERARCHY_CHANGED = "cprivchan"; + public const string USERS_LISTING_ERROR = "whoerr"; + public const string USERS_LISTING_CHANNEL = "whochan"; + public const string USERS_LISTING_SERVER = "who"; + public const string INSUFFICIENT_HIERARCHY = "rankerr"; + public const string MESSAGE_DELETE_ERROR = "delerr"; + public const string KICK_NOT_ALLOWED = "kickna"; + public const string USER_NOT_BANNED = "notban"; + public const string USER_UNBANNED = "unban"; + public const string FLOOD_WARN = "flwarn"; + public const string NICKNAME_CHANGE = "nick"; +} diff --git a/SharpChat.SockChat/S2CPackets/ContextChannelsS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ContextChannelsS2CPacket.cs index 294da5d..245bf40 100644 --- a/SharpChat.SockChat/S2CPackets/ContextChannelsS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ContextChannelsS2CPacket.cs @@ -1,25 +1,25 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ContextChannelsS2CPacket(IEnumerable<ContextChannelsS2CPacket.Entry> entries) : S2CPacket { - public record Entry(string name, bool hasPassword, bool isTemporary); +namespace SharpChat.SockChat.S2CPackets; - public string Pack() { - StringBuilder sb = new(); +public class ContextChannelsS2CPacket(IEnumerable<ContextChannelsS2CPacket.Entry> entries) : S2CPacket { + public record Entry(string name, bool hasPassword, bool isTemporary); - sb.Append("7\t2\t"); - sb.Append(entries.Count()); + public string Pack() { + StringBuilder sb = new(); - foreach(Entry entry in entries) { - sb.Append('\t'); - sb.Append(entry.name); - sb.Append('\t'); - sb.Append(entry.hasPassword ? '1' : '0'); - sb.Append('\t'); - sb.Append(entry.isTemporary ? '1' : '0'); - } + sb.Append("7\t2\t"); + sb.Append(entries.Count()); - return sb.ToString(); + foreach(Entry entry in entries) { + sb.Append('\t'); + sb.Append(entry.name); + sb.Append('\t'); + sb.Append(entry.hasPassword ? '1' : '0'); + sb.Append('\t'); + sb.Append(entry.isTemporary ? '1' : '0'); } + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ContextClearS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ContextClearS2CPacket.cs index 1dfb7f3..a60b931 100644 --- a/SharpChat.SockChat/S2CPackets/ContextClearS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ContextClearS2CPacket.cs @@ -1,22 +1,22 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ContextClearS2CPacket(ContextClearS2CPacket.Mode mode) : S2CPacket { - public enum Mode { - Messages = 0, - Users = 1, - Channels = 2, - MessagesUsers = 3, - MessagesUsersChannels = 4, - } +namespace SharpChat.SockChat.S2CPackets; - public string Pack() { - StringBuilder sb = new(); +public class ContextClearS2CPacket(ContextClearS2CPacket.Mode mode) : S2CPacket { + public enum Mode { + Messages = 0, + Users = 1, + Channels = 2, + MessagesUsers = 3, + MessagesUsersChannels = 4, + } - sb.Append("8\t"); - sb.Append((int)mode); + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("8\t"); + sb.Append((int)mode); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ContextUsersS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ContextUsersS2CPacket.cs index 0390c41..90e723a 100644 --- a/SharpChat.SockChat/S2CPackets/ContextUsersS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ContextUsersS2CPacket.cs @@ -1,37 +1,37 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket { - public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible); +namespace SharpChat.SockChat.S2CPackets; - public string Pack() { - StringBuilder sb = new(); +public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket { + public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible); - sb.Append("7\t0\t"); - sb.Append(entries.Count()); + public string Pack() { + StringBuilder sb = new(); - foreach(Entry entry in entries) { - sb.Append('\t'); - sb.Append(entry.id); - sb.Append('\t'); - sb.Append(entry.name); - sb.Append('\t'); - sb.Append(entry.colour); - sb.Append('\t'); - sb.Append(entry.rank); - sb.Append(' '); - sb.Append(entry.perms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(entry.perms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(entry.perms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(entry.perms.HasFlag(UserPermissions.CreateChannel) ? (entry.perms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); - sb.Append('\t'); - sb.Append(entry.visible ? '1' : '0'); - } + sb.Append("7\t0\t"); + sb.Append(entries.Count()); - return sb.ToString(); + foreach(Entry entry in entries) { + sb.Append('\t'); + sb.Append(entry.id); + sb.Append('\t'); + sb.Append(entry.name); + sb.Append('\t'); + sb.Append(entry.colour); + sb.Append('\t'); + sb.Append(entry.rank); + sb.Append(' '); + sb.Append(entry.perms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(entry.perms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(entry.perms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(entry.perms.HasFlag(UserPermissions.CreateChannel) ? (entry.perms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + sb.Append('\t'); + sb.Append(entry.visible ? '1' : '0'); } + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/ForceDisconnectS2CPacket.cs b/SharpChat.SockChat/S2CPackets/ForceDisconnectS2CPacket.cs index a714c9b..1b7f9a9 100644 --- a/SharpChat.SockChat/S2CPackets/ForceDisconnectS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/ForceDisconnectS2CPacket.cs @@ -1,22 +1,22 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ForceDisconnectS2CPacket(DateTimeOffset? expires = null) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("9\t"); +public class ForceDisconnectS2CPacket(DateTimeOffset? expires = null) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - if(expires.HasValue && expires.Value > DateTimeOffset.UtcNow) { - sb.Append("1\t"); - if(expires.Value < DateTimeOffset.MaxValue) - sb.Append(expires.Value.ToUnixTimeSeconds()); - else - sb.Append("-1"); - } else - sb.Append('0'); - - return sb.ToString(); - } + sb.Append("9\t"); + + if(expires.HasValue && expires.Value > DateTimeOffset.UtcNow) { + sb.Append("1\t"); + if(expires.Value < DateTimeOffset.MaxValue) + sb.Append(expires.Value.ToUnixTimeSeconds()); + else + sb.Append("-1"); + } else + sb.Append('0'); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/PongS2CPacket.cs b/SharpChat.SockChat/S2CPackets/PongS2CPacket.cs index 08b667a..082d674 100644 --- a/SharpChat.SockChat/S2CPackets/PongS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/PongS2CPacket.cs @@ -1,7 +1,7 @@ -namespace SharpChat.SockChat.S2CPackets { - public class PongS2CPacket : S2CPacket { - public string Pack() { - return "0\tpong"; - } +namespace SharpChat.SockChat.S2CPackets; + +public class PongS2CPacket : S2CPacket { + public string Pack() { + return "0\tpong"; } } diff --git a/SharpChat.SockChat/S2CPackets/UserChannelForceJoinS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserChannelForceJoinS2CPacket.cs index 18d9507..b9a628c 100644 --- a/SharpChat.SockChat/S2CPackets/UserChannelForceJoinS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserChannelForceJoinS2CPacket.cs @@ -1,14 +1,14 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserChannelForceJoinS2CPacket(string channelName) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("5\t2\t"); - sb.Append(channelName); +public class UserChannelForceJoinS2CPacket(string channelName) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("5\t2\t"); + sb.Append(channelName); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/UserChannelJoinS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserChannelJoinS2CPacket.cs index 7bc00ac..081a9de 100644 --- a/SharpChat.SockChat/S2CPackets/UserChannelJoinS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserChannelJoinS2CPacket.cs @@ -1,37 +1,37 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserChannelJoinS2CPacket( - long msgId, - string userId, - string userName, - ColourInheritable userColour, - int userRank, - UserPermissions userPerms - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("5\t0\t"); - sb.Append(userId); - sb.Append('\t'); - sb.Append(userName); - sb.Append('\t'); - sb.Append(userColour); - sb.Append('\t'); - sb.Append(userRank); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); - sb.Append('\t'); - sb.Append(msgId); +public class UserChannelJoinS2CPacket( + long msgId, + string userId, + string userName, + ColourInheritable userColour, + int userRank, + UserPermissions userPerms +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("5\t0\t"); + sb.Append(userId); + sb.Append('\t'); + sb.Append(userName); + sb.Append('\t'); + sb.Append(userColour); + sb.Append('\t'); + sb.Append(userRank); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + sb.Append('\t'); + sb.Append(msgId); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/UserChannelLeaveS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserChannelLeaveS2CPacket.cs index 36af72c..1910b9e 100644 --- a/SharpChat.SockChat/S2CPackets/UserChannelLeaveS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserChannelLeaveS2CPacket.cs @@ -1,16 +1,16 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("5\t1\t"); - sb.Append(userId); - sb.Append('\t'); - sb.Append(msgId); +public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("5\t1\t"); + sb.Append(userId); + sb.Append('\t'); + sb.Append(msgId); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/UserConnectS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserConnectS2CPacket.cs index 468e1aa..c495663 100644 --- a/SharpChat.SockChat/S2CPackets/UserConnectS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserConnectS2CPacket.cs @@ -1,40 +1,40 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserConnectS2CPacket( - long msgId, - DateTimeOffset joined, - string userId, - string userName, - ColourInheritable userColour, - int userRank, - UserPermissions userPerms - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("1\t"); - sb.Append(joined.ToUnixTimeSeconds()); - sb.Append('\t'); - sb.Append(userId); - sb.Append('\t'); - sb.Append(userName); - sb.Append('\t'); - sb.Append(userColour); - sb.Append('\t'); - sb.Append(userRank); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); - sb.Append('\t'); - sb.Append(msgId); +public class UserConnectS2CPacket( + long msgId, + DateTimeOffset joined, + string userId, + string userName, + ColourInheritable userColour, + int userRank, + UserPermissions userPerms +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("1\t"); + sb.Append(joined.ToUnixTimeSeconds()); + sb.Append('\t'); + sb.Append(userId); + sb.Append('\t'); + sb.Append(userName); + sb.Append('\t'); + sb.Append(userColour); + sb.Append('\t'); + sb.Append(userRank); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + sb.Append('\t'); + sb.Append(msgId); + + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/UserDisconnectS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserDisconnectS2CPacket.cs index 18d4a69..cd6f0a9 100644 --- a/SharpChat.SockChat/S2CPackets/UserDisconnectS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserDisconnectS2CPacket.cs @@ -1,51 +1,51 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserDisconnectS2CPacket( - long msgId, - DateTimeOffset disconnected, - string userId, - string userName, - UserDisconnectS2CPacket.Reason reason - ) : S2CPacket { - public enum Reason { - Leave, - TimeOut, - Kicked, - Flood, +namespace SharpChat.SockChat.S2CPackets; + +public class UserDisconnectS2CPacket( + long msgId, + DateTimeOffset disconnected, + string userId, + string userName, + UserDisconnectS2CPacket.Reason reason +) : S2CPacket { + public enum Reason { + Leave, + TimeOut, + Kicked, + Flood, + } + + public string Pack() { + StringBuilder sb = new(); + + sb.Append("3\t"); + sb.Append(userId); + sb.Append('\t'); + sb.Append(userName); + sb.Append('\t'); + + switch(reason) { + case Reason.Leave: + default: + sb.Append("leave"); + break; + case Reason.TimeOut: + sb.Append("timeout"); + break; + case Reason.Kicked: + sb.Append("kick"); + break; + case Reason.Flood: + sb.Append("flood"); + break; } - public string Pack() { - StringBuilder sb = new(); + sb.Append('\t'); + sb.Append(disconnected.ToUnixTimeSeconds()); + sb.Append('\t'); + sb.Append(msgId); - sb.Append("3\t"); - sb.Append(userId); - sb.Append('\t'); - sb.Append(userName); - sb.Append('\t'); - - switch(reason) { - case Reason.Leave: - default: - sb.Append("leave"); - break; - case Reason.TimeOut: - sb.Append("timeout"); - break; - case Reason.Kicked: - sb.Append("kick"); - break; - case Reason.Flood: - sb.Append("flood"); - break; - } - - sb.Append('\t'); - sb.Append(disconnected.ToUnixTimeSeconds()); - sb.Append('\t'); - sb.Append(msgId); - - return sb.ToString(); - } + return sb.ToString(); } } diff --git a/SharpChat.SockChat/S2CPackets/UserUpdateS2CPacket.cs b/SharpChat.SockChat/S2CPackets/UserUpdateS2CPacket.cs index 1e22325..2dc6591 100644 --- a/SharpChat.SockChat/S2CPackets/UserUpdateS2CPacket.cs +++ b/SharpChat.SockChat/S2CPackets/UserUpdateS2CPacket.cs @@ -1,34 +1,34 @@ using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class UserUpdateS2CPacket( - string userId, - string userName, - ColourInheritable userColour, - int userRank, - UserPermissions userPerms - ) : S2CPacket { - public string Pack() { - StringBuilder sb = new(); +namespace SharpChat.SockChat.S2CPackets; - sb.Append("10\t"); - sb.Append(userId); - sb.Append('\t'); - sb.Append(userName); - sb.Append('\t'); - sb.Append(userColour); - sb.Append('\t'); - sb.Append(userRank); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); +public class UserUpdateS2CPacket( + string userId, + string userName, + ColourInheritable userColour, + int userRank, + UserPermissions userPerms +) : S2CPacket { + public string Pack() { + StringBuilder sb = new(); - return sb.ToString(); - } + sb.Append("10\t"); + sb.Append(userId); + sb.Append('\t'); + sb.Append(userName); + sb.Append('\t'); + sb.Append(userColour); + sb.Append('\t'); + sb.Append(userRank); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + + return sb.ToString(); } } diff --git a/SharpChat/C2SPacketHandler.cs b/SharpChat/C2SPacketHandler.cs index d90cfc6..6c6cd27 100644 --- a/SharpChat/C2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandler.cs @@ -1,6 +1,6 @@ -namespace SharpChat { - public interface C2SPacketHandler { - bool IsMatch(C2SPacketHandlerContext ctx); - Task Handle(C2SPacketHandlerContext ctx); - } +namespace SharpChat; + +public interface C2SPacketHandler { + bool IsMatch(C2SPacketHandlerContext ctx); + Task Handle(C2SPacketHandlerContext ctx); } diff --git a/SharpChat/C2SPacketHandlerContext.cs b/SharpChat/C2SPacketHandlerContext.cs index c5eacd8..feb008f 100644 --- a/SharpChat/C2SPacketHandlerContext.cs +++ b/SharpChat/C2SPacketHandlerContext.cs @@ -1,19 +1,19 @@ -namespace SharpChat { - public class C2SPacketHandlerContext( - string text, - Context chat, - Connection connection - ) { - public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text)); - public Context Chat { get; } = chat ?? throw new ArgumentNullException(nameof(chat)); - public Connection Connection { get; } = connection ?? throw new ArgumentNullException(nameof(connection)); +namespace SharpChat; - public bool CheckPacketId(string packetId) { - return Text == packetId || Text.StartsWith(packetId + '\t'); - } +public class C2SPacketHandlerContext( + string text, + Context chat, + Connection connection +) { + public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text)); + public Context Chat { get; } = chat ?? throw new ArgumentNullException(nameof(chat)); + public Connection Connection { get; } = connection ?? throw new ArgumentNullException(nameof(connection)); - public string[] SplitText(int expect) { - return Text.Split('\t', expect + 1); - } + public bool CheckPacketId(string packetId) { + return Text == packetId || Text.StartsWith(packetId + '\t'); + } + + public string[] SplitText(int expect) { + return Text.Split('\t', expect + 1); } } diff --git a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs index 998f8f8..4ec1479 100644 --- a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs @@ -3,110 +3,110 @@ using SharpChat.Bans; using SharpChat.Configuration; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.C2SPacketHandlers { - public class AuthC2SPacketHandler( - AuthClient authClient, - BansClient bansClient, - Channel defaultChannel, - CachedValue<int> maxMsgLength, - CachedValue<int> maxConns - ) : C2SPacketHandler { - private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); - private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); - private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns)); +namespace SharpChat.C2SPacketHandlers; - public bool IsMatch(C2SPacketHandlerContext ctx) { - return ctx.CheckPacketId("1"); +public class AuthC2SPacketHandler( + AuthClient authClient, + BansClient bansClient, + Channel defaultChannel, + CachedValue<int> maxMsgLength, + CachedValue<int> maxConns +) : C2SPacketHandler { + private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); + private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); + private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns)); + + public bool IsMatch(C2SPacketHandlerContext ctx) { + return ctx.CheckPacketId("1"); + } + + public async Task Handle(C2SPacketHandlerContext ctx) { + string[] args = ctx.SplitText(3); + + string? authMethod = args.ElementAtOrDefault(1); + string? authToken = args.ElementAtOrDefault(2); + + if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) { + await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); + ctx.Connection.Dispose(); + return; } - public async Task Handle(C2SPacketHandlerContext ctx) { - string[] args = ctx.SplitText(3); + if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { + string[] tokenParts = authToken.Split(':', 2); + authMethod = tokenParts[0]; + authToken = tokenParts[1]; + } - string? authMethod = args.ElementAtOrDefault(1); - string? authToken = args.ElementAtOrDefault(2); + try { + AuthResult authResult = await authClient.AuthVerifyAsync( + ctx.Connection.RemoteAddress, + authMethod, + authToken + ); - if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) { - await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); + BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress); + if(banInfo is not null) { + Logger.Write($"<{ctx.Connection.Id}> User is banned."); + await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt)); ctx.Connection.Dispose(); return; } - if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { - string[] tokenParts = authToken.Split(':', 2); - authMethod = tokenParts[0]; - authToken = tokenParts[1]; - } - + await ctx.Chat.ContextAccess.WaitAsync(); try { - AuthResult authResult = await authClient.AuthVerifyAsync( - ctx.Connection.RemoteAddress, - authMethod, - authToken - ); + User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId); - BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress); - if(banInfo is not null) { - Logger.Write($"<{ctx.Connection.Id}> User is banned."); - await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt)); + if(user == null) + user = new User( + authResult.UserId, + authResult.UserName ?? $"({authResult.UserId})", + authResult.UserColour, + authResult.UserRank, + authResult.UserPermissions + ); + else + await ctx.Chat.UpdateUser( + user, + userName: authResult.UserName ?? $"({authResult.UserId})", + colour: authResult.UserColour, + rank: authResult.UserRank, + perms: authResult.UserPermissions + ); + + // Enforce a maximum amount of connections per user + if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { + await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); ctx.Connection.Dispose(); return; } - await ctx.Chat.ContextAccess.WaitAsync(); - try { - User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId); + ctx.Connection.BumpPing(); + ctx.Connection.User = user; + await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!")); - if(user == null) - user = new User( - authResult.UserId, - authResult.UserName ?? $"({authResult.UserId})", - authResult.UserColour, - authResult.UserRank, - authResult.UserPermissions - ); - else - await ctx.Chat.UpdateUser( - user, - userName: authResult.UserName ?? $"({authResult.UserId})", - colour: authResult.UserColour, - rank: authResult.UserRank, - perms: authResult.UserPermissions - ); + if(File.Exists("welcome.txt")) { + IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); + string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); - // Enforce a maximum amount of connections per user - if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { - await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); - ctx.Connection.Dispose(); - return; - } - - ctx.Connection.BumpPing(); - ctx.Connection.User = user; - await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!")); - - if(File.Exists("welcome.txt")) { - IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); - string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); - - if(!string.IsNullOrWhiteSpace(line)) - await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); - } - - await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); - } finally { - ctx.Chat.ContextAccess.Release(); + if(!string.IsNullOrWhiteSpace(line)) + await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); } - } catch(AuthFailedException ex) { - Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); - await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); - ctx.Connection.Dispose(); - throw; - } catch(Exception ex) { - Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); - await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception)); - ctx.Connection.Dispose(); - throw; + + await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); + } finally { + ctx.Chat.ContextAccess.Release(); } + } catch(AuthFailedException ex) { + Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); + await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); + ctx.Connection.Dispose(); + throw; + } catch(Exception ex) { + Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); + await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception)); + ctx.Connection.Dispose(); + throw; } } } diff --git a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs index 8739a57..c0e658a 100644 --- a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs @@ -2,39 +2,39 @@ using SharpChat.Auth; using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat.C2SPacketHandlers { - public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler { - private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); - private DateTimeOffset LastBump = DateTimeOffset.MinValue; +namespace SharpChat.C2SPacketHandlers; - public bool IsMatch(C2SPacketHandlerContext ctx) { - return ctx.CheckPacketId("0"); - } +public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler { + private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); + private DateTimeOffset LastBump = DateTimeOffset.MinValue; - public async Task Handle(C2SPacketHandlerContext ctx) { - string[] parts = ctx.SplitText(2); + public bool IsMatch(C2SPacketHandlerContext ctx) { + return ctx.CheckPacketId("0"); + } - if(!int.TryParse(parts.FirstOrDefault(), out int pTime)) - return; + public async Task Handle(C2SPacketHandlerContext ctx) { + string[] parts = ctx.SplitText(2); - ctx.Connection.BumpPing(); - await ctx.Connection.Send(new PongS2CPacket()); + if(!int.TryParse(parts.FirstOrDefault(), out int pTime)) + return; - ctx.Chat.ContextAccess.Wait(); - try { - if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { - (IPAddress, string)[] bumpList = [.. ctx.Chat.Users - .Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u)) - .Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))]; + ctx.Connection.BumpPing(); + await ctx.Connection.Send(new PongS2CPacket()); - if(bumpList.Length > 0) - await authClient.AuthBumpUsersOnlineAsync(bumpList); + ctx.Chat.ContextAccess.Wait(); + try { + if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { + (IPAddress, string)[] bumpList = [.. ctx.Chat.Users + .Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u)) + .Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))]; - LastBump = DateTimeOffset.UtcNow; - } - } finally { - ctx.Chat.ContextAccess.Release(); + if(bumpList.Length > 0) + await authClient.AuthBumpUsersOnlineAsync(bumpList); + + LastBump = DateTimeOffset.UtcNow; } + } finally { + ctx.Chat.ContextAccess.Release(); } } } diff --git a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs index a21d3b5..59930b8 100644 --- a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs @@ -4,86 +4,86 @@ using SharpChat.Snowflake; using System.Globalization; using System.Text; -namespace SharpChat.C2SPacketHandlers { - public class SendMessageC2SPacketHandler( - RandomSnowflake randomSnowflake, - CachedValue<int> maxMsgLength - ) : C2SPacketHandler { - private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); +namespace SharpChat.C2SPacketHandlers; - private List<ClientCommand> Commands { get; } = []; +public class SendMessageC2SPacketHandler( + RandomSnowflake randomSnowflake, + CachedValue<int> maxMsgLength +) : C2SPacketHandler { + private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); - public void AddCommand(ClientCommand command) { - Commands.Add(command ?? throw new ArgumentNullException(nameof(command))); - } + private List<ClientCommand> Commands { get; } = []; - public void AddCommands(IEnumerable<ClientCommand> commands) { - Commands.AddRange(commands ?? throw new ArgumentNullException(nameof(commands))); - } + public void AddCommand(ClientCommand command) { + Commands.Add(command ?? throw new ArgumentNullException(nameof(command))); + } - public bool IsMatch(C2SPacketHandlerContext ctx) { - return ctx.CheckPacketId("2"); - } + public void AddCommands(IEnumerable<ClientCommand> commands) { + Commands.AddRange(commands ?? throw new ArgumentNullException(nameof(commands))); + } - public async Task Handle(C2SPacketHandlerContext ctx) { - string[] args = ctx.SplitText(3); + public bool IsMatch(C2SPacketHandlerContext ctx) { + return ctx.CheckPacketId("2"); + } - User? user = ctx.Connection.User; - string? messageText = args.ElementAtOrDefault(2); + public async Task Handle(C2SPacketHandlerContext ctx) { + string[] args = ctx.SplitText(3); - if(user == null || !user.Can(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText)) + User? user = ctx.Connection.User; + string? messageText = args.ElementAtOrDefault(2); + + if(user == null || !user.Can(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText)) + return; + + // Extra validation step, not necessary at all but enforces proper formatting in SCv1. + if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString()) + return; + + ctx.Chat.ContextAccess.Wait(); + try { + if(!ctx.Chat.UserLastChannel.TryGetValue(user.UserId, out Channel? channel) + && (channel is null || !ctx.Chat.IsInChannel(user, channel))) return; - // Extra validation step, not necessary at all but enforces proper formatting in SCv1. - if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString()) - return; + if(user.Status != UserStatus.Online) + await ctx.Chat.UpdateUser(user, status: UserStatus.Online); - ctx.Chat.ContextAccess.Wait(); - try { - if(!ctx.Chat.UserLastChannel.TryGetValue(user.UserId, out Channel? channel) - && (channel is null || !ctx.Chat.IsInChannel(user, channel))) - return; + int maxMsgLength = MaxMessageLength; + StringInfo messageTextInfo = new(messageText); + if(Encoding.UTF8.GetByteCount(messageText) > (maxMsgLength * 10) + || messageTextInfo.LengthInTextElements > maxMsgLength) + messageText = messageTextInfo.SubstringByTextElements(0, Math.Min(messageTextInfo.LengthInTextElements, maxMsgLength)); - if(user.Status != UserStatus.Online) - await ctx.Chat.UpdateUser(user, status: UserStatus.Online); - - int maxMsgLength = MaxMessageLength; - StringInfo messageTextInfo = new(messageText); - if(Encoding.UTF8.GetByteCount(messageText) > (maxMsgLength * 10) - || messageTextInfo.LengthInTextElements > maxMsgLength) - messageText = messageTextInfo.SubstringByTextElements(0, Math.Min(messageTextInfo.LengthInTextElements, maxMsgLength)); - - messageText = messageText.Trim(); + messageText = messageText.Trim(); #if DEBUG - Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}"); + Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}"); #endif - if(messageText.StartsWith('/')) { - ClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel); - foreach(ClientCommand cmd in Commands) - if(cmd.IsMatch(context)) { - await cmd.Dispatch(context); - return; - } - } - - await ctx.Chat.DispatchEvent(new MessageCreateEvent( - randomSnowflake.Next(), - channel.Name, - user.UserId, - user.UserName, - user.Colour, - user.Rank, - user.NickName, - user.Permissions, - DateTimeOffset.Now, - messageText, - false, false, false - )); - } finally { - ctx.Chat.ContextAccess.Release(); + if(messageText.StartsWith('/')) { + ClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel); + foreach(ClientCommand cmd in Commands) + if(cmd.IsMatch(context)) { + await cmd.Dispatch(context); + return; + } } + + await ctx.Chat.DispatchEvent(new MessageCreateEvent( + randomSnowflake.Next(), + channel.Name, + user.UserId, + user.UserName, + user.Colour, + user.Rank, + user.NickName, + user.Permissions, + DateTimeOffset.Now, + messageText, + false, false, false + )); + } finally { + ctx.Chat.ContextAccess.Release(); } } } diff --git a/SharpChat/Channel.cs b/SharpChat/Channel.cs index c23edfe..4cc0361 100644 --- a/SharpChat/Channel.cs +++ b/SharpChat/Channel.cs @@ -1,40 +1,40 @@ -namespace SharpChat { - public class Channel( - string name, - string password = "", - bool isTemporary = false, - int rank = 0, - string ownerId = "" - ) { - public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - public string Password { get; set; } = password ?? string.Empty; - public bool IsTemporary { get; set; } = isTemporary; - public int Rank { get; set; } = rank; - public string OwnerId { get; set; } = ownerId; +namespace SharpChat; - public bool HasPassword - => !string.IsNullOrWhiteSpace(Password); +public class Channel( + string name, + string password = "", + bool isTemporary = false, + int rank = 0, + string ownerId = "" +) { + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + public string Password { get; set; } = password ?? string.Empty; + public bool IsTemporary { get; set; } = isTemporary; + public int Rank { get; set; } = rank; + public string OwnerId { get; set; } = ownerId; - public bool NameEquals(string name) { - return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase); - } + public bool HasPassword + => !string.IsNullOrWhiteSpace(Password); - public bool IsOwner(User user) { - return string.IsNullOrEmpty(OwnerId) - && user != null - && OwnerId == user.UserId; - } + public bool NameEquals(string name) { + return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase); + } - public override int GetHashCode() { - return Name.GetHashCode(); - } + public bool IsOwner(User user) { + return string.IsNullOrEmpty(OwnerId) + && user != null + && OwnerId == user.UserId; + } - public static bool CheckName(string name) { - return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar); - } + public override int GetHashCode() { + return Name.GetHashCode(); + } - public static bool CheckNameChar(char c) { - return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_'; - } + public static bool CheckName(string name) { + return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar); + } + + public static bool CheckNameChar(char c) { + return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_'; } } diff --git a/SharpChat/ClientCommand.cs b/SharpChat/ClientCommand.cs index 63b071b..fdcf95f 100644 --- a/SharpChat/ClientCommand.cs +++ b/SharpChat/ClientCommand.cs @@ -1,6 +1,6 @@ -namespace SharpChat { - public interface ClientCommand { - bool IsMatch(ClientCommandContext ctx); - Task Dispatch(ClientCommandContext ctx); - } +namespace SharpChat; + +public interface ClientCommand { + bool IsMatch(ClientCommandContext ctx); + Task Dispatch(ClientCommandContext ctx); } diff --git a/SharpChat/ClientCommandContext.cs b/SharpChat/ClientCommandContext.cs index 3f51212..d53a9ba 100644 --- a/SharpChat/ClientCommandContext.cs +++ b/SharpChat/ClientCommandContext.cs @@ -1,49 +1,49 @@ -namespace SharpChat { - public class ClientCommandContext { - public string Name { get; } - public string[] Args { get; } - public Context Chat { get; } - public User User { get; } - public Connection Connection { get; } - public Channel Channel { get; } +namespace SharpChat; - public ClientCommandContext( - string text, - Context chat, - User user, - Connection connection, - Channel channel - ) { - ArgumentNullException.ThrowIfNull(text); +public class ClientCommandContext { + public string Name { get; } + public string[] Args { get; } + public Context Chat { get; } + public User User { get; } + public Connection Connection { get; } + public Channel Channel { get; } - Chat = chat ?? throw new ArgumentNullException(nameof(chat)); - User = user ?? throw new ArgumentNullException(nameof(user)); - Connection = connection ?? throw new ArgumentNullException(nameof(connection)); - Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + public ClientCommandContext( + string text, + Context chat, + User user, + Connection connection, + Channel channel + ) { + ArgumentNullException.ThrowIfNull(text); - string[] parts = text[1..].Split(' '); - Name = parts.First().Replace(".", string.Empty); - Args = [.. parts.Skip(1)]; - } + Chat = chat ?? throw new ArgumentNullException(nameof(chat)); + User = user ?? throw new ArgumentNullException(nameof(user)); + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); - public ClientCommandContext( - string name, - string[] args, - Context chat, - User user, - Connection connection, - Channel channel - ) { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Args = args ?? throw new ArgumentNullException(nameof(args)); - Chat = chat ?? throw new ArgumentNullException(nameof(chat)); - User = user ?? throw new ArgumentNullException(nameof(user)); - Connection = connection ?? throw new ArgumentNullException(nameof(connection)); - Channel = channel ?? throw new ArgumentNullException(nameof(channel)); - } + string[] parts = text[1..].Split(' '); + Name = parts.First().Replace(".", string.Empty); + Args = [.. parts.Skip(1)]; + } - public bool NameEquals(string name) { - return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase); - } + public ClientCommandContext( + string name, + string[] args, + Context chat, + User user, + Connection connection, + Channel channel + ) { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Args = args ?? throw new ArgumentNullException(nameof(args)); + Chat = chat ?? throw new ArgumentNullException(nameof(chat)); + User = user ?? throw new ArgumentNullException(nameof(user)); + Connection = connection ?? throw new ArgumentNullException(nameof(connection)); + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + } + + public bool NameEquals(string name) { + return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase); } } diff --git a/SharpChat/ClientCommands/AFKClientCommand.cs b/SharpChat/ClientCommands/AFKClientCommand.cs index bdacf1e..4718d1a 100644 --- a/SharpChat/ClientCommands/AFKClientCommand.cs +++ b/SharpChat/ClientCommands/AFKClientCommand.cs @@ -1,34 +1,34 @@ using System.Globalization; using System.Text; -namespace SharpChat.ClientCommands { - public class AFKClientCommand : ClientCommand { - private const string DEFAULT = "AFK"; - public const int MAX_GRAPHEMES = 5; - public const int MAX_BYTES = MAX_GRAPHEMES * 10; +namespace SharpChat.ClientCommands; - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("afk"); +public class AFKClientCommand : ClientCommand { + private const string DEFAULT = "AFK"; + public const int MAX_GRAPHEMES = 5; + public const int MAX_BYTES = MAX_GRAPHEMES * 10; + + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("afk"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + string? statusText = ctx.Args.FirstOrDefault(); + if(string.IsNullOrWhiteSpace(statusText)) + statusText = DEFAULT; + else { + statusText = statusText.Trim(); + + StringInfo sti = new(statusText); + if(Encoding.UTF8.GetByteCount(statusText) > MAX_BYTES + || sti.LengthInTextElements > MAX_GRAPHEMES) + statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, MAX_GRAPHEMES)).Trim(); } - public async Task Dispatch(ClientCommandContext ctx) { - string? statusText = ctx.Args.FirstOrDefault(); - if(string.IsNullOrWhiteSpace(statusText)) - statusText = DEFAULT; - else { - statusText = statusText.Trim(); - - StringInfo sti = new(statusText); - if(Encoding.UTF8.GetByteCount(statusText) > MAX_BYTES - || sti.LengthInTextElements > MAX_GRAPHEMES) - statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, MAX_GRAPHEMES)).Trim(); - } - - await ctx.Chat.UpdateUser( - ctx.User, - status: UserStatus.Away, - statusText: statusText - ); - } + await ctx.Chat.UpdateUser( + ctx.User, + status: UserStatus.Away, + statusText: statusText + ); } } diff --git a/SharpChat/ClientCommands/ActionClientCommand.cs b/SharpChat/ClientCommands/ActionClientCommand.cs index dae5643..baf6294 100644 --- a/SharpChat/ClientCommands/ActionClientCommand.cs +++ b/SharpChat/ClientCommands/ActionClientCommand.cs @@ -1,33 +1,33 @@ using SharpChat.Events; -namespace SharpChat.ClientCommands { - public class ActionClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("action") - || ctx.NameEquals("me"); - } +namespace SharpChat.ClientCommands; - public async Task Dispatch(ClientCommandContext ctx) { - if(ctx.Args.Length < 1) - return; +public class ActionClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("action") + || ctx.NameEquals("me"); + } - string actionStr = string.Join(' ', ctx.Args); - if(string.IsNullOrWhiteSpace(actionStr)) - return; + public async Task Dispatch(ClientCommandContext ctx) { + if(ctx.Args.Length < 1) + return; - await ctx.Chat.DispatchEvent(new MessageCreateEvent( - ctx.Chat.RandomSnowflake.Next(), - ctx.Channel.Name, - ctx.User.UserId, - ctx.User.UserName, - ctx.User.Colour, - ctx.User.Rank, - ctx.User.NickName, - ctx.User.Permissions, - DateTimeOffset.Now, - actionStr, - false, true, false - )); - } + string actionStr = string.Join(' ', ctx.Args); + if(string.IsNullOrWhiteSpace(actionStr)) + return; + + await ctx.Chat.DispatchEvent(new MessageCreateEvent( + ctx.Chat.RandomSnowflake.Next(), + ctx.Channel.Name, + ctx.User.UserId, + ctx.User.UserName, + ctx.User.Colour, + ctx.User.Rank, + ctx.User.NickName, + ctx.User.Permissions, + DateTimeOffset.Now, + actionStr, + false, true, false + )); } } diff --git a/SharpChat/ClientCommands/BanListClientCommand.cs b/SharpChat/ClientCommands/BanListClientCommand.cs index 02b2162..7907ee3 100644 --- a/SharpChat/ClientCommands/BanListClientCommand.cs +++ b/SharpChat/ClientCommands/BanListClientCommand.cs @@ -1,30 +1,30 @@ using SharpChat.Bans; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class BanListClientCommand(BansClient bansClient) : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("bans") - || ctx.NameEquals("banned"); +namespace SharpChat.ClientCommands; + +public class BanListClientCommand(BansClient bansClient) : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("bans") + || ctx.NameEquals("banned"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - - if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - try { - BanInfo[] banInfos = await bansClient.BanGetListAsync(); - await ctx.Chat.SendTo(ctx.User, new BanListS2CPacket( - msgId, - banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString())) - )); - } catch(Exception) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); - } + try { + BanInfo[] banInfos = await bansClient.BanGetListAsync(); + await ctx.Chat.SendTo(ctx.User, new BanListS2CPacket( + msgId, + banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString())) + )); + } catch(Exception) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); } } } diff --git a/SharpChat/ClientCommands/BroadcastClientCommand.cs b/SharpChat/ClientCommands/BroadcastClientCommand.cs index 6fa95c5..878e9b3 100644 --- a/SharpChat/ClientCommands/BroadcastClientCommand.cs +++ b/SharpChat/ClientCommands/BroadcastClientCommand.cs @@ -1,34 +1,34 @@ using SharpChat.Events; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class BroadcastClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("say") - || ctx.NameEquals("broadcast"); +namespace SharpChat.ClientCommands; + +public class BroadcastClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("say") + || ctx.NameEquals("broadcast"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.Broadcast)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - - if(!ctx.User.Can(UserPermissions.Broadcast)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - await ctx.Chat.DispatchEvent(new MessageCreateEvent( - msgId, - string.Empty, - ctx.User.UserId, - ctx.User.UserName, - ctx.User.Colour, - ctx.User.Rank, - ctx.User.NickName, - ctx.User.Permissions, - DateTimeOffset.Now, - string.Join(' ', ctx.Args), - false, false, true - )); - } + await ctx.Chat.DispatchEvent(new MessageCreateEvent( + msgId, + string.Empty, + ctx.User.UserId, + ctx.User.UserName, + ctx.User.Colour, + ctx.User.Rank, + ctx.User.NickName, + ctx.User.Permissions, + DateTimeOffset.Now, + string.Join(' ', ctx.Args), + false, false, true + )); } } diff --git a/SharpChat/ClientCommands/CreateChannelClientCommand.cs b/SharpChat/ClientCommands/CreateChannelClientCommand.cs index cfa9858..00cf727 100644 --- a/SharpChat/ClientCommands/CreateChannelClientCommand.cs +++ b/SharpChat/ClientCommands/CreateChannelClientCommand.cs @@ -1,62 +1,62 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class CreateChannelClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("create"); +namespace SharpChat.ClientCommands; + +public class CreateChannelClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("create"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.CreateChannel)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); + string firstArg = ctx.Args.First(); - if(!ctx.User.Can(UserPermissions.CreateChannel)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - string firstArg = ctx.Args.First(); - - bool createChanHasHierarchy; - if(ctx.Args.Length < 1 || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - int createChanHierarchy = 0; - if(createChanHasHierarchy) - if(!int.TryParse(firstArg, out createChanHierarchy)) - createChanHierarchy = 0; - - if(createChanHierarchy > ctx.User.Rank) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY)); - return; - } - - string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); - - if(!Channel.CheckName(createChanName)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID)); - return; - } - - if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); - return; - } - - Channel createChan = new( - createChanName, - isTemporary: !ctx.User.Can(UserPermissions.SetChannelPermanent), - rank: createChanHierarchy, - ownerId: ctx.User.UserId - ); - - ctx.Chat.Channels.Add(createChan); - foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank)) - await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(createChan.Name, createChan.HasPassword, createChan.IsTemporary)); - - await ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, createChan.Name)); + bool createChanHasHierarchy; + if(ctx.Args.Length < 1 || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } + + int createChanHierarchy = 0; + if(createChanHasHierarchy) + if(!int.TryParse(firstArg, out createChanHierarchy)) + createChanHierarchy = 0; + + if(createChanHierarchy > ctx.User.Rank) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY)); + return; + } + + string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0)); + + if(!Channel.CheckName(createChanName)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID)); + return; + } + + if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); + return; + } + + Channel createChan = new( + createChanName, + isTemporary: !ctx.User.Can(UserPermissions.SetChannelPermanent), + rank: createChanHierarchy, + ownerId: ctx.User.UserId + ); + + ctx.Chat.Channels.Add(createChan); + foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank)) + await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(createChan.Name, createChan.HasPassword, createChan.IsTemporary)); + + await ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, createChan.Name)); } } diff --git a/SharpChat/ClientCommands/DeleteChannelClientCommand.cs b/SharpChat/ClientCommands/DeleteChannelClientCommand.cs index a819374..bd8cf08 100644 --- a/SharpChat/ClientCommands/DeleteChannelClientCommand.cs +++ b/SharpChat/ClientCommands/DeleteChannelClientCommand.cs @@ -1,37 +1,37 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class DeleteChannelClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("delchan") || ( - ctx.NameEquals("delete") - && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false - ); +namespace SharpChat.ClientCommands; + +public class DeleteChannelClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("delchan") || ( + ctx.NameEquals("delete") + && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false + ); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(ctx.Args.Length < 1 || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); + string delChanName = string.Join('_', ctx.Args); + Channel? delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); - if(ctx.Args.Length < 1 || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - string delChanName = string.Join('_', ctx.Args); - Channel? delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); - - if(delChan == null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName)); - return; - } - - if(!ctx.User.Can(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETE_FAILED, true, delChan.Name)); - return; - } - - await ctx.Chat.RemoveChannel(delChan); - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETED, false, delChan.Name)); + if(delChan == null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName)); + return; } + + if(!ctx.User.Can(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETE_FAILED, true, delChan.Name)); + return; + } + + await ctx.Chat.RemoveChannel(delChan); + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETED, false, delChan.Name)); } } diff --git a/SharpChat/ClientCommands/DeleteMessageClientCommand.cs b/SharpChat/ClientCommands/DeleteMessageClientCommand.cs index 2960b2c..ab14ac6 100644 --- a/SharpChat/ClientCommands/DeleteMessageClientCommand.cs +++ b/SharpChat/ClientCommands/DeleteMessageClientCommand.cs @@ -1,41 +1,40 @@ using SharpChat.EventStorage; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands -{ - public class DeleteMessageClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("delmsg") || ( - ctx.NameEquals("delete") - && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true - ); +namespace SharpChat.ClientCommands; + +public class DeleteMessageClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("delmsg") || ( + ctx.NameEquals("delete") + && ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true + ); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + bool deleteAnyMessage = ctx.User.Can(UserPermissions.DeleteAnyMessage); + + if(!deleteAnyMessage && !ctx.User.Can(UserPermissions.DeleteOwnMessage)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - bool deleteAnyMessage = ctx.User.Can(UserPermissions.DeleteAnyMessage); + string? firstArg = ctx.Args.FirstOrDefault(); - if(!deleteAnyMessage && !ctx.User.Can(UserPermissions.DeleteOwnMessage)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - string? firstArg = ctx.Args.FirstOrDefault(); - - if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - StoredEventInfo? delMsg = ctx.Chat.Events.GetEvent(delSeqId); - - if(delMsg?.Sender is null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.MESSAGE_DELETE_ERROR)); - return; - } - - ctx.Chat.Events.RemoveEvent(delMsg); - await ctx.Chat.Send(new ChatMessageDeleteS2CPacket(delMsg.Id)); + if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } + + StoredEventInfo? delMsg = ctx.Chat.Events.GetEvent(delSeqId); + + if(delMsg?.Sender is null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.MESSAGE_DELETE_ERROR)); + return; + } + + ctx.Chat.Events.RemoveEvent(delMsg); + await ctx.Chat.Send(new ChatMessageDeleteS2CPacket(delMsg.Id)); } } diff --git a/SharpChat/ClientCommands/JoinChannelClientCommand.cs b/SharpChat/ClientCommands/JoinChannelClientCommand.cs index 20eb5b1..d2b9364 100644 --- a/SharpChat/ClientCommands/JoinChannelClientCommand.cs +++ b/SharpChat/ClientCommands/JoinChannelClientCommand.cs @@ -1,23 +1,23 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class JoinChannelClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("join"); +namespace SharpChat.ClientCommands; + +public class JoinChannelClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("join"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel"; + Channel? joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); + + if(joinChan is null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); + await ctx.Chat.ForceChannel(ctx.User); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel"; - Channel? joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); - - if(joinChan is null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); - await ctx.Chat.ForceChannel(ctx.User); - return; - } - - await ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1))); - } + await ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1))); } } diff --git a/SharpChat/ClientCommands/KickBanClientCommand.cs b/SharpChat/ClientCommands/KickBanClientCommand.cs index d1b710e..45e0257 100644 --- a/SharpChat/ClientCommands/KickBanClientCommand.cs +++ b/SharpChat/ClientCommands/KickBanClientCommand.cs @@ -2,72 +2,72 @@ using SharpChat.Bans; using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat.ClientCommands { - public class KickBanClientCommand(BansClient bansClient) : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("kick") - || ctx.NameEquals("ban"); +namespace SharpChat.ClientCommands; + +public class KickBanClientCommand(BansClient bansClient) : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("kick") + || ctx.NameEquals("ban"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + bool isBanning = ctx.NameEquals("ban"); + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - bool isBanning = ctx.NameEquals("ban"); - long msgId = ctx.Chat.RandomSnowflake.Next(); + string? banUserTarget = ctx.Args.ElementAtOrDefault(0); + string? banDurationStr = ctx.Args.ElementAtOrDefault(1); + int banReasonIndex = 1; + User? banUser = null; - if(!ctx.User.Can(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, banUserTarget ?? "User")); + return; + } + + if(banUser.Rank >= ctx.User.Rank && banUser != ctx.User) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName)); + return; + } + + TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero; + if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) { + if(durationSeconds < 0) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); return; } - string? banUserTarget = ctx.Args.ElementAtOrDefault(0); - string? banDurationStr = ctx.Args.ElementAtOrDefault(1); - int banReasonIndex = 1; - User? banUser = null; - - if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, banUserTarget ?? "User")); - return; - } - - if(banUser.Rank >= ctx.User.Rank && banUser != ctx.User) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName)); - return; - } - - TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero; - if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) { - if(durationSeconds < 0) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - duration = TimeSpan.FromSeconds(durationSeconds); - ++banReasonIndex; - } - - if(duration <= TimeSpan.Zero) { - await ctx.Chat.BanUser(banUser, duration); - return; - } - - string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex)); - - BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId); - if(banInfo is not null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName)); - return; - } - - await bansClient.BanCreateAsync( - BanKind.User, - duration, - ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None, - banUser.UserId, - banReason, - ctx.Connection.RemoteAddress, - ctx.User.UserId - ); + duration = TimeSpan.FromSeconds(durationSeconds); + ++banReasonIndex; + } + if(duration <= TimeSpan.Zero) { await ctx.Chat.BanUser(banUser, duration); + return; } + + string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex)); + + BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId); + if(banInfo is not null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName)); + return; + } + + await bansClient.BanCreateAsync( + BanKind.User, + duration, + ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None, + banUser.UserId, + banReason, + ctx.Connection.RemoteAddress, + ctx.User.UserId + ); + + await ctx.Chat.BanUser(banUser, duration); } } diff --git a/SharpChat/ClientCommands/NickClientCommand.cs b/SharpChat/ClientCommands/NickClientCommand.cs index 5c7e207..0d20d73 100644 --- a/SharpChat/ClientCommands/NickClientCommand.cs +++ b/SharpChat/ClientCommands/NickClientCommand.cs @@ -2,62 +2,62 @@ using SharpChat.SockChat.S2CPackets; using System.Globalization; using System.Text; -namespace SharpChat.ClientCommands { - public class NickClientCommand : ClientCommand { - private const int MAX_GRAPHEMES = 16; - private const int MAX_BYTES = MAX_GRAPHEMES * 10; +namespace SharpChat.ClientCommands; - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("nick"); +public class NickClientCommand : ClientCommand { + private const int MAX_GRAPHEMES = 16; + private const int MAX_BYTES = MAX_GRAPHEMES * 10; + + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("nick"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + bool setOthersNick = ctx.User.Can(UserPermissions.SetOthersNickname); + + if(!setOthersNick && !ctx.User.Can(UserPermissions.SetOwnNickname)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - bool setOthersNick = ctx.User.Can(UserPermissions.SetOthersNickname); + User? targetUser = null; + int offset = 0; - if(!setOthersNick && !ctx.User.Can(UserPermissions.SetOwnNickname)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - User? targetUser = null; - int offset = 0; - - if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) { - targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString()); - ++offset; - } - - targetUser ??= ctx.User; - - if(ctx.Args.Length < offset) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - string nickStr = string.Join('_', ctx.Args.Skip(offset)) - .Replace("\n", string.Empty).Replace("\r", string.Empty) - .Replace("\f", string.Empty).Replace("\t", string.Empty) - .Replace(' ', '_').Trim(); - - if(nickStr == targetUser.UserName) - nickStr = string.Empty; - else if(string.IsNullOrEmpty(nickStr)) - nickStr = string.Empty; - else { - StringInfo nsi = new(nickStr); - if(Encoding.UTF8.GetByteCount(nickStr) > MAX_BYTES - || nsi.LengthInTextElements > MAX_GRAPHEMES) - nickStr = nsi.SubstringByTextElements(0, Math.Min(nsi.LengthInTextElements, MAX_GRAPHEMES)).Trim(); - } - - if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.NAME_IN_USE, true, nickStr)); - return; - } - - string? previousName = targetUser.UserId == ctx.User.UserId ? (targetUser.NickName ?? targetUser.UserName) : null; - await ctx.Chat.UpdateUser(targetUser, nickName: nickStr, silent: previousName == null); + if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) { + targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString()); + ++offset; } + + targetUser ??= ctx.User; + + if(ctx.Args.Length < offset) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; + } + + string nickStr = string.Join('_', ctx.Args.Skip(offset)) + .Replace("\n", string.Empty).Replace("\r", string.Empty) + .Replace("\f", string.Empty).Replace("\t", string.Empty) + .Replace(' ', '_').Trim(); + + if(nickStr == targetUser.UserName) + nickStr = string.Empty; + else if(string.IsNullOrEmpty(nickStr)) + nickStr = string.Empty; + else { + StringInfo nsi = new(nickStr); + if(Encoding.UTF8.GetByteCount(nickStr) > MAX_BYTES + || nsi.LengthInTextElements > MAX_GRAPHEMES) + nickStr = nsi.SubstringByTextElements(0, Math.Min(nsi.LengthInTextElements, MAX_GRAPHEMES)).Trim(); + } + + if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.NAME_IN_USE, true, nickStr)); + return; + } + + string? previousName = targetUser.UserId == ctx.User.UserId ? (targetUser.NickName ?? targetUser.UserName) : null; + await ctx.Chat.UpdateUser(targetUser, nickName: nickStr, silent: previousName == null); } } diff --git a/SharpChat/ClientCommands/PardonAddressClientCommand.cs b/SharpChat/ClientCommands/PardonAddressClientCommand.cs index 54dfbd8..b410b21 100644 --- a/SharpChat/ClientCommands/PardonAddressClientCommand.cs +++ b/SharpChat/ClientCommands/PardonAddressClientCommand.cs @@ -2,39 +2,39 @@ using SharpChat.Bans; using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat.ClientCommands { - public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("pardonip") - || ctx.NameEquals("unbanip"); +namespace SharpChat.ClientCommands; + +public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("pardonip") + || ctx.NameEquals("unbanip"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - - if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - string? unbanAddrTarget = ctx.Args.FirstOrDefault(); - if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - unbanAddrTarget = unbanAddr.ToString(); - - BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr); - if(banInfo?.Kind != BanKind.IPAddress) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); - return; - } - - if(await bansClient.BanRevokeAsync(banInfo)) - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget)); - else - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + string? unbanAddrTarget = ctx.Args.FirstOrDefault(); + if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } + + unbanAddrTarget = unbanAddr.ToString(); + + BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr); + if(banInfo?.Kind != BanKind.IPAddress) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + return; + } + + if(await bansClient.BanRevokeAsync(banInfo)) + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget)); + else + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); } } diff --git a/SharpChat/ClientCommands/PardonUserClientCommand.cs b/SharpChat/ClientCommands/PardonUserClientCommand.cs index 5bd6bb7..d489f44 100644 --- a/SharpChat/ClientCommands/PardonUserClientCommand.cs +++ b/SharpChat/ClientCommands/PardonUserClientCommand.cs @@ -1,46 +1,46 @@ using SharpChat.Bans; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class PardonUserClientCommand(BansClient bansClient) : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("pardon") - || ctx.NameEquals("unban"); +namespace SharpChat.ClientCommands; + +public class PardonUserClientCommand(BansClient bansClient) : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("pardon") + || ctx.NameEquals("unban"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - - if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - string? unbanUserTarget = ctx.Args.FirstOrDefault(); - if(string.IsNullOrWhiteSpace(unbanUserTarget)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - string unbanUserDisplay = unbanUserTarget; - User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); - if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) - unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString()); - if(unbanUser != null) { - unbanUserTarget = unbanUser.UserId; - unbanUserDisplay = unbanUser.UserName; - } - - BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget); - if(banInfo?.Kind != BanKind.User) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); - return; - } - - if(await bansClient.BanRevokeAsync(banInfo)) - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay)); - else - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); + string? unbanUserTarget = ctx.Args.FirstOrDefault(); + if(string.IsNullOrWhiteSpace(unbanUserTarget)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } + + string unbanUserDisplay = unbanUserTarget; + User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); + if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) + unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString()); + if(unbanUser != null) { + unbanUserTarget = unbanUser.UserId; + unbanUserDisplay = unbanUser.UserName; + } + + BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget); + if(banInfo?.Kind != BanKind.User) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); + return; + } + + if(await bansClient.BanRevokeAsync(banInfo)) + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay)); + else + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); } } diff --git a/SharpChat/ClientCommands/PasswordChannelClientCommand.cs b/SharpChat/ClientCommands/PasswordChannelClientCommand.cs index a8bc0e6..3fd48d3 100644 --- a/SharpChat/ClientCommands/PasswordChannelClientCommand.cs +++ b/SharpChat/ClientCommands/PasswordChannelClientCommand.cs @@ -1,27 +1,27 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class PasswordChannelClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("pwd") - || ctx.NameEquals("password"); +namespace SharpChat.ClientCommands; + +public class PasswordChannelClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("pwd") + || ctx.NameEquals("password"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); + string chanPass = string.Join(' ', ctx.Args).Trim(); - if(!ctx.User.Can(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } + if(string.IsNullOrWhiteSpace(chanPass)) + chanPass = string.Empty; - string chanPass = string.Join(' ', ctx.Args).Trim(); - - if(string.IsNullOrWhiteSpace(chanPass)) - chanPass = string.Empty; - - await ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_PASSWORD_CHANGED, false)); - } + await ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_PASSWORD_CHANGED, false)); } } diff --git a/SharpChat/ClientCommands/RankChannelClientCommand.cs b/SharpChat/ClientCommands/RankChannelClientCommand.cs index dc7da06..34f8683 100644 --- a/SharpChat/ClientCommands/RankChannelClientCommand.cs +++ b/SharpChat/ClientCommands/RankChannelClientCommand.cs @@ -1,28 +1,28 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class RankChannelClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("rank") - || ctx.NameEquals("privilege") - || ctx.NameEquals("priv"); +namespace SharpChat.ClientCommands; + +public class RankChannelClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("rank") + || ctx.NameEquals("privilege") + || ctx.NameEquals("priv"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.SetChannelHierarchy) || ctx.Channel.IsOwner(ctx.User)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - - if(!ctx.User.Can(UserPermissions.SetChannelHierarchy) || ctx.Channel.IsOwner(ctx.User)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } - - if(ctx.Args.Length < 1 || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY)); - return; - } - - await ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false)); + if(ctx.Args.Length < 1 || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY)); + return; } + + await ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false)); } } diff --git a/SharpChat/ClientCommands/RemoteAddressClientCommand.cs b/SharpChat/ClientCommands/RemoteAddressClientCommand.cs index 509ff92..adc7765 100644 --- a/SharpChat/ClientCommands/RemoteAddressClientCommand.cs +++ b/SharpChat/ClientCommands/RemoteAddressClientCommand.cs @@ -1,31 +1,31 @@ using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat.ClientCommands { - public class RemoteAddressClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("ip") - || ctx.NameEquals("whois"); +namespace SharpChat.ClientCommands; + +public class RemoteAddressClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("ip") + || ctx.NameEquals("whois"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(!ctx.User.Can(UserPermissions.SeeIPAddress)) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, "/ip")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); + string? ipUserStr = ctx.Args.FirstOrDefault(); + User? ipUser = null; - if(!ctx.User.Can(UserPermissions.SeeIPAddress)) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, "/ip")); - return; - } - - string? ipUserStr = ctx.Args.FirstOrDefault(); - User? ipUser = null; - - if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); - return; - } - - foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser)) - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.IP_ADDRESS, false, ipUser.UserName, ip)); + if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); + return; } + + foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser)) + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.IP_ADDRESS, false, ipUser.UserName, ip)); } } diff --git a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs index b0ac76a..63468f2 100644 --- a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs +++ b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs @@ -1,31 +1,31 @@ using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand { - private readonly ManualResetEvent WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle)); - private readonly Func<bool> ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck)); +namespace SharpChat.ClientCommands; - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("shutdown") - || ctx.NameEquals("restart"); +public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand { + private readonly ManualResetEvent WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle)); + private readonly Func<bool> ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck)); + + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("shutdown") + || ctx.NameEquals("restart"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + if(!ctx.User.UserId.Equals("1")) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - if(!ctx.User.UserId.Equals("1")) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); - return; - } + if(!ShutdownCheck()) + return; - if(!ShutdownCheck()) - return; + if(ctx.NameEquals("restart")) + foreach(Connection conn in ctx.Chat.Connections) + conn.PrepareForRestart(); - if(ctx.NameEquals("restart")) - foreach(Connection conn in ctx.Chat.Connections) - conn.PrepareForRestart(); - - await ctx.Chat.Update(); - WaitHandle?.Set(); - } + await ctx.Chat.Update(); + WaitHandle?.Set(); } } diff --git a/SharpChat/ClientCommands/WhisperClientCommand.cs b/SharpChat/ClientCommands/WhisperClientCommand.cs index 78038b9..bff5937 100644 --- a/SharpChat/ClientCommands/WhisperClientCommand.cs +++ b/SharpChat/ClientCommands/WhisperClientCommand.cs @@ -1,45 +1,45 @@ using SharpChat.Events; using SharpChat.SockChat.S2CPackets; -namespace SharpChat.ClientCommands { - public class WhisperClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("whisper") - || ctx.NameEquals("msg"); +namespace SharpChat.ClientCommands; + +public class WhisperClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("whisper") + || ctx.NameEquals("msg"); + } + + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + + if(ctx.Args.Length < 2) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); + return; } - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); + string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty; + User? whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); - if(ctx.Args.Length < 2) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); - return; - } - - string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty; - User? whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); - - if(whisperUser == null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, whisperUserStr)); - return; - } - - if(whisperUser == ctx.User) - return; - - await ctx.Chat.DispatchEvent(new MessageCreateEvent( - msgId, - User.GetDMChannelName(ctx.User, whisperUser), - ctx.User.UserId, - ctx.User.UserName, - ctx.User.Colour, - ctx.User.Rank, - ctx.User.NickName, - ctx.User.Permissions, - DateTimeOffset.Now, - string.Join(' ', ctx.Args.Skip(1)), - true, false, false - )); + if(whisperUser == null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, whisperUserStr)); + return; } + + if(whisperUser == ctx.User) + return; + + await ctx.Chat.DispatchEvent(new MessageCreateEvent( + msgId, + User.GetDMChannelName(ctx.User, whisperUser), + ctx.User.UserId, + ctx.User.UserName, + ctx.User.Colour, + ctx.User.Rank, + ctx.User.NickName, + ctx.User.Permissions, + DateTimeOffset.Now, + string.Join(' ', ctx.Args.Skip(1)), + true, false, false + )); } } diff --git a/SharpChat/ClientCommands/WhoClientCommand.cs b/SharpChat/ClientCommands/WhoClientCommand.cs index 398cfc6..74b07b5 100644 --- a/SharpChat/ClientCommands/WhoClientCommand.cs +++ b/SharpChat/ClientCommands/WhoClientCommand.cs @@ -1,62 +1,62 @@ using SharpChat.SockChat.S2CPackets; using System.Text; -namespace SharpChat.ClientCommands { - public class WhoClientCommand : ClientCommand { - public bool IsMatch(ClientCommandContext ctx) { - return ctx.NameEquals("who"); - } +namespace SharpChat.ClientCommands; - public async Task Dispatch(ClientCommandContext ctx) { - long msgId = ctx.Chat.RandomSnowflake.Next(); - StringBuilder whoChanSB = new(); - string? whoChanStr = ctx.Args.FirstOrDefault(); +public class WhoClientCommand : ClientCommand { + public bool IsMatch(ClientCommandContext ctx) { + return ctx.NameEquals("who"); + } - if(string.IsNullOrEmpty(whoChanStr)) { - foreach(User whoUser in ctx.Chat.Users) { - whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);"""); + public async Task Dispatch(ClientCommandContext ctx) { + long msgId = ctx.Chat.RandomSnowflake.Next(); + StringBuilder whoChanSB = new(); + string? whoChanStr = ctx.Args.FirstOrDefault(); - if(whoUser == ctx.User) - whoChanSB.Append(@" style=""font-weight: bold;"""); + if(string.IsNullOrEmpty(whoChanStr)) { + foreach(User whoUser in ctx.Chat.Users) { + whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);"""); - whoChanSB.Append('>'); - whoChanSB.Append(whoUser.LegacyName); - whoChanSB.Append("</a>, "); - } + if(whoUser == ctx.User) + whoChanSB.Append(@" style=""font-weight: bold;"""); - if(whoChanSB.Length > 2) - whoChanSB.Length -= 2; - - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB)); - } else { - Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); - - if(whoChan is null) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); - return; - } - - if(whoChan.Rank > ctx.User.Rank || (whoChan.HasPassword && !ctx.User.Can(UserPermissions.JoinAnyChannel))) { - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_ERROR, true, whoChanStr)); - return; - } - - foreach(User whoUser in ctx.Chat.GetChannelUsers(whoChan)) { - whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);"""); - - if(whoUser == ctx.User) - whoChanSB.Append(@" style=""font-weight: bold;"""); - - whoChanSB.Append('>'); - whoChanSB.Append(whoUser.LegacyName); - whoChanSB.Append("</a>, "); - } - - if(whoChanSB.Length > 2) - whoChanSB.Length -= 2; - - await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB)); + whoChanSB.Append('>'); + whoChanSB.Append(whoUser.LegacyName); + whoChanSB.Append("</a>, "); } + + if(whoChanSB.Length > 2) + whoChanSB.Length -= 2; + + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB)); + } else { + Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); + + if(whoChan is null) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); + return; + } + + if(whoChan.Rank > ctx.User.Rank || (whoChan.HasPassword && !ctx.User.Can(UserPermissions.JoinAnyChannel))) { + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_ERROR, true, whoChanStr)); + return; + } + + foreach(User whoUser in ctx.Chat.GetChannelUsers(whoChan)) { + whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);"""); + + if(whoUser == ctx.User) + whoChanSB.Append(@" style=""font-weight: bold;"""); + + whoChanSB.Append('>'); + whoChanSB.Append(whoUser.LegacyName); + whoChanSB.Append("</a>, "); + } + + if(whoChanSB.Length > 2) + whoChanSB.Length -= 2; + + await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB)); } } } diff --git a/SharpChat/Connection.cs b/SharpChat/Connection.cs index 3d127e3..ccd3d27 100644 --- a/SharpChat/Connection.cs +++ b/SharpChat/Connection.cs @@ -2,89 +2,89 @@ using Fleck; using SharpChat.SockChat; using System.Net; -namespace SharpChat { - public class Connection : IDisposable { - public const int ID_LENGTH = 20; +namespace SharpChat; + +public class Connection : IDisposable { + public const int ID_LENGTH = 20; #if DEBUG - public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1); + public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1); #else - public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5); + public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5); #endif - public IWebSocketConnection Socket { get; } + public IWebSocketConnection Socket { get; } - public string Id { get; } - public bool IsDisposed { get; private set; } - public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now; - public User? User { get; set; } + public string Id { get; } + public bool IsDisposed { get; private set; } + public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now; + public User? User { get; set; } - private int CloseCode { get; set; } = 1000; + private int CloseCode { get; set; } = 1000; - public IPAddress RemoteAddress { get; } - public ushort RemotePort { get; } + public IPAddress RemoteAddress { get; } + public ushort RemotePort { get; } - public bool IsAlive => !IsDisposed && !HasTimedOut; + public bool IsAlive => !IsDisposed && !HasTimedOut; - public Connection(IWebSocketConnection sock) { - Socket = sock; - Id = RNG.SecureRandomString(ID_LENGTH); + public Connection(IWebSocketConnection sock) { + Socket = sock; + Id = RNG.SecureRandomString(ID_LENGTH); - if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr)) - throw new Exception("Unable to parse remote address?????"); + if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr)) + throw new Exception("Unable to parse remote address?????"); - if(IPAddress.IsLoopback(addr) - && sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr) - && IPAddress.TryParse(addrStr, out IPAddress? realAddr)) - addr = realAddr; + if(IPAddress.IsLoopback(addr) + && sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr) + && IPAddress.TryParse(addrStr, out IPAddress? realAddr)) + addr = realAddr; - RemoteAddress = addr; - RemotePort = (ushort)sock.ConnectionInfo.ClientPort; - } + RemoteAddress = addr; + RemotePort = (ushort)sock.ConnectionInfo.ClientPort; + } - public async Task Send(S2CPacket packet) { - if(!Socket.IsAvailable) - return; + public async Task Send(S2CPacket packet) { + if(!Socket.IsAvailable) + return; - string data = packet.Pack(); - if(!string.IsNullOrWhiteSpace(data)) - await Socket.Send(data); - } + string data = packet.Pack(); + if(!string.IsNullOrWhiteSpace(data)) + await Socket.Send(data); + } - public void BumpPing() { - LastPing = DateTimeOffset.Now; - } + public void BumpPing() { + LastPing = DateTimeOffset.Now; + } - public bool HasTimedOut - => DateTimeOffset.Now - LastPing > SessionTimeOut; + public bool HasTimedOut + => DateTimeOffset.Now - LastPing > SessionTimeOut; - public void PrepareForRestart() { - CloseCode = 1012; - } + public void PrepareForRestart() { + CloseCode = 1012; + } - ~Connection() { - DoDispose(); - } + ~Connection() { + DoDispose(); + } - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } - private void DoDispose() { - if(IsDisposed) - return; + private void DoDispose() { + if(IsDisposed) + return; - IsDisposed = true; - Socket.Close(CloseCode); - } + IsDisposed = true; + Socket.Close(CloseCode); + } - public override string ToString() { - return Id; - } + public override string ToString() { + return Id; + } - public override int GetHashCode() { - return Id.GetHashCode(); - } + public override int GetHashCode() { + return Id.GetHashCode(); } } diff --git a/SharpChat/Context.cs b/SharpChat/Context.cs index 6e47527..a095cf9 100644 --- a/SharpChat/Context.cs +++ b/SharpChat/Context.cs @@ -5,409 +5,409 @@ using SharpChat.SockChat; using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat { - public class Context { - public record ChannelUserAssoc(string UserId, string ChannelName); +namespace SharpChat; - public readonly SemaphoreSlim ContextAccess = new(1, 1); +public class Context { + public record ChannelUserAssoc(string UserId, string ChannelName); - public SnowflakeGenerator SnowflakeGenerator { get; } = new(); - public RandomSnowflake RandomSnowflake { get; } + public readonly SemaphoreSlim ContextAccess = new(1, 1); - public HashSet<Channel> Channels { get; } = []; - public HashSet<Connection> Connections { get; } = []; - public HashSet<User> Users { get; } = []; - public EventStorage.EventStorage Events { get; } - public HashSet<ChannelUserAssoc> ChannelUsers { get; } = []; - public Dictionary<string, RateLimiter> UserRateLimiters { get; } = []; - public Dictionary<string, Channel> UserLastChannel { get; } = []; + public SnowflakeGenerator SnowflakeGenerator { get; } = new(); + public RandomSnowflake RandomSnowflake { get; } - public Context(EventStorage.EventStorage evtStore) { - Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); - RandomSnowflake = new(SnowflakeGenerator); - } + public HashSet<Channel> Channels { get; } = []; + public HashSet<Connection> Connections { get; } = []; + public HashSet<User> Users { get; } = []; + public EventStorage.EventStorage Events { get; } + public HashSet<ChannelUserAssoc> ChannelUsers { get; } = []; + public Dictionary<string, RateLimiter> UserRateLimiters { get; } = []; + public Dictionary<string, Channel> UserLastChannel { get; } = []; - public async Task DispatchEvent(ChatEvent eventInfo) { - if(eventInfo is MessageCreateEvent mce) { - if(mce.IsBroadcast) { - await Send(new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.BROADCAST, false, mce.MessageText)); - } else if(mce.IsPrivate) { - // The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed - // e.g. nook sees @Arysil and Arysil sees @nook + public Context(EventStorage.EventStorage evtStore) { + Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); + RandomSnowflake = new(SnowflakeGenerator); + } - // this entire routine is garbage, channels should probably in the db - if(!mce.ChannelName.StartsWith('@')) - return; + public async Task DispatchEvent(ChatEvent eventInfo) { + if(eventInfo is MessageCreateEvent mce) { + if(mce.IsBroadcast) { + await Send(new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.BROADCAST, false, mce.MessageText)); + } else if(mce.IsPrivate) { + // The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed + // e.g. nook sees @Arysil and Arysil sees @nook - IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString()); - if(uids.Count() != 2) - return; + // this entire routine is garbage, channels should probably in the db + if(!mce.ChannelName.StartsWith('@')) + return; - IEnumerable<User> users = Users.Where(u => uids.Any(uid => uid == u.UserId)); - User? target = users.FirstOrDefault(u => u.UserId != mce.SenderId); - if(target == null) - return; + IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString()); + if(uids.Count() != 2) + return; - foreach(User user in users) - await SendTo(user, new ChatMessageAddS2CPacket( - mce.MessageId, - DateTimeOffset.Now, - mce.SenderId, - mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText, - mce.IsAction, - true - )); - } else { - Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName)); - if(channel is not null) - await SendTo(channel, new ChatMessageAddS2CPacket( - mce.MessageId, - DateTimeOffset.Now, - mce.SenderId, - mce.MessageText, - mce.IsAction, - false - )); - } + IEnumerable<User> users = Users.Where(u => uids.Any(uid => uid == u.UserId)); + User? target = users.FirstOrDefault(u => u.UserId != mce.SenderId); + if(target == null) + return; - Events.AddEvent( - mce.MessageId, "msg:add", - mce.ChannelName, - mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms, - new { text = mce.MessageText }, - (mce.IsBroadcast ? StoredEventFlags.Broadcast : 0) - | (mce.IsAction ? StoredEventFlags.Action : 0) - | (mce.IsPrivate ? StoredEventFlags.Private : 0) - ); - return; - } - } - - public async Task Update() { - foreach(Connection conn in Connections) - if(!conn.IsDisposed && conn.HasTimedOut) { - conn.Dispose(); - Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}."); - } - - Connections.RemoveWhere(conn => conn.IsDisposed); - - foreach(User user in Users) - if(!Connections.Any(conn => conn.User == user)) { - Logger.Write($"Timing out {user} (no more connections)."); - await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut); - } - } - - public async Task SafeUpdate() { - ContextAccess.Wait(); - try { - await Update(); - } finally { - ContextAccess.Release(); - } - } - - public bool IsInChannel(User user, Channel channel) { - return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name)); - } - - public string[] GetUserChannelNames(User user) { - return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)]; - } - - public Channel[] GetUserChannels(User user) { - string[] names = GetUserChannelNames(user); - return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))]; - } - - public string[] GetChannelUserIds(Channel channel) { - return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)]; - } - - public User[] GetChannelUsers(Channel channel) { - string[] ids = GetChannelUserIds(channel); - return [.. Users.Where(u => ids.Contains(u.UserId))]; - } - - public async Task UpdateUser( - User user, - string? userName = null, - string? nickName = null, - ColourInheritable? colour = null, - UserStatus? status = null, - string? statusText = null, - int? rank = null, - UserPermissions? perms = null, - bool silent = false - ) { - ArgumentNullException.ThrowIfNull(user); - - bool hasChanged = false; - string previousName = string.Empty; - - if(userName != null && !user.UserName.Equals(userName)) { - user.UserName = userName; - hasChanged = true; - } - - if(nickName != null && !user.NickName.Equals(nickName)) { - if(!silent) - previousName = user.LegacyName; - - user.NickName = nickName; - hasChanged = true; - } - - if(colour.HasValue && user.Colour != colour.Value) { - user.Colour = colour.Value; - hasChanged = true; - } - - if(status.HasValue && user.Status != status.Value) { - user.Status = status.Value; - hasChanged = true; - } - - if(statusText != null && !user.StatusText.Equals(statusText)) { - user.StatusText = statusText; - hasChanged = true; - } - - if(rank != null && user.Rank != rank) { - user.Rank = (int)rank; - hasChanged = true; - } - - if(perms.HasValue && user.Permissions != perms) { - user.Permissions = perms.Value; - hasChanged = true; - } - - if(hasChanged) { - if(!string.IsNullOrWhiteSpace(previousName)) - await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, user.LegacyNameWithStatus)); - - await SendToUserChannels(user, new UserUpdateS2CPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); - } - } - - public async Task BanUser(User user, TimeSpan duration, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Kicked) { - if(duration > TimeSpan.Zero) { - DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration; - await SendTo(user, new ForceDisconnectS2CPacket(expires)); - } else - await SendTo(user, new ForceDisconnectS2CPacket()); - - foreach(Connection conn in Connections) - if(conn.User == user) - conn.Dispose(); - Connections.RemoveWhere(conn => conn.IsDisposed); - - await HandleDisconnect(user, reason); - } - - public async Task HandleJoin(User user, Channel chan, Connection conn, int maxMsgLength) { - if(!IsInChannel(user, chan)) { - long msgId = RandomSnowflake.Next(); - await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); - Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); - } - - await conn.Send(new AuthSuccessS2CPacket( - user.UserId, - user.LegacyNameWithStatus, - user.Colour, - user.Rank, - user.Permissions, - chan.Name, - maxMsgLength - )); - await conn.Send(new ContextUsersS2CPacket( - GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank) - .Select(u => new ContextUsersS2CPacket.Entry( - u.UserId, - u.LegacyNameWithStatus, - u.Colour, - u.Rank, - u.Permissions, + foreach(User user in users) + await SendTo(user, new ChatMessageAddS2CPacket( + mce.MessageId, + DateTimeOffset.Now, + mce.SenderId, + mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText, + mce.IsAction, true - )) - )); - - foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name)) - await conn.Send(new ContextMessageS2CPacket(msg)); - - await conn.Send(new ContextChannelsS2CPacket( - Channels.Where(c => c.Rank <= user.Rank) - .Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary)) - )); - - Users.Add(user); - - ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); - UserLastChannel[user.UserId] = chan; - } - - public async Task HandleDisconnect(User user, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Leave) { - await UpdateUser(user, status: UserStatus.Offline); - Users.Remove(user); - UserLastChannel.Remove(user.UserId); - - Channel[] channels = GetUserChannels(user); - - foreach(Channel chan in channels) { - ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); - - long msgId = RandomSnowflake.Next(); - await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason)); - Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log); - - if(chan.IsTemporary && chan.IsOwner(user)) - await RemoveChannel(chan); + )); + } else { + Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName)); + if(channel is not null) + await SendTo(channel, new ChatMessageAddS2CPacket( + mce.MessageId, + DateTimeOffset.Now, + mce.SenderId, + mce.MessageText, + mce.IsAction, + false + )); } + + Events.AddEvent( + mce.MessageId, "msg:add", + mce.ChannelName, + mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms, + new { text = mce.MessageText }, + (mce.IsBroadcast ? StoredEventFlags.Broadcast : 0) + | (mce.IsAction ? StoredEventFlags.Action : 0) + | (mce.IsPrivate ? StoredEventFlags.Private : 0) + ); + return; + } + } + + public async Task Update() { + foreach(Connection conn in Connections) + if(!conn.IsDisposed && conn.HasTimedOut) { + conn.Dispose(); + Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}."); + } + + Connections.RemoveWhere(conn => conn.IsDisposed); + + foreach(User user in Users) + if(!Connections.Any(conn => conn.User == user)) { + Logger.Write($"Timing out {user} (no more connections)."); + await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut); + } + } + + public async Task SafeUpdate() { + ContextAccess.Wait(); + try { + await Update(); + } finally { + ContextAccess.Release(); + } + } + + public bool IsInChannel(User user, Channel channel) { + return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name)); + } + + public string[] GetUserChannelNames(User user) { + return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)]; + } + + public Channel[] GetUserChannels(User user) { + string[] names = GetUserChannelNames(user); + return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))]; + } + + public string[] GetChannelUserIds(Channel channel) { + return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)]; + } + + public User[] GetChannelUsers(Channel channel) { + string[] ids = GetChannelUserIds(channel); + return [.. Users.Where(u => ids.Contains(u.UserId))]; + } + + public async Task UpdateUser( + User user, + string? userName = null, + string? nickName = null, + ColourInheritable? colour = null, + UserStatus? status = null, + string? statusText = null, + int? rank = null, + UserPermissions? perms = null, + bool silent = false + ) { + ArgumentNullException.ThrowIfNull(user); + + bool hasChanged = false; + string previousName = string.Empty; + + if(userName != null && !user.UserName.Equals(userName)) { + user.UserName = userName; + hasChanged = true; } - public async Task SwitchChannel(User user, Channel chan, string password) { - if(UserLastChannel.TryGetValue(user.UserId, out Channel? ulc) && chan == ulc) { + if(nickName != null && !user.NickName.Equals(nickName)) { + if(!silent) + previousName = user.LegacyName; + + user.NickName = nickName; + hasChanged = true; + } + + if(colour.HasValue && user.Colour != colour.Value) { + user.Colour = colour.Value; + hasChanged = true; + } + + if(status.HasValue && user.Status != status.Value) { + user.Status = status.Value; + hasChanged = true; + } + + if(statusText != null && !user.StatusText.Equals(statusText)) { + user.StatusText = statusText; + hasChanged = true; + } + + if(rank != null && user.Rank != rank) { + user.Rank = (int)rank; + hasChanged = true; + } + + if(perms.HasValue && user.Permissions != perms) { + user.Permissions = perms.Value; + hasChanged = true; + } + + if(hasChanged) { + if(!string.IsNullOrWhiteSpace(previousName)) + await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, user.LegacyNameWithStatus)); + + await SendToUserChannels(user, new UserUpdateS2CPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); + } + } + + public async Task BanUser(User user, TimeSpan duration, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Kicked) { + if(duration > TimeSpan.Zero) { + DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration; + await SendTo(user, new ForceDisconnectS2CPacket(expires)); + } else + await SendTo(user, new ForceDisconnectS2CPacket()); + + foreach(Connection conn in Connections) + if(conn.User == user) + conn.Dispose(); + Connections.RemoveWhere(conn => conn.IsDisposed); + + await HandleDisconnect(user, reason); + } + + public async Task HandleJoin(User user, Channel chan, Connection conn, int maxMsgLength) { + if(!IsInChannel(user, chan)) { + long msgId = RandomSnowflake.Next(); + await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); + Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); + } + + await conn.Send(new AuthSuccessS2CPacket( + user.UserId, + user.LegacyNameWithStatus, + user.Colour, + user.Rank, + user.Permissions, + chan.Name, + maxMsgLength + )); + await conn.Send(new ContextUsersS2CPacket( + GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank) + .Select(u => new ContextUsersS2CPacket.Entry( + u.UserId, + u.LegacyNameWithStatus, + u.Colour, + u.Rank, + u.Permissions, + true + )) + )); + + foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name)) + await conn.Send(new ContextMessageS2CPacket(msg)); + + await conn.Send(new ContextChannelsS2CPacket( + Channels.Where(c => c.Rank <= user.Rank) + .Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary)) + )); + + Users.Add(user); + + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + UserLastChannel[user.UserId] = chan; + } + + public async Task HandleDisconnect(User user, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Leave) { + await UpdateUser(user, status: UserStatus.Offline); + Users.Remove(user); + UserLastChannel.Remove(user.UserId); + + Channel[] channels = GetUserChannels(user); + + foreach(Channel chan in channels) { + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); + + long msgId = RandomSnowflake.Next(); + await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason)); + Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log); + + if(chan.IsTemporary && chan.IsOwner(user)) + await RemoveChannel(chan); + } + } + + public async Task SwitchChannel(User user, Channel chan, string password) { + if(UserLastChannel.TryGetValue(user.UserId, out Channel? ulc) && chan == ulc) { + await ForceChannel(user); + return; + } + + if(!user.Can(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) { + if(chan.Rank > user.Rank) { + await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name)); await ForceChannel(user); return; } - if(!user.Can(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) { - if(chan.Rank > user.Rank) { - await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name)); - await ForceChannel(user); - return; - } - - if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) { - await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name)); - await ForceChannel(user); - return; - } + if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) { + await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name)); + await ForceChannel(user); + return; } - - await ForceChannelSwitch(user, chan); } - public async Task ForceChannelSwitch(User user, Channel chan) { - if(!Channels.Contains(chan)) - return; + await ForceChannelSwitch(user, chan); + } - Channel oldChan = UserLastChannel[user.UserId]; + public async Task ForceChannelSwitch(User user, Channel chan) { + if(!Channels.Contains(chan)) + return; - long leaveId = RandomSnowflake.Next(); - await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId)); - Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); + Channel oldChan = UserLastChannel[user.UserId]; - long joinId = RandomSnowflake.Next(); - await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); - Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); + long leaveId = RandomSnowflake.Next(); + await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId)); + Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); - await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers)); - await SendTo(user, new ContextUsersS2CPacket( - GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank) - .Select(u => new ContextUsersS2CPacket.Entry( - u.UserId, - u.LegacyNameWithStatus, - u.Colour, - u.Rank, - u.Permissions, - true - )) - )); + long joinId = RandomSnowflake.Next(); + await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions)); + Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log); - foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name)) - await SendTo(user, new ContextMessageS2CPacket(msg)); + await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers)); + await SendTo(user, new ContextUsersS2CPacket( + GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank) + .Select(u => new ContextUsersS2CPacket.Entry( + u.UserId, + u.LegacyNameWithStatus, + u.Colour, + u.Rank, + u.Permissions, + true + )) + )); - await ForceChannel(user, chan); + foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name)) + await SendTo(user, new ContextMessageS2CPacket(msg)); - ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); - ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); - UserLastChannel[user.UserId] = chan; + await ForceChannel(user, chan); - if(oldChan.IsTemporary && oldChan.IsOwner(user)) - await RemoveChannel(oldChan); - } + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + UserLastChannel[user.UserId] = chan; - public async Task Send(S2CPacket packet) { - foreach(Connection conn in Connections) - if(conn.IsAlive && conn.User is not null) - await conn.Send(packet); - } + if(oldChan.IsTemporary && oldChan.IsOwner(user)) + await RemoveChannel(oldChan); + } - public async Task SendTo(User user, S2CPacket packet) { - foreach(Connection conn in Connections) - if(conn.IsAlive && conn.User == user) - await conn.Send(packet); - } - - public async Task SendTo(Channel channel, S2CPacket packet) { - // might be faster to grab the users first and then cascade into that SendTo - IEnumerable<Connection> conns = Connections.Where(c => c.IsAlive && c.User is not null && IsInChannel(c.User, channel)); - foreach(Connection conn in conns) + public async Task Send(S2CPacket packet) { + foreach(Connection conn in Connections) + if(conn.IsAlive && conn.User is not null) await conn.Send(packet); - } + } - public async Task SendToUserChannels(User user, S2CPacket packet) { - IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c)); - IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName)))); - foreach(Connection conn in conns) + public async Task SendTo(User user, S2CPacket packet) { + foreach(Connection conn in Connections) + if(conn.IsAlive && conn.User == user) await conn.Send(packet); - } + } - public IPAddress[] GetRemoteAddresses(User user) { - return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()]; - } + public async Task SendTo(Channel channel, S2CPacket packet) { + // might be faster to grab the users first and then cascade into that SendTo + IEnumerable<Connection> conns = Connections.Where(c => c.IsAlive && c.User is not null && IsInChannel(c.User, channel)); + foreach(Connection conn in conns) + await conn.Send(packet); + } - public async Task ForceChannel(User user, Channel? chan = null) { - ArgumentNullException.ThrowIfNull(user); + public async Task SendToUserChannels(User user, S2CPacket packet) { + IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c)); + IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName)))); + foreach(Connection conn in conns) + await conn.Send(packet); + } - if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan)) - throw new ArgumentException("no channel???"); + public IPAddress[] GetRemoteAddresses(User user) { + return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()]; + } - await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name)); - } + public async Task ForceChannel(User user, Channel? chan = null) { + ArgumentNullException.ThrowIfNull(user); - public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) { - ArgumentNullException.ThrowIfNull(channel); - if(!Channels.Contains(channel)) - throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel)); + if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan)) + throw new ArgumentException("no channel???"); - if(temporary.HasValue) - channel.IsTemporary = temporary.Value; + await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name)); + } - if(hierarchy.HasValue) - channel.Rank = hierarchy.Value; + public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) { + ArgumentNullException.ThrowIfNull(channel); + if(!Channels.Contains(channel)) + throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel)); - if(password != null) - channel.Password = password; + if(temporary.HasValue) + channel.IsTemporary = temporary.Value; - // TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively - foreach(User user in Users.Where(u => u.Rank >= channel.Rank)) - await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary)); - } + if(hierarchy.HasValue) + channel.Rank = hierarchy.Value; - public async Task RemoveChannel(Channel channel) { - if(channel == null || Channels.Count < 1) - return; + if(password != null) + channel.Password = password; - Channel? defaultChannel = Channels.FirstOrDefault(); - if(defaultChannel is null) - return; + // TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively + foreach(User user in Users.Where(u => u.Rank >= channel.Rank)) + await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary)); + } - // Remove channel from the listing - Channels.Remove(channel); + public async Task RemoveChannel(Channel channel) { + if(channel == null || Channels.Count < 1) + return; - // Move all users back to the main channel - // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel. - foreach(User user in GetChannelUsers(channel)) - await SwitchChannel(user, defaultChannel, string.Empty); + Channel? defaultChannel = Channels.FirstOrDefault(); + if(defaultChannel is null) + return; - // Broadcast deletion of channel - foreach(User user in Users.Where(u => u.Rank >= channel.Rank)) - await SendTo(user, new ChannelDeleteS2CPacket(channel.Name)); - } + // Remove channel from the listing + Channels.Remove(channel); + + // Move all users back to the main channel + // TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel. + foreach(User user in GetChannelUsers(channel)) + await SwitchChannel(user, defaultChannel, string.Empty); + + // Broadcast deletion of channel + foreach(User user in Users.Where(u => u.Rank >= channel.Rank)) + await SendTo(user, new ChannelDeleteS2CPacket(channel.Name)); } } diff --git a/SharpChat/EventStorage/EventStorage.cs b/SharpChat/EventStorage/EventStorage.cs index ae0f344..db3a47c 100644 --- a/SharpChat/EventStorage/EventStorage.cs +++ b/SharpChat/EventStorage/EventStorage.cs @@ -1,20 +1,20 @@ -namespace SharpChat.EventStorage { - public interface EventStorage { - void AddEvent( - long id, - string type, - string channelName, - string senderId, - string senderName, - ColourInheritable senderColour, - int senderRank, - string senderNick, - UserPermissions senderPerms, - object? data = null, - StoredEventFlags flags = StoredEventFlags.None - ); - void RemoveEvent(StoredEventInfo evt); - StoredEventInfo? GetEvent(long seqId); - IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0); - } +namespace SharpChat.EventStorage; + +public interface EventStorage { + void AddEvent( + long id, + string type, + string channelName, + string senderId, + string senderName, + ColourInheritable senderColour, + int senderRank, + string senderNick, + UserPermissions senderPerms, + object? data = null, + StoredEventFlags flags = StoredEventFlags.None + ); + void RemoveEvent(StoredEventInfo evt); + StoredEventInfo? GetEvent(long seqId); + IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0); } diff --git a/SharpChat/EventStorage/MariaDBEventStorage.cs b/SharpChat/EventStorage/MariaDBEventStorage.cs index 900b9be..69218fc 100644 --- a/SharpChat/EventStorage/MariaDBEventStorage.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage.cs @@ -2,130 +2,130 @@ using MySqlConnector; using System.Text; using System.Text.Json; -namespace SharpChat.EventStorage { - public partial class MariaDBEventStorage(string connString) : EventStorage { - private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString)); +namespace SharpChat.EventStorage; - public void AddEvent( - long id, - string type, - string channelName, - string senderId, - string senderName, - ColourInheritable senderColour, - int senderRank, - string senderNick, - UserPermissions senderPerms, - object? data = null, - StoredEventFlags flags = StoredEventFlags.None - ) { - RunCommand( - "INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`" - + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)" - + " VALUES (@id, NOW(), @type, @target, @flags, @data" - + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", - new MySqlParameter("id", id), - new MySqlParameter("type", type), - new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName), - new MySqlParameter("flags", (byte)flags), - new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)), - new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), - new MySqlParameter("sender_name", senderName), - new MySqlParameter("sender_colour", senderColour.ToMisuzu()), - new MySqlParameter("sender_rank", senderRank), - new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick), - new MySqlParameter("sender_perms", senderPerms) +public partial class MariaDBEventStorage(string connString) : EventStorage { + private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString)); + + public void AddEvent( + long id, + string type, + string channelName, + string senderId, + string senderName, + ColourInheritable senderColour, + int senderRank, + string senderNick, + UserPermissions senderPerms, + object? data = null, + StoredEventFlags flags = StoredEventFlags.None + ) { + RunCommand( + "INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`" + + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)" + + " VALUES (@id, NOW(), @type, @target, @flags, @data" + + ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)", + new MySqlParameter("id", id), + new MySqlParameter("type", type), + new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName), + new MySqlParameter("flags", (byte)flags), + new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)), + new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), + new MySqlParameter("sender_name", senderName), + new MySqlParameter("sender_colour", senderColour.ToMisuzu()), + new MySqlParameter("sender_rank", senderRank), + new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick), + new MySqlParameter("sender_perms", senderPerms) + ); + } + + public StoredEventInfo? GetEvent(long seqId) { + try { + using MySqlDataReader? reader = RunQuery( + "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" + + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" + + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" + + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" + + " FROM `sqc_events`" + + " WHERE `event_id` = @id", + new MySqlParameter("id", seqId) ); - } - public StoredEventInfo? GetEvent(long seqId) { - try { - using MySqlDataReader? reader = RunQuery( - "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" - + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" - + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" - + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" - + " FROM `sqc_events`" - + " WHERE `event_id` = @id", - new MySqlParameter("id", seqId) - ); + if(reader is null) + return null; - if(reader is null) - return null; - - while(reader.Read()) { - StoredEventInfo evt = ReadEvent(reader); - if(evt != null) - return evt; - } - } catch(MySqlException ex) { - Logger.Write(ex); + while(reader.Read()) { + StoredEventInfo evt = ReadEvent(reader); + if(evt != null) + return evt; } - - return null; + } catch(MySqlException ex) { + Logger.Write(ex); } - private static StoredEventInfo ReadEvent(MySqlDataReader reader) { - return new StoredEventInfo( - reader.GetInt64("event_id"), - Encoding.ASCII.GetString((byte[])reader["event_type"]), - reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User( - reader.GetInt64("event_sender").ToString(), - reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"), - ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")), - reader.GetInt32("event_sender_rank"), - (UserPermissions)reader.GetInt32("event_sender_perms"), - reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick") - ), - DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")), - reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")), - reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]), - JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])), - (StoredEventFlags)reader.GetByte("event_flags") + return null; + } + + private static StoredEventInfo ReadEvent(MySqlDataReader reader) { + return new StoredEventInfo( + reader.GetInt64("event_id"), + Encoding.ASCII.GetString((byte[])reader["event_type"]), + reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User( + reader.GetInt64("event_sender").ToString(), + reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"), + ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")), + reader.GetInt32("event_sender_rank"), + (UserPermissions)reader.GetInt32("event_sender_perms"), + reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick") + ), + DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")), + reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")), + reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]), + JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])), + (StoredEventFlags)reader.GetByte("event_flags") + ); + } + + public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) { + List<StoredEventInfo> events = []; + + try { + using MySqlDataReader? reader = RunQuery( + "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" + + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" + + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" + + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" + + " FROM `sqc_events`" + + " WHERE `event_deleted` IS NULL AND (`event_target` = @target OR `event_target` IS NULL)" + + " AND `event_id` > @offset" + + " ORDER BY `event_id` DESC" + + " LIMIT @amount", + new MySqlParameter("target", channelName), + new MySqlParameter("amount", amount), + new MySqlParameter("offset", offset) ); - } + if(reader is null) + return events; - public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) { - List<StoredEventInfo> events = []; - - try { - using MySqlDataReader? reader = RunQuery( - "SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`" - + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`" - + ", UNIX_TIMESTAMP(`event_created`) AS `event_created`" - + ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`" - + " FROM `sqc_events`" - + " WHERE `event_deleted` IS NULL AND (`event_target` = @target OR `event_target` IS NULL)" - + " AND `event_id` > @offset" - + " ORDER BY `event_id` DESC" - + " LIMIT @amount", - new MySqlParameter("target", channelName), - new MySqlParameter("amount", amount), - new MySqlParameter("offset", offset) - ); - if(reader is null) - return events; - - while(reader.Read()) { - StoredEventInfo evt = ReadEvent(reader); - if(evt != null) - events.Add(evt); - } - } catch(MySqlException ex) { - Logger.Write(ex); + while(reader.Read()) { + StoredEventInfo evt = ReadEvent(reader); + if(evt != null) + events.Add(evt); } - - events.Reverse(); - - return events; + } catch(MySqlException ex) { + Logger.Write(ex); } - public void RemoveEvent(StoredEventInfo evt) { - ArgumentNullException.ThrowIfNull(evt); - RunCommand( - "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", - new MySqlParameter("id", evt.Id) - ); - } + events.Reverse(); + + return events; + } + + public void RemoveEvent(StoredEventInfo evt) { + ArgumentNullException.ThrowIfNull(evt); + RunCommand( + "UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL", + new MySqlParameter("id", evt.Id) + ); } } diff --git a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs index 1035626..055d5c4 100644 --- a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs @@ -1,87 +1,87 @@ using MySqlConnector; using SharpChat.Configuration; -namespace SharpChat.EventStorage { - public partial class MariaDBEventStorage { - public static string BuildConnString(Configuration.Config config) { - return BuildConnString( - config.ReadValue("host", "localhost"), - config.ReadValue("user", string.Empty), - config.ReadValue("pass", string.Empty), - config.ReadValue("db", "sharpchat") - ); +namespace SharpChat.EventStorage; + +public partial class MariaDBEventStorage { + public static string BuildConnString(Configuration.Config config) { + return BuildConnString( + config.ReadValue("host", "localhost"), + config.ReadValue("user", string.Empty), + config.ReadValue("pass", string.Empty), + config.ReadValue("db", "sharpchat") + ); + } + + public static string BuildConnString(string? host, string? username, string? password, string? database) { + return new MySqlConnectionStringBuilder { + Server = host, + UserID = username, + Password = password, + Database = database, + OldGuids = false, + TreatTinyAsBoolean = false, + CharacterSet = "utf8mb4", + SslMode = MySqlSslMode.None, + ForceSynchronous = true, + ConnectionTimeout = 5, + DefaultCommandTimeout = 900, // fuck it, 15 minutes + }.ToString(); + } + + private MySqlConnection GetConnection() { + MySqlConnection conn = new(ConnectionString); + conn.Open(); + return conn; + } + + private int RunCommand(string command, params MySqlParameter[] parameters) { + try { + using MySqlConnection conn = GetConnection(); + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return cmd.ExecuteNonQuery(); + } catch(MySqlException ex) { + Logger.Write(ex); } - public static string BuildConnString(string? host, string? username, string? password, string? database) { - return new MySqlConnectionStringBuilder { - Server = host, - UserID = username, - Password = password, - Database = database, - OldGuids = false, - TreatTinyAsBoolean = false, - CharacterSet = "utf8mb4", - SslMode = MySqlSslMode.None, - ForceSynchronous = true, - ConnectionTimeout = 5, - DefaultCommandTimeout = 900, // fuck it, 15 minutes - }.ToString(); + return 0; + } + + private MySqlDataReader? RunQuery(string command, params MySqlParameter[] parameters) { + try { + MySqlConnection conn = GetConnection(); + MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection); + } catch(MySqlException ex) { + Logger.Write(ex); } - private MySqlConnection GetConnection() { - MySqlConnection conn = new(ConnectionString); - conn.Open(); - return conn; + return null; + } + + private T RunQueryValue<T>(string command, params MySqlParameter[] parameters) + where T : struct { + try { + using MySqlConnection conn = GetConnection(); + using MySqlCommand cmd = conn.CreateCommand(); + if(parameters?.Length > 0) + cmd.Parameters.AddRange(parameters); + cmd.CommandText = command; + cmd.Prepare(); + + object? raw = cmd.ExecuteScalar(); + if(raw is T value) + return value; + } catch(MySqlException ex) { + Logger.Write(ex); } - private int RunCommand(string command, params MySqlParameter[] parameters) { - try { - using MySqlConnection conn = GetConnection(); - using MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - return cmd.ExecuteNonQuery(); - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return 0; - } - - private MySqlDataReader? RunQuery(string command, params MySqlParameter[] parameters) { - try { - MySqlConnection conn = GetConnection(); - MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection); - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return null; - } - - private T RunQueryValue<T>(string command, params MySqlParameter[] parameters) - where T : struct { - try { - using MySqlConnection conn = GetConnection(); - using MySqlCommand cmd = conn.CreateCommand(); - if(parameters?.Length > 0) - cmd.Parameters.AddRange(parameters); - cmd.CommandText = command; - cmd.Prepare(); - - object? raw = cmd.ExecuteScalar(); - if(raw is T value) - return value; - } catch(MySqlException ex) { - Logger.Write(ex); - } - - return default; - } + return default; } } diff --git a/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs b/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs index 3bfaac4..c62bf02 100644 --- a/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage_Migrations.cs @@ -1,84 +1,84 @@ using MySqlConnector; -namespace SharpChat.EventStorage { - public partial class MariaDBEventStorage { - private void DoMigration(string name, Action action) { - bool done = RunQueryValue<long>( - "SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name", +namespace SharpChat.EventStorage; + +public partial class MariaDBEventStorage { + private void DoMigration(string name, Action action) { + bool done = RunQueryValue<long>( + "SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name", + new MySqlParameter("name", name) + ) > 0; + if(!done) { + Logger.Write($"Running migration '{name}'..."); + action(); + RunCommand( + "INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)", new MySqlParameter("name", name) - ) > 0; - if(!done) { - Logger.Write($"Running migration '{name}'..."); - action(); - RunCommand( - "INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)", - new MySqlParameter("name", name) - ); - } - } - - public void RunMigrations() { - RunCommand( - "CREATE TABLE IF NOT EXISTS `sqc_migrations` (" - + "`migration_name` VARCHAR(255) NOT NULL," - + "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp()," - + "UNIQUE INDEX `migration_name` (`migration_name`)," - + "INDEX `migration_completed` (`migration_completed`)" - + ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;" - ); - - DoMigration("create_events_table", CreateEventsTable); - DoMigration("allow_null_target", AllowNullTarget); - DoMigration("event_data_as_medium_blob", EventDataAsMediumBlob); - DoMigration("event_user_and_nick_name_to_1000", EventUserAndNickNameTo1000); - } - - private void EventUserAndNickNameTo1000() { - RunCommand( - "ALTER TABLE `sqc_events`" - + " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`," - + " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`;" - ); - } - - private void EventDataAsMediumBlob() { - RunCommand( - "ALTER TABLE `sqc_events`" - + " CHANGE COLUMN `event_data` `event_data` MEDIUMBLOB NULL DEFAULT NULL AFTER `event_flags`;" - ); - } - - private void AllowNullTarget() { - RunCommand( - "ALTER TABLE `sqc_events`" - + " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;" - ); - } - - private void CreateEventsTable() { - RunCommand( - "CREATE TABLE `sqc_events` (" - + "`event_id` BIGINT(20) NOT NULL," - + "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL," - + "`event_sender_name` VARCHAR(255) NULL DEFAULT NULL," - + "`event_sender_colour` INT(11) NULL DEFAULT NULL," - + "`event_sender_rank` INT(11) NULL DEFAULT NULL," - + "`event_sender_nick` VARCHAR(255) NULL DEFAULT NULL," - + "`event_sender_perms` INT(11) NULL DEFAULT NULL," - + "`event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp()," - + "`event_deleted` TIMESTAMP NULL DEFAULT NULL," - + "`event_type` VARBINARY(255) NOT NULL," - + "`event_target` VARBINARY(255) NOT NULL," - + "`event_flags` TINYINT(3) UNSIGNED NOT NULL," - + "`event_data` BLOB NULL DEFAULT NULL," - + "PRIMARY KEY (`event_id`)," - + "INDEX `event_target` (`event_target`)," - + "INDEX `event_type` (`event_type`)," - + "INDEX `event_sender` (`event_sender`)," - + "INDEX `event_datetime` (`event_created`)," - + "INDEX `event_deleted` (`event_deleted`)" - + ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;" ); } } + + public void RunMigrations() { + RunCommand( + "CREATE TABLE IF NOT EXISTS `sqc_migrations` (" + + "`migration_name` VARCHAR(255) NOT NULL," + + "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp()," + + "UNIQUE INDEX `migration_name` (`migration_name`)," + + "INDEX `migration_completed` (`migration_completed`)" + + ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;" + ); + + DoMigration("create_events_table", CreateEventsTable); + DoMigration("allow_null_target", AllowNullTarget); + DoMigration("event_data_as_medium_blob", EventDataAsMediumBlob); + DoMigration("event_user_and_nick_name_to_1000", EventUserAndNickNameTo1000); + } + + private void EventUserAndNickNameTo1000() { + RunCommand( + "ALTER TABLE `sqc_events`" + + " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`," + + " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`;" + ); + } + + private void EventDataAsMediumBlob() { + RunCommand( + "ALTER TABLE `sqc_events`" + + " CHANGE COLUMN `event_data` `event_data` MEDIUMBLOB NULL DEFAULT NULL AFTER `event_flags`;" + ); + } + + private void AllowNullTarget() { + RunCommand( + "ALTER TABLE `sqc_events`" + + " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;" + ); + } + + private void CreateEventsTable() { + RunCommand( + "CREATE TABLE `sqc_events` (" + + "`event_id` BIGINT(20) NOT NULL," + + "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL," + + "`event_sender_name` VARCHAR(255) NULL DEFAULT NULL," + + "`event_sender_colour` INT(11) NULL DEFAULT NULL," + + "`event_sender_rank` INT(11) NULL DEFAULT NULL," + + "`event_sender_nick` VARCHAR(255) NULL DEFAULT NULL," + + "`event_sender_perms` INT(11) NULL DEFAULT NULL," + + "`event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp()," + + "`event_deleted` TIMESTAMP NULL DEFAULT NULL," + + "`event_type` VARBINARY(255) NOT NULL," + + "`event_target` VARBINARY(255) NOT NULL," + + "`event_flags` TINYINT(3) UNSIGNED NOT NULL," + + "`event_data` BLOB NULL DEFAULT NULL," + + "PRIMARY KEY (`event_id`)," + + "INDEX `event_target` (`event_target`)," + + "INDEX `event_type` (`event_type`)," + + "INDEX `event_sender` (`event_sender`)," + + "INDEX `event_datetime` (`event_created`)," + + "INDEX `event_deleted` (`event_deleted`)" + + ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;" + ); + } } diff --git a/SharpChat/EventStorage/StoredEventFlags.cs b/SharpChat/EventStorage/StoredEventFlags.cs index 351e86c..4cc6b81 100644 --- a/SharpChat/EventStorage/StoredEventFlags.cs +++ b/SharpChat/EventStorage/StoredEventFlags.cs @@ -1,10 +1,10 @@ -namespace SharpChat.EventStorage { - [Flags] - public enum StoredEventFlags { - None = 0, - Action = 1, - Broadcast = 1 << 1, - Log = 1 << 2, - Private = 1 << 3, - } +namespace SharpChat.EventStorage; + +[Flags] +public enum StoredEventFlags { + None = 0, + Action = 1, + Broadcast = 1 << 1, + Log = 1 << 2, + Private = 1 << 3, } diff --git a/SharpChat/EventStorage/StoredEventInfo.cs b/SharpChat/EventStorage/StoredEventInfo.cs index 922a52b..988e0df 100644 --- a/SharpChat/EventStorage/StoredEventInfo.cs +++ b/SharpChat/EventStorage/StoredEventInfo.cs @@ -1,23 +1,23 @@ using System.Text.Json; -namespace SharpChat.EventStorage { - public class StoredEventInfo( - long id, - string type, - User? sender, - DateTimeOffset created, - DateTimeOffset? deleted, - string? channelName, - JsonDocument data, - StoredEventFlags flags - ) { - public long Id { get; set; } = id; - public string Type { get; set; } = type; - public User? Sender { get; set; } = sender; - public DateTimeOffset Created { get; set; } = created; - public DateTimeOffset? Deleted { get; set; } = deleted; - public string? ChannelName { get; set; } = channelName; - public StoredEventFlags Flags { get; set; } = flags; - public JsonDocument Data { get; set; } = data; - } +namespace SharpChat.EventStorage; + +public class StoredEventInfo( + long id, + string type, + User? sender, + DateTimeOffset created, + DateTimeOffset? deleted, + string? channelName, + JsonDocument data, + StoredEventFlags flags +) { + public long Id { get; set; } = id; + public string Type { get; set; } = type; + public User? Sender { get; set; } = sender; + public DateTimeOffset Created { get; set; } = created; + public DateTimeOffset? Deleted { get; set; } = deleted; + public string? ChannelName { get; set; } = channelName; + public StoredEventFlags Flags { get; set; } = flags; + public JsonDocument Data { get; set; } = data; } diff --git a/SharpChat/EventStorage/VirtualEventStorage.cs b/SharpChat/EventStorage/VirtualEventStorage.cs index 72ccc21..7d74bbd 100644 --- a/SharpChat/EventStorage/VirtualEventStorage.cs +++ b/SharpChat/EventStorage/VirtualEventStorage.cs @@ -1,65 +1,65 @@ using System.Text.Json; -namespace SharpChat.EventStorage { - public class VirtualEventStorage : EventStorage { - private readonly Dictionary<long, StoredEventInfo> Events = []; +namespace SharpChat.EventStorage; - public void AddEvent( - long id, - string type, - string channelName, - string senderId, - string senderName, - ColourInheritable senderColour, - int senderRank, - string senderNick, - UserPermissions senderPerms, - object? data = null, - StoredEventFlags flags = StoredEventFlags.None - ) { - Events.Add( +public class VirtualEventStorage : EventStorage { + private readonly Dictionary<long, StoredEventInfo> Events = []; + + public void AddEvent( + long id, + string type, + string channelName, + string senderId, + string senderName, + ColourInheritable senderColour, + int senderRank, + string senderNick, + UserPermissions senderPerms, + object? data = null, + StoredEventFlags flags = StoredEventFlags.None + ) { + Events.Add( + id, + new( id, - new( - id, - type, - long.TryParse(senderId, out long senderId64) && senderId64 > 0 - ? new User( - senderId, - senderName, - senderColour, - senderRank, - senderPerms, - senderNick - ) - : null, - DateTimeOffset.Now, - null, - channelName, - JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)), - flags - ) - ); + type, + long.TryParse(senderId, out long senderId64) && senderId64 > 0 + ? new User( + senderId, + senderName, + senderColour, + senderRank, + senderPerms, + senderNick + ) + : null, + DateTimeOffset.Now, + null, + channelName, + JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)), + flags + ) + ); + } + + public StoredEventInfo? GetEvent(long seqId) { + return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null; + } + + public void RemoveEvent(StoredEventInfo evt) { + ArgumentNullException.ThrowIfNull(evt); + Events.Remove(evt.Id); + } + + public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) { + IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName); + + int start = subset.Count() - offset - amount; + if(start < 0) { + amount += start; + start = 0; } - public StoredEventInfo? GetEvent(long seqId) { - return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null; - } - - public void RemoveEvent(StoredEventInfo evt) { - ArgumentNullException.ThrowIfNull(evt); - Events.Remove(evt.Id); - } - - public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) { - IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName); - - int start = subset.Count() - offset - amount; - if(start < 0) { - amount += start; - start = 0; - } - - return [.. subset.Skip(start).Take(amount)]; - } + return [.. subset.Skip(start).Take(amount)]; } } diff --git a/SharpChat/Events/ChatEvent.cs b/SharpChat/Events/ChatEvent.cs index 768b185..a3f0149 100644 --- a/SharpChat/Events/ChatEvent.cs +++ b/SharpChat/Events/ChatEvent.cs @@ -1,3 +1,3 @@ -namespace SharpChat.Events { - public interface ChatEvent {} -} +namespace SharpChat.Events; + +public interface ChatEvent {} diff --git a/SharpChat/Events/MessageCreateEvent.cs b/SharpChat/Events/MessageCreateEvent.cs index 22bae88..883726c 100644 --- a/SharpChat/Events/MessageCreateEvent.cs +++ b/SharpChat/Events/MessageCreateEvent.cs @@ -1,31 +1,31 @@ -namespace SharpChat.Events { - public class MessageCreateEvent( - long msgId, - string channelName, - string senderId, - string senderName, - ColourInheritable senderColour, - int senderRank, - string senderNickName, - UserPermissions senderPerms, - DateTimeOffset msgCreated, - string msgText, - bool isPrivate, - bool isAction, - bool isBroadcast - ) : ChatEvent { - public long MessageId { get; } = msgId; - public string ChannelName { get; } = channelName; - public string SenderId { get; } = senderId; - public string SenderName { get; } = senderName; - public ColourInheritable SenderColour { get; } = senderColour; - public int SenderRank { get; } = senderRank; - public string SenderNickName { get; } = senderNickName; - public UserPermissions SenderPerms { get; } = senderPerms; - public DateTimeOffset MessageCreated { get; } = msgCreated; - public string MessageText { get; } = msgText; - public bool IsPrivate { get; } = isPrivate; - public bool IsAction { get; } = isAction; - public bool IsBroadcast { get; } = isBroadcast; - } +namespace SharpChat.Events; + +public class MessageCreateEvent( + long msgId, + string channelName, + string senderId, + string senderName, + ColourInheritable senderColour, + int senderRank, + string senderNickName, + UserPermissions senderPerms, + DateTimeOffset msgCreated, + string msgText, + bool isPrivate, + bool isAction, + bool isBroadcast +) : ChatEvent { + public long MessageId { get; } = msgId; + public string ChannelName { get; } = channelName; + public string SenderId { get; } = senderId; + public string SenderName { get; } = senderName; + public ColourInheritable SenderColour { get; } = senderColour; + public int SenderRank { get; } = senderRank; + public string SenderNickName { get; } = senderNickName; + public UserPermissions SenderPerms { get; } = senderPerms; + public DateTimeOffset MessageCreated { get; } = msgCreated; + public string MessageText { get; } = msgText; + public bool IsPrivate { get; } = isPrivate; + public bool IsAction { get; } = isAction; + public bool IsBroadcast { get; } = isBroadcast; } diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs index fa8fe78..7ec67cb 100644 --- a/SharpChat/Program.cs +++ b/SharpChat/Program.cs @@ -3,145 +3,145 @@ using SharpChat.EventStorage; using SharpChat.Flashii; using System.Text; -namespace SharpChat { - public class Program { - public const string CONFIG = "sharpchat.cfg"; +namespace SharpChat; - public static void Main() { - Console.WriteLine(@" _____ __ ________ __ "); - Console.WriteLine(@" / ___// /_ ____ __________ / ____/ /_ ____ _/ /_"); - Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/"); - Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ "); - Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ "); - /**/Console.Write(@" /__/"); - if(SharpInfo.IsDebugBuild) { - Console.WriteLine(); - Console.Write(@"== "); - Console.Write(SharpInfo.VersionString); - Console.WriteLine(@" == DBG =="); - } else - Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' ')); +public class Program { + public const string CONFIG = "sharpchat.cfg"; - using ManualResetEvent mre = new(false); - bool hasCancelled = false; + public static void Main() { + Console.WriteLine(@" _____ __ ________ __ "); + Console.WriteLine(@" / ___// /_ ____ __________ / ____/ /_ ____ _/ /_"); + Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/"); + Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ "); + Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ "); + /**/Console.Write(@" /__/"); + if(SharpInfo.IsDebugBuild) { + Console.WriteLine(); + Console.Write(@"== "); + Console.Write(SharpInfo.VersionString); + Console.WriteLine(@" == DBG =="); + } else + Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' ')); - void cancelKeyPressHandler(object? sender, ConsoleCancelEventArgs ev) { - Console.CancelKeyPress -= cancelKeyPressHandler; - hasCancelled = true; - ev.Cancel = true; - mre.Set(); - }; - Console.CancelKeyPress += cancelKeyPressHandler; + using ManualResetEvent mre = new(false); + bool hasCancelled = false; - if(hasCancelled) return; + void cancelKeyPressHandler(object? sender, ConsoleCancelEventArgs ev) { + Console.CancelKeyPress -= cancelKeyPressHandler; + hasCancelled = true; + ev.Cancel = true; + mre.Set(); + }; + Console.CancelKeyPress += cancelKeyPressHandler; - string configFile = CONFIG; + if(hasCancelled) return; - // If the config file doesn't exist and we're using the default path, run the converter - if(!File.Exists(configFile) && configFile == CONFIG) - ConvertConfiguration(); + string configFile = CONFIG; - using StreamConfig config = StreamConfig.FromPath(configFile); + // If the config file doesn't exist and we're using the default path, run the converter + if(!File.Exists(configFile) && configFile == CONFIG) + ConvertConfiguration(); - if(hasCancelled) return; + using StreamConfig config = StreamConfig.FromPath(configFile); - using HttpClient httpClient = new(new HttpClientHandler() { - UseProxy = false, - }); - httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName); + if(hasCancelled) return; - if(hasCancelled) return; + using HttpClient httpClient = new(new HttpClientHandler() { + UseProxy = false, + }); + httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName); - FlashiiClient flashii = new(httpClient, config.ScopeTo("msz")); + if(hasCancelled) return; - if(hasCancelled) return; + FlashiiClient flashii = new(httpClient, config.ScopeTo("msz")); - EventStorage.EventStorage evtStore; - if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) { - evtStore = new VirtualEventStorage(); - } else { - MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb"))); - evtStore = mdbes; - mdbes.RunMigrations(); - } + if(hasCancelled) return; - if(hasCancelled) return; - - using SockChatServer scs = new(flashii, flashii, evtStore, config.ScopeTo("chat")); - scs.Listen(mre); - - mre.WaitOne(); + EventStorage.EventStorage evtStore; + if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) { + evtStore = new VirtualEventStorage(); + } else { + MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb"))); + evtStore = mdbes; + mdbes.RunMigrations(); } - private static void ConvertConfiguration() { - using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - s.SetLength(0); - s.Flush(); + if(hasCancelled) return; - using StreamWriter sw = new(s, new UTF8Encoding(false)); - sw.WriteLine("# and ; can be used at the start of a line for comments."); - sw.WriteLine(); + using SockChatServer scs = new(flashii, flashii, evtStore, config.ScopeTo("chat")); + scs.Listen(mre); - sw.WriteLine("# General Configuration"); - sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}"); - sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}"); - sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}"); - sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}"); + mre.WaitOne(); + } + + private static void ConvertConfiguration() { + using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + s.SetLength(0); + s.Flush(); + + using StreamWriter sw = new(s, new UTF8Encoding(false)); + sw.WriteLine("# and ; can be used at the start of a line for comments."); + sw.WriteLine(); + + sw.WriteLine("# General Configuration"); + sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}"); + sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}"); + sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}"); + sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}"); - sw.WriteLine(); - sw.WriteLine("# Channels"); - sw.WriteLine("chat:channels lounge staff"); - sw.WriteLine(); + sw.WriteLine(); + sw.WriteLine("# Channels"); + sw.WriteLine("chat:channels lounge staff"); + sw.WriteLine(); - sw.WriteLine("# Lounge channel settings"); - sw.WriteLine("chat:channels:lounge:name Lounge"); - sw.WriteLine("chat:channels:lounge:autoJoin true"); - sw.WriteLine(); + sw.WriteLine("# Lounge channel settings"); + sw.WriteLine("chat:channels:lounge:name Lounge"); + sw.WriteLine("chat:channels:lounge:autoJoin true"); + sw.WriteLine(); - sw.WriteLine("# Staff channel settings"); - sw.WriteLine("chat:channels:staff:name Staff"); - sw.WriteLine("chat:channels:staff:minRank 5"); + sw.WriteLine("# Staff channel settings"); + sw.WriteLine("chat:channels:staff:name Staff"); + sw.WriteLine("chat:channels:staff:minRank 5"); - const string msz_secret = "login_key.txt"; - const string msz_url = "msz_url.txt"; + const string msz_secret = "login_key.txt"; + const string msz_url = "msz_url.txt"; - sw.WriteLine(); - sw.WriteLine("# Misuzu integration settings"); - if(File.Exists(msz_secret)) - sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim())); - else - sw.WriteLine("#msz:secret woomy"); - if(File.Exists(msz_url)) - sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim())); - else - sw.WriteLine("#msz:url https://flashii.net/_sockchat"); + sw.WriteLine(); + sw.WriteLine("# Misuzu integration settings"); + if(File.Exists(msz_secret)) + sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim())); + else + sw.WriteLine("#msz:secret woomy"); + if(File.Exists(msz_url)) + sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim())); + else + sw.WriteLine("#msz:url https://flashii.net/_sockchat"); - const string mdb_config = @"mariadb.txt"; - string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : []; + const string mdb_config = @"mariadb.txt"; + string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : []; - sw.WriteLine(); - sw.WriteLine("# MariaDB configuration"); - if(mdbCfg.Length > 0) - sw.WriteLine($"mariadb:host {mdbCfg[0]}"); - else - sw.WriteLine($"#mariadb:host <username>"); - if(mdbCfg.Length > 1) - sw.WriteLine($"mariadb:user {mdbCfg[1]}"); - else - sw.WriteLine($"#mariadb:user <username>"); - if(mdbCfg.Length > 2) - sw.WriteLine($"mariadb:pass {mdbCfg[2]}"); - else - sw.WriteLine($"#mariadb:pass <password>"); - if(mdbCfg.Length > 3) - sw.WriteLine($"mariadb:db {mdbCfg[3]}"); - else - sw.WriteLine($"#mariadb:db <database>"); + sw.WriteLine(); + sw.WriteLine("# MariaDB configuration"); + if(mdbCfg.Length > 0) + sw.WriteLine($"mariadb:host {mdbCfg[0]}"); + else + sw.WriteLine($"#mariadb:host <username>"); + if(mdbCfg.Length > 1) + sw.WriteLine($"mariadb:user {mdbCfg[1]}"); + else + sw.WriteLine($"#mariadb:user <username>"); + if(mdbCfg.Length > 2) + sw.WriteLine($"mariadb:pass {mdbCfg[2]}"); + else + sw.WriteLine($"#mariadb:pass <password>"); + if(mdbCfg.Length > 3) + sw.WriteLine($"mariadb:db {mdbCfg[3]}"); + else + sw.WriteLine($"#mariadb:db <database>"); - sw.Flush(); - } + sw.Flush(); } } diff --git a/SharpChat/SharpChatWebSocketServer.cs b/SharpChat/SharpChatWebSocketServer.cs index 423ecc8..701fc34 100644 --- a/SharpChat/SharpChatWebSocketServer.cs +++ b/SharpChat/SharpChatWebSocketServer.cs @@ -12,156 +12,156 @@ using System.Text; // Fleck's Socket wrapper doesn't provide any way to do this with the normally provided APIs // https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs -namespace SharpChat { - public class SharpChatWebSocketServer : IWebSocketServer { +namespace SharpChat; - private readonly string _scheme; - private readonly IPAddress _locationIP; - private Action<IWebSocketConnection> _config; +public class SharpChatWebSocketServer : IWebSocketServer { - public SharpChatWebSocketServer(string location, bool supportDualStack = true) { - Uri uri = new(location); + private readonly string _scheme; + private readonly IPAddress _locationIP; + private Action<IWebSocketConnection> _config; - Port = uri.Port; - Location = location; - SupportDualStack = supportDualStack; + public SharpChatWebSocketServer(string location, bool supportDualStack = true) { + Uri uri = new(location); - _locationIP = ParseIPAddress(uri); - _scheme = uri.Scheme; - Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); + Port = uri.Port; + Location = location; + SupportDualStack = supportDualStack; - if(SupportDualStack && Type.GetType("Mono.Runtime") == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); - } + _locationIP = ParseIPAddress(uri); + _scheme = uri.Scheme; + Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); - ListenerSocket = new SocketWrapper(socket); - SupportedSubProtocols = []; + if(SupportDualStack && Type.GetType("Mono.Runtime") == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); } - public ISocket ListenerSocket { get; set; } - public string Location { get; private set; } - public bool SupportDualStack { get; } - public int Port { get; private set; } - public X509Certificate2 Certificate { get; set; } - public SslProtocols EnabledSslProtocols { get; set; } - public IEnumerable<string> SupportedSubProtocols { get; set; } - public bool RestartAfterListenError { get; set; } + ListenerSocket = new SocketWrapper(socket); + SupportedSubProtocols = []; + } - public bool IsSecure { - get { return _scheme == "wss" && Certificate != null; } - } + public ISocket ListenerSocket { get; set; } + public string Location { get; private set; } + public bool SupportDualStack { get; } + public int Port { get; private set; } + public X509Certificate2 Certificate { get; set; } + public SslProtocols EnabledSslProtocols { get; set; } + public IEnumerable<string> SupportedSubProtocols { get; set; } + public bool RestartAfterListenError { get; set; } - public void Dispose() { - ListenerSocket.Dispose(); - GC.SuppressFinalize(this); - } + public bool IsSecure { + get { return _scheme == "wss" && Certificate != null; } + } - private static IPAddress ParseIPAddress(Uri uri) { - string ipStr = uri.Host; + public void Dispose() { + ListenerSocket.Dispose(); + GC.SuppressFinalize(this); + } - if(ipStr == "0.0.0.0") { - return IPAddress.Any; - } else if(ipStr == "[0000:0000:0000:0000:0000:0000:0000:0000]") { - return IPAddress.IPv6Any; - } else { - try { - return IPAddress.Parse(ipStr); - } catch(Exception ex) { - throw new FormatException("Failed to parse the IP address part of the location. Please make sure you specify a valid IP address. Use 0.0.0.0 or [::] to listen on all interfaces.", ex); - } - } - } + private static IPAddress ParseIPAddress(Uri uri) { + string ipStr = uri.Host; - public void Start(Action<IWebSocketConnection> config) { - IPEndPoint ipLocal = new(_locationIP, Port); - ListenerSocket.Bind(ipLocal); - ListenerSocket.Listen(100); - Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port; - FleckLog.Info(string.Format("Server started at {0} (actual port {1})", Location, Port)); - if(_scheme == "wss") { - if(Certificate == null) { - FleckLog.Error("Scheme cannot be 'wss' without a Certificate"); - return; - } - - // makes dotnet shut up, TLS is handled by NGINX anyway - // if(EnabledSslProtocols == SslProtocols.None) { - // EnabledSslProtocols = SslProtocols.Tls; - // FleckLog.Debug("Using default TLS 1.0 security protocol."); - // } - } - ListenForClients(); - _config = config; - } - - private void ListenForClients() { - ListenerSocket.Accept(OnClientConnect, e => { - FleckLog.Error("Listener socket is closed", e); - if(RestartAfterListenError) { - FleckLog.Info("Listener socket restarting"); - try { - ListenerSocket.Dispose(); - Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); - ListenerSocket = new SocketWrapper(socket); - Start(_config); - FleckLog.Info("Listener socket restarted"); - } catch(Exception ex) { - FleckLog.Error("Listener could not be restarted", ex); - } - } - }); - } - - private void OnClientConnect(ISocket clientSocket) { - if(clientSocket == null) return; // socket closed - - FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString())); - ListenForClients(); - - WebSocketConnection connection = null; - - connection = new WebSocketConnection( - clientSocket, - _config, - bytes => RequestParser.Parse(bytes, _scheme), - r => { - try { - return HandlerFactory.BuildHandler( - r, s => connection.OnMessage(s), connection.Close, b => connection.OnBinary(b), - b => connection.OnPing(b), b => connection.OnPong(b) - ); - } catch(WebSocketException) { - const string responseMsg = "HTTP/1.1 200 OK\r\n" - + "Date: {0}\r\n" - + "Server: SharpChat\r\n" - + "Content-Length: {1}\r\n" - + "Content-Type: text/html; charset=utf-8\r\n" - + "Connection: close\r\n" - + "\r\n" - + "{2}"; - string responseBody = File.Exists("http-motd.txt") ? File.ReadAllText("http-motd.txt") : "SharpChat"; - - clientSocket.Stream.Write(Encoding.UTF8.GetBytes(string.Format( - responseMsg, DateTimeOffset.Now.ToString("r"), Encoding.UTF8.GetByteCount(responseBody), responseBody - ))); - clientSocket.Close(); - return null; - } - }, - s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s)); - - if(IsSecure) { - FleckLog.Debug("Authenticating Secure Connection"); - clientSocket - .Authenticate(Certificate, - EnabledSslProtocols, - connection.StartReceiving, - e => FleckLog.Warn("Failed to Authenticate", e)); - } else { - connection.StartReceiving(); + if(ipStr == "0.0.0.0") { + return IPAddress.Any; + } else if(ipStr == "[0000:0000:0000:0000:0000:0000:0000:0000]") { + return IPAddress.IPv6Any; + } else { + try { + return IPAddress.Parse(ipStr); + } catch(Exception ex) { + throw new FormatException("Failed to parse the IP address part of the location. Please make sure you specify a valid IP address. Use 0.0.0.0 or [::] to listen on all interfaces.", ex); } } } + + public void Start(Action<IWebSocketConnection> config) { + IPEndPoint ipLocal = new(_locationIP, Port); + ListenerSocket.Bind(ipLocal); + ListenerSocket.Listen(100); + Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port; + FleckLog.Info(string.Format("Server started at {0} (actual port {1})", Location, Port)); + if(_scheme == "wss") { + if(Certificate == null) { + FleckLog.Error("Scheme cannot be 'wss' without a Certificate"); + return; + } + + // makes dotnet shut up, TLS is handled by NGINX anyway + // if(EnabledSslProtocols == SslProtocols.None) { + // EnabledSslProtocols = SslProtocols.Tls; + // FleckLog.Debug("Using default TLS 1.0 security protocol."); + // } + } + ListenForClients(); + _config = config; + } + + private void ListenForClients() { + ListenerSocket.Accept(OnClientConnect, e => { + FleckLog.Error("Listener socket is closed", e); + if(RestartAfterListenError) { + FleckLog.Info("Listener socket restarting"); + try { + ListenerSocket.Dispose(); + Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); + ListenerSocket = new SocketWrapper(socket); + Start(_config); + FleckLog.Info("Listener socket restarted"); + } catch(Exception ex) { + FleckLog.Error("Listener could not be restarted", ex); + } + } + }); + } + + private void OnClientConnect(ISocket clientSocket) { + if(clientSocket == null) return; // socket closed + + FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString())); + ListenForClients(); + + WebSocketConnection connection = null; + + connection = new WebSocketConnection( + clientSocket, + _config, + bytes => RequestParser.Parse(bytes, _scheme), + r => { + try { + return HandlerFactory.BuildHandler( + r, s => connection.OnMessage(s), connection.Close, b => connection.OnBinary(b), + b => connection.OnPing(b), b => connection.OnPong(b) + ); + } catch(WebSocketException) { + const string responseMsg = "HTTP/1.1 200 OK\r\n" + + "Date: {0}\r\n" + + "Server: SharpChat\r\n" + + "Content-Length: {1}\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Connection: close\r\n" + + "\r\n" + + "{2}"; + string responseBody = File.Exists("http-motd.txt") ? File.ReadAllText("http-motd.txt") : "SharpChat"; + + clientSocket.Stream.Write(Encoding.UTF8.GetBytes(string.Format( + responseMsg, DateTimeOffset.Now.ToString("r"), Encoding.UTF8.GetByteCount(responseBody), responseBody + ))); + clientSocket.Close(); + return null; + } + }, + s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s)); + + if(IsSecure) { + FleckLog.Debug("Authenticating Secure Connection"); + clientSocket + .Authenticate(Certificate, + EnabledSslProtocols, + connection.StartReceiving, + e => FleckLog.Warn("Failed to Authenticate", e)); + } else { + connection.StartReceiving(); + } + } } diff --git a/SharpChat/SockChat/S2CPackets/ContextMessageS2CPacket.cs b/SharpChat/SockChat/S2CPackets/ContextMessageS2CPacket.cs index 1b8172f..95982fb 100644 --- a/SharpChat/SockChat/S2CPackets/ContextMessageS2CPacket.cs +++ b/SharpChat/SockChat/S2CPackets/ContextMessageS2CPacket.cs @@ -1,115 +1,115 @@ using SharpChat.EventStorage; using System.Text; -namespace SharpChat.SockChat.S2CPackets { - public class ContextMessageS2CPacket(StoredEventInfo evt, bool notify = false) : S2CPacket { - public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt)); +namespace SharpChat.SockChat.S2CPackets; - public string Pack() { - bool isAction = Event.Flags.HasFlag(StoredEventFlags.Action); - bool isBroadcast = Event.Flags.HasFlag(StoredEventFlags.Broadcast); - bool isPrivate = Event.Flags.HasFlag(StoredEventFlags.Private); +public class ContextMessageS2CPacket(StoredEventInfo evt, bool notify = false) : S2CPacket { + public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt)); - StringBuilder sb = new(); + public string Pack() { + bool isAction = Event.Flags.HasFlag(StoredEventFlags.Action); + bool isBroadcast = Event.Flags.HasFlag(StoredEventFlags.Broadcast); + bool isPrivate = Event.Flags.HasFlag(StoredEventFlags.Private); - sb.Append("7\t1\t"); - sb.Append(Event.Created.ToUnixTimeSeconds()); - sb.Append('\t'); + StringBuilder sb = new(); - switch(Event.Type) { - case "msg:add": - case "SharpChat.Events.ChatMessage": - if(isBroadcast || Event.Sender is null) { - sb.Append("-1\tChatBot\tinherit\t\t0\fsay\f"); - } else { - sb.Append(Event.Sender.UserId); - sb.Append('\t'); - sb.Append(Event.Sender.LegacyNameWithStatus); - sb.Append('\t'); - sb.Append(Event.Sender.Colour); - sb.Append('\t'); - sb.Append(Event.Sender.Rank); - sb.Append(' '); - sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.KickUser) ? '1' : '0'); - sb.Append(' '); - sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); - sb.Append(' '); - sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); - sb.Append(' '); - sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.CreateChannel) ? (Event.Sender.Permissions.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); - sb.Append('\t'); - } + sb.Append("7\t1\t"); + sb.Append(Event.Created.ToUnixTimeSeconds()); + sb.Append('\t'); - if(isAction) - sb.Append("<i>"); + switch(Event.Type) { + case "msg:add": + case "SharpChat.Events.ChatMessage": + if(isBroadcast || Event.Sender is null) { + sb.Append("-1\tChatBot\tinherit\t\t0\fsay\f"); + } else { + sb.Append(Event.Sender.UserId); + sb.Append('\t'); + sb.Append(Event.Sender.LegacyNameWithStatus); + sb.Append('\t'); + sb.Append(Event.Sender.Colour); + sb.Append('\t'); + sb.Append(Event.Sender.Rank); + sb.Append(' '); + sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.KickUser) ? '1' : '0'); + sb.Append(' '); + sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.ViewLogs) ? '1' : '0'); + sb.Append(' '); + sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0'); + sb.Append(' '); + sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.CreateChannel) ? (Event.Sender.Permissions.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0'); + sb.Append('\t'); + } - sb.Append( - (Event.Data.RootElement.GetProperty("text").GetString()? - .Replace("<", "<") - .Replace(">", ">") - .Replace("\n", " <br/> ") - .Replace("\t", " ")) ?? string.Empty - ); + if(isAction) + sb.Append("<i>"); - if(isAction) - sb.Append("</i>"); - break; + sb.Append( + (Event.Data.RootElement.GetProperty("text").GetString()? + .Replace("<", "<") + .Replace(">", ">") + .Replace("\n", " <br/> ") + .Replace("\t", " ")) ?? string.Empty + ); - case "user:connect": - case "SharpChat.Events.UserConnectEvent": - sb.Append("-1\tChatBot\tinherit\t\t0\fjoin\f"); - sb.Append(Event.Sender?.LegacyName ?? "?????"); - break; + if(isAction) + sb.Append("</i>"); + break; - case "chan:join": - case "SharpChat.Events.UserChannelJoinEvent": - sb.Append("-1\tChatBot\tinherit\t\t0\fjchan\f"); - sb.Append(Event.Sender?.LegacyName ?? "?????"); - break; + case "user:connect": + case "SharpChat.Events.UserConnectEvent": + sb.Append("-1\tChatBot\tinherit\t\t0\fjoin\f"); + sb.Append(Event.Sender?.LegacyName ?? "?????"); + break; - case "chan:leave": - case "SharpChat.Events.UserChannelLeaveEvent": - sb.Append("-1\tChatBot\tinherit\t\t0\flchan\f"); - sb.Append(Event.Sender?.LegacyName ?? "?????"); - break; + case "chan:join": + case "SharpChat.Events.UserChannelJoinEvent": + sb.Append("-1\tChatBot\tinherit\t\t0\fjchan\f"); + sb.Append(Event.Sender?.LegacyName ?? "?????"); + break; - case "user:disconnect": - case "SharpChat.Events.UserDisconnectEvent": - sb.Append("-1\tChatBot\tinherit\t\t0\f"); + case "chan:leave": + case "SharpChat.Events.UserChannelLeaveEvent": + sb.Append("-1\tChatBot\tinherit\t\t0\flchan\f"); + sb.Append(Event.Sender?.LegacyName ?? "?????"); + break; - switch((UserDisconnectS2CPacket.Reason)Event.Data.RootElement.GetProperty("reason").GetByte()) { - case UserDisconnectS2CPacket.Reason.Flood: - sb.Append("flood"); - break; - case UserDisconnectS2CPacket.Reason.Kicked: - sb.Append("kick"); - break; - case UserDisconnectS2CPacket.Reason.TimeOut: - sb.Append("timeout"); - break; - case UserDisconnectS2CPacket.Reason.Leave: - default: - sb.Append("leave"); - break; - } + case "user:disconnect": + case "SharpChat.Events.UserDisconnectEvent": + sb.Append("-1\tChatBot\tinherit\t\t0\f"); - sb.Append('\f'); - sb.Append(Event.Sender?.LegacyName ?? "?????"); - break; - } + switch((UserDisconnectS2CPacket.Reason)Event.Data.RootElement.GetProperty("reason").GetByte()) { + case UserDisconnectS2CPacket.Reason.Flood: + sb.Append("flood"); + break; + case UserDisconnectS2CPacket.Reason.Kicked: + sb.Append("kick"); + break; + case UserDisconnectS2CPacket.Reason.TimeOut: + sb.Append("timeout"); + break; + case UserDisconnectS2CPacket.Reason.Leave: + default: + sb.Append("leave"); + break; + } - sb.Append('\t'); - sb.Append(Event.Id); - sb.Append('\t'); - sb.Append(notify ? '1' : '0'); - sb.AppendFormat( - "\t1{0}0{1}{2}", - isAction ? '1' : '0', - isAction ? '0' : '1', - isPrivate ? '1' : '0' - ); - - return sb.ToString(); + sb.Append('\f'); + sb.Append(Event.Sender?.LegacyName ?? "?????"); + break; } + + sb.Append('\t'); + sb.Append(Event.Id); + sb.Append('\t'); + sb.Append(notify ? '1' : '0'); + sb.AppendFormat( + "\t1{0}0{1}{2}", + isAction ? '1' : '0', + isAction ? '0' : '1', + isPrivate ? '1' : '0' + ); + + return sb.ToString(); } } diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index c823cb7..f4bebf8 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -7,234 +7,234 @@ using SharpChat.Configuration; using SharpChat.SockChat.S2CPackets; using System.Net; -namespace SharpChat { - public class SockChatServer : IDisposable { - 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; - public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9; +namespace SharpChat; - public IWebSocketServer Server { get; } - public Context Context { get; } +public class SockChatServer : IDisposable { + 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; + public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9; - private readonly BansClient BansClient; + public IWebSocketServer Server { get; } + public Context Context { get; } - private readonly CachedValue<int> MaxMessageLength; - private readonly CachedValue<int> MaxConnections; - private readonly CachedValue<int> FloodKickLength; - private readonly CachedValue<int> FloodKickExemptRank; + private readonly BansClient BansClient; - private readonly List<C2SPacketHandler> GuestHandlers = []; - private readonly List<C2SPacketHandler> AuthedHandlers = []; - private readonly SendMessageC2SPacketHandler SendMessageHandler; + private readonly CachedValue<int> MaxMessageLength; + private readonly CachedValue<int> MaxConnections; + private readonly CachedValue<int> FloodKickLength; + private readonly CachedValue<int> FloodKickExemptRank; - private bool IsShuttingDown = false; + private readonly List<C2SPacketHandler> GuestHandlers = []; + private readonly List<C2SPacketHandler> AuthedHandlers = []; + private readonly SendMessageC2SPacketHandler SendMessageHandler; - private static readonly string[] DEFAULT_CHANNELS = ["lounge"]; + private bool IsShuttingDown = false; - private Channel DefaultChannel { get; set; } + private static readonly string[] DEFAULT_CHANNELS = ["lounge"]; - public SockChatServer( - AuthClient authClient, - BansClient bansClient, - EventStorage.EventStorage evtStore, - Config config - ) { - Logger.Write("Initialising Sock Chat server..."); + private Channel DefaultChannel { get; set; } - BansClient = bansClient; + public SockChatServer( + AuthClient authClient, + BansClient bansClient, + EventStorage.EventStorage evtStore, + Config config + ) { + Logger.Write("Initialising Sock Chat server..."); - MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX); - MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); - FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH); - FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK); + BansClient = bansClient; - Context = new Context(evtStore); + MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX); + MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); + FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH); + FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK); - string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS); - if(channelNames is not null) - foreach(string channelName in channelNames) { - Config channelCfg = config.ScopeTo($"channels:{channelName}"); + Context = new Context(evtStore); - string name = channelCfg.SafeReadValue("name", string.Empty)!; - if(string.IsNullOrWhiteSpace(name)) - name = channelName; + string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS); + if(channelNames is not null) + foreach(string channelName in channelNames) { + Config channelCfg = config.ScopeTo($"channels:{channelName}"); - Channel channelInfo = new( - name, - channelCfg.SafeReadValue("password", string.Empty)!, - rank: channelCfg.SafeReadValue("minRank", 0) - ); + string name = channelCfg.SafeReadValue("name", string.Empty)!; + if(string.IsNullOrWhiteSpace(name)) + name = channelName; - Context.Channels.Add(channelInfo); - DefaultChannel ??= channelInfo; - } + Channel channelInfo = new( + name, + channelCfg.SafeReadValue("password", string.Empty)!, + rank: channelCfg.SafeReadValue("minRank", 0) + ); - if(DefaultChannel is null) - throw new Exception("The default channel could not be determined."); + Context.Channels.Add(channelInfo); + DefaultChannel ??= channelInfo; + } - GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections)); + if(DefaultChannel is null) + throw new Exception("The default channel could not be determined."); - AuthedHandlers.AddRange([ - new PingC2SPacketHandler(authClient), - SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength), - ]); + GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections)); - SendMessageHandler.AddCommands([ - new AFKClientCommand(), - new NickClientCommand(), - new WhisperClientCommand(), - new ActionClientCommand(), - new WhoClientCommand(), - new JoinChannelClientCommand(), - new CreateChannelClientCommand(), - new DeleteChannelClientCommand(), - new PasswordChannelClientCommand(), - new RankChannelClientCommand(), - new BroadcastClientCommand(), - new DeleteMessageClientCommand(), - new KickBanClientCommand(bansClient), - new PardonUserClientCommand(bansClient), - new PardonAddressClientCommand(bansClient), - new BanListClientCommand(bansClient), - new RemoteAddressClientCommand(), - ]); + AuthedHandlers.AddRange([ + new PingC2SPacketHandler(authClient), + SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength), + ]); - ushort port = config.SafeReadValue("port", DEFAULT_PORT); - Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}"); + SendMessageHandler.AddCommands([ + new AFKClientCommand(), + new NickClientCommand(), + new WhisperClientCommand(), + new ActionClientCommand(), + new WhoClientCommand(), + new JoinChannelClientCommand(), + new CreateChannelClientCommand(), + new DeleteChannelClientCommand(), + new PasswordChannelClientCommand(), + new RankChannelClientCommand(), + new BroadcastClientCommand(), + new DeleteMessageClientCommand(), + new KickBanClientCommand(bansClient), + new PardonUserClientCommand(bansClient), + new PardonAddressClientCommand(bansClient), + new BanListClientCommand(bansClient), + new RemoteAddressClientCommand(), + ]); + + ushort port = config.SafeReadValue("port", DEFAULT_PORT); + Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}"); + } + + public void Listen(ManualResetEvent waitHandle) { + if(waitHandle != null) + SendMessageHandler.AddCommand(new ShutdownRestartClientCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); + + Server.Start(sock => { + if(IsShuttingDown) { + sock.Close(1013); + return; + } + + Connection conn = new(sock); + Context.Connections.Add(conn); + + sock.OnOpen = () => OnOpen(conn).Wait(); + sock.OnClose = () => OnClose(conn).Wait(); + sock.OnError = err => OnError(conn, err).Wait(); + sock.OnMessage = msg => OnMessage(conn, msg).Wait(); + }); + + Logger.Write("Listening..."); + } + + private async Task OnOpen(Connection conn) { + Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}"); + await Context.SafeUpdate(); + } + + private async Task OnError(Connection conn, Exception ex) { + Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}"); + await Context.SafeUpdate(); + } + + private async Task OnClose(Connection conn) { + Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}"); + + Context.ContextAccess.Wait(); + try { + Context.Connections.Remove(conn); + + if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User)) + await Context.HandleDisconnect(conn.User); + + await Context.Update(); + } finally { + Context.ContextAccess.Release(); } + } - public void Listen(ManualResetEvent waitHandle) { - if(waitHandle != null) - SendMessageHandler.AddCommand(new ShutdownRestartClientCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); + private async Task OnMessage(Connection conn, string msg) { + await Context.SafeUpdate(); - Server.Start(sock => { - if(IsShuttingDown) { - sock.Close(1013); - return; - } - - Connection conn = new(sock); - Context.Connections.Add(conn); - - sock.OnOpen = () => OnOpen(conn).Wait(); - sock.OnClose = () => OnClose(conn).Wait(); - sock.OnError = err => OnError(conn, err).Wait(); - sock.OnMessage = msg => OnMessage(conn, msg).Wait(); - }); - - Logger.Write("Listening..."); - } - - private async Task OnOpen(Connection conn) { - Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}"); - await Context.SafeUpdate(); - } - - private async Task OnError(Connection conn, Exception ex) { - Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}"); - await Context.SafeUpdate(); - } - - private async Task OnClose(Connection conn) { - Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}"); + // this doesn't affect non-authed connections????? + if(conn.User is not null && conn.User.Rank < FloodKickExemptRank) { + User? banUser = null; + string banAddr = string.Empty; + TimeSpan banDuration = TimeSpan.MinValue; Context.ContextAccess.Wait(); try { - Context.Connections.Remove(conn); + if(!Context.UserRateLimiters.TryGetValue(conn.User.UserId, out RateLimiter? rateLimiter)) + Context.UserRateLimiters.Add(conn.User.UserId, rateLimiter = new RateLimiter( + User.DEFAULT_SIZE, + User.DEFAULT_MINIMUM_DELAY, + User.DEFAULT_RISKY_OFFSET + )); - if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User)) - await Context.HandleDisconnect(conn.User); + rateLimiter.Update(); - await Context.Update(); + if(rateLimiter.IsExceeded) { + banDuration = TimeSpan.FromSeconds(FloodKickLength); + banUser = conn.User; + banAddr = conn.RemoteAddress.ToString(); + } else if(rateLimiter.IsRisky) { + banUser = conn.User; + } + + if(banUser is not null) { + if(banDuration == TimeSpan.MinValue) { + await Context.SendTo(conn.User, new CommandResponseS2CPacket(Context.RandomSnowflake.Next(), LCR.FLOOD_WARN, false)); + } else { + await Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood); + + if(banDuration > TimeSpan.Zero) + await BansClient.BanCreateAsync( + BanKind.User, + banDuration, + conn.RemoteAddress, + conn.User.UserId, + "Kicked from chat for flood protection.", + IPAddress.IPv6Loopback + ); + + return; + } + } } finally { Context.ContextAccess.Release(); } } - private async Task OnMessage(Connection conn, string msg) { - await Context.SafeUpdate(); + C2SPacketHandlerContext context = new(msg, Context, conn); + C2SPacketHandler? handler = conn.User is null + ? GuestHandlers.FirstOrDefault(h => h.IsMatch(context)) + : AuthedHandlers.FirstOrDefault(h => h.IsMatch(context)); - // this doesn't affect non-authed connections????? - if(conn.User is not null && conn.User.Rank < FloodKickExemptRank) { - User? banUser = null; - string banAddr = string.Empty; - TimeSpan banDuration = TimeSpan.MinValue; + if(handler is not null) + await handler.Handle(context); + } - Context.ContextAccess.Wait(); - try { - if(!Context.UserRateLimiters.TryGetValue(conn.User.UserId, out RateLimiter? rateLimiter)) - Context.UserRateLimiters.Add(conn.User.UserId, rateLimiter = new RateLimiter( - User.DEFAULT_SIZE, - User.DEFAULT_MINIMUM_DELAY, - User.DEFAULT_RISKY_OFFSET - )); + private bool IsDisposed; - rateLimiter.Update(); + ~SockChatServer() { + DoDispose(); + } - if(rateLimiter.IsExceeded) { - banDuration = TimeSpan.FromSeconds(FloodKickLength); - banUser = conn.User; - banAddr = conn.RemoteAddress.ToString(); - } else if(rateLimiter.IsRisky) { - banUser = conn.User; - } + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } - if(banUser is not null) { - if(banDuration == TimeSpan.MinValue) { - await Context.SendTo(conn.User, new CommandResponseS2CPacket(Context.RandomSnowflake.Next(), LCR.FLOOD_WARN, false)); - } else { - await Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood); + private void DoDispose() { + if(IsDisposed) + return; + IsDisposed = true; + IsShuttingDown = true; - if(banDuration > TimeSpan.Zero) - await BansClient.BanCreateAsync( - BanKind.User, - banDuration, - conn.RemoteAddress, - conn.User.UserId, - "Kicked from chat for flood protection.", - IPAddress.IPv6Loopback - ); + foreach(Connection conn in Context.Connections) + conn.Dispose(); - return; - } - } - } finally { - Context.ContextAccess.Release(); - } - } - - C2SPacketHandlerContext context = new(msg, Context, conn); - C2SPacketHandler? handler = conn.User is null - ? GuestHandlers.FirstOrDefault(h => h.IsMatch(context)) - : AuthedHandlers.FirstOrDefault(h => h.IsMatch(context)); - - if(handler is not null) - await handler.Handle(context); - } - - private bool IsDisposed; - - ~SockChatServer() { - DoDispose(); - } - - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - IsShuttingDown = true; - - foreach(Connection conn in Context.Connections) - conn.Dispose(); - - Server?.Dispose(); - } + Server?.Dispose(); } } diff --git a/SharpChat/User.cs b/SharpChat/User.cs index b87c439..7b0f874 100644 --- a/SharpChat/User.cs +++ b/SharpChat/User.cs @@ -2,72 +2,72 @@ using SharpChat.ClientCommands; using System.Globalization; using System.Text; -namespace SharpChat { - public class User( - string userId, - string userName, - ColourInheritable colour, - int rank, - UserPermissions perms, - string nickName = "", - UserStatus status = UserStatus.Online, - string statusText = "" - ) { - public const int DEFAULT_SIZE = 30; - public const int DEFAULT_MINIMUM_DELAY = 10000; - public const int DEFAULT_RISKY_OFFSET = 5; +namespace SharpChat; - public string UserId { get; } = userId; - public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName)); - public ColourInheritable Colour { get; set; } = colour; - public int Rank { get; set; } = rank; - public UserPermissions Permissions { get; set; } = perms; - public string NickName { get; set; } = nickName; - public UserStatus Status { get; set; } = status; - public string StatusText { get; set; } = statusText; +public class User( + string userId, + string userName, + ColourInheritable colour, + int rank, + UserPermissions perms, + string nickName = "", + UserStatus status = UserStatus.Online, + string statusText = "" +) { + public const int DEFAULT_SIZE = 30; + public const int DEFAULT_MINIMUM_DELAY = 10000; + public const int DEFAULT_RISKY_OFFSET = 5; - public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}"; + public string UserId { get; } = userId; + public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName)); + public ColourInheritable Colour { get; set; } = colour; + public int Rank { get; set; } = rank; + public UserPermissions Permissions { get; set; } = perms; + public string NickName { get; set; } = nickName; + public UserStatus Status { get; set; } = status; + public string StatusText { get; set; } = statusText; - public string LegacyNameWithStatus { - get { - StringBuilder sb = new(); + public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}"; - if(Status == UserStatus.Away) { - string statusText = StatusText.Trim(); - StringInfo sti = new(statusText); - if(Encoding.UTF8.GetByteCount(statusText) > AFKClientCommand.MAX_BYTES - || sti.LengthInTextElements > AFKClientCommand.MAX_GRAPHEMES) - statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, AFKClientCommand.MAX_GRAPHEMES)).Trim(); + public string LegacyNameWithStatus { + get { + StringBuilder sb = new(); - sb.AppendFormat("<{0}>_", statusText.ToUpperInvariant()); - } + if(Status == UserStatus.Away) { + string statusText = StatusText.Trim(); + StringInfo sti = new(statusText); + if(Encoding.UTF8.GetByteCount(statusText) > AFKClientCommand.MAX_BYTES + || sti.LengthInTextElements > AFKClientCommand.MAX_GRAPHEMES) + statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, AFKClientCommand.MAX_GRAPHEMES)).Trim(); - sb.Append(LegacyName); - - return sb.ToString(); + sb.AppendFormat("<{0}>_", statusText.ToUpperInvariant()); } - } - public bool Can(UserPermissions perm, bool strict = false) { - UserPermissions perms = Permissions & perm; - return strict ? perms == perm : perms > 0; - } + sb.Append(LegacyName); - public bool NameEquals(string name) { - return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase) - || string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase) - || string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase) - || string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase); - } - - public override int GetHashCode() { - return UserId.GetHashCode(); - } - - public static string GetDMChannelName(User user1, User user2) { - return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0 - ? $"@{user2.UserId}-{user1.UserId}" - : $"@{user1.UserId}-{user2.UserId}"; + return sb.ToString(); } } + + public bool Can(UserPermissions perm, bool strict = false) { + UserPermissions perms = Permissions & perm; + return strict ? perms == perm : perms > 0; + } + + public bool NameEquals(string name) { + return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase) + || string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase) + || string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase) + || string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase); + } + + public override int GetHashCode() { + return UserId.GetHashCode(); + } + + public static string GetDMChannelName(User user1, User user2) { + return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0 + ? $"@{user2.UserId}-{user1.UserId}" + : $"@{user1.UserId}-{user2.UserId}"; + } } diff --git a/SharpChat/UserStatus.cs b/SharpChat/UserStatus.cs index 447333b..632e109 100644 --- a/SharpChat/UserStatus.cs +++ b/SharpChat/UserStatus.cs @@ -1,7 +1,7 @@ -namespace SharpChat { - public enum UserStatus { - Online, - Away, - Offline, - } +namespace SharpChat; + +public enum UserStatus { + Online, + Away, + Offline, } diff --git a/SharpChatCommon/Auth/AuthClient.cs b/SharpChatCommon/Auth/AuthClient.cs index c0e8528..56fbf4c 100644 --- a/SharpChatCommon/Auth/AuthClient.cs +++ b/SharpChatCommon/Auth/AuthClient.cs @@ -1,8 +1,8 @@ using System.Net; -namespace SharpChat.Auth { - public interface AuthClient { - Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token); - Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries); - } +namespace SharpChat.Auth; + +public interface AuthClient { + Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token); + Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries); } diff --git a/SharpChatCommon/Auth/AuthFailedException.cs b/SharpChatCommon/Auth/AuthFailedException.cs index 460d00a..5426c42 100644 --- a/SharpChatCommon/Auth/AuthFailedException.cs +++ b/SharpChatCommon/Auth/AuthFailedException.cs @@ -1,3 +1,3 @@ -namespace SharpChat.Auth { - public class AuthFailedException(string message) : Exception(message) {} -} +namespace SharpChat.Auth; + +public class AuthFailedException(string message) : Exception(message) {} diff --git a/SharpChatCommon/Auth/AuthResult.cs b/SharpChatCommon/Auth/AuthResult.cs index 95efcae..075e735 100644 --- a/SharpChatCommon/Auth/AuthResult.cs +++ b/SharpChatCommon/Auth/AuthResult.cs @@ -1,9 +1,9 @@ -namespace SharpChat.Auth { - public interface AuthResult { - string UserId { get; } - string UserName { get; } - ColourInheritable UserColour { get; } - int UserRank { get; } - UserPermissions UserPermissions { get; } - } +namespace SharpChat.Auth; + +public interface AuthResult { + string UserId { get; } + string UserName { get; } + ColourInheritable UserColour { get; } + int UserRank { get; } + UserPermissions UserPermissions { get; } } diff --git a/SharpChatCommon/Bans/BanInfo.cs b/SharpChatCommon/Bans/BanInfo.cs index 412364e..92cbe9a 100644 --- a/SharpChatCommon/Bans/BanInfo.cs +++ b/SharpChatCommon/Bans/BanInfo.cs @@ -1,9 +1,9 @@ -namespace SharpChat.Bans { - public interface BanInfo { - BanKind Kind { get; } - bool IsPermanent { get; } - DateTimeOffset ExpiresAt { get; } - public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; - string ToString(); - } +namespace SharpChat.Bans; + +public interface BanInfo { + BanKind Kind { get; } + bool IsPermanent { get; } + DateTimeOffset ExpiresAt { get; } + public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; + string ToString(); } diff --git a/SharpChatCommon/Bans/BanKind.cs b/SharpChatCommon/Bans/BanKind.cs index cea18cd..30f4096 100644 --- a/SharpChatCommon/Bans/BanKind.cs +++ b/SharpChatCommon/Bans/BanKind.cs @@ -1,6 +1,6 @@ -namespace SharpChat.Bans { - public enum BanKind { - User, - IPAddress, - } +namespace SharpChat.Bans; + +public enum BanKind { + User, + IPAddress, } diff --git a/SharpChatCommon/Bans/BansClient.cs b/SharpChatCommon/Bans/BansClient.cs index 34b3ed7..bf8ee38 100644 --- a/SharpChatCommon/Bans/BansClient.cs +++ b/SharpChatCommon/Bans/BansClient.cs @@ -1,18 +1,18 @@ using System.Net; -namespace SharpChat.Bans { - public interface BansClient { - Task BanCreateAsync( - BanKind kind, - TimeSpan duration, - IPAddress remoteAddr, - string? userId = null, - string? reason = null, - IPAddress? issuerRemoteAddr = null, - string? issuerUserId = null - ); - Task<bool> BanRevokeAsync(BanInfo info); - Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null); - Task<BanInfo[]> BanGetListAsync(); - } +namespace SharpChat.Bans; + +public interface BansClient { + Task BanCreateAsync( + BanKind kind, + TimeSpan duration, + IPAddress remoteAddr, + string? userId = null, + string? reason = null, + IPAddress? issuerRemoteAddr = null, + string? issuerUserId = null + ); + Task<bool> BanRevokeAsync(BanInfo info); + Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null); + Task<BanInfo[]> BanGetListAsync(); } diff --git a/SharpChatCommon/Bans/IPAddressBanInfo.cs b/SharpChatCommon/Bans/IPAddressBanInfo.cs index ebae8e2..17746c1 100644 --- a/SharpChatCommon/Bans/IPAddressBanInfo.cs +++ b/SharpChatCommon/Bans/IPAddressBanInfo.cs @@ -1,7 +1,7 @@ using System.Net; -namespace SharpChat.Bans { - public interface IPAddressBanInfo : BanInfo { - IPAddress Address { get; } - } +namespace SharpChat.Bans; + +public interface IPAddressBanInfo : BanInfo { + IPAddress Address { get; } } diff --git a/SharpChatCommon/Bans/UserBanInfo.cs b/SharpChatCommon/Bans/UserBanInfo.cs index 1050007..2088d05 100644 --- a/SharpChatCommon/Bans/UserBanInfo.cs +++ b/SharpChatCommon/Bans/UserBanInfo.cs @@ -1,7 +1,7 @@ -namespace SharpChat.Bans { - public interface UserBanInfo : BanInfo { - string UserId { get; } - string UserName { get; } - ColourInheritable UserColour { get; } - } +namespace SharpChat.Bans; + +public interface UserBanInfo : BanInfo { + string UserId { get; } + string UserName { get; } + ColourInheritable UserColour { get; } } diff --git a/SharpChatCommon/ColourInheritable.cs b/SharpChatCommon/ColourInheritable.cs index 62241f6..fa4558b 100644 --- a/SharpChatCommon/ColourInheritable.cs +++ b/SharpChatCommon/ColourInheritable.cs @@ -1,14 +1,14 @@ -namespace SharpChat { - public readonly record struct ColourInheritable(ColourRgb? rgb) { - public static readonly ColourInheritable None = new(null); - public override string ToString() => rgb.HasValue ? rgb.Value.ToString() : "inherit"; +namespace SharpChat; - public static ColourInheritable FromRaw(int? raw) => raw is null ? None : new(new ColourRgb(raw.Value)); - public static ColourInheritable FromRgb(byte red, byte green, byte blue) => new(ColourRgb.FromRgb(red, green, blue)); +public readonly record struct ColourInheritable(ColourRgb? rgb) { + public static readonly ColourInheritable None = new(null); + public override string ToString() => rgb.HasValue ? rgb.Value.ToString() : "inherit"; - // these should go Away - private const int MSZ_INHERIT = 0x40000000; - public int ToMisuzu() => rgb.HasValue ? rgb.Value.Raw : MSZ_INHERIT; - public static ColourInheritable FromMisuzu(int msz) => (msz & MSZ_INHERIT) > 0 ? None : new(new ColourRgb(msz & 0xFFFFFF)); - } + public static ColourInheritable FromRaw(int? raw) => raw is null ? None : new(new ColourRgb(raw.Value)); + public static ColourInheritable FromRgb(byte red, byte green, byte blue) => new(ColourRgb.FromRgb(red, green, blue)); + + // these should go Away + private const int MSZ_INHERIT = 0x40000000; + public int ToMisuzu() => rgb.HasValue ? rgb.Value.Raw : MSZ_INHERIT; + public static ColourInheritable FromMisuzu(int msz) => (msz & MSZ_INHERIT) > 0 ? None : new(new ColourRgb(msz & 0xFFFFFF)); } diff --git a/SharpChatCommon/ColourRgb.cs b/SharpChatCommon/ColourRgb.cs index 79d349e..bc3df3f 100644 --- a/SharpChatCommon/ColourRgb.cs +++ b/SharpChatCommon/ColourRgb.cs @@ -1,9 +1,9 @@ -namespace SharpChat { - public readonly record struct ColourRgb(int Raw) { - public byte Red => (byte)((Raw >> 16) & 0xFF); - public byte Green => (byte)((Raw >> 8) & 0xFF); - public byte Blue => (byte)(Raw & 0xFF); - public override string ToString() => string.Format("#{0:x2}{1:x2}{2:x2}", Red, Green, Blue); - public static ColourRgb FromRgb(byte red, byte green, byte blue) => new(red << 16 | green << 8 | blue); - } +namespace SharpChat; + +public readonly record struct ColourRgb(int Raw) { + public byte Red => (byte)((Raw >> 16) & 0xFF); + public byte Green => (byte)((Raw >> 8) & 0xFF); + public byte Blue => (byte)(Raw & 0xFF); + public override string ToString() => string.Format("#{0:x2}{1:x2}{2:x2}", Red, Green, Blue); + public static ColourRgb FromRgb(byte red, byte green, byte blue) => new(red << 16 | green << 8 | blue); } diff --git a/SharpChatCommon/Configuration/CachedValue.cs b/SharpChatCommon/Configuration/CachedValue.cs index feaf1d5..214d42c 100644 --- a/SharpChatCommon/Configuration/CachedValue.cs +++ b/SharpChatCommon/Configuration/CachedValue.cs @@ -1,34 +1,34 @@ -namespace SharpChat.Configuration { - public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) { - private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); - private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); - private object ConfigAccess { get; } = new(); +namespace SharpChat.Configuration; - private object? CurrentValue { get; set; } = default(T); - private DateTimeOffset LastRead { get; set; } +public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) { + private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); + private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); + private object ConfigAccess { get; } = new(); - public T? Value { - get { - lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls - DateTimeOffset now = DateTimeOffset.Now; - if((now - LastRead) >= lifetime) { - LastRead = now; - CurrentValue = Config.ReadValue(Name, fallback); - Logger.Debug($"Read {Name} ({CurrentValue})"); - } + private object? CurrentValue { get; set; } = default(T); + private DateTimeOffset LastRead { get; set; } + + public T? Value { + get { + lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls + DateTimeOffset now = DateTimeOffset.Now; + if((now - LastRead) >= lifetime) { + LastRead = now; + CurrentValue = Config.ReadValue(Name, fallback); + Logger.Debug($"Read {Name} ({CurrentValue})"); } - return (T?)CurrentValue; } - } - - public static implicit operator T?(CachedValue<T?> val) => val.Value; - - public void Refresh() { - LastRead = DateTimeOffset.MinValue; - } - - public override string ToString() { - return Value?.ToString() ?? string.Empty; + return (T?)CurrentValue; } } + + public static implicit operator T?(CachedValue<T?> val) => val.Value; + + public void Refresh() { + LastRead = DateTimeOffset.MinValue; + } + + public override string ToString() { + return Value?.ToString() ?? string.Empty; + } } diff --git a/SharpChatCommon/Configuration/Config.cs b/SharpChatCommon/Configuration/Config.cs index 04c4140..068061a 100644 --- a/SharpChatCommon/Configuration/Config.cs +++ b/SharpChatCommon/Configuration/Config.cs @@ -1,29 +1,29 @@ -namespace SharpChat.Configuration { - public interface Config : IDisposable { - /// <summary> - /// Creates a proxy object that forces all names to start with the given prefix. - /// </summary> - Config ScopeTo(string prefix); +namespace SharpChat.Configuration; - /// <summary> - /// Reads a raw (string) value from the config. - /// </summary> - string? ReadValue(string name, string? fallback = null); +public interface Config : IDisposable { + /// <summary> + /// Creates a proxy object that forces all names to start with the given prefix. + /// </summary> + Config ScopeTo(string prefix); - /// <summary> - /// Reads and casts value from the config. - /// </summary> - /// <exception cref="ConfigTypeException">Type conversion failed.</exception> - T? ReadValue<T>(string name, T? fallback = default); + /// <summary> + /// Reads a raw (string) value from the config. + /// </summary> + string? ReadValue(string name, string? fallback = null); - /// <summary> - /// Reads and casts a value from the config. Returns fallback when type conversion fails. - /// </summary> - T? SafeReadValue<T>(string name, T? fallback); + /// <summary> + /// Reads and casts value from the config. + /// </summary> + /// <exception cref="ConfigTypeException">Type conversion failed.</exception> + T? ReadValue<T>(string name, T? fallback = default); - /// <summary> - /// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values. - /// </summary> - CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null); - } + /// <summary> + /// Reads and casts a value from the config. Returns fallback when type conversion fails. + /// </summary> + T? SafeReadValue<T>(string name, T? fallback); + + /// <summary> + /// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values. + /// </summary> + CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null); } diff --git a/SharpChatCommon/Configuration/ConfigExceptions.cs b/SharpChatCommon/Configuration/ConfigExceptions.cs index 6e851bf..8d67762 100644 --- a/SharpChatCommon/Configuration/ConfigExceptions.cs +++ b/SharpChatCommon/Configuration/ConfigExceptions.cs @@ -1,9 +1,9 @@ -namespace SharpChat.Configuration { - public abstract class ConfigException : Exception { - public ConfigException(string message) : base(message) { } - public ConfigException(string message, Exception ex) : base(message, ex) { } - } +namespace SharpChat.Configuration; - public class ConfigLockException() : ConfigException("Unable to acquire lock for reading configuration.") {} - public class ConfigTypeException(Exception ex) : ConfigException("Given type does not match the value in the configuration.", ex) {} +public abstract class ConfigException : Exception { + public ConfigException(string message) : base(message) { } + public ConfigException(string message, Exception ex) : base(message, ex) { } } + +public class ConfigLockException() : ConfigException("Unable to acquire lock for reading configuration.") {} +public class ConfigTypeException(Exception ex) : ConfigException("Given type does not match the value in the configuration.", ex) {} diff --git a/SharpChatCommon/Configuration/ScopedConfig.cs b/SharpChatCommon/Configuration/ScopedConfig.cs index 735496b..86b4770 100644 --- a/SharpChatCommon/Configuration/ScopedConfig.cs +++ b/SharpChatCommon/Configuration/ScopedConfig.cs @@ -1,34 +1,34 @@ -namespace SharpChat.Configuration { - public class ScopedConfig(Config config, string prefix) : Config { - private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); - private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix)); +namespace SharpChat.Configuration; - private string GetName(string name) { - return Prefix + name; - } +public class ScopedConfig(Config config, string prefix) : Config { + private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); + private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix)); - public string? ReadValue(string name, string? fallback = null) { - return Config.ReadValue(GetName(name), fallback); - } + private string GetName(string name) { + return Prefix + name; + } - public T? ReadValue<T>(string name, T? fallback = default) { - return Config.ReadValue(GetName(name), fallback); - } + public string? ReadValue(string name, string? fallback = null) { + return Config.ReadValue(GetName(name), fallback); + } - public T? SafeReadValue<T>(string name, T? fallback) { - return Config.SafeReadValue(GetName(name), fallback); - } + public T? ReadValue<T>(string name, T? fallback = default) { + return Config.ReadValue(GetName(name), fallback); + } - public Config ScopeTo(string prefix) { - return Config.ScopeTo(GetName(prefix)); - } + public T? SafeReadValue<T>(string name, T? fallback) { + return Config.SafeReadValue(GetName(name), fallback); + } - public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) { - return Config.ReadCached(GetName(name), fallback, lifetime); - } + public Config ScopeTo(string prefix) { + return Config.ScopeTo(GetName(prefix)); + } - public void Dispose() { - GC.SuppressFinalize(this); - } + public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) { + return Config.ReadCached(GetName(name), fallback, lifetime); + } + + public void Dispose() { + GC.SuppressFinalize(this); } } diff --git a/SharpChatCommon/Configuration/StreamConfig.cs b/SharpChatCommon/Configuration/StreamConfig.cs index 3c6887e..78b389f 100644 --- a/SharpChatCommon/Configuration/StreamConfig.cs +++ b/SharpChatCommon/Configuration/StreamConfig.cs @@ -1,115 +1,115 @@ using System.Text; -namespace SharpChat.Configuration { - public class StreamConfig : Config { - private Stream Stream { get; } - private StreamReader StreamReader { get; } - private Mutex Lock { get; } +namespace SharpChat.Configuration; - private const int LOCK_TIMEOUT = 10000; +public class StreamConfig : Config { + private Stream Stream { get; } + private StreamReader StreamReader { get; } + private Mutex Lock { get; } - private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15); + private const int LOCK_TIMEOUT = 10000; - public StreamConfig(Stream stream) { - Stream = stream ?? throw new ArgumentNullException(nameof(stream)); - if(!Stream.CanRead) - throw new ArgumentException("Provided stream must be readable.", nameof(stream)); - if(!Stream.CanSeek) - throw new ArgumentException("Provided stream must be seekable.", nameof(stream)); - StreamReader = new StreamReader(stream, new UTF8Encoding(false), false); - Lock = new Mutex(); - } + private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15); - public static StreamConfig FromPath(string fileName) { - return new StreamConfig(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)); - } + public StreamConfig(Stream stream) { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + if(!Stream.CanRead) + throw new ArgumentException("Provided stream must be readable.", nameof(stream)); + if(!Stream.CanSeek) + throw new ArgumentException("Provided stream must be seekable.", nameof(stream)); + StreamReader = new StreamReader(stream, new UTF8Encoding(false), false); + Lock = new Mutex(); + } - public string? ReadValue(string name, string? fallback = null) { - if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong - throw new ConfigLockException(); + public static StreamConfig FromPath(string fileName) { + return new StreamConfig(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)); + } - try { - Stream.Seek(0, SeekOrigin.Begin); + public string? ReadValue(string name, string? fallback = null) { + if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong + throw new ConfigLockException(); - string? line; - while((line = StreamReader.ReadLine()) != null) { - if(string.IsNullOrWhiteSpace(line)) - continue; + try { + Stream.Seek(0, SeekOrigin.Begin); - line = line.TrimStart(); - if(line.StartsWith(';') || line.StartsWith('#')) - continue; + string? line; + while((line = StreamReader.ReadLine()) != null) { + if(string.IsNullOrWhiteSpace(line)) + continue; - string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); - if(parts.Length < 2 || !string.Equals(parts[0], name)) - continue; + line = line.TrimStart(); + if(line.StartsWith(';') || line.StartsWith('#')) + continue; - return parts[1]; - } - } finally { - Lock.ReleaseMutex(); + string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + if(parts.Length < 2 || !string.Equals(parts[0], name)) + continue; + + return parts[1]; } + } finally { + Lock.ReleaseMutex(); + } + return fallback; + } + + public T? ReadValue<T>(string name, T? fallback = default) { + object? value = ReadValue(name); + if(value == null) return fallback; + + Type type = typeof(T); + if(value is string strVal) { + if(type == typeof(bool)) + value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase) + && !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase); + else if(type == typeof(string[])) + value = strVal.Split(' '); } - public T? ReadValue<T>(string name, T? fallback = default) { - object? value = ReadValue(name); - if(value == null) - return fallback; - - Type type = typeof(T); - if(value is string strVal) { - if(type == typeof(bool)) - value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase) - && !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase); - else if(type == typeof(string[])) - value = strVal.Split(' '); - } - - try { - return (T)Convert.ChangeType(value, type); - } catch(InvalidCastException ex) { - throw new ConfigTypeException(ex); - } - } - - public T? SafeReadValue<T>(string name, T? fallback) { - try { - return ReadValue(name, fallback); - } catch(ConfigTypeException) { - return fallback; - } - } - - public Config ScopeTo(string prefix) { - if(string.IsNullOrWhiteSpace(prefix)) - throw new ArgumentException("Prefix must exist.", nameof(prefix)); - if(prefix[^1] != ':') - prefix += ':'; - - return new ScopedConfig(this, prefix); - } - - public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) { - return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback); - } - - private bool IsDisposed; - ~StreamConfig() - => DoDispose(); - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - - StreamReader.Dispose(); - Stream.Dispose(); - Lock.Dispose(); + try { + return (T)Convert.ChangeType(value, type); + } catch(InvalidCastException ex) { + throw new ConfigTypeException(ex); } } + + public T? SafeReadValue<T>(string name, T? fallback) { + try { + return ReadValue(name, fallback); + } catch(ConfigTypeException) { + return fallback; + } + } + + public Config ScopeTo(string prefix) { + if(string.IsNullOrWhiteSpace(prefix)) + throw new ArgumentException("Prefix must exist.", nameof(prefix)); + if(prefix[^1] != ':') + prefix += ':'; + + return new ScopedConfig(this, prefix); + } + + public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) { + return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback); + } + + private bool IsDisposed; + ~StreamConfig() + => DoDispose(); + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } + private void DoDispose() { + if(IsDisposed) + return; + IsDisposed = true; + + StreamReader.Dispose(); + Stream.Dispose(); + Lock.Dispose(); + } } diff --git a/SharpChatCommon/Logger.cs b/SharpChatCommon/Logger.cs index e2354d9..f5603c1 100644 --- a/SharpChatCommon/Logger.cs +++ b/SharpChatCommon/Logger.cs @@ -1,33 +1,33 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Text; -namespace SharpChat { - public static class Logger { - public static void Write(string str) { - Console.WriteLine(string.Format("[{1}] {0}", str, DateTime.Now)); - } +namespace SharpChat; - public static void Write(byte[] bytes) { - Write(Encoding.UTF8.GetString(bytes)); - } +public static class Logger { + public static void Write(string str) { + Console.WriteLine(string.Format("[{1}] {0}", str, DateTime.Now)); + } - public static void Write(object obj) { - Write(obj?.ToString() ?? string.Empty); - } + public static void Write(byte[] bytes) { + Write(Encoding.UTF8.GetString(bytes)); + } - [Conditional("DEBUG")] - public static void Debug(string str) { - Write(str); - } + public static void Write(object obj) { + Write(obj?.ToString() ?? string.Empty); + } - [Conditional("DEBUG")] - public static void Debug(byte[] bytes) { - Write(bytes); - } + [Conditional("DEBUG")] + public static void Debug(string str) { + Write(str); + } - [Conditional("DEBUG")] - public static void Debug(object obj) { - Write(obj); - } + [Conditional("DEBUG")] + public static void Debug(byte[] bytes) { + Write(bytes); + } + + [Conditional("DEBUG")] + public static void Debug(object obj) { + Write(obj); } } diff --git a/SharpChatCommon/RNG.cs b/SharpChatCommon/RNG.cs index c540c20..92e7863 100644 --- a/SharpChatCommon/RNG.cs +++ b/SharpChatCommon/RNG.cs @@ -1,82 +1,82 @@ -using System.Buffers; +using System.Buffers; using System.Security.Cryptography; -namespace SharpChat { - public static class RNG { - public const string CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"; +namespace SharpChat; - private static Random NormalRandom { get; } = new(); - private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create(); +public static class RNG { + public const string CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"; - public static int Next() { - return NormalRandom.Next(); - } + private static Random NormalRandom { get; } = new(); + private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create(); - public static int Next(int max) { - return NormalRandom.Next(max); - } + public static int Next() { + return NormalRandom.Next(); + } - public static int Next(int min, int max) { - return NormalRandom.Next(min, max); - } + public static int Next(int max) { + return NormalRandom.Next(max); + } - public static void NextBytes(byte[] buffer) { + public static int Next(int min, int max) { + return NormalRandom.Next(min, max); + } + + public static void NextBytes(byte[] buffer) { + SecureRandom.GetBytes(buffer); + } + + public static int SecureNext() { + return SecureNext(int.MaxValue); + } + + public static int SecureNext(int max) { + return SecureNext(0, max); + } + + public static int SecureNext(int min, int max) { + --max; + if(min == max) + return min; + + uint umax = (uint)max - (uint)min; + uint num; + + byte[] buffer = ArrayPool<byte>.Shared.Rent(4); + try { SecureRandom.GetBytes(buffer); - } + num = BitConverter.ToUInt32(buffer); - public static int SecureNext() { - return SecureNext(int.MaxValue); - } + if(umax != uint.MaxValue) { + ++umax; - public static int SecureNext(int max) { - return SecureNext(0, max); - } + if((umax & (umax - 1)) != 0) { + uint limit = uint.MaxValue - (uint.MaxValue & umax) - 1; - public static int SecureNext(int min, int max) { - --max; - if(min == max) - return min; - - uint umax = (uint)max - (uint)min; - uint num; - - byte[] buffer = ArrayPool<byte>.Shared.Rent(4); - try { - SecureRandom.GetBytes(buffer); - num = BitConverter.ToUInt32(buffer); - - if(umax != uint.MaxValue) { - ++umax; - - if((umax & (umax - 1)) != 0) { - uint limit = uint.MaxValue - (uint.MaxValue & umax) - 1; - - while(num > limit) { - SecureRandom.GetBytes(buffer); - num = BitConverter.ToUInt32(buffer); - } + while(num > limit) { + SecureRandom.GetBytes(buffer); + num = BitConverter.ToUInt32(buffer); } } - } finally { - ArrayPool<byte>.Shared.Return(buffer); } - - return (int)((num % umax) + min); + } finally { + ArrayPool<byte>.Shared.Return(buffer); } - private static string RandomStringInternal(Func<int, int> next, int length) { - char[] str = new char[length]; - for(int i = 0; i < length; ++i) - str[i] = CHARS[next(CHARS.Length)]; - return new string(str); - } + return (int)((num % umax) + min); + } - public static string RandomString(int length) { - return RandomStringInternal(Next, length); - } + private static string RandomStringInternal(Func<int, int> next, int length) { + char[] str = new char[length]; + for(int i = 0; i < length; ++i) + str[i] = CHARS[next(CHARS.Length)]; + return new string(str); + } - public static string SecureRandomString(int length) { - return RandomStringInternal(SecureNext, length); - } + public static string RandomString(int length) { + return RandomStringInternal(Next, length); + } + + public static string SecureRandomString(int length) { + return RandomStringInternal(SecureNext, length); } } diff --git a/SharpChatCommon/RateLimiter.cs b/SharpChatCommon/RateLimiter.cs index f1cd118..16493ed 100644 --- a/SharpChatCommon/RateLimiter.cs +++ b/SharpChatCommon/RateLimiter.cs @@ -1,35 +1,35 @@ -namespace SharpChat { - public class RateLimiter { - private readonly int Size; - private readonly int MinimumDelay; - private readonly int RiskyOffset; - private readonly long[] TimePoints; +namespace SharpChat; - public RateLimiter(int size, int minDelay, int riskyOffset = 0) { - if(size < 2) - throw new ArgumentException("Size is too small.", nameof(size)); - if(minDelay < 1000) - throw new ArgumentException("Minimum delay is inhuman.", nameof(minDelay)); - if(riskyOffset != 0) { - if(riskyOffset >= size) - throw new ArgumentException("Risky offset may not be greater or equal to the size.", nameof(riskyOffset)); - else if(riskyOffset < 0) - throw new ArgumentException("Risky offset may not be negative.", nameof(riskyOffset)); - } +public class RateLimiter { + private readonly int Size; + private readonly int MinimumDelay; + private readonly int RiskyOffset; + private readonly long[] TimePoints; - Size = size; - MinimumDelay = minDelay; - RiskyOffset = riskyOffset; - TimePoints = new long[Size]; + public RateLimiter(int size, int minDelay, int riskyOffset = 0) { + if(size < 2) + throw new ArgumentException("Size is too small.", nameof(size)); + if(minDelay < 1000) + throw new ArgumentException("Minimum delay is inhuman.", nameof(minDelay)); + if(riskyOffset != 0) { + if(riskyOffset >= size) + throw new ArgumentException("Risky offset may not be greater or equal to the size.", nameof(riskyOffset)); + else if(riskyOffset < 0) + throw new ArgumentException("Risky offset may not be negative.", nameof(riskyOffset)); } - public bool IsRisky => TimePoints[RiskyOffset] != 0 && TimePoints[RiskyOffset + 1] != 0 && TimePoints[RiskyOffset] + MinimumDelay >= TimePoints[Size - 1]; - public bool IsExceeded => TimePoints[0] != 0 && TimePoints[1] != 0 && TimePoints[0] + MinimumDelay >= TimePoints[Size - 1]; + Size = size; + MinimumDelay = minDelay; + RiskyOffset = riskyOffset; + TimePoints = new long[Size]; + } - public void Update() { - for(int i = 1; i < Size; ++i) - TimePoints[i - 1] = TimePoints[i]; - TimePoints[Size - 1] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - } + public bool IsRisky => TimePoints[RiskyOffset] != 0 && TimePoints[RiskyOffset + 1] != 0 && TimePoints[RiskyOffset] + MinimumDelay >= TimePoints[Size - 1]; + public bool IsExceeded => TimePoints[0] != 0 && TimePoints[1] != 0 && TimePoints[0] + MinimumDelay >= TimePoints[Size - 1]; + + public void Update() { + for(int i = 1; i < Size; ++i) + TimePoints[i - 1] = TimePoints[i]; + TimePoints[Size - 1] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } } diff --git a/SharpChatCommon/SharpInfo.cs b/SharpChatCommon/SharpInfo.cs index c187d98..f234e46 100644 --- a/SharpChatCommon/SharpInfo.cs +++ b/SharpChatCommon/SharpInfo.cs @@ -1,36 +1,36 @@ using System.Reflection; using System.Text; -namespace SharpChat { - public static class SharpInfo { - private const string NAME = @"SharpChat"; - private const string UNKNOWN = @"XXXXXXXXXX"; +namespace SharpChat; - public static string VersionString { get; } - public static string VersionStringShort { get; } - public static bool IsDebugBuild { get; } +public static class SharpInfo { + private const string NAME = @"SharpChat"; + private const string UNKNOWN = @"XXXXXXXXXX"; - public static string ProgramName { get; } + public static string VersionString { get; } + public static string VersionStringShort { get; } + public static bool IsDebugBuild { get; } - static SharpInfo() { + public static string ProgramName { get; } + + static SharpInfo() { #if DEBUG - IsDebugBuild = true; + IsDebugBuild = true; #endif - try { - using Stream s = Assembly.GetEntryAssembly()!.GetManifestResourceStream(@"SharpChat.version.txt")!; - using StreamReader sr = new(s); - VersionString = sr.ReadLine()!.Trim(); - VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString; - } catch { - VersionStringShort = VersionString = UNKNOWN; - } - - StringBuilder sb = new(); - sb.Append(NAME); - sb.Append('/'); - sb.Append(VersionStringShort); - ProgramName = sb.ToString(); + try { + using Stream s = Assembly.GetEntryAssembly()!.GetManifestResourceStream(@"SharpChat.version.txt")!; + using StreamReader sr = new(s); + VersionString = sr.ReadLine()!.Trim(); + VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString; + } catch { + VersionStringShort = VersionString = UNKNOWN; } + + StringBuilder sb = new(); + sb.Append(NAME); + sb.Append('/'); + sb.Append(VersionStringShort); + ProgramName = sb.ToString(); } } diff --git a/SharpChatCommon/Snowflake/RandomSnowflake.cs b/SharpChatCommon/Snowflake/RandomSnowflake.cs index 02f149e..2030bf1 100644 --- a/SharpChatCommon/Snowflake/RandomSnowflake.cs +++ b/SharpChatCommon/Snowflake/RandomSnowflake.cs @@ -1,13 +1,13 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; -namespace SharpChat.Snowflake { - public class RandomSnowflake( - SnowflakeGenerator? generator = null - ) { - public readonly SnowflakeGenerator Generator = generator ?? new SnowflakeGenerator(); +namespace SharpChat.Snowflake; - public long Next(DateTimeOffset? at = null) { - return Generator.Next(Math.Abs(BitConverter.ToInt64(RandomNumberGenerator.GetBytes(8))), at); - } +public class RandomSnowflake( + SnowflakeGenerator? generator = null +) { + public readonly SnowflakeGenerator Generator = generator ?? new SnowflakeGenerator(); + + public long Next(DateTimeOffset? at = null) { + return Generator.Next(Math.Abs(BitConverter.ToInt64(RandomNumberGenerator.GetBytes(8))), at); } } diff --git a/SharpChatCommon/Snowflake/SnowflakeGenerator.cs b/SharpChatCommon/Snowflake/SnowflakeGenerator.cs index 57eadf4..dcc66f1 100644 --- a/SharpChatCommon/Snowflake/SnowflakeGenerator.cs +++ b/SharpChatCommon/Snowflake/SnowflakeGenerator.cs @@ -1,35 +1,35 @@ -namespace SharpChat.Snowflake { - public class SnowflakeGenerator { - public const long MASK = 0x7FFFFFFFFFFFFFFF; - // previous default epoch was 1588377600000, but snowflakes are much larger than SharpIds - public const long EPOCH = 1356998400000; - public const byte SHIFT = 16; +namespace SharpChat.Snowflake; - public readonly long Epoch; - public readonly byte Shift; - public readonly long TimestampMask; - public readonly long SequenceMask; +public class SnowflakeGenerator { + public const long MASK = 0x7FFFFFFFFFFFFFFF; + // previous default epoch was 1588377600000, but snowflakes are much larger than SharpIds + public const long EPOCH = 1356998400000; + public const byte SHIFT = 16; - public SnowflakeGenerator(long epoch = EPOCH, byte shift = SHIFT) { - if(epoch is < 0 or > MASK) - throw new ArgumentException("Epoch must be a positive int64.", nameof(epoch)); - if(shift is < 1 or > 63) - throw new ArgumentException("Shift must be between or equal to 1 and 63", nameof(shift)); + public readonly long Epoch; + public readonly byte Shift; + public readonly long TimestampMask; + public readonly long SequenceMask; - Epoch = epoch; - Shift = shift; + public SnowflakeGenerator(long epoch = EPOCH, byte shift = SHIFT) { + if(epoch is < 0 or > MASK) + throw new ArgumentException("Epoch must be a positive int64.", nameof(epoch)); + if(shift is < 1 or > 63) + throw new ArgumentException("Shift must be between or equal to 1 and 63", nameof(shift)); - // i think Index only does this as a hack for how integers work in PHP but its gonna run Once per application instance lol - TimestampMask = ~(~0L << (63 - shift)); - SequenceMask = ~(~0L << shift); - } + Epoch = epoch; + Shift = shift; - public long Now(DateTimeOffset? at = null) { - return Math.Max(0, (at ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds() - Epoch); - } + // i think Index only does this as a hack for how integers work in PHP but its gonna run Once per application instance lol + TimestampMask = ~(~0L << (63 - shift)); + SequenceMask = ~(~0L << shift); + } - public long Next(long sequence, DateTimeOffset? at = null) { - return ((Now(at) & TimestampMask) << Shift) | (sequence & SequenceMask); - } + public long Now(DateTimeOffset? at = null) { + return Math.Max(0, (at ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds() - Epoch); + } + + public long Next(long sequence, DateTimeOffset? at = null) { + return ((Now(at) & TimestampMask) << Shift) | (sequence & SequenceMask); } } diff --git a/SharpChatCommon/UserPermissions.cs b/SharpChatCommon/UserPermissions.cs index 4104501..2424083 100644 --- a/SharpChatCommon/UserPermissions.cs +++ b/SharpChatCommon/UserPermissions.cs @@ -1,24 +1,24 @@ -namespace SharpChat { - [Flags] - public enum UserPermissions : int { - KickUser = 0x00000001, - BanUser = 0x00000002, - //SilenceUser = 0x00000004, - Broadcast = 0x00000008, - SetOwnNickname = 0x00000010, - SetOthersNickname = 0x00000020, - CreateChannel = 0x00000040, - DeleteChannel = 0x00010000, - SetChannelPermanent = 0x00000080, - SetChannelPassword = 0x00000100, - SetChannelHierarchy = 0x00000200, - JoinAnyChannel = 0x00020000, - SendMessage = 0x00000400, - DeleteOwnMessage = 0x00000800, - DeleteAnyMessage = 0x00001000, - EditOwnMessage = 0x00002000, - EditAnyMessage = 0x00004000, - SeeIPAddress = 0x00008000, - ViewLogs = 0x00040000, - } +namespace SharpChat; + +[Flags] +public enum UserPermissions : int { + KickUser = 0x00000001, + BanUser = 0x00000002, + //SilenceUser = 0x00000004, + Broadcast = 0x00000008, + SetOwnNickname = 0x00000010, + SetOthersNickname = 0x00000020, + CreateChannel = 0x00000040, + DeleteChannel = 0x00010000, + SetChannelPermanent = 0x00000080, + SetChannelPassword = 0x00000100, + SetChannelHierarchy = 0x00000200, + JoinAnyChannel = 0x00020000, + SendMessage = 0x00000400, + DeleteOwnMessage = 0x00000800, + DeleteAnyMessage = 0x00001000, + EditOwnMessage = 0x00002000, + EditAnyMessage = 0x00004000, + SeeIPAddress = 0x00008000, + ViewLogs = 0x00040000, }