Split out the Flashii interaction code into a separate library.

This commit is contained in:
flash 2025-04-26 19:42:23 +00:00
parent 51f5c4c948
commit 2eba089a21
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
55 changed files with 769 additions and 552 deletions

View file

@ -1,4 +1,4 @@
root = true
root = true
[*]
end_of_line = lf
@ -11,3 +11,134 @@ indent_size = 4
# IDE1006: Naming Styles
dotnet_diagnostic.IDE1006.severity = none
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_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_prefer_static_local_function = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_prefer_static_anonymous_function = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_readonly_field = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_code_quality_unused_parameters = all:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_method = false:silent

View file

@ -0,0 +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);
[JsonPropertyName("success")]
public bool Success { get; init; }
[JsonPropertyName("reason")]
public string? Reason { get; init; }
[JsonPropertyName("user_id")]
public long UserIdRaw { get; init; }
[JsonPropertyName("username")]
public string? UserNameRaw { get; init; }
[JsonPropertyName("colour_raw")]
public int UserColourRaw { get; init; }
[JsonPropertyName("hierarchy")]
public int UserRank { get; init; }
[JsonPropertyName("perms")]
public UserPermissions UserPermissions { get; init; }
}
}

View file

@ -0,0 +1,13 @@
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();
}
}

View file

@ -0,0 +1,240 @@
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Configuration;
using System.Net;
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);
private const string DEFAULT_SECRET_KEY = "woomy";
private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
private string CreateStringSignature(string str) {
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
}
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 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();
sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr);
formData.Add(string.Format("u[{0}]", userId), remoteAddrStr);
}
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
},
Content = new FormUrlEncodedContent(formData),
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
}
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 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 },
}),
};
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}";
public async Task<bool> BanRevokeAsync(BanInfo info) {
string type;
string target;
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));
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));
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 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.Delete, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
if(response.StatusCode == HttpStatusCode.NotFound)
return false;
response.EnsureSuccessStatusCode();
return response.StatusCode == HttpStatusCode.NoContent;
}
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}";
public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) {
userIdOrName ??= "0";
remoteAddr ??= IPAddress.None;
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);
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));
})];
}
}
}

View file

@ -0,0 +1,9 @@
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();
}
}

View file

@ -1,13 +1,20 @@
using System.Text.Json.Serialization;
namespace SharpChat.Misuzu {
public class MisuzuBanInfo {
namespace SharpChat.Flashii {
public class FlashiiRawBanInfo {
[JsonPropertyName("is_ban")]
public bool IsBanned { get; set; }
[JsonPropertyName("user_id")]
public string? UserId { get; set; }
[JsonPropertyName("user_name")]
public string? UserName { get; set; }
[JsonPropertyName("user_colour")]
public int UserColourRaw { get; set; }
public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
[JsonPropertyName("ip_addr")]
public string? RemoteAddress { get; set; }
@ -17,15 +24,6 @@ namespace SharpChat.Misuzu {
[JsonPropertyName("expires")]
public DateTimeOffset ExpiresAt { get; set; }
// only populated in list request
[JsonPropertyName("user_name")]
public string? UserName { get; set; }
[JsonPropertyName("user_colour")]
public int UserColourRaw { get; set; }
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
}
}

View file

@ -0,0 +1,10 @@
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;
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
</ItemGroup>
</Project>

View file

@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChatCommon", "SharpChatCommon\SharpChatCommon.csproj", "{C8B619A7-7815-426D-B459-20EE26F7460E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.Flashii", "SharpChat.Misuzu\SharpChat.Flashii.csproj", "{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -31,6 +33,10 @@ Global
{C8B619A7-7815-426D-B459-20EE26F7460E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.Build.0 = Release|Any CPU
{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,15 +1,16 @@
using SharpChat.Config;
using SharpChat.Misuzu;
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Configuration;
using SharpChat.S2CPackets;
namespace SharpChat.C2SPacketHandlers {
public class AuthC2SPacketHandler(
MisuzuClient msz,
AuthClient authClient,
BansClient bansClient,
Channel defaultChannel,
CachedValue<int> maxMsgLength,
CachedValue<int> maxConns
) : C2SPacketHandler {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
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));
@ -22,14 +23,9 @@ namespace SharpChat.C2SPacketHandlers {
string[] args = ctx.SplitText(3);
string? authMethod = args.ElementAtOrDefault(1);
if(string.IsNullOrWhiteSpace(authMethod)) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
return;
}
string? authToken = args.ElementAtOrDefault(2);
if(string.IsNullOrWhiteSpace(authToken)) {
if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
return;
@ -42,93 +38,75 @@ namespace SharpChat.C2SPacketHandlers {
}
Task.Run(async () => {
MisuzuAuthInfo? fai;
string ipAddr = ctx.Connection.RemoteAddress.ToString();
try {
fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr);
} catch(Exception ex) {
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
#if DEBUG
throw;
#else
return;
#endif
}
AuthResult authResult = await authClient.AuthVerifyAsync(
ctx.Connection.RemoteAddress,
authMethod,
authToken
);
if(fai?.Success != true) {
Logger.Debug($"<{ctx.Connection.Id}> Auth fail: {fai?.Reason ?? "unknown"}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
return;
}
MisuzuBanInfo? fbi;
try {
fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr);
} catch(Exception ex) {
Logger.Write($"<{ctx.Connection.Id}> Failed auth ban check: {ex}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
#if DEBUG
throw;
#else
return;
#endif
}
if(fbi?.IsBanned == true && !fbi.HasExpired) {
Logger.Write($"<{ctx.Connection.Id}> User is banned.");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, fbi.IsPermanent ? DateTimeOffset.MaxValue : fbi.ExpiresAt));
ctx.Connection.Dispose();
return;
}
await ctx.Chat.ContextAccess.WaitAsync();
try {
User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId);
if(user == null)
user = new User(
fai.UserId,
fai.UserName ?? $"({fai.UserId})",
fai.Colour,
fai.Rank,
fai.Permissions
);
else
ctx.Chat.UpdateUser(
user,
userName: fai.UserName ?? $"({fai.UserId})",
colour: fai.Colour,
rank: fai.Rank,
perms: fai.Permissions
);
// Enforce a maximum amount of connections per user
if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress);
if(banInfo is not null) {
Logger.Write($"<{ctx.Connection.Id}> User is banned.");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt));
ctx.Connection.Dispose();
return;
}
ctx.Connection.BumpPing();
ctx.Connection.User = user;
ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!"));
await ctx.Chat.ContextAccess.WaitAsync();
try {
User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId);
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(user == null)
user = new User(
authResult.UserId,
authResult.UserName ?? $"({authResult.UserId})",
authResult.UserColour,
authResult.UserRank,
authResult.UserPermissions
);
else
ctx.Chat.UpdateUser(
user,
userName: authResult.UserName ?? $"({authResult.UserId})",
colour: authResult.UserColour,
rank: authResult.UserRank,
perms: authResult.UserPermissions
);
if(!string.IsNullOrWhiteSpace(line))
ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
// Enforce a maximum amount of connections per user
if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
ctx.Connection.Dispose();
return;
}
ctx.Connection.BumpPing();
ctx.Connection.User = user;
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))
ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
}
ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength);
} finally {
ctx.Chat.ContextAccess.Release();
}
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}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
ctx.Connection.Dispose();
throw;
} catch(Exception ex) {
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception));
ctx.Connection.Dispose();
throw;
}
}).Wait();
}

View file

@ -1,10 +1,9 @@
using SharpChat.Misuzu;
using SharpChat.Auth;
using SharpChat.S2CPackets;
using System.Net;
namespace SharpChat.C2SPacketHandlers {
public class PingC2SPacketHandler(MisuzuClient msz) : C2SPacketHandler {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler {
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
@ -24,13 +23,13 @@ namespace SharpChat.C2SPacketHandlers {
ctx.Chat.ContextAccess.Wait();
try {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
(string, string)[] bumpList = [.. ctx.Chat.Users
(IPAddress, string)[] bumpList = [.. ctx.Chat.Users
.Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
.Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))];
.Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))];
if(bumpList.Length > 0)
Task.Run(async () => {
await Misuzu.BumpUsersOnlineAsync(bumpList);
await authClient.AuthBumpUsersOnlineAsync(bumpList);
}).Wait();
LastBump = DateTimeOffset.UtcNow;

View file

@ -1,4 +1,4 @@
using SharpChat.Config;
using SharpChat.Configuration;
using SharpChat.Events;
using SharpChat.Snowflake;
using System.Globalization;
@ -35,7 +35,7 @@ namespace SharpChat.C2SPacketHandlers {
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)
if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString())
return;
ctx.Chat.ContextAccess.Wait();

View file

@ -1,16 +1,16 @@
namespace SharpChat {
namespace SharpChat {
public class Channel(
string name,
string password = "",
bool isTemporary = false,
int rank = 0,
long ownerId = 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 long OwnerId { get; set; } = ownerId;
public string OwnerId { get; set; } = ownerId;
public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password);
@ -20,7 +20,7 @@
}
public bool IsOwner(User user) {
return OwnerId > 0
return string.IsNullOrEmpty(OwnerId)
&& user != null
&& OwnerId == user.UserId;
}

View file

@ -1,10 +1,8 @@
using SharpChat.Misuzu;
using SharpChat.Bans;
using SharpChat.S2CPackets;
namespace SharpChat.ClientCommands {
public class BanListClientCommand(MisuzuClient msz) : ClientCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public class BanListClientCommand(BansClient bansClient) : ClientCommand {
public bool IsMatch(ClientCommandContext ctx) {
return ctx.NameEquals("bans")
|| ctx.NameEquals("banned");
@ -19,18 +17,15 @@ namespace SharpChat.ClientCommands {
}
Task.Run(async () => {
MisuzuBanInfo[]? mbis = await Misuzu.GetBanListAsync();
if(mbis is null)
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
else
try {
BanInfo[] banInfos = await bansClient.BanGetListAsync();
ctx.Chat.SendTo(ctx.User, new BanListS2CPacket(
msgId,
mbis.Where(mbi => mbi.IsBanned && !mbi.HasExpired)
.Select(mbi => new BanListS2CPacket.Entry(
BanListS2CPacket.Type.UserName, // Misuzu currently only does username bans so we can just do this
mbi.UserName ?? $"({mbi.UserId})"
))
banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString()))
));
} catch(Exception) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
}
}).Wait();
}
}

View file

@ -1,10 +1,9 @@
using SharpChat.Misuzu;
using SharpChat.Bans;
using SharpChat.S2CPackets;
using System.Net;
namespace SharpChat.ClientCommands {
public class KickBanClientCommand(MisuzuClient msz) : ClientCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public class KickBanClientCommand(BansClient bansClient) : ClientCommand {
public bool IsMatch(ClientCommandContext ctx) {
return ctx.NameEquals("kick")
|| ctx.NameEquals("ban");
@ -53,25 +52,20 @@ namespace SharpChat.ClientCommands {
string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
Task.Run(async () => {
string userId = banUser.UserId.ToString();
string userIp = ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault()?.ToString() ?? string.Empty;
// obviously it makes no sense to only check for one ip address but that's current misuzu limitations
MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId, userIp);
if(fbi is null) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
return;
}
if(fbi.IsBanned && !fbi.HasExpired) {
BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId);
if(banInfo is not null) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
return;
}
await Misuzu.CreateBanAsync(
userId, userIp,
ctx.User.UserId.ToString(), ctx.Connection.RemoteAddress.ToString(),
duration, banReason
await bansClient.BanCreateAsync(
BanKind.User,
duration,
ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None,
banUser.UserId,
banReason,
ctx.Connection.RemoteAddress,
ctx.User.UserId
);
ctx.Chat.BanUser(banUser, duration);

View file

@ -1,4 +1,4 @@
using SharpChat.S2CPackets;
using SharpChat.S2CPackets;
using System.Globalization;
using System.Text;
@ -24,7 +24,7 @@ namespace SharpChat.ClientCommands {
int offset = 0;
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId);
targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString());
++offset;
}

View file

@ -1,11 +1,9 @@
using SharpChat.Misuzu;
using SharpChat.Bans;
using SharpChat.S2CPackets;
using System.Net;
namespace SharpChat.ClientCommands {
public class PardonAddressClientCommand(MisuzuClient msz) : ClientCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand {
public bool IsMatch(ClientCommandContext ctx) {
return ctx.NameEquals("pardonip")
|| ctx.NameEquals("unbanip");
@ -28,19 +26,13 @@ namespace SharpChat.ClientCommands {
unbanAddrTarget = unbanAddr.ToString();
Task.Run(async () => {
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget);
if(banInfo is null) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
return;
}
if(!banInfo.IsBanned || banInfo.HasExpired) {
BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr);
if(banInfo?.Kind != BanKind.IPAddress) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
return;
}
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress);
if(wasBanned)
if(await bansClient.BanRevokeAsync(banInfo))
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget));
else
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));

View file

@ -1,10 +1,9 @@
using SharpChat.Misuzu;
using SharpChat.Bans;
using SharpChat.S2CPackets;
using System.Text.Json;
namespace SharpChat.ClientCommands {
public class PardonUserClientCommand(MisuzuClient msz) : ClientCommand {
private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
public class PardonUserClientCommand(BansClient bansClient) : ClientCommand {
public bool IsMatch(ClientCommandContext ctx) {
return ctx.NameEquals("pardon")
|| ctx.NameEquals("unban");
@ -18,39 +17,32 @@ namespace SharpChat.ClientCommands {
return;
}
bool unbanUserTargetIsName = true;
string? unbanUserTarget = ctx.Args.FirstOrDefault();
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
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)) {
unbanUserTargetIsName = false;
unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId);
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;
}
if(unbanUser != null)
unbanUserTarget = unbanUser.UserId.ToString();
Task.Run(async () => {
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName);
if(banInfo is null) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget);
if(banInfo?.Kind != BanKind.User) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
return;
}
if(!banInfo.IsBanned || banInfo.HasExpired) {
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget));
return;
}
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId);
if(wasBanned)
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserTarget));
if(await bansClient.BanRevokeAsync(banInfo))
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay));
else
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget));
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
}).Wait();
}
}

View file

@ -1,4 +1,4 @@
using SharpChat.S2CPackets;
using SharpChat.S2CPackets;
namespace SharpChat.ClientCommands {
public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand {
@ -11,7 +11,7 @@ namespace SharpChat.ClientCommands {
}
public void Dispatch(ClientCommandContext ctx) {
if(ctx.User.UserId != 1) {
if(!ctx.User.UserId.Equals("1")) {
long msgId = ctx.Chat.RandomSnowflake.Next();
ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
return;

View file

@ -1,4 +1,4 @@
using SharpChat.Events;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.S2CPackets;
using SharpChat.Snowflake;
@ -6,7 +6,7 @@ using System.Net;
namespace SharpChat {
public class Context {
public record ChannelUserAssoc(long UserId, string ChannelName);
public record ChannelUserAssoc(string UserId, string ChannelName);
public readonly SemaphoreSlim ContextAccess = new(1, 1);
@ -18,8 +18,8 @@ namespace SharpChat {
public HashSet<User> Users { get; } = [];
public EventStorage.EventStorage Events { get; }
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
public Dictionary<long, RateLimiter> UserRateLimiters { get; } = [];
public Dictionary<long, Channel> UserLastChannel { get; } = [];
public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
public Dictionary<string, Channel> UserLastChannel { get; } = [];
public Context(EventStorage.EventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
@ -38,7 +38,7 @@ namespace SharpChat {
if(!mce.ChannelName.StartsWith('@'))
return;
IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString());
if(uids.Count() != 2)
return;
@ -120,12 +120,12 @@ namespace SharpChat {
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
}
public long[] GetChannelUserIds(Channel channel) {
public string[] GetChannelUserIds(Channel channel) {
return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
}
public User[] GetChannelUsers(Channel channel) {
long[] ids = GetChannelUserIds(channel);
string[] ids = GetChannelUserIds(channel);
return [.. Users.Where(u => ids.Contains(u.UserId))];
}

View file

@ -1,10 +1,10 @@
namespace SharpChat.EventStorage {
namespace SharpChat.EventStorage {
public interface EventStorage {
void AddEvent(
long id,
string type,
string channelName,
long senderId,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,

View file

@ -1,4 +1,4 @@
using MySqlConnector;
using MySqlConnector;
using System.Text;
using System.Text.Json;
@ -10,7 +10,7 @@ namespace SharpChat.EventStorage {
long id,
string type,
string channelName,
long senderId,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
@ -19,8 +19,6 @@ namespace SharpChat.EventStorage {
object? data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
ArgumentNullException.ThrowIfNull(type);
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`)"
@ -31,7 +29,7 @@ namespace SharpChat.EventStorage {
new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName),
new MySqlParameter("flags", (byte)flags),
new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)),
new MySqlParameter("sender", senderId < 1 ? null : senderId),
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),
@ -72,7 +70,7 @@ namespace SharpChat.EventStorage {
reader.GetInt64("event_id"),
Encoding.ASCII.GetString((byte[])reader["event_type"]),
reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User(
reader.GetInt64("event_sender"),
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"),

View file

@ -1,9 +1,9 @@
using MySqlConnector;
using SharpChat.Config;
using MySqlConnector;
using SharpChat.Configuration;
namespace SharpChat.EventStorage {
public partial class MariaDBEventStorage {
public static string BuildConnString(Config.Config config) {
public static string BuildConnString(Configuration.Config config) {
return BuildConnString(
config.ReadValue("host", "localhost"),
config.ReadValue("user", string.Empty),

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
namespace SharpChat.EventStorage {
public class VirtualEventStorage : EventStorage {
@ -8,7 +8,7 @@ namespace SharpChat.EventStorage {
long id,
string type,
string channelName,
long senderId,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
@ -17,18 +17,28 @@ namespace SharpChat.EventStorage {
object? data = null,
StoredEventFlags flags = StoredEventFlags.None
) {
ArgumentNullException.ThrowIfNull(type);
// VES is meant as an emergency fallback but this is something else
JsonDocument hack = JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data));
Events.Add(id, new(id, type, senderId < 1 ? null : new User(
senderId,
senderName,
senderColour,
senderRank,
senderPerms,
senderNick
), DateTimeOffset.Now, null, channelName, hack, flags));
Events.Add(
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
)
);
}
public StoredEventInfo? GetEvent(long seqId) {

View file

@ -1,8 +1,8 @@
namespace SharpChat.Events {
namespace SharpChat.Events {
public class MessageCreateEvent(
long msgId,
string channelName,
long senderId,
string senderId,
string senderName,
ColourInheritable senderColour,
int senderRank,
@ -16,7 +16,7 @@
) : ChatEvent {
public long MessageId { get; } = msgId;
public string ChannelName { get; } = channelName;
public long SenderId { get; } = senderId;
public string SenderId { get; } = senderId;
public string SenderName { get; } = senderName;
public ColourInheritable SenderColour { get; } = senderColour;
public int SenderRank { get; } = senderRank;

View file

@ -1,28 +0,0 @@
using System.Text.Json.Serialization;
namespace SharpChat.Misuzu {
public class MisuzuAuthInfo {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; } = "none";
[JsonPropertyName("user_id")]
public long UserId { get; set; }
[JsonPropertyName("username")]
public string? UserName { get; set; }
[JsonPropertyName("colour_raw")]
public int ColourRaw { get; set; }
public ColourInheritable Colour => ColourInheritable.FromMisuzu(ColourRaw);
[JsonPropertyName("hierarchy")]
public int Rank { get; set; }
[JsonPropertyName("perms")]
public UserPermissions Permissions { get; set; }
}
}

View file

@ -1,241 +0,0 @@
using SharpChat.Config;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace SharpChat.Misuzu {
public class MisuzuClient {
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
private const string DEFAULT_SECRET_KEY = "woomy";
private const string BUMP_ONLINE_URL = "{0}/bump";
private const string AUTH_VERIFY_URL = "{0}/verify";
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
private const string BANS_CREATE_URL = "{0}/bans/create";
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
private const string VERIFY_SIG = "verify#{0}#{1}#{2}";
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
private const string BANS_LIST_SIG = "list#{0}";
private readonly HttpClient HttpClient;
private CachedValue<string> BaseURL { get; }
private CachedValue<string> SecretKey { get; }
public MisuzuClient(HttpClient httpClient, Config.Config config) {
ArgumentNullException.ThrowIfNull(config);
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
}
public string CreateStringSignature(string str) {
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
}
public 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<MisuzuAuthInfo?> AuthVerifyAsync(string method, string token, string ipAddr) {
method ??= string.Empty;
token ??= string.Empty;
ipAddr ??= string.Empty;
string sig = string.Format(VERIFY_SIG, method, token, ipAddr);
HttpRequestMessage req = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "method", method },
{ "token", token },
{ "ipaddr", ipAddr },
}),
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
return JsonSerializer.Deserialize<MisuzuAuthInfo>(
await res.Content.ReadAsByteArrayAsync()
);
}
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
ArgumentNullException.ThrowIfNull(list);
if(!list.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 (userId, ipAddr) in list) {
sb.AppendFormat("#{0}:{1}", userId, ipAddr);
formData.Add(string.Format("u[{0}]", userId), ipAddr);
}
HttpRequestMessage req = new(HttpMethod.Post, string.Format(BUMP_ONLINE_URL, BaseURL)) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
},
Content = new FormUrlEncodedContent(formData),
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
try {
res.EnsureSuccessStatusCode();
} catch(HttpRequestException) {
Logger.Debug(await res.Content.ReadAsStringAsync());
#if DEBUG
throw;
#endif
}
}
public async Task<MisuzuBanInfo?> CheckBanAsync(string? userId = null, string? ipAddr = null, bool userIdIsName = false) {
userId ??= string.Empty;
ipAddr ??= string.Empty;
string userIdIsNameStr = userIdIsName ? "1" : "0";
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr));
string sig = string.Format(BANS_CHECK_SIG, now, userId, ipAddr, userIdIsNameStr);
HttpRequestMessage req = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
return JsonSerializer.Deserialize<MisuzuBanInfo>(
await res.Content.ReadAsByteArrayAsync()
);
}
public async Task<MisuzuBanInfo[]?> GetBanListAsync() {
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 req = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
return JsonSerializer.Deserialize<MisuzuBanInfo[]>(
await res.Content.ReadAsByteArrayAsync()
);
}
public enum BanRevokeKind {
UserId,
RemoteAddress,
}
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
ArgumentNullException.ThrowIfNull(banInfo);
string type = kind switch {
BanRevokeKind.UserId => "user",
BanRevokeKind.RemoteAddress => "addr",
_ => throw new ArgumentException("Invalid kind specified.", nameof(kind)),
};
string target = kind switch {
BanRevokeKind.UserId => banInfo.UserId,
BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
_ => null,
} ?? string.Empty;
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 req = new(HttpMethod.Delete, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
if(res.StatusCode == HttpStatusCode.NotFound)
return false;
res.EnsureSuccessStatusCode();
return res.StatusCode == HttpStatusCode.NoContent;
}
public async Task CreateBanAsync(
string targetId,
string targetAddr,
string modId,
string modAddr,
TimeSpan duration,
string reason
) {
if(string.IsNullOrWhiteSpace(targetAddr))
throw new ArgumentNullException(nameof(targetAddr));
if(string.IsNullOrWhiteSpace(modAddr))
throw new ArgumentNullException(nameof(modAddr));
if(duration <= TimeSpan.Zero)
return;
modId ??= string.Empty;
targetId ??= string.Empty;
reason ??= string.Empty;
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string sig = string.Format(
BANS_CREATE_SIG,
now, targetId, targetAddr,
modId, modAddr,
durationStr, isPerma, reason
);
HttpRequestMessage req = 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", targetId },
{ "ua", targetAddr },
{ "mi", modId },
{ "ma", modAddr },
{ "d", durationStr },
{ "p", isPerma },
{ "r", reason },
}),
};
using HttpResponseMessage res = await HttpClient.SendAsync(req);
res.EnsureSuccessStatusCode();
}
}
}

View file

@ -1,6 +1,6 @@
using SharpChat.Config;
using SharpChat.Configuration;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.Flashii;
using System.Text;
namespace SharpChat {
@ -52,7 +52,7 @@ namespace SharpChat {
if(hasCancelled) return;
MisuzuClient msz = new(httpClient, config.ScopeTo("msz"));
FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
if(hasCancelled) return;
@ -67,7 +67,7 @@ namespace SharpChat {
if(hasCancelled) return;
using SockChatServer scs = new(httpClient, msz, evtStore, config.ScopeTo("chat"));
using SockChatServer scs = new(httpClient, flashii, flashii, evtStore, config.ScopeTo("chat"));
scs.Listen(mre);
mre.WaitOne();

View file

@ -1,4 +1,4 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class AuthFailS2CPacket(
@ -9,6 +9,7 @@ namespace SharpChat.S2CPackets {
AuthInvalid,
MaxSessions,
Banned,
Exception,
}
public string Pack() {
@ -21,6 +22,9 @@ namespace SharpChat.S2CPackets {
default:
sb.Append("authfail");
break;
case Reason.Exception:
sb.Append("userfail");
break;
case Reason.MaxSessions:
sb.Append("sockfail");
break;

View file

@ -1,8 +1,8 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class AuthSuccessS2CPacket(
long userId,
string userId,
string userName,
ColourInheritable userColour,
int userRank,

View file

@ -1,16 +1,12 @@
using System.Text;
using SharpChat.Bans;
using System.Text;
namespace SharpChat.S2CPackets {
public class BanListS2CPacket(
long msgId,
IEnumerable<BanListS2CPacket.Entry> entries
) : S2CPacket {
public enum Type {
UserName,
IPAddress,
}
public record Entry(Type type, string value);
public record Entry(BanKind type, string value);
public string Pack() {
StringBuilder sb = new();
@ -20,7 +16,7 @@ namespace SharpChat.S2CPackets {
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 == Type.IPAddress ? "/unbanip" : "/unban",
entry.type == BanKind.IPAddress ? "/unbanip" : "/unban",
entry.value
))));
sb.Append('\t');

View file

@ -1,10 +1,10 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class ChatMessageAddS2CPacket(
long msgId,
DateTimeOffset created,
long userId,
string userId,
string text,
bool isAction,
bool isPrivate

View file

@ -1,8 +1,8 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket {
public record Entry(long id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
public string Pack() {
StringBuilder sb = new();

View file

@ -1,9 +1,9 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class UserChannelJoinS2CPacket(
long msgId,
long userId,
string userId,
string userName,
ColourInheritable userColour,
int userRank,

View file

@ -1,7 +1,7 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class UserChannelLeaveS2CPacket(long msgId, long userId) : S2CPacket {
public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket {
public string Pack() {
StringBuilder sb = new();

View file

@ -1,10 +1,10 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class UserConnectS2CPacket(
long msgId,
DateTimeOffset joined,
long userId,
string userId,
string userName,
ColourInheritable userColour,
int userRank,

View file

@ -1,10 +1,10 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class UserDisconnectS2CPacket(
long msgId,
DateTimeOffset disconnected,
long userId,
string userId,
string userName,
UserDisconnectS2CPacket.Reason reason
) : S2CPacket {

View file

@ -1,8 +1,8 @@
using System.Text;
using System.Text;
namespace SharpChat.S2CPackets {
public class UserUpdateS2CPacket(
long userId,
string userId,
string userName,
ColourInheritable userColour,
int userRank,

View file

@ -33,6 +33,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChat.Misuzu\SharpChat.Flashii.csproj" />
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
</ItemGroup>

View file

@ -1,10 +1,11 @@
using Fleck;
using SharpChat.ClientCommands;
using SharpChat.Config;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.S2CPackets;
using Fleck;
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.C2SPacketHandlers;
using SharpChat.ClientCommands;
using SharpChat.Configuration;
using SharpChat.S2CPackets;
using System.Net;
namespace SharpChat {
public class SockChatServer : IDisposable {
@ -18,7 +19,7 @@ namespace SharpChat {
public Context Context { get; }
private readonly HttpClient HttpClient;
private readonly MisuzuClient Misuzu;
private readonly BansClient BansClient;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
@ -35,11 +36,17 @@ namespace SharpChat {
private Channel DefaultChannel { get; set; }
public SockChatServer(HttpClient httpClient, MisuzuClient msz, EventStorage.EventStorage evtStore, Config.Config config) {
public SockChatServer(
HttpClient httpClient,
AuthClient authClient,
BansClient bansClient,
EventStorage.EventStorage evtStore,
Config config
) {
Logger.Write("Initialising Sock Chat server...");
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
HttpClient = httpClient;
BansClient = bansClient;
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
@ -51,7 +58,7 @@ namespace SharpChat {
string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
if(channelNames is not null)
foreach(string channelName in channelNames) {
Config.Config channelCfg = config.ScopeTo($"channels:{channelName}");
Config channelCfg = config.ScopeTo($"channels:{channelName}");
string name = channelCfg.SafeReadValue("name", string.Empty)!;
if(string.IsNullOrWhiteSpace(name))
@ -70,10 +77,10 @@ namespace SharpChat {
if(DefaultChannel is null)
throw new Exception("The default channel could not be determined.");
GuestHandlers.Add(new AuthC2SPacketHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections));
GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections));
AuthedHandlers.AddRange([
new PingC2SPacketHandler(Misuzu),
new PingC2SPacketHandler(authClient),
SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength),
]);
@ -90,10 +97,10 @@ namespace SharpChat {
new RankChannelClientCommand(),
new BroadcastClientCommand(),
new DeleteMessageClientCommand(),
new KickBanClientCommand(msz),
new PardonUserClientCommand(msz),
new PardonAddressClientCommand(msz),
new BanListClientCommand(msz),
new KickBanClientCommand(bansClient),
new PardonUserClientCommand(bansClient),
new PardonAddressClientCommand(bansClient),
new BanListClientCommand(bansClient),
new RemoteAddressClientCommand(),
]);
@ -184,11 +191,13 @@ namespace SharpChat {
Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood);
if(banDuration > TimeSpan.Zero)
Misuzu.CreateBanAsync(
conn.User.UserId.ToString(), conn.RemoteAddress.ToString(),
string.Empty, "::1",
BansClient.BanCreateAsync(
BanKind.User,
banDuration,
"Kicked from chat for flood protection."
conn.RemoteAddress,
conn.User.UserId,
"Kicked from chat for flood protection.",
IPAddress.IPv6Loopback
).Wait();
return;

View file

@ -1,10 +1,10 @@
using SharpChat.ClientCommands;
using SharpChat.ClientCommands;
using System.Globalization;
using System.Text;
namespace SharpChat {
public class User(
long userId,
string userId,
string userName,
ColourInheritable colour,
int rank,
@ -17,7 +17,7 @@ namespace SharpChat {
public const int DEFAULT_MINIMUM_DELAY = 10000;
public const int DEFAULT_RISKY_OFFSET = 5;
public long UserId { get; } = userId;
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;
@ -65,9 +65,9 @@ namespace SharpChat {
}
public static string GetDMChannelName(User user1, User user2) {
return user1.UserId < user2.UserId
? $"@{user1.UserId}-{user2.UserId}"
: $"@{user2.UserId}-{user1.UserId}";
return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0
? $"@{user2.UserId}-{user1.UserId}"
: $"@{user1.UserId}-{user2.UserId}";
}
}
}

View file

@ -0,0 +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);
}
}

View file

@ -0,0 +1,3 @@
namespace SharpChat.Auth {
public class AuthFailedException(string message) : Exception(message) {}
}

View file

@ -0,0 +1,9 @@
namespace SharpChat.Auth {
public interface AuthResult {
string UserId { get; }
string UserName { get; }
ColourInheritable UserColour { get; }
int UserRank { get; }
UserPermissions UserPermissions { get; }
}
}

View file

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

View file

@ -0,0 +1,6 @@
namespace SharpChat.Bans {
public enum BanKind {
User,
IPAddress,
}
}

View file

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

View file

@ -0,0 +1,7 @@
using System.Net;
namespace SharpChat.Bans {
public interface IPAddressBanInfo : BanInfo {
IPAddress Address { get; }
}
}

View file

@ -0,0 +1,7 @@
namespace SharpChat.Bans {
public interface UserBanInfo : BanInfo {
string UserId { get; }
string UserName { get; }
ColourInheritable UserColour { get; }
}
}

View file

@ -1,4 +1,4 @@
namespace SharpChat.Config {
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));

View file

@ -1,4 +1,4 @@
namespace SharpChat.Config {
namespace SharpChat.Configuration {
public interface Config : IDisposable {
/// <summary>
/// Creates a proxy object that forces all names to start with the given prefix.

View file

@ -1,4 +1,4 @@
namespace SharpChat.Config {
namespace SharpChat.Configuration {
public abstract class ConfigException : Exception {
public ConfigException(string message) : base(message) { }
public ConfigException(string message, Exception ex) : base(message, ex) { }

View file

@ -1,4 +1,4 @@
namespace SharpChat.Config {
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));

View file

@ -1,6 +1,6 @@
using System.Text;
using System.Text;
namespace SharpChat.Config {
namespace SharpChat.Configuration {
public class StreamConfig : Config {
private Stream Stream { get; }
private StreamReader StreamReader { get; }