Split out the Flashii interaction code into a separate library.
This commit is contained in:
parent
51f5c4c948
commit
2eba089a21
55 changed files with 769 additions and 552 deletions
.editorconfig
SharpChat.Misuzu
FlashiiAuthResult.csFlashiiBanInfo.csFlashiiClient.csFlashiiIPAddressBanInfo.csFlashiiRawBanInfo.csFlashiiUserBanInfo.csSharpChat.Flashii.csproj
SharpChat.slnSharpChat
C2SPacketHandlers
Channel.csClientCommands
BanListClientCommand.csKickBanClientCommand.csNickClientCommand.csPardonAddressClientCommand.csPardonUserClientCommand.csShutdownRestartClientCommand.cs
Context.csEventStorage
Events
Misuzu
Program.csS2CPackets
AuthFailS2CPacket.csAuthSuccessS2CPacket.csBanListS2CPacket.csChatMessageAddS2CPacket.csContextUsersS2CPacket.csUserChannelJoinS2CPacket.csUserChannelLeaveS2CPacket.csUserConnectS2CPacket.csUserDisconnectS2CPacket.csUserUpdateS2CPacket.cs
SharpChat.csprojSockChatServer.csUser.csSharpChatCommon
133
.editorconfig
133
.editorconfig
|
@ -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
|
||||
|
|
31
SharpChat.Misuzu/FlashiiAuthResult.cs
Normal file
31
SharpChat.Misuzu/FlashiiAuthResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
13
SharpChat.Misuzu/FlashiiBanInfo.cs
Normal file
13
SharpChat.Misuzu/FlashiiBanInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
240
SharpChat.Misuzu/FlashiiClient.cs
Normal file
240
SharpChat.Misuzu/FlashiiClient.cs
Normal 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));
|
||||
})];
|
||||
}
|
||||
}
|
||||
}
|
9
SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs
Normal file
9
SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
10
SharpChat.Misuzu/FlashiiUserBanInfo.cs
Normal file
10
SharpChat.Misuzu/FlashiiUserBanInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
13
SharpChat.Misuzu/SharpChat.Flashii.csproj
Normal file
13
SharpChat.Misuzu/SharpChat.Flashii.csproj
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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))];
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpChat.Misuzu\SharpChat.Flashii.csproj" />
|
||||
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
SharpChatCommon/Auth/AuthClient.cs
Normal file
8
SharpChatCommon/Auth/AuthClient.cs
Normal 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);
|
||||
}
|
||||
}
|
3
SharpChatCommon/Auth/AuthFailedException.cs
Normal file
3
SharpChatCommon/Auth/AuthFailedException.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace SharpChat.Auth {
|
||||
public class AuthFailedException(string message) : Exception(message) {}
|
||||
}
|
9
SharpChatCommon/Auth/AuthResult.cs
Normal file
9
SharpChatCommon/Auth/AuthResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
9
SharpChatCommon/Bans/BanInfo.cs
Normal file
9
SharpChatCommon/Bans/BanInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
6
SharpChatCommon/Bans/BanKind.cs
Normal file
6
SharpChatCommon/Bans/BanKind.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Bans {
|
||||
public enum BanKind {
|
||||
User,
|
||||
IPAddress,
|
||||
}
|
||||
}
|
18
SharpChatCommon/Bans/BansClient.cs
Normal file
18
SharpChatCommon/Bans/BansClient.cs
Normal 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();
|
||||
}
|
||||
}
|
7
SharpChatCommon/Bans/IPAddressBanInfo.cs
Normal file
7
SharpChatCommon/Bans/IPAddressBanInfo.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System.Net;
|
||||
|
||||
namespace SharpChat.Bans {
|
||||
public interface IPAddressBanInfo : BanInfo {
|
||||
IPAddress Address { get; }
|
||||
}
|
||||
}
|
7
SharpChatCommon/Bans/UserBanInfo.cs
Normal file
7
SharpChatCommon/Bans/UserBanInfo.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace SharpChat.Bans {
|
||||
public interface UserBanInfo : BanInfo {
|
||||
string UserId { get; }
|
||||
string UserName { get; }
|
||||
ColourInheritable UserColour { get; }
|
||||
}
|
||||
}
|
|
@ -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));
|
|
@ -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.
|
|
@ -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) { }
|
|
@ -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));
|
|
@ -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; }
|
Loading…
Add table
Add a link
Reference in a new issue