Imported master branch.

This commit is contained in:
flash 2022-08-30 17:05:29 +02:00
parent 5d2b9f62c1
commit 435635db2d
441 changed files with 14427 additions and 6131 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
SharpChat.Common/version.txt
# User-specific files
*.suo
*.user

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "hamakaze"]
path = hamakaze
url = https://git.flash.moe/flash/hamakaze.git

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Hamakaze.Headers {
public class HttpAcceptEncodingHeader : HttpHeader {
public const string NAME = @"Accept-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public HttpEncoding[] Encodings { get; }
public HttpAcceptEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpAcceptEncodingHeader(string[] encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
) {}
public HttpAcceptEncodingHeader(IEnumerable<HttpEncoding> encodings) {
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
}
}
}

View File

@ -1,17 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpConnectionHeader : HttpHeader {
public const string NAME = @"Connection";
public override string Name => NAME;
public override object Value { get; }
public const string CLOSE = @"close";
public const string KEEP_ALIVE = @"keep-alive";
public HttpConnectionHeader(string mode) {
Value = mode ?? throw new ArgumentNullException(nameof(mode));
}
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpContentEncodingHeader : HttpHeader {
public const string NAME = @"Content-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public string[] Encodings { get; }
public HttpContentEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpContentEncodingHeader(string[] encodings) {
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
}
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.IO;
namespace Hamakaze.Headers {
public class HttpContentLengthHeader : HttpHeader {
public const string NAME = @"Content-Length";
public override string Name => NAME;
public override object Value => Stream?.Length ?? Length;
private Stream Stream { get; }
private long Length { get; }
public HttpContentLengthHeader(Stream stream) {
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
if(!stream.CanRead || !stream.CanSeek)
throw new ArgumentException(@"Body must readable and seekable.", nameof(stream));
}
public HttpContentLengthHeader(long length) {
Length = length;
}
public HttpContentLengthHeader(string length) {
if(!long.TryParse(length, out long ll))
throw new ArgumentException(@"Invalid length value.", nameof(length));
Length = ll;
}
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpContentTypeHeader : HttpHeader {
public const string NAME = @"Content-Type";
public override string Name => NAME;
public override object Value => MediaType.ToString();
public HttpMediaType MediaType { get; }
public HttpContentTypeHeader(string mediaType) {
MediaType = HttpMediaType.Parse(mediaType ?? throw new ArgumentNullException(nameof(mediaType)));
}
public HttpContentTypeHeader(HttpMediaType mediaType) {
MediaType = mediaType;
}
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpCustomHeader : HttpHeader {
public override string Name { get; }
public override object Value { get; }
public HttpCustomHeader(string name, object value) {
Name = NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
Value = value;
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Globalization;
namespace Hamakaze.Headers {
public class HttpDateHeader : HttpHeader {
public const string NAME = @"Date";
public override string Name => NAME;
public override object Value { get; }
public DateTimeOffset DateTime { get; }
public HttpDateHeader(string dateString) {
Value = dateString ?? throw new ArgumentNullException(nameof(dateString));
DateTime = DateTimeOffset.ParseExact(dateString, @"r", CultureInfo.InvariantCulture);
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Globalization;
namespace Hamakaze.Headers {
public abstract class HttpHeader {
public abstract string Name { get; }
public abstract object Value { get; }
public override string ToString() {
return string.Format(@"{0}: {1}", Name, Value);
}
public static string NormaliseName(string name) {
if(string.IsNullOrWhiteSpace(name))
return string.Empty;
string[] parts = name.ToLowerInvariant().Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for(int i = 0; i < parts.Length; ++i)
parts[i] = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(parts[i]);
return string.Join('-', parts);
}
public static HttpHeader Create(string name, object value) {
return name switch {
HttpTeHeader.NAME => new HttpTeHeader(value.ToString()),
HttpDateHeader.NAME => new HttpDateHeader(value.ToString()),
HttpHostHeader.NAME => new HttpHostHeader(value.ToString()),
HttpServerHeader.NAME => new HttpServerHeader(value.ToString()),
HttpUserAgentHeader.NAME => new HttpUserAgentHeader(value.ToString()),
HttpKeepAliveHeader.NAME => new HttpKeepAliveHeader(value.ToString()),
HttpConnectionHeader.NAME => new HttpConnectionHeader(value.ToString()),
HttpContentTypeHeader.NAME => new HttpContentTypeHeader(value.ToString()),
HttpContentLengthHeader.NAME => new HttpContentLengthHeader(value.ToString()),
HttpAcceptEncodingHeader.NAME => new HttpAcceptEncodingHeader(value.ToString()),
HttpContentEncodingHeader.NAME => new HttpContentEncodingHeader(value.ToString()),
HttpTransferEncodingHeader.NAME => new HttpTransferEncodingHeader(value.ToString()),
_ => new HttpCustomHeader(name, value),
};
}
}
}

View File

@ -1,37 +0,0 @@
using System;
using System.Linq;
using System.Text;
namespace Hamakaze.Headers {
public class HttpHostHeader : HttpHeader {
public const string NAME = @"Host";
public override string Name => NAME;
public override object Value {
get {
StringBuilder sb = new();
sb.Append(Host);
if(Port != -1)
sb.AppendFormat(@":{0}", Port);
return sb.ToString();
}
}
public string Host { get; }
public int Port { get; }
public bool IsSecure { get; }
public HttpHostHeader(string host, int port) {
Host = host;
Port = port;
}
public HttpHostHeader(string hostAndPort) {
string[] parts = hostAndPort.Split(':', 2, StringSplitOptions.TrimEntries);
Host = parts.ElementAtOrDefault(0) ?? throw new ArgumentNullException(nameof(hostAndPort));
if(!ushort.TryParse(parts.ElementAtOrDefault(1), out ushort port))
throw new FormatException(@"Host is not in valid format.");
Port = port;
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
namespace Hamakaze.Headers {
public class HttpKeepAliveHeader : HttpHeader {
public const string NAME = @"Keep-Alive";
public override string Name => NAME;
public override object Value {
get {
List<string> parts = new();
if(MaxIdle != TimeSpan.MaxValue)
parts.Add(string.Format(@"timeout={0}", MaxIdle.TotalSeconds));
if(MaxRequests >= 0)
parts.Add(string.Format(@"max={0}", MaxRequests));
return string.Join(@", ", parts);
}
}
public TimeSpan MaxIdle { get; } = TimeSpan.MaxValue;
public int MaxRequests { get; } = -1;
public HttpKeepAliveHeader(string value) {
IEnumerable<string> kvps = (value ?? throw new ArgumentNullException(nameof(value))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach(string kvp in kvps) {
string[] parts = kvp.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if(parts[0] == @"timeout" && int.TryParse(parts[1], out int timeout))
MaxIdle = TimeSpan.FromSeconds(timeout);
else if(parts[0] == @"max" && int.TryParse(parts[1], out int max))
MaxRequests = max;
}
}
}
}

View File

@ -1,14 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpServerHeader : HttpHeader {
public const string NAME = @"Server";
public override string Name => NAME;
public override object Value { get; }
public HttpServerHeader(string server) {
Value = server ?? throw new ArgumentNullException(nameof(server));
}
}
}

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Hamakaze.Headers {
public class HttpTeHeader : HttpHeader {
public const string NAME = @"TE";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public HttpEncoding[] Encodings { get; }
public HttpTeHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpTeHeader(string[] encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
) { }
public HttpTeHeader(IEnumerable<HttpEncoding> encodings) {
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
}
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpTransferEncodingHeader : HttpHeader {
public const string NAME = @"Transfer-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public string[] Encodings { get; }
public HttpTransferEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) {}
public HttpTransferEncodingHeader(string[] encodings) {
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
}
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace Hamakaze.Headers {
public class HttpUserAgentHeader : HttpHeader {
public const string NAME = @"User-Agent";
public override string Name => NAME;
public override object Value { get; }
public HttpUserAgentHeader(string userAgent) {
if(userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if(string.IsNullOrWhiteSpace(userAgent) || userAgent.Equals(HttpClient.USER_AGENT))
Value = HttpClient.USER_AGENT;
else
Value = string.Format(@"{0} {1}", userAgent, HttpClient.USER_AGENT);
}
}
}

View File

@ -1,118 +0,0 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
namespace Hamakaze {
public class HttpClient : IDisposable {
public const string PRODUCT_STRING = @"HMKZ";
public const string VERSION_MAJOR = @"1";
public const string VERSION_MINOR = @"0";
public const string USER_AGENT = PRODUCT_STRING + @"/" + VERSION_MAJOR + @"." + VERSION_MINOR;
private static HttpClient InstanceValue { get; set; }
public static HttpClient Instance {
get {
if(InstanceValue == null)
InstanceValue = new HttpClient();
return InstanceValue;
}
}
private HttpConnectionManager Connections { get; }
private HttpTaskManager Tasks { get; }
public string DefaultUserAgent { get; set; } = USER_AGENT;
public bool ReuseConnections { get; set; } = true;
public IEnumerable<HttpEncoding> AcceptedEncodings { get; set; } = new[] { HttpEncoding.GZip, HttpEncoding.Deflate, HttpEncoding.Brotli };
public HttpClient() {
Connections = new HttpConnectionManager();
Tasks = new HttpTaskManager();
}
public HttpTask CreateTask(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
if(request == null)
throw new ArgumentNullException(nameof(request));
if(string.IsNullOrWhiteSpace(request.UserAgent))
request.UserAgent = DefaultUserAgent;
if(!request.HasHeader(HttpAcceptEncodingHeader.NAME))
request.AcceptedEncodings = AcceptedEncodings;
request.Connection = ReuseConnections ? HttpConnectionHeader.KEEP_ALIVE : HttpConnectionHeader.CLOSE;
HttpTask task = new(Connections, request, disposeRequest, disposeResponse);
if(onComplete != null)
task.OnComplete += onComplete;
if(onError != null)
task.OnError += onError;
if(onCancel != null)
task.OnCancel += onCancel;
if(onDownloadProgress != null)
task.OnDownloadProgress += onDownloadProgress;
if(onUploadProgress != null)
task.OnUploadProgress += onUploadProgress;
if(onStateChange != null)
task.OnStateChange += onStateChange;
return task;
}
public void RunTask(HttpTask task) {
Tasks.RunTask(task);
}
public void SendRequest(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
RunTask(CreateTask(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse));
}
public static void Send(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
Instance.SendRequest(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse);
}
private bool IsDisposed;
~HttpClient()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Tasks.Dispose();
Connections.Dispose();
}
}
}

View File

@ -1,81 +0,0 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading;
namespace Hamakaze {
public class HttpConnection : IDisposable {
public IPEndPoint EndPoint { get; }
public Stream Stream { get; }
public Socket Socket { get; }
public NetworkStream NetworkStream { get; }
public SslStream SslStream { get; }
public string Host { get; }
public bool IsSecure { get; }
public bool HasTimedOut => MaxRequests == 0 || (DateTimeOffset.Now - LastOperation) > MaxIdle;
public int MaxRequests { get; set; } = -1;
public TimeSpan MaxIdle { get; set; } = TimeSpan.MaxValue;
public DateTimeOffset LastOperation { get; private set; } = DateTimeOffset.Now;
public bool InUse { get; private set; }
public HttpConnection(string host, IPEndPoint endPoint, bool secure) {
Host = host ?? throw new ArgumentNullException(nameof(host));
EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
IsSecure = secure;
if(endPoint.AddressFamily is not AddressFamily.InterNetwork and not AddressFamily.InterNetworkV6)
throw new ArgumentException(@"Address must be an IPv4 or IPv6 address.", nameof(endPoint));
Socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {
NoDelay = true,
Blocking = true,
};
Socket.Connect(endPoint);
NetworkStream = new NetworkStream(Socket, true);
if(IsSecure) {
SslStream = new SslStream(NetworkStream, false, (s, ce, ch, e) => e == SslPolicyErrors.None, null);
Stream = SslStream;
SslStream.AuthenticateAsClient(Host, null, SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, true);
} else
Stream = NetworkStream;
}
public void MarkUsed() {
LastOperation = DateTimeOffset.Now;
if(MaxRequests > 0)
--MaxRequests;
}
public bool Acquire() {
return !InUse && (InUse = true);
}
public void Release() {
InUse = false;
}
private bool IsDisposed;
~HttpConnection()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Stream.Dispose();
}
}
}

View File

@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
namespace Hamakaze {
public class HttpConnectionManager : IDisposable {
private List<HttpConnection> Connections { get; } = new();
private Mutex Lock { get; } = new();
public HttpConnectionManager() {
}
private void AcquireLock() {
if(!Lock.WaitOne(10000))
throw new HttpConnectionManagerLockException();
}
private void ReleaseLock() {
Lock.ReleaseMutex();
}
public HttpConnection CreateConnection(string host, IPEndPoint endPoint, bool secure) {
if(host == null)
throw new ArgumentNullException(nameof(host));
if(endPoint == null)
throw new ArgumentNullException(nameof(endPoint));
HttpConnection conn = null;
AcquireLock();
try {
conn = CreateConnectionInternal(host, endPoint, secure);
} finally {
ReleaseLock();
}
return conn;
}
private HttpConnection CreateConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
HttpConnection conn = new(host, endPoint, secure);
Connections.Add(conn);
return conn;
}
public HttpConnection GetConnection(string host, IPEndPoint endPoint, bool secure) {
if(host == null)
throw new ArgumentNullException(nameof(host));
if(endPoint == null)
throw new ArgumentNullException(nameof(endPoint));
HttpConnection conn = null;
AcquireLock();
try {
conn = GetConnectionInternal(host, endPoint, secure);
} finally {
ReleaseLock();
}
return conn;
}
private HttpConnection GetConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
CleanConnectionsInternal();
HttpConnection conn = Connections.FirstOrDefault(c => host.Equals(c.Host) && endPoint.Equals(c.EndPoint) && c.IsSecure == secure && c.Acquire());
if(conn == null) {
conn = CreateConnectionInternal(host, endPoint, secure);
conn.Acquire();
}
return conn;
}
public void EndConnection(HttpConnection conn) {
if(conn == null)
throw new ArgumentNullException(nameof(conn));
AcquireLock();
try {
EndConnectionInternal(conn);
} finally {
ReleaseLock();
}
}
private void EndConnectionInternal(HttpConnection conn) {
Connections.Remove(conn);
conn.Dispose();
}
public void CleanConnection() {
AcquireLock();
try {
CleanConnectionsInternal();
} finally {
ReleaseLock();
}
}
private void CleanConnectionsInternal() {
IEnumerable<HttpConnection> conns = Connections.Where(x => x.HasTimedOut).ToArray();
foreach(HttpConnection conn in conns) {
Connections.Remove(conn);
conn.Dispose();
}
}
private bool IsDisposed;
~HttpConnectionManager()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Lock.Dispose();
foreach(HttpConnection conn in Connections)
conn.Dispose();
Connections.Clear();
}
}
}

View File

@ -1,69 +0,0 @@
using System;
using System.Globalization;
using System.Text;
namespace Hamakaze {
public readonly struct HttpEncoding : IComparable<HttpEncoding?>, IEquatable<HttpEncoding?> {
public const string DEFLATE = @"deflate";
public const string GZIP = @"gzip";
public const string XGZIP = @"x-gzip";
public const string BROTLI = @"br";
public const string IDENTITY = @"identity";
public const string CHUNKED = @"chunked";
public const string ANY = @"*";
public static readonly HttpEncoding Any = new(ANY);
public static readonly HttpEncoding None = new(ANY, 0f);
public static readonly HttpEncoding Deflate = new(DEFLATE);
public static readonly HttpEncoding GZip = new(GZIP);
public static readonly HttpEncoding Brotli = new(BROTLI);
public static readonly HttpEncoding Identity = new(IDENTITY);
public string Name { get; }
public float Quality { get; }
public HttpEncoding(string name, float quality = 1f) {
Name = name ?? throw new ArgumentNullException(nameof(name));
Quality = quality;
}
public HttpEncoding WithQuality(float quality) {
return new HttpEncoding(Name, quality);
}
public static HttpEncoding Parse(string encoding) {
string[] parts = encoding.Split(';', StringSplitOptions.TrimEntries);
float quality = 1f;
encoding = parts[0];
for(int i = 1; i < parts.Length; ++i)
if(parts[i].StartsWith(@"q=")) {
if(!float.TryParse(parts[i], out quality))
quality = 1f;
break;
}
return new HttpEncoding(encoding, quality);
}
public override string ToString() {
StringBuilder sb = new();
sb.Append(Name);
if(Quality is >= 0f and < 1f)
sb.AppendFormat(CultureInfo.InvariantCulture, @";q={0:0.0}", Quality);
return sb.ToString();
}
public int CompareTo(HttpEncoding? other) {
if(!other.HasValue || other.Value.Quality < Quality)
return -1;
if(other.Value.Quality > Quality)
return 1;
return 0;
}
public bool Equals(HttpEncoding? other) {
return other.HasValue && Name.Equals(other.Value.Name) && Quality.Equals(other.Value.Quality);
}
}
}

View File

@ -1,40 +0,0 @@
using System;
namespace Hamakaze {
public class HttpException : Exception {
public HttpException(string message) : base(message) { }
}
public class HttpConnectionManagerException : HttpException {
public HttpConnectionManagerException(string message) : base(message) { }
}
public class HttpConnectionManagerLockException : HttpConnectionManagerException {
public HttpConnectionManagerLockException() : base(@"Failed to lock the connection manager in time.") { }
}
public class HttpTaskException : HttpException {
public HttpTaskException(string message) : base(message) { }
}
public class HttpTaskAlreadyStartedException : HttpTaskException {
public HttpTaskAlreadyStartedException() : base(@"Task has already started.") { }
}
public class HttpTaskInvalidStateException : HttpTaskException {
public HttpTaskInvalidStateException() : base(@"Task has ended up in an invalid state.") { }
}
public class HttpTaskNoAddressesException : HttpTaskException {
public HttpTaskNoAddressesException() : base(@"Could not find any addresses for this host.") { }
}
public class HttpTaskNoConnectionException : HttpTaskException {
public HttpTaskNoConnectionException() : base(@"Was unable to create a connection with this host.") { }
}
public class HttpTaskRequestFailedException : HttpTaskException {
public HttpTaskRequestFailedException() : base(@"Request failed for unknown reasons.") { }
}
public class HttpTaskManagerException : HttpException {
public HttpTaskManagerException(string message) : base(message) { }
}
public class HttpTaskManagerLockException : HttpTaskManagerException {
public HttpTaskManagerLockException() : base(@"Failed to reserve a thread.") { }
}
}

View File

@ -1,159 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Hamakaze {
public readonly struct HttpMediaType : IComparable<HttpMediaType?>, IEquatable<HttpMediaType?> {
public const string TYPE_APPLICATION = @"application";
public const string TYPE_AUDIO = @"audio";
public const string TYPE_IMAGE = @"image";
public const string TYPE_MESSAGE = @"message";
public const string TYPE_MULTIPART = @"multipart";
public const string TYPE_TEXT = @"text";
public const string TYPE_VIDEO = @"video";
public static readonly HttpMediaType OctetStream = new(TYPE_APPLICATION, @"octet-stream");
public static readonly HttpMediaType FWIF = new(TYPE_APPLICATION, @"x.fwif");
public static readonly HttpMediaType JSON = new(TYPE_APPLICATION, @"json");
public static readonly HttpMediaType HTML = new(TYPE_TEXT, @"html", args: new[] { Param.UTF8 });
public string Type { get; }
public string Subtype { get; }
public string Suffix { get; }
public IEnumerable<Param> Params { get; }
public HttpMediaType(string type, string subtype, string suffix = null, IEnumerable<Param> args = null) {
Type = type ?? throw new ArgumentNullException(nameof(type));
Subtype = subtype ?? throw new ArgumentNullException(nameof(subtype));
Suffix = suffix ?? string.Empty;
Params = args ?? Enumerable.Empty<Param>();
}
public string GetParamValue(string name) {
foreach(Param param in Params)
if(param.Name.ToLowerInvariant() == name)
return param.Value;
return null;
}
public static explicit operator HttpMediaType(string mediaTypeString) => Parse(mediaTypeString);
public static HttpMediaType Parse(string mediaTypeString) {
if(mediaTypeString == null)
throw new ArgumentNullException(nameof(mediaTypeString));
int slashIndex = mediaTypeString.IndexOf('/');
if(slashIndex == -1)
return OctetStream;
string type = mediaTypeString[..slashIndex];
string subtype = mediaTypeString[(slashIndex + 1)..];
string suffix = null;
IEnumerable<Param> args = null;
int paramIndex = subtype.IndexOf(';');
if(paramIndex != -1) {
args = subtype[(paramIndex + 1)..]
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(Param.Parse);
subtype = subtype[..paramIndex];
}
int suffixIndex = subtype.IndexOf('+');
if(suffixIndex != -1) {
suffix = subtype[(suffixIndex + 1)..];
subtype = subtype[..suffixIndex];
}
return new HttpMediaType(type, subtype, suffix, args);
}
public override string ToString() {
StringBuilder sb = new();
sb.AppendFormat(@"{0}/{1}", Type, Subtype);
if(!string.IsNullOrWhiteSpace(Suffix))
sb.AppendFormat(@"+{0}", Suffix);
if(Params.Any())
sb.AppendFormat(@";{0}", string.Join(';', Params));
return sb.ToString();
}
public int CompareTo(HttpMediaType? other) {
if(!other.HasValue)
return -1;
int type = Type.CompareTo(other.Value.Type);
if(type != 0)
return type;
int subtype = Subtype.CompareTo(other.Value.Subtype);
if(subtype != 0)
return subtype;
int suffix = Suffix.CompareTo(other.Value.Suffix);
if(suffix != 0)
return suffix;
int paramCount = Params.Count();
int args = paramCount - other.Value.Params.Count();
if(args != 0)
return args;
for(int i = 0; i < paramCount; ++i) {
args = Params.ElementAt(i).CompareTo(other.Value.Params.ElementAt(i));
if(args != 0)
return args;
}
return 0;
}
public bool Equals(HttpMediaType? other) {
if(!other.HasValue)
return false;
if(!Type.Equals(other.Value.Type) || !Subtype.Equals(other.Value.Subtype) || !Suffix.Equals(other.Value.Suffix))
return false;
int paramCount = Params.Count();
if(paramCount != other.Value.Params.Count())
return false;
for(int i = 0; i < paramCount; ++i)
if(!Params.ElementAt(i).Equals(other.Value.Params.ElementAt(i)))
return false;
return true;
}
public readonly struct Param : IComparable<Param?>, IEquatable<Param?> {
public const string CHARSET = @"charset";
public static readonly Param ASCII = new(CHARSET, @"us-ascii");
public static readonly Param UTF8 = new(CHARSET, @"utf-8");
public string Name { get; }
public string Value { get; }
public Param(string name, string value) {
Name = name ?? throw new ArgumentNullException(nameof(name));
Value = value ?? throw new ArgumentNullException(nameof(name));
}
public override string ToString() {
return string.Format(@"{0}={1}", Name, Value);
}
public static explicit operator Param(string paramStr) => Parse(paramStr);
public static Param Parse(string paramStr) {
string[] parts = (paramStr ?? throw new ArgumentNullException(nameof(paramStr))).Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return new Param(parts[0], parts[1]);
}
public int CompareTo(Param? other) {
if(!other.HasValue)
return -1;
int name = Name.CompareTo(other.Value.Name);
return name != 0
? name
: Value.CompareTo(other.Value.Value);
}
public bool Equals(Param? other) {
return other.HasValue && Name.Equals(other.Value.Name) && Value.Equals(other.Value.Value);
}
}
}
}

View File

@ -1,46 +0,0 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Hamakaze {
public abstract class HttpMessage : IDisposable {
public abstract string ProtocolVersion { get; }
public abstract IEnumerable<HttpHeader> Headers { get; }
public abstract Stream Body { get; }
public virtual bool HasBody => Body != null;
protected bool OwnsBodyStream { get; set; }
public virtual IEnumerable<HttpHeader> GetHeader(string header) {
header = HttpHeader.NormaliseName(header);
return Headers.Where(h => h.Name == header);
}
public virtual bool HasHeader(string header) {
header = HttpHeader.NormaliseName(header);
return Headers.Any(h => h.Name == header);
}
public virtual string GetHeaderLine(string header) {
return string.Join(@", ", GetHeader(header).Select(h => h.Value));
}
private bool IsDisposed;
~HttpMessage()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
protected void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
if(OwnsBodyStream && Body != null)
Body.Dispose();
}
}
}

View File

@ -1,190 +0,0 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Hamakaze {
public class HttpRequestMessage : HttpMessage {
public const string GET = @"GET";
public const string PUT = @"PUT";
public const string HEAD = @"HEAD";
public const string POST = @"POST";
public const string DELETE = @"DELETE";
public override string ProtocolVersion => @"1.1";
public string Method { get; }
public string RequestTarget { get; }
public bool IsSecure { get; }
public string Host { get; }
public ushort Port { get; }
public bool IsDefaultPort { get; }
public override IEnumerable<HttpHeader> Headers => HeaderList;
private List<HttpHeader> HeaderList { get; } = new();
private Stream BodyStream { get; set; }
public override Stream Body {
get {
if(BodyStream == null) {
OwnsBodyStream = true;
SetBody(new MemoryStream());
}
return BodyStream;
}
}
private static readonly string[] HEADERS_READONLY = new[] {
HttpHostHeader.NAME, HttpContentLengthHeader.NAME,
};
private static readonly string[] HEADERS_SINGLE = new[] {
HttpUserAgentHeader.NAME, HttpConnectionHeader.NAME, HttpAcceptEncodingHeader.NAME,
};
public IEnumerable<HttpEncoding> AcceptedEncodings {
get => HeaderList.Where(x => x.Name == HttpAcceptEncodingHeader.NAME).Cast<HttpAcceptEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<HttpEncoding>();
set {
HeaderList.RemoveAll(x => x.Name == HttpAcceptEncodingHeader.NAME);
HeaderList.Add(new HttpAcceptEncodingHeader(value));
}
}
public string UserAgent {
get => HeaderList.FirstOrDefault(x => x.Name == HttpUserAgentHeader.NAME)?.Value.ToString()
?? string.Empty;
set {
HeaderList.RemoveAll(x => x.Name == HttpUserAgentHeader.NAME);
HeaderList.Add(new HttpUserAgentHeader(value));
}
}
public string Connection {
get => HeaderList.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString()
?? string.Empty;
set {
HeaderList.RemoveAll(x => x.Name == HttpConnectionHeader.NAME);
HeaderList.Add(new HttpConnectionHeader(value));
}
}
public HttpMediaType ContentType {
get => HeaderList.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
?? HttpMediaType.OctetStream;
set {
HeaderList.RemoveAll(x => x.Name == HttpContentTypeHeader.NAME);
HeaderList.Add(new HttpContentTypeHeader(value));
}
}
public HttpRequestMessage(string method, string uri) : this(
method, new Uri(uri)
) {}
public const ushort HTTP = 80;
public const ushort HTTPS = 443;
public HttpRequestMessage(string method, Uri uri) {
Method = method ?? throw new ArgumentNullException(nameof(method));
RequestTarget = uri.PathAndQuery;
IsSecure = uri.Scheme.Equals(@"https", StringComparison.InvariantCultureIgnoreCase);
Host = uri.Host;
ushort defaultPort = (IsSecure ? HTTPS : HTTP);
Port = uri.Port == -1 ? defaultPort : (ushort)uri.Port;
IsDefaultPort = Port == defaultPort;
HeaderList.Add(new HttpHostHeader(Host, IsDefaultPort ? -1 : Port));
}
public static bool IsHeaderReadOnly(string name)
=> HEADERS_READONLY.Contains(name ?? throw new ArgumentNullException(nameof(name)));
public static bool IsHeaderSingleInstance(string name)
=> HEADERS_SINGLE.Contains(name ?? throw new ArgumentNullException(nameof(name)));
public void SetHeader(string name, object value) {
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
if(IsHeaderReadOnly(name))
throw new ArgumentException(@"This header is read-only.", nameof(name));
HeaderList.RemoveAll(x => x.Name == name);
HeaderList.Add(HttpHeader.Create(name, value));
}
public void AddHeader(string name, object value) {
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
if(IsHeaderReadOnly(name))
throw new ArgumentException(@"This header is read-only.", nameof(name));
if(IsHeaderSingleInstance(name))
HeaderList.RemoveAll(x => x.Name == name);
HeaderList.Add(HttpHeader.Create(name, value));
}
public void RemoveHeader(string name) {
name = HttpHeader.NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
if(IsHeaderReadOnly(name))
throw new ArgumentException(@"This header is read-only.", nameof(name));
HeaderList.RemoveAll(x => x.Name == name);
}
public void SetBody(Stream stream) {
if(stream == null) {
if(OwnsBodyStream)
BodyStream?.Dispose();
OwnsBodyStream = false;
BodyStream = null;
HeaderList.RemoveAll(x => x.Name == HttpContentLengthHeader.NAME);
} else {
if(!stream.CanRead || !stream.CanSeek)
throw new ArgumentException(@"Body must readable and seekable.", nameof(stream));
if(OwnsBodyStream)
BodyStream?.Dispose();
OwnsBodyStream = false;
BodyStream = stream;
HeaderList.Add(new HttpContentLengthHeader(BodyStream));
}
}
public void SetBody(byte[] buffer) {
SetBody(new MemoryStream(buffer));
OwnsBodyStream = true;
}
public void SetBody(string str, Encoding encoding = null) {
SetBody((encoding ?? Encoding.UTF8).GetBytes(str));
}
public void WriteTo(Stream stream, Action<long, long> onProgress = null) {
using(StreamWriter sw = new(stream, new ASCIIEncoding(), leaveOpen: true)) {
sw.NewLine = "\r\n";
sw.Write(Method);
sw.Write(' ');
sw.Write(RequestTarget);
sw.Write(@" HTTP/");
sw.WriteLine(ProtocolVersion);
foreach(HttpHeader header in Headers)
sw.WriteLine(header);
sw.WriteLine();
sw.Flush();
}
if(BodyStream != null) {
const int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
int read;
long totalRead = 0;
onProgress?.Invoke(totalRead, BodyStream.Length);
BodyStream.Seek(0, SeekOrigin.Begin);
while((read = BodyStream.Read(buffer, 0, bufferSize)) > 0) {
stream.Write(buffer, 0, read);
totalRead += read;
onProgress?.Invoke(totalRead, BodyStream.Length);
}
}
}
}
}

View File

@ -1,265 +0,0 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
namespace Hamakaze {
public class HttpResponseMessage : HttpMessage {
public override string ProtocolVersion { get; }
public int StatusCode { get; }
public string StatusMessage { get; }
public override IEnumerable<HttpHeader> Headers { get; }
public override Stream Body { get; }
public string Connection
=> Headers.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString() ?? string.Empty;
public string Server
=> Headers.FirstOrDefault(x => x.Name == HttpServerHeader.NAME)?.Value.ToString() ?? string.Empty;
public DateTimeOffset Date
=> Headers.Where(x => x.Name == HttpDateHeader.NAME).Cast<HttpDateHeader>().FirstOrDefault()?.DateTime ?? DateTimeOffset.MinValue;
public HttpMediaType ContentType
=> Headers.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
?? HttpMediaType.OctetStream;
public Encoding ResponseEncoding
=> Encoding.GetEncoding(ContentType.GetParamValue(@"charset") ?? @"iso8859-1");
public IEnumerable<string> ContentEncodings
=> Headers.Where(x => x.Name == HttpContentEncodingHeader.NAME).Cast<HttpContentEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<string>();
public IEnumerable<string> TransferEncodings
=> Headers.Where(x => x.Name == HttpTransferEncodingHeader.NAME).Cast<HttpTransferEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<string>();
public HttpResponseMessage(
int statusCode, string statusMessage, string protocolVersion,
IEnumerable<HttpHeader> headers, Stream body
) {
ProtocolVersion = protocolVersion ?? throw new ArgumentNullException(nameof(protocolVersion));
StatusCode = statusCode;
StatusMessage = statusMessage ?? string.Empty;
Headers = (headers ?? throw new ArgumentNullException(nameof(headers))).ToArray();
OwnsBodyStream = true;
Body = body;
}
public byte[] GetBodyBytes() {
if(Body == null)
return null;
if(Body is MemoryStream msBody)
return msBody.ToArray();
using MemoryStream ms = new();
if(Body.CanSeek)
Body.Seek(0, SeekOrigin.Begin);
Body.CopyTo(ms);
return ms.ToArray();
}
public string GetBodyString() {
byte[] bytes = GetBodyBytes();
return bytes == null || bytes.Length < 1
? string.Empty
: ResponseEncoding.GetString(bytes);
}
// there's probably a less stupid way to do this, be my guest and call me an idiot
private static void ProcessEncoding(Stack<string> encodings, Stream stream, bool transfer) {
using MemoryStream temp = new();
bool inTemp = false;
while(encodings.TryPop(out string encoding)) {
Stream target = (inTemp = !inTemp) ? temp : stream,
source = inTemp ? stream : temp;
target.SetLength(0);
source.Seek(0, SeekOrigin.Begin);
switch(encoding) {
case HttpEncoding.GZIP:
case HttpEncoding.XGZIP:
using(GZipStream gzs = new(source, CompressionMode.Decompress, true))
gzs.CopyTo(target);
break;
case HttpEncoding.DEFLATE:
using(DeflateStream def = new(source, CompressionMode.Decompress, true))
def.CopyTo(target);
break;
case HttpEncoding.BROTLI:
if(transfer)
goto default;
using(BrotliStream br = new(source, CompressionMode.Decompress, true))
br.CopyTo(target);
break;
case HttpEncoding.IDENTITY:
break;
case HttpEncoding.CHUNKED:
if(!transfer)
goto default;
throw new IOException(@"Invalid use of chunked encoding type in Transfer-Encoding header.");
default:
throw new IOException(@"Unsupported encoding supplied.");
}
}
if(inTemp) {
stream.SetLength(0);
temp.Seek(0, SeekOrigin.Begin);
temp.CopyTo(stream);
}
}
public static HttpResponseMessage ReadFrom(Stream stream, Action<long, long> onProgress = null) {
// ignore this function, it doesn't exist
string readLine() {
const ushort crlf = 0x0D0A;
using MemoryStream ms = new();
int byt; ushort lastTwo = 0;
for(; ; ) {
byt = stream.ReadByte();
if(byt == -1 && ms.Length == 0)
return null;
ms.WriteByte((byte)byt);
lastTwo <<= 8;
lastTwo |= (byte)byt;
if(lastTwo == crlf) {
ms.SetLength(ms.Length - 2);
break;
}
}
return Encoding.ASCII.GetString(ms.ToArray());
}
long contentLength = -1;
Stack<string> transferEncodings = null;
Stack<string> contentEncodings = null;
// Read initial header
string line = readLine();
if(line == null)
throw new IOException(@"Failed to read initial HTTP header.");
if(!line.StartsWith(@"HTTP/"))
throw new IOException(@"Response is not a valid HTTP message.");
string[] parts = line[5..].Split(' ', 3);
if(!int.TryParse(parts.ElementAtOrDefault(1), out int statusCode))
throw new IOException(@"Invalid HTTP status code format.");
string protocolVersion = parts.ElementAtOrDefault(0);
string statusMessage = parts.ElementAtOrDefault(2);
// Read header key-value pairs
List<HttpHeader> headers = new();
while((line = readLine()) != null) {
if(string.IsNullOrWhiteSpace(line))
break;
parts = line.Split(':', 2, StringSplitOptions.TrimEntries);
if(parts.Length < 2)
throw new IOException(@"Invalid HTTP header in response.");
string hName = HttpHeader.NormaliseName(parts.ElementAtOrDefault(0) ?? string.Empty),
hValue = parts.ElementAtOrDefault(1);
if(string.IsNullOrEmpty(hName))
throw new IOException(@"Invalid HTTP header name.");
HttpHeader header = HttpHeader.Create(hName, hValue);
if(header is HttpContentLengthHeader hclh)
contentLength = (long)hclh.Value;
else if(header is HttpTransferEncodingHeader hteh)
transferEncodings = new Stack<string>(hteh.Encodings);
else if(header is HttpContentEncodingHeader hceh)
contentEncodings = new Stack<string>(hceh.Encodings);
headers.Add(header);
}
if(statusCode is < 200 or 201 or 204 or 205)
contentLength = 0;
Stream body = null;
long totalRead = 0;
const int buffer_size = 8192;
byte[] buffer = new byte[buffer_size];
int read;
void readBuffer(long length = -1) {
if(length == 0)
return;
long remaining = length;
int bufferRead = buffer_size;
if(bufferRead > length)
bufferRead = (int)length;
if(totalRead < 1)
onProgress?.Invoke(0, contentLength);
while((read = stream.Read(buffer, 0, bufferRead)) > 0) {
body.Write(buffer, 0, read);
totalRead += read;
onProgress?.Invoke(totalRead, contentLength);
if(length >= 0) {
remaining -= read;
if(remaining < 1)
break;
if(bufferRead > remaining)
bufferRead = (int)remaining;
}
}
}
// Read body
if(transferEncodings != null && transferEncodings.Any() && transferEncodings.Peek() == HttpEncoding.CHUNKED) {
// oh no the poop is chunky
transferEncodings.Pop();
body = new MemoryStream();
while((line = readLine()) != null) {
if(string.IsNullOrWhiteSpace(line))
break;
if(!int.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int chunkLength))
throw new IOException(@"Failed to decode chunk length.");
if(chunkLength == 0) // final chunk
break;
readBuffer(chunkLength);
readLine();
}
readLine();
} else if(contentLength != 0) {
body = new MemoryStream();
readBuffer(contentLength);
readLine();
}
if(body != null)
// Check if body is empty and null it again if so
if(body.Length == 0) {
body.Dispose();
body = null;
} else {
if(transferEncodings != null)
ProcessEncoding(transferEncodings, body, true);
if(contentEncodings != null)
ProcessEncoding(contentEncodings, body, false);
body.Seek(0, SeekOrigin.Begin);
}
return new HttpResponseMessage(statusCode, statusMessage, protocolVersion, headers, body);
}
}
}

View File

@ -1,189 +0,0 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
namespace Hamakaze {
public class HttpTask {
public TaskState State { get; private set; } = TaskState.Initial;
public bool IsStarted
=> State != TaskState.Initial;
public bool IsFinished
=> State == TaskState.Finished;
public bool IsCancelled
=> State == TaskState.Cancelled;
public bool IsErrored
=> Exception != null;
public Exception Exception { get; private set; }
public HttpRequestMessage Request { get; }
public HttpResponseMessage Response { get; private set; }
private HttpConnectionManager Connections { get; }
private IEnumerable<IPAddress> Addresses { get; set; }
private HttpConnection Connection { get; set; }
public bool DisposeRequest { get; set; }
public bool DisposeResponse { get; set; }
public event Action<HttpTask, HttpResponseMessage> OnComplete;
public event Action<HttpTask, Exception> OnError;
public event Action<HttpTask> OnCancel;
public event Action<HttpTask, long, long> OnUploadProgress;
public event Action<HttpTask, long, long> OnDownloadProgress;
public event Action<HttpTask, TaskState> OnStateChange;
public HttpTask(HttpConnectionManager conns, HttpRequestMessage request, bool disposeRequest, bool disposeResponse) {
Connections = conns ?? throw new ArgumentNullException(nameof(conns));
Request = request ?? throw new ArgumentNullException(nameof(request));
DisposeRequest = disposeRequest;
DisposeResponse = disposeResponse;
}
public void Run() {
if(IsStarted)
throw new HttpTaskAlreadyStartedException();
while(NextStep());
}
public void Cancel() {
State = TaskState.Cancelled;
OnStateChange?.Invoke(this, State);
OnCancel?.Invoke(this);
if(DisposeResponse)
Response?.Dispose();
if(DisposeRequest)
Request?.Dispose();
}
private void Error(Exception ex) {
Exception = ex;
OnError?.Invoke(this, ex);
Cancel();
}
public bool NextStep() {
if(IsCancelled)
return false;
switch(State) {
case TaskState.Initial:
State = TaskState.Lookup;
OnStateChange?.Invoke(this, State);
DoLookup();
break;
case TaskState.Lookup:
State = TaskState.Request;
OnStateChange?.Invoke(this, State);
DoRequest();
break;
case TaskState.Request:
State = TaskState.Response;
OnStateChange?.Invoke(this, State);
DoResponse();
break;
case TaskState.Response:
State = TaskState.Finished;
OnStateChange?.Invoke(this, State);
OnComplete?.Invoke(this, Response);
if(DisposeResponse)
Response?.Dispose();
if(DisposeRequest)
Request?.Dispose();
return false;
default:
Error(new HttpTaskInvalidStateException());
return false;
}
return true;
}
private void DoLookup() {
try {
Addresses = Dns.GetHostAddresses(Request.Host);
} catch(Exception ex) {
Error(ex);
return;
}
if(!Addresses.Any())
Error(new HttpTaskNoAddressesException());
}
private void DoRequest() {
Exception exception = null;
try {
foreach(IPAddress addr in Addresses) {
int tries = 0;
IPEndPoint endPoint = new(addr, Request.Port);
exception = null;
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
retry:
++tries;
try {
Request.WriteTo(Connection.Stream, (p, t) => OnUploadProgress?.Invoke(this, p, t));
break;
} catch(IOException ex) {
Connection.Dispose();
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
if(tries < 2)
goto retry;
exception = ex;
continue;
} finally {
Connection.MarkUsed();
}
}
} catch(Exception ex) {
Error(ex);
}
if(exception != null)
Error(exception);
else if(Connection == null)
Error(new HttpTaskNoConnectionException());
}
private void DoResponse() {
try {
Response = HttpResponseMessage.ReadFrom(Connection.Stream, (p, t) => OnDownloadProgress?.Invoke(this, p, t));
} catch(Exception ex) {
Error(ex);
return;
}
if(Response.Connection == HttpConnectionHeader.CLOSE)
Connection.Dispose();
if(Response == null)
Error(new HttpTaskRequestFailedException());
HttpKeepAliveHeader hkah = Response.Headers.Where(x => x.Name == HttpKeepAliveHeader.NAME).Cast<HttpKeepAliveHeader>().FirstOrDefault();
if(hkah != null) {
Connection.MaxIdle = hkah.MaxIdle;
Connection.MaxRequests = hkah.MaxRequests;
}
Connection.Release();
}
public enum TaskState {
Initial = 0,
Lookup = 10,
Request = 20,
Response = 30,
Finished = 40,
Cancelled = -1,
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Threading;
namespace Hamakaze {
public class HttpTaskManager : IDisposable {
private Semaphore Lock { get; set; }
public HttpTaskManager(int maxThreads = 5) {
Lock = new Semaphore(maxThreads, maxThreads);
}
public void RunTask(HttpTask task) {
if(task == null)
throw new ArgumentNullException(nameof(task));
if(!Lock.WaitOne())
throw new HttpTaskManagerLockException();
new Thread(() => {
try {
task.Run();
} finally {
Lock?.Release();
}
}).Start();
}
private bool IsDisposed;
~HttpTaskManager()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Lock.Dispose();
Lock = null;
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\hamakaze\Hamakaze\Hamakaze.csproj" />
</ItemGroup>
</Project>

147
HttpClientTest/Program.cs Normal file
View File

@ -0,0 +1,147 @@
using Hamakaze;
using System;
using System.Threading;
using static System.Console;
namespace HttpClientTest {
public static class Program {
public static void Main(string[] args) {
ResetColor();
HttpClient.Instance.DefaultUserAgent = @"SharpChat/1.0";
/*string[] commonMediaTypes = new[] {
@"application/x-executable",
@"application/graphql",
@"application/javascript",
@"application/x.fwif",
@"application/json",
@"application/ld+json",
@"application/msword",
@"application/pdf",
@"application/sql",
@"application/vnd.api+json",
@"application/vnd.ms-excel",
@"application/vnd.ms-powerpoint",
@"application/vnd.oasis.opendocument.text",
@"application/vnd.openxmlformats-officedocument.presentationml.presentation",
@"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@"application/x-www-form-urlencoded",
@"application/xml",
@"application/zip",
@"application/zstd",
@"audio/mpeg",
@"audio/ogg",
@"image/gif",
@"image/apng",
@"image/flif",
@"image/webp",
@"image/x-mng",
@"image/jpeg",
@"image/png",
@"multipart/form-data",
@"text/css",
@"text/csv",
@"text/html",
@"text/php",
@"text/plain",
@"text/xml",
@"text/html; charset=utf-8",
};
Logger.Write(@"Testing Media Type parsing...");
foreach(string mts in commonMediaTypes) {
HttpMediaType hmt = HttpMediaType.Parse(mts);
Logger.Write($@"O {mts}");
Logger.Write($@"P {hmt}");
}
return;*/
static void setForeground(ConsoleColor color) {
ResetColor();
ForegroundColor = color;
}
using ManualResetEvent mre = new(false);
bool kill = false;
string[] urls = {
@"https://flashii.net/",
@"https://flashii.net/changelog",
@"https://abyss.flash.moe/",
@"https://flashii.net/info/contact",
@"https://flashii.net/news/",
@"https://flash.moe/",
@"https://flashii.net/forum/",
};
foreach(string url in urls) {
// routine lifted out of satori
string paramUrl = Uri.EscapeDataString(url);
HttpClient.Send(
new HttpRequestMessage(HttpRequestMessage.GET, $@"https://mii.flashii.net/metadata?url={paramUrl}"),
onComplete: (task, res) => {
WriteLine($@"Connection: {task.Request.Connection}");
WriteLine($@"AcceptEncodings: {string.Join(@", ", task.Request.AcceptedEncodings)}");
WriteLine($@"IsSecure: {task.Request.IsSecure}");
WriteLine($@"RequestTarget: {task.Request.RequestTarget}");
WriteLine($@"UserAgent: {task.Request.UserAgent}");
WriteLine($@"ContentType: {task.Request.ContentType}");
WriteLine();
setForeground(ConsoleColor.Green);
WriteLine($@"Connection: {res.StatusCode}");
WriteLine($@"Connection: {res.StatusMessage}");
WriteLine($@"Connection: {res.Connection}");
WriteLine($@"ContentEncodings: {string.Join(@", ", res.ContentEncodings)}");
WriteLine($@"TransferEncodings: {string.Join(@", ", res.TransferEncodings)}");
WriteLine($@"Date: {res.Date}");
WriteLine($@"Server: {res.Server}");
WriteLine($@"ContentType: {res.ContentType}");
WriteLine();
/*if(res.HasBody) {
string line;
using StreamWriter sw = new StreamWriter(@"out.html", false, new UTF8Encoding(false));
using StreamReader sr = new StreamReader(res.Body, new UTF8Encoding(false), false, leaveOpen: true);
while((line = sr.ReadLine()) != null) {
//Logger.Debug(line);
sw.WriteLine(line);
}
}*/
},
onError: (task, ex) => {
setForeground(ConsoleColor.Red);
WriteLine(ex);
},
onCancel: task => {
setForeground(ConsoleColor.Yellow);
WriteLine(@"Cancelled.");
},
onDownloadProgress: (task, p, t) => {
setForeground(ConsoleColor.Blue);
WriteLine($@"Downloaded {p} bytes of {t} bytes.");
},
onUploadProgress: (task, p, t) => {
setForeground(ConsoleColor.Magenta);
WriteLine($@"Uploaded {p} bytes of {t} bytes.");
},
onStateChange: (task, s) => {
setForeground(ConsoleColor.White);
WriteLine($@"State changed: {s}");
if(!kill && (task.IsFinished || task.IsCancelled)) {
kill = true;
mre?.Set();
}
}
);
}
mre.WaitOne();
ResetColor();
}
}
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2022 flashwave
Copyright (c) 2019-2022 flashwave <me@flash.moe>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpChat.Common\SharpChat.Common.csproj" />
<ProjectReference Include="..\SharpChat.DataProvider.Misuzu\SharpChat.DataProvider.Misuzu.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,99 @@
using Hamakaze;
using SharpChat.Bans;
using SharpChat.Configuration;
using SharpChat.DataProvider;
using SharpChat.DataProvider.Misuzu;
using SharpChat.Users.Remote;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using static System.Console;
namespace MisuzuDataProviderTest {
public static class Program {
public static void Main() {
WriteLine("Misuzu Authentication Tester");
using ManualResetEvent mre = new(false);
string cfgPath = Path.GetDirectoryName(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
string buildMode = Path.GetFileName(cfgPath);
cfgPath = Path.Combine(
Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(cfgPath))),
@"SharpChat", @"bin", buildMode, @"net5.0", @"sharpchat.cfg"
);
WriteLine($@"Reading config from {cfgPath}");
using IConfig config = new StreamConfig(cfgPath);
WriteLine($@"Enter token found on {config.ReadValue(@"dp:misuzu:endpoint")}/login:");
string[] token = ReadLine().Split(new[] { '_' }, 2);
HttpClient.Instance.DefaultUserAgent = @"SharpChat/1.0";
IDataProvider dataProvider = new MisuzuDataProvider(config.ScopeTo(@"dp:misuzu"), HttpClient.Instance);
long userId = long.Parse(token[0]);
IPAddress remoteAddr = IPAddress.Parse(@"1.2.4.8");
IUserAuthResponse authRes = null;
mre.Reset();
dataProvider.UserClient.AuthenticateUser(
new UserAuthRequest(userId, token[1], remoteAddr),
onSuccess: res => {
authRes = res;
WriteLine(@"Auth success!");
WriteLine($@" User ID: {authRes.UserId}");
WriteLine($@" Username: {authRes.UserName}");
WriteLine($@" Colour: {authRes.Colour.Raw:X8}");
WriteLine($@" Hierarchy: {authRes.Rank}");
WriteLine($@" Silenced: {authRes.SilencedUntil}");
WriteLine($@" Perms: {authRes.Permissions}");
mre.Set();
},
onFailure: ex => {
WriteLine($@"Auth failed: {ex.Message}");
mre.Set();
}
);
mre.WaitOne();
if(authRes == null)
return;
#if FUCKED
WriteLine(@"Bumping last seen...");
mre.Reset();
dataProvider.UserBumpClient.SubmitBumpUsers(
new[] { new User(authRes) },
onSuccess: () => mre.Set(),
onFailure: ex => {
WriteLine($@"Bump failed: {ex.Message}");
mre.Set();
}
);
mre.WaitOne();
#endif
WriteLine(@"Fetching ban list...");
IEnumerable<IBanRecord> bans = Enumerable.Empty<IBanRecord>();
mre.Reset();
dataProvider.BanClient.GetBanList(x => { bans = x; mre.Set(); }, e => { WriteLine(e); mre.Set(); });
mre.WaitOne();
WriteLine($@"{bans.Count()} BANS");
foreach(IBanRecord ban in bans) {
WriteLine($@"BAN INFO");
WriteLine($@" User ID: {ban.UserId}");
WriteLine($@" Username: {ban.UserName}");
WriteLine($@" IP Address: {ban.UserIP}");
WriteLine($@" Expires: {ban.Expires}");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1010,7 +1010,7 @@ Actions sent through messages prefixed with `/` in Version 1 of the protocol. Ar
</tr>
<tr>
<td>
<code>/join [channel]</code>
<code>/join [channel] [password?]</code>
</td>
<td>Switches the current user to a different channel.</td>
<td>

View File

@ -7,4 +7,6 @@
/_/
```
Welcome to the repository of the temporary Flashii chat server. This is a reimplementation of the old [PHP based Sock Chat server](https://github.com/flashwave/mahou-chat/) in C#.
Welcome to the repository of the temporary Flashii chat server. SharpChat is an event based chat server supporting multiple protocols (currently Sock Chat and IRC).
> Formerly [PHP Sock Chat](https://github.com/flashwave/mahou-chat/) but without PHP but with C# but also with multiple sessions

View File

@ -0,0 +1,344 @@
using SharpChat.Events;
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Net;
namespace SharpChat.Bans {
public class BanManager {
private UserManager Users { get; }
private IBanClient BanClient { get; }
private IRemoteUserClient RemoteUserClient { get; }
private IEventDispatcher Dispatcher { get; }
private readonly object Sync = new();
public BanManager(
UserManager users,
IBanClient banClient,
IRemoteUserClient remoteUserClient,
IEventDispatcher dispatcher
) {
Users = users ?? throw new ArgumentNullException(nameof(users));
BanClient = banClient ?? throw new ArgumentNullException(nameof(banClient));
RemoteUserClient = remoteUserClient ?? throw new ArgumentNullException(nameof(remoteUserClient));
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
public void GetBanList(
Action<IEnumerable<IBanRecord>> onSuccess,
Action<Exception> onFailure
) {
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.GetBanList(onSuccess, onFailure);
}
public void CheckBan(
long userId,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userId,
rui => {
if(rui == null)
onSuccess(null);
else
CheckBan(rui, ipAddress, onSuccess, onFailure);
},
onFailure
);
}
public void CheckBan(
IUser localUserInfo,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(localUserInfo == null)
throw new ArgumentNullException(nameof(localUserInfo));
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localUserInfo,
rui => {
if(rui == null)
onSuccess(null);
else
CheckBan(rui, ipAddress, onSuccess, onFailure);
},
onFailure
);
}
public void CheckBan(
IRemoteUser remoteUserInfo,
IPAddress ipAddress,
Action<IBanRecord> onSuccess,
Action<Exception> onFailure
) {
if(remoteUserInfo == null)
throw new ArgumentNullException(nameof(remoteUserInfo));
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.CheckBan(remoteUserInfo, ipAddress, onSuccess, onFailure);
}
public void CreateBan(
string subjectName,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(subjectName == null)
throw new ArgumentNullException(nameof(subjectName));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
subjectName,
remoteSubject => {
if(remoteSubject == null)
onSuccess(false);
else
CreateBan(remoteSubject, localModerator, permanent, duration, reason, onSuccess, onFailure);
},
onFailure
);
}
public void CreateBan(
IUser localSubject,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(localSubject == null)
throw new ArgumentNullException(nameof(localSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localSubject,
remoteSubject => {
if(remoteSubject == null)
onSuccess(false);
else
CreateBan(remoteSubject, localModerator, permanent, duration, reason, onSuccess, onFailure);
},
onFailure
);
}
public void CreateBan(
IRemoteUser remoteSubject,
IUser localModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteSubject == null)
throw new ArgumentNullException(nameof(remoteSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localModerator,
remoteModerator => CreateBan(remoteSubject, remoteModerator, permanent, duration, reason, onSuccess, onFailure),
onFailure
);
}
public void CreateBan(
IRemoteUser remoteSubject,
IRemoteUser remoteModerator,
bool permanent,
TimeSpan duration,
string reason,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteSubject == null)
throw new ArgumentNullException(nameof(remoteSubject));
if(reason == null)
throw new ArgumentNullException(nameof(reason));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.CreateBan(remoteSubject, remoteModerator, permanent, duration, reason, success => {
Dispatcher.DispatchEvent(this, new UserBanCreatedEvent(remoteSubject, remoteModerator, permanent, duration, reason));
Users.Disconnect(
remoteSubject,
remoteModerator == null
? UserDisconnectReason.Flood
: UserDisconnectReason.Kicked
);
onSuccess.Invoke(success);
}, onFailure);
}
public void RemoveBan(
long userId,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userId,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
string userName,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(userName == null)
throw new ArgumentNullException(nameof(userName));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
userName,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
IUser localUser,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(localUser == null)
throw new ArgumentNullException(nameof(localUser));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
RemoteUserClient.ResolveUser(
localUser,
remoteUser => {
if(remoteUser == null)
onSuccess(false);
else
RemoveBan(remoteUser, onSuccess, onFailure);
},
onFailure
);
}
public void RemoveBan(
IRemoteUser remoteUser,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(remoteUser == null)
throw new ArgumentNullException(nameof(remoteUser));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.RemoveBan(remoteUser, success => {
Dispatcher.DispatchEvent(this, new UserBanRemovedEvent(remoteUser));
onSuccess.Invoke(success);
}, onFailure);
}
public void RemoveBan(
IPAddress ipAddress,
Action<bool> onSuccess,
Action<Exception> onFailure
) {
if(ipAddress == null)
throw new ArgumentNullException(nameof(ipAddress));
if(onSuccess == null)
throw new ArgumentNullException(nameof(onSuccess));
if(onFailure == null)
throw new ArgumentNullException(nameof(onFailure));
lock(Sync)
BanClient.RemoveBan(ipAddress, success => {
Dispatcher.DispatchEvent(this, new IPBanRemovedEvent(ipAddress));
onSuccess.Invoke(success);
}, onFailure);
}
}
}

View File

@ -0,0 +1,14 @@
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Net;
namespace SharpChat.Bans {
public interface IBanClient {
void GetBanList(Action<IEnumerable<IBanRecord>> onSuccess, Action<Exception> onFailure);
void CheckBan(IRemoteUser subject, IPAddress ipAddress, Action<IBanRecord> onSuccess, Action<Exception> onFailure);
void CreateBan(IRemoteUser subject, IRemoteUser moderator, bool perma, TimeSpan duration, string reason, Action<bool> onSuccess, Action<Exception> onFailure);
void RemoveBan(IRemoteUser subject, Action<bool> onSuccess, Action<Exception> onFailure);
void RemoveBan(IPAddress ipAddress, Action<bool> onSuccess, Action<Exception> onFailure);
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Users.Remote;
using System;
using System.Net;
namespace SharpChat.Bans {
public interface IBanRecord : IRemoteUser {
IPAddress UserIP { get; }
DateTimeOffset Expires { get; }
bool IsPermanent { get; }
}
}

View File

@ -0,0 +1,159 @@
using SharpChat.Events;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class Channel : IChannel, IEventHandler {
public const int ID_LENGTH = 8;
public string ChannelId { get; }
public string Name { get; private set; }
public string Topic { get; private set; }
public bool IsTemporary { get; private set; }
public int MinimumRank { get; private set; }
public bool AutoJoin { get; private set; }
public uint MaxCapacity { get; private set; }
public int Order { get; private set; }
public long OwnerId { get; private set; }
private readonly object Sync = new();
private HashSet<long> Users { get; } = new();
private Dictionary<string, long> Sessions { get; } = new();
public bool HasTopic
=> !string.IsNullOrWhiteSpace(Topic);
public string Password { get; private set; } = string.Empty;
public bool HasPassword
=> !string.IsNullOrWhiteSpace(Password);
public Channel(
string channelId,
string name,
string topic,
bool temp,
int minimumRank,
string password,
bool autoJoin,
uint maxCapacity,
long ownerId,
int order
) {
ChannelId = channelId ?? throw new ArgumentNullException(nameof(channelId));
Name = name ?? throw new ArgumentNullException(nameof(name));
Topic = topic;
IsTemporary = temp;
MinimumRank = minimumRank;
Password = password ?? string.Empty;
AutoJoin = autoJoin;
MaxCapacity = maxCapacity;
OwnerId = ownerId;
Order = order;
}
public bool VerifyPassword(string password) {
if(password == null)
throw new ArgumentNullException(nameof(password));
lock(Sync)
return !HasPassword || Password.Equals(password);
}
public bool HasUser(IUser user) {
if(user == null)
return false;
lock(Sync)
return Users.Contains(user.UserId);
}
public bool HasSession(ISession session) {
if(session == null)
return false;
lock(Sync)
return Sessions.ContainsKey(session.SessionId);
}
public void GetUserIds(Action<IEnumerable<long>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Users);
}
public void GetSessionIds(Action<IEnumerable<string>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Sessions.Keys);
}
public int CountUsers() {
lock(Sync)
return Users.Count;
}
public int CountUserSessions(IUser user) {
if(user == null)
throw new ArgumentNullException(nameof(user));
lock(Sync)
return Sessions.Values.Count(u => u == user.UserId);
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case ChannelUpdateEvent update: // Owner?
lock(Sync) {
if(update.HasName)
Name = update.Name;
if(update.HasTopic)
Topic = update.Topic;
if(update.IsTemporary.HasValue)
IsTemporary = update.IsTemporary.Value;
if(update.MinimumRank.HasValue)
MinimumRank = update.MinimumRank.Value;
if(update.HasPassword)
Password = update.Password;
if(update.AutoJoin.HasValue)
AutoJoin = update.AutoJoin.Value;
if(update.MaxCapacity.HasValue)
MaxCapacity = update.MaxCapacity.Value;
if(update.Order.HasValue)
Order = update.Order.Value;
}
break;
case ChannelUserJoinEvent cuje:
lock(Sync) {
Sessions.Add(cuje.SessionId, cuje.UserId);
Users.Add(cuje.UserId);
}
break;
case ChannelSessionJoinEvent csje:
lock(Sync)
Sessions.Add(csje.SessionId, csje.UserId);
break;
case ChannelUserLeaveEvent cule:
lock(Sync) {
Users.Remove(cule.UserId);
Queue<string> delete = new(Sessions.Where(s => s.Value == cule.UserId).Select(s => s.Key));
while(delete.TryDequeue(out string sessionId))
Sessions.Remove(sessionId);
}
break;
case ChannelSessionLeaveEvent csle:
lock(Sync)
Sessions.Remove(csle.SessionId);
break;
}
}
public bool Equals(IChannel other)
=> other != null && ChannelId.Equals(other.ChannelId);
public override string ToString()
=> $@"<Channel {ChannelId}#{Name}>";
}
}

View File

@ -0,0 +1,447 @@
using SharpChat.Configuration;
using SharpChat.Events;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class ChannelException : Exception { }
public class ChannelExistException : ChannelException { }
public class ChannelInvalidNameException : ChannelException { }
public class ChannelManager : IEventHandler {
private Dictionary<string, Channel> Channels { get; } = new();
private IConfig Config { get; }
private CachedValue<string[]> ChannelIds { get; }
private IEventDispatcher Dispatcher { get; }
private ChatBot Bot { get; }
private object Sync { get; } = new();
public ChannelManager(IEventDispatcher dispatcher, IConfig config, ChatBot bot) {
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Config = config ?? throw new ArgumentNullException(nameof(config));
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
ChannelIds = Config.ReadCached(@"channels", new[] { @"lounge" });
}
public void UpdateChannels() {
lock(Sync) {
string[] channelIds = ChannelIds.Value.Clone() as string[];
foreach(IChannel channel in Channels.Values) {
if(channelIds.Contains(channel.ChannelId)) {
using IConfig config = Config.ScopeTo($@"channels:{channel.ChannelId}");
string name = config.ReadValue(@"name", channel.ChannelId);
string topic = config.ReadValue(@"topic");
bool autoJoin = config.ReadValue(@"autoJoin", false);
string password = null;
int? minRank = null;
uint? maxCapacity = null;
if(!autoJoin) {
password = config.ReadValue(@"password", string.Empty);
if(string.IsNullOrEmpty(password))
password = null;
minRank = config.SafeReadValue(@"minRank", 0);
maxCapacity = config.SafeReadValue(@"maxCapacity", 0u);
}
Update(channel, name, topic, false, minRank, password, autoJoin, maxCapacity);
} else if(!channel.IsTemporary) // Not in config == temporary
Update(channel, temporary: true);
}
foreach(string channelId in channelIds) {
if(Channels.ContainsKey(channelId))
continue;
using IConfig config = Config.ScopeTo($@"channels:{channelId}");
string name = config.ReadValue(@"name", channelId);
string topic = config.ReadValue(@"topic");
bool autoJoin = config.ReadValue(@"autoJoin", false);
string password = null;
int minRank = 0;
uint maxCapacity = 0;
if(!autoJoin) {
password = config.ReadValue(@"password", string.Empty);
if(string.IsNullOrEmpty(password))
password = null;
minRank = config.SafeReadValue(@"minRank", 0);
maxCapacity = config.SafeReadValue(@"maxCapacity", 0u);
}
Create(channelId, Bot.UserId, name, topic, false, minRank, password, autoJoin, maxCapacity);
}
}
}
public void Remove(IChannel channel, IUser user = null) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
lock(Sync) {
Channel chan = null;
if(channel is Channel c && Channels.ContainsValue(c))
chan = c;
else if(Channels.TryGetValue(channel.ChannelId, out Channel c2))
chan = c2;
if(chan == null)
return; // exception?
// Remove channel from the listing
Channels.Remove(chan.ChannelId);
// Broadcast death
Dispatcher.DispatchEvent(this, new ChannelDeleteEvent(user ?? Bot, chan));
// 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.
// Could be handled by the user/session itself?
//foreach(ChatUser user in channel.GetUsers()) {
// Context.SwitchChannel(user, DefaultChannel);
//}
// Broadcast deletion of channel (deprecated)
/*foreach(IUser u in Users.OfRank(chan.MinimumRank))
u.SendPacket(new ChannelDeletePacket(chan));*/
}
}
private bool Exists(string name) {
if(name == null)
throw new ArgumentNullException(nameof(name));
lock(Sync)
return Channels.Values.Any(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
}
private void ValidateName(string name) {
if(!name.All(c => char.IsLetter(c) || char.IsNumber(c) || c == '-'))
throw new ChannelInvalidNameException();
if(Exists(name))
throw new ChannelExistException();
}
public IChannel Create(
IUser user,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0
) {
if(user == null)
throw new ArgumentNullException(nameof(user));
return Create(user.UserId, name, topic, temp, minRank, password, autoJoin, maxCapacity);
}
public IChannel Create(
long ownerId,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0
) => Create(RNG.NextString(Channel.ID_LENGTH), ownerId, name, topic, temp, minRank, password, autoJoin, maxCapacity);
public IChannel Create(
string channelId,
long ownerId,
string name,
string topic = null,
bool temp = true,
int minRank = 0,
string password = null,
bool autoJoin = false,
uint maxCapacity = 0,
int order = 0
) {
if(name == null)
throw new ArgumentNullException(nameof(name));
ValidateName(name);
lock(Sync) {
Channel channel = new(channelId, name, topic, temp, minRank, password, autoJoin, maxCapacity, ownerId, order);
Channels.Add(channel.ChannelId, channel);
Dispatcher.DispatchEvent(this, new ChannelCreateEvent(channel));
// Broadcast creation of channel (deprecated)
/*if(Users != null)
foreach(IUser user in Users.OfRank(channel.MinimumRank))
user.SendPacket(new ChannelCreatePacket(channel));*/
return channel;
}
}
public void Update(
IChannel channel,
string name = null,
string topic = null,
bool? temporary = null,
int? minRank = null,
string password = null,
bool? autoJoin = null,
uint? maxCapacity = null,
int? order = null
) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(!(channel is Channel c && Channels.ContainsValue(c))) {
if(Channels.TryGetValue(channel.ChannelId, out Channel c2))
channel = c2;
else
throw new ArgumentException(@"Provided channel is not registered with this manager.", nameof(channel));
}
lock(Sync) {
string prevName = channel.Name;
bool nameUpdated = !string.IsNullOrWhiteSpace(name) && name != prevName;
if(nameUpdated)
ValidateName(name);
if(topic != null && channel.Topic.Equals(topic))
topic = null;
if(temporary.HasValue && channel.IsTemporary == temporary.Value)
temporary = null;
if(minRank.HasValue && channel.MinimumRank == minRank.Value)
minRank = null;
if(password != null && channel.Password == password)
password = null;
if(autoJoin.HasValue && channel.AutoJoin == autoJoin.Value)
autoJoin = null;
if(maxCapacity.HasValue && channel.MaxCapacity == maxCapacity.Value)
maxCapacity = null;
if(order.HasValue && channel.Order == order.Value)
order = null;
Dispatcher.DispatchEvent(this, new ChannelUpdateEvent(channel, Bot, name, topic, temporary, minRank, password, autoJoin, maxCapacity, order));
// 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
// TODO: should be moved to the usermanager probably
/*foreach(IUser user in Users.OfRank(channel.MinimumRank)) {
user.SendPacket(new ChannelUpdatePacket(prevName, channel));
if(nameUpdated)
user.ForceChannel();
}*/
}
}
public void GetChannel(Func<IChannel, bool> predicate, Action<IChannel> callback) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync)
callback(Channels.Values.FirstOrDefault(predicate));
}
public void GetChannelById(string channelId, Action<IChannel> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(null);
return;
}
lock(Sync)
callback(Channels.TryGetValue(channelId, out Channel channel) ? channel : null);
}
public void GetChannelByName(string name, Action<IChannel> callback) {
if(name == null)
throw new ArgumentNullException(nameof(name));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(name)) {
callback(null);
return;
}
GetChannel(c => name.Equals(c.Name, StringComparison.InvariantCultureIgnoreCase), callback);
}
public void GetChannel(IChannel channel, Action<IChannel> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
if(channel is Channel c && Channels.ContainsValue(c)) {
callback(c);
return;
}
GetChannel(channel.Equals, callback);
}
}
public void GetChannels(Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
IEnumerable<IChannel> channels = Channels.Values;
if(ordered)
channels = channels.OrderBy(c => c.Order);
callback(channels);
}
}
public void GetChannels(Func<IChannel, bool> predicate, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(predicate == null)
throw new ArgumentNullException(nameof(predicate));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
lock(Sync) {
IEnumerable<IChannel> channels = Channels.Values.Where(predicate);
if(ordered)
channels = channels.OrderBy(c => c.Order);
callback(channels);
}
}
public void GetDefaultChannels(Action<IEnumerable<IChannel>> callback, bool ordered = true) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
// it doesn't really make sense for a channel to be temporary and autojoin
// maybe reconsider this in the future if the temp channel nuking strategy has adjusted
GetChannels(c => c.AutoJoin && !c.IsTemporary, callback, ordered);
}
public void GetChannelsById(IEnumerable<string> channelIds, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(channelIds == null)
throw new ArgumentNullException(nameof(channelIds));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => channelIds.Contains(c.ChannelId), callback, ordered);
}
public void GetChannelsByName(IEnumerable<string> names, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(names == null)
throw new ArgumentNullException(nameof(names));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => names.Contains(c.Name), callback, ordered);
}
public void GetChannels(IEnumerable<IChannel> channels, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(channels == null)
throw new ArgumentNullException(nameof(channels));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c1 => channels.Any(c2 => c2.Equals(c1)), callback, ordered);
}
public void GetChannels(int minRank, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => c.MinimumRank <= minRank, callback, ordered);
}
public void GetChannels(IUser user, Action<IEnumerable<IChannel>> callback, bool ordered = false) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(c => c is Channel channel && channel.HasUser(user), callback, ordered);
}
public void VerifyPassword(IChannel channel, string password, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(password == null)
throw new ArgumentNullException(nameof(password));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
if(!channel.HasPassword) {
callback(true);
return;
}
callback(channel.VerifyPassword(password));
});
}
private void OnCreate(object sender, ChannelCreateEvent cce) {
if(sender == this)
return;
lock(Sync) {
if(Exists(cce.Name))
throw new ArgumentException(@"Channel already registered??????", nameof(cce));
Channels.Add(cce.ChannelId, new Channel(
cce.ChannelId,
cce.Name,
cce.Topic,
cce.IsTemporary,
cce.MinimumRank,
cce.Password,
cce.AutoJoin,
cce.MaxCapacity,
cce.UserId,
cce.Order
));
}
}
private void OnDelete(object sender, ChannelDeleteEvent cde) {
if(sender == this)
return;
lock(Sync)
Channels.Remove(cde.ChannelId);
}
private void OnEvent(object sender, IEvent evt) {
Channel channel;
lock(Sync)
if(!Channels.TryGetValue(evt.ChannelId, out channel))
channel = null;
channel?.HandleEvent(sender, evt);
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case ChannelCreateEvent cce:
OnCreate(sender, cce);
break;
case ChannelDeleteEvent cde:
OnDelete(sender, cde);
break;
case ChannelUpdateEvent _:
case ChannelUserJoinEvent _:
case ChannelUserLeaveEvent _:
OnEvent(sender, evt);
break;
}
}
}
}

View File

@ -0,0 +1,421 @@
using SharpChat.Events;
using SharpChat.Messages;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Channels {
public class ChannelUserRelations : IEventHandler {
private IEventDispatcher Dispatcher { get; }
private ChannelManager Channels { get; }
private UserManager Users { get; }
private SessionManager Sessions { get; }
private MessageManager Messages { get; }
public ChannelUserRelations(
IEventDispatcher dispatcher,
ChannelManager channels,
UserManager users,
SessionManager sessions,
MessageManager messages
) {
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Channels = channels ?? throw new ArgumentNullException(nameof(channels));
Users = users ?? throw new ArgumentNullException(nameof(users));
Sessions = sessions ?? throw new ArgumentNullException(nameof(sessions));
Messages = messages ?? throw new ArgumentNullException(nameof(messages));
}
public void HasUser(IChannel channel, IUser user, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
callback(channel.HasUser(user));
});
}
public void HasSession(IChannel channel, ISession session, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(false);
return;
}
callback(channel.HasSession(session));
});
}
public void CountUsers(IChannel channel, Action<int> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(-1);
return;
}
callback(channel.CountUsers());
});
}
public void CountUserSessions(IChannel channel, IUser user, Action<int> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => {
if(c is not Channel channel) {
callback(-1);
return;
}
callback(channel.CountUserSessions(user));
});
}
public void CheckOverCapacity(IChannel channel, IUser user, Action<bool> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, channel => {
if(channel == null) {
callback(true);
return;
}
if(!channel.HasMaxCapacity() || user.UserId == channel.OwnerId) {
callback(false);
return;
}
CountUsers(channel, userCount => callback(channel == null || userCount >= channel.MaxCapacity));
});
}
public void GetUsersByChannelId(string channelId, Action<IEnumerable<ILocalUser>> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
Channels.GetChannelById(channelId, c => GetUsersWithChannelCallback(c, callback));
}
public void GetUsersByChannelName(string channelName, Action<IEnumerable<ILocalUser>> callback) {
if(channelName == null)
throw new ArgumentNullException(nameof(channelName));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelName)) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
Channels.GetChannelByName(channelName, c => GetUsersWithChannelCallback(c, callback));
}
public void GetUsers(IChannel channel, Action<IEnumerable<ILocalUser>> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => GetUsersWithChannelCallback(c, callback));
}
private void GetUsersWithChannelCallback(IChannel c, Action<IEnumerable<ILocalUser>> callback) {
if(c is not Channel channel) {
callback(Enumerable.Empty<ILocalUser>());
return;
}
channel.GetUserIds(ids => Users.GetUsers(ids, callback));
}
public void GetUsers(IEnumerable<IChannel> channels, Action<IEnumerable<ILocalUser>> callback) {
if(channels == null)
throw new ArgumentNullException(nameof(channels));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
// this is pretty disgusting
Channels.GetChannels(channels, channels => {
HashSet<long> ids = new();
foreach(IChannel c in channels) {
if(c is not Channel channel)
continue;
channel.GetUserIds(u => {
foreach(long id in u)
ids.Add(id);
});
}
Users.GetUsers(ids, callback);
});
}
// this makes me cry
public void GetUsers(IUser user, Action<IEnumerable<ILocalUser>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
HashSet<ILocalUser> all = new();
Channels.GetChannels(channels => {
foreach(IChannel channel in channels) {
GetUsers(channel, users => {
foreach(ILocalUser user in users)
all.Add(user);
});
}
});
callback(all);
}
public void GetLocalSessionsByChannelId(string channelId, Action<IEnumerable<ISession>> callback) {
if(channelId == null)
throw new ArgumentNullException(nameof(channelId));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelId)) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannelById(channelId, c => GetLocalSessionsChannelCallback(c, callback));
}
public void GetLocalSessionsByChannelName(string channelName, Action<IEnumerable<ISession>> callback) {
if(channelName == null)
throw new ArgumentNullException(nameof(channelName));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(string.IsNullOrWhiteSpace(channelName)) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannelByName(channelName, c => GetLocalSessionsChannelCallback(c, callback));
}
public void GetLocalSessions(IChannel channel, Action<IEnumerable<ISession>> callback) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Channels.GetChannel(channel, c => GetLocalSessionsChannelCallback(c, callback));
}
private void GetLocalSessionsChannelCallback(IChannel c, Action<IEnumerable<ISession>> callback) {
if(c is not Channel channel) {
callback(Enumerable.Empty<ISession>());
return;
}
channel.GetSessionIds(ids => Sessions.GetLocalSessions(ids, callback));
}
public void GetLocalSessions(IUser user, Action<IEnumerable<ISession>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
GetChannels(user, channels => GetLocalSessionsUserCallback(channels, callback));
}
public void GetLocalSessionsByUserId(long userId, Action<IUser, IEnumerable<ISession>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(userId < 1) {
callback(null, Enumerable.Empty<ISession>());
return;
}
GetChannelsByUserId(userId, (user, channels) => GetLocalSessionsUserCallback(channels, sessions => callback(user, sessions)));
}
private void GetLocalSessionsUserCallback(IEnumerable<IChannel> channels, Action<IEnumerable<ISession>> callback) {
if(!channels.Any()) {
callback(Enumerable.Empty<ISession>());
return;
}
Channels.GetChannels(channels, channels => {
HashSet<string> sessionIds = new();
foreach(IChannel c in channels) {
if(c is not Channel channel)
continue;
channel.GetSessionIds(ids => {
foreach(string id in ids)
sessionIds.Add(id);
});
}
Sessions.GetLocalSessions(sessionIds, callback);
});
}
public void GetChannelsByUserId(long userId, Action<IUser, IEnumerable<IChannel>> callback) {
if(callback == null)
throw new ArgumentNullException(nameof(callback));
if(userId < 1) {
callback(null, Enumerable.Empty<IChannel>());
return;
}
Users.GetUser(userId, u => GetChannelsUserCallback(u, channels => callback(u, channels)));
}
public void GetChannels(IUser user, Action<IEnumerable<IChannel>> callback) {
if(user == null)
throw new ArgumentNullException(nameof(user));
if(callback == null)
throw new ArgumentNullException(nameof(callback));
Users.GetUser(user, u => GetChannelsUserCallback(u, callback));
}
private void GetChannelsUserCallback(IUser u, Action<IEnumerable<IChannel>> callback) {
if(u is not User user) {
callback(Enumerable.Empty<IChannel>());
return;
}
user.GetChannels(c => Channels.GetChannelsByName(c, callback));
}
public void JoinChannel(IChannel channel, ISession session) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
HasSession(channel, session, hasSession => {
if(hasSession)
return;
// SessionJoin and UserJoin should be combined
HasUser(channel, session.User, HasUser => {
Dispatcher.DispatchEvent(
this,
HasUser
? new ChannelSessionJoinEvent(channel, session)
: new ChannelUserJoinEvent(channel, session)
);
});
});
}
public void LeaveChannel(IChannel channel, IUser user, UserDisconnectReason reason = UserDisconnectReason.Unknown) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(user == null)
throw new ArgumentNullException(nameof(user));
HasUser(channel, user, hasUser => {
if(hasUser)
Dispatcher.DispatchEvent(this, new ChannelUserLeaveEvent(user, channel, reason));
});
}
public void LeaveChannel(IChannel channel, ISession session) {
if(channel == null)
throw new ArgumentNullException(nameof(channel));
if(session == null)
throw new ArgumentNullException(nameof(session));
HasSession(channel, session, hasSession => {
// UserLeave and SessionLeave should be combined
CountUserSessions(channel, session.User, sessionCount => {
Dispatcher.DispatchEvent(
this,
sessionCount <= 1
? new ChannelUserLeaveEvent(session.User, channel, UserDisconnectReason.Leave)
: new ChannelSessionLeaveEvent(channel, session)
);
});
});
}
public void LeaveChannels(ISession session) {
if(session == null)
throw new ArgumentNullException(nameof(session));
Channels.GetChannels(channels => {
foreach(IChannel channel in channels)
LeaveChannel(channel, session);
});
}
public void HandleEvent(object sender, IEvent evt) {
switch(evt) {
case UserUpdateEvent uue: // fetch up to date user info
GetChannelsByUserId(evt.UserId, (user, channels) => GetUsers(channels, users => {
foreach(ILocalUser user in users)
GetLocalSessions(user, sessions => {
foreach(ISession session in sessions)
session.HandleEvent(sender, new UserUpdateEvent(user, uue));
});
}));
break;
case ChannelUserJoinEvent cje:
// THIS DOES NOT DO WHAT YOU WANT IT TO DO
// I THINK
// it really doesn't, figure out how to leave channels when MCHAN isn't active for the session
//if((Sessions.GetCapabilities(cje.User) & ClientCapability.MCHAN) == 0)
// LeaveChannel(cje.Channel, cje.User, UserDisconnectReason.Leave);
break;
case ChannelUserLeaveEvent cle: // Should ownership just be passed on to another user instead of Destruction?
Channels.GetChannelById(evt.ChannelId, channel => {
if(channel.IsTemporary && evt.UserId == channel.OwnerId)
Channels.Remove(channel);
});
break;
case SessionDestroyEvent sde:
Users.GetUser(sde.UserId, user => {
if(user == null)
return;
Sessions.GetSessionCount(user, sessionCount => {
if(sessionCount < 1)
Users.Disconnect(user, UserDisconnectReason.TimeOut);
});
});
break;
}
}
}
}

View File

@ -0,0 +1,18 @@
using System;
namespace SharpChat.Channels {
public interface IChannel : IEquatable<IChannel> {
string ChannelId { get; }
string Name { get; }
string Topic { get; }
bool IsTemporary { get; }
int MinimumRank { get; }
bool AutoJoin { get; }
uint MaxCapacity { get; }
int Order { get; }
long OwnerId { get; }
string Password { get; }
bool HasPassword { get; }
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Users;
namespace SharpChat.Channels {
public static class IChannelExtensions {
public static bool HasMaxCapacity(this IChannel channel)
=> channel.MaxCapacity != 0;
public static bool IsOwner(this IChannel channel, IUser user)
=> channel != null && user != null && channel.OwnerId == user.UserId;
}
}

View File

@ -0,0 +1,29 @@
using System;
namespace SharpChat {
public readonly struct Colour : IEquatable<Colour?> {
public const int INHERIT = 0x40000000;
public int Raw { get; }
public Colour(int argb) {
Raw = argb;
}
public static implicit operator Colour(int argb) => new(argb);
public bool Equals(Colour? other)
=> other.HasValue && other.Value.Raw == Raw;
public bool Inherit => (Raw & INHERIT) > 0;
public int Red => (Raw >> 16) & 0xFF;
public int Green => (Raw >> 8) & 0xFF;
public int Blue => Raw & 0xFF;
public override string ToString() {
if (Inherit)
return @"inherit";
return string.Format(@"#{0:X6}", Raw);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
namespace SharpChat.Configuration {
public class CachedValue<T> {
private IConfig Config { get; }
private string Name { get; }
private TimeSpan Lifetime { get; }
private T Fallback { get; }
private object Sync { get; } = new();
private object CurrentValue { get; set; }
private DateTimeOffset LastRead { get; set; }
public T Value {
get {
lock(Sync) {
DateTimeOffset now = DateTimeOffset.Now;
if((now - LastRead) >= Lifetime) {
LastRead = now;
CurrentValue = Config.ReadValue(Name, Fallback);
Logger.Debug($@"Read {Name} ({CurrentValue})");
}
}
return (T)CurrentValue;
}
}
public static implicit operator T(CachedValue<T> val) => val.Value;
public CachedValue(IConfig config, string name, TimeSpan lifetime, T fallback) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Name = name ?? throw new ArgumentNullException(nameof(name));
Lifetime = lifetime;
Fallback = fallback;
if(string.IsNullOrWhiteSpace(name))
throw new ArgumentException(@"Name cannot be empty.", nameof(name));
}
public void Refresh() {
lock(Sync) {
LastRead = DateTimeOffset.MinValue;
}
}
}
}

View File

@ -0,0 +1,16 @@
using System;
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 {
public ConfigLockException() : base(@"Unable to acquire lock for reading configuration.") { }
}
public class ConfigTypeException : ConfigException {
public ConfigTypeException(Exception ex) : base(@"Given type does not match the value in the configuration.", ex) { }
}
}

View File

@ -0,0 +1,31 @@
using System;
namespace SharpChat.Configuration {
public interface IConfig : IDisposable {
/// <summary>
/// Creates a proxy object that forces all names to start with the given prefix.
/// </summary>
IConfig ScopeTo(string prefix);
/// <summary>
/// Reads a raw (string) value from the config.
/// </summary>
string ReadValue(string name, string fallback = null);
/// <summary>
/// Reads and casts value from the config.
/// </summary>
/// <exception cref="ConfigTypeException">Type conversion failed.</exception>
T ReadValue<T>(string name, T fallback = default);
/// <summary>
/// Reads and casts a value from the config. Returns fallback when type conversion fails.
/// </summary>
T SafeReadValue<T>(string name, T fallback);
/// <summary>
/// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values.
/// </summary>
CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null);
}
}

View File

@ -0,0 +1,45 @@
using System;
namespace SharpChat.Configuration {
public class ScopedConfig : IConfig {
private IConfig Config { get; }
private string Prefix { get; }
public ScopedConfig(IConfig config, string prefix) {
Config = config ?? throw new ArgumentNullException(nameof(config));
Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
if(string.IsNullOrWhiteSpace(prefix))
throw new ArgumentException(@"Prefix must exist.", nameof(prefix));
if(Prefix[^1] != ':')
Prefix += ':';
}
private string GetName(string name) {
return Prefix + name;
}
public string ReadValue(string name, string fallback = null) {
return Config.ReadValue(GetName(name), fallback);
}
public T ReadValue<T>(string name, T fallback = default) {
return Config.ReadValue(GetName(name), fallback);
}
public T SafeReadValue<T>(string name, T fallback) {
return Config.SafeReadValue(GetName(name), fallback);
}
public IConfig ScopeTo(string prefix) {
return Config.ScopeTo(GetName(prefix));
}
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
return Config.ReadCached(GetName(name), fallback, lifetime);
}
public void Dispose() {
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
namespace SharpChat.Configuration {
public class StreamConfig : IConfig {
private Stream Stream { get; }
private StreamReader StreamReader { get; }
private Mutex Lock { get; }
private const int LOCK_TIMEOUT = 10000;
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
public StreamConfig(string fileName)
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) {}
public StreamConfig(Stream stream) {
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
if(!Stream.CanRead)
throw new ArgumentException(@"Provided stream must be readable.", nameof(stream));
if(!Stream.CanSeek)
throw new ArgumentException(@"Provided stream must be seekable.", nameof(stream));
StreamReader = new StreamReader(stream, new UTF8Encoding(false), false);
Lock = new Mutex();
}
public string ReadValue(string name, string fallback = null) {
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
throw new ConfigLockException();
try {
Stream.Seek(0, SeekOrigin.Begin);
string line;
while((line = StreamReader.ReadLine()) != null) {
if(string.IsNullOrWhiteSpace(line))
continue;
line = line.TrimStart();
if(line.StartsWith(@";") || line.StartsWith(@"#"))
continue;
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if(parts.Length < 2 || !string.Equals(parts[0], name))
continue;
return parts[1];
}
} finally {
Lock.ReleaseMutex();
}
return fallback;
}
public T ReadValue<T>(string name, T fallback = default) {
object value = ReadValue(name);
if(value == null)
return fallback;
Type type = typeof(T);
if(value is string strVal) {
if(type == typeof(bool))
value = !string.Equals(strVal, @"0", StringComparison.InvariantCultureIgnoreCase)
&& !string.Equals(strVal, @"false", StringComparison.InvariantCultureIgnoreCase);
else if(type == typeof(string[]))
value = strVal.Split(' ');
}
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 IConfig ScopeTo(string 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();
}
}
}

145
SharpChat.Common/Context.cs Normal file
View File

@ -0,0 +1,145 @@
using SharpChat.Bans;
using SharpChat.Channels;
using SharpChat.Configuration;
using SharpChat.Database;
using SharpChat.DataProvider;
using SharpChat.Events;
using SharpChat.Messages;
using SharpChat.Messages.Storage;
using SharpChat.RateLimiting;
using SharpChat.Sessions;
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace SharpChat {
public class Context : IDisposable {
public const int ID_LENGTH = 8;
public string ServerId { get; }
public EventDispatcher Events { get; }
public SessionManager Sessions { get; }
public UserManager Users { get; }
public ChannelManager Channels { get; }
public ChannelUserRelations ChannelUsers { get; }
public MessageManager Messages { get; }
public BanManager Bans { get; }
public IDataProvider DataProvider { get; }
public RateLimitManager RateLimiting { get; }
public WelcomeMessage WelcomeMessage { get; }
public ChatBot Bot { get; } = new();
private Timer BumpTimer { get; }
public DateTimeOffset Created { get; }
public Context(IConfig config, IDatabaseBackend databaseBackend, IDataProvider dataProvider) {
if(config == null)
throw new ArgumentNullException(nameof(config));
ServerId = RNG.NextString(ID_LENGTH); // maybe read this from the cfg instead
Created = DateTimeOffset.Now; // read this from config definitely
DatabaseWrapper db = new(databaseBackend ?? throw new ArgumentNullException(nameof(databaseBackend)));
IMessageStorage msgStore = db.IsNullBackend
? new MemoryMessageStorage()
: new ADOMessageStorage(db);
Events = new EventDispatcher();
DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
Users = new UserManager(Events);
Sessions = new SessionManager(Events, Users, config.ScopeTo(@"sessions"), ServerId);
Messages = new MessageManager(Events, msgStore, config.ScopeTo(@"messages"));
Channels = new ChannelManager(Events, config, Bot);
ChannelUsers = new ChannelUserRelations(Events, Channels, Users, Sessions, Messages);
Bans = new BanManager(Users, DataProvider.BanClient, DataProvider.UserClient, Events);
RateLimiting = new RateLimitManager(config.ScopeTo(@"rateLimit"));
WelcomeMessage = new WelcomeMessage(config.ScopeTo(@"welcome"));
Events.AddEventHandler(Sessions);
Events.ProtectEventHandler(Sessions);
Events.AddEventHandler(Users);
Events.ProtectEventHandler(Users);
Events.AddEventHandler(Channels);
Events.ProtectEventHandler(Channels);
Events.AddEventHandler(ChannelUsers);
Events.ProtectEventHandler(ChannelUsers);
Events.AddEventHandler(Messages);
Events.ProtectEventHandler(Messages);
Events.StartProcessing();
Channels.UpdateChannels();
// Should probably not rely on Timers in the future
BumpTimer = new Timer(e => {
Logger.Write(@"Nuking dead sessions and bumping remote online status...");
Sessions.CheckTimeOut();
Sessions.GetActiveLocalSessions(sessions => {
Dictionary<IUser, List<ISession>> data = new();
foreach(ISession session in sessions) {
if(!data.ContainsKey(session.User))
data.Add(session.User, new());
data[session.User].Add(session);
}
DataProvider.UserClient.BumpUsers(
data.Select(kvp => new UserBumpInfo(kvp.Key, kvp.Value)),
() => Logger.Debug(@"Successfully bumped remote online status!"),
ex => { Logger.Write(@"Failed to bump remote online status."); Logger.Debug(ex); }
);
});
}, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
}
public void BroadcastMessage(string text) {
Events.DispatchEvent(this, new BroadcastMessageEvent(Bot, text));
}
[Obsolete(@"Use ChannelUsers.JoinChannel")]
public void JoinChannel(IUser user, IChannel channel) {
// handle in channelusers
//channel.SendPacket(new UserChannelJoinPacket(user));
// send after join packet for v1
//user.SendPacket(new ContextClearPacket(channel, ContextClearMode.MessagesUsers));
// send after join
//ChannelUsers.GetUsers(channel, u => user.SendPacket(new ContextUsersPacket(u.Except(new[] { user }).OrderByDescending(u => u.Rank))));
// send after join, maybe add a capability that makes this implicit?
/*Messages.GetMessages(channel, m => {
foreach(IMessage msg in m)
user.SendPacket(new ContextMessagePacket(msg));
});*/
// should happen implicitly for v1 clients
//user.ForceChannel(channel);
}
private bool IsDisposed;
~Context()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if (IsDisposed)
return;
IsDisposed = true;
BumpTimer.Dispose();
Events.FinishProcessing();
}
}
}

View File

@ -0,0 +1,8 @@
using SharpChat.Reflection;
namespace SharpChat.DataProvider {
public class DataProviderAttribute : ObjectConstructorAttribute {
public DataProviderAttribute(string name) : base(name) {
}
}
}

View File

@ -0,0 +1,9 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
namespace SharpChat.DataProvider {
public interface IDataProvider {
IBanClient BanClient { get; }
IRemoteUserClient UserClient { get; }
}
}

View File

@ -0,0 +1,30 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace SharpChat.DataProvider.Null {
public class NullBanClient : IBanClient {
public void CheckBan(IRemoteUser subject, IPAddress ipAddress, Action<IBanRecord> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void CreateBan(IRemoteUser subject, IRemoteUser moderator, bool perma, TimeSpan duration, string reason, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(true);
}
public void GetBanList(Action<IEnumerable<IBanRecord>> onSuccess, Action<Exception> onFailure) {
onSuccess(Enumerable.Empty<IBanRecord>());
}
public void RemoveBan(IRemoteUser subject, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(false);
}
public void RemoveBan(IPAddress ipAddress, Action<bool> onSuccess, Action<Exception> onFailure) {
onSuccess(false);
}
}
}

View File

@ -0,0 +1,15 @@
using SharpChat.Bans;
using SharpChat.Users.Remote;
namespace SharpChat.DataProvider.Null {
[DataProvider(@"null")]
public class NullDataProvider : IDataProvider {
public IBanClient BanClient { get; }
public IRemoteUserClient UserClient { get; }
public NullDataProvider() {
BanClient = new NullBanClient();
UserClient = new NullUserClient();
}
}
}

View File

@ -0,0 +1,28 @@
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
namespace SharpChat.DataProvider.Null {
public class NullUserAuthResponse : IUserAuthResponse {
public long UserId { get; }
public string UserName { get; }
public int Rank { get; }
public Colour Colour { get; }
public UserPermissions Permissions { get; }
public DateTimeOffset SilencedUntil => DateTimeOffset.MinValue;
public NullUserAuthResponse(UserAuthRequest uar) {
UserId = uar.UserId;
UserName = $@"Misaka-{uar.UserId}";
Rank = (int)(uar.UserId % 10);
Random rng = new((int)uar.UserId);
Colour = new(rng.Next());
Permissions = (UserPermissions)rng.Next();
}
public bool Equals(IUser other)
=> other is NullUserAuthResponse && other.UserId == UserId;
public bool Equals(IRemoteUser other)
=> other is NullUserAuthResponse && other.UserId == UserId;
}
}

View File

@ -0,0 +1,33 @@
using SharpChat.Users;
using SharpChat.Users.Remote;
using System;
using System.Collections.Generic;
namespace SharpChat.DataProvider.Null {
public class NullUserClient : IRemoteUserClient {
public void AuthenticateUser(UserAuthRequest request, Action<IUserAuthResponse> onSuccess, Action<Exception> onFailure) {
if(request.Token.StartsWith(@"FAIL:")) {
onFailure(new UserAuthFailedException(request.Token[5..]));
return;
}
onSuccess(new NullUserAuthResponse(request));
}
public void BumpUsers(IEnumerable<UserBumpInfo> users, Action onSuccess, Action<Exception> onFailure) {
onSuccess();
}
public void ResolveUser(long userId, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void ResolveUser(string userName, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
public void ResolveUser(IUser localUser, Action<IRemoteUser> onSuccess, Action<Exception> onFailure) {
onSuccess(null);
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Data.Common;
namespace SharpChat.Database {
public class ADODatabaseReader : IDatabaseReader {
private DbDataReader Reader { get; }
public ADODatabaseReader(DbDataReader reader) {
Reader = reader;
}
public bool Next()
=> Reader.Read();
public string GetName(int ordinal)
=> Reader.GetName(ordinal);
public int GetOrdinal(string name)
=> Reader.GetOrdinal(name);
public bool IsNull(int ordinal)
=> Reader.IsDBNull(ordinal);
public bool IsNull(string name)
=> Reader.IsDBNull(GetOrdinal(name));
public object GetValue(int ordinal)
=> Reader.GetValue(ordinal);
public object GetValue(string name)
=> Reader.GetValue(GetOrdinal(name));
public string ReadString(int ordinal)
=> Reader.GetString(ordinal);
public string ReadString(string name)
=> Reader.GetString(GetOrdinal(name));
public byte ReadU8(int ordinal)
=> Reader.GetByte(ordinal);
public byte ReadU8(string name)
=> Reader.GetByte(GetOrdinal(name));
public short ReadI16(int ordinal)
=> Reader.GetInt16(ordinal);
public short ReadI16(string name)
=> Reader.GetInt16(GetOrdinal(name));
public int ReadI32(int ordinal)
=> Reader.GetInt32(ordinal);
public int ReadI32(string name)
=> Reader.GetInt32(GetOrdinal(name));
public long ReadI64(int ordinal)
=> Reader.GetInt64(ordinal);
public long ReadI64(string name)
=> Reader.GetInt64(GetOrdinal(name));
public float ReadF32(int ordinal)
=> Reader.GetFloat(ordinal);
public float ReadF32(string name)
=> Reader.GetFloat(GetOrdinal(name));
public double ReadF64(int ordinal)
=> Reader.GetDouble(ordinal);
public double ReadF64(string name)
=> Reader.GetDouble(GetOrdinal(name));
private bool IsDisposed;
~ADODatabaseReader()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
if(Reader is IDisposable disposable)
disposable.Dispose();
}
}
}

View File

@ -0,0 +1,8 @@
using SharpChat.Reflection;
namespace SharpChat.Database {
public class DatabaseBackendAttribute : ObjectConstructorAttribute {
public DatabaseBackendAttribute(string name) : base(name) {
}
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace SharpChat.Database {
public class DatabaseException : Exception {}
public class InvalidParameterClassTypeException : DatabaseException { }
}

View File

@ -0,0 +1,14 @@
namespace SharpChat.Database {
public enum DatabaseType {
AsciiString,
UnicodeString,
Int8,
Int16,
Int32,
Int64,
UInt8,
UInt16,
UInt32,
UInt64,
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpChat.Database {
public class DatabaseWrapper {
private IDatabaseBackend Backend { get; }
public bool IsNullBackend
=> Backend is Null.NullDatabaseBackend;
public DatabaseWrapper(IDatabaseBackend backend) {
Backend = backend ?? throw new ArgumentNullException(nameof(backend));
}
public IDatabaseParameter CreateParam(string name, object value)
=> Backend.CreateParameter(name, value);
public string TimestampType
=> Backend.TimestampType;
public string TextType
=> Backend.TextType;
public string BlobType
=> Backend.BlobType;
public string VarCharType(int size)
=> Backend.VarCharType(size);
public string VarBinaryType(int size)
=> Backend.VarBinaryType(size);
public string BigIntType(int length)
=> Backend.BigIntType(length);
public string BigUIntType(int length)
=> Backend.BigUIntType(length);
public string IntType(int length)
=> Backend.IntType(length);
public string UIntType(int length)
=> Backend.UIntType(length);
public string TinyIntType(int length)
=> Backend.TinyIntType(length);
public string TinyUIntType(int length)
=> Backend.TinyUIntType(length);
public string ToUnixTime(string param)
=> Backend.ToUnixTime(param);
public string FromUnixTime(string param)
=> Backend.FromUnixTime(param);
public string DateTimeNow()
=> Backend.DateTimeNow();
public string Concat(params string[] args)
=> Backend.Concat(args);
public string ToLower(string param)
=> Backend.ToLower(param);
public bool SupportsJson
=> Backend.SupportsJson;
public string JsonValue(string field, string path)
=> Backend.JsonValue(field, path);
public bool SupportsAlterTableCollate
=> Backend.SupportsAlterTableCollate;
public string AsciiCollation
=> Backend.AsciiCollation;
public string UnicodeCollation
=> Backend.UnicodeCollation;
public void RunCommand(object query, int timeout, Action<IDatabaseCommand> action, params IDatabaseParameter[] @params) {
#if LOG_SQL
Logger.Debug(query);
#endif
using IDatabaseConnection conn = Backend.CreateConnection();
using IDatabaseCommand comm = conn.CreateCommand(query);
comm.CommandTimeout = timeout;
if(@params.Any()) {
comm.AddParameters(@params);
comm.Prepare();
}
action.Invoke(comm);
}
public void RunCommand(object query, Action<IDatabaseCommand> action, params IDatabaseParameter[] @params)
=> RunCommand(query, 30, action, @params);
public int RunCommand(object query, params IDatabaseParameter[] @params) {
int affected = 0;
RunCommand(query, comm => affected = comm.Execute(), @params);
return affected;
}
public int RunCommand(object query, int timeout, params IDatabaseParameter[] @params) {
int affected = 0;
RunCommand(query, timeout, comm => affected = comm.Execute(), @params);
return affected;
}
public object RunQueryValue(object query, params IDatabaseParameter[] @params) {
object value = null;
RunCommand(query, comm => value = comm.ExecuteScalar(), @params);
return value;
}
public void RunQuery(object query, Action<IDatabaseReader> action, params IDatabaseParameter[] @params) {
RunCommand(query, comm => {
using IDatabaseReader reader = comm.ExecuteReader();
action.Invoke(reader);
}, @params);
}
}
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
namespace SharpChat.Database {
public interface IDatabaseBackend {
IDatabaseConnection CreateConnection();
IDatabaseParameter CreateParameter(string name, object value);
IDatabaseParameter CreateParameter(string name, DatabaseType type);
string TimestampType { get; }
string TextType { get; }
string BlobType { get; }
string VarCharType(int length);
string VarBinaryType(int length);
string BigIntType(int length);
string BigUIntType(int length);
string IntType(int length);
string UIntType(int length);
string TinyIntType(int length);
string TinyUIntType(int length);
string FromUnixTime(string param);
string ToUnixTime(string param);
string DateTimeNow();
string Concat(params string[] args);
string ToLower(string param);
bool SupportsJson { get; }
string JsonValue(string field, string path);
bool SupportsAlterTableCollate { get; }
string AsciiCollation { get; }
string UnicodeCollation { get; }
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseCommand : IDisposable {
IDatabaseConnection Connection { get; }
string CommandString { get; }
int CommandTimeout { get; set; }
IDatabaseParameter AddParameter(string name, object value);
IDatabaseParameter AddParameter(string name, DatabaseType type);
IDatabaseParameter AddParameter(IDatabaseParameter param);
void AddParameters(IDatabaseParameter[] @params);
void ClearParameters();
void Prepare();
int Execute();
IDatabaseReader ExecuteReader();
object ExecuteScalar();
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseConnection : IDisposable {
IDatabaseCommand CreateCommand(object query);
}
}

View File

@ -0,0 +1,6 @@
namespace SharpChat.Database {
public interface IDatabaseParameter {
string Name { get; }
object Value { get; set; }
}
}

View File

@ -0,0 +1,37 @@
using System;
namespace SharpChat.Database {
public interface IDatabaseReader : IDisposable {
bool Next();
object GetValue(int ordinal);
object GetValue(string name);
bool IsNull(int ordinal);
bool IsNull(string name);
string GetName(int ordinal);
int GetOrdinal(string name);
string ReadString(int ordinal);
string ReadString(string name);
byte ReadU8(int ordinal);
byte ReadU8(string name);
short ReadI16(int ordinal);
short ReadI16(string name);
int ReadI32(int ordinal);
int ReadI32(string name);
long ReadI64(int ordinal);
long ReadI64(string name);
float ReadF32(int ordinal);
float ReadF32(string name);
double ReadF64(int ordinal);
double ReadF64(string name);
}
}

View File

@ -0,0 +1,63 @@
using SharpChat.Configuration;
using System.Collections.Generic;
namespace SharpChat.Database.Null {
[DatabaseBackend(@"null")]
public class NullDatabaseBackend : IDatabaseBackend {
public NullDatabaseBackend(IConfig _ = null) { }
public IDatabaseConnection CreateConnection()
=> new NullDatabaseConnection();
public IDatabaseParameter CreateParameter(string name, object value)
=> new NullDatabaseParameter();
public IDatabaseParameter CreateParameter(string name, DatabaseType type)
=> new NullDatabaseParameter();
public string TimestampType
=> string.Empty;
public string TextType
=> string.Empty;
public string BlobType
=> string.Empty;
public string VarCharType(int size)
=> string.Empty;
public string VarBinaryType(int size)
=> string.Empty;
public string BigIntType(int length)
=> string.Empty;
public string BigUIntType(int length)
=> string.Empty;
public string IntType(int length)
=> string.Empty;
public string UIntType(int length)
=> string.Empty;
public string TinyIntType(int length)
=> string.Empty;
public string TinyUIntType(int length)
=> string.Empty;
public string FromUnixTime(string param)
=> string.Empty;
public string ToUnixTime(string param)
=> string.Empty;
public string DateTimeNow()
=> string.Empty;
public string Concat(params string[] args)
=> string.Empty;
public string ToLower(string param)
=> string.Empty;
public bool SupportsJson => false;
public string JsonValue(string field, string path)
=> string.Empty;
public bool SupportsAlterTableCollate => true;
public string AsciiCollation => string.Empty;
public string UnicodeCollation => string.Empty;
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseCommand : IDatabaseCommand {
public IDatabaseConnection Connection { get; }
public string CommandString => string.Empty;
public int CommandTimeout { get => -1; set { } }
public NullDatabaseCommand(NullDatabaseConnection conn) {
Connection = conn ?? throw new ArgumentNullException(nameof(conn));
}
public IDatabaseParameter AddParameter(string name, object value)
=> new NullDatabaseParameter();
public IDatabaseParameter AddParameter(string name, DatabaseType type)
=> new NullDatabaseParameter();
public IDatabaseParameter AddParameter(IDatabaseParameter param) {
if(param is not NullDatabaseParameter)
throw new InvalidParameterClassTypeException();
return param;
}
public void AddParameters(IDatabaseParameter[] @params) {}
public void ClearParameters() {}
public void Dispose() {
GC.SuppressFinalize(this);
}
public int Execute() {
return 0;
}
public IDatabaseReader ExecuteReader() {
return new NullDatabaseReader();
}
public object ExecuteScalar() {
return null;
}
public void Prepare() {}
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseConnection : IDatabaseConnection {
public IDatabaseCommand CreateCommand(object query) {
return new NullDatabaseCommand(this);
}
public void Dispose() {
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,6 @@
namespace SharpChat.Database.Null {
public class NullDatabaseParameter : IDatabaseParameter {
public string Name => string.Empty;
public object Value { get => null; set { } }
}
}

View File

@ -0,0 +1,92 @@
using System;
namespace SharpChat.Database.Null {
public class NullDatabaseReader : IDatabaseReader {
public void Dispose() {
GC.SuppressFinalize(this);
}
public string GetName(int ordinal) {
return string.Empty;
}
public int GetOrdinal(string name) {
return 0;
}
public object GetValue(int ordinal) {
return null;
}
public object GetValue(string name) {
return null;
}
public bool IsNull(int ordinal) {
return true;
}
public bool IsNull(string name) {
return true;
}
public bool Next() {
return false;
}
public float ReadF32(int ordinal) {
return 0f;
}
public float ReadF32(string name) {
return 0f;
}
public double ReadF64(int ordinal) {
return 0d;
}
public double ReadF64(string name) {
return 0d;
}
public short ReadI16(int ordinal) {
return 0;
}
public short ReadI16(string name) {
return 0;
}
public int ReadI32(int ordinal) {
return 0;
}
public int ReadI32(string name) {
return 0;
}
public long ReadI64(int ordinal) {
return 0;
}
public long ReadI64(string name) {
return 0;
}
public string ReadString(int ordinal) {
return string.Empty;
}
public string ReadString(string name) {
return string.Empty;
}
public byte ReadU8(int ordinal) {
return 0;
}
public byte ReadU8(string name) {
return 0;
}
}
}

View File

@ -0,0 +1,15 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class BroadcastMessageEvent : Event {
public const string TYPE = @"broadcast:message";
public string Text { get; }
public BroadcastMessageEvent(ChatBot chatBot, string text) : base(chatBot) {
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View File

@ -0,0 +1,28 @@
using SharpChat.Channels;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelCreateEvent : Event {
public const string TYPE = @"channel:create";
public string Name { get; }
public string Topic { get; }
public bool IsTemporary { get; }
public int MinimumRank { get; }
public string Password { get; }
public bool AutoJoin { get; }
public uint MaxCapacity { get; }
public int Order { get; }
public ChannelCreateEvent(IChannel channel) : base(channel) {
Name = channel.Name;
Topic = channel.Topic;
IsTemporary = channel.IsTemporary;
MinimumRank = channel.MinimumRank;
Password = channel.Password;
AutoJoin = channel.AutoJoin;
MaxCapacity = channel.MaxCapacity;
Order = channel.Order;
}
}
}

View File

@ -0,0 +1,13 @@
using SharpChat.Channels;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelDeleteEvent : Event {
public const string TYPE = @"channel:delete";
public ChannelDeleteEvent(IChannel channel) : base(channel) { }
public ChannelDeleteEvent(IUser user, IChannel channel) : base(user, channel) { }
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelSessionJoinEvent : Event {
public const string TYPE = @"channel:session:join";
public ChannelSessionJoinEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelSessionLeaveEvent : Event {
public const string TYPE = @"channel:session:leave";
public ChannelSessionLeaveEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View File

@ -0,0 +1,47 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUpdateEvent : Event {
public const string TYPE = @"channel:update";
public string PreviousName { get; }
public string Name { get; }
public string Topic { get; }
public bool? IsTemporary { get; }
public int? MinimumRank { get; }
public string Password { get; }
public bool? AutoJoin { get; }
public uint? MaxCapacity { get; }
public int? Order { get; }
public bool HasName => Name != null;
public bool HasTopic => Topic != null;
public bool HasPassword => Password != null;
public ChannelUpdateEvent(
IChannel channel,
IUser owner,
string name,
string topic,
bool? temp,
int? minRank,
string password,
bool? autoJoin,
uint? maxCapacity,
int? order
) : base(owner, channel ?? throw new ArgumentNullException(nameof(channel))) {
PreviousName = channel.Name;
Name = name;
Topic = topic;
IsTemporary = temp;
MinimumRank = minRank;
Password = password;
AutoJoin = autoJoin;
MaxCapacity = maxCapacity;
Order = order;
}
}
}

View File

@ -0,0 +1,14 @@
using SharpChat.Channels;
using SharpChat.Sessions;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUserJoinEvent : Event {
public const string TYPE = @"channel:user:join";
public ChannelUserJoinEvent(IUser user, IChannel channel) : base(user, channel) { }
public ChannelUserJoinEvent(IChannel channel, ISession session) : base(channel, session) { }
}
}

View File

@ -0,0 +1,17 @@
using SharpChat.Channels;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class ChannelUserLeaveEvent : Event {
public const string TYPE = @"channel:user:leave";
public UserDisconnectReason Reason { get; }
public ChannelUserLeaveEvent(IUser user, IChannel channel, UserDisconnectReason reason)
: base(user, channel) {
Reason = reason;
}
}
}

View File

@ -0,0 +1,83 @@
using SharpChat.Channels;
using SharpChat.Protocol;
using SharpChat.Sessions;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
public abstract class Event : IEvent {
public long EventId { get; }
public DateTimeOffset DateTime { get; }
public long UserId { get; }
public string ChannelId { get; }
public string SessionId { get; }
public string ConnectionId { get; }
public Event(
long eventId,
DateTimeOffset dateTime,
long userId,
string channelId,
string sessionId,
string connectionId
) {
EventId = eventId;
DateTime = dateTime;
UserId = userId;
ChannelId = channelId ?? string.Empty;
SessionId = sessionId ?? string.Empty;
ConnectionId = connectionId ?? string.Empty;
}
public Event(DateTimeOffset dateTime, long userId, string channelId, string sessionId, string connectionId)
: this(SharpId.Next(), dateTime, userId, channelId, sessionId, connectionId) { }
public Event(long userId, string channelId, string sessionId, string connectionId)
: this(DateTimeOffset.Now, userId, channelId, sessionId, connectionId) { }
public Event(string channelName, string sessionId, string connectionId)
: this(-1L, channelName, sessionId, connectionId) { }
public Event(IUser user, IChannel channel, ISession session, IConnection connection)
: this(user?.UserId ?? -1L, channel?.ChannelId, session?.SessionId, connection?.ConnectionId) { }
public Event(IUser user, ISession session, IConnection connection)
: this(user, null, session, connection) { }
public Event(IUser user, IChannel channel, ISession session)
: this(user, channel, session, session?.Connection) { }
public Event(IUser user, IChannel channel)
: this(user, channel, null, null) { }
public Event(long userId, IChannel channel)
: this(userId, channel.ChannelId, null, null) { }
public Event(IChannel channel, ISession session)
: this(session?.User, channel, session, session?.Connection) { }
public Event(ISession session, IConnection connection)
: this(session?.User, null, session, connection) { }
public Event(IUser user)
: this(user, null, null, null) { }
public Event(long userId)
: this(userId, null, null, null) { }
public Event(IChannel channel)
: this(null, channel, null, null) { }
public Event(ISession session)
: this(session?.User, null, session, session?.Connection) { }
public Event(IConnection connection)
: this(connection?.Session?.User, null, connection?.Session, connection) { }
public Event()
: this(-1L, null, null, null) { }
public override string ToString()
=> $@"[{EventId}:{GetType().Name}] U:{UserId} Ch:{ChannelId} S:{SessionId} Co:{ConnectionId}";
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace SharpChat.Events {
[AttributeUsage(AttributeTargets.Class)]
public class EventAttribute : Attribute {
public string Type { get; }
public EventAttribute(string type) {
Type = type ?? throw new ArgumentNullException(nameof(type));
}
}
}

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace SharpChat.Events {
public class EventDispatcher : IEventDispatcher {
private Queue<(object sender, IEvent evt)> EventQueue { get; } = new();
private object SyncQueue { get; } = new();
private HashSet<IEventHandler> EventHandlers { get; } = new();
private object SyncHandlers { get; } = new();
private HashSet<IEventHandler> PreventDelete { get; } = new();
private object SyncPrevent { get; } = new();
private bool IsRunning = false;
private bool RunUntilEmpty = false;
private Thread ProcessThread = null;
[Conditional(@"DEBUG")]
private static void WithDebugColour(string str, ConsoleColor colour) {
ConsoleColor prev = Console.ForegroundColor;
Console.ForegroundColor = colour;
Logger.Debug(str);
Console.ForegroundColor = prev;
}
public void DispatchEvent(object sender, IEvent evt) {
lock(SyncQueue) {
WithDebugColour($@"+ {evt} <- {sender}.", ConsoleColor.Red);
EventQueue.Enqueue((sender, evt));
}
}
public void AddEventHandler(IEventHandler handler) {
if(handler == null)
throw new ArgumentNullException(nameof(handler));
lock(SyncHandlers)
EventHandlers.Add(handler);
}
internal void ProtectEventHandler(IEventHandler handler) {
lock(SyncPrevent)
PreventDelete.Add(handler);
}
public void RemoveEventHandler(IEventHandler handler) {
if(handler == null)
throw new ArgumentNullException(nameof(handler));
// prevent asshattery
lock(SyncPrevent)
if(PreventDelete.Contains(handler))
return;
lock(SyncHandlers)
EventHandlers.Remove(handler);
}
public bool ProcessNextQueue() {
(object sender, IEvent evt) queued;
lock(SyncQueue) {
if(!EventQueue.TryDequeue(out queued))
return false;
WithDebugColour($@"~ {queued.evt} <- {queued.sender}.", ConsoleColor.Green);
}
lock(SyncHandlers)
foreach(IEventHandler handler in EventHandlers)
handler.HandleEvent(queued.sender, queued.evt);
return true;
}
public void StartProcessing() {
if(IsRunning)
return;
IsRunning = true;
ProcessThread = new Thread(() => {
bool hadEvent;
do {
hadEvent = ProcessNextQueue();
if(RunUntilEmpty && !hadEvent)
StopProcessing();
else
Thread.Sleep(1);
} while(IsRunning);
});
ProcessThread.Start();
}
public void FinishProcessing() {
RunUntilEmpty = true;
ProcessThread.Join();
}
public void StopProcessing() {
IsRunning = false;
RunUntilEmpty = false;
}
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace SharpChat.Events {
public interface IEvent {
long EventId { get; }
DateTimeOffset DateTime { get; }
long UserId { get; }
string ChannelId { get; }
string SessionId { get; }
string ConnectionId { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace SharpChat.Events {
public interface IEventDispatcher {
void AddEventHandler(IEventHandler handler);
void RemoveEventHandler(IEventHandler handler);
void DispatchEvent(object sender, IEvent evt);
}
}

View File

@ -0,0 +1,6 @@
namespace SharpChat.Events {
public static class IEventExtensions {
public static bool IsBroadcast(this IEvent evt)
=> evt.ChannelId == null;
}
}

View File

@ -0,0 +1,5 @@
namespace SharpChat.Events {
public interface IEventHandler {
void HandleEvent(object sender, IEvent evt);
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class IPBanRemovedEvent : Event {
public const string TYPE = @"ban:remove:ip";
public IPAddress IPAddress { get; }
public IPBanRemovedEvent(IPAddress ipAddress) : base() {
IPAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));
}
}
}

View File

@ -0,0 +1,34 @@
using SharpChat.Messages;
using SharpChat.Sessions;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageCreateEvent : Event {
public const string TYPE = @"message:create";
public long MessageId { get; }
public string Text { get; }
public bool IsAction { get; }
public string UserName { get; }
public Colour UserColour { get; }
public int UserRank { get; }
public string UserNickName { get; }
public UserPermissions UserPermissions { get; }
public MessageCreateEvent(ISession session, IMessage message)
: base(message.Channel, session) {
MessageId = message.MessageId;
Text = message.Text;
IsAction = message.IsAction;
UserName = message.Sender.UserName;
UserColour = message.Sender.Colour;
UserRank = message.Sender.Rank;
UserNickName = message.Sender is ILocalUser localUser && !string.IsNullOrWhiteSpace(localUser.NickName)
? localUser.NickName
: null;
UserPermissions = message.Sender.Permissions;
}
}
}

View File

@ -0,0 +1,21 @@
using SharpChat.Messages;
using SharpChat.Users;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageDeleteEvent : Event {
public const string TYPE = @"message:delete";
public long MessageId { get; }
public MessageDeleteEvent(IUser actor, IMessage message)
: base(actor, message.Channel) {
MessageId = message.MessageId;
}
public MessageDeleteEvent(MessageUpdateEvent mue)
: base(mue.UserId, mue.ChannelId, null, null) {
MessageId = mue.MessageId;
}
}
}

View File

@ -0,0 +1,22 @@
using SharpChat.Messages;
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class MessageUpdateEvent : Event {
public const string TYPE = @"message:update";
public long MessageId { get; }
public string Text { get; }
public bool HasText
=> !string.IsNullOrEmpty(Text);
public MessageUpdateEvent(IMessage message, IUser editor, string text)
: base(editor, message.Channel) {
MessageId = message.MessageId;
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View File

@ -0,0 +1,12 @@
using SharpChat.Channels;
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionChannelSwitchEvent : Event {
public const string TYPE = @"session:channel:switch";
public SessionChannelSwitchEvent(IChannel channel, ISession session)
: base(channel, session) { }
}
}

View File

@ -0,0 +1,24 @@
using SharpChat.Sessions;
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionCreatedEvent : Event {
public const string TYPE = @"session:create";
public string ServerId { get; }
public DateTimeOffset LastPing { get; }
public bool IsSecure { get; }
public bool IsConnected { get; }
public IPAddress RemoteAddress { get; }
public SessionCreatedEvent(ISession session) : base(session) {
ServerId = session.ServerId;
LastPing = session.LastPing;
IsSecure = session.IsSecure;
IsConnected = session.IsConnected;
RemoteAddress = session.RemoteAddress;
}
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionDestroyEvent : Event {
public const string TYPE = @"session:destroy";
public SessionDestroyEvent(ISession session)
: base(session) {}
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionPingEvent : Event {
public const string TYPE = @"session:ping";
public SessionPingEvent(ISession session)
: base(session) { }
}
}

View File

@ -0,0 +1,29 @@
using SharpChat.Protocol;
using SharpChat.Sessions;
using System;
using System.Net;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionResumeEvent : Event {
public const string TYPE = @"session:resume";
public string ServerId { get; }
public IPAddress RemoteAddress { get; }
public bool HasConnection
=> ConnectionId != null;
public SessionResumeEvent(ISession session, string serverId, IPAddress remoteAddress)
: base(session) {
ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId));
RemoteAddress = remoteAddress ?? throw new ArgumentNullException(nameof(remoteAddress));
}
public SessionResumeEvent(ISession session, IConnection connection, string serverId)
: base(session, connection) {
ServerId = serverId ?? throw new ArgumentNullException(nameof(serverId));
RemoteAddress = connection?.RemoteAddress ?? throw new ArgumentNullException(nameof(connection));
}
}
}

View File

@ -0,0 +1,11 @@
using SharpChat.Sessions;
namespace SharpChat.Events {
[Event(TYPE)]
public class SessionSuspendEvent : Event {
public const string TYPE = @"session:suspend";
public SessionSuspendEvent(ISession session)
: base(session) { }
}
}

View File

@ -0,0 +1,29 @@
using SharpChat.Users.Remote;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserBanCreatedEvent : Event {
public const string TYPE = @"ban:create";
public long ModeratorUserId { get; }
public bool IsPermanent { get; }
public long Duration { get; }
public string Reason { get; }
public UserBanCreatedEvent(
IRemoteUser subject,
IRemoteUser moderator,
bool permanent,
TimeSpan duration,
string reason
) : base(
(subject ?? throw new ArgumentNullException(nameof(subject))).UserId
) {
ModeratorUserId = moderator?.UserId ?? -1;
IsPermanent = permanent;
Duration = (long)duration.TotalSeconds;
Reason = reason ?? throw new ArgumentNullException(nameof(reason));
}
}
}

View File

@ -0,0 +1,14 @@
using SharpChat.Users.Remote;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserBanRemovedEvent : Event {
public const string TYPE = @"ban:remove:user";
public UserBanRemovedEvent(IRemoteUser remoteUser)
: base(
(remoteUser ?? throw new ArgumentNullException(nameof(remoteUser))).UserId
) { }
}
}

View File

@ -0,0 +1,28 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserConnectEvent : Event {
public const string TYPE = @"user:connect";
public string Name { get; }
public Colour Colour { get; }
public int Rank { get; }
public UserPermissions Permissions { get; }
public string NickName { get; }
public UserStatus Status { get; }
public string StatusMessage { get; }
public UserConnectEvent(ILocalUser user)
: base(user ?? throw new ArgumentNullException(nameof(user))) {
Name = user.UserName;
Colour = user.Colour;
Rank = user.Rank;
Permissions = user.Permissions;
NickName = string.IsNullOrWhiteSpace(user.NickName) ? null : user.NickName;
Status = user.Status;
StatusMessage = user.StatusMessage;
}
}
}

View File

@ -0,0 +1,16 @@
using SharpChat.Users;
using System;
namespace SharpChat.Events {
[Event(TYPE)]
public class UserDisconnectEvent : Event {
public const string TYPE = @"user:disconnect";
public UserDisconnectReason Reason { get; }
public UserDisconnectEvent(IUser user, UserDisconnectReason reason)
: base(user ?? throw new ArgumentNullException(nameof(user))) {
Reason = reason;
}
}
}

Some files were not shown because too many files have changed in this diff Show More