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
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
SharpChat.Common/version.txt
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.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
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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