Switched to file namespace declarations.
This commit is contained in:
parent
6593929827
commit
34e4e9b1a9
93 changed files with 3470 additions and 3471 deletions
.editorconfig
SharpChat.Flashii
FlashiiAuthResult.csFlashiiBanInfo.csFlashiiClient.csFlashiiIPAddressBanInfo.csFlashiiRawBanInfo.csFlashiiUserBanInfo.cs
SharpChat.SockChat
S2CPacket.cs
S2CPackets
AuthFailS2CPacket.csAuthSuccessS2CPacket.csBanListS2CPacket.csChannelCreateS2CPacket.csChannelDeleteS2CPacket.csChannelUpdateS2CPacket.csChatMessageAddS2CPacket.csChatMessageDeleteS2CPacket.csCommandResponseS2CPacket.csContextChannelsS2CPacket.csContextClearS2CPacket.csContextUsersS2CPacket.csForceDisconnectS2CPacket.csPongS2CPacket.csUserChannelForceJoinS2CPacket.csUserChannelJoinS2CPacket.csUserChannelLeaveS2CPacket.csUserConnectS2CPacket.csUserDisconnectS2CPacket.csUserUpdateS2CPacket.cs
SharpChat
C2SPacketHandler.csC2SPacketHandlerContext.cs
C2SPacketHandlers
Channel.csClientCommand.csClientCommandContext.csClientCommands
AFKClientCommand.csActionClientCommand.csBanListClientCommand.csBroadcastClientCommand.csCreateChannelClientCommand.csDeleteChannelClientCommand.csDeleteMessageClientCommand.csJoinChannelClientCommand.csKickBanClientCommand.csNickClientCommand.csPardonAddressClientCommand.csPardonUserClientCommand.csPasswordChannelClientCommand.csRankChannelClientCommand.csRemoteAddressClientCommand.csShutdownRestartClientCommand.csWhisperClientCommand.csWhoClientCommand.cs
Connection.csContext.csEventStorage
EventStorage.csMariaDBEventStorage.csMariaDBEventStorage_Database.csMariaDBEventStorage_Migrations.csStoredEventFlags.csStoredEventInfo.csVirtualEventStorage.cs
Events
Program.csSharpChatWebSocketServer.csSockChat/S2CPackets
SockChatServer.csUser.csUserStatus.csSharpChatCommon
Auth
Bans
ColourInheritable.csColourRgb.csConfiguration
Logger.csRNG.csRateLimiter.csSharpInfo.csSnowflake
UserPermissions.cs
|
@ -15,7 +15,7 @@ csharp_indent_labels = one_less_than_current
|
||||||
csharp_using_directive_placement = outside_namespace:silent
|
csharp_using_directive_placement = outside_namespace:silent
|
||||||
csharp_prefer_simple_using_statement = true:suggestion
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
csharp_prefer_braces = true:silent
|
csharp_prefer_braces = true:silent
|
||||||
csharp_style_namespace_declarations = block_scoped:silent
|
csharp_style_namespace_declarations = file_scoped:silent
|
||||||
csharp_style_prefer_method_group_conversion = true:silent
|
csharp_style_prefer_method_group_conversion = true:silent
|
||||||
csharp_style_prefer_top_level_statements = true:silent
|
csharp_style_prefer_top_level_statements = true:silent
|
||||||
csharp_style_prefer_primary_constructors = true:suggestion
|
csharp_style_prefer_primary_constructors = true:suggestion
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
using SharpChat.Auth;
|
using SharpChat.Auth;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
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 class FlashiiAuthResult : AuthResult {
|
||||||
public bool Success { get; init; }
|
public string UserId => UserIdRaw.ToString();
|
||||||
|
public string UserName => UserNameRaw ?? string.Empty;
|
||||||
|
public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
|
||||||
|
|
||||||
[JsonPropertyName("reason")]
|
[JsonPropertyName("success")]
|
||||||
public string? Reason { get; init; }
|
public bool Success { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("user_id")]
|
[JsonPropertyName("reason")]
|
||||||
public long UserIdRaw { get; init; }
|
public string? Reason { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("username")]
|
[JsonPropertyName("user_id")]
|
||||||
public string? UserNameRaw { get; init; }
|
public long UserIdRaw { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("colour_raw")]
|
[JsonPropertyName("username")]
|
||||||
public int UserColourRaw { get; init; }
|
public string? UserNameRaw { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("hierarchy")]
|
[JsonPropertyName("colour_raw")]
|
||||||
public int UserRank { get; init; }
|
public int UserColourRaw { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("perms")]
|
[JsonPropertyName("hierarchy")]
|
||||||
public UserPermissions UserPermissions { get; init; }
|
public int UserRank { get; init; }
|
||||||
}
|
|
||||||
|
[JsonPropertyName("perms")]
|
||||||
|
public UserPermissions UserPermissions { get; init; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii;
|
||||||
public abstract class FlashiiBanInfo(
|
|
||||||
BanKind kind,
|
public abstract class FlashiiBanInfo(
|
||||||
FlashiiRawBanInfo rawBanInfo
|
BanKind kind,
|
||||||
) : BanInfo {
|
FlashiiRawBanInfo rawBanInfo
|
||||||
public BanKind Kind { get; } = kind;
|
) : BanInfo {
|
||||||
public bool IsPermanent { get; } = rawBanInfo.IsPermanent;
|
public BanKind Kind { get; } = kind;
|
||||||
public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt;
|
public bool IsPermanent { get; } = rawBanInfo.IsPermanent;
|
||||||
public abstract override string ToString();
|
public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt;
|
||||||
}
|
public abstract override string ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,235 +6,235 @@ using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
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";
|
public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient {
|
||||||
private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
|
||||||
|
private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
||||||
|
|
||||||
private string CreateStringSignature(string str) {
|
private const string DEFAULT_SECRET_KEY = "woomy";
|
||||||
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
|
private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
||||||
}
|
|
||||||
|
|
||||||
private string CreateBufferSignature(byte[] bytes) {
|
private string CreateStringSignature(string str) {
|
||||||
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
|
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
|
||||||
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private const string AUTH_VERIFY_URL = "{0}/verify";
|
private string CreateBufferSignature(byte[] bytes) {
|
||||||
private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";
|
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
|
||||||
|
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) {
|
private const string AUTH_VERIFY_URL = "{0}/verify";
|
||||||
|
private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";
|
||||||
|
|
||||||
|
public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) {
|
||||||
|
string remoteAddrStr = remoteAddr.ToString();
|
||||||
|
|
||||||
|
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
|
||||||
|
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||||
|
{ "method", scheme },
|
||||||
|
{ "token", token },
|
||||||
|
{ "ipaddr", remoteAddrStr },
|
||||||
|
}),
|
||||||
|
Headers = {
|
||||||
|
{ "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using Stream stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream);
|
||||||
|
if(authResult?.Success != true)
|
||||||
|
throw new AuthFailedException(authResult?.Reason ?? "none");
|
||||||
|
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump";
|
||||||
|
|
||||||
|
public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
|
||||||
|
if(!entries.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendFormat("bump#{0}", now);
|
||||||
|
|
||||||
|
Dictionary<string, string> formData = new() {
|
||||||
|
{ "t", now },
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach(var (remoteAddr, userId) in entries) {
|
||||||
string remoteAddrStr = remoteAddr.ToString();
|
string remoteAddrStr = remoteAddr.ToString();
|
||||||
|
sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr);
|
||||||
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
|
formData.Add(string.Format("u[{0}]", userId), remoteAddrStr);
|
||||||
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";
|
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) {
|
||||||
|
Headers = {
|
||||||
|
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
|
||||||
|
},
|
||||||
|
Content = new FormUrlEncodedContent(formData),
|
||||||
|
};
|
||||||
|
|
||||||
public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
if(!entries.Any())
|
response.EnsureSuccessStatusCode();
|
||||||
return;
|
}
|
||||||
|
|
||||||
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
private const string BANS_CREATE_URL = "{0}/bans/create";
|
||||||
StringBuilder sb = new();
|
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
|
||||||
sb.AppendFormat("bump#{0}", now);
|
|
||||||
|
|
||||||
Dictionary<string, string> formData = new() {
|
public async Task BanCreateAsync(
|
||||||
|
BanKind kind,
|
||||||
|
TimeSpan duration,
|
||||||
|
IPAddress remoteAddr,
|
||||||
|
string? userId = null,
|
||||||
|
string? reason = null,
|
||||||
|
IPAddress? issuerRemoteAddr = null,
|
||||||
|
string? issuerUserId = null
|
||||||
|
) {
|
||||||
|
if(duration <= TimeSpan.Zero || kind != BanKind.User)
|
||||||
|
return;
|
||||||
|
|
||||||
|
issuerUserId ??= string.Empty;
|
||||||
|
userId ??= string.Empty;
|
||||||
|
reason ??= string.Empty;
|
||||||
|
issuerRemoteAddr ??= IPAddress.IPv6None;
|
||||||
|
|
||||||
|
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
|
||||||
|
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
|
||||||
|
string remoteAddrStr = remoteAddr.ToString();
|
||||||
|
string issuerRemoteAddrStr = issuerRemoteAddr.ToString();
|
||||||
|
|
||||||
|
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||||
|
string sig = string.Format(
|
||||||
|
BANS_CREATE_SIG,
|
||||||
|
now, userId, remoteAddrStr,
|
||||||
|
issuerUserId, issuerRemoteAddrStr,
|
||||||
|
durationStr, isPerma, reason
|
||||||
|
);
|
||||||
|
|
||||||
|
HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
|
||||||
|
Headers = {
|
||||||
|
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||||
|
},
|
||||||
|
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||||
{ "t", now },
|
{ "t", now },
|
||||||
};
|
{ "ui", userId },
|
||||||
|
{ "ua", remoteAddrStr },
|
||||||
|
{ "mi", issuerUserId },
|
||||||
|
{ "ma", issuerRemoteAddrStr },
|
||||||
|
{ "d", durationStr },
|
||||||
|
{ "p", isPerma },
|
||||||
|
{ "r", reason },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
foreach(var (remoteAddr, userId) in entries) {
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
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)) {
|
response.EnsureSuccessStatusCode();
|
||||||
Headers = {
|
}
|
||||||
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
|
|
||||||
},
|
|
||||||
Content = new FormUrlEncodedContent(formData),
|
|
||||||
};
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
|
||||||
response.EnsureSuccessStatusCode();
|
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
|
||||||
}
|
|
||||||
|
|
||||||
private const string BANS_CREATE_URL = "{0}/bans/create";
|
public async Task<bool> BanRevokeAsync(BanInfo info) {
|
||||||
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
|
string type;
|
||||||
|
string target;
|
||||||
|
|
||||||
public async Task BanCreateAsync(
|
if(info is UserBanInfo ubi) {
|
||||||
BanKind kind,
|
if(info.Kind != BanKind.User)
|
||||||
TimeSpan duration,
|
throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info));
|
||||||
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;
|
type = "user";
|
||||||
userId ??= string.Empty;
|
target = ubi.UserId;
|
||||||
reason ??= string.Empty;
|
} else if(info is IPAddressBanInfo iabi) {
|
||||||
issuerRemoteAddr ??= IPAddress.IPv6None;
|
if(info.Kind != BanKind.IPAddress)
|
||||||
|
throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info));
|
||||||
|
|
||||||
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
|
type = "addr";
|
||||||
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
|
target = iabi.Address.ToString();
|
||||||
string remoteAddrStr = remoteAddr.ToString();
|
} else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));
|
||||||
string issuerRemoteAddrStr = issuerRemoteAddr.ToString();
|
|
||||||
|
|
||||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||||
string sig = string.Format(
|
string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
|
||||||
BANS_CREATE_SIG,
|
string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
|
||||||
now, userId, remoteAddrStr,
|
|
||||||
issuerUserId, issuerRemoteAddrStr,
|
|
||||||
durationStr, isPerma, reason
|
|
||||||
);
|
|
||||||
|
|
||||||
HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
|
HttpRequestMessage request = new(HttpMethod.Delete, url) {
|
||||||
Headers = {
|
Headers = {
|
||||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
{ "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);
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
|
if(response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
return false;
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
|
|
||||||
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
|
return response.StatusCode == HttpStatusCode.NoContent;
|
||||||
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
|
}
|
||||||
|
|
||||||
public async Task<bool> BanRevokeAsync(BanInfo info) {
|
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
|
||||||
string type;
|
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
|
||||||
string target;
|
|
||||||
|
|
||||||
if(info is UserBanInfo ubi) {
|
public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) {
|
||||||
if(info.Kind != BanKind.User)
|
userIdOrName ??= "0";
|
||||||
throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info));
|
remoteAddr ??= IPAddress.None;
|
||||||
|
|
||||||
type = "user";
|
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||||
target = ubi.UserId;
|
bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
|
||||||
} else if(info is IPAddressBanInfo iabi) {
|
string remoteAddrStr = remoteAddr.ToString();
|
||||||
if(info.Kind != BanKind.IPAddress)
|
string usingUserNameStr = usingUserName ? "1" : "0";
|
||||||
throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info));
|
string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr));
|
||||||
|
string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr);
|
||||||
|
|
||||||
type = "addr";
|
HttpRequestMessage request = new(HttpMethod.Get, url) {
|
||||||
target = iabi.Address.ToString();
|
Headers = {
|
||||||
} else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));
|
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
|
response.EnsureSuccessStatusCode();
|
||||||
string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
|
|
||||||
|
|
||||||
HttpRequestMessage request = new(HttpMethod.Delete, url) {
|
using Stream stream = await response.Content.ReadAsStreamAsync();
|
||||||
Headers = {
|
FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream);
|
||||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired)
|
||||||
},
|
return null;
|
||||||
};
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
return rawBanInfo.RemoteAddress is null or "::"
|
||||||
if(response.StatusCode == HttpStatusCode.NotFound)
|
? new FlashiiUserBanInfo(rawBanInfo)
|
||||||
return false;
|
: new FlashiiIPAddressBanInfo(rawBanInfo);
|
||||||
|
}
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
|
||||||
|
private const string BANS_LIST_SIG = "list#{0}";
|
||||||
|
|
||||||
return response.StatusCode == HttpStatusCode.NoContent;
|
public async Task<BanInfo[]> BanGetListAsync() {
|
||||||
}
|
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||||
|
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
||||||
|
string sig = string.Format(BANS_LIST_SIG, now);
|
||||||
|
|
||||||
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
|
HttpRequestMessage request = new(HttpMethod.Get, url) {
|
||||||
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
|
Headers = {
|
||||||
|
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) {
|
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||||
userIdOrName ??= "0";
|
response.EnsureSuccessStatusCode();
|
||||||
remoteAddr ??= IPAddress.None;
|
|
||||||
|
|
||||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
using Stream stream = await response.Content.ReadAsStreamAsync();
|
||||||
bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
|
FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream);
|
||||||
string remoteAddrStr = remoteAddr.ToString();
|
if(list is null || list.Length < 1)
|
||||||
string usingUserNameStr = usingUserName ? "1" : "0";
|
return [];
|
||||||
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) {
|
return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => {
|
||||||
Headers = {
|
return (BanInfo)(b.RemoteAddress is null or "::"
|
||||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b));
|
||||||
},
|
})];
|
||||||
};
|
|
||||||
|
|
||||||
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));
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
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 class FlashiiIPAddressBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.IPAddress, rawBanInfo), IPAddressBanInfo {
|
||||||
public override string ToString() => Address.ToString();
|
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,29 +1,29 @@
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii;
|
||||||
public class FlashiiRawBanInfo {
|
|
||||||
[JsonPropertyName("is_ban")]
|
|
||||||
public bool IsBanned { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("user_id")]
|
public class FlashiiRawBanInfo {
|
||||||
public string? UserId { get; set; }
|
[JsonPropertyName("is_ban")]
|
||||||
|
public bool IsBanned { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("user_name")]
|
[JsonPropertyName("user_id")]
|
||||||
public string? UserName { get; set; }
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("user_colour")]
|
[JsonPropertyName("user_name")]
|
||||||
public int UserColourRaw { get; set; }
|
public string? UserName { get; set; }
|
||||||
public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
|
|
||||||
|
|
||||||
[JsonPropertyName("ip_addr")]
|
[JsonPropertyName("user_colour")]
|
||||||
public string? RemoteAddress { get; set; }
|
public int UserColourRaw { get; set; }
|
||||||
|
public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
|
||||||
|
|
||||||
[JsonPropertyName("is_perma")]
|
[JsonPropertyName("ip_addr")]
|
||||||
public bool IsPermanent { get; set; }
|
public string? RemoteAddress { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("expires")]
|
[JsonPropertyName("is_perma")]
|
||||||
public DateTimeOffset ExpiresAt { get; set; }
|
public bool IsPermanent { get; set; }
|
||||||
|
|
||||||
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
[JsonPropertyName("expires")]
|
||||||
}
|
public DateTimeOffset ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
|
|
||||||
namespace SharpChat.Flashii {
|
namespace SharpChat.Flashii;
|
||||||
public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo {
|
|
||||||
public string UserId { get; } = rawBanInfo.UserId ?? string.Empty;
|
public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo {
|
||||||
public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})";
|
public string UserId { get; } = rawBanInfo.UserId ?? string.Empty;
|
||||||
public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw);
|
public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})";
|
||||||
public override string ToString() => UserName;
|
public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw);
|
||||||
}
|
public override string ToString() => UserName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
namespace SharpChat.SockChat {
|
namespace SharpChat.SockChat;
|
||||||
public interface S2CPacket {
|
|
||||||
string Pack();
|
public interface S2CPacket {
|
||||||
}
|
string Pack();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class AuthFailS2CPacket(
|
|
||||||
AuthFailS2CPacket.Reason reason,
|
public class AuthFailS2CPacket(
|
||||||
DateTimeOffset? expiresAt = null
|
AuthFailS2CPacket.Reason reason,
|
||||||
) : S2CPacket {
|
DateTimeOffset? expiresAt = null
|
||||||
public enum Reason {
|
) : S2CPacket {
|
||||||
AuthInvalid,
|
public enum Reason {
|
||||||
MaxSessions,
|
AuthInvalid,
|
||||||
Banned,
|
MaxSessions,
|
||||||
Exception,
|
Banned,
|
||||||
|
Exception,
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
|
sb.Append("1\tn\t");
|
||||||
|
|
||||||
|
switch(reason) {
|
||||||
|
case Reason.AuthInvalid:
|
||||||
|
default:
|
||||||
|
sb.Append("authfail");
|
||||||
|
break;
|
||||||
|
case Reason.Exception:
|
||||||
|
sb.Append("userfail");
|
||||||
|
break;
|
||||||
|
case Reason.MaxSessions:
|
||||||
|
sb.Append("sockfail");
|
||||||
|
break;
|
||||||
|
case Reason.Banned:
|
||||||
|
sb.Append("joinfail\t");
|
||||||
|
if(expiresAt is null || expiresAt == DateTimeOffset.MaxValue)
|
||||||
|
sb.Append("-1");
|
||||||
|
else
|
||||||
|
sb.Append(expiresAt.Value.ToUnixTimeSeconds());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Pack() {
|
return sb.ToString();
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("1\tn\t");
|
|
||||||
|
|
||||||
switch(reason) {
|
|
||||||
case Reason.AuthInvalid:
|
|
||||||
default:
|
|
||||||
sb.Append("authfail");
|
|
||||||
break;
|
|
||||||
case Reason.Exception:
|
|
||||||
sb.Append("userfail");
|
|
||||||
break;
|
|
||||||
case Reason.MaxSessions:
|
|
||||||
sb.Append("sockfail");
|
|
||||||
break;
|
|
||||||
case Reason.Banned:
|
|
||||||
sb.Append("joinfail\t");
|
|
||||||
if(expiresAt is null || expiresAt == DateTimeOffset.MaxValue)
|
|
||||||
sb.Append("-1");
|
|
||||||
else
|
|
||||||
sb.Append(expiresAt.Value.ToUnixTimeSeconds());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class AuthSuccessS2CPacket(
|
|
||||||
string userId,
|
|
||||||
string userName,
|
|
||||||
ColourInheritable userColour,
|
|
||||||
int userRank,
|
|
||||||
UserPermissions userPerms,
|
|
||||||
string channelName,
|
|
||||||
int maxMsgLength
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("1\ty\t");
|
public class AuthSuccessS2CPacket(
|
||||||
sb.Append(userId);
|
string userId,
|
||||||
sb.Append('\t');
|
string userName,
|
||||||
sb.Append(userName);
|
ColourInheritable userColour,
|
||||||
sb.Append('\t');
|
int userRank,
|
||||||
sb.Append(userColour);
|
UserPermissions userPerms,
|
||||||
sb.Append('\t');
|
string channelName,
|
||||||
sb.Append(userRank);
|
int maxMsgLength
|
||||||
sb.Append(' ');
|
) : S2CPacket {
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
public string Pack() {
|
||||||
sb.Append(' ');
|
StringBuilder sb = new();
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(channelName);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(maxMsgLength);
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("1\ty\t");
|
||||||
}
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userColour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userRank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(channelName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(maxMsgLength);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class BanListS2CPacket(
|
|
||||||
long msgId,
|
|
||||||
IEnumerable<BanListS2CPacket.Entry> entries
|
|
||||||
) : S2CPacket {
|
|
||||||
public record Entry(BanKind type, string value);
|
|
||||||
|
|
||||||
public string Pack() {
|
public class BanListS2CPacket(
|
||||||
StringBuilder sb = new();
|
long msgId,
|
||||||
|
IEnumerable<BanListS2CPacket.Entry> entries
|
||||||
|
) : S2CPacket {
|
||||||
|
public record Entry(BanKind type, string value);
|
||||||
|
|
||||||
sb.Append("2\t");
|
public string Pack() {
|
||||||
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
StringBuilder sb = new();
|
||||||
sb.Append("\t-1\t0\fbanlist\f");
|
|
||||||
sb.Append(string.Join(", ", entries.Select(entry => string.Format(
|
|
||||||
@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>",
|
|
||||||
entry.type == BanKind.IPAddress ? "/unbanip" : "/unban",
|
|
||||||
entry.value
|
|
||||||
))));
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(msgId);
|
|
||||||
sb.Append("\t10010");
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("2\t");
|
||||||
}
|
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||||
|
sb.Append("\t-1\t0\fbanlist\f");
|
||||||
|
sb.Append(string.Join(", ", entries.Select(entry => string.Format(
|
||||||
|
@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>",
|
||||||
|
entry.type == BanKind.IPAddress ? "/unbanip" : "/unban",
|
||||||
|
entry.value
|
||||||
|
))));
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(msgId);
|
||||||
|
sb.Append("\t10010");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ChannelCreateS2CPacket(
|
|
||||||
string name,
|
|
||||||
bool hasPassword,
|
|
||||||
bool isTemporary
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("4\t0\t");
|
public class ChannelCreateS2CPacket(
|
||||||
sb.Append(name);
|
string name,
|
||||||
sb.Append('\t');
|
bool hasPassword,
|
||||||
sb.Append(hasPassword ? '1' : '0');
|
bool isTemporary
|
||||||
sb.Append('\t');
|
) : S2CPacket {
|
||||||
sb.Append(isTemporary ? '1' : '0');
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("4\t0\t");
|
||||||
}
|
sb.Append(name);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(hasPassword ? '1' : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(isTemporary ? '1' : '0');
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ChannelDeleteS2CPacket(
|
|
||||||
string channelName
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("4\t2\t");
|
public class ChannelDeleteS2CPacket(
|
||||||
sb.Append(channelName);
|
string channelName
|
||||||
|
) : S2CPacket {
|
||||||
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("4\t2\t");
|
||||||
}
|
sb.Append(channelName);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ChannelUpdateS2CPacket(
|
|
||||||
string previousName,
|
|
||||||
string newName,
|
|
||||||
bool hasPassword,
|
|
||||||
bool isTemporary
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("4\t1\t");
|
public class ChannelUpdateS2CPacket(
|
||||||
sb.Append(previousName);
|
string previousName,
|
||||||
sb.Append('\t');
|
string newName,
|
||||||
sb.Append(newName);
|
bool hasPassword,
|
||||||
sb.Append('\t');
|
bool isTemporary
|
||||||
sb.Append(hasPassword ? '1' : '0');
|
) : S2CPacket {
|
||||||
sb.Append('\t');
|
public string Pack() {
|
||||||
sb.Append(isTemporary ? '1' : '0');
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("4\t1\t");
|
||||||
}
|
sb.Append(previousName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(newName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(hasPassword ? '1' : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(isTemporary ? '1' : '0');
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,48 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ChatMessageAddS2CPacket(
|
|
||||||
long msgId,
|
|
||||||
DateTimeOffset created,
|
|
||||||
string userId,
|
|
||||||
string text,
|
|
||||||
bool isAction,
|
|
||||||
bool isPrivate
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("2\t");
|
public class ChatMessageAddS2CPacket(
|
||||||
|
long msgId,
|
||||||
|
DateTimeOffset created,
|
||||||
|
string userId,
|
||||||
|
string text,
|
||||||
|
bool isAction,
|
||||||
|
bool isPrivate
|
||||||
|
) : S2CPacket {
|
||||||
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
sb.Append(created.ToUnixTimeSeconds());
|
sb.Append("2\t");
|
||||||
sb.Append('\t');
|
|
||||||
|
|
||||||
sb.Append(userId);
|
sb.Append(created.ToUnixTimeSeconds());
|
||||||
sb.Append('\t');
|
sb.Append('\t');
|
||||||
|
|
||||||
if(isAction)
|
sb.Append(userId);
|
||||||
sb.Append("<i>");
|
sb.Append('\t');
|
||||||
|
|
||||||
sb.Append(
|
if(isAction)
|
||||||
text.Replace("<", "<")
|
sb.Append("<i>");
|
||||||
.Replace(">", ">")
|
|
||||||
.Replace("\n", " <br/> ")
|
|
||||||
.Replace("\t", " ")
|
|
||||||
);
|
|
||||||
|
|
||||||
if(isAction)
|
sb.Append(
|
||||||
sb.Append("</i>");
|
text.Replace("<", "<")
|
||||||
|
.Replace(">", ">")
|
||||||
|
.Replace("\n", " <br/> ")
|
||||||
|
.Replace("\t", " ")
|
||||||
|
);
|
||||||
|
|
||||||
sb.Append('\t');
|
if(isAction)
|
||||||
sb.Append(msgId);
|
sb.Append("</i>");
|
||||||
sb.AppendFormat(
|
|
||||||
"\t1{0}0{1}{2}",
|
|
||||||
isAction ? '1' : '0',
|
|
||||||
isAction ? '0' : '1',
|
|
||||||
isPrivate ? '1' : '0'
|
|
||||||
);
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append('\t');
|
||||||
}
|
sb.Append(msgId);
|
||||||
|
sb.AppendFormat(
|
||||||
|
"\t1{0}0{1}{2}",
|
||||||
|
isAction ? '1' : '0',
|
||||||
|
isAction ? '0' : '1',
|
||||||
|
isPrivate ? '1' : '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ChatMessageDeleteS2CPacket(long eventId) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("6\t");
|
public class ChatMessageDeleteS2CPacket(long eventId) : S2CPacket {
|
||||||
sb.Append(eventId);
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("6\t");
|
||||||
}
|
sb.Append(eventId);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +1,85 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class CommandResponseS2CPacket(
|
|
||||||
long msgId,
|
|
||||||
string stringId,
|
|
||||||
bool isError = true,
|
|
||||||
params object[] args
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
if(stringId == LCR.WELCOME) {
|
public class CommandResponseS2CPacket(
|
||||||
sb.Append("7\t1\t");
|
long msgId,
|
||||||
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
string stringId,
|
||||||
sb.Append("\t-1\tChatBot\tinherit\t\t");
|
bool isError = true,
|
||||||
} else {
|
params object[] args
|
||||||
sb.Append("2\t");
|
) : S2CPacket {
|
||||||
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
public string Pack() {
|
||||||
sb.Append("\t-1\t");
|
StringBuilder sb = new();
|
||||||
|
|
||||||
|
if(stringId == LCR.WELCOME) {
|
||||||
|
sb.Append("7\t1\t");
|
||||||
|
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||||
|
sb.Append("\t-1\tChatBot\tinherit\t\t");
|
||||||
|
} else {
|
||||||
|
sb.Append("2\t");
|
||||||
|
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||||
|
sb.Append("\t-1\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(isError ? '1' : '0');
|
||||||
|
sb.Append('\f');
|
||||||
|
sb.Append(stringId == LCR.WELCOME ? LCR.BROADCAST : stringId);
|
||||||
|
|
||||||
|
if(args.Length > 0)
|
||||||
|
foreach(object arg in args) {
|
||||||
|
sb.Append('\f');
|
||||||
|
sb.Append(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Append(isError ? '1' : '0');
|
sb.Append('\t');
|
||||||
sb.Append('\f');
|
|
||||||
sb.Append(stringId == LCR.WELCOME ? LCR.BROADCAST : stringId);
|
|
||||||
|
|
||||||
if(args.Length > 0)
|
if(stringId == LCR.WELCOME) {
|
||||||
foreach(object arg in args) {
|
sb.Append(stringId);
|
||||||
sb.Append('\f');
|
sb.Append("\t0");
|
||||||
sb.Append(arg);
|
} else
|
||||||
}
|
sb.Append(msgId);
|
||||||
|
|
||||||
sb.Append('\t');
|
sb.Append("\t10010");
|
||||||
|
/*sb.AppendFormat(
|
||||||
|
"\t1{0}0{1}{2}",
|
||||||
|
Flags.HasFlag(ChatMessageFlags.Action) ? '1' : '0',
|
||||||
|
Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1',
|
||||||
|
Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0'
|
||||||
|
);*/
|
||||||
|
|
||||||
if(stringId == LCR.WELCOME) {
|
return sb.ToString();
|
||||||
sb.Append(stringId);
|
|
||||||
sb.Append("\t0");
|
|
||||||
} else
|
|
||||||
sb.Append(msgId);
|
|
||||||
|
|
||||||
sb.Append("\t10010");
|
|
||||||
/*sb.AppendFormat(
|
|
||||||
"\t1{0}0{1}{2}",
|
|
||||||
Flags.HasFlag(ChatMessageFlags.Action) ? '1' : '0',
|
|
||||||
Flags.HasFlag(ChatMessageFlags.Action) ? '0' : '1',
|
|
||||||
Flags.HasFlag(ChatMessageFlags.Private) ? '1' : '0'
|
|
||||||
);*/
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abbreviated class name because otherwise shit gets wide
|
|
||||||
public static class LCR {
|
|
||||||
public const string GENERIC_ERROR = "generr";
|
|
||||||
public const string COMMAND_NOT_FOUND = "nocmd";
|
|
||||||
public const string COMMAND_NOT_ALLOWED = "cmdna";
|
|
||||||
public const string COMMAND_FORMAT_ERROR = "cmderr";
|
|
||||||
public const string WELCOME = "welcome";
|
|
||||||
public const string BROADCAST = "say";
|
|
||||||
public const string IP_ADDRESS = "ipaddr";
|
|
||||||
public const string USER_NOT_FOUND = "usernf";
|
|
||||||
public const string NAME_IN_USE = "nameinuse";
|
|
||||||
public const string CHANNEL_INSUFFICIENT_HIERARCHY = "ipchan";
|
|
||||||
public const string CHANNEL_INVALID_PASSWORD = "ipwchan";
|
|
||||||
public const string CHANNEL_NOT_FOUND = "nochan";
|
|
||||||
public const string CHANNEL_ALREADY_EXISTS = "nischan";
|
|
||||||
public const string CHANNEL_NAME_INVALID = "inchan";
|
|
||||||
public const string CHANNEL_CREATED = "crchan";
|
|
||||||
public const string CHANNEL_DELETE_FAILED = "ndchan";
|
|
||||||
public const string CHANNEL_DELETED = "delchan";
|
|
||||||
public const string CHANNEL_PASSWORD_CHANGED = "cpwdchan";
|
|
||||||
public const string CHANNEL_HIERARCHY_CHANGED = "cprivchan";
|
|
||||||
public const string USERS_LISTING_ERROR = "whoerr";
|
|
||||||
public const string USERS_LISTING_CHANNEL = "whochan";
|
|
||||||
public const string USERS_LISTING_SERVER = "who";
|
|
||||||
public const string INSUFFICIENT_HIERARCHY = "rankerr";
|
|
||||||
public const string MESSAGE_DELETE_ERROR = "delerr";
|
|
||||||
public const string KICK_NOT_ALLOWED = "kickna";
|
|
||||||
public const string USER_NOT_BANNED = "notban";
|
|
||||||
public const string USER_UNBANNED = "unban";
|
|
||||||
public const string FLOOD_WARN = "flwarn";
|
|
||||||
public const string NICKNAME_CHANGE = "nick";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abbreviated class name because otherwise shit gets wide
|
||||||
|
public static class LCR {
|
||||||
|
public const string GENERIC_ERROR = "generr";
|
||||||
|
public const string COMMAND_NOT_FOUND = "nocmd";
|
||||||
|
public const string COMMAND_NOT_ALLOWED = "cmdna";
|
||||||
|
public const string COMMAND_FORMAT_ERROR = "cmderr";
|
||||||
|
public const string WELCOME = "welcome";
|
||||||
|
public const string BROADCAST = "say";
|
||||||
|
public const string IP_ADDRESS = "ipaddr";
|
||||||
|
public const string USER_NOT_FOUND = "usernf";
|
||||||
|
public const string NAME_IN_USE = "nameinuse";
|
||||||
|
public const string CHANNEL_INSUFFICIENT_HIERARCHY = "ipchan";
|
||||||
|
public const string CHANNEL_INVALID_PASSWORD = "ipwchan";
|
||||||
|
public const string CHANNEL_NOT_FOUND = "nochan";
|
||||||
|
public const string CHANNEL_ALREADY_EXISTS = "nischan";
|
||||||
|
public const string CHANNEL_NAME_INVALID = "inchan";
|
||||||
|
public const string CHANNEL_CREATED = "crchan";
|
||||||
|
public const string CHANNEL_DELETE_FAILED = "ndchan";
|
||||||
|
public const string CHANNEL_DELETED = "delchan";
|
||||||
|
public const string CHANNEL_PASSWORD_CHANGED = "cpwdchan";
|
||||||
|
public const string CHANNEL_HIERARCHY_CHANGED = "cprivchan";
|
||||||
|
public const string USERS_LISTING_ERROR = "whoerr";
|
||||||
|
public const string USERS_LISTING_CHANNEL = "whochan";
|
||||||
|
public const string USERS_LISTING_SERVER = "who";
|
||||||
|
public const string INSUFFICIENT_HIERARCHY = "rankerr";
|
||||||
|
public const string MESSAGE_DELETE_ERROR = "delerr";
|
||||||
|
public const string KICK_NOT_ALLOWED = "kickna";
|
||||||
|
public const string USER_NOT_BANNED = "notban";
|
||||||
|
public const string USER_UNBANNED = "unban";
|
||||||
|
public const string FLOOD_WARN = "flwarn";
|
||||||
|
public const string NICKNAME_CHANGE = "nick";
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ContextChannelsS2CPacket(IEnumerable<ContextChannelsS2CPacket.Entry> entries) : S2CPacket {
|
|
||||||
public record Entry(string name, bool hasPassword, bool isTemporary);
|
|
||||||
|
|
||||||
public string Pack() {
|
public class ContextChannelsS2CPacket(IEnumerable<ContextChannelsS2CPacket.Entry> entries) : S2CPacket {
|
||||||
StringBuilder sb = new();
|
public record Entry(string name, bool hasPassword, bool isTemporary);
|
||||||
|
|
||||||
sb.Append("7\t2\t");
|
public string Pack() {
|
||||||
sb.Append(entries.Count());
|
StringBuilder sb = new();
|
||||||
|
|
||||||
foreach(Entry entry in entries) {
|
sb.Append("7\t2\t");
|
||||||
sb.Append('\t');
|
sb.Append(entries.Count());
|
||||||
sb.Append(entry.name);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.hasPassword ? '1' : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.isTemporary ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
foreach(Entry entry in entries) {
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.name);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.hasPassword ? '1' : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.isTemporary ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ContextClearS2CPacket(ContextClearS2CPacket.Mode mode) : S2CPacket {
|
|
||||||
public enum Mode {
|
|
||||||
Messages = 0,
|
|
||||||
Users = 1,
|
|
||||||
Channels = 2,
|
|
||||||
MessagesUsers = 3,
|
|
||||||
MessagesUsersChannels = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Pack() {
|
public class ContextClearS2CPacket(ContextClearS2CPacket.Mode mode) : S2CPacket {
|
||||||
StringBuilder sb = new();
|
public enum Mode {
|
||||||
|
Messages = 0,
|
||||||
|
Users = 1,
|
||||||
|
Channels = 2,
|
||||||
|
MessagesUsers = 3,
|
||||||
|
MessagesUsersChannels = 4,
|
||||||
|
}
|
||||||
|
|
||||||
sb.Append("8\t");
|
public string Pack() {
|
||||||
sb.Append((int)mode);
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("8\t");
|
||||||
}
|
sb.Append((int)mode);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket {
|
|
||||||
public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
|
|
||||||
|
|
||||||
public string Pack() {
|
public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket {
|
||||||
StringBuilder sb = new();
|
public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
|
||||||
|
|
||||||
sb.Append("7\t0\t");
|
public string Pack() {
|
||||||
sb.Append(entries.Count());
|
StringBuilder sb = new();
|
||||||
|
|
||||||
foreach(Entry entry in entries) {
|
sb.Append("7\t0\t");
|
||||||
sb.Append('\t');
|
sb.Append(entries.Count());
|
||||||
sb.Append(entry.id);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.name);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.colour);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.rank);
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(entry.perms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(entry.perms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(entry.perms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(entry.perms.HasFlag(UserPermissions.CreateChannel) ? (entry.perms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(entry.visible ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
foreach(Entry entry in entries) {
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.id);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.name);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.colour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.rank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(entry.perms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(entry.perms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(entry.perms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(entry.perms.HasFlag(UserPermissions.CreateChannel) ? (entry.perms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(entry.visible ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ForceDisconnectS2CPacket(DateTimeOffset? expires = null) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("9\t");
|
public class ForceDisconnectS2CPacket(DateTimeOffset? expires = null) : S2CPacket {
|
||||||
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
if(expires.HasValue && expires.Value > DateTimeOffset.UtcNow) {
|
sb.Append("9\t");
|
||||||
sb.Append("1\t");
|
|
||||||
if(expires.Value < DateTimeOffset.MaxValue)
|
if(expires.HasValue && expires.Value > DateTimeOffset.UtcNow) {
|
||||||
sb.Append(expires.Value.ToUnixTimeSeconds());
|
sb.Append("1\t");
|
||||||
else
|
if(expires.Value < DateTimeOffset.MaxValue)
|
||||||
sb.Append("-1");
|
sb.Append(expires.Value.ToUnixTimeSeconds());
|
||||||
} else
|
else
|
||||||
sb.Append('0');
|
sb.Append("-1");
|
||||||
|
} else
|
||||||
return sb.ToString();
|
sb.Append('0');
|
||||||
}
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class PongS2CPacket : S2CPacket {
|
|
||||||
public string Pack() {
|
public class PongS2CPacket : S2CPacket {
|
||||||
return "0\tpong";
|
public string Pack() {
|
||||||
}
|
return "0\tpong";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserChannelForceJoinS2CPacket(string channelName) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("5\t2\t");
|
public class UserChannelForceJoinS2CPacket(string channelName) : S2CPacket {
|
||||||
sb.Append(channelName);
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("5\t2\t");
|
||||||
}
|
sb.Append(channelName);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserChannelJoinS2CPacket(
|
|
||||||
long msgId,
|
|
||||||
string userId,
|
|
||||||
string userName,
|
|
||||||
ColourInheritable userColour,
|
|
||||||
int userRank,
|
|
||||||
UserPermissions userPerms
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("5\t0\t");
|
public class UserChannelJoinS2CPacket(
|
||||||
sb.Append(userId);
|
long msgId,
|
||||||
sb.Append('\t');
|
string userId,
|
||||||
sb.Append(userName);
|
string userName,
|
||||||
sb.Append('\t');
|
ColourInheritable userColour,
|
||||||
sb.Append(userColour);
|
int userRank,
|
||||||
sb.Append('\t');
|
UserPermissions userPerms
|
||||||
sb.Append(userRank);
|
) : S2CPacket {
|
||||||
sb.Append(' ');
|
public string Pack() {
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
StringBuilder sb = new();
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(msgId);
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("5\t0\t");
|
||||||
}
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userColour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userRank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(msgId);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("5\t1\t");
|
public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket {
|
||||||
sb.Append(userId);
|
public string Pack() {
|
||||||
sb.Append('\t');
|
StringBuilder sb = new();
|
||||||
sb.Append(msgId);
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("5\t1\t");
|
||||||
}
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(msgId);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserConnectS2CPacket(
|
|
||||||
long msgId,
|
|
||||||
DateTimeOffset joined,
|
|
||||||
string userId,
|
|
||||||
string userName,
|
|
||||||
ColourInheritable userColour,
|
|
||||||
int userRank,
|
|
||||||
UserPermissions userPerms
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("1\t");
|
public class UserConnectS2CPacket(
|
||||||
sb.Append(joined.ToUnixTimeSeconds());
|
long msgId,
|
||||||
sb.Append('\t');
|
DateTimeOffset joined,
|
||||||
sb.Append(userId);
|
string userId,
|
||||||
sb.Append('\t');
|
string userName,
|
||||||
sb.Append(userName);
|
ColourInheritable userColour,
|
||||||
sb.Append('\t');
|
int userRank,
|
||||||
sb.Append(userColour);
|
UserPermissions userPerms
|
||||||
sb.Append('\t');
|
) : S2CPacket {
|
||||||
sb.Append(userRank);
|
public string Pack() {
|
||||||
sb.Append(' ');
|
StringBuilder sb = new();
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(msgId);
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("1\t");
|
||||||
}
|
sb.Append(joined.ToUnixTimeSeconds());
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userColour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userRank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(msgId);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +1,51 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserDisconnectS2CPacket(
|
|
||||||
long msgId,
|
public class UserDisconnectS2CPacket(
|
||||||
DateTimeOffset disconnected,
|
long msgId,
|
||||||
string userId,
|
DateTimeOffset disconnected,
|
||||||
string userName,
|
string userId,
|
||||||
UserDisconnectS2CPacket.Reason reason
|
string userName,
|
||||||
) : S2CPacket {
|
UserDisconnectS2CPacket.Reason reason
|
||||||
public enum Reason {
|
) : S2CPacket {
|
||||||
Leave,
|
public enum Reason {
|
||||||
TimeOut,
|
Leave,
|
||||||
Kicked,
|
TimeOut,
|
||||||
Flood,
|
Kicked,
|
||||||
|
Flood,
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Pack() {
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
|
sb.Append("3\t");
|
||||||
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userName);
|
||||||
|
sb.Append('\t');
|
||||||
|
|
||||||
|
switch(reason) {
|
||||||
|
case Reason.Leave:
|
||||||
|
default:
|
||||||
|
sb.Append("leave");
|
||||||
|
break;
|
||||||
|
case Reason.TimeOut:
|
||||||
|
sb.Append("timeout");
|
||||||
|
break;
|
||||||
|
case Reason.Kicked:
|
||||||
|
sb.Append("kick");
|
||||||
|
break;
|
||||||
|
case Reason.Flood:
|
||||||
|
sb.Append("flood");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Pack() {
|
sb.Append('\t');
|
||||||
StringBuilder sb = new();
|
sb.Append(disconnected.ToUnixTimeSeconds());
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(msgId);
|
||||||
|
|
||||||
sb.Append("3\t");
|
return sb.ToString();
|
||||||
sb.Append(userId);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(userName);
|
|
||||||
sb.Append('\t');
|
|
||||||
|
|
||||||
switch(reason) {
|
|
||||||
case Reason.Leave:
|
|
||||||
default:
|
|
||||||
sb.Append("leave");
|
|
||||||
break;
|
|
||||||
case Reason.TimeOut:
|
|
||||||
sb.Append("timeout");
|
|
||||||
break;
|
|
||||||
case Reason.Kicked:
|
|
||||||
sb.Append("kick");
|
|
||||||
break;
|
|
||||||
case Reason.Flood:
|
|
||||||
sb.Append("flood");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(disconnected.ToUnixTimeSeconds());
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(msgId);
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class UserUpdateS2CPacket(
|
|
||||||
string userId,
|
|
||||||
string userName,
|
|
||||||
ColourInheritable userColour,
|
|
||||||
int userRank,
|
|
||||||
UserPermissions userPerms
|
|
||||||
) : S2CPacket {
|
|
||||||
public string Pack() {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
sb.Append("10\t");
|
public class UserUpdateS2CPacket(
|
||||||
sb.Append(userId);
|
string userId,
|
||||||
sb.Append('\t');
|
string userName,
|
||||||
sb.Append(userName);
|
ColourInheritable userColour,
|
||||||
sb.Append('\t');
|
int userRank,
|
||||||
sb.Append(userColour);
|
UserPermissions userPerms
|
||||||
sb.Append('\t');
|
) : S2CPacket {
|
||||||
sb.Append(userRank);
|
public string Pack() {
|
||||||
sb.Append(' ');
|
StringBuilder sb = new();
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
|
|
||||||
return sb.ToString();
|
sb.Append("10\t");
|
||||||
}
|
sb.Append(userId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userName);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userColour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(userRank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(userPerms.HasFlag(UserPermissions.CreateChannel) ? (userPerms.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public interface C2SPacketHandler {
|
|
||||||
bool IsMatch(C2SPacketHandlerContext ctx);
|
public interface C2SPacketHandler {
|
||||||
Task Handle(C2SPacketHandlerContext ctx);
|
bool IsMatch(C2SPacketHandlerContext ctx);
|
||||||
}
|
Task Handle(C2SPacketHandlerContext ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class C2SPacketHandlerContext(
|
|
||||||
string text,
|
|
||||||
Context chat,
|
|
||||||
Connection connection
|
|
||||||
) {
|
|
||||||
public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text));
|
|
||||||
public Context Chat { get; } = chat ?? throw new ArgumentNullException(nameof(chat));
|
|
||||||
public Connection Connection { get; } = connection ?? throw new ArgumentNullException(nameof(connection));
|
|
||||||
|
|
||||||
public bool CheckPacketId(string packetId) {
|
public class C2SPacketHandlerContext(
|
||||||
return Text == packetId || Text.StartsWith(packetId + '\t');
|
string text,
|
||||||
}
|
Context chat,
|
||||||
|
Connection connection
|
||||||
|
) {
|
||||||
|
public string Text { get; } = text ?? throw new ArgumentNullException(nameof(text));
|
||||||
|
public Context Chat { get; } = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||||
|
public Connection Connection { get; } = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
|
||||||
public string[] SplitText(int expect) {
|
public bool CheckPacketId(string packetId) {
|
||||||
return Text.Split('\t', expect + 1);
|
return Text == packetId || Text.StartsWith(packetId + '\t');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] SplitText(int expect) {
|
||||||
|
return Text.Split('\t', expect + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,110 +3,110 @@ using SharpChat.Bans;
|
||||||
using SharpChat.Configuration;
|
using SharpChat.Configuration;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.C2SPacketHandlers {
|
namespace SharpChat.C2SPacketHandlers;
|
||||||
public class AuthC2SPacketHandler(
|
|
||||||
AuthClient authClient,
|
|
||||||
BansClient bansClient,
|
|
||||||
Channel defaultChannel,
|
|
||||||
CachedValue<int> maxMsgLength,
|
|
||||||
CachedValue<int> maxConns
|
|
||||||
) : C2SPacketHandler {
|
|
||||||
private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
|
|
||||||
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
|
|
||||||
private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns));
|
|
||||||
|
|
||||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
public class AuthC2SPacketHandler(
|
||||||
return ctx.CheckPacketId("1");
|
AuthClient authClient,
|
||||||
|
BansClient bansClient,
|
||||||
|
Channel defaultChannel,
|
||||||
|
CachedValue<int> maxMsgLength,
|
||||||
|
CachedValue<int> maxConns
|
||||||
|
) : C2SPacketHandler {
|
||||||
|
private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
|
||||||
|
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
|
||||||
|
private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns));
|
||||||
|
|
||||||
|
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||||
|
return ctx.CheckPacketId("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(C2SPacketHandlerContext ctx) {
|
||||||
|
string[] args = ctx.SplitText(3);
|
||||||
|
|
||||||
|
string? authMethod = args.ElementAtOrDefault(1);
|
||||||
|
string? authToken = args.ElementAtOrDefault(2);
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) {
|
||||||
|
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
|
||||||
|
ctx.Connection.Dispose();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(C2SPacketHandlerContext ctx) {
|
if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) {
|
||||||
string[] args = ctx.SplitText(3);
|
string[] tokenParts = authToken.Split(':', 2);
|
||||||
|
authMethod = tokenParts[0];
|
||||||
|
authToken = tokenParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
string? authMethod = args.ElementAtOrDefault(1);
|
try {
|
||||||
string? authToken = args.ElementAtOrDefault(2);
|
AuthResult authResult = await authClient.AuthVerifyAsync(
|
||||||
|
ctx.Connection.RemoteAddress,
|
||||||
|
authMethod,
|
||||||
|
authToken
|
||||||
|
);
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) {
|
BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress);
|
||||||
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
|
if(banInfo is not null) {
|
||||||
|
Logger.Write($"<{ctx.Connection.Id}> User is banned.");
|
||||||
|
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt));
|
||||||
ctx.Connection.Dispose();
|
ctx.Connection.Dispose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) {
|
await ctx.Chat.ContextAccess.WaitAsync();
|
||||||
string[] tokenParts = authToken.Split(':', 2);
|
|
||||||
authMethod = tokenParts[0];
|
|
||||||
authToken = tokenParts[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
AuthResult authResult = await authClient.AuthVerifyAsync(
|
User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId);
|
||||||
ctx.Connection.RemoteAddress,
|
|
||||||
authMethod,
|
|
||||||
authToken
|
|
||||||
);
|
|
||||||
|
|
||||||
BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress);
|
if(user == null)
|
||||||
if(banInfo is not null) {
|
user = new User(
|
||||||
Logger.Write($"<{ctx.Connection.Id}> User is banned.");
|
authResult.UserId,
|
||||||
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt));
|
authResult.UserName ?? $"({authResult.UserId})",
|
||||||
|
authResult.UserColour,
|
||||||
|
authResult.UserRank,
|
||||||
|
authResult.UserPermissions
|
||||||
|
);
|
||||||
|
else
|
||||||
|
await ctx.Chat.UpdateUser(
|
||||||
|
user,
|
||||||
|
userName: authResult.UserName ?? $"({authResult.UserId})",
|
||||||
|
colour: authResult.UserColour,
|
||||||
|
rank: authResult.UserRank,
|
||||||
|
perms: authResult.UserPermissions
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enforce a maximum amount of connections per user
|
||||||
|
if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
|
||||||
|
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
|
||||||
ctx.Connection.Dispose();
|
ctx.Connection.Dispose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Chat.ContextAccess.WaitAsync();
|
ctx.Connection.BumpPing();
|
||||||
try {
|
ctx.Connection.User = user;
|
||||||
User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId);
|
await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!"));
|
||||||
|
|
||||||
if(user == null)
|
if(File.Exists("welcome.txt")) {
|
||||||
user = new User(
|
IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x));
|
||||||
authResult.UserId,
|
string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
|
||||||
authResult.UserName ?? $"({authResult.UserId})",
|
|
||||||
authResult.UserColour,
|
|
||||||
authResult.UserRank,
|
|
||||||
authResult.UserPermissions
|
|
||||||
);
|
|
||||||
else
|
|
||||||
await ctx.Chat.UpdateUser(
|
|
||||||
user,
|
|
||||||
userName: authResult.UserName ?? $"({authResult.UserId})",
|
|
||||||
colour: authResult.UserColour,
|
|
||||||
rank: authResult.UserRank,
|
|
||||||
perms: authResult.UserPermissions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enforce a maximum amount of connections per user
|
if(!string.IsNullOrWhiteSpace(line))
|
||||||
if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
|
await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
|
||||||
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
|
|
||||||
ctx.Connection.Dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Connection.BumpPing();
|
|
||||||
ctx.Connection.User = user;
|
|
||||||
await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!"));
|
|
||||||
|
|
||||||
if(File.Exists("welcome.txt")) {
|
|
||||||
IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x));
|
|
||||||
string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(line))
|
|
||||||
await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength);
|
|
||||||
} finally {
|
|
||||||
ctx.Chat.ContextAccess.Release();
|
|
||||||
}
|
}
|
||||||
} catch(AuthFailedException ex) {
|
|
||||||
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
|
await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength);
|
||||||
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
|
} finally {
|
||||||
ctx.Connection.Dispose();
|
ctx.Chat.ContextAccess.Release();
|
||||||
throw;
|
|
||||||
} catch(Exception ex) {
|
|
||||||
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
|
|
||||||
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception));
|
|
||||||
ctx.Connection.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
|
} catch(AuthFailedException ex) {
|
||||||
|
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
|
||||||
|
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
|
||||||
|
ctx.Connection.Dispose();
|
||||||
|
throw;
|
||||||
|
} catch(Exception ex) {
|
||||||
|
Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
|
||||||
|
await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception));
|
||||||
|
ctx.Connection.Dispose();
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,39 +2,39 @@ using SharpChat.Auth;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.C2SPacketHandlers {
|
namespace SharpChat.C2SPacketHandlers;
|
||||||
public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler {
|
|
||||||
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
|
|
||||||
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
|
|
||||||
|
|
||||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler {
|
||||||
return ctx.CheckPacketId("0");
|
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
|
||||||
}
|
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
|
||||||
|
|
||||||
public async Task Handle(C2SPacketHandlerContext ctx) {
|
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||||
string[] parts = ctx.SplitText(2);
|
return ctx.CheckPacketId("0");
|
||||||
|
}
|
||||||
|
|
||||||
if(!int.TryParse(parts.FirstOrDefault(), out int pTime))
|
public async Task Handle(C2SPacketHandlerContext ctx) {
|
||||||
return;
|
string[] parts = ctx.SplitText(2);
|
||||||
|
|
||||||
ctx.Connection.BumpPing();
|
if(!int.TryParse(parts.FirstOrDefault(), out int pTime))
|
||||||
await ctx.Connection.Send(new PongS2CPacket());
|
return;
|
||||||
|
|
||||||
ctx.Chat.ContextAccess.Wait();
|
ctx.Connection.BumpPing();
|
||||||
try {
|
await ctx.Connection.Send(new PongS2CPacket());
|
||||||
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
|
|
||||||
(IPAddress, string)[] bumpList = [.. ctx.Chat.Users
|
|
||||||
.Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
|
|
||||||
.Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))];
|
|
||||||
|
|
||||||
if(bumpList.Length > 0)
|
ctx.Chat.ContextAccess.Wait();
|
||||||
await authClient.AuthBumpUsersOnlineAsync(bumpList);
|
try {
|
||||||
|
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
|
||||||
|
(IPAddress, string)[] bumpList = [.. ctx.Chat.Users
|
||||||
|
.Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
|
||||||
|
.Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))];
|
||||||
|
|
||||||
LastBump = DateTimeOffset.UtcNow;
|
if(bumpList.Length > 0)
|
||||||
}
|
await authClient.AuthBumpUsersOnlineAsync(bumpList);
|
||||||
} finally {
|
|
||||||
ctx.Chat.ContextAccess.Release();
|
LastBump = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
ctx.Chat.ContextAccess.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,86 +4,86 @@ using SharpChat.Snowflake;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.C2SPacketHandlers {
|
namespace SharpChat.C2SPacketHandlers;
|
||||||
public class SendMessageC2SPacketHandler(
|
|
||||||
RandomSnowflake randomSnowflake,
|
|
||||||
CachedValue<int> maxMsgLength
|
|
||||||
) : C2SPacketHandler {
|
|
||||||
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
|
|
||||||
|
|
||||||
private List<ClientCommand> Commands { get; } = [];
|
public class SendMessageC2SPacketHandler(
|
||||||
|
RandomSnowflake randomSnowflake,
|
||||||
|
CachedValue<int> maxMsgLength
|
||||||
|
) : C2SPacketHandler {
|
||||||
|
private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
|
||||||
|
|
||||||
public void AddCommand(ClientCommand command) {
|
private List<ClientCommand> Commands { get; } = [];
|
||||||
Commands.Add(command ?? throw new ArgumentNullException(nameof(command)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddCommands(IEnumerable<ClientCommand> commands) {
|
public void AddCommand(ClientCommand command) {
|
||||||
Commands.AddRange(commands ?? throw new ArgumentNullException(nameof(commands)));
|
Commands.Add(command ?? throw new ArgumentNullException(nameof(command)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
public void AddCommands(IEnumerable<ClientCommand> commands) {
|
||||||
return ctx.CheckPacketId("2");
|
Commands.AddRange(commands ?? throw new ArgumentNullException(nameof(commands)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(C2SPacketHandlerContext ctx) {
|
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||||
string[] args = ctx.SplitText(3);
|
return ctx.CheckPacketId("2");
|
||||||
|
}
|
||||||
|
|
||||||
User? user = ctx.Connection.User;
|
public async Task Handle(C2SPacketHandlerContext ctx) {
|
||||||
string? messageText = args.ElementAtOrDefault(2);
|
string[] args = ctx.SplitText(3);
|
||||||
|
|
||||||
if(user == null || !user.Can(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText))
|
User? user = ctx.Connection.User;
|
||||||
|
string? messageText = args.ElementAtOrDefault(2);
|
||||||
|
|
||||||
|
if(user == null || !user.Can(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Extra validation step, not necessary at all but enforces proper formatting in SCv1.
|
||||||
|
if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ctx.Chat.ContextAccess.Wait();
|
||||||
|
try {
|
||||||
|
if(!ctx.Chat.UserLastChannel.TryGetValue(user.UserId, out Channel? channel)
|
||||||
|
&& (channel is null || !ctx.Chat.IsInChannel(user, channel)))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Extra validation step, not necessary at all but enforces proper formatting in SCv1.
|
if(user.Status != UserStatus.Online)
|
||||||
if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString())
|
await ctx.Chat.UpdateUser(user, status: UserStatus.Online);
|
||||||
return;
|
|
||||||
|
|
||||||
ctx.Chat.ContextAccess.Wait();
|
int maxMsgLength = MaxMessageLength;
|
||||||
try {
|
StringInfo messageTextInfo = new(messageText);
|
||||||
if(!ctx.Chat.UserLastChannel.TryGetValue(user.UserId, out Channel? channel)
|
if(Encoding.UTF8.GetByteCount(messageText) > (maxMsgLength * 10)
|
||||||
&& (channel is null || !ctx.Chat.IsInChannel(user, channel)))
|
|| messageTextInfo.LengthInTextElements > maxMsgLength)
|
||||||
return;
|
messageText = messageTextInfo.SubstringByTextElements(0, Math.Min(messageTextInfo.LengthInTextElements, maxMsgLength));
|
||||||
|
|
||||||
if(user.Status != UserStatus.Online)
|
messageText = messageText.Trim();
|
||||||
await ctx.Chat.UpdateUser(user, status: UserStatus.Online);
|
|
||||||
|
|
||||||
int maxMsgLength = MaxMessageLength;
|
|
||||||
StringInfo messageTextInfo = new(messageText);
|
|
||||||
if(Encoding.UTF8.GetByteCount(messageText) > (maxMsgLength * 10)
|
|
||||||
|| messageTextInfo.LengthInTextElements > maxMsgLength)
|
|
||||||
messageText = messageTextInfo.SubstringByTextElements(0, Math.Min(messageTextInfo.LengthInTextElements, maxMsgLength));
|
|
||||||
|
|
||||||
messageText = messageText.Trim();
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}");
|
Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(messageText.StartsWith('/')) {
|
if(messageText.StartsWith('/')) {
|
||||||
ClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel);
|
ClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel);
|
||||||
foreach(ClientCommand cmd in Commands)
|
foreach(ClientCommand cmd in Commands)
|
||||||
if(cmd.IsMatch(context)) {
|
if(cmd.IsMatch(context)) {
|
||||||
await cmd.Dispatch(context);
|
await cmd.Dispatch(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
|
||||||
randomSnowflake.Next(),
|
|
||||||
channel.Name,
|
|
||||||
user.UserId,
|
|
||||||
user.UserName,
|
|
||||||
user.Colour,
|
|
||||||
user.Rank,
|
|
||||||
user.NickName,
|
|
||||||
user.Permissions,
|
|
||||||
DateTimeOffset.Now,
|
|
||||||
messageText,
|
|
||||||
false, false, false
|
|
||||||
));
|
|
||||||
} finally {
|
|
||||||
ctx.Chat.ContextAccess.Release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
||||||
|
randomSnowflake.Next(),
|
||||||
|
channel.Name,
|
||||||
|
user.UserId,
|
||||||
|
user.UserName,
|
||||||
|
user.Colour,
|
||||||
|
user.Rank,
|
||||||
|
user.NickName,
|
||||||
|
user.Permissions,
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
messageText,
|
||||||
|
false, false, false
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
ctx.Chat.ContextAccess.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class Channel(
|
|
||||||
string name,
|
|
||||||
string password = "",
|
|
||||||
bool isTemporary = false,
|
|
||||||
int rank = 0,
|
|
||||||
string ownerId = ""
|
|
||||||
) {
|
|
||||||
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
public string Password { get; set; } = password ?? string.Empty;
|
|
||||||
public bool IsTemporary { get; set; } = isTemporary;
|
|
||||||
public int Rank { get; set; } = rank;
|
|
||||||
public string OwnerId { get; set; } = ownerId;
|
|
||||||
|
|
||||||
public bool HasPassword
|
public class Channel(
|
||||||
=> !string.IsNullOrWhiteSpace(Password);
|
string name,
|
||||||
|
string password = "",
|
||||||
|
bool isTemporary = false,
|
||||||
|
int rank = 0,
|
||||||
|
string ownerId = ""
|
||||||
|
) {
|
||||||
|
public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
public string Password { get; set; } = password ?? string.Empty;
|
||||||
|
public bool IsTemporary { get; set; } = isTemporary;
|
||||||
|
public int Rank { get; set; } = rank;
|
||||||
|
public string OwnerId { get; set; } = ownerId;
|
||||||
|
|
||||||
public bool NameEquals(string name) {
|
public bool HasPassword
|
||||||
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
|
=> !string.IsNullOrWhiteSpace(Password);
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsOwner(User user) {
|
public bool NameEquals(string name) {
|
||||||
return string.IsNullOrEmpty(OwnerId)
|
return string.Equals(name, Name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
&& user != null
|
}
|
||||||
&& OwnerId == user.UserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() {
|
public bool IsOwner(User user) {
|
||||||
return Name.GetHashCode();
|
return string.IsNullOrEmpty(OwnerId)
|
||||||
}
|
&& user != null
|
||||||
|
&& OwnerId == user.UserId;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool CheckName(string name) {
|
public override int GetHashCode() {
|
||||||
return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar);
|
return Name.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CheckNameChar(char c) {
|
public static bool CheckName(string name) {
|
||||||
return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_';
|
return !string.IsNullOrWhiteSpace(name) && name.All(CheckNameChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool CheckNameChar(char c) {
|
||||||
|
return char.IsLetter(c) || char.IsNumber(c) || c == '-' || c == '_';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public interface ClientCommand {
|
|
||||||
bool IsMatch(ClientCommandContext ctx);
|
public interface ClientCommand {
|
||||||
Task Dispatch(ClientCommandContext ctx);
|
bool IsMatch(ClientCommandContext ctx);
|
||||||
}
|
Task Dispatch(ClientCommandContext ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,49 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class ClientCommandContext {
|
|
||||||
public string Name { get; }
|
|
||||||
public string[] Args { get; }
|
|
||||||
public Context Chat { get; }
|
|
||||||
public User User { get; }
|
|
||||||
public Connection Connection { get; }
|
|
||||||
public Channel Channel { get; }
|
|
||||||
|
|
||||||
public ClientCommandContext(
|
public class ClientCommandContext {
|
||||||
string text,
|
public string Name { get; }
|
||||||
Context chat,
|
public string[] Args { get; }
|
||||||
User user,
|
public Context Chat { get; }
|
||||||
Connection connection,
|
public User User { get; }
|
||||||
Channel channel
|
public Connection Connection { get; }
|
||||||
) {
|
public Channel Channel { get; }
|
||||||
ArgumentNullException.ThrowIfNull(text);
|
|
||||||
|
|
||||||
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
public ClientCommandContext(
|
||||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
string text,
|
||||||
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
Context chat,
|
||||||
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
User user,
|
||||||
|
Connection connection,
|
||||||
|
Channel channel
|
||||||
|
) {
|
||||||
|
ArgumentNullException.ThrowIfNull(text);
|
||||||
|
|
||||||
string[] parts = text[1..].Split(' ');
|
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||||
Name = parts.First().Replace(".", string.Empty);
|
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||||
Args = [.. parts.Skip(1)];
|
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
}
|
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
||||||
|
|
||||||
public ClientCommandContext(
|
string[] parts = text[1..].Split(' ');
|
||||||
string name,
|
Name = parts.First().Replace(".", string.Empty);
|
||||||
string[] args,
|
Args = [.. parts.Skip(1)];
|
||||||
Context chat,
|
}
|
||||||
User user,
|
|
||||||
Connection connection,
|
|
||||||
Channel channel
|
|
||||||
) {
|
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
Args = args ?? throw new ArgumentNullException(nameof(args));
|
|
||||||
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
|
||||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
|
||||||
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
|
||||||
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool NameEquals(string name) {
|
public ClientCommandContext(
|
||||||
return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
|
string name,
|
||||||
}
|
string[] args,
|
||||||
|
Context chat,
|
||||||
|
User user,
|
||||||
|
Connection connection,
|
||||||
|
Channel channel
|
||||||
|
) {
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Args = args ?? throw new ArgumentNullException(nameof(args));
|
||||||
|
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||||
|
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||||
|
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool NameEquals(string name) {
|
||||||
|
return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class AFKClientCommand : ClientCommand {
|
|
||||||
private const string DEFAULT = "AFK";
|
|
||||||
public const int MAX_GRAPHEMES = 5;
|
|
||||||
public const int MAX_BYTES = MAX_GRAPHEMES * 10;
|
|
||||||
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class AFKClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("afk");
|
private const string DEFAULT = "AFK";
|
||||||
|
public const int MAX_GRAPHEMES = 5;
|
||||||
|
public const int MAX_BYTES = MAX_GRAPHEMES * 10;
|
||||||
|
|
||||||
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|
return ctx.NameEquals("afk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
string? statusText = ctx.Args.FirstOrDefault();
|
||||||
|
if(string.IsNullOrWhiteSpace(statusText))
|
||||||
|
statusText = DEFAULT;
|
||||||
|
else {
|
||||||
|
statusText = statusText.Trim();
|
||||||
|
|
||||||
|
StringInfo sti = new(statusText);
|
||||||
|
if(Encoding.UTF8.GetByteCount(statusText) > MAX_BYTES
|
||||||
|
|| sti.LengthInTextElements > MAX_GRAPHEMES)
|
||||||
|
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, MAX_GRAPHEMES)).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
await ctx.Chat.UpdateUser(
|
||||||
string? statusText = ctx.Args.FirstOrDefault();
|
ctx.User,
|
||||||
if(string.IsNullOrWhiteSpace(statusText))
|
status: UserStatus.Away,
|
||||||
statusText = DEFAULT;
|
statusText: statusText
|
||||||
else {
|
);
|
||||||
statusText = statusText.Trim();
|
|
||||||
|
|
||||||
StringInfo sti = new(statusText);
|
|
||||||
if(Encoding.UTF8.GetByteCount(statusText) > MAX_BYTES
|
|
||||||
|| sti.LengthInTextElements > MAX_GRAPHEMES)
|
|
||||||
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, MAX_GRAPHEMES)).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.UpdateUser(
|
|
||||||
ctx.User,
|
|
||||||
status: UserStatus.Away,
|
|
||||||
statusText: statusText
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class ActionClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
|
||||||
return ctx.NameEquals("action")
|
|
||||||
|| ctx.NameEquals("me");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
public class ActionClientCommand : ClientCommand {
|
||||||
if(ctx.Args.Length < 1)
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
return;
|
return ctx.NameEquals("action")
|
||||||
|
|| ctx.NameEquals("me");
|
||||||
|
}
|
||||||
|
|
||||||
string actionStr = string.Join(' ', ctx.Args);
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
if(string.IsNullOrWhiteSpace(actionStr))
|
if(ctx.Args.Length < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
string actionStr = string.Join(' ', ctx.Args);
|
||||||
ctx.Chat.RandomSnowflake.Next(),
|
if(string.IsNullOrWhiteSpace(actionStr))
|
||||||
ctx.Channel.Name,
|
return;
|
||||||
ctx.User.UserId,
|
|
||||||
ctx.User.UserName,
|
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
||||||
ctx.User.Colour,
|
ctx.Chat.RandomSnowflake.Next(),
|
||||||
ctx.User.Rank,
|
ctx.Channel.Name,
|
||||||
ctx.User.NickName,
|
ctx.User.UserId,
|
||||||
ctx.User.Permissions,
|
ctx.User.UserName,
|
||||||
DateTimeOffset.Now,
|
ctx.User.Colour,
|
||||||
actionStr,
|
ctx.User.Rank,
|
||||||
false, true, false
|
ctx.User.NickName,
|
||||||
));
|
ctx.User.Permissions,
|
||||||
}
|
DateTimeOffset.Now,
|
||||||
|
actionStr,
|
||||||
|
false, true, false
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class BanListClientCommand(BansClient bansClient) : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class BanListClientCommand(BansClient bansClient) : ClientCommand {
|
||||||
return ctx.NameEquals("bans")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("banned");
|
return ctx.NameEquals("bans")
|
||||||
|
|| ctx.NameEquals("banned");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
try {
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
BanInfo[] banInfos = await bansClient.BanGetListAsync();
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new BanListS2CPacket(
|
||||||
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
msgId,
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString()))
|
||||||
return;
|
));
|
||||||
}
|
} catch(Exception) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
|
||||||
try {
|
|
||||||
BanInfo[] banInfos = await bansClient.BanGetListAsync();
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new BanListS2CPacket(
|
|
||||||
msgId,
|
|
||||||
banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString()))
|
|
||||||
));
|
|
||||||
} catch(Exception) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class BroadcastClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class BroadcastClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("say")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("broadcast");
|
return ctx.NameEquals("say")
|
||||||
|
|| ctx.NameEquals("broadcast");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.Broadcast)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
msgId,
|
||||||
|
string.Empty,
|
||||||
if(!ctx.User.Can(UserPermissions.Broadcast)) {
|
ctx.User.UserId,
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
ctx.User.UserName,
|
||||||
return;
|
ctx.User.Colour,
|
||||||
}
|
ctx.User.Rank,
|
||||||
|
ctx.User.NickName,
|
||||||
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
ctx.User.Permissions,
|
||||||
msgId,
|
DateTimeOffset.Now,
|
||||||
string.Empty,
|
string.Join(' ', ctx.Args),
|
||||||
ctx.User.UserId,
|
false, false, true
|
||||||
ctx.User.UserName,
|
));
|
||||||
ctx.User.Colour,
|
|
||||||
ctx.User.Rank,
|
|
||||||
ctx.User.NickName,
|
|
||||||
ctx.User.Permissions,
|
|
||||||
DateTimeOffset.Now,
|
|
||||||
string.Join(' ', ctx.Args),
|
|
||||||
false, false, true
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class CreateChannelClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class CreateChannelClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("create");
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|
return ctx.NameEquals("create");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.CreateChannel)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string firstArg = ctx.Args.First();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
|
||||||
|
|
||||||
if(!ctx.User.Can(UserPermissions.CreateChannel)) {
|
bool createChanHasHierarchy;
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
if(ctx.Args.Length < 1 || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
|
||||||
return;
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
}
|
return;
|
||||||
|
|
||||||
string firstArg = ctx.Args.First();
|
|
||||||
|
|
||||||
bool createChanHasHierarchy;
|
|
||||||
if(ctx.Args.Length < 1 || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int createChanHierarchy = 0;
|
|
||||||
if(createChanHasHierarchy)
|
|
||||||
if(!int.TryParse(firstArg, out createChanHierarchy))
|
|
||||||
createChanHierarchy = 0;
|
|
||||||
|
|
||||||
if(createChanHierarchy > ctx.User.Rank) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
|
|
||||||
|
|
||||||
if(!Channel.CheckName(createChanName)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Channel createChan = new(
|
|
||||||
createChanName,
|
|
||||||
isTemporary: !ctx.User.Can(UserPermissions.SetChannelPermanent),
|
|
||||||
rank: createChanHierarchy,
|
|
||||||
ownerId: ctx.User.UserId
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.Chat.Channels.Add(createChan);
|
|
||||||
foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank))
|
|
||||||
await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(createChan.Name, createChan.HasPassword, createChan.IsTemporary));
|
|
||||||
|
|
||||||
await ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password);
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, createChan.Name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int createChanHierarchy = 0;
|
||||||
|
if(createChanHasHierarchy)
|
||||||
|
if(!int.TryParse(firstArg, out createChanHierarchy))
|
||||||
|
createChanHierarchy = 0;
|
||||||
|
|
||||||
|
if(createChanHierarchy > ctx.User.Rank) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string createChanName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
|
||||||
|
|
||||||
|
if(!Channel.CheckName(createChanName)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NAME_INVALID));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_ALREADY_EXISTS, true, createChanName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel createChan = new(
|
||||||
|
createChanName,
|
||||||
|
isTemporary: !ctx.User.Can(UserPermissions.SetChannelPermanent),
|
||||||
|
rank: createChanHierarchy,
|
||||||
|
ownerId: ctx.User.UserId
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.Chat.Channels.Add(createChan);
|
||||||
|
foreach(User ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank))
|
||||||
|
await ctx.Chat.SendTo(ccu, new ChannelCreateS2CPacket(createChan.Name, createChan.HasPassword, createChan.IsTemporary));
|
||||||
|
|
||||||
|
await ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password);
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_CREATED, false, createChan.Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class DeleteChannelClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class DeleteChannelClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("delchan") || (
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
ctx.NameEquals("delete")
|
return ctx.NameEquals("delchan") || (
|
||||||
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false
|
ctx.NameEquals("delete")
|
||||||
);
|
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(ctx.Args.Length < 1 || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string delChanName = string.Join('_', ctx.Args);
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
Channel? delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName));
|
||||||
|
|
||||||
if(ctx.Args.Length < 1 || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
|
if(delChan == null) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName));
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
string delChanName = string.Join('_', ctx.Args);
|
|
||||||
Channel? delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName));
|
|
||||||
|
|
||||||
if(delChan == null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, delChanName));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!ctx.User.Can(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETE_FAILED, true, delChan.Name));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.RemoveChannel(delChan);
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETED, false, delChan.Name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.DeleteChannel) && delChan.IsOwner(ctx.User)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETE_FAILED, true, delChan.Name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Chat.RemoveChannel(delChan);
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_DELETED, false, delChan.Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,40 @@
|
||||||
using SharpChat.EventStorage;
|
using SharpChat.EventStorage;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands
|
namespace SharpChat.ClientCommands;
|
||||||
{
|
|
||||||
public class DeleteMessageClientCommand : ClientCommand {
|
public class DeleteMessageClientCommand : ClientCommand {
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
return ctx.NameEquals("delmsg") || (
|
return ctx.NameEquals("delmsg") || (
|
||||||
ctx.NameEquals("delete")
|
ctx.NameEquals("delete")
|
||||||
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true
|
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
bool deleteAnyMessage = ctx.User.Can(UserPermissions.DeleteAnyMessage);
|
||||||
|
|
||||||
|
if(!deleteAnyMessage && !ctx.User.Can(UserPermissions.DeleteOwnMessage)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string? firstArg = ctx.Args.FirstOrDefault();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
|
||||||
bool deleteAnyMessage = ctx.User.Can(UserPermissions.DeleteAnyMessage);
|
|
||||||
|
|
||||||
if(!deleteAnyMessage && !ctx.User.Can(UserPermissions.DeleteOwnMessage)) {
|
if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
string? firstArg = ctx.Args.FirstOrDefault();
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long delSeqId)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StoredEventInfo? delMsg = ctx.Chat.Events.GetEvent(delSeqId);
|
|
||||||
|
|
||||||
if(delMsg?.Sender is null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.MESSAGE_DELETE_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Chat.Events.RemoveEvent(delMsg);
|
|
||||||
await ctx.Chat.Send(new ChatMessageDeleteS2CPacket(delMsg.Id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StoredEventInfo? delMsg = ctx.Chat.Events.GetEvent(delSeqId);
|
||||||
|
|
||||||
|
if(delMsg?.Sender is null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.MESSAGE_DELETE_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Chat.Events.RemoveEvent(delMsg);
|
||||||
|
await ctx.Chat.Send(new ChatMessageDeleteS2CPacket(delMsg.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class JoinChannelClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class JoinChannelClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("join");
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|
return ctx.NameEquals("join");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel";
|
||||||
|
Channel? joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr));
|
||||||
|
|
||||||
|
if(joinChan is null) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr));
|
||||||
|
await ctx.Chat.ForceChannel(ctx.User);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
await ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1)));
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
|
||||||
string joinChanStr = ctx.Args.FirstOrDefault() ?? "Channel";
|
|
||||||
Channel? joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr));
|
|
||||||
|
|
||||||
if(joinChan is null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, joinChanStr));
|
|
||||||
await ctx.Chat.ForceChannel(ctx.User);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.SwitchChannel(ctx.User, joinChan, string.Join(' ', ctx.Args.Skip(1)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,72 +2,72 @@ using SharpChat.Bans;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class KickBanClientCommand(BansClient bansClient) : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class KickBanClientCommand(BansClient bansClient) : ClientCommand {
|
||||||
return ctx.NameEquals("kick")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("ban");
|
return ctx.NameEquals("kick")
|
||||||
|
|| ctx.NameEquals("ban");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
bool isBanning = ctx.NameEquals("ban");
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string? banUserTarget = ctx.Args.ElementAtOrDefault(0);
|
||||||
bool isBanning = ctx.NameEquals("ban");
|
string? banDurationStr = ctx.Args.ElementAtOrDefault(1);
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
int banReasonIndex = 1;
|
||||||
|
User? banUser = null;
|
||||||
|
|
||||||
if(!ctx.User.Can(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) {
|
if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, banUserTarget ?? "User"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(banUser.Rank >= ctx.User.Rank && banUser != ctx.User) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero;
|
||||||
|
if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) {
|
||||||
|
if(durationSeconds < 0) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? banUserTarget = ctx.Args.ElementAtOrDefault(0);
|
duration = TimeSpan.FromSeconds(durationSeconds);
|
||||||
string? banDurationStr = ctx.Args.ElementAtOrDefault(1);
|
++banReasonIndex;
|
||||||
int banReasonIndex = 1;
|
}
|
||||||
User? banUser = null;
|
|
||||||
|
|
||||||
if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, banUserTarget ?? "User"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(banUser.Rank >= ctx.User.Rank && banUser != ctx.User) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero;
|
|
||||||
if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) {
|
|
||||||
if(durationSeconds < 0) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
duration = TimeSpan.FromSeconds(durationSeconds);
|
|
||||||
++banReasonIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(duration <= TimeSpan.Zero) {
|
|
||||||
await ctx.Chat.BanUser(banUser, duration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
|
|
||||||
|
|
||||||
BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId);
|
|
||||||
if(banInfo is not null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await bansClient.BanCreateAsync(
|
|
||||||
BanKind.User,
|
|
||||||
duration,
|
|
||||||
ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None,
|
|
||||||
banUser.UserId,
|
|
||||||
banReason,
|
|
||||||
ctx.Connection.RemoteAddress,
|
|
||||||
ctx.User.UserId
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if(duration <= TimeSpan.Zero) {
|
||||||
await ctx.Chat.BanUser(banUser, duration);
|
await ctx.Chat.BanUser(banUser, duration);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
|
||||||
|
|
||||||
|
BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId);
|
||||||
|
if(banInfo is not null) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bansClient.BanCreateAsync(
|
||||||
|
BanKind.User,
|
||||||
|
duration,
|
||||||
|
ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None,
|
||||||
|
banUser.UserId,
|
||||||
|
banReason,
|
||||||
|
ctx.Connection.RemoteAddress,
|
||||||
|
ctx.User.UserId
|
||||||
|
);
|
||||||
|
|
||||||
|
await ctx.Chat.BanUser(banUser, duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,62 +2,62 @@ using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class NickClientCommand : ClientCommand {
|
|
||||||
private const int MAX_GRAPHEMES = 16;
|
|
||||||
private const int MAX_BYTES = MAX_GRAPHEMES * 10;
|
|
||||||
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class NickClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("nick");
|
private const int MAX_GRAPHEMES = 16;
|
||||||
|
private const int MAX_BYTES = MAX_GRAPHEMES * 10;
|
||||||
|
|
||||||
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|
return ctx.NameEquals("nick");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
bool setOthersNick = ctx.User.Can(UserPermissions.SetOthersNickname);
|
||||||
|
|
||||||
|
if(!setOthersNick && !ctx.User.Can(UserPermissions.SetOwnNickname)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
User? targetUser = null;
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
int offset = 0;
|
||||||
bool setOthersNick = ctx.User.Can(UserPermissions.SetOthersNickname);
|
|
||||||
|
|
||||||
if(!setOthersNick && !ctx.User.Can(UserPermissions.SetOwnNickname)) {
|
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString());
|
||||||
return;
|
++offset;
|
||||||
}
|
|
||||||
|
|
||||||
User? targetUser = null;
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
|
|
||||||
targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString());
|
|
||||||
++offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
targetUser ??= ctx.User;
|
|
||||||
|
|
||||||
if(ctx.Args.Length < offset) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string nickStr = string.Join('_', ctx.Args.Skip(offset))
|
|
||||||
.Replace("\n", string.Empty).Replace("\r", string.Empty)
|
|
||||||
.Replace("\f", string.Empty).Replace("\t", string.Empty)
|
|
||||||
.Replace(' ', '_').Trim();
|
|
||||||
|
|
||||||
if(nickStr == targetUser.UserName)
|
|
||||||
nickStr = string.Empty;
|
|
||||||
else if(string.IsNullOrEmpty(nickStr))
|
|
||||||
nickStr = string.Empty;
|
|
||||||
else {
|
|
||||||
StringInfo nsi = new(nickStr);
|
|
||||||
if(Encoding.UTF8.GetByteCount(nickStr) > MAX_BYTES
|
|
||||||
|| nsi.LengthInTextElements > MAX_GRAPHEMES)
|
|
||||||
nickStr = nsi.SubstringByTextElements(0, Math.Min(nsi.LengthInTextElements, MAX_GRAPHEMES)).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.NAME_IN_USE, true, nickStr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? previousName = targetUser.UserId == ctx.User.UserId ? (targetUser.NickName ?? targetUser.UserName) : null;
|
|
||||||
await ctx.Chat.UpdateUser(targetUser, nickName: nickStr, silent: previousName == null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetUser ??= ctx.User;
|
||||||
|
|
||||||
|
if(ctx.Args.Length < offset) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string nickStr = string.Join('_', ctx.Args.Skip(offset))
|
||||||
|
.Replace("\n", string.Empty).Replace("\r", string.Empty)
|
||||||
|
.Replace("\f", string.Empty).Replace("\t", string.Empty)
|
||||||
|
.Replace(' ', '_').Trim();
|
||||||
|
|
||||||
|
if(nickStr == targetUser.UserName)
|
||||||
|
nickStr = string.Empty;
|
||||||
|
else if(string.IsNullOrEmpty(nickStr))
|
||||||
|
nickStr = string.Empty;
|
||||||
|
else {
|
||||||
|
StringInfo nsi = new(nickStr);
|
||||||
|
if(Encoding.UTF8.GetByteCount(nickStr) > MAX_BYTES
|
||||||
|
|| nsi.LengthInTextElements > MAX_GRAPHEMES)
|
||||||
|
nickStr = nsi.SubstringByTextElements(0, Math.Min(nsi.LengthInTextElements, MAX_GRAPHEMES)).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.NAME_IN_USE, true, nickStr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? previousName = targetUser.UserId == ctx.User.UserId ? (targetUser.NickName ?? targetUser.UserName) : null;
|
||||||
|
await ctx.Chat.UpdateUser(targetUser, nickName: nickStr, silent: previousName == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,39 +2,39 @@ using SharpChat.Bans;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand {
|
||||||
return ctx.NameEquals("pardonip")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("unbanip");
|
return ctx.NameEquals("pardonip")
|
||||||
|
|| ctx.NameEquals("unbanip");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string? unbanAddrTarget = ctx.Args.FirstOrDefault();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
return;
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? unbanAddrTarget = ctx.Args.FirstOrDefault();
|
|
||||||
if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unbanAddrTarget = unbanAddr.ToString();
|
|
||||||
|
|
||||||
BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr);
|
|
||||||
if(banInfo?.Kind != BanKind.IPAddress) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await bansClient.BanRevokeAsync(banInfo))
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget));
|
|
||||||
else
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbanAddrTarget = unbanAddr.ToString();
|
||||||
|
|
||||||
|
BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr);
|
||||||
|
if(banInfo?.Kind != BanKind.IPAddress) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(await bansClient.BanRevokeAsync(banInfo))
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget));
|
||||||
|
else
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,46 @@
|
||||||
using SharpChat.Bans;
|
using SharpChat.Bans;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class PardonUserClientCommand(BansClient bansClient) : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class PardonUserClientCommand(BansClient bansClient) : ClientCommand {
|
||||||
return ctx.NameEquals("pardon")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("unban");
|
return ctx.NameEquals("pardon")
|
||||||
|
|| ctx.NameEquals("unban");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string? unbanUserTarget = ctx.Args.FirstOrDefault();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
if(!ctx.User.Can(UserPermissions.BanUser | UserPermissions.KickUser)) {
|
return;
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? unbanUserTarget = ctx.Args.FirstOrDefault();
|
|
||||||
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string unbanUserDisplay = unbanUserTarget;
|
|
||||||
User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
|
|
||||||
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId))
|
|
||||||
unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString());
|
|
||||||
if(unbanUser != null) {
|
|
||||||
unbanUserTarget = unbanUser.UserId;
|
|
||||||
unbanUserDisplay = unbanUser.UserName;
|
|
||||||
}
|
|
||||||
|
|
||||||
BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget);
|
|
||||||
if(banInfo?.Kind != BanKind.User) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(await bansClient.BanRevokeAsync(banInfo))
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay));
|
|
||||||
else
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string unbanUserDisplay = unbanUserTarget;
|
||||||
|
User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
|
||||||
|
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId))
|
||||||
|
unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString());
|
||||||
|
if(unbanUser != null) {
|
||||||
|
unbanUserTarget = unbanUser.UserId;
|
||||||
|
unbanUserDisplay = unbanUser.UserName;
|
||||||
|
}
|
||||||
|
|
||||||
|
BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget);
|
||||||
|
if(banInfo?.Kind != BanKind.User) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(await bansClient.BanRevokeAsync(banInfo))
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay));
|
||||||
|
else
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class PasswordChannelClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class PasswordChannelClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("pwd")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("password");
|
return ctx.NameEquals("pwd")
|
||||||
|
|| ctx.NameEquals("password");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string chanPass = string.Join(' ', ctx.Args).Trim();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
|
||||||
|
|
||||||
if(!ctx.User.Can(UserPermissions.SetChannelPassword) || ctx.Channel.IsOwner(ctx.User)) {
|
if(string.IsNullOrWhiteSpace(chanPass))
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
chanPass = string.Empty;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string chanPass = string.Join(' ', ctx.Args).Trim();
|
await ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass);
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_PASSWORD_CHANGED, false));
|
||||||
if(string.IsNullOrWhiteSpace(chanPass))
|
|
||||||
chanPass = string.Empty;
|
|
||||||
|
|
||||||
await ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass);
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_PASSWORD_CHANGED, false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class RankChannelClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class RankChannelClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("rank")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("privilege")
|
return ctx.NameEquals("rank")
|
||||||
|| ctx.NameEquals("priv");
|
|| ctx.NameEquals("privilege")
|
||||||
|
|| ctx.NameEquals("priv");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.SetChannelHierarchy) || ctx.Channel.IsOwner(ctx.User)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
if(ctx.Args.Length < 1 || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) {
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY));
|
||||||
|
return;
|
||||||
if(!ctx.User.Can(UserPermissions.SetChannelHierarchy) || ctx.Channel.IsOwner(ctx.User)) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ctx.Args.Length < 1 || !int.TryParse(ctx.Args.First(), out int chanHierarchy) || chanHierarchy > ctx.User.Rank) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.INSUFFICIENT_HIERARCHY));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy);
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy);
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_HIERARCHY_CHANGED, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class RemoteAddressClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class RemoteAddressClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("ip")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("whois");
|
return ctx.NameEquals("ip")
|
||||||
|
|| ctx.NameEquals("whois");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(!ctx.User.Can(UserPermissions.SeeIPAddress)) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, "/ip"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string? ipUserStr = ctx.Args.FirstOrDefault();
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
User? ipUser = null;
|
||||||
|
|
||||||
if(!ctx.User.Can(UserPermissions.SeeIPAddress)) {
|
if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, "/ip"));
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, ipUserStr ?? "User"));
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
string? ipUserStr = ctx.Args.FirstOrDefault();
|
|
||||||
User? ipUser = null;
|
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, ipUserStr ?? "User"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser))
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.IP_ADDRESS, false, ipUser.UserName, ip));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser))
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.IP_ADDRESS, false, ipUser.UserName, ip));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand {
|
|
||||||
private readonly ManualResetEvent WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle));
|
|
||||||
private readonly Func<bool> ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck));
|
|
||||||
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand {
|
||||||
return ctx.NameEquals("shutdown")
|
private readonly ManualResetEvent WaitHandle = waitHandle ?? throw new ArgumentNullException(nameof(waitHandle));
|
||||||
|| ctx.NameEquals("restart");
|
private readonly Func<bool> ShutdownCheck = shutdownCheck ?? throw new ArgumentNullException(nameof(shutdownCheck));
|
||||||
|
|
||||||
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|
return ctx.NameEquals("shutdown")
|
||||||
|
|| ctx.NameEquals("restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
if(!ctx.User.UserId.Equals("1")) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
if(!ShutdownCheck())
|
||||||
if(!ctx.User.UserId.Equals("1")) {
|
return;
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!ShutdownCheck())
|
if(ctx.NameEquals("restart"))
|
||||||
return;
|
foreach(Connection conn in ctx.Chat.Connections)
|
||||||
|
conn.PrepareForRestart();
|
||||||
|
|
||||||
if(ctx.NameEquals("restart"))
|
await ctx.Chat.Update();
|
||||||
foreach(Connection conn in ctx.Chat.Connections)
|
WaitHandle?.Set();
|
||||||
conn.PrepareForRestart();
|
|
||||||
|
|
||||||
await ctx.Chat.Update();
|
|
||||||
WaitHandle?.Set();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
using SharpChat.Events;
|
using SharpChat.Events;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class WhisperClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
public class WhisperClientCommand : ClientCommand {
|
||||||
return ctx.NameEquals("whisper")
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
|| ctx.NameEquals("msg");
|
return ctx.NameEquals("whisper")
|
||||||
|
|| ctx.NameEquals("msg");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
|
|
||||||
|
if(ctx.Args.Length < 2) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
User? whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr));
|
||||||
|
|
||||||
if(ctx.Args.Length < 2) {
|
if(whisperUser == null) {
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, whisperUserStr));
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
|
|
||||||
User? whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr));
|
|
||||||
|
|
||||||
if(whisperUser == null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_FOUND, true, whisperUserStr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(whisperUser == ctx.User)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
|
||||||
msgId,
|
|
||||||
User.GetDMChannelName(ctx.User, whisperUser),
|
|
||||||
ctx.User.UserId,
|
|
||||||
ctx.User.UserName,
|
|
||||||
ctx.User.Colour,
|
|
||||||
ctx.User.Rank,
|
|
||||||
ctx.User.NickName,
|
|
||||||
ctx.User.Permissions,
|
|
||||||
DateTimeOffset.Now,
|
|
||||||
string.Join(' ', ctx.Args.Skip(1)),
|
|
||||||
true, false, false
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(whisperUser == ctx.User)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ctx.Chat.DispatchEvent(new MessageCreateEvent(
|
||||||
|
msgId,
|
||||||
|
User.GetDMChannelName(ctx.User, whisperUser),
|
||||||
|
ctx.User.UserId,
|
||||||
|
ctx.User.UserName,
|
||||||
|
ctx.User.Colour,
|
||||||
|
ctx.User.Rank,
|
||||||
|
ctx.User.NickName,
|
||||||
|
ctx.User.Permissions,
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
string.Join(' ', ctx.Args.Skip(1)),
|
||||||
|
true, false, false
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.ClientCommands {
|
namespace SharpChat.ClientCommands;
|
||||||
public class WhoClientCommand : ClientCommand {
|
|
||||||
public bool IsMatch(ClientCommandContext ctx) {
|
|
||||||
return ctx.NameEquals("who");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Dispatch(ClientCommandContext ctx) {
|
public class WhoClientCommand : ClientCommand {
|
||||||
long msgId = ctx.Chat.RandomSnowflake.Next();
|
public bool IsMatch(ClientCommandContext ctx) {
|
||||||
StringBuilder whoChanSB = new();
|
return ctx.NameEquals("who");
|
||||||
string? whoChanStr = ctx.Args.FirstOrDefault();
|
}
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(whoChanStr)) {
|
public async Task Dispatch(ClientCommandContext ctx) {
|
||||||
foreach(User whoUser in ctx.Chat.Users) {
|
long msgId = ctx.Chat.RandomSnowflake.Next();
|
||||||
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
StringBuilder whoChanSB = new();
|
||||||
|
string? whoChanStr = ctx.Args.FirstOrDefault();
|
||||||
|
|
||||||
if(whoUser == ctx.User)
|
if(string.IsNullOrEmpty(whoChanStr)) {
|
||||||
whoChanSB.Append(@" style=""font-weight: bold;""");
|
foreach(User whoUser in ctx.Chat.Users) {
|
||||||
|
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
||||||
|
|
||||||
whoChanSB.Append('>');
|
if(whoUser == ctx.User)
|
||||||
whoChanSB.Append(whoUser.LegacyName);
|
whoChanSB.Append(@" style=""font-weight: bold;""");
|
||||||
whoChanSB.Append("</a>, ");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(whoChanSB.Length > 2)
|
whoChanSB.Append('>');
|
||||||
whoChanSB.Length -= 2;
|
whoChanSB.Append(whoUser.LegacyName);
|
||||||
|
whoChanSB.Append("</a>, ");
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB));
|
|
||||||
} else {
|
|
||||||
Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
|
|
||||||
|
|
||||||
if(whoChan is null) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(whoChan.Rank > ctx.User.Rank || (whoChan.HasPassword && !ctx.User.Can(UserPermissions.JoinAnyChannel))) {
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_ERROR, true, whoChanStr));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(User whoUser in ctx.Chat.GetChannelUsers(whoChan)) {
|
|
||||||
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
|
||||||
|
|
||||||
if(whoUser == ctx.User)
|
|
||||||
whoChanSB.Append(@" style=""font-weight: bold;""");
|
|
||||||
|
|
||||||
whoChanSB.Append('>');
|
|
||||||
whoChanSB.Append(whoUser.LegacyName);
|
|
||||||
whoChanSB.Append("</a>, ");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(whoChanSB.Length > 2)
|
|
||||||
whoChanSB.Length -= 2;
|
|
||||||
|
|
||||||
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(whoChanSB.Length > 2)
|
||||||
|
whoChanSB.Length -= 2;
|
||||||
|
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_SERVER, false, whoChanSB));
|
||||||
|
} else {
|
||||||
|
Channel? whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
|
||||||
|
|
||||||
|
if(whoChan is null) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.CHANNEL_NOT_FOUND, true, whoChanStr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(whoChan.Rank > ctx.User.Rank || (whoChan.HasPassword && !ctx.User.Can(UserPermissions.JoinAnyChannel))) {
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_ERROR, true, whoChanStr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(User whoUser in ctx.Chat.GetChannelUsers(whoChan)) {
|
||||||
|
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
|
||||||
|
|
||||||
|
if(whoUser == ctx.User)
|
||||||
|
whoChanSB.Append(@" style=""font-weight: bold;""");
|
||||||
|
|
||||||
|
whoChanSB.Append('>');
|
||||||
|
whoChanSB.Append(whoUser.LegacyName);
|
||||||
|
whoChanSB.Append("</a>, ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(whoChanSB.Length > 2)
|
||||||
|
whoChanSB.Length -= 2;
|
||||||
|
|
||||||
|
await ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USERS_LISTING_CHANNEL, false, whoChan.Name, whoChanSB));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,89 +2,89 @@ using Fleck;
|
||||||
using SharpChat.SockChat;
|
using SharpChat.SockChat;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class Connection : IDisposable {
|
|
||||||
public const int ID_LENGTH = 20;
|
public class Connection : IDisposable {
|
||||||
|
public const int ID_LENGTH = 20;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1);
|
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1);
|
||||||
#else
|
#else
|
||||||
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5);
|
public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public IWebSocketConnection Socket { get; }
|
public IWebSocketConnection Socket { get; }
|
||||||
|
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now;
|
public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now;
|
||||||
public User? User { get; set; }
|
public User? User { get; set; }
|
||||||
|
|
||||||
private int CloseCode { get; set; } = 1000;
|
private int CloseCode { get; set; } = 1000;
|
||||||
|
|
||||||
public IPAddress RemoteAddress { get; }
|
public IPAddress RemoteAddress { get; }
|
||||||
public ushort RemotePort { get; }
|
public ushort RemotePort { get; }
|
||||||
|
|
||||||
public bool IsAlive => !IsDisposed && !HasTimedOut;
|
public bool IsAlive => !IsDisposed && !HasTimedOut;
|
||||||
|
|
||||||
public Connection(IWebSocketConnection sock) {
|
public Connection(IWebSocketConnection sock) {
|
||||||
Socket = sock;
|
Socket = sock;
|
||||||
Id = RNG.SecureRandomString(ID_LENGTH);
|
Id = RNG.SecureRandomString(ID_LENGTH);
|
||||||
|
|
||||||
if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr))
|
if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr))
|
||||||
throw new Exception("Unable to parse remote address?????");
|
throw new Exception("Unable to parse remote address?????");
|
||||||
|
|
||||||
if(IPAddress.IsLoopback(addr)
|
if(IPAddress.IsLoopback(addr)
|
||||||
&& sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr)
|
&& sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr)
|
||||||
&& IPAddress.TryParse(addrStr, out IPAddress? realAddr))
|
&& IPAddress.TryParse(addrStr, out IPAddress? realAddr))
|
||||||
addr = realAddr;
|
addr = realAddr;
|
||||||
|
|
||||||
RemoteAddress = addr;
|
RemoteAddress = addr;
|
||||||
RemotePort = (ushort)sock.ConnectionInfo.ClientPort;
|
RemotePort = (ushort)sock.ConnectionInfo.ClientPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Send(S2CPacket packet) {
|
public async Task Send(S2CPacket packet) {
|
||||||
if(!Socket.IsAvailable)
|
if(!Socket.IsAvailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string data = packet.Pack();
|
string data = packet.Pack();
|
||||||
if(!string.IsNullOrWhiteSpace(data))
|
if(!string.IsNullOrWhiteSpace(data))
|
||||||
await Socket.Send(data);
|
await Socket.Send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BumpPing() {
|
public void BumpPing() {
|
||||||
LastPing = DateTimeOffset.Now;
|
LastPing = DateTimeOffset.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasTimedOut
|
public bool HasTimedOut
|
||||||
=> DateTimeOffset.Now - LastPing > SessionTimeOut;
|
=> DateTimeOffset.Now - LastPing > SessionTimeOut;
|
||||||
|
|
||||||
public void PrepareForRestart() {
|
public void PrepareForRestart() {
|
||||||
CloseCode = 1012;
|
CloseCode = 1012;
|
||||||
}
|
}
|
||||||
|
|
||||||
~Connection() {
|
~Connection() {
|
||||||
DoDispose();
|
DoDispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
DoDispose();
|
DoDispose();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoDispose() {
|
private void DoDispose() {
|
||||||
if(IsDisposed)
|
if(IsDisposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
Socket.Close(CloseCode);
|
Socket.Close(CloseCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return Id;
|
return Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode() {
|
public override int GetHashCode() {
|
||||||
return Id.GetHashCode();
|
return Id.GetHashCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,409 +5,409 @@ using SharpChat.SockChat;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class Context {
|
|
||||||
public record ChannelUserAssoc(string UserId, string ChannelName);
|
|
||||||
|
|
||||||
public readonly SemaphoreSlim ContextAccess = new(1, 1);
|
public class Context {
|
||||||
|
public record ChannelUserAssoc(string UserId, string ChannelName);
|
||||||
|
|
||||||
public SnowflakeGenerator SnowflakeGenerator { get; } = new();
|
public readonly SemaphoreSlim ContextAccess = new(1, 1);
|
||||||
public RandomSnowflake RandomSnowflake { get; }
|
|
||||||
|
|
||||||
public HashSet<Channel> Channels { get; } = [];
|
public SnowflakeGenerator SnowflakeGenerator { get; } = new();
|
||||||
public HashSet<Connection> Connections { get; } = [];
|
public RandomSnowflake RandomSnowflake { get; }
|
||||||
public HashSet<User> Users { get; } = [];
|
|
||||||
public EventStorage.EventStorage Events { get; }
|
|
||||||
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
|
|
||||||
public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
|
|
||||||
public Dictionary<string, Channel> UserLastChannel { get; } = [];
|
|
||||||
|
|
||||||
public Context(EventStorage.EventStorage evtStore) {
|
public HashSet<Channel> Channels { get; } = [];
|
||||||
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
|
public HashSet<Connection> Connections { get; } = [];
|
||||||
RandomSnowflake = new(SnowflakeGenerator);
|
public HashSet<User> Users { get; } = [];
|
||||||
}
|
public EventStorage.EventStorage Events { get; }
|
||||||
|
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
|
||||||
|
public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
|
||||||
|
public Dictionary<string, Channel> UserLastChannel { get; } = [];
|
||||||
|
|
||||||
public async Task DispatchEvent(ChatEvent eventInfo) {
|
public Context(EventStorage.EventStorage evtStore) {
|
||||||
if(eventInfo is MessageCreateEvent mce) {
|
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
|
||||||
if(mce.IsBroadcast) {
|
RandomSnowflake = new(SnowflakeGenerator);
|
||||||
await Send(new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.BROADCAST, false, mce.MessageText));
|
}
|
||||||
} else if(mce.IsPrivate) {
|
|
||||||
// The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
|
|
||||||
// e.g. nook sees @Arysil and Arysil sees @nook
|
|
||||||
|
|
||||||
// this entire routine is garbage, channels should probably in the db
|
public async Task DispatchEvent(ChatEvent eventInfo) {
|
||||||
if(!mce.ChannelName.StartsWith('@'))
|
if(eventInfo is MessageCreateEvent mce) {
|
||||||
return;
|
if(mce.IsBroadcast) {
|
||||||
|
await Send(new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.BROADCAST, false, mce.MessageText));
|
||||||
|
} else if(mce.IsPrivate) {
|
||||||
|
// The channel name returned by GetDMChannelName should not be exposed to the user, instead @<Target User> should be displayed
|
||||||
|
// e.g. nook sees @Arysil and Arysil sees @nook
|
||||||
|
|
||||||
IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString());
|
// this entire routine is garbage, channels should probably in the db
|
||||||
if(uids.Count() != 2)
|
if(!mce.ChannelName.StartsWith('@'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IEnumerable<User> users = Users.Where(u => uids.Any(uid => uid == u.UserId));
|
IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString());
|
||||||
User? target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
|
if(uids.Count() != 2)
|
||||||
if(target == null)
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
foreach(User user in users)
|
IEnumerable<User> users = Users.Where(u => uids.Any(uid => uid == u.UserId));
|
||||||
await SendTo(user, new ChatMessageAddS2CPacket(
|
User? target = users.FirstOrDefault(u => u.UserId != mce.SenderId);
|
||||||
mce.MessageId,
|
if(target == null)
|
||||||
DateTimeOffset.Now,
|
return;
|
||||||
mce.SenderId,
|
|
||||||
mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
|
|
||||||
mce.IsAction,
|
|
||||||
true
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
|
|
||||||
if(channel is not null)
|
|
||||||
await SendTo(channel, new ChatMessageAddS2CPacket(
|
|
||||||
mce.MessageId,
|
|
||||||
DateTimeOffset.Now,
|
|
||||||
mce.SenderId,
|
|
||||||
mce.MessageText,
|
|
||||||
mce.IsAction,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Events.AddEvent(
|
foreach(User user in users)
|
||||||
mce.MessageId, "msg:add",
|
await SendTo(user, new ChatMessageAddS2CPacket(
|
||||||
mce.ChannelName,
|
mce.MessageId,
|
||||||
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
|
DateTimeOffset.Now,
|
||||||
new { text = mce.MessageText },
|
mce.SenderId,
|
||||||
(mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
|
mce.SenderId == user.UserId ? $"{target.LegacyName} {mce.MessageText}" : mce.MessageText,
|
||||||
| (mce.IsAction ? StoredEventFlags.Action : 0)
|
mce.IsAction,
|
||||||
| (mce.IsPrivate ? StoredEventFlags.Private : 0)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Update() {
|
|
||||||
foreach(Connection conn in Connections)
|
|
||||||
if(!conn.IsDisposed && conn.HasTimedOut) {
|
|
||||||
conn.Dispose();
|
|
||||||
Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections.RemoveWhere(conn => conn.IsDisposed);
|
|
||||||
|
|
||||||
foreach(User user in Users)
|
|
||||||
if(!Connections.Any(conn => conn.User == user)) {
|
|
||||||
Logger.Write($"Timing out {user} (no more connections).");
|
|
||||||
await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SafeUpdate() {
|
|
||||||
ContextAccess.Wait();
|
|
||||||
try {
|
|
||||||
await Update();
|
|
||||||
} finally {
|
|
||||||
ContextAccess.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsInChannel(User user, Channel channel) {
|
|
||||||
return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetUserChannelNames(User user) {
|
|
||||||
return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public Channel[] GetUserChannels(User user) {
|
|
||||||
string[] names = GetUserChannelNames(user);
|
|
||||||
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public string[] GetChannelUserIds(Channel channel) {
|
|
||||||
return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public User[] GetChannelUsers(Channel channel) {
|
|
||||||
string[] ids = GetChannelUserIds(channel);
|
|
||||||
return [.. Users.Where(u => ids.Contains(u.UserId))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateUser(
|
|
||||||
User user,
|
|
||||||
string? userName = null,
|
|
||||||
string? nickName = null,
|
|
||||||
ColourInheritable? colour = null,
|
|
||||||
UserStatus? status = null,
|
|
||||||
string? statusText = null,
|
|
||||||
int? rank = null,
|
|
||||||
UserPermissions? perms = null,
|
|
||||||
bool silent = false
|
|
||||||
) {
|
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
|
||||||
|
|
||||||
bool hasChanged = false;
|
|
||||||
string previousName = string.Empty;
|
|
||||||
|
|
||||||
if(userName != null && !user.UserName.Equals(userName)) {
|
|
||||||
user.UserName = userName;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(nickName != null && !user.NickName.Equals(nickName)) {
|
|
||||||
if(!silent)
|
|
||||||
previousName = user.LegacyName;
|
|
||||||
|
|
||||||
user.NickName = nickName;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(colour.HasValue && user.Colour != colour.Value) {
|
|
||||||
user.Colour = colour.Value;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(status.HasValue && user.Status != status.Value) {
|
|
||||||
user.Status = status.Value;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(statusText != null && !user.StatusText.Equals(statusText)) {
|
|
||||||
user.StatusText = statusText;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(rank != null && user.Rank != rank) {
|
|
||||||
user.Rank = (int)rank;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(perms.HasValue && user.Permissions != perms) {
|
|
||||||
user.Permissions = perms.Value;
|
|
||||||
hasChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(hasChanged) {
|
|
||||||
if(!string.IsNullOrWhiteSpace(previousName))
|
|
||||||
await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, user.LegacyNameWithStatus));
|
|
||||||
|
|
||||||
await SendToUserChannels(user, new UserUpdateS2CPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BanUser(User user, TimeSpan duration, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Kicked) {
|
|
||||||
if(duration > TimeSpan.Zero) {
|
|
||||||
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
|
|
||||||
await SendTo(user, new ForceDisconnectS2CPacket(expires));
|
|
||||||
} else
|
|
||||||
await SendTo(user, new ForceDisconnectS2CPacket());
|
|
||||||
|
|
||||||
foreach(Connection conn in Connections)
|
|
||||||
if(conn.User == user)
|
|
||||||
conn.Dispose();
|
|
||||||
Connections.RemoveWhere(conn => conn.IsDisposed);
|
|
||||||
|
|
||||||
await HandleDisconnect(user, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleJoin(User user, Channel chan, Connection conn, int maxMsgLength) {
|
|
||||||
if(!IsInChannel(user, chan)) {
|
|
||||||
long msgId = RandomSnowflake.Next();
|
|
||||||
await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
|
||||||
Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
|
||||||
}
|
|
||||||
|
|
||||||
await conn.Send(new AuthSuccessS2CPacket(
|
|
||||||
user.UserId,
|
|
||||||
user.LegacyNameWithStatus,
|
|
||||||
user.Colour,
|
|
||||||
user.Rank,
|
|
||||||
user.Permissions,
|
|
||||||
chan.Name,
|
|
||||||
maxMsgLength
|
|
||||||
));
|
|
||||||
await conn.Send(new ContextUsersS2CPacket(
|
|
||||||
GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
|
|
||||||
.Select(u => new ContextUsersS2CPacket.Entry(
|
|
||||||
u.UserId,
|
|
||||||
u.LegacyNameWithStatus,
|
|
||||||
u.Colour,
|
|
||||||
u.Rank,
|
|
||||||
u.Permissions,
|
|
||||||
true
|
true
|
||||||
))
|
));
|
||||||
));
|
} else {
|
||||||
|
Channel? channel = Channels.FirstOrDefault(c => c.NameEquals(mce.ChannelName));
|
||||||
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
|
if(channel is not null)
|
||||||
await conn.Send(new ContextMessageS2CPacket(msg));
|
await SendTo(channel, new ChatMessageAddS2CPacket(
|
||||||
|
mce.MessageId,
|
||||||
await conn.Send(new ContextChannelsS2CPacket(
|
DateTimeOffset.Now,
|
||||||
Channels.Where(c => c.Rank <= user.Rank)
|
mce.SenderId,
|
||||||
.Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary))
|
mce.MessageText,
|
||||||
));
|
mce.IsAction,
|
||||||
|
false
|
||||||
Users.Add(user);
|
));
|
||||||
|
|
||||||
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
|
|
||||||
UserLastChannel[user.UserId] = chan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleDisconnect(User user, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Leave) {
|
|
||||||
await UpdateUser(user, status: UserStatus.Offline);
|
|
||||||
Users.Remove(user);
|
|
||||||
UserLastChannel.Remove(user.UserId);
|
|
||||||
|
|
||||||
Channel[] channels = GetUserChannels(user);
|
|
||||||
|
|
||||||
foreach(Channel chan in channels) {
|
|
||||||
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
|
|
||||||
|
|
||||||
long msgId = RandomSnowflake.Next();
|
|
||||||
await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason));
|
|
||||||
Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log);
|
|
||||||
|
|
||||||
if(chan.IsTemporary && chan.IsOwner(user))
|
|
||||||
await RemoveChannel(chan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Events.AddEvent(
|
||||||
|
mce.MessageId, "msg:add",
|
||||||
|
mce.ChannelName,
|
||||||
|
mce.SenderId, mce.SenderName, mce.SenderColour, mce.SenderRank, mce.SenderNickName, mce.SenderPerms,
|
||||||
|
new { text = mce.MessageText },
|
||||||
|
(mce.IsBroadcast ? StoredEventFlags.Broadcast : 0)
|
||||||
|
| (mce.IsAction ? StoredEventFlags.Action : 0)
|
||||||
|
| (mce.IsPrivate ? StoredEventFlags.Private : 0)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Update() {
|
||||||
|
foreach(Connection conn in Connections)
|
||||||
|
if(!conn.IsDisposed && conn.HasTimedOut) {
|
||||||
|
conn.Dispose();
|
||||||
|
Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections.RemoveWhere(conn => conn.IsDisposed);
|
||||||
|
|
||||||
|
foreach(User user in Users)
|
||||||
|
if(!Connections.Any(conn => conn.User == user)) {
|
||||||
|
Logger.Write($"Timing out {user} (no more connections).");
|
||||||
|
await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SafeUpdate() {
|
||||||
|
ContextAccess.Wait();
|
||||||
|
try {
|
||||||
|
await Update();
|
||||||
|
} finally {
|
||||||
|
ContextAccess.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsInChannel(User user, Channel channel) {
|
||||||
|
return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetUserChannelNames(User user) {
|
||||||
|
return [.. ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Channel[] GetUserChannels(User user) {
|
||||||
|
string[] names = GetUserChannelNames(user);
|
||||||
|
return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetChannelUserIds(Channel channel) {
|
||||||
|
return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public User[] GetChannelUsers(Channel channel) {
|
||||||
|
string[] ids = GetChannelUserIds(channel);
|
||||||
|
return [.. Users.Where(u => ids.Contains(u.UserId))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateUser(
|
||||||
|
User user,
|
||||||
|
string? userName = null,
|
||||||
|
string? nickName = null,
|
||||||
|
ColourInheritable? colour = null,
|
||||||
|
UserStatus? status = null,
|
||||||
|
string? statusText = null,
|
||||||
|
int? rank = null,
|
||||||
|
UserPermissions? perms = null,
|
||||||
|
bool silent = false
|
||||||
|
) {
|
||||||
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
|
bool hasChanged = false;
|
||||||
|
string previousName = string.Empty;
|
||||||
|
|
||||||
|
if(userName != null && !user.UserName.Equals(userName)) {
|
||||||
|
user.UserName = userName;
|
||||||
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SwitchChannel(User user, Channel chan, string password) {
|
if(nickName != null && !user.NickName.Equals(nickName)) {
|
||||||
if(UserLastChannel.TryGetValue(user.UserId, out Channel? ulc) && chan == ulc) {
|
if(!silent)
|
||||||
|
previousName = user.LegacyName;
|
||||||
|
|
||||||
|
user.NickName = nickName;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(colour.HasValue && user.Colour != colour.Value) {
|
||||||
|
user.Colour = colour.Value;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(status.HasValue && user.Status != status.Value) {
|
||||||
|
user.Status = status.Value;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(statusText != null && !user.StatusText.Equals(statusText)) {
|
||||||
|
user.StatusText = statusText;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(rank != null && user.Rank != rank) {
|
||||||
|
user.Rank = (int)rank;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(perms.HasValue && user.Permissions != perms) {
|
||||||
|
user.Permissions = perms.Value;
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasChanged) {
|
||||||
|
if(!string.IsNullOrWhiteSpace(previousName))
|
||||||
|
await SendToUserChannels(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.NICKNAME_CHANGE, false, previousName, user.LegacyNameWithStatus));
|
||||||
|
|
||||||
|
await SendToUserChannels(user, new UserUpdateS2CPacket(user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BanUser(User user, TimeSpan duration, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Kicked) {
|
||||||
|
if(duration > TimeSpan.Zero) {
|
||||||
|
DateTimeOffset expires = duration >= TimeSpan.MaxValue ? DateTimeOffset.MaxValue : DateTimeOffset.Now + duration;
|
||||||
|
await SendTo(user, new ForceDisconnectS2CPacket(expires));
|
||||||
|
} else
|
||||||
|
await SendTo(user, new ForceDisconnectS2CPacket());
|
||||||
|
|
||||||
|
foreach(Connection conn in Connections)
|
||||||
|
if(conn.User == user)
|
||||||
|
conn.Dispose();
|
||||||
|
Connections.RemoveWhere(conn => conn.IsDisposed);
|
||||||
|
|
||||||
|
await HandleDisconnect(user, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleJoin(User user, Channel chan, Connection conn, int maxMsgLength) {
|
||||||
|
if(!IsInChannel(user, chan)) {
|
||||||
|
long msgId = RandomSnowflake.Next();
|
||||||
|
await SendTo(chan, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
||||||
|
Events.AddEvent(msgId, "user:connect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.Send(new AuthSuccessS2CPacket(
|
||||||
|
user.UserId,
|
||||||
|
user.LegacyNameWithStatus,
|
||||||
|
user.Colour,
|
||||||
|
user.Rank,
|
||||||
|
user.Permissions,
|
||||||
|
chan.Name,
|
||||||
|
maxMsgLength
|
||||||
|
));
|
||||||
|
await conn.Send(new ContextUsersS2CPacket(
|
||||||
|
GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
|
||||||
|
.Select(u => new ContextUsersS2CPacket.Entry(
|
||||||
|
u.UserId,
|
||||||
|
u.LegacyNameWithStatus,
|
||||||
|
u.Colour,
|
||||||
|
u.Rank,
|
||||||
|
u.Permissions,
|
||||||
|
true
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
|
||||||
|
await conn.Send(new ContextMessageS2CPacket(msg));
|
||||||
|
|
||||||
|
await conn.Send(new ContextChannelsS2CPacket(
|
||||||
|
Channels.Where(c => c.Rank <= user.Rank)
|
||||||
|
.Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary))
|
||||||
|
));
|
||||||
|
|
||||||
|
Users.Add(user);
|
||||||
|
|
||||||
|
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
|
||||||
|
UserLastChannel[user.UserId] = chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HandleDisconnect(User user, UserDisconnectS2CPacket.Reason reason = UserDisconnectS2CPacket.Reason.Leave) {
|
||||||
|
await UpdateUser(user, status: UserStatus.Offline);
|
||||||
|
Users.Remove(user);
|
||||||
|
UserLastChannel.Remove(user.UserId);
|
||||||
|
|
||||||
|
Channel[] channels = GetUserChannels(user);
|
||||||
|
|
||||||
|
foreach(Channel chan in channels) {
|
||||||
|
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
|
||||||
|
|
||||||
|
long msgId = RandomSnowflake.Next();
|
||||||
|
await SendTo(chan, new UserDisconnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.LegacyNameWithStatus, reason));
|
||||||
|
Events.AddEvent(msgId, "user:disconnect", chan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, new { reason = (int)reason }, StoredEventFlags.Log);
|
||||||
|
|
||||||
|
if(chan.IsTemporary && chan.IsOwner(user))
|
||||||
|
await RemoveChannel(chan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SwitchChannel(User user, Channel chan, string password) {
|
||||||
|
if(UserLastChannel.TryGetValue(user.UserId, out Channel? ulc) && chan == ulc) {
|
||||||
|
await ForceChannel(user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!user.Can(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
|
||||||
|
if(chan.Rank > user.Rank) {
|
||||||
|
await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
|
||||||
await ForceChannel(user);
|
await ForceChannel(user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!user.Can(UserPermissions.JoinAnyChannel) && chan.IsOwner(user)) {
|
if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) {
|
||||||
if(chan.Rank > user.Rank) {
|
await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
|
||||||
await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INSUFFICIENT_HIERARCHY, true, chan.Name));
|
await ForceChannel(user);
|
||||||
await ForceChannel(user);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!string.IsNullOrEmpty(chan.Password) && chan.Password != password) {
|
|
||||||
await SendTo(user, new CommandResponseS2CPacket(RandomSnowflake.Next(), LCR.CHANNEL_INVALID_PASSWORD, true, chan.Name));
|
|
||||||
await ForceChannel(user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ForceChannelSwitch(user, chan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ForceChannelSwitch(User user, Channel chan) {
|
await ForceChannelSwitch(user, chan);
|
||||||
if(!Channels.Contains(chan))
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
Channel oldChan = UserLastChannel[user.UserId];
|
public async Task ForceChannelSwitch(User user, Channel chan) {
|
||||||
|
if(!Channels.Contains(chan))
|
||||||
|
return;
|
||||||
|
|
||||||
long leaveId = RandomSnowflake.Next();
|
Channel oldChan = UserLastChannel[user.UserId];
|
||||||
await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId));
|
|
||||||
Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
|
||||||
|
|
||||||
long joinId = RandomSnowflake.Next();
|
long leaveId = RandomSnowflake.Next();
|
||||||
await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
await SendTo(oldChan, new UserChannelLeaveS2CPacket(leaveId, user.UserId));
|
||||||
Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
Events.AddEvent(leaveId, "chan:leave", oldChan.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
||||||
|
|
||||||
await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers));
|
long joinId = RandomSnowflake.Next();
|
||||||
await SendTo(user, new ContextUsersS2CPacket(
|
await SendTo(chan, new UserChannelJoinS2CPacket(joinId, user.UserId, user.LegacyNameWithStatus, user.Colour, user.Rank, user.Permissions));
|
||||||
GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
|
Events.AddEvent(joinId, "chan:join", chan.Name, user.UserId, user.LegacyName, user.Colour, user.Rank, user.NickName, user.Permissions, null, StoredEventFlags.Log);
|
||||||
.Select(u => new ContextUsersS2CPacket.Entry(
|
|
||||||
u.UserId,
|
|
||||||
u.LegacyNameWithStatus,
|
|
||||||
u.Colour,
|
|
||||||
u.Rank,
|
|
||||||
u.Permissions,
|
|
||||||
true
|
|
||||||
))
|
|
||||||
));
|
|
||||||
|
|
||||||
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
|
await SendTo(user, new ContextClearS2CPacket(ContextClearS2CPacket.Mode.MessagesUsers));
|
||||||
await SendTo(user, new ContextMessageS2CPacket(msg));
|
await SendTo(user, new ContextUsersS2CPacket(
|
||||||
|
GetChannelUsers(chan).Except([user]).OrderByDescending(u => u.Rank)
|
||||||
|
.Select(u => new ContextUsersS2CPacket.Entry(
|
||||||
|
u.UserId,
|
||||||
|
u.LegacyNameWithStatus,
|
||||||
|
u.Colour,
|
||||||
|
u.Rank,
|
||||||
|
u.Permissions,
|
||||||
|
true
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
await ForceChannel(user, chan);
|
foreach(StoredEventInfo msg in Events.GetChannelEventLog(chan.Name))
|
||||||
|
await SendTo(user, new ContextMessageS2CPacket(msg));
|
||||||
|
|
||||||
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
|
await ForceChannel(user, chan);
|
||||||
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
|
|
||||||
UserLastChannel[user.UserId] = chan;
|
|
||||||
|
|
||||||
if(oldChan.IsTemporary && oldChan.IsOwner(user))
|
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
|
||||||
await RemoveChannel(oldChan);
|
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
|
||||||
}
|
UserLastChannel[user.UserId] = chan;
|
||||||
|
|
||||||
public async Task Send(S2CPacket packet) {
|
if(oldChan.IsTemporary && oldChan.IsOwner(user))
|
||||||
foreach(Connection conn in Connections)
|
await RemoveChannel(oldChan);
|
||||||
if(conn.IsAlive && conn.User is not null)
|
}
|
||||||
await conn.Send(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendTo(User user, S2CPacket packet) {
|
public async Task Send(S2CPacket packet) {
|
||||||
foreach(Connection conn in Connections)
|
foreach(Connection conn in Connections)
|
||||||
if(conn.IsAlive && conn.User == user)
|
if(conn.IsAlive && conn.User is not null)
|
||||||
await conn.Send(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendTo(Channel channel, S2CPacket packet) {
|
|
||||||
// might be faster to grab the users first and then cascade into that SendTo
|
|
||||||
IEnumerable<Connection> conns = Connections.Where(c => c.IsAlive && c.User is not null && IsInChannel(c.User, channel));
|
|
||||||
foreach(Connection conn in conns)
|
|
||||||
await conn.Send(packet);
|
await conn.Send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendToUserChannels(User user, S2CPacket packet) {
|
public async Task SendTo(User user, S2CPacket packet) {
|
||||||
IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c));
|
foreach(Connection conn in Connections)
|
||||||
IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
|
if(conn.IsAlive && conn.User == user)
|
||||||
foreach(Connection conn in conns)
|
|
||||||
await conn.Send(packet);
|
await conn.Send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPAddress[] GetRemoteAddresses(User user) {
|
public async Task SendTo(Channel channel, S2CPacket packet) {
|
||||||
return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()];
|
// might be faster to grab the users first and then cascade into that SendTo
|
||||||
}
|
IEnumerable<Connection> conns = Connections.Where(c => c.IsAlive && c.User is not null && IsInChannel(c.User, channel));
|
||||||
|
foreach(Connection conn in conns)
|
||||||
|
await conn.Send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ForceChannel(User user, Channel? chan = null) {
|
public async Task SendToUserChannels(User user, S2CPacket packet) {
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
IEnumerable<Channel> chans = Channels.Where(c => IsInChannel(user, c));
|
||||||
|
IEnumerable<Connection> conns = Connections.Where(conn => conn.IsAlive && conn.User is not null && ChannelUsers.Any(cu => cu.UserId == conn.User.UserId && chans.Any(chan => chan.NameEquals(cu.ChannelName))));
|
||||||
|
foreach(Connection conn in conns)
|
||||||
|
await conn.Send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
|
public IPAddress[] GetRemoteAddresses(User user) {
|
||||||
throw new ArgumentException("no channel???");
|
return [.. Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct()];
|
||||||
|
}
|
||||||
|
|
||||||
await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
|
public async Task ForceChannel(User user, Channel? chan = null) {
|
||||||
}
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) {
|
if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
|
||||||
ArgumentNullException.ThrowIfNull(channel);
|
throw new ArgumentException("no channel???");
|
||||||
if(!Channels.Contains(channel))
|
|
||||||
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
|
|
||||||
|
|
||||||
if(temporary.HasValue)
|
await SendTo(user, new UserChannelForceJoinS2CPacket(chan.Name));
|
||||||
channel.IsTemporary = temporary.Value;
|
}
|
||||||
|
|
||||||
if(hierarchy.HasValue)
|
public async Task UpdateChannel(Channel channel, bool? temporary = null, int? hierarchy = null, string? password = null) {
|
||||||
channel.Rank = hierarchy.Value;
|
ArgumentNullException.ThrowIfNull(channel);
|
||||||
|
if(!Channels.Contains(channel))
|
||||||
|
throw new ArgumentException("Provided channel is not registered with this manager.", nameof(channel));
|
||||||
|
|
||||||
if(password != null)
|
if(temporary.HasValue)
|
||||||
channel.Password = password;
|
channel.IsTemporary = temporary.Value;
|
||||||
|
|
||||||
// TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
|
if(hierarchy.HasValue)
|
||||||
foreach(User user in Users.Where(u => u.Rank >= channel.Rank))
|
channel.Rank = hierarchy.Value;
|
||||||
await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveChannel(Channel channel) {
|
if(password != null)
|
||||||
if(channel == null || Channels.Count < 1)
|
channel.Password = password;
|
||||||
return;
|
|
||||||
|
|
||||||
Channel? defaultChannel = Channels.FirstOrDefault();
|
// TODO: Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively
|
||||||
if(defaultChannel is null)
|
foreach(User user in Users.Where(u => u.Rank >= channel.Rank))
|
||||||
return;
|
await SendTo(user, new ChannelUpdateS2CPacket(channel.Name, channel.Name, channel.HasPassword, channel.IsTemporary));
|
||||||
|
}
|
||||||
|
|
||||||
// Remove channel from the listing
|
public async Task RemoveChannel(Channel channel) {
|
||||||
Channels.Remove(channel);
|
if(channel == null || Channels.Count < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
// Move all users back to the main channel
|
Channel? defaultChannel = Channels.FirstOrDefault();
|
||||||
// TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
|
if(defaultChannel is null)
|
||||||
foreach(User user in GetChannelUsers(channel))
|
return;
|
||||||
await SwitchChannel(user, defaultChannel, string.Empty);
|
|
||||||
|
|
||||||
// Broadcast deletion of channel
|
// Remove channel from the listing
|
||||||
foreach(User user in Users.Where(u => u.Rank >= channel.Rank))
|
Channels.Remove(channel);
|
||||||
await SendTo(user, new ChannelDeleteS2CPacket(channel.Name));
|
|
||||||
}
|
// Move all users back to the main channel
|
||||||
|
// TODO: Replace this with a kick. SCv2 supports being in 0 channels, SCv1 should force the user back to DefaultChannel.
|
||||||
|
foreach(User user in GetChannelUsers(channel))
|
||||||
|
await SwitchChannel(user, defaultChannel, string.Empty);
|
||||||
|
|
||||||
|
// Broadcast deletion of channel
|
||||||
|
foreach(User user in Users.Where(u => u.Rank >= channel.Rank))
|
||||||
|
await SendTo(user, new ChannelDeleteS2CPacket(channel.Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public interface EventStorage {
|
|
||||||
void AddEvent(
|
public interface EventStorage {
|
||||||
long id,
|
void AddEvent(
|
||||||
string type,
|
long id,
|
||||||
string channelName,
|
string type,
|
||||||
string senderId,
|
string channelName,
|
||||||
string senderName,
|
string senderId,
|
||||||
ColourInheritable senderColour,
|
string senderName,
|
||||||
int senderRank,
|
ColourInheritable senderColour,
|
||||||
string senderNick,
|
int senderRank,
|
||||||
UserPermissions senderPerms,
|
string senderNick,
|
||||||
object? data = null,
|
UserPermissions senderPerms,
|
||||||
StoredEventFlags flags = StoredEventFlags.None
|
object? data = null,
|
||||||
);
|
StoredEventFlags flags = StoredEventFlags.None
|
||||||
void RemoveEvent(StoredEventInfo evt);
|
);
|
||||||
StoredEventInfo? GetEvent(long seqId);
|
void RemoveEvent(StoredEventInfo evt);
|
||||||
IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0);
|
StoredEventInfo? GetEvent(long seqId);
|
||||||
}
|
IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,130 +2,130 @@ using MySqlConnector;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public partial class MariaDBEventStorage(string connString) : EventStorage {
|
|
||||||
private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString));
|
|
||||||
|
|
||||||
public void AddEvent(
|
public partial class MariaDBEventStorage(string connString) : EventStorage {
|
||||||
long id,
|
private string ConnectionString { get; } = connString ?? throw new ArgumentNullException(nameof(connString));
|
||||||
string type,
|
|
||||||
string channelName,
|
public void AddEvent(
|
||||||
string senderId,
|
long id,
|
||||||
string senderName,
|
string type,
|
||||||
ColourInheritable senderColour,
|
string channelName,
|
||||||
int senderRank,
|
string senderId,
|
||||||
string senderNick,
|
string senderName,
|
||||||
UserPermissions senderPerms,
|
ColourInheritable senderColour,
|
||||||
object? data = null,
|
int senderRank,
|
||||||
StoredEventFlags flags = StoredEventFlags.None
|
string senderNick,
|
||||||
) {
|
UserPermissions senderPerms,
|
||||||
RunCommand(
|
object? data = null,
|
||||||
"INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`"
|
StoredEventFlags flags = StoredEventFlags.None
|
||||||
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)"
|
) {
|
||||||
+ " VALUES (@id, NOW(), @type, @target, @flags, @data"
|
RunCommand(
|
||||||
+ ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)",
|
"INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`"
|
||||||
new MySqlParameter("id", id),
|
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)"
|
||||||
new MySqlParameter("type", type),
|
+ " VALUES (@id, NOW(), @type, @target, @flags, @data"
|
||||||
new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName),
|
+ ", @sender, @sender_name, @sender_colour, @sender_rank, @sender_nick, @sender_perms)",
|
||||||
new MySqlParameter("flags", (byte)flags),
|
new MySqlParameter("id", id),
|
||||||
new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)),
|
new MySqlParameter("type", type),
|
||||||
new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null),
|
new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName),
|
||||||
new MySqlParameter("sender_name", senderName),
|
new MySqlParameter("flags", (byte)flags),
|
||||||
new MySqlParameter("sender_colour", senderColour.ToMisuzu()),
|
new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)),
|
||||||
new MySqlParameter("sender_rank", senderRank),
|
new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null),
|
||||||
new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick),
|
new MySqlParameter("sender_name", senderName),
|
||||||
new MySqlParameter("sender_perms", senderPerms)
|
new MySqlParameter("sender_colour", senderColour.ToMisuzu()),
|
||||||
|
new MySqlParameter("sender_rank", senderRank),
|
||||||
|
new MySqlParameter("sender_nick", string.IsNullOrWhiteSpace(senderNick) ? null : senderNick),
|
||||||
|
new MySqlParameter("sender_perms", senderPerms)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredEventInfo? GetEvent(long seqId) {
|
||||||
|
try {
|
||||||
|
using MySqlDataReader? reader = RunQuery(
|
||||||
|
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
||||||
|
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
||||||
|
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
||||||
|
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
|
||||||
|
+ " FROM `sqc_events`"
|
||||||
|
+ " WHERE `event_id` = @id",
|
||||||
|
new MySqlParameter("id", seqId)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
public StoredEventInfo? GetEvent(long seqId) {
|
if(reader is null)
|
||||||
try {
|
return null;
|
||||||
using MySqlDataReader? reader = RunQuery(
|
|
||||||
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
|
||||||
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
|
||||||
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
|
||||||
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
|
|
||||||
+ " FROM `sqc_events`"
|
|
||||||
+ " WHERE `event_id` = @id",
|
|
||||||
new MySqlParameter("id", seqId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if(reader is null)
|
while(reader.Read()) {
|
||||||
return null;
|
StoredEventInfo evt = ReadEvent(reader);
|
||||||
|
if(evt != null)
|
||||||
while(reader.Read()) {
|
return evt;
|
||||||
StoredEventInfo evt = ReadEvent(reader);
|
|
||||||
if(evt != null)
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
}
|
||||||
|
} catch(MySqlException ex) {
|
||||||
return null;
|
Logger.Write(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StoredEventInfo ReadEvent(MySqlDataReader reader) {
|
return null;
|
||||||
return new StoredEventInfo(
|
}
|
||||||
reader.GetInt64("event_id"),
|
|
||||||
Encoding.ASCII.GetString((byte[])reader["event_type"]),
|
private static StoredEventInfo ReadEvent(MySqlDataReader reader) {
|
||||||
reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User(
|
return new StoredEventInfo(
|
||||||
reader.GetInt64("event_sender").ToString(),
|
reader.GetInt64("event_id"),
|
||||||
reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"),
|
Encoding.ASCII.GetString((byte[])reader["event_type"]),
|
||||||
ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")),
|
reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User(
|
||||||
reader.GetInt32("event_sender_rank"),
|
reader.GetInt64("event_sender").ToString(),
|
||||||
(UserPermissions)reader.GetInt32("event_sender_perms"),
|
reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"),
|
||||||
reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick")
|
ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")),
|
||||||
),
|
reader.GetInt32("event_sender_rank"),
|
||||||
DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")),
|
(UserPermissions)reader.GetInt32("event_sender_perms"),
|
||||||
reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")),
|
reader.IsDBNull(reader.GetOrdinal("event_sender_nick")) ? string.Empty : reader.GetString("event_sender_nick")
|
||||||
reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]),
|
),
|
||||||
JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])),
|
DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_created")),
|
||||||
(StoredEventFlags)reader.GetByte("event_flags")
|
reader.IsDBNull(reader.GetOrdinal("event_deleted")) ? null : DateTimeOffset.FromUnixTimeSeconds(reader.GetInt32("event_deleted")),
|
||||||
|
reader.IsDBNull(reader.GetOrdinal("event_target")) ? null : Encoding.ASCII.GetString((byte[])reader["event_target"]),
|
||||||
|
JsonDocument.Parse(Encoding.ASCII.GetString((byte[])reader["event_data"])),
|
||||||
|
(StoredEventFlags)reader.GetByte("event_flags")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
|
||||||
|
List<StoredEventInfo> events = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
using MySqlDataReader? reader = RunQuery(
|
||||||
|
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
||||||
|
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
||||||
|
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
||||||
|
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
|
||||||
|
+ " FROM `sqc_events`"
|
||||||
|
+ " WHERE `event_deleted` IS NULL AND (`event_target` = @target OR `event_target` IS NULL)"
|
||||||
|
+ " AND `event_id` > @offset"
|
||||||
|
+ " ORDER BY `event_id` DESC"
|
||||||
|
+ " LIMIT @amount",
|
||||||
|
new MySqlParameter("target", channelName),
|
||||||
|
new MySqlParameter("amount", amount),
|
||||||
|
new MySqlParameter("offset", offset)
|
||||||
);
|
);
|
||||||
}
|
if(reader is null)
|
||||||
|
return events;
|
||||||
|
|
||||||
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
|
while(reader.Read()) {
|
||||||
List<StoredEventInfo> events = [];
|
StoredEventInfo evt = ReadEvent(reader);
|
||||||
|
if(evt != null)
|
||||||
try {
|
events.Add(evt);
|
||||||
using MySqlDataReader? reader = RunQuery(
|
|
||||||
"SELECT `event_id`, `event_type`, `event_flags`, `event_data`, `event_target`"
|
|
||||||
+ ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`"
|
|
||||||
+ ", UNIX_TIMESTAMP(`event_created`) AS `event_created`"
|
|
||||||
+ ", UNIX_TIMESTAMP(`event_deleted`) AS `event_deleted`"
|
|
||||||
+ " FROM `sqc_events`"
|
|
||||||
+ " WHERE `event_deleted` IS NULL AND (`event_target` = @target OR `event_target` IS NULL)"
|
|
||||||
+ " AND `event_id` > @offset"
|
|
||||||
+ " ORDER BY `event_id` DESC"
|
|
||||||
+ " LIMIT @amount",
|
|
||||||
new MySqlParameter("target", channelName),
|
|
||||||
new MySqlParameter("amount", amount),
|
|
||||||
new MySqlParameter("offset", offset)
|
|
||||||
);
|
|
||||||
if(reader is null)
|
|
||||||
return events;
|
|
||||||
|
|
||||||
while(reader.Read()) {
|
|
||||||
StoredEventInfo evt = ReadEvent(reader);
|
|
||||||
if(evt != null)
|
|
||||||
events.Add(evt);
|
|
||||||
}
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
}
|
||||||
|
} catch(MySqlException ex) {
|
||||||
events.Reverse();
|
Logger.Write(ex);
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveEvent(StoredEventInfo evt) {
|
events.Reverse();
|
||||||
ArgumentNullException.ThrowIfNull(evt);
|
|
||||||
RunCommand(
|
return events;
|
||||||
"UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL",
|
}
|
||||||
new MySqlParameter("id", evt.Id)
|
|
||||||
);
|
public void RemoveEvent(StoredEventInfo evt) {
|
||||||
}
|
ArgumentNullException.ThrowIfNull(evt);
|
||||||
|
RunCommand(
|
||||||
|
"UPDATE IGNORE `sqc_events` SET `event_deleted` = NOW() WHERE `event_id` = @id AND `event_deleted` IS NULL",
|
||||||
|
new MySqlParameter("id", evt.Id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,87 @@
|
||||||
using MySqlConnector;
|
using MySqlConnector;
|
||||||
using SharpChat.Configuration;
|
using SharpChat.Configuration;
|
||||||
|
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public partial class MariaDBEventStorage {
|
|
||||||
public static string BuildConnString(Configuration.Config config) {
|
public partial class MariaDBEventStorage {
|
||||||
return BuildConnString(
|
public static string BuildConnString(Configuration.Config config) {
|
||||||
config.ReadValue("host", "localhost"),
|
return BuildConnString(
|
||||||
config.ReadValue("user", string.Empty),
|
config.ReadValue("host", "localhost"),
|
||||||
config.ReadValue("pass", string.Empty),
|
config.ReadValue("user", string.Empty),
|
||||||
config.ReadValue("db", "sharpchat")
|
config.ReadValue("pass", string.Empty),
|
||||||
);
|
config.ReadValue("db", "sharpchat")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildConnString(string? host, string? username, string? password, string? database) {
|
||||||
|
return new MySqlConnectionStringBuilder {
|
||||||
|
Server = host,
|
||||||
|
UserID = username,
|
||||||
|
Password = password,
|
||||||
|
Database = database,
|
||||||
|
OldGuids = false,
|
||||||
|
TreatTinyAsBoolean = false,
|
||||||
|
CharacterSet = "utf8mb4",
|
||||||
|
SslMode = MySqlSslMode.None,
|
||||||
|
ForceSynchronous = true,
|
||||||
|
ConnectionTimeout = 5,
|
||||||
|
DefaultCommandTimeout = 900, // fuck it, 15 minutes
|
||||||
|
}.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MySqlConnection GetConnection() {
|
||||||
|
MySqlConnection conn = new(ConnectionString);
|
||||||
|
conn.Open();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RunCommand(string command, params MySqlParameter[] parameters) {
|
||||||
|
try {
|
||||||
|
using MySqlConnection conn = GetConnection();
|
||||||
|
using MySqlCommand cmd = conn.CreateCommand();
|
||||||
|
if(parameters?.Length > 0)
|
||||||
|
cmd.Parameters.AddRange(parameters);
|
||||||
|
cmd.CommandText = command;
|
||||||
|
return cmd.ExecuteNonQuery();
|
||||||
|
} catch(MySqlException ex) {
|
||||||
|
Logger.Write(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string BuildConnString(string? host, string? username, string? password, string? database) {
|
return 0;
|
||||||
return new MySqlConnectionStringBuilder {
|
}
|
||||||
Server = host,
|
|
||||||
UserID = username,
|
private MySqlDataReader? RunQuery(string command, params MySqlParameter[] parameters) {
|
||||||
Password = password,
|
try {
|
||||||
Database = database,
|
MySqlConnection conn = GetConnection();
|
||||||
OldGuids = false,
|
MySqlCommand cmd = conn.CreateCommand();
|
||||||
TreatTinyAsBoolean = false,
|
if(parameters?.Length > 0)
|
||||||
CharacterSet = "utf8mb4",
|
cmd.Parameters.AddRange(parameters);
|
||||||
SslMode = MySqlSslMode.None,
|
cmd.CommandText = command;
|
||||||
ForceSynchronous = true,
|
return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
|
||||||
ConnectionTimeout = 5,
|
} catch(MySqlException ex) {
|
||||||
DefaultCommandTimeout = 900, // fuck it, 15 minutes
|
Logger.Write(ex);
|
||||||
}.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MySqlConnection GetConnection() {
|
return null;
|
||||||
MySqlConnection conn = new(ConnectionString);
|
}
|
||||||
conn.Open();
|
|
||||||
return conn;
|
private T RunQueryValue<T>(string command, params MySqlParameter[] parameters)
|
||||||
|
where T : struct {
|
||||||
|
try {
|
||||||
|
using MySqlConnection conn = GetConnection();
|
||||||
|
using MySqlCommand cmd = conn.CreateCommand();
|
||||||
|
if(parameters?.Length > 0)
|
||||||
|
cmd.Parameters.AddRange(parameters);
|
||||||
|
cmd.CommandText = command;
|
||||||
|
cmd.Prepare();
|
||||||
|
|
||||||
|
object? raw = cmd.ExecuteScalar();
|
||||||
|
if(raw is T value)
|
||||||
|
return value;
|
||||||
|
} catch(MySqlException ex) {
|
||||||
|
Logger.Write(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int RunCommand(string command, params MySqlParameter[] parameters) {
|
return default;
|
||||||
try {
|
|
||||||
using MySqlConnection conn = GetConnection();
|
|
||||||
using MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
return cmd.ExecuteNonQuery();
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MySqlDataReader? RunQuery(string command, params MySqlParameter[] parameters) {
|
|
||||||
try {
|
|
||||||
MySqlConnection conn = GetConnection();
|
|
||||||
MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private T RunQueryValue<T>(string command, params MySqlParameter[] parameters)
|
|
||||||
where T : struct {
|
|
||||||
try {
|
|
||||||
using MySqlConnection conn = GetConnection();
|
|
||||||
using MySqlCommand cmd = conn.CreateCommand();
|
|
||||||
if(parameters?.Length > 0)
|
|
||||||
cmd.Parameters.AddRange(parameters);
|
|
||||||
cmd.CommandText = command;
|
|
||||||
cmd.Prepare();
|
|
||||||
|
|
||||||
object? raw = cmd.ExecuteScalar();
|
|
||||||
if(raw is T value)
|
|
||||||
return value;
|
|
||||||
} catch(MySqlException ex) {
|
|
||||||
Logger.Write(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +1,84 @@
|
||||||
using MySqlConnector;
|
using MySqlConnector;
|
||||||
|
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public partial class MariaDBEventStorage {
|
|
||||||
private void DoMigration(string name, Action action) {
|
public partial class MariaDBEventStorage {
|
||||||
bool done = RunQueryValue<long>(
|
private void DoMigration(string name, Action action) {
|
||||||
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
|
bool done = RunQueryValue<long>(
|
||||||
|
"SELECT COUNT(*) FROM `sqc_migrations` WHERE `migration_name` = @name",
|
||||||
|
new MySqlParameter("name", name)
|
||||||
|
) > 0;
|
||||||
|
if(!done) {
|
||||||
|
Logger.Write($"Running migration '{name}'...");
|
||||||
|
action();
|
||||||
|
RunCommand(
|
||||||
|
"INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)",
|
||||||
new MySqlParameter("name", name)
|
new MySqlParameter("name", name)
|
||||||
) > 0;
|
|
||||||
if(!done) {
|
|
||||||
Logger.Write($"Running migration '{name}'...");
|
|
||||||
action();
|
|
||||||
RunCommand(
|
|
||||||
"INSERT INTO `sqc_migrations` (`migration_name`) VALUES (@name)",
|
|
||||||
new MySqlParameter("name", name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunMigrations() {
|
|
||||||
RunCommand(
|
|
||||||
"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
|
|
||||||
+ "`migration_name` VARCHAR(255) NOT NULL,"
|
|
||||||
+ "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
|
|
||||||
+ "UNIQUE INDEX `migration_name` (`migration_name`),"
|
|
||||||
+ "INDEX `migration_completed` (`migration_completed`)"
|
|
||||||
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
|
|
||||||
);
|
|
||||||
|
|
||||||
DoMigration("create_events_table", CreateEventsTable);
|
|
||||||
DoMigration("allow_null_target", AllowNullTarget);
|
|
||||||
DoMigration("event_data_as_medium_blob", EventDataAsMediumBlob);
|
|
||||||
DoMigration("event_user_and_nick_name_to_1000", EventUserAndNickNameTo1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EventUserAndNickNameTo1000() {
|
|
||||||
RunCommand(
|
|
||||||
"ALTER TABLE `sqc_events`"
|
|
||||||
+ " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`,"
|
|
||||||
+ " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`;"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EventDataAsMediumBlob() {
|
|
||||||
RunCommand(
|
|
||||||
"ALTER TABLE `sqc_events`"
|
|
||||||
+ " CHANGE COLUMN `event_data` `event_data` MEDIUMBLOB NULL DEFAULT NULL AFTER `event_flags`;"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AllowNullTarget() {
|
|
||||||
RunCommand(
|
|
||||||
"ALTER TABLE `sqc_events`"
|
|
||||||
+ " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateEventsTable() {
|
|
||||||
RunCommand(
|
|
||||||
"CREATE TABLE `sqc_events` ("
|
|
||||||
+ "`event_id` BIGINT(20) NOT NULL,"
|
|
||||||
+ "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL,"
|
|
||||||
+ "`event_sender_name` VARCHAR(255) NULL DEFAULT NULL,"
|
|
||||||
+ "`event_sender_colour` INT(11) NULL DEFAULT NULL,"
|
|
||||||
+ "`event_sender_rank` INT(11) NULL DEFAULT NULL,"
|
|
||||||
+ "`event_sender_nick` VARCHAR(255) NULL DEFAULT NULL,"
|
|
||||||
+ "`event_sender_perms` INT(11) NULL DEFAULT NULL,"
|
|
||||||
+ "`event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
|
|
||||||
+ "`event_deleted` TIMESTAMP NULL DEFAULT NULL,"
|
|
||||||
+ "`event_type` VARBINARY(255) NOT NULL,"
|
|
||||||
+ "`event_target` VARBINARY(255) NOT NULL,"
|
|
||||||
+ "`event_flags` TINYINT(3) UNSIGNED NOT NULL,"
|
|
||||||
+ "`event_data` BLOB NULL DEFAULT NULL,"
|
|
||||||
+ "PRIMARY KEY (`event_id`),"
|
|
||||||
+ "INDEX `event_target` (`event_target`),"
|
|
||||||
+ "INDEX `event_type` (`event_type`),"
|
|
||||||
+ "INDEX `event_sender` (`event_sender`),"
|
|
||||||
+ "INDEX `event_datetime` (`event_created`),"
|
|
||||||
+ "INDEX `event_deleted` (`event_deleted`)"
|
|
||||||
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RunMigrations() {
|
||||||
|
RunCommand(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `sqc_migrations` ("
|
||||||
|
+ "`migration_name` VARCHAR(255) NOT NULL,"
|
||||||
|
+ "`migration_completed` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
|
||||||
|
+ "UNIQUE INDEX `migration_name` (`migration_name`),"
|
||||||
|
+ "INDEX `migration_completed` (`migration_completed`)"
|
||||||
|
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
|
||||||
|
);
|
||||||
|
|
||||||
|
DoMigration("create_events_table", CreateEventsTable);
|
||||||
|
DoMigration("allow_null_target", AllowNullTarget);
|
||||||
|
DoMigration("event_data_as_medium_blob", EventDataAsMediumBlob);
|
||||||
|
DoMigration("event_user_and_nick_name_to_1000", EventUserAndNickNameTo1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EventUserAndNickNameTo1000() {
|
||||||
|
RunCommand(
|
||||||
|
"ALTER TABLE `sqc_events`"
|
||||||
|
+ " CHANGE COLUMN `event_sender_name` `event_sender_name` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender`,"
|
||||||
|
+ " CHANGE COLUMN `event_sender_nick` `event_sender_nick` VARCHAR(1000) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_520_ci' AFTER `event_sender_rank`;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EventDataAsMediumBlob() {
|
||||||
|
RunCommand(
|
||||||
|
"ALTER TABLE `sqc_events`"
|
||||||
|
+ " CHANGE COLUMN `event_data` `event_data` MEDIUMBLOB NULL DEFAULT NULL AFTER `event_flags`;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AllowNullTarget() {
|
||||||
|
RunCommand(
|
||||||
|
"ALTER TABLE `sqc_events`"
|
||||||
|
+ " CHANGE COLUMN `event_target` `event_target` VARBINARY(255) NULL AFTER `event_type`;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateEventsTable() {
|
||||||
|
RunCommand(
|
||||||
|
"CREATE TABLE `sqc_events` ("
|
||||||
|
+ "`event_id` BIGINT(20) NOT NULL,"
|
||||||
|
+ "`event_sender` BIGINT(20) UNSIGNED NULL DEFAULT NULL,"
|
||||||
|
+ "`event_sender_name` VARCHAR(255) NULL DEFAULT NULL,"
|
||||||
|
+ "`event_sender_colour` INT(11) NULL DEFAULT NULL,"
|
||||||
|
+ "`event_sender_rank` INT(11) NULL DEFAULT NULL,"
|
||||||
|
+ "`event_sender_nick` VARCHAR(255) NULL DEFAULT NULL,"
|
||||||
|
+ "`event_sender_perms` INT(11) NULL DEFAULT NULL,"
|
||||||
|
+ "`event_created` TIMESTAMP NOT NULL DEFAULT current_timestamp(),"
|
||||||
|
+ "`event_deleted` TIMESTAMP NULL DEFAULT NULL,"
|
||||||
|
+ "`event_type` VARBINARY(255) NOT NULL,"
|
||||||
|
+ "`event_target` VARBINARY(255) NOT NULL,"
|
||||||
|
+ "`event_flags` TINYINT(3) UNSIGNED NOT NULL,"
|
||||||
|
+ "`event_data` BLOB NULL DEFAULT NULL,"
|
||||||
|
+ "PRIMARY KEY (`event_id`),"
|
||||||
|
+ "INDEX `event_target` (`event_target`),"
|
||||||
|
+ "INDEX `event_type` (`event_type`),"
|
||||||
|
+ "INDEX `event_sender` (`event_sender`),"
|
||||||
|
+ "INDEX `event_datetime` (`event_created`),"
|
||||||
|
+ "INDEX `event_deleted` (`event_deleted`)"
|
||||||
|
+ ") COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB;"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
[Flags]
|
|
||||||
public enum StoredEventFlags {
|
[Flags]
|
||||||
None = 0,
|
public enum StoredEventFlags {
|
||||||
Action = 1,
|
None = 0,
|
||||||
Broadcast = 1 << 1,
|
Action = 1,
|
||||||
Log = 1 << 2,
|
Broadcast = 1 << 1,
|
||||||
Private = 1 << 3,
|
Log = 1 << 2,
|
||||||
}
|
Private = 1 << 3,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public class StoredEventInfo(
|
|
||||||
long id,
|
public class StoredEventInfo(
|
||||||
string type,
|
long id,
|
||||||
User? sender,
|
string type,
|
||||||
DateTimeOffset created,
|
User? sender,
|
||||||
DateTimeOffset? deleted,
|
DateTimeOffset created,
|
||||||
string? channelName,
|
DateTimeOffset? deleted,
|
||||||
JsonDocument data,
|
string? channelName,
|
||||||
StoredEventFlags flags
|
JsonDocument data,
|
||||||
) {
|
StoredEventFlags flags
|
||||||
public long Id { get; set; } = id;
|
) {
|
||||||
public string Type { get; set; } = type;
|
public long Id { get; set; } = id;
|
||||||
public User? Sender { get; set; } = sender;
|
public string Type { get; set; } = type;
|
||||||
public DateTimeOffset Created { get; set; } = created;
|
public User? Sender { get; set; } = sender;
|
||||||
public DateTimeOffset? Deleted { get; set; } = deleted;
|
public DateTimeOffset Created { get; set; } = created;
|
||||||
public string? ChannelName { get; set; } = channelName;
|
public DateTimeOffset? Deleted { get; set; } = deleted;
|
||||||
public StoredEventFlags Flags { get; set; } = flags;
|
public string? ChannelName { get; set; } = channelName;
|
||||||
public JsonDocument Data { get; set; } = data;
|
public StoredEventFlags Flags { get; set; } = flags;
|
||||||
}
|
public JsonDocument Data { get; set; } = data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,65 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpChat.EventStorage {
|
namespace SharpChat.EventStorage;
|
||||||
public class VirtualEventStorage : EventStorage {
|
|
||||||
private readonly Dictionary<long, StoredEventInfo> Events = [];
|
|
||||||
|
|
||||||
public void AddEvent(
|
public class VirtualEventStorage : EventStorage {
|
||||||
long id,
|
private readonly Dictionary<long, StoredEventInfo> Events = [];
|
||||||
string type,
|
|
||||||
string channelName,
|
public void AddEvent(
|
||||||
string senderId,
|
long id,
|
||||||
string senderName,
|
string type,
|
||||||
ColourInheritable senderColour,
|
string channelName,
|
||||||
int senderRank,
|
string senderId,
|
||||||
string senderNick,
|
string senderName,
|
||||||
UserPermissions senderPerms,
|
ColourInheritable senderColour,
|
||||||
object? data = null,
|
int senderRank,
|
||||||
StoredEventFlags flags = StoredEventFlags.None
|
string senderNick,
|
||||||
) {
|
UserPermissions senderPerms,
|
||||||
Events.Add(
|
object? data = null,
|
||||||
|
StoredEventFlags flags = StoredEventFlags.None
|
||||||
|
) {
|
||||||
|
Events.Add(
|
||||||
|
id,
|
||||||
|
new(
|
||||||
id,
|
id,
|
||||||
new(
|
type,
|
||||||
id,
|
long.TryParse(senderId, out long senderId64) && senderId64 > 0
|
||||||
type,
|
? new User(
|
||||||
long.TryParse(senderId, out long senderId64) && senderId64 > 0
|
senderId,
|
||||||
? new User(
|
senderName,
|
||||||
senderId,
|
senderColour,
|
||||||
senderName,
|
senderRank,
|
||||||
senderColour,
|
senderPerms,
|
||||||
senderRank,
|
senderNick
|
||||||
senderPerms,
|
)
|
||||||
senderNick
|
: null,
|
||||||
)
|
DateTimeOffset.Now,
|
||||||
: null,
|
null,
|
||||||
DateTimeOffset.Now,
|
channelName,
|
||||||
null,
|
JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)),
|
||||||
channelName,
|
flags
|
||||||
JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)),
|
)
|
||||||
flags
|
);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
public StoredEventInfo? GetEvent(long seqId) {
|
||||||
|
return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveEvent(StoredEventInfo evt) {
|
||||||
|
ArgumentNullException.ThrowIfNull(evt);
|
||||||
|
Events.Remove(evt.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
|
||||||
|
IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
|
||||||
|
|
||||||
|
int start = subset.Count() - offset - amount;
|
||||||
|
if(start < 0) {
|
||||||
|
amount += start;
|
||||||
|
start = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoredEventInfo? GetEvent(long seqId) {
|
return [.. subset.Skip(start).Take(amount)];
|
||||||
return Events.TryGetValue(seqId, out StoredEventInfo? evt) ? evt : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveEvent(StoredEventInfo evt) {
|
|
||||||
ArgumentNullException.ThrowIfNull(evt);
|
|
||||||
Events.Remove(evt.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<StoredEventInfo> GetChannelEventLog(string channelName, int amount = 20, int offset = 0) {
|
|
||||||
IEnumerable<StoredEventInfo> subset = Events.Values.Where(ev => ev.ChannelName == channelName);
|
|
||||||
|
|
||||||
int start = subset.Count() - offset - amount;
|
|
||||||
if(start < 0) {
|
|
||||||
amount += start;
|
|
||||||
start = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [.. subset.Skip(start).Take(amount)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
namespace SharpChat.Events {
|
namespace SharpChat.Events;
|
||||||
public interface ChatEvent {}
|
|
||||||
}
|
public interface ChatEvent {}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
namespace SharpChat.Events {
|
namespace SharpChat.Events;
|
||||||
public class MessageCreateEvent(
|
|
||||||
long msgId,
|
public class MessageCreateEvent(
|
||||||
string channelName,
|
long msgId,
|
||||||
string senderId,
|
string channelName,
|
||||||
string senderName,
|
string senderId,
|
||||||
ColourInheritable senderColour,
|
string senderName,
|
||||||
int senderRank,
|
ColourInheritable senderColour,
|
||||||
string senderNickName,
|
int senderRank,
|
||||||
UserPermissions senderPerms,
|
string senderNickName,
|
||||||
DateTimeOffset msgCreated,
|
UserPermissions senderPerms,
|
||||||
string msgText,
|
DateTimeOffset msgCreated,
|
||||||
bool isPrivate,
|
string msgText,
|
||||||
bool isAction,
|
bool isPrivate,
|
||||||
bool isBroadcast
|
bool isAction,
|
||||||
) : ChatEvent {
|
bool isBroadcast
|
||||||
public long MessageId { get; } = msgId;
|
) : ChatEvent {
|
||||||
public string ChannelName { get; } = channelName;
|
public long MessageId { get; } = msgId;
|
||||||
public string SenderId { get; } = senderId;
|
public string ChannelName { get; } = channelName;
|
||||||
public string SenderName { get; } = senderName;
|
public string SenderId { get; } = senderId;
|
||||||
public ColourInheritable SenderColour { get; } = senderColour;
|
public string SenderName { get; } = senderName;
|
||||||
public int SenderRank { get; } = senderRank;
|
public ColourInheritable SenderColour { get; } = senderColour;
|
||||||
public string SenderNickName { get; } = senderNickName;
|
public int SenderRank { get; } = senderRank;
|
||||||
public UserPermissions SenderPerms { get; } = senderPerms;
|
public string SenderNickName { get; } = senderNickName;
|
||||||
public DateTimeOffset MessageCreated { get; } = msgCreated;
|
public UserPermissions SenderPerms { get; } = senderPerms;
|
||||||
public string MessageText { get; } = msgText;
|
public DateTimeOffset MessageCreated { get; } = msgCreated;
|
||||||
public bool IsPrivate { get; } = isPrivate;
|
public string MessageText { get; } = msgText;
|
||||||
public bool IsAction { get; } = isAction;
|
public bool IsPrivate { get; } = isPrivate;
|
||||||
public bool IsBroadcast { get; } = isBroadcast;
|
public bool IsAction { get; } = isAction;
|
||||||
}
|
public bool IsBroadcast { get; } = isBroadcast;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,145 +3,145 @@ using SharpChat.EventStorage;
|
||||||
using SharpChat.Flashii;
|
using SharpChat.Flashii;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class Program {
|
|
||||||
public const string CONFIG = "sharpchat.cfg";
|
|
||||||
|
|
||||||
public static void Main() {
|
public class Program {
|
||||||
Console.WriteLine(@" _____ __ ________ __ ");
|
public const string CONFIG = "sharpchat.cfg";
|
||||||
Console.WriteLine(@" / ___// /_ ____ __________ / ____/ /_ ____ _/ /_");
|
|
||||||
Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/");
|
|
||||||
Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ ");
|
|
||||||
Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ ");
|
|
||||||
/**/Console.Write(@" /__/");
|
|
||||||
if(SharpInfo.IsDebugBuild) {
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.Write(@"== ");
|
|
||||||
Console.Write(SharpInfo.VersionString);
|
|
||||||
Console.WriteLine(@" == DBG ==");
|
|
||||||
} else
|
|
||||||
Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' '));
|
|
||||||
|
|
||||||
using ManualResetEvent mre = new(false);
|
public static void Main() {
|
||||||
bool hasCancelled = false;
|
Console.WriteLine(@" _____ __ ________ __ ");
|
||||||
|
Console.WriteLine(@" / ___// /_ ____ __________ / ____/ /_ ____ _/ /_");
|
||||||
|
Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/");
|
||||||
|
Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ ");
|
||||||
|
Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ ");
|
||||||
|
/**/Console.Write(@" /__/");
|
||||||
|
if(SharpInfo.IsDebugBuild) {
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Write(@"== ");
|
||||||
|
Console.Write(SharpInfo.VersionString);
|
||||||
|
Console.WriteLine(@" == DBG ==");
|
||||||
|
} else
|
||||||
|
Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' '));
|
||||||
|
|
||||||
void cancelKeyPressHandler(object? sender, ConsoleCancelEventArgs ev) {
|
using ManualResetEvent mre = new(false);
|
||||||
Console.CancelKeyPress -= cancelKeyPressHandler;
|
bool hasCancelled = false;
|
||||||
hasCancelled = true;
|
|
||||||
ev.Cancel = true;
|
|
||||||
mre.Set();
|
|
||||||
};
|
|
||||||
Console.CancelKeyPress += cancelKeyPressHandler;
|
|
||||||
|
|
||||||
if(hasCancelled) return;
|
void cancelKeyPressHandler(object? sender, ConsoleCancelEventArgs ev) {
|
||||||
|
Console.CancelKeyPress -= cancelKeyPressHandler;
|
||||||
|
hasCancelled = true;
|
||||||
|
ev.Cancel = true;
|
||||||
|
mre.Set();
|
||||||
|
};
|
||||||
|
Console.CancelKeyPress += cancelKeyPressHandler;
|
||||||
|
|
||||||
string configFile = CONFIG;
|
if(hasCancelled) return;
|
||||||
|
|
||||||
// If the config file doesn't exist and we're using the default path, run the converter
|
string configFile = CONFIG;
|
||||||
if(!File.Exists(configFile) && configFile == CONFIG)
|
|
||||||
ConvertConfiguration();
|
|
||||||
|
|
||||||
using StreamConfig config = StreamConfig.FromPath(configFile);
|
// If the config file doesn't exist and we're using the default path, run the converter
|
||||||
|
if(!File.Exists(configFile) && configFile == CONFIG)
|
||||||
|
ConvertConfiguration();
|
||||||
|
|
||||||
if(hasCancelled) return;
|
using StreamConfig config = StreamConfig.FromPath(configFile);
|
||||||
|
|
||||||
using HttpClient httpClient = new(new HttpClientHandler() {
|
if(hasCancelled) return;
|
||||||
UseProxy = false,
|
|
||||||
});
|
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName);
|
|
||||||
|
|
||||||
if(hasCancelled) return;
|
using HttpClient httpClient = new(new HttpClientHandler() {
|
||||||
|
UseProxy = false,
|
||||||
|
});
|
||||||
|
httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName);
|
||||||
|
|
||||||
FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
|
if(hasCancelled) return;
|
||||||
|
|
||||||
if(hasCancelled) return;
|
FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
|
||||||
|
|
||||||
EventStorage.EventStorage evtStore;
|
if(hasCancelled) return;
|
||||||
if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) {
|
|
||||||
evtStore = new VirtualEventStorage();
|
|
||||||
} else {
|
|
||||||
MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb")));
|
|
||||||
evtStore = mdbes;
|
|
||||||
mdbes.RunMigrations();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(hasCancelled) return;
|
EventStorage.EventStorage evtStore;
|
||||||
|
if(string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))) {
|
||||||
using SockChatServer scs = new(flashii, flashii, evtStore, config.ScopeTo("chat"));
|
evtStore = new VirtualEventStorage();
|
||||||
scs.Listen(mre);
|
} else {
|
||||||
|
MariaDBEventStorage mdbes = new(MariaDBEventStorage.BuildConnString(config.ScopeTo("mariadb")));
|
||||||
mre.WaitOne();
|
evtStore = mdbes;
|
||||||
|
mdbes.RunMigrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConvertConfiguration() {
|
if(hasCancelled) return;
|
||||||
using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
|
||||||
s.SetLength(0);
|
|
||||||
s.Flush();
|
|
||||||
|
|
||||||
using StreamWriter sw = new(s, new UTF8Encoding(false));
|
using SockChatServer scs = new(flashii, flashii, evtStore, config.ScopeTo("chat"));
|
||||||
sw.WriteLine("# and ; can be used at the start of a line for comments.");
|
scs.Listen(mre);
|
||||||
sw.WriteLine();
|
|
||||||
|
|
||||||
sw.WriteLine("# General Configuration");
|
mre.WaitOne();
|
||||||
sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}");
|
}
|
||||||
sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}");
|
|
||||||
sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}");
|
private static void ConvertConfiguration() {
|
||||||
sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}");
|
using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
s.SetLength(0);
|
||||||
|
s.Flush();
|
||||||
|
|
||||||
|
using StreamWriter sw = new(s, new UTF8Encoding(false));
|
||||||
|
sw.WriteLine("# and ; can be used at the start of a line for comments.");
|
||||||
|
sw.WriteLine();
|
||||||
|
|
||||||
|
sw.WriteLine("# General Configuration");
|
||||||
|
sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}");
|
||||||
|
sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}");
|
||||||
|
sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}");
|
||||||
|
sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}");
|
||||||
|
|
||||||
|
|
||||||
sw.WriteLine();
|
sw.WriteLine();
|
||||||
sw.WriteLine("# Channels");
|
sw.WriteLine("# Channels");
|
||||||
sw.WriteLine("chat:channels lounge staff");
|
sw.WriteLine("chat:channels lounge staff");
|
||||||
sw.WriteLine();
|
sw.WriteLine();
|
||||||
|
|
||||||
sw.WriteLine("# Lounge channel settings");
|
sw.WriteLine("# Lounge channel settings");
|
||||||
sw.WriteLine("chat:channels:lounge:name Lounge");
|
sw.WriteLine("chat:channels:lounge:name Lounge");
|
||||||
sw.WriteLine("chat:channels:lounge:autoJoin true");
|
sw.WriteLine("chat:channels:lounge:autoJoin true");
|
||||||
sw.WriteLine();
|
sw.WriteLine();
|
||||||
|
|
||||||
sw.WriteLine("# Staff channel settings");
|
sw.WriteLine("# Staff channel settings");
|
||||||
sw.WriteLine("chat:channels:staff:name Staff");
|
sw.WriteLine("chat:channels:staff:name Staff");
|
||||||
sw.WriteLine("chat:channels:staff:minRank 5");
|
sw.WriteLine("chat:channels:staff:minRank 5");
|
||||||
|
|
||||||
|
|
||||||
const string msz_secret = "login_key.txt";
|
const string msz_secret = "login_key.txt";
|
||||||
const string msz_url = "msz_url.txt";
|
const string msz_url = "msz_url.txt";
|
||||||
|
|
||||||
sw.WriteLine();
|
sw.WriteLine();
|
||||||
sw.WriteLine("# Misuzu integration settings");
|
sw.WriteLine("# Misuzu integration settings");
|
||||||
if(File.Exists(msz_secret))
|
if(File.Exists(msz_secret))
|
||||||
sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim()));
|
sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim()));
|
||||||
else
|
else
|
||||||
sw.WriteLine("#msz:secret woomy");
|
sw.WriteLine("#msz:secret woomy");
|
||||||
if(File.Exists(msz_url))
|
if(File.Exists(msz_url))
|
||||||
sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim()));
|
sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim()));
|
||||||
else
|
else
|
||||||
sw.WriteLine("#msz:url https://flashii.net/_sockchat");
|
sw.WriteLine("#msz:url https://flashii.net/_sockchat");
|
||||||
|
|
||||||
|
|
||||||
const string mdb_config = @"mariadb.txt";
|
const string mdb_config = @"mariadb.txt";
|
||||||
string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : [];
|
string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : [];
|
||||||
|
|
||||||
sw.WriteLine();
|
sw.WriteLine();
|
||||||
sw.WriteLine("# MariaDB configuration");
|
sw.WriteLine("# MariaDB configuration");
|
||||||
if(mdbCfg.Length > 0)
|
if(mdbCfg.Length > 0)
|
||||||
sw.WriteLine($"mariadb:host {mdbCfg[0]}");
|
sw.WriteLine($"mariadb:host {mdbCfg[0]}");
|
||||||
else
|
else
|
||||||
sw.WriteLine($"#mariadb:host <username>");
|
sw.WriteLine($"#mariadb:host <username>");
|
||||||
if(mdbCfg.Length > 1)
|
if(mdbCfg.Length > 1)
|
||||||
sw.WriteLine($"mariadb:user {mdbCfg[1]}");
|
sw.WriteLine($"mariadb:user {mdbCfg[1]}");
|
||||||
else
|
else
|
||||||
sw.WriteLine($"#mariadb:user <username>");
|
sw.WriteLine($"#mariadb:user <username>");
|
||||||
if(mdbCfg.Length > 2)
|
if(mdbCfg.Length > 2)
|
||||||
sw.WriteLine($"mariadb:pass {mdbCfg[2]}");
|
sw.WriteLine($"mariadb:pass {mdbCfg[2]}");
|
||||||
else
|
else
|
||||||
sw.WriteLine($"#mariadb:pass <password>");
|
sw.WriteLine($"#mariadb:pass <password>");
|
||||||
if(mdbCfg.Length > 3)
|
if(mdbCfg.Length > 3)
|
||||||
sw.WriteLine($"mariadb:db {mdbCfg[3]}");
|
sw.WriteLine($"mariadb:db {mdbCfg[3]}");
|
||||||
else
|
else
|
||||||
sw.WriteLine($"#mariadb:db <database>");
|
sw.WriteLine($"#mariadb:db <database>");
|
||||||
|
|
||||||
sw.Flush();
|
sw.Flush();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,156 +12,156 @@ using System.Text;
|
||||||
// Fleck's Socket wrapper doesn't provide any way to do this with the normally provided APIs
|
// Fleck's Socket wrapper doesn't provide any way to do this with the normally provided APIs
|
||||||
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
|
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class SharpChatWebSocketServer : IWebSocketServer {
|
|
||||||
|
|
||||||
private readonly string _scheme;
|
public class SharpChatWebSocketServer : IWebSocketServer {
|
||||||
private readonly IPAddress _locationIP;
|
|
||||||
private Action<IWebSocketConnection> _config;
|
|
||||||
|
|
||||||
public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
|
private readonly string _scheme;
|
||||||
Uri uri = new(location);
|
private readonly IPAddress _locationIP;
|
||||||
|
private Action<IWebSocketConnection> _config;
|
||||||
|
|
||||||
Port = uri.Port;
|
public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
|
||||||
Location = location;
|
Uri uri = new(location);
|
||||||
SupportDualStack = supportDualStack;
|
|
||||||
|
|
||||||
_locationIP = ParseIPAddress(uri);
|
Port = uri.Port;
|
||||||
_scheme = uri.Scheme;
|
Location = location;
|
||||||
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
|
SupportDualStack = supportDualStack;
|
||||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
|
|
||||||
|
|
||||||
if(SupportDualStack && Type.GetType("Mono.Runtime") == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
_locationIP = ParseIPAddress(uri);
|
||||||
socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
|
_scheme = uri.Scheme;
|
||||||
}
|
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
|
||||||
|
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
|
||||||
|
|
||||||
ListenerSocket = new SocketWrapper(socket);
|
if(SupportDualStack && Type.GetType("Mono.Runtime") == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||||
SupportedSubProtocols = [];
|
socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ISocket ListenerSocket { get; set; }
|
ListenerSocket = new SocketWrapper(socket);
|
||||||
public string Location { get; private set; }
|
SupportedSubProtocols = [];
|
||||||
public bool SupportDualStack { get; }
|
}
|
||||||
public int Port { get; private set; }
|
|
||||||
public X509Certificate2 Certificate { get; set; }
|
|
||||||
public SslProtocols EnabledSslProtocols { get; set; }
|
|
||||||
public IEnumerable<string> SupportedSubProtocols { get; set; }
|
|
||||||
public bool RestartAfterListenError { get; set; }
|
|
||||||
|
|
||||||
public bool IsSecure {
|
public ISocket ListenerSocket { get; set; }
|
||||||
get { return _scheme == "wss" && Certificate != null; }
|
public string Location { get; private set; }
|
||||||
}
|
public bool SupportDualStack { get; }
|
||||||
|
public int Port { get; private set; }
|
||||||
|
public X509Certificate2 Certificate { get; set; }
|
||||||
|
public SslProtocols EnabledSslProtocols { get; set; }
|
||||||
|
public IEnumerable<string> SupportedSubProtocols { get; set; }
|
||||||
|
public bool RestartAfterListenError { get; set; }
|
||||||
|
|
||||||
public void Dispose() {
|
public bool IsSecure {
|
||||||
ListenerSocket.Dispose();
|
get { return _scheme == "wss" && Certificate != null; }
|
||||||
GC.SuppressFinalize(this);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static IPAddress ParseIPAddress(Uri uri) {
|
public void Dispose() {
|
||||||
string ipStr = uri.Host;
|
ListenerSocket.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
if(ipStr == "0.0.0.0") {
|
private static IPAddress ParseIPAddress(Uri uri) {
|
||||||
return IPAddress.Any;
|
string ipStr = uri.Host;
|
||||||
} else if(ipStr == "[0000:0000:0000:0000:0000:0000:0000:0000]") {
|
|
||||||
return IPAddress.IPv6Any;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return IPAddress.Parse(ipStr);
|
|
||||||
} catch(Exception ex) {
|
|
||||||
throw new FormatException("Failed to parse the IP address part of the location. Please make sure you specify a valid IP address. Use 0.0.0.0 or [::] to listen on all interfaces.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start(Action<IWebSocketConnection> config) {
|
if(ipStr == "0.0.0.0") {
|
||||||
IPEndPoint ipLocal = new(_locationIP, Port);
|
return IPAddress.Any;
|
||||||
ListenerSocket.Bind(ipLocal);
|
} else if(ipStr == "[0000:0000:0000:0000:0000:0000:0000:0000]") {
|
||||||
ListenerSocket.Listen(100);
|
return IPAddress.IPv6Any;
|
||||||
Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port;
|
} else {
|
||||||
FleckLog.Info(string.Format("Server started at {0} (actual port {1})", Location, Port));
|
try {
|
||||||
if(_scheme == "wss") {
|
return IPAddress.Parse(ipStr);
|
||||||
if(Certificate == null) {
|
} catch(Exception ex) {
|
||||||
FleckLog.Error("Scheme cannot be 'wss' without a Certificate");
|
throw new FormatException("Failed to parse the IP address part of the location. Please make sure you specify a valid IP address. Use 0.0.0.0 or [::] to listen on all interfaces.", ex);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// makes dotnet shut up, TLS is handled by NGINX anyway
|
|
||||||
// if(EnabledSslProtocols == SslProtocols.None) {
|
|
||||||
// EnabledSslProtocols = SslProtocols.Tls;
|
|
||||||
// FleckLog.Debug("Using default TLS 1.0 security protocol.");
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
ListenForClients();
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ListenForClients() {
|
|
||||||
ListenerSocket.Accept(OnClientConnect, e => {
|
|
||||||
FleckLog.Error("Listener socket is closed", e);
|
|
||||||
if(RestartAfterListenError) {
|
|
||||||
FleckLog.Info("Listener socket restarting");
|
|
||||||
try {
|
|
||||||
ListenerSocket.Dispose();
|
|
||||||
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
|
|
||||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
|
|
||||||
ListenerSocket = new SocketWrapper(socket);
|
|
||||||
Start(_config);
|
|
||||||
FleckLog.Info("Listener socket restarted");
|
|
||||||
} catch(Exception ex) {
|
|
||||||
FleckLog.Error("Listener could not be restarted", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClientConnect(ISocket clientSocket) {
|
|
||||||
if(clientSocket == null) return; // socket closed
|
|
||||||
|
|
||||||
FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
|
|
||||||
ListenForClients();
|
|
||||||
|
|
||||||
WebSocketConnection connection = null;
|
|
||||||
|
|
||||||
connection = new WebSocketConnection(
|
|
||||||
clientSocket,
|
|
||||||
_config,
|
|
||||||
bytes => RequestParser.Parse(bytes, _scheme),
|
|
||||||
r => {
|
|
||||||
try {
|
|
||||||
return HandlerFactory.BuildHandler(
|
|
||||||
r, s => connection.OnMessage(s), connection.Close, b => connection.OnBinary(b),
|
|
||||||
b => connection.OnPing(b), b => connection.OnPong(b)
|
|
||||||
);
|
|
||||||
} catch(WebSocketException) {
|
|
||||||
const string responseMsg = "HTTP/1.1 200 OK\r\n"
|
|
||||||
+ "Date: {0}\r\n"
|
|
||||||
+ "Server: SharpChat\r\n"
|
|
||||||
+ "Content-Length: {1}\r\n"
|
|
||||||
+ "Content-Type: text/html; charset=utf-8\r\n"
|
|
||||||
+ "Connection: close\r\n"
|
|
||||||
+ "\r\n"
|
|
||||||
+ "{2}";
|
|
||||||
string responseBody = File.Exists("http-motd.txt") ? File.ReadAllText("http-motd.txt") : "SharpChat";
|
|
||||||
|
|
||||||
clientSocket.Stream.Write(Encoding.UTF8.GetBytes(string.Format(
|
|
||||||
responseMsg, DateTimeOffset.Now.ToString("r"), Encoding.UTF8.GetByteCount(responseBody), responseBody
|
|
||||||
)));
|
|
||||||
clientSocket.Close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s));
|
|
||||||
|
|
||||||
if(IsSecure) {
|
|
||||||
FleckLog.Debug("Authenticating Secure Connection");
|
|
||||||
clientSocket
|
|
||||||
.Authenticate(Certificate,
|
|
||||||
EnabledSslProtocols,
|
|
||||||
connection.StartReceiving,
|
|
||||||
e => FleckLog.Warn("Failed to Authenticate", e));
|
|
||||||
} else {
|
|
||||||
connection.StartReceiving();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Start(Action<IWebSocketConnection> config) {
|
||||||
|
IPEndPoint ipLocal = new(_locationIP, Port);
|
||||||
|
ListenerSocket.Bind(ipLocal);
|
||||||
|
ListenerSocket.Listen(100);
|
||||||
|
Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port;
|
||||||
|
FleckLog.Info(string.Format("Server started at {0} (actual port {1})", Location, Port));
|
||||||
|
if(_scheme == "wss") {
|
||||||
|
if(Certificate == null) {
|
||||||
|
FleckLog.Error("Scheme cannot be 'wss' without a Certificate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// makes dotnet shut up, TLS is handled by NGINX anyway
|
||||||
|
// if(EnabledSslProtocols == SslProtocols.None) {
|
||||||
|
// EnabledSslProtocols = SslProtocols.Tls;
|
||||||
|
// FleckLog.Debug("Using default TLS 1.0 security protocol.");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
ListenForClients();
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ListenForClients() {
|
||||||
|
ListenerSocket.Accept(OnClientConnect, e => {
|
||||||
|
FleckLog.Error("Listener socket is closed", e);
|
||||||
|
if(RestartAfterListenError) {
|
||||||
|
FleckLog.Info("Listener socket restarting");
|
||||||
|
try {
|
||||||
|
ListenerSocket.Dispose();
|
||||||
|
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
|
||||||
|
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
|
||||||
|
ListenerSocket = new SocketWrapper(socket);
|
||||||
|
Start(_config);
|
||||||
|
FleckLog.Info("Listener socket restarted");
|
||||||
|
} catch(Exception ex) {
|
||||||
|
FleckLog.Error("Listener could not be restarted", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClientConnect(ISocket clientSocket) {
|
||||||
|
if(clientSocket == null) return; // socket closed
|
||||||
|
|
||||||
|
FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
|
||||||
|
ListenForClients();
|
||||||
|
|
||||||
|
WebSocketConnection connection = null;
|
||||||
|
|
||||||
|
connection = new WebSocketConnection(
|
||||||
|
clientSocket,
|
||||||
|
_config,
|
||||||
|
bytes => RequestParser.Parse(bytes, _scheme),
|
||||||
|
r => {
|
||||||
|
try {
|
||||||
|
return HandlerFactory.BuildHandler(
|
||||||
|
r, s => connection.OnMessage(s), connection.Close, b => connection.OnBinary(b),
|
||||||
|
b => connection.OnPing(b), b => connection.OnPong(b)
|
||||||
|
);
|
||||||
|
} catch(WebSocketException) {
|
||||||
|
const string responseMsg = "HTTP/1.1 200 OK\r\n"
|
||||||
|
+ "Date: {0}\r\n"
|
||||||
|
+ "Server: SharpChat\r\n"
|
||||||
|
+ "Content-Length: {1}\r\n"
|
||||||
|
+ "Content-Type: text/html; charset=utf-8\r\n"
|
||||||
|
+ "Connection: close\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ "{2}";
|
||||||
|
string responseBody = File.Exists("http-motd.txt") ? File.ReadAllText("http-motd.txt") : "SharpChat";
|
||||||
|
|
||||||
|
clientSocket.Stream.Write(Encoding.UTF8.GetBytes(string.Format(
|
||||||
|
responseMsg, DateTimeOffset.Now.ToString("r"), Encoding.UTF8.GetByteCount(responseBody), responseBody
|
||||||
|
)));
|
||||||
|
clientSocket.Close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s));
|
||||||
|
|
||||||
|
if(IsSecure) {
|
||||||
|
FleckLog.Debug("Authenticating Secure Connection");
|
||||||
|
clientSocket
|
||||||
|
.Authenticate(Certificate,
|
||||||
|
EnabledSslProtocols,
|
||||||
|
connection.StartReceiving,
|
||||||
|
e => FleckLog.Warn("Failed to Authenticate", e));
|
||||||
|
} else {
|
||||||
|
connection.StartReceiving();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,115 @@
|
||||||
using SharpChat.EventStorage;
|
using SharpChat.EventStorage;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.SockChat.S2CPackets {
|
namespace SharpChat.SockChat.S2CPackets;
|
||||||
public class ContextMessageS2CPacket(StoredEventInfo evt, bool notify = false) : S2CPacket {
|
|
||||||
public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt));
|
|
||||||
|
|
||||||
public string Pack() {
|
public class ContextMessageS2CPacket(StoredEventInfo evt, bool notify = false) : S2CPacket {
|
||||||
bool isAction = Event.Flags.HasFlag(StoredEventFlags.Action);
|
public StoredEventInfo Event { get; private set; } = evt ?? throw new ArgumentNullException(nameof(evt));
|
||||||
bool isBroadcast = Event.Flags.HasFlag(StoredEventFlags.Broadcast);
|
|
||||||
bool isPrivate = Event.Flags.HasFlag(StoredEventFlags.Private);
|
|
||||||
|
|
||||||
StringBuilder sb = new();
|
public string Pack() {
|
||||||
|
bool isAction = Event.Flags.HasFlag(StoredEventFlags.Action);
|
||||||
|
bool isBroadcast = Event.Flags.HasFlag(StoredEventFlags.Broadcast);
|
||||||
|
bool isPrivate = Event.Flags.HasFlag(StoredEventFlags.Private);
|
||||||
|
|
||||||
sb.Append("7\t1\t");
|
StringBuilder sb = new();
|
||||||
sb.Append(Event.Created.ToUnixTimeSeconds());
|
|
||||||
sb.Append('\t');
|
|
||||||
|
|
||||||
switch(Event.Type) {
|
sb.Append("7\t1\t");
|
||||||
case "msg:add":
|
sb.Append(Event.Created.ToUnixTimeSeconds());
|
||||||
case "SharpChat.Events.ChatMessage":
|
sb.Append('\t');
|
||||||
if(isBroadcast || Event.Sender is null) {
|
|
||||||
sb.Append("-1\tChatBot\tinherit\t\t0\fsay\f");
|
|
||||||
} else {
|
|
||||||
sb.Append(Event.Sender.UserId);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(Event.Sender.LegacyNameWithStatus);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(Event.Sender.Colour);
|
|
||||||
sb.Append('\t');
|
|
||||||
sb.Append(Event.Sender.Rank);
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
|
||||||
sb.Append(' ');
|
|
||||||
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.CreateChannel) ? (Event.Sender.Permissions.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
|
||||||
sb.Append('\t');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isAction)
|
switch(Event.Type) {
|
||||||
sb.Append("<i>");
|
case "msg:add":
|
||||||
|
case "SharpChat.Events.ChatMessage":
|
||||||
|
if(isBroadcast || Event.Sender is null) {
|
||||||
|
sb.Append("-1\tChatBot\tinherit\t\t0\fsay\f");
|
||||||
|
} else {
|
||||||
|
sb.Append(Event.Sender.UserId);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(Event.Sender.LegacyNameWithStatus);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(Event.Sender.Colour);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(Event.Sender.Rank);
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.KickUser) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.ViewLogs) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.SetOwnNickname) ? '1' : '0');
|
||||||
|
sb.Append(' ');
|
||||||
|
sb.Append(Event.Sender.Permissions.HasFlag(UserPermissions.CreateChannel) ? (Event.Sender.Permissions.HasFlag(UserPermissions.SetChannelPermanent) ? '2' : '1') : '0');
|
||||||
|
sb.Append('\t');
|
||||||
|
}
|
||||||
|
|
||||||
sb.Append(
|
if(isAction)
|
||||||
(Event.Data.RootElement.GetProperty("text").GetString()?
|
sb.Append("<i>");
|
||||||
.Replace("<", "<")
|
|
||||||
.Replace(">", ">")
|
|
||||||
.Replace("\n", " <br/> ")
|
|
||||||
.Replace("\t", " ")) ?? string.Empty
|
|
||||||
);
|
|
||||||
|
|
||||||
if(isAction)
|
sb.Append(
|
||||||
sb.Append("</i>");
|
(Event.Data.RootElement.GetProperty("text").GetString()?
|
||||||
break;
|
.Replace("<", "<")
|
||||||
|
.Replace(">", ">")
|
||||||
|
.Replace("\n", " <br/> ")
|
||||||
|
.Replace("\t", " ")) ?? string.Empty
|
||||||
|
);
|
||||||
|
|
||||||
case "user:connect":
|
if(isAction)
|
||||||
case "SharpChat.Events.UserConnectEvent":
|
sb.Append("</i>");
|
||||||
sb.Append("-1\tChatBot\tinherit\t\t0\fjoin\f");
|
break;
|
||||||
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "chan:join":
|
case "user:connect":
|
||||||
case "SharpChat.Events.UserChannelJoinEvent":
|
case "SharpChat.Events.UserConnectEvent":
|
||||||
sb.Append("-1\tChatBot\tinherit\t\t0\fjchan\f");
|
sb.Append("-1\tChatBot\tinherit\t\t0\fjoin\f");
|
||||||
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "chan:leave":
|
case "chan:join":
|
||||||
case "SharpChat.Events.UserChannelLeaveEvent":
|
case "SharpChat.Events.UserChannelJoinEvent":
|
||||||
sb.Append("-1\tChatBot\tinherit\t\t0\flchan\f");
|
sb.Append("-1\tChatBot\tinherit\t\t0\fjchan\f");
|
||||||
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "user:disconnect":
|
case "chan:leave":
|
||||||
case "SharpChat.Events.UserDisconnectEvent":
|
case "SharpChat.Events.UserChannelLeaveEvent":
|
||||||
sb.Append("-1\tChatBot\tinherit\t\t0\f");
|
sb.Append("-1\tChatBot\tinherit\t\t0\flchan\f");
|
||||||
|
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
||||||
|
break;
|
||||||
|
|
||||||
switch((UserDisconnectS2CPacket.Reason)Event.Data.RootElement.GetProperty("reason").GetByte()) {
|
case "user:disconnect":
|
||||||
case UserDisconnectS2CPacket.Reason.Flood:
|
case "SharpChat.Events.UserDisconnectEvent":
|
||||||
sb.Append("flood");
|
sb.Append("-1\tChatBot\tinherit\t\t0\f");
|
||||||
break;
|
|
||||||
case UserDisconnectS2CPacket.Reason.Kicked:
|
|
||||||
sb.Append("kick");
|
|
||||||
break;
|
|
||||||
case UserDisconnectS2CPacket.Reason.TimeOut:
|
|
||||||
sb.Append("timeout");
|
|
||||||
break;
|
|
||||||
case UserDisconnectS2CPacket.Reason.Leave:
|
|
||||||
default:
|
|
||||||
sb.Append("leave");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append('\f');
|
switch((UserDisconnectS2CPacket.Reason)Event.Data.RootElement.GetProperty("reason").GetByte()) {
|
||||||
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
case UserDisconnectS2CPacket.Reason.Flood:
|
||||||
break;
|
sb.Append("flood");
|
||||||
}
|
break;
|
||||||
|
case UserDisconnectS2CPacket.Reason.Kicked:
|
||||||
|
sb.Append("kick");
|
||||||
|
break;
|
||||||
|
case UserDisconnectS2CPacket.Reason.TimeOut:
|
||||||
|
sb.Append("timeout");
|
||||||
|
break;
|
||||||
|
case UserDisconnectS2CPacket.Reason.Leave:
|
||||||
|
default:
|
||||||
|
sb.Append("leave");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
sb.Append('\t');
|
sb.Append('\f');
|
||||||
sb.Append(Event.Id);
|
sb.Append(Event.Sender?.LegacyName ?? "?????");
|
||||||
sb.Append('\t');
|
break;
|
||||||
sb.Append(notify ? '1' : '0');
|
|
||||||
sb.AppendFormat(
|
|
||||||
"\t1{0}0{1}{2}",
|
|
||||||
isAction ? '1' : '0',
|
|
||||||
isAction ? '0' : '1',
|
|
||||||
isPrivate ? '1' : '0'
|
|
||||||
);
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(Event.Id);
|
||||||
|
sb.Append('\t');
|
||||||
|
sb.Append(notify ? '1' : '0');
|
||||||
|
sb.AppendFormat(
|
||||||
|
"\t1{0}0{1}{2}",
|
||||||
|
isAction ? '1' : '0',
|
||||||
|
isAction ? '0' : '1',
|
||||||
|
isPrivate ? '1' : '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,234 +7,234 @@ using SharpChat.Configuration;
|
||||||
using SharpChat.SockChat.S2CPackets;
|
using SharpChat.SockChat.S2CPackets;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class SockChatServer : IDisposable {
|
|
||||||
public const ushort DEFAULT_PORT = 6770;
|
|
||||||
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
|
|
||||||
public const int DEFAULT_MAX_CONNECTIONS = 5;
|
|
||||||
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
|
|
||||||
public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9;
|
|
||||||
|
|
||||||
public IWebSocketServer Server { get; }
|
public class SockChatServer : IDisposable {
|
||||||
public Context Context { get; }
|
public const ushort DEFAULT_PORT = 6770;
|
||||||
|
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
|
||||||
|
public const int DEFAULT_MAX_CONNECTIONS = 5;
|
||||||
|
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
|
||||||
|
public const int DEFAULT_FLOOD_KICK_EXEMPT_RANK = 9;
|
||||||
|
|
||||||
private readonly BansClient BansClient;
|
public IWebSocketServer Server { get; }
|
||||||
|
public Context Context { get; }
|
||||||
|
|
||||||
private readonly CachedValue<int> MaxMessageLength;
|
private readonly BansClient BansClient;
|
||||||
private readonly CachedValue<int> MaxConnections;
|
|
||||||
private readonly CachedValue<int> FloodKickLength;
|
|
||||||
private readonly CachedValue<int> FloodKickExemptRank;
|
|
||||||
|
|
||||||
private readonly List<C2SPacketHandler> GuestHandlers = [];
|
private readonly CachedValue<int> MaxMessageLength;
|
||||||
private readonly List<C2SPacketHandler> AuthedHandlers = [];
|
private readonly CachedValue<int> MaxConnections;
|
||||||
private readonly SendMessageC2SPacketHandler SendMessageHandler;
|
private readonly CachedValue<int> FloodKickLength;
|
||||||
|
private readonly CachedValue<int> FloodKickExemptRank;
|
||||||
|
|
||||||
private bool IsShuttingDown = false;
|
private readonly List<C2SPacketHandler> GuestHandlers = [];
|
||||||
|
private readonly List<C2SPacketHandler> AuthedHandlers = [];
|
||||||
|
private readonly SendMessageC2SPacketHandler SendMessageHandler;
|
||||||
|
|
||||||
private static readonly string[] DEFAULT_CHANNELS = ["lounge"];
|
private bool IsShuttingDown = false;
|
||||||
|
|
||||||
private Channel DefaultChannel { get; set; }
|
private static readonly string[] DEFAULT_CHANNELS = ["lounge"];
|
||||||
|
|
||||||
public SockChatServer(
|
private Channel DefaultChannel { get; set; }
|
||||||
AuthClient authClient,
|
|
||||||
BansClient bansClient,
|
|
||||||
EventStorage.EventStorage evtStore,
|
|
||||||
Config config
|
|
||||||
) {
|
|
||||||
Logger.Write("Initialising Sock Chat server...");
|
|
||||||
|
|
||||||
BansClient = bansClient;
|
public SockChatServer(
|
||||||
|
AuthClient authClient,
|
||||||
|
BansClient bansClient,
|
||||||
|
EventStorage.EventStorage evtStore,
|
||||||
|
Config config
|
||||||
|
) {
|
||||||
|
Logger.Write("Initialising Sock Chat server...");
|
||||||
|
|
||||||
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
|
BansClient = bansClient;
|
||||||
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
|
||||||
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
|
||||||
FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK);
|
|
||||||
|
|
||||||
Context = new Context(evtStore);
|
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
|
||||||
|
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
||||||
|
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
||||||
|
FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK);
|
||||||
|
|
||||||
string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
|
Context = new Context(evtStore);
|
||||||
if(channelNames is not null)
|
|
||||||
foreach(string channelName in channelNames) {
|
|
||||||
Config channelCfg = config.ScopeTo($"channels:{channelName}");
|
|
||||||
|
|
||||||
string name = channelCfg.SafeReadValue("name", string.Empty)!;
|
string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
|
||||||
if(string.IsNullOrWhiteSpace(name))
|
if(channelNames is not null)
|
||||||
name = channelName;
|
foreach(string channelName in channelNames) {
|
||||||
|
Config channelCfg = config.ScopeTo($"channels:{channelName}");
|
||||||
|
|
||||||
Channel channelInfo = new(
|
string name = channelCfg.SafeReadValue("name", string.Empty)!;
|
||||||
name,
|
if(string.IsNullOrWhiteSpace(name))
|
||||||
channelCfg.SafeReadValue("password", string.Empty)!,
|
name = channelName;
|
||||||
rank: channelCfg.SafeReadValue("minRank", 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
Context.Channels.Add(channelInfo);
|
Channel channelInfo = new(
|
||||||
DefaultChannel ??= channelInfo;
|
name,
|
||||||
}
|
channelCfg.SafeReadValue("password", string.Empty)!,
|
||||||
|
rank: channelCfg.SafeReadValue("minRank", 0)
|
||||||
|
);
|
||||||
|
|
||||||
if(DefaultChannel is null)
|
Context.Channels.Add(channelInfo);
|
||||||
throw new Exception("The default channel could not be determined.");
|
DefaultChannel ??= channelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections));
|
if(DefaultChannel is null)
|
||||||
|
throw new Exception("The default channel could not be determined.");
|
||||||
|
|
||||||
AuthedHandlers.AddRange([
|
GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections));
|
||||||
new PingC2SPacketHandler(authClient),
|
|
||||||
SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength),
|
|
||||||
]);
|
|
||||||
|
|
||||||
SendMessageHandler.AddCommands([
|
AuthedHandlers.AddRange([
|
||||||
new AFKClientCommand(),
|
new PingC2SPacketHandler(authClient),
|
||||||
new NickClientCommand(),
|
SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength),
|
||||||
new WhisperClientCommand(),
|
]);
|
||||||
new ActionClientCommand(),
|
|
||||||
new WhoClientCommand(),
|
|
||||||
new JoinChannelClientCommand(),
|
|
||||||
new CreateChannelClientCommand(),
|
|
||||||
new DeleteChannelClientCommand(),
|
|
||||||
new PasswordChannelClientCommand(),
|
|
||||||
new RankChannelClientCommand(),
|
|
||||||
new BroadcastClientCommand(),
|
|
||||||
new DeleteMessageClientCommand(),
|
|
||||||
new KickBanClientCommand(bansClient),
|
|
||||||
new PardonUserClientCommand(bansClient),
|
|
||||||
new PardonAddressClientCommand(bansClient),
|
|
||||||
new BanListClientCommand(bansClient),
|
|
||||||
new RemoteAddressClientCommand(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
SendMessageHandler.AddCommands([
|
||||||
Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}");
|
new AFKClientCommand(),
|
||||||
|
new NickClientCommand(),
|
||||||
|
new WhisperClientCommand(),
|
||||||
|
new ActionClientCommand(),
|
||||||
|
new WhoClientCommand(),
|
||||||
|
new JoinChannelClientCommand(),
|
||||||
|
new CreateChannelClientCommand(),
|
||||||
|
new DeleteChannelClientCommand(),
|
||||||
|
new PasswordChannelClientCommand(),
|
||||||
|
new RankChannelClientCommand(),
|
||||||
|
new BroadcastClientCommand(),
|
||||||
|
new DeleteMessageClientCommand(),
|
||||||
|
new KickBanClientCommand(bansClient),
|
||||||
|
new PardonUserClientCommand(bansClient),
|
||||||
|
new PardonAddressClientCommand(bansClient),
|
||||||
|
new BanListClientCommand(bansClient),
|
||||||
|
new RemoteAddressClientCommand(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
||||||
|
Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Listen(ManualResetEvent waitHandle) {
|
||||||
|
if(waitHandle != null)
|
||||||
|
SendMessageHandler.AddCommand(new ShutdownRestartClientCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true)));
|
||||||
|
|
||||||
|
Server.Start(sock => {
|
||||||
|
if(IsShuttingDown) {
|
||||||
|
sock.Close(1013);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection conn = new(sock);
|
||||||
|
Context.Connections.Add(conn);
|
||||||
|
|
||||||
|
sock.OnOpen = () => OnOpen(conn).Wait();
|
||||||
|
sock.OnClose = () => OnClose(conn).Wait();
|
||||||
|
sock.OnError = err => OnError(conn, err).Wait();
|
||||||
|
sock.OnMessage = msg => OnMessage(conn, msg).Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Write("Listening...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnOpen(Connection conn) {
|
||||||
|
Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}");
|
||||||
|
await Context.SafeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnError(Connection conn, Exception ex) {
|
||||||
|
Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}");
|
||||||
|
await Context.SafeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClose(Connection conn) {
|
||||||
|
Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}");
|
||||||
|
|
||||||
|
Context.ContextAccess.Wait();
|
||||||
|
try {
|
||||||
|
Context.Connections.Remove(conn);
|
||||||
|
|
||||||
|
if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User))
|
||||||
|
await Context.HandleDisconnect(conn.User);
|
||||||
|
|
||||||
|
await Context.Update();
|
||||||
|
} finally {
|
||||||
|
Context.ContextAccess.Release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Listen(ManualResetEvent waitHandle) {
|
private async Task OnMessage(Connection conn, string msg) {
|
||||||
if(waitHandle != null)
|
await Context.SafeUpdate();
|
||||||
SendMessageHandler.AddCommand(new ShutdownRestartClientCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true)));
|
|
||||||
|
|
||||||
Server.Start(sock => {
|
// this doesn't affect non-authed connections?????
|
||||||
if(IsShuttingDown) {
|
if(conn.User is not null && conn.User.Rank < FloodKickExemptRank) {
|
||||||
sock.Close(1013);
|
User? banUser = null;
|
||||||
return;
|
string banAddr = string.Empty;
|
||||||
}
|
TimeSpan banDuration = TimeSpan.MinValue;
|
||||||
|
|
||||||
Connection conn = new(sock);
|
|
||||||
Context.Connections.Add(conn);
|
|
||||||
|
|
||||||
sock.OnOpen = () => OnOpen(conn).Wait();
|
|
||||||
sock.OnClose = () => OnClose(conn).Wait();
|
|
||||||
sock.OnError = err => OnError(conn, err).Wait();
|
|
||||||
sock.OnMessage = msg => OnMessage(conn, msg).Wait();
|
|
||||||
});
|
|
||||||
|
|
||||||
Logger.Write("Listening...");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnOpen(Connection conn) {
|
|
||||||
Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}");
|
|
||||||
await Context.SafeUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnError(Connection conn, Exception ex) {
|
|
||||||
Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}");
|
|
||||||
await Context.SafeUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnClose(Connection conn) {
|
|
||||||
Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}");
|
|
||||||
|
|
||||||
Context.ContextAccess.Wait();
|
Context.ContextAccess.Wait();
|
||||||
try {
|
try {
|
||||||
Context.Connections.Remove(conn);
|
if(!Context.UserRateLimiters.TryGetValue(conn.User.UserId, out RateLimiter? rateLimiter))
|
||||||
|
Context.UserRateLimiters.Add(conn.User.UserId, rateLimiter = new RateLimiter(
|
||||||
|
User.DEFAULT_SIZE,
|
||||||
|
User.DEFAULT_MINIMUM_DELAY,
|
||||||
|
User.DEFAULT_RISKY_OFFSET
|
||||||
|
));
|
||||||
|
|
||||||
if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User))
|
rateLimiter.Update();
|
||||||
await Context.HandleDisconnect(conn.User);
|
|
||||||
|
|
||||||
await Context.Update();
|
if(rateLimiter.IsExceeded) {
|
||||||
|
banDuration = TimeSpan.FromSeconds(FloodKickLength);
|
||||||
|
banUser = conn.User;
|
||||||
|
banAddr = conn.RemoteAddress.ToString();
|
||||||
|
} else if(rateLimiter.IsRisky) {
|
||||||
|
banUser = conn.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(banUser is not null) {
|
||||||
|
if(banDuration == TimeSpan.MinValue) {
|
||||||
|
await Context.SendTo(conn.User, new CommandResponseS2CPacket(Context.RandomSnowflake.Next(), LCR.FLOOD_WARN, false));
|
||||||
|
} else {
|
||||||
|
await Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood);
|
||||||
|
|
||||||
|
if(banDuration > TimeSpan.Zero)
|
||||||
|
await BansClient.BanCreateAsync(
|
||||||
|
BanKind.User,
|
||||||
|
banDuration,
|
||||||
|
conn.RemoteAddress,
|
||||||
|
conn.User.UserId,
|
||||||
|
"Kicked from chat for flood protection.",
|
||||||
|
IPAddress.IPv6Loopback
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
Context.ContextAccess.Release();
|
Context.ContextAccess.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnMessage(Connection conn, string msg) {
|
C2SPacketHandlerContext context = new(msg, Context, conn);
|
||||||
await Context.SafeUpdate();
|
C2SPacketHandler? handler = conn.User is null
|
||||||
|
? GuestHandlers.FirstOrDefault(h => h.IsMatch(context))
|
||||||
|
: AuthedHandlers.FirstOrDefault(h => h.IsMatch(context));
|
||||||
|
|
||||||
// this doesn't affect non-authed connections?????
|
if(handler is not null)
|
||||||
if(conn.User is not null && conn.User.Rank < FloodKickExemptRank) {
|
await handler.Handle(context);
|
||||||
User? banUser = null;
|
}
|
||||||
string banAddr = string.Empty;
|
|
||||||
TimeSpan banDuration = TimeSpan.MinValue;
|
|
||||||
|
|
||||||
Context.ContextAccess.Wait();
|
private bool IsDisposed;
|
||||||
try {
|
|
||||||
if(!Context.UserRateLimiters.TryGetValue(conn.User.UserId, out RateLimiter? rateLimiter))
|
|
||||||
Context.UserRateLimiters.Add(conn.User.UserId, rateLimiter = new RateLimiter(
|
|
||||||
User.DEFAULT_SIZE,
|
|
||||||
User.DEFAULT_MINIMUM_DELAY,
|
|
||||||
User.DEFAULT_RISKY_OFFSET
|
|
||||||
));
|
|
||||||
|
|
||||||
rateLimiter.Update();
|
~SockChatServer() {
|
||||||
|
DoDispose();
|
||||||
|
}
|
||||||
|
|
||||||
if(rateLimiter.IsExceeded) {
|
public void Dispose() {
|
||||||
banDuration = TimeSpan.FromSeconds(FloodKickLength);
|
DoDispose();
|
||||||
banUser = conn.User;
|
GC.SuppressFinalize(this);
|
||||||
banAddr = conn.RemoteAddress.ToString();
|
}
|
||||||
} else if(rateLimiter.IsRisky) {
|
|
||||||
banUser = conn.User;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(banUser is not null) {
|
private void DoDispose() {
|
||||||
if(banDuration == TimeSpan.MinValue) {
|
if(IsDisposed)
|
||||||
await Context.SendTo(conn.User, new CommandResponseS2CPacket(Context.RandomSnowflake.Next(), LCR.FLOOD_WARN, false));
|
return;
|
||||||
} else {
|
IsDisposed = true;
|
||||||
await Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood);
|
IsShuttingDown = true;
|
||||||
|
|
||||||
if(banDuration > TimeSpan.Zero)
|
foreach(Connection conn in Context.Connections)
|
||||||
await BansClient.BanCreateAsync(
|
conn.Dispose();
|
||||||
BanKind.User,
|
|
||||||
banDuration,
|
|
||||||
conn.RemoteAddress,
|
|
||||||
conn.User.UserId,
|
|
||||||
"Kicked from chat for flood protection.",
|
|
||||||
IPAddress.IPv6Loopback
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
Server?.Dispose();
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Context.ContextAccess.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
C2SPacketHandlerContext context = new(msg, Context, conn);
|
|
||||||
C2SPacketHandler? handler = conn.User is null
|
|
||||||
? GuestHandlers.FirstOrDefault(h => h.IsMatch(context))
|
|
||||||
: AuthedHandlers.FirstOrDefault(h => h.IsMatch(context));
|
|
||||||
|
|
||||||
if(handler is not null)
|
|
||||||
await handler.Handle(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
|
|
||||||
~SockChatServer() {
|
|
||||||
DoDispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
IsShuttingDown = true;
|
|
||||||
|
|
||||||
foreach(Connection conn in Context.Connections)
|
|
||||||
conn.Dispose();
|
|
||||||
|
|
||||||
Server?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,72 +2,72 @@ using SharpChat.ClientCommands;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class User(
|
|
||||||
string userId,
|
|
||||||
string userName,
|
|
||||||
ColourInheritable colour,
|
|
||||||
int rank,
|
|
||||||
UserPermissions perms,
|
|
||||||
string nickName = "",
|
|
||||||
UserStatus status = UserStatus.Online,
|
|
||||||
string statusText = ""
|
|
||||||
) {
|
|
||||||
public const int DEFAULT_SIZE = 30;
|
|
||||||
public const int DEFAULT_MINIMUM_DELAY = 10000;
|
|
||||||
public const int DEFAULT_RISKY_OFFSET = 5;
|
|
||||||
|
|
||||||
public string UserId { get; } = userId;
|
public class User(
|
||||||
public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName));
|
string userId,
|
||||||
public ColourInheritable Colour { get; set; } = colour;
|
string userName,
|
||||||
public int Rank { get; set; } = rank;
|
ColourInheritable colour,
|
||||||
public UserPermissions Permissions { get; set; } = perms;
|
int rank,
|
||||||
public string NickName { get; set; } = nickName;
|
UserPermissions perms,
|
||||||
public UserStatus Status { get; set; } = status;
|
string nickName = "",
|
||||||
public string StatusText { get; set; } = statusText;
|
UserStatus status = UserStatus.Online,
|
||||||
|
string statusText = ""
|
||||||
|
) {
|
||||||
|
public const int DEFAULT_SIZE = 30;
|
||||||
|
public const int DEFAULT_MINIMUM_DELAY = 10000;
|
||||||
|
public const int DEFAULT_RISKY_OFFSET = 5;
|
||||||
|
|
||||||
public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}";
|
public string UserId { get; } = userId;
|
||||||
|
public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName));
|
||||||
|
public ColourInheritable Colour { get; set; } = colour;
|
||||||
|
public int Rank { get; set; } = rank;
|
||||||
|
public UserPermissions Permissions { get; set; } = perms;
|
||||||
|
public string NickName { get; set; } = nickName;
|
||||||
|
public UserStatus Status { get; set; } = status;
|
||||||
|
public string StatusText { get; set; } = statusText;
|
||||||
|
|
||||||
public string LegacyNameWithStatus {
|
public string LegacyName => string.IsNullOrWhiteSpace(NickName) ? UserName : $"~{NickName}";
|
||||||
get {
|
|
||||||
StringBuilder sb = new();
|
|
||||||
|
|
||||||
if(Status == UserStatus.Away) {
|
public string LegacyNameWithStatus {
|
||||||
string statusText = StatusText.Trim();
|
get {
|
||||||
StringInfo sti = new(statusText);
|
StringBuilder sb = new();
|
||||||
if(Encoding.UTF8.GetByteCount(statusText) > AFKClientCommand.MAX_BYTES
|
|
||||||
|| sti.LengthInTextElements > AFKClientCommand.MAX_GRAPHEMES)
|
|
||||||
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, AFKClientCommand.MAX_GRAPHEMES)).Trim();
|
|
||||||
|
|
||||||
sb.AppendFormat("<{0}>_", statusText.ToUpperInvariant());
|
if(Status == UserStatus.Away) {
|
||||||
}
|
string statusText = StatusText.Trim();
|
||||||
|
StringInfo sti = new(statusText);
|
||||||
|
if(Encoding.UTF8.GetByteCount(statusText) > AFKClientCommand.MAX_BYTES
|
||||||
|
|| sti.LengthInTextElements > AFKClientCommand.MAX_GRAPHEMES)
|
||||||
|
statusText = sti.SubstringByTextElements(0, Math.Min(sti.LengthInTextElements, AFKClientCommand.MAX_GRAPHEMES)).Trim();
|
||||||
|
|
||||||
sb.Append(LegacyName);
|
sb.AppendFormat("<{0}>_", statusText.ToUpperInvariant());
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool Can(UserPermissions perm, bool strict = false) {
|
sb.Append(LegacyName);
|
||||||
UserPermissions perms = Permissions & perm;
|
|
||||||
return strict ? perms == perm : perms > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool NameEquals(string name) {
|
return sb.ToString();
|
||||||
return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
|| string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
|| string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
|| string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() {
|
|
||||||
return UserId.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetDMChannelName(User user1, User user2) {
|
|
||||||
return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0
|
|
||||||
? $"@{user2.UserId}-{user1.UserId}"
|
|
||||||
: $"@{user1.UserId}-{user2.UserId}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Can(UserPermissions perm, bool strict = false) {
|
||||||
|
UserPermissions perms = Permissions & perm;
|
||||||
|
return strict ? perms == perm : perms > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool NameEquals(string name) {
|
||||||
|
return string.Equals(name, UserName, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| string.Equals(name, NickName, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| string.Equals(name, LegacyName, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| string.Equals(name, LegacyNameWithStatus, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return UserId.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetDMChannelName(User user1, User user2) {
|
||||||
|
return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0
|
||||||
|
? $"@{user2.UserId}-{user1.UserId}"
|
||||||
|
: $"@{user1.UserId}-{user2.UserId}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public enum UserStatus {
|
|
||||||
Online,
|
public enum UserStatus {
|
||||||
Away,
|
Online,
|
||||||
Offline,
|
Away,
|
||||||
}
|
Offline,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.Auth {
|
namespace SharpChat.Auth;
|
||||||
public interface AuthClient {
|
|
||||||
Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token);
|
public interface AuthClient {
|
||||||
Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries);
|
Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token);
|
||||||
}
|
Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
namespace SharpChat.Auth {
|
namespace SharpChat.Auth;
|
||||||
public class AuthFailedException(string message) : Exception(message) {}
|
|
||||||
}
|
public class AuthFailedException(string message) : Exception(message) {}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
namespace SharpChat.Auth {
|
namespace SharpChat.Auth;
|
||||||
public interface AuthResult {
|
|
||||||
string UserId { get; }
|
public interface AuthResult {
|
||||||
string UserName { get; }
|
string UserId { get; }
|
||||||
ColourInheritable UserColour { get; }
|
string UserName { get; }
|
||||||
int UserRank { get; }
|
ColourInheritable UserColour { get; }
|
||||||
UserPermissions UserPermissions { get; }
|
int UserRank { get; }
|
||||||
}
|
UserPermissions UserPermissions { get; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
namespace SharpChat.Bans {
|
namespace SharpChat.Bans;
|
||||||
public interface BanInfo {
|
|
||||||
BanKind Kind { get; }
|
public interface BanInfo {
|
||||||
bool IsPermanent { get; }
|
BanKind Kind { get; }
|
||||||
DateTimeOffset ExpiresAt { get; }
|
bool IsPermanent { get; }
|
||||||
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
DateTimeOffset ExpiresAt { get; }
|
||||||
string ToString();
|
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
||||||
}
|
string ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace SharpChat.Bans {
|
namespace SharpChat.Bans;
|
||||||
public enum BanKind {
|
|
||||||
User,
|
public enum BanKind {
|
||||||
IPAddress,
|
User,
|
||||||
}
|
IPAddress,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.Bans {
|
namespace SharpChat.Bans;
|
||||||
public interface BansClient {
|
|
||||||
Task BanCreateAsync(
|
public interface BansClient {
|
||||||
BanKind kind,
|
Task BanCreateAsync(
|
||||||
TimeSpan duration,
|
BanKind kind,
|
||||||
IPAddress remoteAddr,
|
TimeSpan duration,
|
||||||
string? userId = null,
|
IPAddress remoteAddr,
|
||||||
string? reason = null,
|
string? userId = null,
|
||||||
IPAddress? issuerRemoteAddr = null,
|
string? reason = null,
|
||||||
string? issuerUserId = null
|
IPAddress? issuerRemoteAddr = null,
|
||||||
);
|
string? issuerUserId = null
|
||||||
Task<bool> BanRevokeAsync(BanInfo info);
|
);
|
||||||
Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null);
|
Task<bool> BanRevokeAsync(BanInfo info);
|
||||||
Task<BanInfo[]> BanGetListAsync();
|
Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null);
|
||||||
}
|
Task<BanInfo[]> BanGetListAsync();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SharpChat.Bans {
|
namespace SharpChat.Bans;
|
||||||
public interface IPAddressBanInfo : BanInfo {
|
|
||||||
IPAddress Address { get; }
|
public interface IPAddressBanInfo : BanInfo {
|
||||||
}
|
IPAddress Address { get; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace SharpChat.Bans {
|
namespace SharpChat.Bans;
|
||||||
public interface UserBanInfo : BanInfo {
|
|
||||||
string UserId { get; }
|
public interface UserBanInfo : BanInfo {
|
||||||
string UserName { get; }
|
string UserId { get; }
|
||||||
ColourInheritable UserColour { get; }
|
string UserName { get; }
|
||||||
}
|
ColourInheritable UserColour { get; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public readonly record struct ColourInheritable(ColourRgb? rgb) {
|
|
||||||
public static readonly ColourInheritable None = new(null);
|
|
||||||
public override string ToString() => rgb.HasValue ? rgb.Value.ToString() : "inherit";
|
|
||||||
|
|
||||||
public static ColourInheritable FromRaw(int? raw) => raw is null ? None : new(new ColourRgb(raw.Value));
|
public readonly record struct ColourInheritable(ColourRgb? rgb) {
|
||||||
public static ColourInheritable FromRgb(byte red, byte green, byte blue) => new(ColourRgb.FromRgb(red, green, blue));
|
public static readonly ColourInheritable None = new(null);
|
||||||
|
public override string ToString() => rgb.HasValue ? rgb.Value.ToString() : "inherit";
|
||||||
|
|
||||||
// these should go Away
|
public static ColourInheritable FromRaw(int? raw) => raw is null ? None : new(new ColourRgb(raw.Value));
|
||||||
private const int MSZ_INHERIT = 0x40000000;
|
public static ColourInheritable FromRgb(byte red, byte green, byte blue) => new(ColourRgb.FromRgb(red, green, blue));
|
||||||
public int ToMisuzu() => rgb.HasValue ? rgb.Value.Raw : MSZ_INHERIT;
|
|
||||||
public static ColourInheritable FromMisuzu(int msz) => (msz & MSZ_INHERIT) > 0 ? None : new(new ColourRgb(msz & 0xFFFFFF));
|
// these should go Away
|
||||||
}
|
private const int MSZ_INHERIT = 0x40000000;
|
||||||
|
public int ToMisuzu() => rgb.HasValue ? rgb.Value.Raw : MSZ_INHERIT;
|
||||||
|
public static ColourInheritable FromMisuzu(int msz) => (msz & MSZ_INHERIT) > 0 ? None : new(new ColourRgb(msz & 0xFFFFFF));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public readonly record struct ColourRgb(int Raw) {
|
|
||||||
public byte Red => (byte)((Raw >> 16) & 0xFF);
|
public readonly record struct ColourRgb(int Raw) {
|
||||||
public byte Green => (byte)((Raw >> 8) & 0xFF);
|
public byte Red => (byte)((Raw >> 16) & 0xFF);
|
||||||
public byte Blue => (byte)(Raw & 0xFF);
|
public byte Green => (byte)((Raw >> 8) & 0xFF);
|
||||||
public override string ToString() => string.Format("#{0:x2}{1:x2}{2:x2}", Red, Green, Blue);
|
public byte Blue => (byte)(Raw & 0xFF);
|
||||||
public static ColourRgb FromRgb(byte red, byte green, byte blue) => new(red << 16 | green << 8 | blue);
|
public override string ToString() => string.Format("#{0:x2}{1:x2}{2:x2}", Red, Green, Blue);
|
||||||
}
|
public static ColourRgb FromRgb(byte red, byte green, byte blue) => new(red << 16 | green << 8 | blue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
namespace SharpChat.Configuration {
|
namespace SharpChat.Configuration;
|
||||||
public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) {
|
|
||||||
private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
|
|
||||||
private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
|
|
||||||
private object ConfigAccess { get; } = new();
|
|
||||||
|
|
||||||
private object? CurrentValue { get; set; } = default(T);
|
public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) {
|
||||||
private DateTimeOffset LastRead { get; set; }
|
private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
private object ConfigAccess { get; } = new();
|
||||||
|
|
||||||
public T? Value {
|
private object? CurrentValue { get; set; } = default(T);
|
||||||
get {
|
private DateTimeOffset LastRead { get; set; }
|
||||||
lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls
|
|
||||||
DateTimeOffset now = DateTimeOffset.Now;
|
public T? Value {
|
||||||
if((now - LastRead) >= lifetime) {
|
get {
|
||||||
LastRead = now;
|
lock(ConfigAccess) { // this lock doesn't really make sense since it doesn't affect other config calls
|
||||||
CurrentValue = Config.ReadValue(Name, fallback);
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
Logger.Debug($"Read {Name} ({CurrentValue})");
|
if((now - LastRead) >= lifetime) {
|
||||||
}
|
LastRead = now;
|
||||||
|
CurrentValue = Config.ReadValue(Name, fallback);
|
||||||
|
Logger.Debug($"Read {Name} ({CurrentValue})");
|
||||||
}
|
}
|
||||||
return (T?)CurrentValue;
|
|
||||||
}
|
}
|
||||||
}
|
return (T?)CurrentValue;
|
||||||
|
|
||||||
public static implicit operator T?(CachedValue<T?> val) => val.Value;
|
|
||||||
|
|
||||||
public void Refresh() {
|
|
||||||
LastRead = DateTimeOffset.MinValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() {
|
|
||||||
return Value?.ToString() ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static implicit operator T?(CachedValue<T?> val) => val.Value;
|
||||||
|
|
||||||
|
public void Refresh() {
|
||||||
|
LastRead = DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return Value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
namespace SharpChat.Configuration {
|
namespace SharpChat.Configuration;
|
||||||
public interface Config : IDisposable {
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a proxy object that forces all names to start with the given prefix.
|
|
||||||
/// </summary>
|
|
||||||
Config ScopeTo(string prefix);
|
|
||||||
|
|
||||||
/// <summary>
|
public interface Config : IDisposable {
|
||||||
/// Reads a raw (string) value from the config.
|
/// <summary>
|
||||||
/// </summary>
|
/// Creates a proxy object that forces all names to start with the given prefix.
|
||||||
string? ReadValue(string name, string? fallback = null);
|
/// </summary>
|
||||||
|
Config ScopeTo(string prefix);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads and casts value from the config.
|
/// Reads a raw (string) value from the config.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="ConfigTypeException">Type conversion failed.</exception>
|
string? ReadValue(string name, string? fallback = null);
|
||||||
T? ReadValue<T>(string name, T? fallback = default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads and casts a value from the config. Returns fallback when type conversion fails.
|
/// Reads and casts value from the config.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
T? SafeReadValue<T>(string name, T? fallback);
|
/// <exception cref="ConfigTypeException">Type conversion failed.</exception>
|
||||||
|
T? ReadValue<T>(string name, T? fallback = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values.
|
/// Reads and casts a value from the config. Returns fallback when type conversion fails.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null);
|
T? SafeReadValue<T>(string name, T? fallback);
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values.
|
||||||
|
/// </summary>
|
||||||
|
CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
namespace SharpChat.Configuration {
|
namespace SharpChat.Configuration;
|
||||||
public abstract class ConfigException : Exception {
|
|
||||||
public ConfigException(string message) : base(message) { }
|
|
||||||
public ConfigException(string message, Exception ex) : base(message, ex) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ConfigLockException() : ConfigException("Unable to acquire lock for reading configuration.") {}
|
public abstract class ConfigException : Exception {
|
||||||
public class ConfigTypeException(Exception ex) : ConfigException("Given type does not match the value in the configuration.", ex) {}
|
public ConfigException(string message) : base(message) { }
|
||||||
|
public ConfigException(string message, Exception ex) : base(message, ex) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ConfigLockException() : ConfigException("Unable to acquire lock for reading configuration.") {}
|
||||||
|
public class ConfigTypeException(Exception ex) : ConfigException("Given type does not match the value in the configuration.", ex) {}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
namespace SharpChat.Configuration {
|
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));
|
|
||||||
|
|
||||||
private string GetName(string name) {
|
public class ScopedConfig(Config config, string prefix) : Config {
|
||||||
return Prefix + name;
|
private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
}
|
private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix));
|
||||||
|
|
||||||
public string? ReadValue(string name, string? fallback = null) {
|
private string GetName(string name) {
|
||||||
return Config.ReadValue(GetName(name), fallback);
|
return Prefix + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T? ReadValue<T>(string name, T? fallback = default) {
|
public string? ReadValue(string name, string? fallback = null) {
|
||||||
return Config.ReadValue(GetName(name), fallback);
|
return Config.ReadValue(GetName(name), fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T? SafeReadValue<T>(string name, T? fallback) {
|
public T? ReadValue<T>(string name, T? fallback = default) {
|
||||||
return Config.SafeReadValue(GetName(name), fallback);
|
return Config.ReadValue(GetName(name), fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Config ScopeTo(string prefix) {
|
public T? SafeReadValue<T>(string name, T? fallback) {
|
||||||
return Config.ScopeTo(GetName(prefix));
|
return Config.SafeReadValue(GetName(name), fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
public Config ScopeTo(string prefix) {
|
||||||
return Config.ReadCached(GetName(name), fallback, lifetime);
|
return Config.ScopeTo(GetName(prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
||||||
GC.SuppressFinalize(this);
|
return Config.ReadCached(GetName(name), fallback, lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,115 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat.Configuration {
|
namespace SharpChat.Configuration;
|
||||||
public class StreamConfig : Config {
|
|
||||||
private Stream Stream { get; }
|
|
||||||
private StreamReader StreamReader { get; }
|
|
||||||
private Mutex Lock { get; }
|
|
||||||
|
|
||||||
private const int LOCK_TIMEOUT = 10000;
|
public class StreamConfig : Config {
|
||||||
|
private Stream Stream { get; }
|
||||||
|
private StreamReader StreamReader { get; }
|
||||||
|
private Mutex Lock { get; }
|
||||||
|
|
||||||
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
|
private const int LOCK_TIMEOUT = 10000;
|
||||||
|
|
||||||
public StreamConfig(Stream stream) {
|
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
|
||||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
|
||||||
if(!Stream.CanRead)
|
|
||||||
throw new ArgumentException("Provided stream must be readable.", nameof(stream));
|
|
||||||
if(!Stream.CanSeek)
|
|
||||||
throw new ArgumentException("Provided stream must be seekable.", nameof(stream));
|
|
||||||
StreamReader = new StreamReader(stream, new UTF8Encoding(false), false);
|
|
||||||
Lock = new Mutex();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StreamConfig FromPath(string fileName) {
|
public StreamConfig(Stream stream) {
|
||||||
return new StreamConfig(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite));
|
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||||
}
|
if(!Stream.CanRead)
|
||||||
|
throw new ArgumentException("Provided stream must be readable.", nameof(stream));
|
||||||
|
if(!Stream.CanSeek)
|
||||||
|
throw new ArgumentException("Provided stream must be seekable.", nameof(stream));
|
||||||
|
StreamReader = new StreamReader(stream, new UTF8Encoding(false), false);
|
||||||
|
Lock = new Mutex();
|
||||||
|
}
|
||||||
|
|
||||||
public string? ReadValue(string name, string? fallback = null) {
|
public static StreamConfig FromPath(string fileName) {
|
||||||
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
|
return new StreamConfig(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite));
|
||||||
throw new ConfigLockException();
|
}
|
||||||
|
|
||||||
try {
|
public string? ReadValue(string name, string? fallback = null) {
|
||||||
Stream.Seek(0, SeekOrigin.Begin);
|
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
|
||||||
|
throw new ConfigLockException();
|
||||||
|
|
||||||
string? line;
|
try {
|
||||||
while((line = StreamReader.ReadLine()) != null) {
|
Stream.Seek(0, SeekOrigin.Begin);
|
||||||
if(string.IsNullOrWhiteSpace(line))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
line = line.TrimStart();
|
string? line;
|
||||||
if(line.StartsWith(';') || line.StartsWith('#'))
|
while((line = StreamReader.ReadLine()) != null) {
|
||||||
continue;
|
if(string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
line = line.TrimStart();
|
||||||
if(parts.Length < 2 || !string.Equals(parts[0], name))
|
if(line.StartsWith(';') || line.StartsWith('#'))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
return parts[1];
|
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
}
|
if(parts.Length < 2 || !string.Equals(parts[0], name))
|
||||||
} finally {
|
continue;
|
||||||
Lock.ReleaseMutex();
|
|
||||||
|
return parts[1];
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
Lock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? ReadValue<T>(string name, T? fallback = default) {
|
||||||
|
object? value = ReadValue(name);
|
||||||
|
if(value == null)
|
||||||
return fallback;
|
return fallback;
|
||||||
|
|
||||||
|
Type type = typeof(T);
|
||||||
|
if(value is string strVal) {
|
||||||
|
if(type == typeof(bool))
|
||||||
|
value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
&& !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
else if(type == typeof(string[]))
|
||||||
|
value = strVal.Split(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
public T? ReadValue<T>(string name, T? fallback = default) {
|
try {
|
||||||
object? value = ReadValue(name);
|
return (T)Convert.ChangeType(value, type);
|
||||||
if(value == null)
|
} catch(InvalidCastException ex) {
|
||||||
return fallback;
|
throw new ConfigTypeException(ex);
|
||||||
|
|
||||||
Type type = typeof(T);
|
|
||||||
if(value is string strVal) {
|
|
||||||
if(type == typeof(bool))
|
|
||||||
value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
&& !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
else if(type == typeof(string[]))
|
|
||||||
value = strVal.Split(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return (T)Convert.ChangeType(value, type);
|
|
||||||
} catch(InvalidCastException ex) {
|
|
||||||
throw new ConfigTypeException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? SafeReadValue<T>(string name, T? fallback) {
|
|
||||||
try {
|
|
||||||
return ReadValue(name, fallback);
|
|
||||||
} catch(ConfigTypeException) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Config ScopeTo(string prefix) {
|
|
||||||
if(string.IsNullOrWhiteSpace(prefix))
|
|
||||||
throw new ArgumentException("Prefix must exist.", nameof(prefix));
|
|
||||||
if(prefix[^1] != ':')
|
|
||||||
prefix += ':';
|
|
||||||
|
|
||||||
return new ScopedConfig(this, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
|
||||||
return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
~StreamConfig()
|
|
||||||
=> DoDispose();
|
|
||||||
public void Dispose() {
|
|
||||||
DoDispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
private void DoDispose() {
|
|
||||||
if(IsDisposed)
|
|
||||||
return;
|
|
||||||
IsDisposed = true;
|
|
||||||
|
|
||||||
StreamReader.Dispose();
|
|
||||||
Stream.Dispose();
|
|
||||||
Lock.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public T? SafeReadValue<T>(string name, T? fallback) {
|
||||||
|
try {
|
||||||
|
return ReadValue(name, fallback);
|
||||||
|
} catch(ConfigTypeException) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config ScopeTo(string prefix) {
|
||||||
|
if(string.IsNullOrWhiteSpace(prefix))
|
||||||
|
throw new ArgumentException("Prefix must exist.", nameof(prefix));
|
||||||
|
if(prefix[^1] != ':')
|
||||||
|
prefix += ':';
|
||||||
|
|
||||||
|
return new ScopedConfig(this, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CachedValue<T> ReadCached<T>(string name, T? fallback = default, TimeSpan? lifetime = null) {
|
||||||
|
return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDisposed;
|
||||||
|
~StreamConfig()
|
||||||
|
=> DoDispose();
|
||||||
|
public void Dispose() {
|
||||||
|
DoDispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
private void DoDispose() {
|
||||||
|
if(IsDisposed)
|
||||||
|
return;
|
||||||
|
IsDisposed = true;
|
||||||
|
|
||||||
|
StreamReader.Dispose();
|
||||||
|
Stream.Dispose();
|
||||||
|
Lock.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public static class Logger {
|
|
||||||
public static void Write(string str) {
|
|
||||||
Console.WriteLine(string.Format("[{1}] {0}", str, DateTime.Now));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Write(byte[] bytes) {
|
public static class Logger {
|
||||||
Write(Encoding.UTF8.GetString(bytes));
|
public static void Write(string str) {
|
||||||
}
|
Console.WriteLine(string.Format("[{1}] {0}", str, DateTime.Now));
|
||||||
|
}
|
||||||
|
|
||||||
public static void Write(object obj) {
|
public static void Write(byte[] bytes) {
|
||||||
Write(obj?.ToString() ?? string.Empty);
|
Write(Encoding.UTF8.GetString(bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
public static void Write(object obj) {
|
||||||
public static void Debug(string str) {
|
Write(obj?.ToString() ?? string.Empty);
|
||||||
Write(str);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
public static void Debug(byte[] bytes) {
|
public static void Debug(string str) {
|
||||||
Write(bytes);
|
Write(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
public static void Debug(object obj) {
|
public static void Debug(byte[] bytes) {
|
||||||
Write(obj);
|
Write(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
public static void Debug(object obj) {
|
||||||
|
Write(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,82 +1,82 @@
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public static class RNG {
|
|
||||||
public const string CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
|
|
||||||
|
|
||||||
private static Random NormalRandom { get; } = new();
|
public static class RNG {
|
||||||
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
|
public const string CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
|
||||||
|
|
||||||
public static int Next() {
|
private static Random NormalRandom { get; } = new();
|
||||||
return NormalRandom.Next();
|
private static RandomNumberGenerator SecureRandom { get; } = RandomNumberGenerator.Create();
|
||||||
}
|
|
||||||
|
|
||||||
public static int Next(int max) {
|
public static int Next() {
|
||||||
return NormalRandom.Next(max);
|
return NormalRandom.Next();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int Next(int min, int max) {
|
public static int Next(int max) {
|
||||||
return NormalRandom.Next(min, max);
|
return NormalRandom.Next(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void NextBytes(byte[] buffer) {
|
public static int Next(int min, int max) {
|
||||||
|
return NormalRandom.Next(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NextBytes(byte[] buffer) {
|
||||||
|
SecureRandom.GetBytes(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SecureNext() {
|
||||||
|
return SecureNext(int.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SecureNext(int max) {
|
||||||
|
return SecureNext(0, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SecureNext(int min, int max) {
|
||||||
|
--max;
|
||||||
|
if(min == max)
|
||||||
|
return min;
|
||||||
|
|
||||||
|
uint umax = (uint)max - (uint)min;
|
||||||
|
uint num;
|
||||||
|
|
||||||
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(4);
|
||||||
|
try {
|
||||||
SecureRandom.GetBytes(buffer);
|
SecureRandom.GetBytes(buffer);
|
||||||
}
|
num = BitConverter.ToUInt32(buffer);
|
||||||
|
|
||||||
public static int SecureNext() {
|
if(umax != uint.MaxValue) {
|
||||||
return SecureNext(int.MaxValue);
|
++umax;
|
||||||
}
|
|
||||||
|
|
||||||
public static int SecureNext(int max) {
|
if((umax & (umax - 1)) != 0) {
|
||||||
return SecureNext(0, max);
|
uint limit = uint.MaxValue - (uint.MaxValue & umax) - 1;
|
||||||
}
|
|
||||||
|
|
||||||
public static int SecureNext(int min, int max) {
|
while(num > limit) {
|
||||||
--max;
|
SecureRandom.GetBytes(buffer);
|
||||||
if(min == max)
|
num = BitConverter.ToUInt32(buffer);
|
||||||
return min;
|
|
||||||
|
|
||||||
uint umax = (uint)max - (uint)min;
|
|
||||||
uint num;
|
|
||||||
|
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(4);
|
|
||||||
try {
|
|
||||||
SecureRandom.GetBytes(buffer);
|
|
||||||
num = BitConverter.ToUInt32(buffer);
|
|
||||||
|
|
||||||
if(umax != uint.MaxValue) {
|
|
||||||
++umax;
|
|
||||||
|
|
||||||
if((umax & (umax - 1)) != 0) {
|
|
||||||
uint limit = uint.MaxValue - (uint.MaxValue & umax) - 1;
|
|
||||||
|
|
||||||
while(num > limit) {
|
|
||||||
SecureRandom.GetBytes(buffer);
|
|
||||||
num = BitConverter.ToUInt32(buffer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
return (int)((num % umax) + min);
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RandomStringInternal(Func<int, int> next, int length) {
|
return (int)((num % umax) + min);
|
||||||
char[] str = new char[length];
|
}
|
||||||
for(int i = 0; i < length; ++i)
|
|
||||||
str[i] = CHARS[next(CHARS.Length)];
|
|
||||||
return new string(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RandomString(int length) {
|
private static string RandomStringInternal(Func<int, int> next, int length) {
|
||||||
return RandomStringInternal(Next, length);
|
char[] str = new char[length];
|
||||||
}
|
for(int i = 0; i < length; ++i)
|
||||||
|
str[i] = CHARS[next(CHARS.Length)];
|
||||||
|
return new string(str);
|
||||||
|
}
|
||||||
|
|
||||||
public static string SecureRandomString(int length) {
|
public static string RandomString(int length) {
|
||||||
return RandomStringInternal(SecureNext, length);
|
return RandomStringInternal(Next, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SecureRandomString(int length) {
|
||||||
|
return RandomStringInternal(SecureNext, length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public class RateLimiter {
|
|
||||||
private readonly int Size;
|
|
||||||
private readonly int MinimumDelay;
|
|
||||||
private readonly int RiskyOffset;
|
|
||||||
private readonly long[] TimePoints;
|
|
||||||
|
|
||||||
public RateLimiter(int size, int minDelay, int riskyOffset = 0) {
|
public class RateLimiter {
|
||||||
if(size < 2)
|
private readonly int Size;
|
||||||
throw new ArgumentException("Size is too small.", nameof(size));
|
private readonly int MinimumDelay;
|
||||||
if(minDelay < 1000)
|
private readonly int RiskyOffset;
|
||||||
throw new ArgumentException("Minimum delay is inhuman.", nameof(minDelay));
|
private readonly long[] TimePoints;
|
||||||
if(riskyOffset != 0) {
|
|
||||||
if(riskyOffset >= size)
|
|
||||||
throw new ArgumentException("Risky offset may not be greater or equal to the size.", nameof(riskyOffset));
|
|
||||||
else if(riskyOffset < 0)
|
|
||||||
throw new ArgumentException("Risky offset may not be negative.", nameof(riskyOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
Size = size;
|
public RateLimiter(int size, int minDelay, int riskyOffset = 0) {
|
||||||
MinimumDelay = minDelay;
|
if(size < 2)
|
||||||
RiskyOffset = riskyOffset;
|
throw new ArgumentException("Size is too small.", nameof(size));
|
||||||
TimePoints = new long[Size];
|
if(minDelay < 1000)
|
||||||
|
throw new ArgumentException("Minimum delay is inhuman.", nameof(minDelay));
|
||||||
|
if(riskyOffset != 0) {
|
||||||
|
if(riskyOffset >= size)
|
||||||
|
throw new ArgumentException("Risky offset may not be greater or equal to the size.", nameof(riskyOffset));
|
||||||
|
else if(riskyOffset < 0)
|
||||||
|
throw new ArgumentException("Risky offset may not be negative.", nameof(riskyOffset));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsRisky => TimePoints[RiskyOffset] != 0 && TimePoints[RiskyOffset + 1] != 0 && TimePoints[RiskyOffset] + MinimumDelay >= TimePoints[Size - 1];
|
Size = size;
|
||||||
public bool IsExceeded => TimePoints[0] != 0 && TimePoints[1] != 0 && TimePoints[0] + MinimumDelay >= TimePoints[Size - 1];
|
MinimumDelay = minDelay;
|
||||||
|
RiskyOffset = riskyOffset;
|
||||||
|
TimePoints = new long[Size];
|
||||||
|
}
|
||||||
|
|
||||||
public void Update() {
|
public bool IsRisky => TimePoints[RiskyOffset] != 0 && TimePoints[RiskyOffset + 1] != 0 && TimePoints[RiskyOffset] + MinimumDelay >= TimePoints[Size - 1];
|
||||||
for(int i = 1; i < Size; ++i)
|
public bool IsExceeded => TimePoints[0] != 0 && TimePoints[1] != 0 && TimePoints[0] + MinimumDelay >= TimePoints[Size - 1];
|
||||||
TimePoints[i - 1] = TimePoints[i];
|
|
||||||
TimePoints[Size - 1] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
public void Update() {
|
||||||
}
|
for(int i = 1; i < Size; ++i)
|
||||||
|
TimePoints[i - 1] = TimePoints[i];
|
||||||
|
TimePoints[Size - 1] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
public static class SharpInfo {
|
|
||||||
private const string NAME = @"SharpChat";
|
|
||||||
private const string UNKNOWN = @"XXXXXXXXXX";
|
|
||||||
|
|
||||||
public static string VersionString { get; }
|
public static class SharpInfo {
|
||||||
public static string VersionStringShort { get; }
|
private const string NAME = @"SharpChat";
|
||||||
public static bool IsDebugBuild { get; }
|
private const string UNKNOWN = @"XXXXXXXXXX";
|
||||||
|
|
||||||
public static string ProgramName { get; }
|
public static string VersionString { get; }
|
||||||
|
public static string VersionStringShort { get; }
|
||||||
|
public static bool IsDebugBuild { get; }
|
||||||
|
|
||||||
static SharpInfo() {
|
public static string ProgramName { get; }
|
||||||
|
|
||||||
|
static SharpInfo() {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
IsDebugBuild = true;
|
IsDebugBuild = true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
try {
|
try {
|
||||||
using Stream s = Assembly.GetEntryAssembly()!.GetManifestResourceStream(@"SharpChat.version.txt")!;
|
using Stream s = Assembly.GetEntryAssembly()!.GetManifestResourceStream(@"SharpChat.version.txt")!;
|
||||||
using StreamReader sr = new(s);
|
using StreamReader sr = new(s);
|
||||||
VersionString = sr.ReadLine()!.Trim();
|
VersionString = sr.ReadLine()!.Trim();
|
||||||
VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString;
|
VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString;
|
||||||
} catch {
|
} catch {
|
||||||
VersionStringShort = VersionString = UNKNOWN;
|
VersionStringShort = VersionString = UNKNOWN;
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new();
|
|
||||||
sb.Append(NAME);
|
|
||||||
sb.Append('/');
|
|
||||||
sb.Append(VersionStringShort);
|
|
||||||
ProgramName = sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.Append(NAME);
|
||||||
|
sb.Append('/');
|
||||||
|
sb.Append(VersionStringShort);
|
||||||
|
ProgramName = sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace SharpChat.Snowflake {
|
namespace SharpChat.Snowflake;
|
||||||
public class RandomSnowflake(
|
|
||||||
SnowflakeGenerator? generator = null
|
|
||||||
) {
|
|
||||||
public readonly SnowflakeGenerator Generator = generator ?? new SnowflakeGenerator();
|
|
||||||
|
|
||||||
public long Next(DateTimeOffset? at = null) {
|
public class RandomSnowflake(
|
||||||
return Generator.Next(Math.Abs(BitConverter.ToInt64(RandomNumberGenerator.GetBytes(8))), at);
|
SnowflakeGenerator? generator = null
|
||||||
}
|
) {
|
||||||
|
public readonly SnowflakeGenerator Generator = generator ?? new SnowflakeGenerator();
|
||||||
|
|
||||||
|
public long Next(DateTimeOffset? at = null) {
|
||||||
|
return Generator.Next(Math.Abs(BitConverter.ToInt64(RandomNumberGenerator.GetBytes(8))), at);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
namespace SharpChat.Snowflake {
|
namespace SharpChat.Snowflake;
|
||||||
public class SnowflakeGenerator {
|
|
||||||
public const long MASK = 0x7FFFFFFFFFFFFFFF;
|
|
||||||
// previous default epoch was 1588377600000, but snowflakes are much larger than SharpIds
|
|
||||||
public const long EPOCH = 1356998400000;
|
|
||||||
public const byte SHIFT = 16;
|
|
||||||
|
|
||||||
public readonly long Epoch;
|
public class SnowflakeGenerator {
|
||||||
public readonly byte Shift;
|
public const long MASK = 0x7FFFFFFFFFFFFFFF;
|
||||||
public readonly long TimestampMask;
|
// previous default epoch was 1588377600000, but snowflakes are much larger than SharpIds
|
||||||
public readonly long SequenceMask;
|
public const long EPOCH = 1356998400000;
|
||||||
|
public const byte SHIFT = 16;
|
||||||
|
|
||||||
public SnowflakeGenerator(long epoch = EPOCH, byte shift = SHIFT) {
|
public readonly long Epoch;
|
||||||
if(epoch is < 0 or > MASK)
|
public readonly byte Shift;
|
||||||
throw new ArgumentException("Epoch must be a positive int64.", nameof(epoch));
|
public readonly long TimestampMask;
|
||||||
if(shift is < 1 or > 63)
|
public readonly long SequenceMask;
|
||||||
throw new ArgumentException("Shift must be between or equal to 1 and 63", nameof(shift));
|
|
||||||
|
|
||||||
Epoch = epoch;
|
public SnowflakeGenerator(long epoch = EPOCH, byte shift = SHIFT) {
|
||||||
Shift = shift;
|
if(epoch is < 0 or > MASK)
|
||||||
|
throw new ArgumentException("Epoch must be a positive int64.", nameof(epoch));
|
||||||
|
if(shift is < 1 or > 63)
|
||||||
|
throw new ArgumentException("Shift must be between or equal to 1 and 63", nameof(shift));
|
||||||
|
|
||||||
// i think Index only does this as a hack for how integers work in PHP but its gonna run Once per application instance lol
|
Epoch = epoch;
|
||||||
TimestampMask = ~(~0L << (63 - shift));
|
Shift = shift;
|
||||||
SequenceMask = ~(~0L << shift);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long Now(DateTimeOffset? at = null) {
|
// i think Index only does this as a hack for how integers work in PHP but its gonna run Once per application instance lol
|
||||||
return Math.Max(0, (at ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds() - Epoch);
|
TimestampMask = ~(~0L << (63 - shift));
|
||||||
}
|
SequenceMask = ~(~0L << shift);
|
||||||
|
}
|
||||||
|
|
||||||
public long Next(long sequence, DateTimeOffset? at = null) {
|
public long Now(DateTimeOffset? at = null) {
|
||||||
return ((Now(at) & TimestampMask) << Shift) | (sequence & SequenceMask);
|
return Math.Max(0, (at ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds() - Epoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long Next(long sequence, DateTimeOffset? at = null) {
|
||||||
|
return ((Now(at) & TimestampMask) << Shift) | (sequence & SequenceMask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
namespace SharpChat {
|
namespace SharpChat;
|
||||||
[Flags]
|
|
||||||
public enum UserPermissions : int {
|
[Flags]
|
||||||
KickUser = 0x00000001,
|
public enum UserPermissions : int {
|
||||||
BanUser = 0x00000002,
|
KickUser = 0x00000001,
|
||||||
//SilenceUser = 0x00000004,
|
BanUser = 0x00000002,
|
||||||
Broadcast = 0x00000008,
|
//SilenceUser = 0x00000004,
|
||||||
SetOwnNickname = 0x00000010,
|
Broadcast = 0x00000008,
|
||||||
SetOthersNickname = 0x00000020,
|
SetOwnNickname = 0x00000010,
|
||||||
CreateChannel = 0x00000040,
|
SetOthersNickname = 0x00000020,
|
||||||
DeleteChannel = 0x00010000,
|
CreateChannel = 0x00000040,
|
||||||
SetChannelPermanent = 0x00000080,
|
DeleteChannel = 0x00010000,
|
||||||
SetChannelPassword = 0x00000100,
|
SetChannelPermanent = 0x00000080,
|
||||||
SetChannelHierarchy = 0x00000200,
|
SetChannelPassword = 0x00000100,
|
||||||
JoinAnyChannel = 0x00020000,
|
SetChannelHierarchy = 0x00000200,
|
||||||
SendMessage = 0x00000400,
|
JoinAnyChannel = 0x00020000,
|
||||||
DeleteOwnMessage = 0x00000800,
|
SendMessage = 0x00000400,
|
||||||
DeleteAnyMessage = 0x00001000,
|
DeleteOwnMessage = 0x00000800,
|
||||||
EditOwnMessage = 0x00002000,
|
DeleteAnyMessage = 0x00001000,
|
||||||
EditAnyMessage = 0x00004000,
|
EditOwnMessage = 0x00002000,
|
||||||
SeeIPAddress = 0x00008000,
|
EditAnyMessage = 0x00004000,
|
||||||
ViewLogs = 0x00040000,
|
SeeIPAddress = 0x00008000,
|
||||||
}
|
ViewLogs = 0x00040000,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue