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 { // temporary, whether gateway data should be written to a file public bool LogGateway = false; // temporary, whether rest data should be written to the console public bool LogRest = false; /// /// 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; if (chan.Type == DiscordChannelType.Private) { // update recipients } else { // update permissions too chan.Position = channel.Position ?? 0; } if (chan.Type == DiscordChannelType.Voice) { chan.Bitrate = channel.Bitrate ?? 0; chan.UserLimit = channel.UserLimit ?? 0; } else { chan.Topic = channel.Topic; chan.LastMessageId = channel.LastMessageId ?? 0; } OnChannelUpdate?.Invoke(chan); } private void ShardManager_OnChannelDelete(GatewayShard shard, Channel channel) { DiscordChannel chan = channels.Find(x => x.Id == channel.Id); 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 { server = servers.Find(x => x.Id == guild.Id); server.Name = guild.Name; server.OwnerId = guild.OwnerId ?? 0; server.IconHash = guild.IconHash; } 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 ?? 0; /*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 ?? 0; server.IconHash = guild.IconHash; /*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); member.Nickname = sMember.Nickname; member.IsDeaf = sMember.IsDeafened == true; member.IsMute = sMember.IsMuted == true; member.roles = new List(sMember.Roles); 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 = roles.Where(x => x.Id == sRole.Role.Value.Id).FirstOrDefault(); if (role == default(DiscordRole)) { role = new DiscordRole(this, sRole.Role.Value, server); roles.Add(role); } else { role.Colour.Raw = sRole.Role.Value.Colour.Value; } 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); members.Where(x => x.roles.Contains(role.Id)).ToList().ForEach(x => x.roles.RemoveAll(y => y == role.Id)); 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; role.IsHoisted = dRole.IsHoisted == true; role.IsMentionable = dRole.IsMentionable == true; role.Perms = (DiscordPermission)dRole.Permissions; role.Position = dRole.Position ?? 0; role.Colour.Raw = dRole.Colour ?? 0; 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) { 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.Where(x => x.User.Id == message.User.Id && x.Server == channel.Server).FirstOrDefault(); DiscordMessage msg = new DiscordMessage(this, message, channel, member); 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.Where(x => x.Id == message.Id).FirstOrDefault(); 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); messages.Add(msg); } msg.Edited = DateTime.Now; if (!string.IsNullOrEmpty(message.Content)) msg.Text = message.Content; msg.IsPinned = message.IsPinned == true; OnMessageUpdate?.Invoke(msg); } private void ShardManager_OnTypingStart(GatewayShard shard, TypingStart typing) { DiscordChannel channel = channels.Find(x => x.Id == typing.Channel); DiscordUser user = users.Find(x => x.Id == typing.User); OnTypingStart?.Invoke(user, channel); } private void ShardManager_OnPresenceUpdate(GatewayShard shard, Presence presence) { DiscordMember member = members.Find(x => x.User.Id == presence.User.Id && x.Server.Id == presence.Guild); member.User.Game = presence.Game.HasValue ? new DiscordGame(presence.Game.Value) : null; if (presence.Roles != null) member.roles = new List(presence.Roles); switch (presence.Status.ToLower()) { case @"online": member.User.Status = DiscordUserStatus.Online; break; case @"away": case @"idle": member.User.Status = DiscordUserStatus.Idle; break; case @"dnd": member.User.Status = DiscordUserStatus.DoNotDisturb; break; case @"offline": default: member.User.Status = DiscordUserStatus.Offline; break; } OnPresenceUpdate?.Invoke(member); } private void ShardManager_OnReady(GatewayShard shard, GatewayReady ready) { foreach (Channel chan in ready.PrivateChannels) { DiscordChannel channel = new DiscordChannel(this, chan); // this shouldn't ever happen but just in case if (channels.Where(x => x.Id == channel.Id).Count() > 0) continue; channels.Add(channel); } foreach (Guild guild in ready.UnavailableGuilds) { DiscordServer server = new DiscordServer(this, guild); if (servers.Where(x => x.Id == server.Id).Count() > 0) continue; servers.Add(server); } 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 (LogGateway) { 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 } }