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