diff --git a/Maki/Discord.cs b/Maki/Discord.cs index 9aff807..ccb6acc 100644 --- a/Maki/Discord.cs +++ b/Maki/Discord.cs @@ -34,21 +34,30 @@ namespace Maki /// public DiscordTokenType TokenType = DiscordTokenType.Bot; + #region Token + /// /// Discord token /// - public string Token = string.Empty; + public string Token { + get => token; + + set + { + token = value; + WebRequest.Authorisation = (IsBot ? "Bot " : string.Empty) + token; + } + } + + private string token = string.Empty; + + #endregion /// /// Gateway Url /// internal string Gateway { get; private set; } - /// - /// Rest API request client - /// - internal readonly RestClient RestClient; - /// /// Gateway shard client /// @@ -209,7 +218,6 @@ namespace Maki public Discord(DiscordParams parameters = null) { Params = parameters ?? new DiscordParams(); - RestClient = new RestClient(this); ShardClient = new GatewayShardClient(this); #region Assigning event handlers @@ -268,15 +276,22 @@ namespace Maki { ClearContainers(); - RestResponse gateway = RestClient.Request( - RestRequestMethod.GET, - IsBot ? RestEndpoints.GatewayBot : RestEndpoints.Gateway - ); + int shards = 1; - if (gateway.ErrorCode != RestErrorCode.Ok) - throw new DiscordException($"{gateway.ErrorCode}: {gateway.ErrorMessage}"); + using (WebRequest wr = new WebRequest(HttpMethod.GET, IsBot ? RestEndpoints.GatewayBot : RestEndpoints.Gateway)) + { + wr.Perform(); - Gateway = gateway.Response.Url; + if (wr.Status != 200) + throw new DiscordException("Failed to retrieve gateway url, is your token valid?"); + + GatewayInfo gi = wr.ResponseJson(); + + Gateway = gi.Url; + + if (gi.Shards.HasValue) + shards = gi.Shards.Value; + } if (Gateway.Contains("?")) Gateway = Gateway.Substring(0, Gateway.IndexOf('?')); @@ -284,8 +299,6 @@ namespace Maki if (!Gateway.EndsWith("/")) Gateway += "/"; - int shards = gateway.Response.Shards ?? 1; - for (int i = 0; i < shards; i++) ShardClient.Create(i); } @@ -298,54 +311,64 @@ namespace Maki /// Multi factor authentication token public void Connect(string email, string password, string code = null) { - TokenType = DiscordTokenType.User; + // TODO: not tested with new request backend yet - RestResponse login = RestClient.Request( - RestRequestMethod.POST, - RestEndpoints.AuthLogin, - new LoginRequest + TokenType = DiscordTokenType.User; + LoginResponse login; + + using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.AuthLogin)) + { + wr.AddJson(new LoginRequest { Email = email, Password = password, - } - ); + }); + wr.Perform(); - if (login.ErrorCode != RestErrorCode.Ok) - throw new DiscordException($"{login.ErrorCode}: {login.ErrorMessage}"); + if (wr.Response.Length < 1) + throw new DiscordException("Login failed"); - if (login.Response.UsernameError?.Length > 0) - throw new DiscordException(login.Response.UsernameError.FirstOrDefault()); + login = wr.ResponseJson(); + } - if (login.Response.PasswordError?.Length > 0) - throw new DiscordException(login.Response.PasswordError.FirstOrDefault()); + if (login.UsernameError?.Length > 0) + throw new DiscordException(login.UsernameError.FirstOrDefault()); - Token = login.Response.Token; + if (login.PasswordError?.Length > 0) + throw new DiscordException(login.PasswordError.FirstOrDefault()); + + Token = login.Token; if (string.IsNullOrEmpty(Token)) { - if (login.Response.MFA == true && !string.IsNullOrEmpty(login.Response.Ticket)) + if (login.MFA == true && !string.IsNullOrEmpty(login.Ticket)) { - RestResponse totp = RestClient.Request( - RestRequestMethod.POST, - RestEndpoints.AuthMfaTotp, - new LoginMultiFactorAuth + using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.AuthMfaTotp)) + { + wr.AddJson(new LoginMultiFactorAuth { Code = code, - Ticket = login.Response.Ticket, - } - ); + Ticket = login.Ticket, + }); + wr.Perform(); - if (totp.ErrorCode != RestErrorCode.Ok) - throw new DiscordException($"{totp.ErrorCode}: {totp.ErrorMessage}"); + if (wr.Response.Length < 1) + throw new DiscordException("Multi factor auth failed"); - Token = totp.Response.Token; + login = wr.ResponseJson(); + } + + //if (totp.ErrorCode != RestErrorCode.Ok) + // throw new DiscordException($"{totp.ErrorCode}: {totp.ErrorMessage}"); + + Token = login.Token; } else throw new DiscordException("Token was null but MFA is false and/or ticket is empty?"); } if (string.IsNullOrEmpty(Token)) - throw new DiscordException("Authentication failed!"); + throw new DiscordException("Authentication failed"); Connect(); } @@ -468,6 +491,7 @@ namespace Maki server.Name = guild.Name; server.OwnerId = guild.OwnerId ?? 0; server.IconHash = guild.IconHash; + server.Created = guild.Created ?? DateTime.MinValue; } if (guild.Channels != null) @@ -693,20 +717,30 @@ namespace Maki if (msg == null) { - RestResponse getMsg = RestClient.Request(RestRequestMethod.GET, RestEndpoints.ChannelMessage(message.ChannelId, message.Id)); - DiscordChannel channel = channels.Where(x => x.Id == getMsg.Response.ChannelId).FirstOrDefault(); - DiscordMember member = members.Where(x => x.User.Id == getMsg.Response.User.Id && (channel.Server == null || channel.Server == x.Server)).FirstOrDefault(); - msg = new DiscordMessage(this, getMsg.Response, channel, member); + using (WebRequest wr = new WebRequest(HttpMethod.GET, RestEndpoints.ChannelMessage(message.ChannelId, message.Id))) + { + wr.Perform(); + + if (wr.Status != 200 || wr.Response.Length < 1) + throw new DiscordException("Failed to load message from API"); + + message = wr.ResponseJson(); + } + + DiscordChannel channel = channels.Where(x => x.Id == message.ChannelId).FirstOrDefault(); + DiscordMember member = members.Where(x => x.User.Id == message.User.Id && (channel.Server == null || channel.Server == x.Server)).FirstOrDefault(); + msg = new DiscordMessage(this, message, channel, member); messages.Add(msg); + } else + { + if (!string.IsNullOrEmpty(message.Content)) + msg.Text = message.Content; + + msg.IsPinned = message.IsPinned == true; } msg.Edited = DateTime.Now; - if (!string.IsNullOrEmpty(message.Content)) - msg.Text = message.Content; - - msg.IsPinned = message.IsPinned == true; - OnMessageUpdate?.Invoke(msg); } diff --git a/Maki/DiscordChannel.cs b/Maki/DiscordChannel.cs index a2b5dea..fe8ae57 100644 --- a/Maki/DiscordChannel.cs +++ b/Maki/DiscordChannel.cs @@ -2,6 +2,8 @@ using Maki.Structures.Channels; using Maki.Structures.Messages; using Maki.Structures.Rest; +using Newtonsoft.Json; +using System.IO; namespace Maki { @@ -38,19 +40,41 @@ namespace Maki Server = server; } - public DiscordMessage Send(string text = "", DiscordEmbed embed = null) + public DiscordMessage Send(Stream stream, string filename, string text = "", DiscordEmbed embed = null) { - RestResponse msg = client.RestClient.Request( - RestRequestMethod.POST, - RestEndpoints.ChannelMessages(Id), - new MessageCreate + byte[] bytes = new byte[stream.Length]; + stream.Read(bytes, 0, bytes.Length); + return Send(text, embed, filename, bytes); + } + + public DiscordMessage Send(string text = "", DiscordEmbed embed = null, string filename = null, byte[] file = null) + { + Message? msg = null; + + using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.ChannelMessages(Id))) + { + wr.AddParam("payload_json", JsonConvert.SerializeObject(new MessageCreate { Text = text, Embed = embed?.ToStruct(), - } - ); + })); + + if (file != null && !string.IsNullOrEmpty(filename)) + wr.AddFile(filename, file); - DiscordMessage message = new DiscordMessage(client, msg.Response, this, client.members.Find(x => x.User.Id == msg.Response.User.Id)); + wr.Perform(); + + if (wr.Status != 200 || wr.Response.Length < 1) + // TODO: elaborate + throw new DiscordException("Failed to send message"); + + msg = wr.ResponseJson(); + } + + if (!msg.HasValue) + throw new DiscordException("Empty response?"); + + DiscordMessage message = new DiscordMessage(client, msg.Value, this, client.members.Find(x => x.User.Id == msg.Value.User.Id)); client.messages.Add(message); return message; } diff --git a/Maki/DiscordMember.cs b/Maki/DiscordMember.cs index 60cff3f..1f3b959 100644 --- a/Maki/DiscordMember.cs +++ b/Maki/DiscordMember.cs @@ -44,7 +44,9 @@ namespace Maki { foreach (DiscordRole role in roles) { - client.RestClient.Request(RestRequestMethod.PUT, RestEndpoints.GuildMemberRole(Server.Id, User.Id, role.Id)); + using (WebRequest wr = new WebRequest(HttpMethod.PUT, RestEndpoints.GuildMemberRole(Server.Id, User.Id, role.Id))) + wr.Perform(); + this.roles.Add(role.Id); } } diff --git a/Maki/DiscordMessage.cs b/Maki/DiscordMessage.cs index fbec2f5..efe24d3 100644 --- a/Maki/DiscordMessage.cs +++ b/Maki/DiscordMessage.cs @@ -55,20 +55,31 @@ namespace Maki public DiscordMessage Edit(string text = null, DiscordEmbed embed = null) { - RestResponse msg = client.RestClient.Request(RestRequestMethod.PATCH, RestEndpoints.ChannelMessage(Channel.Id, Id), new MessageEdit { - Text = text, - Embed = embed?.ToStruct(), - }); + using (WebRequest wr = new WebRequest(HttpMethod.PATCH, RestEndpoints.ChannelMessage(Channel.Id, Id))) + { + wr.AddJson(new MessageEdit + { + Text = text, + Embed = embed?.ToStruct(), + }); + wr.Perform(); - Text = msg.Response.Content; - Edited = msg.Response.Edited ?? DateTime.UtcNow; + if (wr.Status != 200) + return this; + + Message msg = wr.ResponseJson(); + + Text = msg.Content; + Edited = msg.Edited ?? DateTime.UtcNow; + } return this; } public void Delete() { - client.RestClient.Request(RestRequestMethod.DELETE, RestEndpoints.ChannelMessage(Channel.Id, Id)); + using (WebRequest wr = new WebRequest(HttpMethod.DELETE, RestEndpoints.ChannelMessage(Channel.Id, Id))) + wr.Perform(); } } } diff --git a/Maki/DiscordRole.cs b/Maki/DiscordRole.cs index d295641..69f221f 100644 --- a/Maki/DiscordRole.cs +++ b/Maki/DiscordRole.cs @@ -35,31 +35,46 @@ namespace Maki public void Delete() { - RestResponse resp = client.RestClient.Request(RestRequestMethod.DELETE, RestEndpoints.GuildRole(Server.Id, Id)); + using (WebRequest wr = new WebRequest(HttpMethod.DELETE, RestEndpoints.GuildRole(Server.Id, Id))) + { + wr.Perform(); - if (resp.Status != 204) - throw new DiscordException("Failed to delete role!"); + if (wr.Status != 204) + throw new DiscordException($"Failed to delete role {Id} in guild {Server.Id}"); + } } public void Edit(string name = null, DiscordPermission? perms = null, DiscordColour colour = null, bool? hoist = null, bool? mentionable = null) { - RestResponse role = client.RestClient.Request(RestRequestMethod.PATCH, RestEndpoints.GuildRole(Server.Id, Id), new RoleCreate + Role? role; + + using (WebRequest wr = new WebRequest(HttpMethod.PATCH, RestEndpoints.GuildRole(Server.Id, Id))) { - Name = name ?? Name, - Perms = perms ?? Perms, - Colour = colour == null ? 0 : colour.Raw, - Hoist = hoist ?? IsHoisted, - Mentionable = mentionable ?? IsMentionable - }); + wr.AddJson(new RoleCreate + { + Name = name ?? Name, + Perms = perms ?? Perms, + Colour = colour == null ? 0 : colour.Raw, + Hoist = hoist ?? IsHoisted, + Mentionable = mentionable ?? IsMentionable + }); + wr.Perform(); - if (role.ErrorCode != RestErrorCode.Ok) - throw new DiscordException($"{role.ErrorCode}: {role.ErrorMessage}"); + if (wr.Status != 200 || wr.Response.Length < 1) + // TODO: elaborate + throw new DiscordException("Failed to edit role"); - Name = role.Response.Name; - Perms = (DiscordPermission)role.Response.Permissions; - Colour.Raw = role.Response.Colour ?? 0; - IsHoisted = role.Response.IsHoisted == true; - IsMentionable = role.Response.IsMentionable == true; + role = wr.ResponseJson(); + } + + if (!role.HasValue) + throw new DiscordException("Empty response?"); + + Name = role.Value.Name; + Perms = (DiscordPermission)role.Value.Permissions; + Colour.Raw = role.Value.Colour ?? 0; + IsHoisted = role.Value.IsHoisted == true; + IsMentionable = role.Value.IsMentionable == true; } } } diff --git a/Maki/DiscordServer.cs b/Maki/DiscordServer.cs index 7c7cbad..85292a3 100644 --- a/Maki/DiscordServer.cs +++ b/Maki/DiscordServer.cs @@ -14,13 +14,14 @@ namespace Maki public string Name { get; internal set; } public ulong OwnerId { get; internal set; } + public DateTime Created { get; internal set; } internal string IconHash; public DiscordMember[] Members => client.members.Where(x => x.Server == this).ToArray(); public DiscordMember Owner => Members.Where(x => x.User.Id == OwnerId).FirstOrDefault(); public DiscordMember Me => Members.Where(x => x.User == client.Me).FirstOrDefault(); - public DiscordChannel[] TextChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Text).OrderByDescending(x => x.Position).ToArray(); - public DiscordChannel[] VoiceChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Voice).OrderByDescending(x => x.Position).ToArray(); + public DiscordChannel[] TextChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Text).OrderBy(x => x.Position).ToArray(); + public DiscordChannel[] VoiceChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Voice).OrderBy(x => x.Position).ToArray(); public DiscordRole[] Roles => client.roles.Where(x => x.Server == this).OrderByDescending(x => x.Position).ToArray(); public string Icon(string ext = @"png", int size = 128) => RestEndpoints.CDN_URL + $@"/icons/{Id}/{IconHash}.{ext}?size={size}"; @@ -29,29 +30,41 @@ namespace Maki client = discord; Id = guild.Id; Name = guild.Name; + Created = guild.Created ?? DateTime.MinValue; OwnerId = guild.OwnerId ?? ulong.MinValue; IconHash = guild.IconHash; } public DiscordRole CreateRole(string name = null, DiscordPermission perms = DiscordPermission.None, DiscordColour colour = null, bool hoist = false, bool mentionable = false) { - RestResponse roleResp = client.RestClient.Request(RestRequestMethod.POST, RestEndpoints.GuildRoles(Id), new RoleCreate + Role? roleStruct; + + using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.GuildRoles(Id))) { - Name = name, - Perms = perms, - Colour = colour == null ? 0 : colour.Raw, - Hoist = hoist, - Mentionable = mentionable - }); + wr.AddJson(new RoleCreate + { + Name = name, + Perms = perms, + Colour = colour == null ? 0 : colour.Raw, + Hoist = hoist, + Mentionable = mentionable + }); + wr.Perform(); - if (roleResp.ErrorCode != RestErrorCode.Ok) - throw new DiscordException($"{roleResp.ErrorCode}: {roleResp.ErrorMessage}"); + if (wr.Status != 200 || wr.Response.Length < 1) + throw new DiscordException("Failed to create role"); - DiscordRole role = client.roles.Where(x => x.Id == roleResp.Response.Id).FirstOrDefault(); + roleStruct = wr.ResponseJson(); + } + + if (!roleStruct.HasValue) + throw new DiscordException("Empty response?"); + + DiscordRole role = client.roles.Where(x => x.Id == roleStruct.Value.Id).FirstOrDefault(); if (role == default(DiscordRole)) { - role = new DiscordRole(client, roleResp.Response, this); + role = new DiscordRole(client, roleStruct.Value, this); client.roles.Add(role); } diff --git a/Maki/DiscordUser.cs b/Maki/DiscordUser.cs index 432c76e..036a63b 100644 --- a/Maki/DiscordUser.cs +++ b/Maki/DiscordUser.cs @@ -31,7 +31,7 @@ namespace Maki { client = discord; Id = user.Id; - Created = Utility.FromDiscordTimeMilliseconds((long) Id >> 22); + Created = Utility.FromDiscordSnowflake(Id); Username = user.Username; Tag = user.Tag.Value; IsBot = user.IsBot; diff --git a/Maki/Maki.csproj b/Maki/Maki.csproj index 299ba02..6baf447 100644 --- a/Maki/Maki.csproj +++ b/Maki/Maki.csproj @@ -70,15 +70,14 @@ - - + - + diff --git a/Maki/Rest/RestRequestMethod.cs b/Maki/Rest/HttpMethod.cs similarity index 95% rename from Maki/Rest/RestRequestMethod.cs rename to Maki/Rest/HttpMethod.cs index 44054a4..1529e35 100644 --- a/Maki/Rest/RestRequestMethod.cs +++ b/Maki/Rest/HttpMethod.cs @@ -3,7 +3,7 @@ /// /// Discord Rest request methods /// - enum RestRequestMethod + enum HttpMethod { /// /// GET, does not send additional data diff --git a/Maki/Rest/RestClient.cs b/Maki/Rest/RestClient.cs deleted file mode 100644 index edc8b38..0000000 --- a/Maki/Rest/RestClient.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Maki.Structures.Rest; -using Newtonsoft.Json; -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; - -namespace Maki.Rest -{ - /// - /// Handles requests to Discord's REST API - /// - internal class RestClient - { - /// - /// User agent that is send alongside requests - /// - private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)"; - - /// - /// Container for parent DiscordClient instance - /// - private readonly Discord client; - - /// - /// Request types that should be handled as data requests - /// - private readonly RestRequestMethod[] dataRequestTypes = new RestRequestMethod[] { - RestRequestMethod.POST, - RestRequestMethod.PUT, - RestRequestMethod.PATCH, - }; - - /// - /// Constructor - /// - /// Parent DiscordClient instance - public RestClient(Discord c) - { - client = c; - } - - /// - /// Creates a base HttpWebRequest - /// - /// Request method - /// Endpoint url - /// Prepared HttpWebRequest - private HttpWebRequest BaseRequest(RestRequestMethod method, string url) - { - Uri uri = new Uri(RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH + url); - HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest; - - request.Method = method.ToString(); - request.UserAgent = USER_AGENT; - - if (!string.IsNullOrEmpty(client.Token)) - request.Headers[HttpRequestHeader.Authorization] = (client.IsBot ? "Bot " : string.Empty) + client.Token; - - return request; - } - - /// - /// Gets a response string from HttpWebRequest - /// - /// HttpWebRequest instance - /// string output - private RestResponse HandleResponse(HttpWebRequest request) - { - HttpWebResponse webResponse; - RestResponse response = new RestResponse(); - - try - { - webResponse = request.GetResponse() as HttpWebResponse; - } - catch (WebException ex) - { - webResponse = ex.Response as HttpWebResponse; - } - - Enum.TryParse(webResponse.Method, out response.Method); - response.Uri = webResponse.ResponseUri; - response.Status = (ushort)webResponse.StatusCode; - - using (Stream httpResponseStream = webResponse.GetResponseStream()) - using (StreamReader streamReader = new StreamReader(httpResponseStream)) - response.RawResponse = streamReader.ReadToEnd(); - - webResponse.Close(); - - Error error = default(Error); - - try - { - error = JsonConvert.DeserializeObject(response.RawResponse); - } - catch - { - } - - if (error.Equals(default(Error))) - { - response.ErrorCode = RestErrorCode.Ok; - response.ErrorMessage = string.Empty; - } - else - { - response.ErrorCode = (RestErrorCode)error.Code; - response.ErrorMessage = error.Message; - } - - if (response.Status == 200) - response.Response = JsonConvert.DeserializeObject(response.RawResponse); - - return response; - } - - /// - /// Make a json request - /// - /// Type to use when deserialising the JSON object - /// Request method - /// Endpoint url - /// Request data - /// Deserialised JSON object - public RestResponse Request(RestRequestMethod method, string url, object data = null) - { - HttpWebRequest request = BaseRequest(method, url); - - request.ContentType = "application/json;charset=utf-8"; - request.ContentLength = 0; - - if (dataRequestTypes.Contains(method) && data != null) - { - byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)); - request.ContentLength = bytes.LongLength; - - using (Stream requestStream = request.GetRequestStream()) - requestStream.Write(bytes, 0, bytes.Length); - } - - return HandleResponse(request); - } - } -} diff --git a/Maki/Rest/RestResponse.cs b/Maki/Rest/RestResponse.cs deleted file mode 100644 index 397690c..0000000 --- a/Maki/Rest/RestResponse.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Maki.Rest -{ - internal class RestResponse - { - public RestRequestMethod Method; - public Uri Uri; - public ushort Status; - public RestErrorCode ErrorCode; - public string ErrorMessage; - public string RawResponse; - public T Response; - } -} diff --git a/Maki/Rest/WebRequest.cs b/Maki/Rest/WebRequest.cs new file mode 100644 index 0000000..d353530 --- /dev/null +++ b/Maki/Rest/WebRequest.cs @@ -0,0 +1,260 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; + +namespace Maki.Rest +{ + internal class WebRequest : IDisposable + { + private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)"; + private const string GENERIC_CONTENT_TYPE = @"application/octet-stream"; + private const string JSON_CONTENT_TYPE = @"application/json"; + private const string FORM_CONTENT_TYPE = @"multipart/form-data"; + + internal readonly HttpMethod Method; + internal readonly string Url; + + internal string ContentType { get; set; } = GENERIC_CONTENT_TYPE; + + // TODO: make this not static + internal static string Authorisation { get; set; } + + private readonly Dictionary headers = new Dictionary(); + private readonly Dictionary parameters = new Dictionary(); + private readonly Dictionary files = new Dictionary(); + + private readonly Dictionary mimeTypes = new Dictionary() + { + { "png", "image/png" }, + { "jpg", "image/jpeg" }, + { "jpeg", "image/jpeg" }, + { "gif", "image/gif" }, + }; + + private byte[] rawContent = new byte[0]; + private HttpWebRequest wRequest; + private Stream requestStream; + private HttpWebResponse wResponse; + private Stream responseStream; + + private byte[] rawResponse; + internal byte[] RawResponse + { + get + { + if (rawResponse == null) + { + rawResponse = new byte[4096]; + responseStream.Read(rawResponse, 0, rawResponse.Length); + } + + return rawResponse; + } + } + + private string responseString = string.Empty; + + internal string Response + { + get + { + if (string.IsNullOrEmpty(responseString)) + responseString = Encoding.UTF8.GetString(RawResponse).Trim('\0'); + + return responseString; + } + } + + internal T ResponseJson() => + JsonConvert.DeserializeObject(Response); + + internal short Status => + (short)wResponse?.StatusCode; + + static WebRequest() + { + ServicePointManager.Expect100Continue = false; + } + + internal WebRequest(HttpMethod method, string url) + { + Method = method; + Url = url; + } + + internal void AddRaw(byte[] bytes) => + rawContent = bytes; + + internal void AddRaw(string str) => + AddRaw(Encoding.UTF8.GetBytes(str)); + + internal void AddJson(object obj) + { + ContentType = JSON_CONTENT_TYPE; + AddRaw(JsonConvert.SerializeObject(obj)); + } + + internal void AddParam(string name, string contents) => + parameters.Add(name, contents); + + internal void AddFile(string name, byte[] bytes) => + files.Add(name, bytes); + + internal void Perform() + { + StringBuilder urlBuilder = new StringBuilder(); + + if (!Url.StartsWith("http://") || !Url.StartsWith("https://")) + { + urlBuilder.Append(RestEndpoints.BASE_URL); + urlBuilder.Append(RestEndpoints.BASE_PATH); + } + + urlBuilder.Append(Url); + + if (Method == HttpMethod.GET + || Method == HttpMethod.DELETE) + if (parameters.Count > 1) + { + if (!Url.Contains('?')) + urlBuilder.Append(@"?"); + + foreach (KeyValuePair param in parameters) + urlBuilder.Append($@"{param.Key}={param.Value}&"); + } + + string url = urlBuilder.ToString().TrimEnd('&'); + + wRequest = System.Net.WebRequest.Create(url) as HttpWebRequest; + wRequest.Method = Method.ToString(); + wRequest.UserAgent = USER_AGENT; + wRequest.KeepAlive = true; + //wRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + wRequest.ReadWriteTimeout = Timeout.Infinite; + wRequest.Timeout = Timeout.Infinite; + + if (!string.IsNullOrEmpty(Authorisation)) + wRequest.Headers[HttpRequestHeader.Authorization] = Authorisation; + + foreach (KeyValuePair header in headers) + wRequest.Headers[header.Key] = header.Value; + + if (Method == HttpMethod.POST + || Method == HttpMethod.PUT + || Method == HttpMethod.PATCH) + { + requestStream = wRequest.GetRequestStream(); + + if (parameters.Count + files.Count < 1) + requestStream.Write(rawContent, 0, rawContent.Length); + else + { + string boundary = $@"-----------------------------{DateTime.Now.Ticks}"; + ContentType = $@"{FORM_CONTENT_TYPE}; boundary={boundary}"; + + if (parameters.Count >= 1) + { + StringBuilder postBodyBuilder = new StringBuilder(); + byte[] postBody = new byte[0]; + + foreach (KeyValuePair param in parameters) + { + postBodyBuilder.AppendLine($@"--{boundary}"); + postBodyBuilder.AppendLine($@"Content-Disposition: form-data; name=""{param.Key}"""); + postBodyBuilder.AppendLine(); + postBodyBuilder.AppendLine(param.Value); + } + + postBody = Encoding.UTF8.GetBytes(postBodyBuilder.ToString()); + requestStream.Write(postBody, 0, postBody.Length); + } + + if (files.Count >= 1) + { + byte[] boundaryBytes = Encoding.UTF8.GetBytes($@"--{boundary}"); + byte[] newLineBytes = Encoding.UTF8.GetBytes("\r\n"); + + foreach (KeyValuePair file in files) + { + string cType = GENERIC_CONTENT_TYPE; + string fileExt = Path.GetExtension(file.Key).ToLower().TrimStart('.'); + + if (mimeTypes.ContainsKey(fileExt)) + cType = mimeTypes[fileExt]; + + byte[] cDisposBytes = Encoding.UTF8.GetBytes($@"Content-Disposition: form-data; name=""{file.Key}""; filename=""{file.Key}"""); + byte[] cTypeBytes = Encoding.UTF8.GetBytes($@"Content-Type: {cType}"); + + // Boundary + newline + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + requestStream.Write(newLineBytes, 0, newLineBytes.Length); + + // Disposition header + newline + requestStream.Write(cDisposBytes, 0, cDisposBytes.Length); + requestStream.Write(newLineBytes, 0, newLineBytes.Length); + + // Type header + newline + requestStream.Write(cTypeBytes, 0, cTypeBytes.Length); + requestStream.Write(newLineBytes, 0, newLineBytes.Length); + + // newline + contents + newline + requestStream.Write(newLineBytes, 0, newLineBytes.Length); + requestStream.Write(file.Value, 0, file.Value.Length); + requestStream.Write(newLineBytes, 0, newLineBytes.Length); + } + } + + byte[] closingBound = Encoding.UTF8.GetBytes($@"--{boundary}--"); + requestStream.Write(closingBound, 0, closingBound.Length); + } + } + + wRequest.ContentType = ContentType; + + try + { + wResponse = wRequest.GetResponse() as HttpWebResponse; + } catch (WebException ex) + { + wResponse = ex.Response as HttpWebResponse; + } + + responseStream = wResponse.GetResponseStream(); + } + + #region IDisposable + private bool isDisposed = false; + + private void Dispose(bool disposing) + { + if (!isDisposed) + { + isDisposed = true; + requestStream?.Dispose(); + wRequest?.Abort(); + responseStream?.Dispose(); + wResponse?.Close(); + } + } + + ~WebRequest() + { + Dispose(false); + } + + /// + /// Disconnects and releases all unmanaged objects + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(true); + } + #endregion + } +} diff --git a/Maki/Utility.cs b/Maki/Utility.cs index 977d91a..047f811 100644 --- a/Maki/Utility.cs +++ b/Maki/Utility.cs @@ -10,5 +10,7 @@ namespace Maki public static DateTime FromUnixTimeMilliseconds(long ms) => UnixEpoch.AddMilliseconds(ms); public static DateTime FromDiscordTimeMilliseconds(long ms) => DiscordEpoch.AddMilliseconds(ms); + + public static DateTime FromDiscordSnowflake(ulong snowflake) => FromDiscordTimeMilliseconds((long)snowflake >> 22); } }