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 BaseURL { get; } private CachedValue SecretKey { get; } public MisuzuClient(HttpClient httpClient, IConfig config) { if(config == null) throw new ArgumentNullException(nameof(config)); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(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 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 { { "method", method }, { "token", token }, { "ipaddr", ipAddr }, }), Headers = { { "X-SharpChat-Signature", CreateStringSignature(sig) }, }, }; using HttpResponseMessage res = await HttpClient.SendAsync(req); return JsonSerializer.Deserialize( await res.Content.ReadAsByteArrayAsync() ); } public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) { if(list == null) throw new ArgumentNullException(nameof(list)); if(!list.Any()) return; string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); StringBuilder sb = new(); sb.AppendFormat("bump#{0}", now); Dictionary 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 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( await res.Content.ReadAsByteArrayAsync() ); } public async Task 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( await res.Content.ReadAsByteArrayAsync() ); } public enum BanRevokeKind { UserId, RemoteAddress, } public async Task RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) { if(banInfo == null) throw new ArgumentNullException(nameof(banInfo)); 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 ArgumentNullException(nameof(targetAddr)); if(string.IsNullOrWhiteSpace(modAddr)) throw new ArgumentNullException(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 { { "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(); } } }