diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..4dc11a7
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Julian van de Groep
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Maki.sln b/Maki.sln
index 27135d6..357b051 100644
--- a/Maki.sln
+++ b/Maki.sln
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26403.7
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maki", "Maki\Maki.csproj", "{4DC6C0A6-520E-4A0A-9A47-3515458B44A2}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maki", "Maki\Maki.csproj", "{97523AED-B694-42C2-96AC-86A1D65109F7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakiTest", "MakiTest\MakiTest.csproj", "{3B147886-0307-4AB8-92A5-4C5CBB93B580}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +13,14 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {4DC6C0A6-520E-4A0A-9A47-3515458B44A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4DC6C0A6-520E-4A0A-9A47-3515458B44A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4DC6C0A6-520E-4A0A-9A47-3515458B44A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4DC6C0A6-520E-4A0A-9A47-3515458B44A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {97523AED-B694-42C2-96AC-86A1D65109F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {97523AED-B694-42C2-96AC-86A1D65109F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {97523AED-B694-42C2-96AC-86A1D65109F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {97523AED-B694-42C2-96AC-86A1D65109F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3B147886-0307-4AB8-92A5-4C5CBB93B580}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B147886-0307-4AB8-92A5-4C5CBB93B580}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B147886-0307-4AB8-92A5-4C5CBB93B580}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3B147886-0307-4AB8-92A5-4C5CBB93B580}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Maki/Discord.cs b/Maki/Discord.cs
new file mode 100644
index 0000000..a92e0a7
--- /dev/null
+++ b/Maki/Discord.cs
@@ -0,0 +1,838 @@
+using Maki.Gateway;
+using Maki.Rest;
+using Maki.Structures.Auth;
+using Maki.Structures.Channels;
+using Maki.Structures.Gateway;
+using Maki.Structures.Guilds;
+using Maki.Structures.Messages;
+using Maki.Structures.Presences;
+using Maki.Structures.Roles;
+using Maki.Structures.Users;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Maki
+{
+ ///
+ /// Discord Client
+ ///
+ public class Discord : IDisposable
+ {
+ ///
+ /// Discord Gateway/API version we're targeting
+ ///
+ internal const int GATEWAY_VERSION = 6;
+
+ ///
+ /// Whether this client is a bot
+ ///
+ public bool IsBot => TokenType == DiscordTokenType.Bot;
+
+ ///
+ /// Token type, bot and user connections are handled differently
+ ///
+ public DiscordTokenType TokenType = DiscordTokenType.Bot;
+
+ ///
+ /// Discord token
+ ///
+ public string Token = string.Empty;
+
+ ///
+ /// Gateway Url
+ ///
+ internal string Gateway { get; private set; }
+
+ ///
+ /// Rest API request client
+ ///
+ internal readonly RestClient RestClient;
+
+ ///
+ /// Gateway shard client
+ ///
+ internal readonly GatewayShardClient ShardClient;
+
+ #region Containers
+ ///
+ /// Servers we're in
+ ///
+ internal readonly List servers = new List();
+
+ ///
+ /// Users we are affiliated with
+ ///
+ internal readonly List users = new List();
+
+ ///
+ /// Members of servers, different from users!
+ ///
+ internal readonly List members = new List();
+
+ ///
+ /// Roles in servers
+ ///
+ internal readonly List roles = new List();
+
+ ///
+ /// Channels
+ ///
+ internal readonly List channels = new List();
+
+ ///
+ /// Messages
+ ///
+ internal readonly List messages = new List();
+ #endregion
+
+ #region Events
+ ///
+ /// A channel has been created
+ ///
+ public event Action OnChannelCreate;
+
+ ///
+ /// A channel has been updated
+ ///
+ public event Action OnChannelUpdate;
+
+ ///
+ /// A channel has been deleted
+ ///
+ public event Action OnChannelDelete;
+
+ ///
+ /// Someone has been banned from a server
+ ///
+ public event Action OnBanAdd;
+
+ ///
+ /// Someone's ban has been lifted from a server
+ ///
+ public event Action OnBanRemove;
+
+ ///
+ /// A server has been created
+ ///
+ public event Action OnServerCreate;
+
+ ///
+ /// A server has been updated
+ ///
+ public event Action OnServerUpdate;
+
+ ///
+ /// A server has been deleted
+ ///
+ public event Action OnServerDelete;
+
+ ///
+ /// A server's emoji set has been updated
+ ///
+ public event Action OnEmojisUpdate;
+
+ ///
+ /// Someone joined a server
+ ///
+ public event Action OnMemberAdd;
+
+ ///
+ /// A server member has been updated
+ ///
+ public event Action OnMemberUpdate;
+
+ ///
+ /// Someone left a server
+ ///
+ public event Action OnMemberRemove;
+
+ ///
+ /// A user role has been created in a server
+ ///
+ public event Action OnRoleCreate;
+
+ ///
+ /// A user role has been updated in a server
+ ///
+ public event Action OnRoleUpdate;
+
+ ///
+ /// A user role has been deleted in a server
+ ///
+ public event Action OnRoleDelete;
+
+ ///
+ /// A message has been received
+ ///
+ public event Action OnMessageCreate;
+
+ ///
+ /// A message has been edited
+ ///
+ public event Action OnMessageUpdate;
+
+ ///
+ /// A message has been deleted
+ ///
+ public event Action OnMessageDelete;
+
+ ///
+ /// Someone started typing, fired again after 8 seconds
+ ///
+ public event Action OnTypingStart;
+
+ ///
+ /// Someone's status and/or game updated
+ ///
+ public event Action OnPresenceUpdate;
+
+ ///
+ /// Fired when the connection was successful and we're currently logged into Discord
+ ///
+ public event Action OnReady;
+
+ ///
+ /// Someone updated their account
+ ///
+ public event Action OnUserUpdate;
+ #endregion
+
+ public DiscordServer[] Servers => servers.ToArray();
+ public DiscordUser Me => users.FirstOrDefault();
+
+ ///
+ /// Constructor
+ ///
+ public Discord()
+ {
+ RestClient = new RestClient(this);
+ ShardClient = new GatewayShardClient(this);
+
+ ShardClient.OnChannelCreate += ShardManager_OnChannelCreate;
+ ShardClient.OnChannelUpdate += ShardManager_OnChannelUpdate;
+ ShardClient.OnChannelDelete += ShardManager_OnChannelDelete;
+
+ ShardClient.OnGuildBanAdd += ShardManager_OnGuildBanAdd;
+ ShardClient.OnGuildBanRemove += ShardManager_OnGuildBanRemove;
+ ShardClient.OnGuildCreate += ShardManager_OnGuildCreate;
+ ShardClient.OnGuildDelete += ShardManager_OnGuildDelete;
+ ShardClient.OnGuildUpdate += ShardManager_OnGuildUpdate;
+ ShardClient.OnGuildEmojisUpdate += ShardManager_OnGuildEmojisUpdate;
+ ShardClient.OnGuildMemberAdd += ShardManager_OnGuildMemberAdd;
+ ShardClient.OnGuildMemberUpdate += ShardManager_OnGuildMemberUpdate;
+ ShardClient.OnGuildMemberRemove += ShardManager_OnGuildMemberRemove;
+ ShardClient.OnGuildRoleCreate += ShardManager_OnGuildRoleCreate;
+ ShardClient.OnGuildRoleDelete += ShardManager_OnGuildRoleDelete;
+ ShardClient.OnGuildRoleUpdate += ShardManager_OnGuildRoleUpdate;
+ ShardClient.OnGuildIntegrationsUpdate += ShardManager_OnGuildIntegrationsUpdate;
+ ShardClient.OnGuildMembersChunk += ShardManager_OnGuildMembersChunk;
+
+ ShardClient.OnMessageCreate += ShardManager_OnMessageCreate;
+ ShardClient.OnMessageDelete += ShardManager_OnMessageDelete;
+ ShardClient.OnMessageUpdate += ShardManager_OnMessageUpdate;
+
+ ShardClient.OnTypingStart += ShardManager_OnTypingStart;
+ ShardClient.OnPresenceUpdate += ShardManager_OnPresenceUpdate;
+
+ ShardClient.OnReady += ShardManager_OnReady;
+ ShardClient.OnResumed += ShardManager_OnResumed;
+
+ ShardClient.OnUserUpdate += ShardManager_OnUserUpdate;
+
+ ShardClient.OnSocketOpen += ShardManager_OnSocketOpen;
+ ShardClient.OnSocketClose += ShardManager_OnSocketClose;
+ ShardClient.OnSocketError += ShardManager_OnSocketError;
+ ShardClient.OnSocketMessage += ShardManager_OnSocketMessage;
+ }
+
+ private void ClearContainers()
+ {
+ servers.Clear();
+ users.Clear();
+ roles.Clear();
+ channels.Clear();
+ messages.Clear();
+ members.Clear();
+ }
+
+ ///
+ /// Connects to Discord using the token assigned to Token
+ ///
+ public void Connect()
+ {
+ ClearContainers();
+
+ RestResponse gateway = RestClient.Request(
+ RestRequestMethod.GET,
+ IsBot ? RestEndpoints.GatewayBot : RestEndpoints.Gateway
+ );
+
+ if (gateway.ErrorCode != RestErrorCode.Ok)
+ throw new Exception($"{gateway.ErrorCode}: {gateway.ErrorMessage}");
+
+ Gateway = gateway.Response.Url;
+
+ if (Gateway.Contains("?"))
+ Gateway = Gateway.Substring(0, Gateway.IndexOf('?'));
+
+ if (!Gateway.EndsWith("/"))
+ Gateway += "/";
+
+ int shards = gateway.Response.Shards ?? 1;
+
+ for (int i = 0; i < shards; i++)
+ ShardClient.Create(i);
+ }
+
+ ///
+ /// Connects to Discord with the provided email, password and, optionally, mfa token
+ ///
+ /// Discord account email
+ /// Discord account password
+ /// Multi factor authentication token
+ public void Connect(string email, string password, string code = null)
+ {
+ TokenType = DiscordTokenType.User;
+
+ RestResponse login = RestClient.Request(
+ RestRequestMethod.POST,
+ RestEndpoints.AuthLogin,
+ new LoginRequest
+ {
+ Email = email,
+ Password = password,
+ }
+ );
+
+ if (login.ErrorCode != RestErrorCode.Ok)
+ throw new Exception($"{login.ErrorCode}: {login.ErrorMessage}");
+
+ if (login.Response.UsernameError?.Length > 0)
+ throw new Exception(login.Response.UsernameError.FirstOrDefault());
+
+ if (login.Response.PasswordError?.Length > 0)
+ throw new Exception(login.Response.PasswordError.FirstOrDefault());
+
+ Token = login.Response.Token;
+
+ if (string.IsNullOrEmpty(Token))
+ {
+ if (login.Response.MFA == true && !string.IsNullOrEmpty(login.Response.Ticket))
+ {
+ RestResponse totp = RestClient.Request(
+ RestRequestMethod.POST,
+ RestEndpoints.AuthMfaTotp,
+ new LoginMultiFactorAuth
+ {
+ Code = code,
+ Ticket = login.Response.Ticket,
+ }
+ );
+
+ if (totp.ErrorCode != RestErrorCode.Ok)
+ throw new Exception($"{totp.ErrorCode}: {totp.ErrorMessage}");
+
+ Token = totp.Response.Token;
+ }
+ else
+ throw new Exception("Token was null but MFA is false and/or ticket is empty?");
+ }
+
+ if (string.IsNullOrEmpty(Token))
+ throw new Exception("Authentication failed!");
+
+ Connect();
+ }
+
+ ///
+ /// Connects to Discord using a provided token and token type
+ ///
+ ///
+ ///
+ public void Connect(string token, DiscordTokenType type = DiscordTokenType.Bot)
+ {
+ TokenType = type;
+ Token = token;
+ Connect();
+ }
+
+ ///
+ /// Disconnects from Discord, use Dispose instead if you're not restarting the connection
+ ///
+ public void Disconnect()
+ {
+ ShardClient.Disconnect();
+ ClearContainers();
+ Token = string.Empty;
+ Gateway = string.Empty;
+ }
+
+ private void ShardManager_OnChannelCreate(GatewayShard shard, Channel channel)
+ {
+ DiscordServer server = servers.Find(x => x.Id == channel.GuildId);
+ DiscordChannel chan = new DiscordChannel(this, channel, server);
+ channels.Add(chan);
+ OnChannelCreate?.Invoke(chan);
+ }
+
+ private void ShardManager_OnChannelUpdate(GatewayShard shard, Channel channel)
+ {
+ DiscordChannel chan = channels.Find(x => x.Id == channel.Id);
+
+ chan.Name = channel.Name;
+ /*existing.Type = channel.Type;
+ existing.LastMessageId = channel.LastMessageId;
+
+ if (existing.Type == ChannelType.Private)
+ existing.Recipients = channel.Recipients;
+ else
+ {
+ existing.GuildId = channel.GuildId;
+ existing.Topic = channel.Topic;
+ existing.PermissionOverwrites = channel.PermissionOverwrites;
+ existing.Position = channel.Position;
+ }
+
+ if (existing.Type == ChannelType.Voice)
+ {
+ existing.Bitrate = channel.Bitrate;
+ existing.UserLimit = channel.UserLimit;
+ }*/
+
+ OnChannelUpdate?.Invoke(chan);
+ }
+
+ private void ShardManager_OnChannelDelete(GatewayShard shard, Channel channel)
+ {
+ DiscordChannel chan = channels.Find(x => x.Id == channel.Id);
+
+ chan.Name = channel.Name;
+ /*chan.Type = channel.Type;
+ chan.LastMessageId = channel.LastMessageId;
+
+ if (chan.Type == ChannelType.Private)
+ chan.Recipients = channel.Recipients;
+ else
+ {
+ chan.GuildId = channel.GuildId;
+ chan.Topic = channel.Topic;
+ chan.PermissionOverwrites = channel.PermissionOverwrites;
+ chan.Position = channel.Position;
+ }
+
+ if (chan.Type == ChannelType.Voice)
+ {
+ chan.Bitrate = channel.Bitrate;
+ chan.UserLimit = channel.UserLimit;
+ }*/
+
+ channels.Remove(chan);
+ OnChannelDelete?.Invoke(chan);
+ }
+
+ private void ShardManager_OnGuildBanAdd(GatewayShard shard, User sUser)
+ {
+ /*$"Id: {user.Id}",
+ $"Guild Id: {user.GuildId}",
+ $"Username: {user.Username}",
+ $"Tag: {user.Tag:0000}",
+ $"Avatar Hash: {user.AvatarHash}",
+ $"Is bot?: {user.IsBot}",
+ $"Has MFA?: {user.HasMFA}",
+ $"Is verified?: {user.IsVerified}",
+ $"E-mail: {user.EMail}"*/
+
+ DiscordUser user = users.Find(x => x.Id == sUser.Id);
+ DiscordServer server = servers.Find(x => x.Id == sUser.GuildId);
+
+ OnBanAdd?.Invoke(user, server);
+ }
+
+ private void ShardManager_OnGuildBanRemove(GatewayShard shard, User sUser)
+ {
+ /*$"Id: {user.Id}",
+ $"Guild Id: {user.GuildId}",
+ $"Username: {user.Username}",
+ $"Tag: {user.Tag:0000}",
+ $"Avatar Hash: {user.AvatarHash}",
+ $"Is bot?: {user.IsBot}",
+ $"Has MFA?: {user.HasMFA}",
+ $"Is verified?: {user.IsVerified}",
+ $"E-mail: {user.EMail}"*/
+
+ DiscordUser user = users.Find(x => x.Id == sUser.Id);
+ DiscordServer server = servers.Find(x => x.Id == sUser.GuildId);
+
+ OnBanRemove?.Invoke(user, server);
+ }
+
+ private void ShardManager_OnGuildCreate(GatewayShard shard, Guild guild)
+ {
+ DiscordServer server = servers.Where(x => x.Id == guild.Id).FirstOrDefault();
+
+ if (server == default(DiscordServer))
+ {
+ server = new DiscordServer(this, guild);
+ servers.Add(server);
+ }
+ else
+ {
+ // should this explode instead if the guild isn't unavailable?
+ server = servers.Find(x => x.Id == guild.Id);
+ server.Name = guild.Name;
+ }
+
+ if (guild.Channels != null)
+ for (int i = 0; i < guild.Channels.Length; i++)
+ {
+ Channel channel = guild.Channels[i];
+ channel.GuildId = server.Id;
+
+ if (channels.Where(x => x.Id == channel.Id).Count() > 0)
+ ShardManager_OnChannelUpdate(shard, channel);
+ else
+ ShardManager_OnChannelCreate(shard, channel);
+ }
+
+ if (guild.Roles != null)
+ foreach (Role role in guild.Roles) {
+ GuildRole gRole = new GuildRole
+ {
+ Guild = server.Id,
+ Role = role,
+ RoleId = role.Id
+ };
+
+ if (roles.Where(x => x.Id == role.Id).Count() > 0)
+ ShardManager_OnGuildRoleUpdate(shard, gRole);
+ else
+ ShardManager_OnGuildRoleCreate(shard, gRole);
+ }
+
+ shard.Send(GatewayOPCode.RequestGuildMembers, new GatewayRequestGuildMembers {
+ Guild = server.Id,
+ Query = string.Empty,
+ Limit = 0,
+ });
+
+ OnServerCreate?.Invoke(server);
+ }
+
+ private void ShardManager_OnGuildDelete(GatewayShard shard, Guild guild)
+ {
+ DiscordServer server = servers.Find(x => x.Id == guild.Id);
+
+ server.Name = guild.Name;
+ /*server.OwnerId = guild.OwnerId;
+ server.VoiceRegionId = guild.VoiceRegionId;
+ server.AfkChannelId = guild.AfkChannelId;
+ server.AfkTimeout = guild.AfkTimeout;
+ server.EmbedEnabled = guild.EmbedEnabled;
+ server.EmbedChannelId = guild.EmbedChannelId;
+ server.VerificationLevel = guild.VerificationLevel;
+ server.MessageNotificationLevel = guild.MessageNotificationLevel;
+ server.Roles = guild.Roles;
+ server.Emojis = guild.Emojis;
+ server.Features = guild.Features;
+ server.MultiFactorAuthLevel = guild.MultiFactorAuthLevel;*/
+
+ OnServerDelete?.Invoke(server);
+ messages.Where(x => x.Channel.Server == server).ToList().ForEach(x => messages.Remove(x));
+ channels.Where(x => x.Server == server).ToList().ForEach(x => channels.Remove(x));
+ members.Where(x => x.Server == server).ToList().ForEach(x => members.Remove(x));
+ roles.Where(x => x.Server == server).ToList().ForEach(x => roles.Remove(x));
+ servers.Remove(server);
+ }
+
+ private void ShardManager_OnGuildUpdate(GatewayShard shard, Guild guild)
+ {
+ DiscordServer server = servers.Find(x => x.Id == guild.Id);
+
+ server.Name = guild.Name;
+ /*server.OwnerId = guild.OwnerId;
+ server.VoiceRegionId = guild.VoiceRegionId;
+ server.AfkChannelId = guild.AfkChannelId;
+ server.AfkTimeout = guild.AfkTimeout;
+ server.EmbedEnabled = guild.EmbedEnabled;
+ server.EmbedChannelId = guild.EmbedChannelId;
+ server.VerificationLevel = guild.VerificationLevel;
+ server.MessageNotificationLevel = guild.MessageNotificationLevel;
+ server.Roles = guild.Roles;
+ server.Emojis = guild.Emojis;
+ server.Features = guild.Features;
+ server.MultiFactorAuthLevel = guild.MultiFactorAuthLevel;*/
+
+ OnServerUpdate?.Invoke(server);
+ }
+
+ private void ShardManager_OnGuildEmojisUpdate(GatewayShard shard, Guild guild)
+ {
+ DiscordServer server = servers.Find(x => x.Id == guild.Id);
+ //servers.Emojis = guild.Emojis;
+ OnEmojisUpdate?.Invoke(server);
+ }
+
+ private void ShardManager_OnGuildMemberAdd(GatewayShard shard, GuildMember sMember)
+ {
+ DiscordServer server = servers.Find(x => x.Id == sMember.GuildId);
+ DiscordUser user = users.Where(x => x.Id == sMember.User.Id).FirstOrDefault();
+
+ if (user == default(DiscordUser))
+ {
+ user = new DiscordUser(this, sMember.User);
+ users.Add(user);
+ }
+
+ DiscordMember member = new DiscordMember(this, sMember, user, server);
+ members.Add(member);
+ OnMemberAdd?.Invoke(member);
+ }
+
+ private void ShardManager_OnGuildMemberUpdate(GatewayShard shard, GuildMember sMember)
+ {
+ DiscordMember member = members.Find(x => x.User.Id == sMember.User.Id && x.Server.Id == sMember.GuildId);
+
+ /*existing.IsDeafened = member.IsDeafened;
+ existing.IsMuted = member.IsMuted;
+ existing.JoinedAt = member.JoinedAt;
+ existing.Roles = member.Roles;*/
+ member.Nickname = sMember.Nickname;
+
+ OnMemberUpdate?.Invoke(member);
+ }
+
+ private void ShardManager_OnGuildMemberRemove(GatewayShard shard, GuildMember sMember)
+ {
+ DiscordMember member = members.Find(x => x.User.Id == sMember.User.Id && x.Server.Id == sMember.GuildId);
+ members.Remove(member);
+ OnMemberRemove?.Invoke(member);
+ }
+
+ private void ShardManager_OnGuildRoleCreate(GatewayShard shard, GuildRole sRole)
+ {
+ /*$"Guild Id: {role.Guild}",
+ $"Id: {role.Role.Value.Id}",
+ $"Name: {role.Role.Value.Name}",
+ $"Colour: {role.Role.Value.Colour}",
+ $"Is Hoisted?: {role.Role.Value.IsHoisted}",
+ $"Position: {role.Role.Value.Position}",
+ $"Is Managed?: {role.Role.Value.IsManaged}",
+ $"Is Mentionable?: {role.Role.Value.IsMentionable}",
+ $"Permissions: {role.Role.Value.Permissions}"*/
+
+ DiscordServer server = servers.Find(x => x.Id == sRole.Guild);
+ DiscordRole role = new DiscordRole(this, sRole.Role.Value, server);
+ roles.Add(role);
+ OnRoleCreate?.Invoke(role);
+ }
+
+ private void ShardManager_OnGuildRoleDelete(GatewayShard shard, GuildRole sRole)
+ {
+ DiscordRole role = roles.Find(x => x.Id == sRole.RoleId && x.Server.Id == sRole.Guild);
+ roles.Remove(role);
+ OnRoleDelete?.Invoke(role);
+ }
+
+ private void ShardManager_OnGuildRoleUpdate(GatewayShard shard, GuildRole sRole)
+ {
+ Role dRole = sRole.Role.Value;
+ DiscordRole role = roles.Find(x => x.Id == dRole.Id && x.Server.Id == sRole.Guild);
+
+ role.Name = dRole.Name;
+
+ OnRoleUpdate?.Invoke(role);
+ }
+
+ private void ShardManager_OnGuildIntegrationsUpdate(GatewayShard shard, GuildIntegration integration)
+ {
+ /*$"Id: {integration.Id}",
+ $"Name: {integration.Name}",
+ $"Type: {integration.Type}",
+ $"Is Enabled?: {integration.IsEnabled}",
+ $"Is Syncing?: {integration.IsSyncing}",
+ $"Role Id: {integration.RoleId}",
+ $"Expire Behaviour: {integration.ExpireBehaviour}",
+ $"Expire Grace Period: {integration.ExpireGracePeriod}",
+ $"User: {integration.User.Id}",
+ $"Account: {integration.Account.Id}, {integration.Account.Name}",
+ $"Last Sync: {integration.LastSync}"*/
+ }
+
+ private void ShardManager_OnGuildMembersChunk(GatewayShard shard, GuildMembersChunk membersChunk)
+ {
+ /*$"Guild Id: {membersChunk.Guild}",
+ $"Members {string.Join(", ", membersChunk.Members.Select(x => x.User.Id).ToArray())}"*/
+
+ for (int i = 0; i < membersChunk.Members.Length; i++)
+ {
+ GuildMember member = membersChunk.Members[i];
+ member.GuildId = membersChunk.Guild;
+ ShardManager_OnGuildMemberAdd(shard, member);
+ }
+ }
+
+ private void ShardManager_OnMessageCreate(GatewayShard shard, Message message)
+ {
+ DiscordChannel channel = channels.Find(x => x.Id == message.ChannelId);
+ DiscordMember member = members.Find(x => x.User.Id == message.User.Id);
+ DiscordMessage msg = new DiscordMessage(this, message, member, channel);
+ messages.Add(msg);
+ OnMessageCreate?.Invoke(msg);
+ }
+
+ private void ShardManager_OnMessageDelete(GatewayShard shard, Message message)
+ {
+ DiscordMessage msg = messages.Find(x => x.Id == message.Id);
+ messages.Remove(msg);
+ OnMessageDelete?.Invoke(msg);
+ }
+
+ private void ShardManager_OnMessageUpdate(GatewayShard shard, Message message)
+ {
+ DiscordMessage msg = messages.Find(x => x.Id == message.Id);
+
+ msg.Edited = DateTime.Now;
+
+ if (!string.IsNullOrEmpty(message.Content))
+ msg.Text = message.Content;
+
+ /*existing.IsTTS = message.IsTTS == true;
+ existing.IsPinned = message.IsPinned == true;*/
+
+ OnMessageUpdate?.Invoke(msg);
+ }
+
+ private void ShardManager_OnTypingStart(GatewayShard shard, TypingStart typing)
+ {
+ DiscordChannel channel = channels.Find(x => x.Id == typing.Channel);
+ DiscordMember member = members.Find(x => x.User.Id == typing.User && x.Server.Id == channel.Server.Id);
+ OnTypingStart?.Invoke(member, channel);
+ }
+
+ private void ShardManager_OnPresenceUpdate(GatewayShard shard, Presence presence)
+ {
+ /*$"User Id: {presence.User.Id}",
+ $"Roles: " + (presence.Roles != null ? string.Join(", ", presence.Roles) : string.Empty),
+ "Game: " + (presence.Game.HasValue ? presence.Game.Value.Name : string.Empty),
+ $"Guild Id: {presence.Guild}",
+ $"Status: {presence.Status}"*/
+
+ DiscordMember member = members.Find(x => x.User.Id == presence.User.Id && x.Server.Id == presence.Guild);
+
+ // update user presence
+
+ OnPresenceUpdate?.Invoke(member);
+ }
+
+ private void ShardManager_OnReady(GatewayShard shard, GatewayReady ready)
+ {
+ foreach (Guild guild in ready.UnavailableGuilds)
+ // should this call an event handler?
+ ShardManager_OnGuildCreate(shard, guild);
+
+ /* not ready for these yet
+ foreach (Channel channel in ready.PrivateChannels)
+ ShardManager_OnChannelCreate(shard, channel);*/
+
+ // keep track of self
+
+ /*$"Version: {ready.Version}",
+ $"User: {ready.User.Id}, {ready.User.Username}, {ready.User.EMail}",
+ $"Private Channels: {string.Join(", ", ready.PrivateChannels.Select(x => x.Id).ToArray())}",
+ $"Unavailable Guilds: {string.Join(", ", ready.UnavailableGuilds.Select(x => x.Id).ToArray())}",
+ $"Session: {ready.Session}"*/
+
+ DiscordUser user = new DiscordUser(this, ready.User);
+ users.Add(user);
+ OnReady?.Invoke(user);
+ }
+
+ private void ShardManager_OnResumed(GatewayShard shard)
+ {
+ OnReady?.Invoke(Me);
+ }
+
+ private void ShardManager_OnUserUpdate(GatewayShard shard, User sUser)
+ {
+ /*$"Id: {user.Id}",
+ $"Guild Id: {user.GuildId}",
+ $"Username: {user.Username}",
+ $"Tag: {user.Tag:0000}",
+ $"Avatar Hash: {user.AvatarHash}",
+ $"Is bot?: {user.IsBot}",
+ $"Has MFA?: {user.HasMFA}",
+ $"Is verified?: {user.IsVerified}",
+ $"E-mail: {user.EMail}"*/
+
+ DiscordUser user = users.Where(x => x.Id == sUser.Id).FirstOrDefault();
+
+ if (user == default(DiscordUser))
+ {
+ user = new DiscordUser(this, sUser);
+ users.Add(user);
+ }
+
+ OnUserUpdate?.Invoke(user);
+ }
+
+ private void ShardManager_OnSocketOpen(GatewayShard shard)
+ {
+ //MultiLineWrite($"Connection opened on shard {shard.Id}");
+ }
+
+ private void ShardManager_OnSocketClose(GatewayShard shard, bool wasClean, ushort code, string reason)
+ {
+ //MultiLineWrite($"Connection closed on shard {shard.Id} ({wasClean}/{code}/{reason})");
+ }
+
+ private void ShardManager_OnSocketError(GatewayShard shard, Exception ex)
+ {
+ //MultiLineWrite($"Socket error on shard {shard.Id}", ex.Message);
+ }
+
+ private void ShardManager_OnSocketMessage(GatewayShard shard, string text)
+ {
+ if (!Directory.Exists("Json"))
+ Directory.CreateDirectory("Json");
+
+ File.WriteAllText(
+ $"Json/{DateTime.Now:yyyy-MM-dd HH-mm-ss.fffffff}.json",
+ JsonConvert.SerializeObject(
+ JsonConvert.DeserializeObject(text),
+ Formatting.Indented
+ )
+ );
+ }
+
+ #region IDisposable
+
+ private bool IsDisposed = false;
+
+ private void Dispose(bool disposing)
+ {
+ if (!IsDisposed) {
+ IsDisposed = true;
+ Disconnect();
+ }
+ }
+
+ ~Discord()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Disconnects and releases all unmanaged objects
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(true);
+ }
+
+ #endregion
+ }
+}
diff --git a/Maki/DiscordChannel.cs b/Maki/DiscordChannel.cs
new file mode 100644
index 0000000..2786c96
--- /dev/null
+++ b/Maki/DiscordChannel.cs
@@ -0,0 +1,56 @@
+using Maki.Rest;
+using Maki.Structures.Channels;
+using Maki.Structures.Messages;
+using Maki.Structures.Rest;
+
+namespace Maki
+{
+ public class DiscordChannel
+ {
+ public readonly ulong Id;
+ public readonly DiscordServer Server;
+ private readonly Discord client;
+
+ public string Name { get; internal set; }
+
+ internal DiscordChannel(Discord discord, Channel channel, DiscordServer server)
+ {
+ client = discord;
+ Id = channel.Id;
+ Name = channel.Name;
+ Server = server;
+ }
+
+ public DiscordMessage Send(string text)
+ {
+ RestResponse msg = client.RestClient.Request(
+ RestRequestMethod.POST,
+ RestEndpoints.ChannelMessages(Id),
+ new MessageCreate {
+ Text = text,
+ }
+ );
+
+ DiscordMessage message = new DiscordMessage(client, msg.Response, client.members.Find(x => x.User.Id == msg.Response.User.Id), this);
+ client.messages.Add(message);
+ return message;
+ }
+
+ public DiscordMessage Send(DiscordEmbed embed)
+ {
+ RestResponse msg = client.RestClient.Request(
+ RestRequestMethod.POST,
+ RestEndpoints.ChannelMessages(Id),
+ new MessageCreate
+ {
+ Text = string.Empty,
+ Embed = embed.ToStruct(),
+ }
+ );
+
+ DiscordMessage message = new DiscordMessage(client, msg.Response, client.members.Find(x => x.User.Id == msg.Response.User.Id), this);
+ client.messages.Add(message);
+ return message;
+ }
+ }
+}
diff --git a/Maki/DiscordColour.cs b/Maki/DiscordColour.cs
new file mode 100644
index 0000000..323538a
--- /dev/null
+++ b/Maki/DiscordColour.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Globalization;
+
+namespace Maki
+{
+ public class DiscordColour
+ {
+ public DiscordColour(uint raw)
+ {
+ Raw = raw;
+ }
+
+ public DiscordColour(byte red, byte green, byte blue)
+ {
+ Raw = (uint)((red << 16) + (green << 8) + blue);
+ }
+
+ public DiscordColour(string hex)
+ {
+ hex = hex.ToUpper();
+
+ if (hex.Length == 3)
+ hex = new string(hex[0], 2) + new string(hex[1], 2) + new string(hex[2], 2);
+
+ if (hex.Length != 6)
+ throw new FormatException("Invalid hex colour format!");
+
+ Red = byte.Parse(hex.Substring(0, 2), NumberStyles.HexNumber);
+ Green = byte.Parse(hex.Substring(2, 2), NumberStyles.HexNumber);
+ Blue = byte.Parse(hex.Substring(4, 2), NumberStyles.HexNumber);
+ }
+
+ public uint Raw = uint.MinValue;
+
+ public byte Red
+ {
+ get => (byte)(Raw >> 16 & 0xFF);
+ set => Raw = (uint)((value << 16) + (Green << 8) + Blue);
+ }
+
+ public byte Green
+ {
+ get => (byte)(Raw >> 8 & 0xFF);
+ set => Raw = (uint)((Red << 16) + (value << 8) + Blue);
+ }
+
+ public byte Blue
+ {
+ get => (byte)(Raw & 0xFF);
+ set => Raw = (uint)((Red << 16) + (Green << 8) + value);
+ }
+
+ public string Hex => $"{Red:X2}{Green:X2}{Blue:X2}";
+ }
+}
diff --git a/Maki/DiscordEmbed.cs b/Maki/DiscordEmbed.cs
new file mode 100644
index 0000000..8640fbd
--- /dev/null
+++ b/Maki/DiscordEmbed.cs
@@ -0,0 +1,60 @@
+using Maki.Structures.Embeds;
+using System;
+using System.Collections.Generic;
+
+namespace Maki
+{
+ public class DiscordEmbed
+ {
+ public string Title;
+ public string Description;
+ public string Url;
+ public DateTime DateTime = DateTime.MinValue;
+ public DiscordColour Colour;
+ public DiscordEmbedAuthor Author;
+ public DiscordEmbedField[] Fields;
+ public DiscordEmbedFooter Footer;
+ public DiscordEmbedImage Image;
+ public DiscordEmbedThumbnail Thumbnail;
+
+ internal Embed ToStruct()
+ {
+ Embed embed = new Embed() {
+ Title = Title,
+ Description = Description,
+ Url = Url,
+ };
+
+ if (DateTime != null && DateTime != DateTime.MinValue)
+ embed.Timestamp = DateTime;
+
+ if (Colour != null)
+ embed.Colour = Colour.Raw;
+
+ if (Author != null)
+ embed.Author = Author.ToStruct();
+
+ if (Fields != null && Fields.Length > 0)
+ {
+ List fields = new List();
+
+ foreach (DiscordEmbedField field in Fields)
+ if (!string.IsNullOrEmpty(field.Name) && !string.IsNullOrEmpty(field.Value))
+ fields.Add(field.ToStruct());
+
+ embed.Fields = fields.ToArray();
+ }
+
+ if (Footer != null)
+ embed.Footer = Footer.ToStruct();
+
+ if (Image != null)
+ embed.Image = Image.ToStruct();
+
+ if (Thumbnail != null)
+ embed.Thumbnail = Thumbnail.ToStruct();
+
+ return embed;
+ }
+ }
+}
diff --git a/Maki/DiscordEmbedAuthor.cs b/Maki/DiscordEmbedAuthor.cs
new file mode 100644
index 0000000..9888531
--- /dev/null
+++ b/Maki/DiscordEmbedAuthor.cs
@@ -0,0 +1,20 @@
+using Maki.Structures.Embeds;
+
+namespace Maki
+{
+ public class DiscordEmbedAuthor
+ {
+ public string Name;
+ public string Url;
+ public string Icon;
+
+ internal EmbedAuthor ToStruct()
+ {
+ return new EmbedAuthor() {
+ Name = Name,
+ Url = Url,
+ IconUrl = Icon,
+ };
+ }
+ }
+}
diff --git a/Maki/DiscordEmbedField.cs b/Maki/DiscordEmbedField.cs
new file mode 100644
index 0000000..389b48e
--- /dev/null
+++ b/Maki/DiscordEmbedField.cs
@@ -0,0 +1,21 @@
+using Maki.Structures.Embeds;
+
+namespace Maki
+{
+ public class DiscordEmbedField
+ {
+ public string Name;
+ public string Value;
+ public bool Inline = false;
+
+ internal EmbedField ToStruct()
+ {
+ return new EmbedField()
+ {
+ Name = Name,
+ Value = Value,
+ Inline = Inline
+ };
+ }
+ }
+}
diff --git a/Maki/DiscordEmbedFooter.cs b/Maki/DiscordEmbedFooter.cs
new file mode 100644
index 0000000..fca84fa
--- /dev/null
+++ b/Maki/DiscordEmbedFooter.cs
@@ -0,0 +1,19 @@
+using Maki.Structures.Embeds;
+
+namespace Maki
+{
+ public class DiscordEmbedFooter
+ {
+ public string Text;
+ public string Icon;
+
+ internal EmbedFooter ToStruct()
+ {
+ return new EmbedFooter()
+ {
+ Text = Text,
+ IconUrl = Icon,
+ };
+ }
+ }
+}
diff --git a/Maki/DiscordEmbedImage.cs b/Maki/DiscordEmbedImage.cs
new file mode 100644
index 0000000..6fab723
--- /dev/null
+++ b/Maki/DiscordEmbedImage.cs
@@ -0,0 +1,24 @@
+using Maki.Structures.Embeds;
+
+namespace Maki
+{
+ public class DiscordEmbedImage
+ {
+ public string Url;
+
+ internal EmbedImage ToStruct()
+ {
+ return new EmbedImage()
+ {
+ Url = Url,
+ };
+ }
+ }
+
+ ///
+ /// An alias of DiscordEmbedImage, for aesthetic reasons
+ ///
+ public class DiscordEmbedThumbnail : DiscordEmbedImage
+ {
+ }
+}
diff --git a/Maki/DiscordMember.cs b/Maki/DiscordMember.cs
new file mode 100644
index 0000000..5c3f561
--- /dev/null
+++ b/Maki/DiscordMember.cs
@@ -0,0 +1,49 @@
+using Maki.Rest;
+using Maki.Structures.Guilds;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Maki
+{
+ public class DiscordMember
+ {
+ public readonly DiscordUser User;
+ public readonly DiscordServer Server;
+ private readonly Discord client;
+
+ public DiscordRole[] Roles => client.roles.Where(x => HasRole(x.Id)).ToArray();
+
+ public DateTime Joined { get; internal set; }
+ public string Nickname { get; internal set; }
+ public string Name => string.IsNullOrEmpty(Nickname) ? User.Username : Nickname;
+ public string NameWithTag => $"{Name}#{User.Tag:0000}";
+ public override string ToString() => User.ToString();
+
+ private List roles = new List();
+
+ internal DiscordMember(Discord discord, GuildMember member, DiscordUser user, DiscordServer server)
+ {
+ client = discord;
+ User = user;
+ Server = server;
+ Nickname = member.Nickname;
+ Joined = member.JoinedAt ?? DateTime.MinValue;
+ roles.AddRange(member.Roles);
+ }
+
+ public bool HasRole(ulong id)
+ {
+ return roles.Contains(id);
+ }
+
+ public void AddRoles(params DiscordRole[] roles)
+ {
+ foreach (DiscordRole role in roles)
+ {
+ client.RestClient.Request