Compare commits
6 commits
new-master
...
master
Author | SHA1 | Date | |
---|---|---|---|
8ab396668f | |||
3c800dd3e7 | |||
afd8d5b38a | |||
64affb6ef8 | |||
da62de6410 | |||
435635db2d |
442 changed files with 14646 additions and 7561 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "hamakaze"]
|
||||
path = hamakaze
|
||||
url = https://git.flash.moe/flash/hamakaze.git
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.") { }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
12
HttpClientTest/HttpClientTest.csproj
Normal file
12
HttpClientTest/HttpClientTest.csproj
Normal 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
147
HttpClientTest/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
13
MisuzuDataProviderTest/MisuzuDataProviderTest.csproj
Normal file
13
MisuzuDataProviderTest/MisuzuDataProviderTest.csproj
Normal 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>
|
99
MisuzuDataProviderTest/Program.cs
Normal file
99
MisuzuDataProviderTest/Program.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1323
Protocol-draft.md
1323
Protocol-draft.md
File diff suppressed because it is too large
Load diff
1584
Protocol.md
1584
Protocol.md
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
344
SharpChat.Common/Bans/BanManager.cs
Normal file
344
SharpChat.Common/Bans/BanManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Bans/IBanClient.cs
Normal file
14
SharpChat.Common/Bans/IBanClient.cs
Normal 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);
|
||||
}
|
||||
}
|
11
SharpChat.Common/Bans/IBanRecord.cs
Normal file
11
SharpChat.Common/Bans/IBanRecord.cs
Normal 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; }
|
||||
}
|
||||
}
|
159
SharpChat.Common/Channels/Channel.cs
Normal file
159
SharpChat.Common/Channels/Channel.cs
Normal 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}>";
|
||||
}
|
||||
}
|
447
SharpChat.Common/Channels/ChannelManager.cs
Normal file
447
SharpChat.Common/Channels/ChannelManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
421
SharpChat.Common/Channels/ChannelUserRelations.cs
Normal file
421
SharpChat.Common/Channels/ChannelUserRelations.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
SharpChat.Common/Channels/IChannel.cs
Normal file
18
SharpChat.Common/Channels/IChannel.cs
Normal 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; }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Channels/IChannelExtensions.cs
Normal file
11
SharpChat.Common/Channels/IChannelExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
29
SharpChat.Common/Colour.cs
Normal file
29
SharpChat.Common/Colour.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
45
SharpChat.Common/Configuration/CachedValue.cs
Normal file
45
SharpChat.Common/Configuration/CachedValue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
SharpChat.Common/Configuration/ConfigException.cs
Normal file
16
SharpChat.Common/Configuration/ConfigException.cs
Normal 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) { }
|
||||
}
|
||||
}
|
31
SharpChat.Common/Configuration/IConfig.cs
Normal file
31
SharpChat.Common/Configuration/IConfig.cs
Normal 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);
|
||||
}
|
||||
}
|
45
SharpChat.Common/Configuration/ScopedConfig.cs
Normal file
45
SharpChat.Common/Configuration/ScopedConfig.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
112
SharpChat.Common/Configuration/StreamConfig.cs
Normal file
112
SharpChat.Common/Configuration/StreamConfig.cs
Normal 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
145
SharpChat.Common/Context.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
8
SharpChat.Common/DataProvider/DataProviderAttribute.cs
Normal file
8
SharpChat.Common/DataProvider/DataProviderAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using SharpChat.Reflection;
|
||||
|
||||
namespace SharpChat.DataProvider {
|
||||
public class DataProviderAttribute : ObjectConstructorAttribute {
|
||||
public DataProviderAttribute(string name) : base(name) {
|
||||
}
|
||||
}
|
||||
}
|
9
SharpChat.Common/DataProvider/IDataProvider.cs
Normal file
9
SharpChat.Common/DataProvider/IDataProvider.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using SharpChat.Bans;
|
||||
using SharpChat.Users.Remote;
|
||||
|
||||
namespace SharpChat.DataProvider {
|
||||
public interface IDataProvider {
|
||||
IBanClient BanClient { get; }
|
||||
IRemoteUserClient UserClient { get; }
|
||||
}
|
||||
}
|
30
SharpChat.Common/DataProvider/Null/NullBanClient.cs
Normal file
30
SharpChat.Common/DataProvider/Null/NullBanClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
15
SharpChat.Common/DataProvider/Null/NullDataProvider.cs
Normal file
15
SharpChat.Common/DataProvider/Null/NullDataProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.Common/DataProvider/Null/NullUserAuthResponse.cs
Normal file
28
SharpChat.Common/DataProvider/Null/NullUserAuthResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
33
SharpChat.Common/DataProvider/Null/NullUserClient.cs
Normal file
33
SharpChat.Common/DataProvider/Null/NullUserClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
81
SharpChat.Common/Database/ADODatabaseReader.cs
Normal file
81
SharpChat.Common/Database/ADODatabaseReader.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
8
SharpChat.Common/Database/DatabaseBackendAttribute.cs
Normal file
8
SharpChat.Common/Database/DatabaseBackendAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
using SharpChat.Reflection;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public class DatabaseBackendAttribute : ObjectConstructorAttribute {
|
||||
public DatabaseBackendAttribute(string name) : base(name) {
|
||||
}
|
||||
}
|
||||
}
|
7
SharpChat.Common/Database/DatabaseException.cs
Normal file
7
SharpChat.Common/Database/DatabaseException.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public class DatabaseException : Exception {}
|
||||
|
||||
public class InvalidParameterClassTypeException : DatabaseException { }
|
||||
}
|
14
SharpChat.Common/Database/DatabaseType.cs
Normal file
14
SharpChat.Common/Database/DatabaseType.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace SharpChat.Database {
|
||||
public enum DatabaseType {
|
||||
AsciiString,
|
||||
UnicodeString,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
}
|
||||
}
|
109
SharpChat.Common/Database/DatabaseWrapper.cs
Normal file
109
SharpChat.Common/Database/DatabaseWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
37
SharpChat.Common/Database/IDatabaseBackend.cs
Normal file
37
SharpChat.Common/Database/IDatabaseBackend.cs
Normal 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; }
|
||||
}
|
||||
}
|
21
SharpChat.Common/Database/IDatabaseCommand.cs
Normal file
21
SharpChat.Common/Database/IDatabaseCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
7
SharpChat.Common/Database/IDatabaseConnection.cs
Normal file
7
SharpChat.Common/Database/IDatabaseConnection.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Database {
|
||||
public interface IDatabaseConnection : IDisposable {
|
||||
IDatabaseCommand CreateCommand(object query);
|
||||
}
|
||||
}
|
6
SharpChat.Common/Database/IDatabaseParameter.cs
Normal file
6
SharpChat.Common/Database/IDatabaseParameter.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Database {
|
||||
public interface IDatabaseParameter {
|
||||
string Name { get; }
|
||||
object Value { get; set; }
|
||||
}
|
||||
}
|
37
SharpChat.Common/Database/IDatabaseReader.cs
Normal file
37
SharpChat.Common/Database/IDatabaseReader.cs
Normal 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);
|
||||
}
|
||||
}
|
63
SharpChat.Common/Database/Null/NullDatabaseBackend.cs
Normal file
63
SharpChat.Common/Database/Null/NullDatabaseBackend.cs
Normal 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;
|
||||
}
|
||||
}
|
47
SharpChat.Common/Database/Null/NullDatabaseCommand.cs
Normal file
47
SharpChat.Common/Database/Null/NullDatabaseCommand.cs
Normal 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() {}
|
||||
}
|
||||
}
|
13
SharpChat.Common/Database/Null/NullDatabaseConnection.cs
Normal file
13
SharpChat.Common/Database/Null/NullDatabaseConnection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
6
SharpChat.Common/Database/Null/NullDatabaseParameter.cs
Normal file
6
SharpChat.Common/Database/Null/NullDatabaseParameter.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Database.Null {
|
||||
public class NullDatabaseParameter : IDatabaseParameter {
|
||||
public string Name => string.Empty;
|
||||
public object Value { get => null; set { } }
|
||||
}
|
||||
}
|
92
SharpChat.Common/Database/Null/NullDatabaseReader.cs
Normal file
92
SharpChat.Common/Database/Null/NullDatabaseReader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
15
SharpChat.Common/Events/BroadcastMessageEvent.cs
Normal file
15
SharpChat.Common/Events/BroadcastMessageEvent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.Common/Events/ChannelCreateEvent.cs
Normal file
28
SharpChat.Common/Events/ChannelCreateEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
13
SharpChat.Common/Events/ChannelDeleteEvent.cs
Normal file
13
SharpChat.Common/Events/ChannelDeleteEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/ChannelSessionJoinEvent.cs
Normal file
11
SharpChat.Common/Events/ChannelSessionJoinEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/ChannelSessionLeaveEvent.cs
Normal file
11
SharpChat.Common/Events/ChannelSessionLeaveEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
47
SharpChat.Common/Events/ChannelUpdateEvent.cs
Normal file
47
SharpChat.Common/Events/ChannelUpdateEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Events/ChannelUserJoinEvent.cs
Normal file
14
SharpChat.Common/Events/ChannelUserJoinEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
17
SharpChat.Common/Events/ChannelUserLeaveEvent.cs
Normal file
17
SharpChat.Common/Events/ChannelUserLeaveEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
83
SharpChat.Common/Events/Event.cs
Normal file
83
SharpChat.Common/Events/Event.cs
Normal 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}";
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/EventAttribute.cs
Normal file
12
SharpChat.Common/Events/EventAttribute.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
103
SharpChat.Common/Events/EventDispatcher.cs
Normal file
103
SharpChat.Common/Events/EventDispatcher.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/IEvent.cs
Normal file
12
SharpChat.Common/Events/IEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
7
SharpChat.Common/Events/IEventDispatcher.cs
Normal file
7
SharpChat.Common/Events/IEventDispatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
6
SharpChat.Common/Events/IEventExtensions.cs
Normal file
6
SharpChat.Common/Events/IEventExtensions.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.Events {
|
||||
public static class IEventExtensions {
|
||||
public static bool IsBroadcast(this IEvent evt)
|
||||
=> evt.ChannelId == null;
|
||||
}
|
||||
}
|
5
SharpChat.Common/Events/IEventHandler.cs
Normal file
5
SharpChat.Common/Events/IEventHandler.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace SharpChat.Events {
|
||||
public interface IEventHandler {
|
||||
void HandleEvent(object sender, IEvent evt);
|
||||
}
|
||||
}
|
15
SharpChat.Common/Events/IPBanRemovedEvent.cs
Normal file
15
SharpChat.Common/Events/IPBanRemovedEvent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
34
SharpChat.Common/Events/MessageCreateEvent.cs
Normal file
34
SharpChat.Common/Events/MessageCreateEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.Common/Events/MessageDeleteEvent.cs
Normal file
21
SharpChat.Common/Events/MessageDeleteEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
22
SharpChat.Common/Events/MessageUpdateEvent.cs
Normal file
22
SharpChat.Common/Events/MessageUpdateEvent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
12
SharpChat.Common/Events/SessionChannelSwitchEvent.cs
Normal file
12
SharpChat.Common/Events/SessionChannelSwitchEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
24
SharpChat.Common/Events/SessionCreatedEvent.cs
Normal file
24
SharpChat.Common/Events/SessionCreatedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionDestroyEvent.cs
Normal file
11
SharpChat.Common/Events/SessionDestroyEvent.cs
Normal 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) {}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionPingEvent.cs
Normal file
11
SharpChat.Common/Events/SessionPingEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
29
SharpChat.Common/Events/SessionResumeEvent.cs
Normal file
29
SharpChat.Common/Events/SessionResumeEvent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
11
SharpChat.Common/Events/SessionSuspendEvent.cs
Normal file
11
SharpChat.Common/Events/SessionSuspendEvent.cs
Normal 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) { }
|
||||
}
|
||||
}
|
29
SharpChat.Common/Events/UserBanCreatedEvent.cs
Normal file
29
SharpChat.Common/Events/UserBanCreatedEvent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
14
SharpChat.Common/Events/UserBanRemovedEvent.cs
Normal file
14
SharpChat.Common/Events/UserBanRemovedEvent.cs
Normal 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
|
||||
) { }
|
||||
}
|
||||
}
|
28
SharpChat.Common/Events/UserConnectEvent.cs
Normal file
28
SharpChat.Common/Events/UserConnectEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
SharpChat.Common/Events/UserDisconnectEvent.cs
Normal file
16
SharpChat.Common/Events/UserDisconnectEvent.cs
Normal 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
Loading…
Add table
Reference in a new issue