Compare commits
92 commits
master
...
new-master
Author | SHA1 | Date | |
---|---|---|---|
42a0160cde | |||
6bda1ee09d | |||
86effa0452 | |||
2eae48325a | |||
09d5bfef82 | |||
d426df91f0 | |||
5daad52aba | |||
454a460441 | |||
651c3f127d | |||
4ace355374 | |||
968df2b161 | |||
12e7bd2768 | |||
cfbe98d34a | |||
e0f83ca259 | |||
980ec5b855 | |||
a0e6fbbeea | |||
610f9ab142 | |||
fa8c416b77 | |||
1d781bd72c | |||
042b6ddbd6 | |||
c490dcf128 | |||
549c80740d | |||
1a8c44a4ba | |||
bd23d3aa15 | |||
68a523f76a | |||
322500739e | |||
a6a7e56bd1 | |||
7bcf5acb7e | |||
38f17c325a | |||
907711e753 | |||
8cc00fe1a8 | |||
3c58e5201a | |||
795a87fe56 | |||
a6569815af | |||
b95cd06cb1 | |||
356409eb16 | |||
1ba94a526c | |||
0b0de00cc4 | |||
b1fae4bdeb | |||
fc7d428f76 | |||
54af837c82 | |||
0d0f2e68b9 | |||
e291a17705 | |||
cd32995367 | |||
5985f63744 | |||
a9ca3705ad | |||
294471dcfd | |||
c46d117d15 | |||
05fcbcb0f8 | |||
03b3b6b0a3 | |||
a7a05f04bd | |||
dc4989a3cf | |||
903e39ab76 | |||
8c19c22736 | |||
4e0def980f | |||
82973f7a33 | |||
8de3ba8dbb | |||
86a46539f2 | |||
70df99fe9b | |||
546e8a2c83 | |||
d268a419dc | |||
1466562c54 | |||
a5089f14b8 | |||
13ae843c8d | |||
06af94e94f | |||
c8a589c1c1 | |||
ea56af0210 | |||
d1f78a7e8b | |||
dbdaaeec9e | |||
8050a295c1 | |||
c291ef178d | |||
c21605cf3b | |||
e1e3def62c | |||
56a818254e | |||
40c7ba4ded | |||
27c28aafcd | |||
36f3ff6385 | |||
5e3eecda8c | |||
4104e40843 | |||
c9cc5ff23a | |||
513539319f | |||
d2fef02e08 | |||
1051a26494 | |||
6f50ec66a9 | |||
e6dffe06e6 | |||
9790f77a16 | |||
08f9a2c5a1 | |||
ea83c8cca0 | |||
bfd1819798 | |||
2de19035ff | |||
3f8c2781ee | |||
23f0bd478f |
226 changed files with 7292 additions and 8053 deletions
4
.editorconfig
Normal file
4
.editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
|||
[*.{cs,vb}]
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_style_prefer_conditional_expression_over_return = false
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,6 +1,15 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
welcome.txt
|
||||
mariadb.txt
|
||||
login_key.txt
|
||||
http-motd.txt
|
||||
_webdb.txt
|
||||
msz_url.txt
|
||||
sharpchat.cfg
|
||||
SharpChat/version.txt
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019-2022 flashwave
|
||||
Copyright (c) 2019-2024 flashwave
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
1323
Protocol-draft.md
1323
Protocol-draft.md
File diff suppressed because it is too large
Load diff
1917
Protocol.md
1917
Protocol.md
File diff suppressed because it is too large
Load diff
|
@ -7,4 +7,7 @@
|
|||
/_/
|
||||
```
|
||||
|
||||
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 Flashii Chat server!
|
||||
|
||||
The protocol used is based on the protocol found in the original [PHP Sock Chat](https://patchii.net/sockchat/sockchat).
|
||||
A rendered version of the Protocol.md document can be found on [railgun.sh/sockchat](https://railgun.sh/sockchat).
|
||||
|
|
31
SharpChat.Misuzu/MisuzuAuthInfo.cs
Normal file
31
SharpChat.Misuzu/MisuzuAuthInfo.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuAuthInfo {
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = "none";
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public long UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string? UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("colour_raw")]
|
||||
public int ColourRaw { get; set; }
|
||||
|
||||
public Colour Colour => Colour.FromMisuzu(ColourRaw);
|
||||
|
||||
[JsonPropertyName("hierarchy")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
[JsonPropertyName("perms")]
|
||||
public UserPermissions Permissions { get; set; }
|
||||
|
||||
[JsonPropertyName("super")]
|
||||
public bool IsSuper { get; set; }
|
||||
}
|
||||
}
|
32
SharpChat.Misuzu/MisuzuBanInfo.cs
Normal file
32
SharpChat.Misuzu/MisuzuBanInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuBanInfo {
|
||||
[JsonPropertyName("is_ban")]
|
||||
public bool IsBanned { get; set; }
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("ip_addr")]
|
||||
public string? RemoteAddress { get; set; }
|
||||
|
||||
[JsonPropertyName("is_perma")]
|
||||
public bool IsPermanent { get; set; }
|
||||
|
||||
[JsonPropertyName("expires")]
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
|
||||
// only populated in list request
|
||||
[JsonPropertyName("user_name")]
|
||||
public string? UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("user_colour")]
|
||||
public int UserColourRaw { get; set; }
|
||||
|
||||
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
|
||||
|
||||
public Colour UserColour => Colour.FromMisuzu(UserColourRaw);
|
||||
}
|
||||
}
|
246
SharpChat.Misuzu/MisuzuClient.cs
Normal file
246
SharpChat.Misuzu/MisuzuClient.cs
Normal file
|
@ -0,0 +1,246 @@
|
|||
using SharpChat.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuClient {
|
||||
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
|
||||
private const string DEFAULT_SECRET_KEY = "woomy";
|
||||
|
||||
private const string BUMP_ONLINE_URL = "{0}/bump";
|
||||
private const string AUTH_VERIFY_URL = "{0}/verify";
|
||||
|
||||
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
|
||||
private const string BANS_CREATE_URL = "{0}/bans/create";
|
||||
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
|
||||
private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
|
||||
|
||||
private const string VERIFY_SIG = "verify#{0}#{1}#{2}";
|
||||
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
|
||||
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
|
||||
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
|
||||
private const string BANS_LIST_SIG = "list#{0}";
|
||||
|
||||
private readonly HttpClient HttpClient;
|
||||
|
||||
private CachedValue<string> BaseURL { get; }
|
||||
private CachedValue<string> SecretKey { get; }
|
||||
|
||||
public MisuzuClient(HttpClient httpClient, IConfig config) {
|
||||
HttpClient = httpClient;
|
||||
|
||||
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
||||
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
||||
}
|
||||
|
||||
public string CreateStringSignature(string str) {
|
||||
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
public string CreateBufferSignature(byte[] bytes) {
|
||||
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey.Value ?? string.Empty));
|
||||
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
|
||||
}
|
||||
|
||||
public async Task<MisuzuAuthInfo?> AuthVerifyAsync(string method, string token, string ipAddr) {
|
||||
method ??= string.Empty;
|
||||
token ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
||||
string sig = string.Format(VERIFY_SIG, method, token, ipAddr);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "method", method },
|
||||
{ "token", token },
|
||||
{ "ipaddr", ipAddr },
|
||||
}),
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuAuthInfo>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
|
||||
if(!list.Any())
|
||||
return;
|
||||
|
||||
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
StringBuilder sb = new();
|
||||
sb.AppendFormat("bump#{0}", now);
|
||||
|
||||
Dictionary<string, string> formData = new() {
|
||||
{ "t", now }
|
||||
};
|
||||
|
||||
foreach(var (userId, ipAddr) in list) {
|
||||
sb.AppendFormat("#{0}:{1}", userId, ipAddr);
|
||||
formData.Add(string.Format("u[{0}]", userId), ipAddr);
|
||||
}
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(BUMP_ONLINE_URL, BaseURL)) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
|
||||
},
|
||||
Content = new FormUrlEncodedContent(formData),
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
try {
|
||||
res.EnsureSuccessStatusCode();
|
||||
} catch(HttpRequestException) {
|
||||
Logger.Debug(await res.Content.ReadAsStringAsync());
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo?> CheckBanAsync(
|
||||
string? userId = null,
|
||||
string? ipAddr = null,
|
||||
bool userIdIsName = false
|
||||
) {
|
||||
userId ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
||||
string userIdIsNameStr = userIdIsName ? "1" : "0";
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr));
|
||||
string sig = string.Format(BANS_CHECK_SIG, now, userId, ipAddr, userIdIsNameStr);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Get, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuBanInfo>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo[]?> GetBanListAsync() {
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
||||
string sig = string.Format(BANS_LIST_SIG, now);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Get, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuBanInfo[]>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public enum BanRevokeKind {
|
||||
UserId,
|
||||
RemoteAddress,
|
||||
}
|
||||
|
||||
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
|
||||
string type = kind switch {
|
||||
BanRevokeKind.UserId => "user",
|
||||
BanRevokeKind.RemoteAddress => "addr",
|
||||
_ => throw new ArgumentException("Invalid kind specified.", nameof(kind)),
|
||||
};
|
||||
|
||||
string target = kind switch {
|
||||
BanRevokeKind.UserId => banInfo?.UserId ?? string.Empty,
|
||||
BanRevokeKind.RemoteAddress => banInfo?.RemoteAddress ?? string.Empty,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
|
||||
string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Delete, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
if(res.StatusCode == HttpStatusCode.NotFound)
|
||||
return false;
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
return res.StatusCode == HttpStatusCode.NoContent;
|
||||
}
|
||||
|
||||
public async Task CreateBanAsync(
|
||||
string targetId,
|
||||
string targetAddr,
|
||||
string modId,
|
||||
string modAddr,
|
||||
TimeSpan duration,
|
||||
string reason
|
||||
) {
|
||||
if(string.IsNullOrWhiteSpace(targetAddr))
|
||||
throw new ArgumentException("targetAddr may not be empty", nameof(targetAddr));
|
||||
if(string.IsNullOrWhiteSpace(modAddr))
|
||||
throw new ArgumentException("modAddr may not be empty", nameof(modAddr));
|
||||
if(duration <= TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
modId ??= string.Empty;
|
||||
targetId ??= string.Empty;
|
||||
reason ??= string.Empty;
|
||||
|
||||
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
|
||||
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
|
||||
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string sig = string.Format(
|
||||
BANS_CREATE_SIG,
|
||||
now, targetId, targetAddr,
|
||||
modId, modAddr,
|
||||
durationStr, isPerma, reason
|
||||
);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "t", now },
|
||||
{ "ui", targetId },
|
||||
{ "ua", targetAddr },
|
||||
{ "mi", modId },
|
||||
{ "ma", modAddr },
|
||||
{ "d", durationStr },
|
||||
{ "p", isPerma },
|
||||
{ "r", reason },
|
||||
}),
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
12
SharpChat.Misuzu/SharpChat.Misuzu.csproj
Normal file
12
SharpChat.Misuzu/SharpChat.Misuzu.csproj
Normal file
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
36
SharpChat.SockChat/Commands/BanListCommand.cs
Normal file
36
SharpChat.SockChat/Commands/BanListCommand.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class BanListCommand : ISockChatClientCommand {
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
public BanListCommand(MisuzuClient msz) {
|
||||
Misuzu = msz;
|
||||
}
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("bans")
|
||||
|| ctx.NameEquals("banned");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
|
||||
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(async () => {
|
||||
ctx.Chat.SendTo(ctx.User, new BanListResponseS2CPacket(
|
||||
(await Misuzu.GetBanListAsync() ?? Array.Empty<MisuzuBanInfo>()).Select(
|
||||
ban => string.IsNullOrEmpty(ban.UserName) ? (string.IsNullOrEmpty(ban.RemoteAddress) ? string.Empty : ban.RemoteAddress) : ban.UserName
|
||||
).ToArray()
|
||||
));
|
||||
}).Wait();
|
||||
}
|
||||
}
|
||||
}
|
66
SharpChat.SockChat/Commands/ChannelCreateCommand.cs
Normal file
66
SharpChat.SockChat/Commands/ChannelCreateCommand.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ChannelCreateCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("create");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.CreateChannel)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string firstArg = ctx.Args.First();
|
||||
|
||||
bool createChanHasHierarchy;
|
||||
if(!ctx.Args.Any() || (createChanHasHierarchy = firstArg.All(char.IsDigit) && ctx.Args.Length < 2)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
int createChanHierarchy = 0;
|
||||
if(createChanHasHierarchy)
|
||||
if(!int.TryParse(firstArg, out createChanHierarchy))
|
||||
createChanHierarchy = 0;
|
||||
|
||||
if(createChanHierarchy > ctx.User.Rank) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelRankTooHighErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
string channelName = string.Join('_', ctx.Args.Skip(createChanHasHierarchy ? 1 : 0));
|
||||
|
||||
if(!SockChatUtility.CheckChannelName(channelName)) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelNameFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
if(ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName) != null) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelNameInUseErrorS2CPacket(channelName));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"chan:add",
|
||||
channelName,
|
||||
ctx.User,
|
||||
new ChannelAddEventData(
|
||||
!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPermanent),
|
||||
createChanHierarchy,
|
||||
string.Empty
|
||||
)
|
||||
);
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User);
|
||||
ctx.Chat.Events.Dispatch("chan:join", now, channelName, ctx.User);
|
||||
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelCreateResponseS2CPacket(channelName));
|
||||
}
|
||||
}
|
||||
}
|
36
SharpChat.SockChat/Commands/ChannelDeleteCommand.cs
Normal file
36
SharpChat.SockChat/Commands/ChannelDeleteCommand.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ChannelDeleteCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("delchan") || (
|
||||
ctx.NameEquals("delete")
|
||||
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == false
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.Args.Any() || string.IsNullOrWhiteSpace(ctx.Args.FirstOrDefault())) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
string delChanName = string.Join('_', ctx.Args);
|
||||
ChannelInfo? delChan = ctx.Chat.Channels.Get(delChanName, SockChatUtility.SanitiseChannelName);
|
||||
|
||||
if(delChan == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(delChanName));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.DeleteChannel) || delChan.OwnerId != ctx.User.UserId) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelDeleteNotAllowedErrorS2CPacket(delChan.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch("chan:delete", delChan, ctx.User);
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelDeleteResponseS2CPacket(delChan.Name));
|
||||
}
|
||||
}
|
||||
}
|
48
SharpChat.SockChat/Commands/ChannelJoinCommand.cs
Normal file
48
SharpChat.SockChat/Commands/ChannelJoinCommand.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ChannelJoinCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("join");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
string channelName = ctx.Args.FirstOrDefault() ?? string.Empty;
|
||||
string password = string.Join(' ', ctx.Args.Skip(1));
|
||||
|
||||
ChannelInfo? channelInfo = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName);
|
||||
if(channelInfo == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(channelName));
|
||||
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
if(channelInfo.Name.Equals(ctx.Channel.Name, StringComparison.InvariantCultureIgnoreCase)) {
|
||||
// this is where the elusive commented out "samechan" error would go!
|
||||
// https://patchii.net/sockchat/sockchat/src/commit/6c2111fb4b0241f9ef31060b0f86e7176664f572/server/lib/context.php#L61
|
||||
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.JoinAnyChannel) && channelInfo.OwnerId != ctx.User.UserId) {
|
||||
if(channelInfo.Rank > ctx.User.Rank) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelRankTooLowErrorS2CPacket(channelInfo.Name));
|
||||
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(channelInfo.Password) && channelInfo.Password.Equals(password)) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelPasswordWrongErrorS2CPacket(channelInfo.Name));
|
||||
ctx.Chat.SendTo(ctx.User, new UserChannelForceJoinS2CPacket(ctx.Channel.Name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
ctx.Chat.Events.Dispatch("chan:leave", now, ctx.Channel, ctx.User);
|
||||
ctx.Chat.Events.Dispatch("chan:join", now, channelInfo, ctx.User);
|
||||
}
|
||||
}
|
||||
}
|
26
SharpChat.SockChat/Commands/ChannelPasswordCommand.cs
Normal file
26
SharpChat.SockChat/Commands/ChannelPasswordCommand.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ChannelPasswordCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("pwd")
|
||||
|| ctx.NameEquals("password");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelPassword) || ctx.Channel.OwnerId != ctx.User.UserId) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string chanPass = string.Join(' ', ctx.Args).Trim();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(chanPass))
|
||||
chanPass = string.Empty;
|
||||
|
||||
ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(password: chanPass));
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelPasswordChangedResponseS2CPacket());
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.SockChat/Commands/ChannelRankCommand.cs
Normal file
28
SharpChat.SockChat/Commands/ChannelRankCommand.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ChannelRankCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("rank")
|
||||
|| ctx.NameEquals("privilege")
|
||||
|| ctx.NameEquals("priv");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.SetChannelHierarchy) || ctx.Channel.OwnerId != ctx.User.UserId) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ctx.Args.Any() || !int.TryParse(ctx.Args.First(), out int chanMinRank) || chanMinRank > ctx.User.Rank) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelRankTooHighErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch("chan:update", ctx.Channel.Name, ctx.User, new ChannelUpdateEventData(minRank: chanMinRank));
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelRankChangedResponseS2CPacket());
|
||||
}
|
||||
}
|
||||
}
|
6
SharpChat.SockChat/Commands/ISockChatClientCommand.cs
Normal file
6
SharpChat.SockChat/Commands/ISockChatClientCommand.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.SockChat.Commands {
|
||||
public interface ISockChatClientCommand {
|
||||
bool IsMatch(SockChatClientCommandContext ctx);
|
||||
void Dispatch(SockChatClientCommandContext ctx);
|
||||
}
|
||||
}
|
86
SharpChat.SockChat/Commands/KickBanCommand.cs
Normal file
86
SharpChat.SockChat/Commands/KickBanCommand.cs
Normal file
|
@ -0,0 +1,86 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class KickBanCommand : ISockChatClientCommand {
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
public KickBanCommand(MisuzuClient msz) {
|
||||
Misuzu = msz;
|
||||
}
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("kick")
|
||||
|| ctx.NameEquals("ban");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
bool isBanning = ctx.NameEquals("ban");
|
||||
|
||||
if(!ctx.User.Permissions.HasFlag(isBanning ? UserPermissions.BanUser : UserPermissions.KickUser)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string banUserTarget = ctx.Args.ElementAtOrDefault(0) ?? string.Empty;
|
||||
string? banDurationStr = ctx.Args.ElementAtOrDefault(1);
|
||||
int banReasonIndex = 1;
|
||||
UserInfo? banUser = null;
|
||||
|
||||
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(banUserTarget);
|
||||
if(string.IsNullOrEmpty(name) || (banUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(banUserTarget));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ctx.User.IsSuper && banUser.Rank >= ctx.User.Rank && banUser != ctx.User) {
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorS2CPacket(SockChatUtility.GetUserName(banUser)));
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero;
|
||||
if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) {
|
||||
if(durationSeconds < 0) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
duration = TimeSpan.FromSeconds(durationSeconds);
|
||||
++banReasonIndex;
|
||||
}
|
||||
|
||||
if(duration <= TimeSpan.Zero) {
|
||||
ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, TimeSpan.Zero));
|
||||
return;
|
||||
}
|
||||
|
||||
string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
|
||||
|
||||
Task.Run(async () => {
|
||||
string userId = banUser.UserId.ToString();
|
||||
|
||||
MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId);
|
||||
if(fbi != null && fbi.IsBanned && !fbi.HasExpired) {
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNotAllowedErrorS2CPacket(SockChatUtility.GetUserName(banUser)));
|
||||
return;
|
||||
}
|
||||
|
||||
string[] userRemoteAddrs = ctx.Chat.Connections.GetUserRemoteAddresses(banUser);
|
||||
string userRemoteAddr = string.Format(", ", userRemoteAddrs);
|
||||
|
||||
// Misuzu only stores the IP address in private comment and doesn't do any checking, so this is fine.
|
||||
await Misuzu.CreateBanAsync(
|
||||
userId, userRemoteAddr,
|
||||
ctx.User.UserId.ToString(), ctx.Connection.RemoteAddress,
|
||||
duration, banReason
|
||||
);
|
||||
|
||||
ctx.Chat.Events.Dispatch("user:kickban", banUser, UserKickBanEventData.OfDuration(UserDisconnectReason.Kicked, duration));
|
||||
}).Wait();
|
||||
}
|
||||
}
|
||||
}
|
22
SharpChat.SockChat/Commands/MessageActionCommand.cs
Normal file
22
SharpChat.SockChat/Commands/MessageActionCommand.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using SharpChat.Events;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class MessageActionCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("action")
|
||||
|| ctx.NameEquals("me");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.Args.Any())
|
||||
return;
|
||||
|
||||
string actionStr = string.Join(' ', ctx.Args);
|
||||
if(string.IsNullOrWhiteSpace(actionStr))
|
||||
return;
|
||||
|
||||
ctx.Chat.Events.Dispatch("msg:add", ctx.Channel, ctx.User, new MessageAddEventData(actionStr, true));
|
||||
}
|
||||
}
|
||||
}
|
24
SharpChat.SockChat/Commands/MessageBroadcastCommand.cs
Normal file
24
SharpChat.SockChat/Commands/MessageBroadcastCommand.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class MessageBroadcastCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("say")
|
||||
|| ctx.NameEquals("broadcast");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.Broadcast)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"msg:add",
|
||||
ctx.User,
|
||||
new MessageAddEventData(string.Join(' ', ctx.Args))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
42
SharpChat.SockChat/Commands/MessageDeleteCommand.cs
Normal file
42
SharpChat.SockChat/Commands/MessageDeleteCommand.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class MessageDeleteCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("delmsg") || (
|
||||
ctx.NameEquals("delete")
|
||||
&& ctx.Args.FirstOrDefault()?.All(char.IsDigit) == true
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
bool deleteAnyMessage = ctx.User.Permissions.HasFlag(UserPermissions.DeleteAnyMessage);
|
||||
|
||||
if(!deleteAnyMessage && !ctx.User.Permissions.HasFlag(UserPermissions.DeleteOwnMessage)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string? firstArg = ctx.Args.FirstOrDefault();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(firstArg) || !firstArg.All(char.IsDigit) || !long.TryParse(firstArg, out long eventId)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
ChatEventInfo? eventInfo = ctx.Chat.EventStorage.GetEvent(eventId);
|
||||
|
||||
if(eventInfo == null
|
||||
|| !eventInfo.Type.Equals("msg:add")
|
||||
|| eventInfo.SenderRank > ctx.User.Rank
|
||||
|| (!deleteAnyMessage && eventInfo.SenderId != ctx.User.UserId)) {
|
||||
ctx.Chat.SendTo(ctx.User, new MessageDeleteNotAllowedErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch("msg:delete", eventInfo.ChannelName, ctx.User, new MessageDeleteEventData(eventInfo.Id.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
38
SharpChat.SockChat/Commands/MessageWhisperCommand.cs
Normal file
38
SharpChat.SockChat/Commands/MessageWhisperCommand.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class MessageWhisperCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("whisper")
|
||||
|| ctx.NameEquals("msg");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(ctx.Args.Length < 2) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
string whisperUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
|
||||
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(whisperUserStr);
|
||||
UserInfo? whisperUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
|
||||
|
||||
if(whisperUser == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(whisperUserStr));
|
||||
return;
|
||||
}
|
||||
|
||||
if(whisperUser == ctx.User)
|
||||
return;
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"msg:add",
|
||||
UserInfo.GetDMChannelName(ctx.User, whisperUser),
|
||||
ctx.User,
|
||||
new MessageAddEventData(string.Join(' ', ctx.Args.Skip(1)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
51
SharpChat.SockChat/Commands/PardonAddressCommand.cs
Normal file
51
SharpChat.SockChat/Commands/PardonAddressCommand.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class PardonAddressCommand : ISockChatClientCommand {
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
public PardonAddressCommand(MisuzuClient msz) {
|
||||
Misuzu = msz;
|
||||
}
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("pardonip")
|
||||
|| ctx.NameEquals("unbanip");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
|
||||
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string? unbanAddrTarget = ctx.Args.FirstOrDefault();
|
||||
if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress? unbanAddr)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
unbanAddrTarget = unbanAddr.ToString();
|
||||
|
||||
Task.Run(async () => {
|
||||
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget);
|
||||
|
||||
if(banInfo?.IsBanned != true || banInfo.HasExpired) {
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanAddrTarget));
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress);
|
||||
if(wasBanned)
|
||||
ctx.Chat.SendTo(ctx.User, new PardonResponseS2CPacket(unbanAddrTarget));
|
||||
else
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanAddrTarget));
|
||||
}).Wait();
|
||||
}
|
||||
}
|
||||
}
|
59
SharpChat.SockChat/Commands/PardonUserCommand.cs
Normal file
59
SharpChat.SockChat/Commands/PardonUserCommand.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class PardonUserCommand : ISockChatClientCommand {
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
public PardonUserCommand(MisuzuClient msz) {
|
||||
Misuzu = msz;
|
||||
}
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("pardon")
|
||||
|| ctx.NameEquals("unban");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.KickUser)
|
||||
&& !ctx.User.Permissions.HasFlag(UserPermissions.BanUser)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
bool unbanUserTargetIsName = true;
|
||||
string? unbanUserTarget = ctx.Args.FirstOrDefault();
|
||||
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(unbanUserTarget);
|
||||
UserInfo? unbanUser = ctx.Chat.Users.Get(name: name, nameTarget: target);
|
||||
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
|
||||
unbanUserTargetIsName = false;
|
||||
unbanUser = ctx.Chat.Users.Get(unbanUserId);
|
||||
}
|
||||
|
||||
if(unbanUser != null)
|
||||
unbanUserTarget = unbanUser.UserId.ToString();
|
||||
|
||||
Task.Run(async () => {
|
||||
MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName);
|
||||
|
||||
if(banInfo?.IsBanned != true || banInfo.HasExpired) {
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanUserTarget));
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId);
|
||||
if(wasBanned)
|
||||
ctx.Chat.SendTo(ctx.User, new PardonResponseS2CPacket(unbanUserTarget));
|
||||
else
|
||||
ctx.Chat.SendTo(ctx.User, new KickBanNoRecordErrorS2CPacket(unbanUserTarget));
|
||||
}).Wait();
|
||||
}
|
||||
}
|
||||
}
|
40
SharpChat.SockChat/Commands/ShutdownRestartCommand.cs
Normal file
40
SharpChat.SockChat/Commands/ShutdownRestartCommand.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class ShutdownRestartCommand : ISockChatClientCommand {
|
||||
private readonly ManualResetEvent WaitHandle;
|
||||
private readonly Func<bool> ShuttingDown;
|
||||
private readonly Action<bool> SetShutdown;
|
||||
|
||||
public ShutdownRestartCommand(
|
||||
ManualResetEvent waitHandle,
|
||||
Func<bool> shuttingDown,
|
||||
Action<bool> setShutdown
|
||||
) {
|
||||
WaitHandle = waitHandle;
|
||||
ShuttingDown = shuttingDown;
|
||||
SetShutdown = setShutdown;
|
||||
}
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("shutdown")
|
||||
|| ctx.NameEquals("restart");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(ctx.User.UserId != 1) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
if(ShuttingDown())
|
||||
return;
|
||||
|
||||
SetShutdown(ctx.NameEquals("restart"));
|
||||
ctx.Chat.Update();
|
||||
WaitHandle?.Set();
|
||||
}
|
||||
}
|
||||
}
|
34
SharpChat.SockChat/Commands/SockChatClientCommandContext.cs
Normal file
34
SharpChat.SockChat/Commands/SockChatClientCommandContext.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class SockChatClientCommandContext {
|
||||
public string Name { get; }
|
||||
public string[] Args { get; }
|
||||
public SockChatContext Chat { get; }
|
||||
public UserInfo User { get; }
|
||||
public ConnectionInfo Connection { get; }
|
||||
public ChannelInfo Channel { get; }
|
||||
|
||||
public SockChatClientCommandContext(
|
||||
string text,
|
||||
SockChatContext chat,
|
||||
UserInfo user,
|
||||
ConnectionInfo connection,
|
||||
ChannelInfo channel
|
||||
) {
|
||||
Chat = chat;
|
||||
User = user;
|
||||
Connection = connection;
|
||||
Channel = channel;
|
||||
|
||||
string[] parts = text[1..].Split(' ');
|
||||
Name = parts.First().Replace(".", string.Empty);
|
||||
Args = parts.Skip(1).ToArray();
|
||||
}
|
||||
|
||||
public bool NameEquals(string name) {
|
||||
return Name.Equals(name, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
30
SharpChat.SockChat/Commands/UserAFKCommand.cs
Normal file
30
SharpChat.SockChat/Commands/UserAFKCommand.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using SharpChat.Events;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class UserAFKCommand : ISockChatClientCommand {
|
||||
private const string DEFAULT = "AFK";
|
||||
private const int MAX_LENGTH = 5;
|
||||
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("afk");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
string? statusText = ctx.Args.FirstOrDefault();
|
||||
if(string.IsNullOrWhiteSpace(statusText))
|
||||
statusText = DEFAULT;
|
||||
else {
|
||||
statusText = statusText.Trim();
|
||||
if(statusText.Length > MAX_LENGTH)
|
||||
statusText = statusText[..MAX_LENGTH].Trim();
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:status",
|
||||
ctx.User,
|
||||
new UserStatusUpdateEventData(UserStatus.Away, statusText)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
61
SharpChat.SockChat/Commands/UserNickCommand.cs
Normal file
61
SharpChat.SockChat/Commands/UserNickCommand.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class UserNickCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("nick");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
bool setOthersNick = ctx.User.Permissions.HasFlag(UserPermissions.SetOthersNickname);
|
||||
|
||||
if(!setOthersNick && !ctx.User.Permissions.HasFlag(UserPermissions.SetOwnNickname)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
UserInfo? targetUser = null;
|
||||
int offset = 0;
|
||||
|
||||
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
|
||||
targetUser = ctx.Chat.Users.Get(targetUserId);
|
||||
++offset;
|
||||
}
|
||||
|
||||
targetUser ??= ctx.User;
|
||||
|
||||
if(ctx.Args.Length < offset) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandFormatErrorS2CPacket());
|
||||
return;
|
||||
}
|
||||
|
||||
string nickStr = string.Join('_', ctx.Args.Skip(offset))
|
||||
.Replace("\n", string.Empty).Replace("\r", string.Empty)
|
||||
.Replace("\f", string.Empty).Replace("\t", string.Empty)
|
||||
.Replace(' ', '_').Trim();
|
||||
|
||||
if(nickStr == targetUser.UserName)
|
||||
nickStr = string.Empty;
|
||||
else if(nickStr.Length > 15)
|
||||
nickStr = nickStr[..15];
|
||||
|
||||
if(string.IsNullOrWhiteSpace(nickStr))
|
||||
nickStr = string.Empty;
|
||||
else if(ctx.Chat.Users.Get(name: nickStr, nameTarget: UsersContext.NameTarget.UserAndNickName) != null) {
|
||||
ctx.Chat.SendTo(ctx.User, new UserNameInUseErrorS2CPacket(nickStr));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:update",
|
||||
targetUser,
|
||||
new UserUpdateEventData(
|
||||
nickName: nickStr,
|
||||
notify: targetUser.UserId == ctx.User.UserId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
44
SharpChat.SockChat/Commands/WhoCommand.cs
Normal file
44
SharpChat.SockChat/Commands/WhoCommand.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class WhoCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("who");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
string? channelName = ctx.Args.FirstOrDefault();
|
||||
|
||||
if(string.IsNullOrEmpty(channelName)) {
|
||||
ctx.Chat.SendTo(ctx.User, new WhoServerResponseS2CPacket(
|
||||
ctx.Chat.Users.All.Select(u => SockChatUtility.GetUserName(u, ctx.Chat.UserStatuses.Get(u))).ToArray(),
|
||||
SockChatUtility.GetUserName(ctx.User)
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelInfo? channel = ctx.Chat.Channels.Get(channelName, SockChatUtility.SanitiseChannelName);
|
||||
|
||||
if(channel == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new ChannelNotFoundErrorS2CPacket(channelName));
|
||||
return;
|
||||
}
|
||||
|
||||
if(channel.Rank > ctx.User.Rank || (channel.HasPassword && !ctx.User.Permissions.HasFlag(UserPermissions.JoinAnyChannel))) {
|
||||
ctx.Chat.SendTo(ctx.User, new WhoChannelNotFoundErrorS2CPacket(channelName));
|
||||
return;
|
||||
}
|
||||
|
||||
UserInfo[] userInfos = ctx.Chat.Users.GetMany(
|
||||
ctx.Chat.ChannelsUsers.GetChannelUserIds(channel)
|
||||
);
|
||||
|
||||
ctx.Chat.SendTo(ctx.User, new WhoChannelResponseS2CPacket(
|
||||
channel.Name,
|
||||
userInfos.Select(user => SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user))).ToArray(),
|
||||
SockChatUtility.GetUserName(ctx.User, ctx.Chat.UserStatuses.Get(ctx.User))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
30
SharpChat.SockChat/Commands/WhoisCommand.cs
Normal file
30
SharpChat.SockChat/Commands/WhoisCommand.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.Commands {
|
||||
public class WhoisCommand : ISockChatClientCommand {
|
||||
public bool IsMatch(SockChatClientCommandContext ctx) {
|
||||
return ctx.NameEquals("ip")
|
||||
|| ctx.NameEquals("whois");
|
||||
}
|
||||
|
||||
public void Dispatch(SockChatClientCommandContext ctx) {
|
||||
if(!ctx.User.Permissions.HasFlag(UserPermissions.SeeIPAddress)) {
|
||||
ctx.Chat.SendTo(ctx.User, new CommandNotAllowedErrorS2CPacket(ctx.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
string ipUserStr = ctx.Args.FirstOrDefault() ?? string.Empty;
|
||||
UserInfo? ipUser;
|
||||
|
||||
(string name, UsersContext.NameTarget target) = SockChatUtility.ExplodeUserName(ipUserStr);
|
||||
if(string.IsNullOrWhiteSpace(name) || (ipUser = ctx.Chat.Users.Get(name: name, nameTarget: target)) == null) {
|
||||
ctx.Chat.SendTo(ctx.User, new UserNotFoundErrorS2CPacket(ipUserStr));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(string remoteAddr in ctx.Chat.Connections.GetUserRemoteAddresses(ipUser))
|
||||
ctx.Chat.SendTo(ctx.User, new WhoisResponseS2CPacket(ipUser.UserName, remoteAddr));
|
||||
}
|
||||
}
|
||||
}
|
237
SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs
Normal file
237
SharpChat.SockChat/PacketsC2S/AuthC2SPacketHandler.cs
Normal file
|
@ -0,0 +1,237 @@
|
|||
using SharpChat.Config;
|
||||
using SharpChat.Events;
|
||||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsC2S {
|
||||
public class AuthC2SPacketHandler : IC2SPacketHandler {
|
||||
public const string MOTD_FILE = @"welcome.txt";
|
||||
|
||||
private readonly DateTimeOffset Started;
|
||||
private readonly MisuzuClient Misuzu;
|
||||
private readonly ChannelInfo DefaultChannel;
|
||||
private readonly CachedValue<string> MOTDHeaderFormat;
|
||||
private readonly CachedValue<int> MaxMessageLength;
|
||||
private readonly CachedValue<int> MaxConnections;
|
||||
|
||||
public AuthC2SPacketHandler(
|
||||
DateTimeOffset started,
|
||||
MisuzuClient msz,
|
||||
ChannelInfo? defaultChannel,
|
||||
CachedValue<string> motdHeaderFormat,
|
||||
CachedValue<int> maxMsgLength,
|
||||
CachedValue<int> maxConns
|
||||
) {
|
||||
Started = started;
|
||||
Misuzu = msz;
|
||||
DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
|
||||
MOTDHeaderFormat = motdHeaderFormat;
|
||||
MaxMessageLength = maxMsgLength;
|
||||
MaxConnections = maxConns;
|
||||
}
|
||||
|
||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||
return ctx.CheckPacketId("1");
|
||||
}
|
||||
|
||||
public void Handle(C2SPacketHandlerContext ctx) {
|
||||
string[] args = ctx.SplitText(3);
|
||||
|
||||
string? authMethod = args.ElementAtOrDefault(1);
|
||||
if(string.IsNullOrWhiteSpace(authMethod)) {
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
string? authToken = args.ElementAtOrDefault(2);
|
||||
if(string.IsNullOrWhiteSpace(authToken)) {
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) {
|
||||
string[] tokenParts = authToken.Split(':', 2);
|
||||
authMethod = tokenParts[0];
|
||||
authToken = tokenParts[1];
|
||||
}
|
||||
|
||||
Task.Run(async () => {
|
||||
MisuzuAuthInfo? fai;
|
||||
string ipAddr = ctx.Connection.RemoteAddress;
|
||||
|
||||
try {
|
||||
fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr);
|
||||
} catch(Exception ex) {
|
||||
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> Failed to authenticate: {ex}");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
|
||||
ctx.Connection.Close(1000);
|
||||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
if(fai == null) {
|
||||
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Auth fail: <null>");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!fai.Success) {
|
||||
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Auth fail: {fai.Reason}");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
MisuzuBanInfo? fbi;
|
||||
try {
|
||||
fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr);
|
||||
} catch(Exception ex) {
|
||||
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> Failed auth ban check: {ex}");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.AuthInvalid));
|
||||
ctx.Connection.Close(1000);
|
||||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
if(fbi == null) {
|
||||
Logger.Debug($"<{ctx.Connection.RemoteEndPoint}> Ban check fail: <null>");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if(fbi.IsBanned && !fbi.HasExpired) {
|
||||
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> User is banned.");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(fbi.ExpiresAt));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if(ctx.Chat.Connections.GetCountForUser(fai.UserId) >= MaxConnections) {
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.MaxSessions));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Chat.ContextAccess.WaitAsync();
|
||||
try {
|
||||
UserInfo? user = ctx.Chat.Users.Get(fai.UserId);
|
||||
|
||||
if(user == null) {
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:add",
|
||||
fai.UserId,
|
||||
fai.UserName ?? string.Empty,
|
||||
fai.Colour,
|
||||
fai.Rank,
|
||||
string.Empty,
|
||||
fai.Permissions,
|
||||
new UserAddEventData(fai.IsSuper)
|
||||
);
|
||||
|
||||
user = ctx.Chat.Users.Get(fai.UserId);
|
||||
if(user == null) {
|
||||
Logger.Write($"<{ctx.Connection.RemoteEndPoint}> User didn't get added.");
|
||||
ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.FailReason.Null));
|
||||
ctx.Connection.Close(1000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
string? updName = !user.UserName.Equals(fai.UserName) ? fai.UserName : null;
|
||||
int? updColour = (updColour = fai.Colour.ToMisuzu()) != user.Colour.ToMisuzu() ? updColour : null;
|
||||
int? updRank = user.Rank != fai.Rank ? fai.Rank : null;
|
||||
UserPermissions? updPerms = user.Permissions != fai.Permissions ? fai.Permissions : null;
|
||||
bool? updSuper = user.IsSuper != fai.IsSuper ? fai.IsSuper : null;
|
||||
|
||||
if(updName != null || updColour != null || updRank != null || updPerms != null || updSuper != null)
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:update",
|
||||
user,
|
||||
new UserUpdateEventData(
|
||||
name: updName,
|
||||
colour: updColour,
|
||||
rank: updRank,
|
||||
perms: updPerms,
|
||||
isSuper: updSuper
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
ctx.Connection.BumpPing();
|
||||
ctx.Chat.Connections.SetUser(ctx.Connection, user);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(MOTDHeaderFormat.Value))
|
||||
ctx.Connection.Send(new MOTDS2CPacket(Started, string.Format(MOTDHeaderFormat.Value, user.UserName)));
|
||||
|
||||
if(File.Exists(MOTD_FILE)) {
|
||||
IEnumerable<string> lines = File.ReadAllLines(MOTD_FILE).Where(x => !string.IsNullOrWhiteSpace(x));
|
||||
string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(line))
|
||||
ctx.Connection.Send(new MOTDS2CPacket(File.GetLastWriteTimeUtc(MOTD_FILE), line));
|
||||
}
|
||||
|
||||
ctx.Connection.Send(new AuthSuccessS2CPacket(
|
||||
user.UserId,
|
||||
SockChatUtility.GetUserName(user, ctx.Chat.UserStatuses.Get(user)),
|
||||
user.Colour,
|
||||
user.Rank,
|
||||
user.Permissions,
|
||||
DefaultChannel.Name,
|
||||
MaxMessageLength
|
||||
));
|
||||
|
||||
UserInfo[] chanUsers = ctx.Chat.Users.GetMany(
|
||||
ctx.Chat.ChannelsUsers.GetChannelUserIds(DefaultChannel)
|
||||
);
|
||||
List<UsersPopulateS2CPacket.ListEntry> chanUserEntries = new();
|
||||
foreach(UserInfo chanUserInfo in chanUsers)
|
||||
if(chanUserInfo.UserId != user.UserId)
|
||||
chanUserEntries.Add(new(
|
||||
chanUserInfo.UserId,
|
||||
SockChatUtility.GetUserName(chanUserInfo, ctx.Chat.UserStatuses.Get(chanUserInfo)),
|
||||
chanUserInfo.Colour,
|
||||
chanUserInfo.Rank,
|
||||
chanUserInfo.Permissions,
|
||||
true
|
||||
));
|
||||
ctx.Connection.Send(new UsersPopulateS2CPacket(chanUserEntries.ToArray()));
|
||||
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:connect",
|
||||
DefaultChannel,
|
||||
user,
|
||||
new UserConnectEventData(!ctx.Chat.ChannelsUsers.Has(DefaultChannel, user))
|
||||
);
|
||||
ctx.Chat.HandleChannelEventLog(DefaultChannel.Name, p => ctx.Connection.Send(p));
|
||||
|
||||
ChannelInfo[] chans = ctx.Chat.Channels.GetMany(isPublic: true, minRank: user.Rank);
|
||||
List<ChannelsPopulateS2CPacket.ListEntry> chanEntries = new();
|
||||
foreach(ChannelInfo chanInfo in chans)
|
||||
chanEntries.Add(new(
|
||||
chanInfo.Name,
|
||||
chanInfo.HasPassword,
|
||||
chanInfo.IsTemporary
|
||||
));
|
||||
ctx.Connection.Send(new ChannelsPopulateS2CPacket(chanEntries.ToArray()));
|
||||
} finally {
|
||||
ctx.Chat.ContextAccess.Release();
|
||||
}
|
||||
}).Wait();
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.SockChat/PacketsC2S/C2SPacketHandlerContext.cs
Normal file
21
SharpChat.SockChat/PacketsC2S/C2SPacketHandlerContext.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace SharpChat.SockChat.PacketsC2S {
|
||||
public class C2SPacketHandlerContext {
|
||||
public string Text { get; }
|
||||
public SockChatContext Chat { get; }
|
||||
public SockChatConnectionInfo Connection { get; }
|
||||
|
||||
public C2SPacketHandlerContext(string text, SockChatContext chat, SockChatConnectionInfo connection) {
|
||||
Text = text;
|
||||
Chat = chat;
|
||||
Connection = connection;
|
||||
}
|
||||
|
||||
public bool CheckPacketId(string packetId) {
|
||||
return Text == packetId || Text.StartsWith(packetId + '\t');
|
||||
}
|
||||
|
||||
public string[] SplitText(int expect) {
|
||||
return Text.Split('\t', expect + 1);
|
||||
}
|
||||
}
|
||||
}
|
6
SharpChat.SockChat/PacketsC2S/IC2SPacketHandler.cs
Normal file
6
SharpChat.SockChat/PacketsC2S/IC2SPacketHandler.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace SharpChat.SockChat.PacketsC2S {
|
||||
public interface IC2SPacketHandler {
|
||||
bool IsMatch(C2SPacketHandlerContext ctx);
|
||||
void Handle(C2SPacketHandlerContext ctx);
|
||||
}
|
||||
}
|
60
SharpChat.SockChat/PacketsC2S/PingC2SPacketHandler.cs
Normal file
60
SharpChat.SockChat/PacketsC2S/PingC2SPacketHandler.cs
Normal file
|
@ -0,0 +1,60 @@
|
|||
using SharpChat.Misuzu;
|
||||
using SharpChat.SockChat.PacketsS2C;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsC2S {
|
||||
public class PingC2SPacketHandler : IC2SPacketHandler {
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
|
||||
private DateTimeOffset LastBump = DateTimeOffset.MinValue;
|
||||
|
||||
public PingC2SPacketHandler(MisuzuClient msz) {
|
||||
Misuzu = msz;
|
||||
}
|
||||
|
||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||
return ctx.CheckPacketId("0");
|
||||
}
|
||||
|
||||
public void Handle(C2SPacketHandlerContext ctx) {
|
||||
string[] parts = ctx.SplitText(2);
|
||||
|
||||
if(!int.TryParse(parts.FirstOrDefault(), out int pTime))
|
||||
return;
|
||||
|
||||
ctx.Connection.BumpPing();
|
||||
ctx.Connection.Send(new PongS2CPacket());
|
||||
|
||||
ctx.Chat.ContextAccess.Wait();
|
||||
try {
|
||||
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
|
||||
List<(string, string)> bumpList = new();
|
||||
|
||||
foreach(UserInfo userInfo in ctx.Chat.Users.All) {
|
||||
if(ctx.Chat.UserStatuses.GetStatus(userInfo) != UserStatus.Online)
|
||||
continue;
|
||||
|
||||
string[] remoteAddrs = ctx.Chat.Connections.GetUserRemoteAddresses(userInfo);
|
||||
if(remoteAddrs.Length < 1)
|
||||
continue;
|
||||
|
||||
bumpList.Add((userInfo.UserId.ToString(), remoteAddrs[0]));
|
||||
}
|
||||
|
||||
if(bumpList.Count > 0)
|
||||
Task.Run(async () => {
|
||||
await Misuzu.BumpUsersOnlineAsync(bumpList);
|
||||
}).Wait();
|
||||
|
||||
LastBump = DateTimeOffset.UtcNow;
|
||||
}
|
||||
} finally {
|
||||
ctx.Chat.ContextAccess.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
92
SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs
Normal file
92
SharpChat.SockChat/PacketsC2S/SendMessageC2SPacketHandler.cs
Normal file
|
@ -0,0 +1,92 @@
|
|||
using SharpChat.Config;
|
||||
using SharpChat.Events;
|
||||
using SharpChat.SockChat.Commands;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsC2S {
|
||||
public class SendMessageC2SPacketHandler : IC2SPacketHandler {
|
||||
private readonly CachedValue<int> MaxMessageLength;
|
||||
|
||||
private List<ISockChatClientCommand> Commands { get; } = new();
|
||||
|
||||
public SendMessageC2SPacketHandler(CachedValue<int> maxMsgLength) {
|
||||
MaxMessageLength = maxMsgLength;
|
||||
}
|
||||
|
||||
public void AddCommand(ISockChatClientCommand command) {
|
||||
Commands.Add(command);
|
||||
}
|
||||
|
||||
public void AddCommands(IEnumerable<ISockChatClientCommand> commands) {
|
||||
Commands.AddRange(commands);
|
||||
}
|
||||
|
||||
public bool IsMatch(C2SPacketHandlerContext ctx) {
|
||||
return ctx.CheckPacketId("2");
|
||||
}
|
||||
|
||||
public void Handle(C2SPacketHandlerContext ctx) {
|
||||
string[] args = ctx.SplitText(3);
|
||||
|
||||
UserInfo? user = ctx.Chat.Users.Get(ctx.Connection.UserId);
|
||||
|
||||
// No longer concats everything after index 1 with \t, no previous implementation did that either
|
||||
string? messageText = args.ElementAtOrDefault(2);
|
||||
|
||||
if(user == null || !user.Permissions.HasFlag(UserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText))
|
||||
return;
|
||||
|
||||
// Extra validation step, not necessary at all but enforces proper formatting in SCv1.
|
||||
if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId)
|
||||
return;
|
||||
|
||||
ctx.Chat.ContextAccess.Wait();
|
||||
try {
|
||||
ChannelInfo? channelInfo = ctx.Chat.Channels.Get(
|
||||
ctx.Chat.ChannelsUsers.GetUserLastChannel(user)
|
||||
);
|
||||
if(channelInfo == null)
|
||||
return;
|
||||
|
||||
if(ctx.Chat.UserStatuses.GetStatus(user) != UserStatus.Online)
|
||||
ctx.Chat.Events.Dispatch(
|
||||
"user:status",
|
||||
user,
|
||||
new UserStatusUpdateEventData(UserStatus.Online)
|
||||
);
|
||||
|
||||
int maxMsgLength = MaxMessageLength;
|
||||
if(messageText.Length > maxMsgLength)
|
||||
messageText = messageText[..maxMsgLength];
|
||||
|
||||
messageText = messageText.Trim();
|
||||
|
||||
#if DEBUG
|
||||
Logger.Write($"<{user.UserId} {user.UserName}> {messageText}");
|
||||
#endif
|
||||
|
||||
if(messageText.StartsWith("/")) {
|
||||
SockChatClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channelInfo);
|
||||
|
||||
ISockChatClientCommand? command = null;
|
||||
|
||||
foreach(ISockChatClientCommand cmd in Commands)
|
||||
if(cmd.IsMatch(context)) {
|
||||
command = cmd;
|
||||
break;
|
||||
}
|
||||
|
||||
if(command != null) {
|
||||
command.Dispatch(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Chat.Events.Dispatch("msg:add", channelInfo, user, new MessageAddEventData(messageText));
|
||||
} finally {
|
||||
ctx.Chat.ContextAccess.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
SharpChat.SockChat/PacketsS2C/AuthFailS2CPacket.cs
Normal file
38
SharpChat.SockChat/PacketsS2C/AuthFailS2CPacket.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class AuthFailS2CPacket : ISockChatS2CPacket {
|
||||
public enum FailReason {
|
||||
AuthInvalid,
|
||||
MaxSessions,
|
||||
Banned,
|
||||
Null,
|
||||
}
|
||||
|
||||
private readonly FailReason Reason;
|
||||
private readonly long Expires;
|
||||
|
||||
public AuthFailS2CPacket(FailReason reason) {
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public AuthFailS2CPacket(DateTimeOffset expires) {
|
||||
Reason = FailReason.Banned;
|
||||
Expires = expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
string packet = string.Format("1\tn\t{0}fail", Reason switch {
|
||||
FailReason.AuthInvalid => "auth",
|
||||
FailReason.MaxSessions => "sock",
|
||||
FailReason.Banned => "join",
|
||||
_ => "user",
|
||||
});
|
||||
|
||||
if(Reason == FailReason.Banned)
|
||||
packet += string.Format("\t{0}", Expires);
|
||||
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
}
|
47
SharpChat.SockChat/PacketsS2C/AuthSuccessS2CPacket.cs
Normal file
47
SharpChat.SockChat/PacketsS2C/AuthSuccessS2CPacket.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class AuthSuccessS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long UserId;
|
||||
private readonly string UserName;
|
||||
private readonly Colour UserColour;
|
||||
private readonly int UserRank;
|
||||
private readonly UserPermissions UserPerms;
|
||||
private readonly string ChannelName;
|
||||
private readonly int MaxMessageLength;
|
||||
|
||||
public AuthSuccessS2CPacket(
|
||||
long userId,
|
||||
string userName,
|
||||
Colour userColour,
|
||||
int userRank,
|
||||
UserPermissions userPerms,
|
||||
string channelName,
|
||||
int maxMsgLength
|
||||
) {
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
UserColour = userColour;
|
||||
UserRank = userRank;
|
||||
UserPerms = userPerms;
|
||||
ChannelName = channelName;
|
||||
MaxMessageLength = maxMsgLength;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"1\ty\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}\t{8}\t{9}",
|
||||
UserId,
|
||||
UserName,
|
||||
UserColour,
|
||||
UserRank,
|
||||
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
|
||||
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
|
||||
) : 0,
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MaxMessageLength
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
32
SharpChat.SockChat/PacketsS2C/BanListResponseS2CPacket.cs
Normal file
32
SharpChat.SockChat/PacketsS2C/BanListResponseS2CPacket.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class BanListResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string[] Bans;
|
||||
|
||||
public BanListResponseS2CPacket(string[] bans) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
Bans = bans;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
StringBuilder sb = new();
|
||||
|
||||
sb.AppendFormat("2\t{0}\t-1\t0\fbanlist\f", TimeStamp.ToUnixTimeSeconds());
|
||||
|
||||
foreach(string ban in Bans)
|
||||
sb.AppendFormat(@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('/unban '+ this.innerHTML);"">{0}</a>, ", ban);
|
||||
|
||||
if(Bans.Length > 0)
|
||||
sb.Length -= 2;
|
||||
|
||||
sb.AppendFormat("\t{0}\t10010", MessageId);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelCreateResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelCreateResponseS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fcrchan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
26
SharpChat.SockChat/PacketsS2C/ChannelCreateS2CPacket.cs
Normal file
26
SharpChat.SockChat/PacketsS2C/ChannelCreateS2CPacket.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelCreateS2CPacket : ISockChatS2CPacket {
|
||||
private readonly string ChannelName;
|
||||
private readonly bool ChannelHasPassword;
|
||||
private readonly bool ChannelIsTemporary;
|
||||
|
||||
public ChannelCreateS2CPacket(
|
||||
string channelName,
|
||||
bool channelHasPassword,
|
||||
bool channelIsTemporary
|
||||
) {
|
||||
ChannelName = channelName;
|
||||
ChannelHasPassword = channelHasPassword;
|
||||
ChannelIsTemporary = channelIsTemporary;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"4\t0\t{0}\t{1}\t{2}",
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
ChannelHasPassword ? 1 : 0,
|
||||
ChannelIsTemporary ? 1 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelDeleteNotAllowedErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelDeleteNotAllowedErrorS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fndchan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelDeleteResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelDeleteResponseS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fdelchan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
16
SharpChat.SockChat/PacketsS2C/ChannelDeleteS2CPacket.cs
Normal file
16
SharpChat.SockChat/PacketsS2C/ChannelDeleteS2CPacket.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelDeleteS2CPacket : ISockChatS2CPacket {
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelDeleteS2CPacket(string channelName) {
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"4\t2\t{0}",
|
||||
SockChatUtility.SanitiseChannelName(ChannelName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelNameFormatErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public ChannelNameFormatErrorS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\finchan\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelNameInUseErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelNameInUseErrorS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fnischan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelNotFoundErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelNotFoundErrorS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fnochan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelPasswordChangedResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public ChannelPasswordChangedResponseS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fcpwdchan\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelPasswordWrongErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelPasswordWrongErrorS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fipwchan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelRankChangedResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public ChannelRankChangedResponseS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fcprivchan\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelRankTooHighErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public ChannelRankTooHighErrorS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\frankerr\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelRankTooLowErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string ChannelName;
|
||||
|
||||
public ChannelRankTooLowErrorS2CPacket(string channelName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fipchan\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseChannelName(ChannelName),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
30
SharpChat.SockChat/PacketsS2C/ChannelUpdateS2CPacket.cs
Normal file
30
SharpChat.SockChat/PacketsS2C/ChannelUpdateS2CPacket.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelUpdateS2CPacket : ISockChatS2CPacket {
|
||||
private readonly string ChannelNamePrevious;
|
||||
private readonly string ChannelNameNew;
|
||||
private readonly bool ChannelHasPassword;
|
||||
private readonly bool ChannelIsTemporary;
|
||||
|
||||
public ChannelUpdateS2CPacket(
|
||||
string channelNamePrevious,
|
||||
string channelNameNew,
|
||||
bool channelHasPassword,
|
||||
bool channelIsTemporary
|
||||
) {
|
||||
ChannelNamePrevious = channelNamePrevious;
|
||||
ChannelNameNew = channelNameNew;
|
||||
ChannelHasPassword = channelHasPassword;
|
||||
ChannelIsTemporary = channelIsTemporary;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"4\t1\t{0}\t{1}\t{2}\t{3}",
|
||||
SockChatUtility.SanitiseChannelName(ChannelNamePrevious),
|
||||
SockChatUtility.SanitiseChannelName(ChannelNameNew),
|
||||
ChannelHasPassword ? 1 : 0,
|
||||
ChannelIsTemporary ? 1 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
29
SharpChat.SockChat/PacketsS2C/ChannelsPopulateS2CPacket.cs
Normal file
29
SharpChat.SockChat/PacketsS2C/ChannelsPopulateS2CPacket.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Text;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ChannelsPopulateS2CPacket : ISockChatS2CPacket {
|
||||
public record ListEntry(string Name, bool HasPassword, bool IsTemporary);
|
||||
|
||||
private readonly ListEntry[] Entries;
|
||||
|
||||
public ChannelsPopulateS2CPacket(ListEntry[] entries) {
|
||||
Entries = entries;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
StringBuilder sb = new();
|
||||
|
||||
sb.AppendFormat("7\t2\t{0}", Entries.Length);
|
||||
|
||||
foreach(ListEntry entry in Entries)
|
||||
sb.AppendFormat(
|
||||
"\t{0}\t{1}\t{2}",
|
||||
SockChatUtility.SanitiseChannelName(entry.Name),
|
||||
entry.HasPassword ? 1 : 0,
|
||||
entry.IsTemporary ? 1 : 0
|
||||
);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ClearMessagesAndUsersS2CPacket : ISockChatS2CPacket {
|
||||
public string Pack() {
|
||||
return "8\t3";
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.SockChat/PacketsS2C/CommandFormatErrorS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/CommandFormatErrorS2CPacket.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class CommandFormatErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public CommandFormatErrorS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fcmdna\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class CommandNotAllowedErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string CommandName;
|
||||
|
||||
public CommandNotAllowedErrorS2CPacket(string commandName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
CommandName = commandName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fcmdna\f/{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
CommandName,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.SockChat/PacketsS2C/FloodWarningS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/FloodWarningS2CPacket.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class FloodWarningS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public FloodWarningS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fflwarn\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
19
SharpChat.SockChat/PacketsS2C/ForceDisconnectS2CPacket.cs
Normal file
19
SharpChat.SockChat/PacketsS2C/ForceDisconnectS2CPacket.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class ForceDisconnectS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long Expires;
|
||||
|
||||
public ForceDisconnectS2CPacket(DateTimeOffset expires) {
|
||||
Expires = expires <= DateTimeOffset.UtcNow
|
||||
? 0 : (expires.Year >= 2100 ? -1 : expires.ToUnixTimeSeconds());
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
if(Expires != 0)
|
||||
return string.Format("9\t1\t{0}", Expires);
|
||||
|
||||
return "9\t0";
|
||||
}
|
||||
}
|
||||
}
|
5
SharpChat.SockChat/PacketsS2C/ISockChatS2CPacket.cs
Normal file
5
SharpChat.SockChat/PacketsS2C/ISockChatS2CPacket.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public interface ISockChatS2CPacket {
|
||||
string Pack();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class KickBanNoRecordErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string TargetName;
|
||||
|
||||
public KickBanNoRecordErrorS2CPacket(string targetName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
TargetName = targetName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fnotban\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
TargetName,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class KickBanNotAllowedErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string UserName;
|
||||
|
||||
public KickBanNotAllowedErrorS2CPacket(string userName) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
UserName = userName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fkickna\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
UserName,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
21
SharpChat.SockChat/PacketsS2C/MOTDS2CPacket.cs
Normal file
21
SharpChat.SockChat/PacketsS2C/MOTDS2CPacket.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MOTDS2CPacket : ISockChatS2CPacket {
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string Body;
|
||||
|
||||
public MOTDS2CPacket(DateTimeOffset timeStamp, string body) {
|
||||
TimeStamp = timeStamp;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fsay\f{1}\twelcome\t0\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseMessageBody(Body)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
83
SharpChat.SockChat/PacketsS2C/MessageAddLogS2CPacket.cs
Normal file
83
SharpChat.SockChat/PacketsS2C/MessageAddLogS2CPacket.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MessageAddLogS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly long UserId;
|
||||
private readonly string UserName;
|
||||
private readonly Colour UserColour;
|
||||
private readonly int UserRank;
|
||||
private readonly UserPermissions UserPerms;
|
||||
private readonly string Body;
|
||||
private readonly bool IsAction;
|
||||
private readonly bool IsPrivate;
|
||||
private readonly bool IsBroadcast; // this should be MessageBroadcastLogPacket
|
||||
private readonly bool Notify;
|
||||
|
||||
public MessageAddLogS2CPacket(
|
||||
long messageId,
|
||||
DateTimeOffset timeStamp,
|
||||
long userId,
|
||||
string userName,
|
||||
Colour userColour,
|
||||
int userRank,
|
||||
UserPermissions userPerms,
|
||||
string body,
|
||||
bool isAction,
|
||||
bool isPrivate,
|
||||
bool isBroadcast,
|
||||
bool notify
|
||||
) {
|
||||
MessageId = messageId;
|
||||
TimeStamp = timeStamp;
|
||||
UserId = userId < 0 ? -1 : userId;
|
||||
UserName = userName;
|
||||
UserColour = userColour;
|
||||
UserRank = userRank;
|
||||
UserPerms = userPerms;
|
||||
Body = body;
|
||||
IsAction = isAction;
|
||||
IsPrivate = isPrivate;
|
||||
IsBroadcast = isBroadcast;
|
||||
Notify = notify;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
string body = SockChatUtility.SanitiseMessageBody(Body);
|
||||
if(IsAction)
|
||||
body = string.Format("<i>{0}</i>", body);
|
||||
|
||||
if(IsBroadcast)
|
||||
body = "0\fsay\f" + body;
|
||||
|
||||
string userPerms = UserId < 0 ? string.Empty : string.Format(
|
||||
"{0} {1} {2} {3} {4}",
|
||||
UserRank,
|
||||
UserPerms.HasFlag(UserPermissions.KickUser) == true ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.ViewLogs) == true ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.SetOwnNickname) == true ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.CreateChannel) == true ? (
|
||||
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) == true ? 2 : 1
|
||||
) : 0
|
||||
);
|
||||
|
||||
return string.Format(
|
||||
"7\t1\t{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}{9}{10}{11}{12}",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
UserId,
|
||||
UserName,
|
||||
UserColour,
|
||||
userPerms,
|
||||
body,
|
||||
MessageId,
|
||||
Notify ? 1 : 0,
|
||||
1,
|
||||
IsAction ? 1 : 0,
|
||||
0,
|
||||
IsAction ? 0 : 1,
|
||||
IsPrivate ? 1 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
47
SharpChat.SockChat/PacketsS2C/MessageAddS2CPacket.cs
Normal file
47
SharpChat.SockChat/PacketsS2C/MessageAddS2CPacket.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MessageAddS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly long UserId;
|
||||
private readonly string Body;
|
||||
private readonly bool IsAction;
|
||||
private readonly bool IsPrivate;
|
||||
|
||||
public MessageAddS2CPacket(
|
||||
long messageId,
|
||||
DateTimeOffset timeStamp,
|
||||
long userId,
|
||||
string body,
|
||||
bool isAction,
|
||||
bool isPrivate
|
||||
) {
|
||||
MessageId = messageId;
|
||||
TimeStamp = timeStamp;
|
||||
UserId = userId < 0 ? -1 : userId;
|
||||
Body = body;
|
||||
IsAction = isAction;
|
||||
IsPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
string body = SockChatUtility.SanitiseMessageBody(Body);
|
||||
if(IsAction)
|
||||
body = string.Format("<i>{0}</i>", body);
|
||||
|
||||
return string.Format(
|
||||
"2\t{0}\t{1}\t{2}\t{3}\t{4}{5}{6}{7}{8}",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
UserId,
|
||||
body,
|
||||
MessageId,
|
||||
1,
|
||||
IsAction ? 1 : 0,
|
||||
0,
|
||||
IsAction ? 0 : 1,
|
||||
IsPrivate ? 1 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
24
SharpChat.SockChat/PacketsS2C/MessageBroadcastS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/MessageBroadcastS2CPacket.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MessageBroadcastS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string Body;
|
||||
|
||||
public MessageBroadcastS2CPacket(long messageId, DateTimeOffset timeStamp, string body) {
|
||||
MessageId = messageId;
|
||||
TimeStamp = timeStamp;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\fsay\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
SockChatUtility.SanitiseMessageBody(Body),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MessageDeleteNotAllowedErrorS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
|
||||
public MessageDeleteNotAllowedErrorS2CPacket() {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t1\fdelerr\t{1}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
13
SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs
Normal file
13
SharpChat.SockChat/PacketsS2C/MessageDeleteS2CPacket.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class MessageDeleteS2CPacket : ISockChatS2CPacket {
|
||||
private readonly string DeletedMessageId;
|
||||
|
||||
public MessageDeleteS2CPacket(string deletedMessageId) {
|
||||
DeletedMessageId = deletedMessageId;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format("6\t{0}", DeletedMessageId);
|
||||
}
|
||||
}
|
||||
}
|
24
SharpChat.SockChat/PacketsS2C/PardonResponseS2CPacket.cs
Normal file
24
SharpChat.SockChat/PacketsS2C/PardonResponseS2CPacket.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class PardonResponseS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string Subject;
|
||||
|
||||
public PardonResponseS2CPacket(string subject) {
|
||||
MessageId = SharpId.Next();
|
||||
TimeStamp = DateTimeOffset.UtcNow;
|
||||
Subject = subject;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"2\t{0}\t-1\t0\funban\f{1}\t{2}\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
Subject,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
7
SharpChat.SockChat/PacketsS2C/PongS2CPacket.cs
Normal file
7
SharpChat.SockChat/PacketsS2C/PongS2CPacket.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class PongS2CPacket : ISockChatS2CPacket {
|
||||
public string Pack() {
|
||||
return "0\tpong";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class UserChannelForceJoinS2CPacket : ISockChatS2CPacket {
|
||||
private readonly string ChannelName;
|
||||
|
||||
public UserChannelForceJoinS2CPacket(string channelName) {
|
||||
ChannelName = channelName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"5\t2\t{0}",
|
||||
SockChatUtility.SanitiseChannelName(ChannelName)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
28
SharpChat.SockChat/PacketsS2C/UserChannelJoinLogS2CPacket.cs
Normal file
28
SharpChat.SockChat/PacketsS2C/UserChannelJoinLogS2CPacket.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class UserChannelJoinLogS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string UserName;
|
||||
|
||||
public UserChannelJoinLogS2CPacket(
|
||||
long messageId,
|
||||
DateTimeOffset timeStamp,
|
||||
string userName
|
||||
) {
|
||||
MessageId = messageId;
|
||||
TimeStamp = timeStamp;
|
||||
UserName = userName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\fjchan\f{1}\t{2}\t0\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
UserName,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
42
SharpChat.SockChat/PacketsS2C/UserChannelJoinS2CPacket.cs
Normal file
42
SharpChat.SockChat/PacketsS2C/UserChannelJoinS2CPacket.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class UserChannelJoinS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly long UserId;
|
||||
private readonly string UserName;
|
||||
private readonly Colour UserColour;
|
||||
private readonly int UserRank;
|
||||
private readonly UserPermissions UserPerms;
|
||||
|
||||
public UserChannelJoinS2CPacket(
|
||||
long userId,
|
||||
string userName,
|
||||
Colour userColour,
|
||||
int userRank,
|
||||
UserPermissions userPerms
|
||||
) {
|
||||
MessageId = SharpId.Next();
|
||||
UserId = userId;
|
||||
UserName = userName;
|
||||
UserColour = userColour;
|
||||
UserRank = userRank;
|
||||
UserPerms = userPerms;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"5\t0\t{0}\t{1}\t{2}\t{3} {4} {5} {6} {7}\t{8}",
|
||||
UserId,
|
||||
UserName,
|
||||
UserColour,
|
||||
UserRank,
|
||||
UserPerms.HasFlag(UserPermissions.KickUser) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.ViewLogs) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.SetOwnNickname) ? 1 : 0,
|
||||
UserPerms.HasFlag(UserPermissions.CreateChannel) ? (
|
||||
UserPerms.HasFlag(UserPermissions.SetChannelPermanent) ? 2 : 1
|
||||
) : 0,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class UserChannelLeaveLogS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly DateTimeOffset TimeStamp;
|
||||
private readonly string UserName;
|
||||
|
||||
public UserChannelLeaveLogS2CPacket(
|
||||
long messageId,
|
||||
DateTimeOffset timeStamp,
|
||||
string userName
|
||||
) {
|
||||
MessageId = messageId;
|
||||
TimeStamp = timeStamp;
|
||||
UserName = userName;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"7\t1\t{0}\t-1\tChatBot\tinherit\t\t0\flchan\f{1}\t{2}\t0\t10010",
|
||||
TimeStamp.ToUnixTimeSeconds(),
|
||||
UserName,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
19
SharpChat.SockChat/PacketsS2C/UserChannelLeaveS2CPacket.cs
Normal file
19
SharpChat.SockChat/PacketsS2C/UserChannelLeaveS2CPacket.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace SharpChat.SockChat.PacketsS2C {
|
||||
public class UserChannelLeaveS2CPacket : ISockChatS2CPacket {
|
||||
private readonly long MessageId;
|
||||
private readonly long UserId;
|
||||
|
||||
public UserChannelLeaveS2CPacket(long userId) {
|
||||
MessageId = SharpId.Next();
|
||||
UserId = userId;
|
||||
}
|
||||
|
||||
public string Pack() {
|
||||
return string.Format(
|
||||
"5\t1\t{0}\t{1}",
|
||||
UserId,
|
||||
MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue