using Hamakaze; using SharpChat.Users; using SharpChat.Users.Remote; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; namespace SharpChat.DataProvider.Misuzu.Users { public class MisuzuUserClient : IRemoteUserClient { private MisuzuDataProvider DataProvider { get; } private HttpClient HttpClient { get; } private const string AUTH_URL = @"/verify"; private const string BUMP_URL = @"/bump"; private const string RESOLVE_URL = @"/resolve?m={0}&p={1}"; private Dictionary UserIdCache { get; } = new(); private readonly object UserIdCacheSync = new(); private static readonly TimeSpan CacheMaxAge = TimeSpan.FromMinutes(1); public MisuzuUserClient(MisuzuDataProvider dataProvider, HttpClient httpClient) { DataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public void AuthenticateUser(UserAuthRequest request, Action onSuccess, Action onFailure) { if(request == null) throw new ArgumentNullException(nameof(request)); if(onSuccess == null) throw new ArgumentNullException(nameof(onSuccess)); if(onFailure == null) throw new ArgumentNullException(nameof(onFailure)); #if DEBUG if(request.UserId >= 10000) { onSuccess.Invoke(new MisuzuUserAuthResponse { Success = true, UserId = request.UserId, UserName = @"Misaka-" + (request.UserId - 10000), ColourRaw = (RNG.Next(0, 255) << 16) | (RNG.Next(0, 255) << 8) | RNG.Next(0, 255), Rank = 0, SilencedUntil = DateTimeOffset.MinValue, Permissions = UserPermissions.SendMessage | UserPermissions.EditOwnMessage | UserPermissions.DeleteOwnMessage, }); return; } #endif MisuzuUserAuthRequest mar = new(request); HttpRequestMessage req = new(HttpRequestMessage.POST, DataProvider.GetURL(AUTH_URL)); req.SetHeader(@"X-SharpChat-Signature", DataProvider.GetSignedHash(mar)); req.SetBody(JsonSerializer.SerializeToUtf8Bytes(mar)); HttpClient.SendRequest( req, onComplete: (t, r) => { using MemoryStream ms = new(); r.Body.CopyTo(ms); MisuzuUserAuthResponse res = JsonSerializer.Deserialize(ms.ToArray()); if(res.Success) onSuccess.Invoke(res); else onFailure.Invoke(new UserAuthFailedException(res.Reason)); }, onError: (t, e) => { Logger.Write(@"An error occurred during authentication."); Logger.Debug(e); onFailure.Invoke(e); } ); } public void BumpUsers(IEnumerable users, Action onSuccess, Action onFailure) { if(users == null) throw new ArgumentNullException(nameof(users)); if(onSuccess == null) throw new ArgumentNullException(nameof(onSuccess)); if(onFailure == null) throw new ArgumentNullException(nameof(onFailure)); if(!users.Any()) return; byte[] data = JsonSerializer.SerializeToUtf8Bytes(users.Select(ubi => new MisuzuUserBumpInfo(ubi))); HttpRequestMessage request = new(HttpRequestMessage.POST, DataProvider.GetURL(BUMP_URL)); request.SetHeader(@"X-SharpChat-Signature", DataProvider.GetSignedHash(data)); request.SetBody(data); HttpClient.SendRequest( request, disposeRequest: false, onComplete: (t, r) => { request.Dispose(); onSuccess?.Invoke(); }, onError: (t, e) => { Logger.Write(@"User bump request failed. Retrying once..."); Logger.Debug(e); HttpClient.SendRequest( request, onComplete: (t, r) => { Logger.Write(@"Second user bump attempt succeeded!"); onSuccess?.Invoke(); }, onError: (t, e) => { Logger.Write(@"User bump request failed again."); Logger.Debug(e); onFailure?.Invoke(e); } ); } ); } private const string RESOLVE_ID = @"id"; private const string RESOLVE_NAME = @"name"; public void ResolveUser(long userId, Action onSuccess, Action onFailure) { ResolveUser(RESOLVE_ID, userId, onSuccess, onFailure); } public void ResolveUser(string userName, Action onSuccess, Action onFailure) { ResolveUser(RESOLVE_NAME, userName, onSuccess, onFailure); } public void ResolveUser(IUser localUser, Action onSuccess, Action onFailure) { if(localUser == null) onSuccess(null); else ResolveUser(RESOLVE_ID, localUser.UserId, onSuccess, onFailure); } private void ResolveUser(string method, object param, Action onSuccess, Action onFailure) { if(method == null) throw new ArgumentNullException(nameof(method)); if(param == null) throw new ArgumentNullException(nameof(param)); if(onSuccess == null) throw new ArgumentNullException(nameof(onSuccess)); if(onFailure == null) throw new ArgumentNullException(nameof(onFailure)); if(method == RESOLVE_ID) { MisuzuUser mui = null; lock(UserIdCacheSync) if(UserIdCache.TryGetValue((long)param, out (DateTimeOffset age, MisuzuUser mui) cache) && (DateTimeOffset.Now - cache.age) < CacheMaxAge) mui = cache.mui; if(mui != null) { onSuccess(mui); return; } } HttpRequestMessage req = new(HttpRequestMessage.GET, DataProvider.GetURL( string.Format(RESOLVE_URL, method, param) )); req.SetHeader(@"X-SharpChat-Signature", DataProvider.GetSignedHash(string.Format( @"resolve#{0}#{1}", method, param ))); HttpClient.SendRequest( req, (t, r) => { try { MisuzuUser mui = JsonSerializer.Deserialize(r.GetBodyBytes()); lock(UserIdCacheSync) UserIdCache[mui.UserId] = (DateTimeOffset.Now, mui); onSuccess.Invoke(mui); } catch(Exception ex) { Logger.Debug(ex); onFailure.Invoke(ex); } }, (t, e) => { Logger.Debug(e); onFailure.Invoke(e); } ); } } }