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); } // TODO: account for DMs 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 } }