using Maki.Gateway; using Maki.Rest; 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 System; using System.Collections.Generic; using System.Diagnostics; 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; #region Token /// /// Discord token /// 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; } /// /// Gateway shard client /// internal readonly GatewayShardClient ShardClient; #region Containers internal readonly ServerManager ServerManager; /// /// Servers we're in /// public IEnumerable Servers => ServerManager.Items; internal readonly UserManager UserManager; /// /// Users we are affiliated with /// public IEnumerable Users => UserManager.Items; internal readonly MemberManager MemberManager; /// /// Members of servers, different from users! /// public IEnumerable Members => MemberManager.Items; internal readonly RoleManager RoleManager; /// /// Roles in servers /// public IEnumerable Roles => RoleManager.Items; internal readonly ChannelManager ChannelManager; /// /// Channels /// public IEnumerable Channels => ChannelManager.Items; internal readonly MessageManager MessageManager; /// /// Messages /// public IEnumerable Messages => MessageManager.Items; #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 DiscordUser Me => UserManager.Items.FirstOrDefault(); private DiscordParams Params; /// /// Constructor /// public Discord(DiscordParams parameters = null) { Params = parameters ?? new DiscordParams(); ShardClient = new GatewayShardClient(this); UserManager = new UserManager(); RoleManager = new RoleManager(); MemberManager = new MemberManager(); ChannelManager = new ChannelManager(); MessageManager = new MessageManager(); ServerManager = new ServerManager(); #region Assigning event handlers 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; #endregion } /// /// Connects to Discord using the token assigned to Token /// public void Connect() { int shards = 1; using (WebRequest wr = new WebRequest(HttpMethod.GET, IsBot ? RestEndpoints.GatewayBot : RestEndpoints.Gateway)) { wr.Perform(); 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; } for (int i = 0; i < shards; i++) ShardClient.Create(i); } /// /// 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(); Token = string.Empty; Gateway = string.Empty; } #region Event Handlers private void ShardManager_OnChannelCreate(GatewayShard shard, Channel channel) { Debug.Assert(channel.GuildId.HasValue, "Guild ID does not have a value?"); DiscordServer server = ServerManager.Id(channel.GuildId ?? 0); Debug.Assert(server != null, "Target guild/server was not present in ServerManager"); DiscordChannel chan = new DiscordChannel(this, channel, server); ChannelManager.Add(chan); OnChannelCreate?.Invoke(chan); } private void ShardManager_OnChannelUpdate(GatewayShard shard, Channel channel) { DiscordChannel chan = ChannelManager.Id(channel.Id); Debug.Assert(chan != null, "channel is null"); 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 = ChannelManager.Id(channel.Id); Debug.Assert(chan != null, "channel is null"); ChannelManager.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 = UserManager.Id(sUser.Id); Debug.Assert(user != null, "user is null"); Debug.Assert(sUser.GuildId.HasValue, "Guild ID does not have a value."); DiscordServer server = ServerManager.Id(sUser.GuildId ?? 0); Debug.Assert(user != null, "user is null"); Debug.Assert(server != null, "server is null"); 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 = UserManager.Id(sUser.Id); Debug.Assert(user != null, "user is null"); DiscordServer server = ServerManager.Id(sUser.GuildId ?? 0); Debug.Assert(user != null, "user is null"); Debug.Assert(server != null, "server is null"); OnBanRemove?.Invoke(user, server); } private void ShardManager_OnGuildCreate(GatewayShard shard, Guild guild) { DiscordServer server = ServerManager.Id(guild.Id); if (server == null) { server = new DiscordServer(this, guild); ServerManager.Add(server); } else { server.Name = guild.Name; server.OwnerId = guild.OwnerId ?? 0; server.IconHash = guild.IconHash; server.Created = guild.Created ?? DateTime.MinValue; } if (guild.Channels != null) for (int i = 0; i < guild.Channels.Length; i++) { Channel channel = guild.Channels[i]; channel.GuildId = server.Id; if (ChannelManager.Exists(channel.Id)) 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 (RoleManager.Exists(role.Id)) 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 = ServerManager.Id(guild.Id); Debug.Assert(server != null, "server is null"); 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); // NOT THREAD SAFE AT ALL MessageManager .Server(server).ToList().ForEach(x => MessageManager.Remove(x)); ChannelManager .Server(server).ToList().ForEach(x => ChannelManager.Remove(x)); MemberManager .Server(server).ToList().ForEach(x => MemberManager.Remove(x)); RoleManager .Server(server).ToList().ForEach(x => RoleManager.Remove(x)); ServerManager.Remove(server); } private void ShardManager_OnGuildUpdate(GatewayShard shard, Guild guild) { DiscordServer server = ServerManager.Id(guild.Id); Debug.Assert(server != null, "server is null"); 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 = ServerManager.Id(guild.Id); Debug.Assert(server != null, "server is null"); //servers.Emojis = guild.Emojis; OnEmojisUpdate?.Invoke(server); } private void ShardManager_OnGuildMemberAdd(GatewayShard shard, GuildMember sMember) { Debug.Assert(sMember.GuildId.HasValue, "GuildId has no value"); DiscordServer server = ServerManager.Id(sMember.GuildId ?? 0); Debug.Assert(server != null, "server is null"); DiscordUser user = UserManager.Id(sMember.User.Id); //Debug.Assert(user != null, "user is null"); if (user == null) { user = new DiscordUser(this, sMember.User); UserManager.Add(user); } DiscordMember member = new DiscordMember(this, sMember, user, server); MemberManager.Add(member); OnMemberAdd?.Invoke(member); } private void ShardManager_OnGuildMemberUpdate(GatewayShard shard, GuildMember sMember) { Debug.Assert(sMember.GuildId.HasValue, "GuildId has no value"); DiscordMember member = MemberManager.Id(sMember.GuildId ?? 0, sMember.User.Id); Debug.Assert(member != null, "member is null"); 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) { Debug.Assert(sMember.GuildId.HasValue, "GuildId has no value"); DiscordMember member = MemberManager.Id(sMember.GuildId ?? 0, sMember.User.Id); Debug.Assert(member != null, "member is null"); MemberManager.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 = ServerManager.Id(sRole.Guild); Debug.Assert(server != null, "server is null"); DiscordRole role = RoleManager.Id(sRole.Role.Value.Id); //Debug.Assert(role != null, "role is null"); // fixme if (server == null) return; if (role == null) { role = new DiscordRole(this, sRole.Role.Value, server); RoleManager.Add(role); } else { role.Colour.Raw = sRole.Role.Value.Colour.Value; } OnRoleCreate?.Invoke(role); } private void ShardManager_OnGuildRoleDelete(GatewayShard shard, GuildRole sRole) { Debug.Assert(sRole.RoleId.HasValue, "RoleId has no value"); DiscordRole role = RoleManager.Id(sRole.RoleId ?? 0); Debug.Assert(role != null, "role is null"); // NOT THREAD SAFE MemberManager.Role(role).ToList().ForEach(x => x.roles.RemoveAll(y => y == role.Id)); RoleManager.Remove(role); OnRoleDelete?.Invoke(role); } private void ShardManager_OnGuildRoleUpdate(GatewayShard shard, GuildRole sRole) { Debug.Assert(sRole.Role.HasValue, "Role has no value"); Role dRole = sRole.Role.Value; DiscordRole role = RoleManager.Id(dRole.Id); Debug.Assert(role != null, "role is null"); 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 = ChannelManager.Id(message.ChannelId); Debug.Assert(channel != null, "channel is null"); DiscordMember member = MemberManager.Id(channel.Server, message.User.Id); Debug.Assert(member != null, "member is null"); DiscordMessage msg = new DiscordMessage(this, message, channel, member); MessageManager.Add(msg); OnMessageCreate?.Invoke(msg); } private void ShardManager_OnMessageDelete(GatewayShard shard, Message message) { DiscordMessage msg = MessageManager.Id(message.Id); Debug.Assert(msg != null, "message is null"); MessageManager.Remove(msg); OnMessageDelete?.Invoke(msg); } private void ShardManager_OnMessageUpdate(GatewayShard shard, Message message) { DiscordMessage msg = MessageManager.Id(message.Id); //Debug.Assert(msg != null, "message is null"); // basically what should happen if (msg == null) { using (WebRequest wr = new WebRequest(HttpMethod.GET, RestEndpoints.ChannelMessage(message.ChannelId, message.Id))) { wr.Perform(); if (wr.Status != 200 || wr.ResponseString.Length < 1) throw new DiscordException("Failed to load message from API"); message = wr.ResponseJson(); } DiscordChannel channel = ChannelManager.Id(message.ChannelId); Debug.Assert(channel != null, "channel is null"); DiscordMember member = MemberManager.Id(channel.Server, message.User.Id) ?? MemberManager.Id(message.User.Id); Debug.Assert(member != null, "member is null"); msg = new DiscordMessage(this, message, channel, member); MessageManager.Add(msg); } else { if (!string.IsNullOrEmpty(message.Content)) msg.Text = message.Content; msg.IsPinned = message.IsPinned == true; } msg.Edited = DateTime.Now; OnMessageUpdate?.Invoke(msg); } private void ShardManager_OnTypingStart(GatewayShard shard, TypingStart typing) { DiscordChannel channel = ChannelManager.Id(typing.Channel); Debug.Assert(channel != null, "channel is null"); DiscordUser user = UserManager.Id(typing.User); Debug.Assert(user != null, "user is null"); OnTypingStart?.Invoke(user, channel); } private void ShardManager_OnPresenceUpdate(GatewayShard shard, Presence presence) { DiscordMember member = MemberManager.Id(presence.Guild, presence.User.Id); Debug.Assert(member != null, "member is null"); if (!string.IsNullOrEmpty(presence.User.Username)) member.User.Username = presence.User.Username; if (presence.User.Tag.HasValue) member.User.Tag = presence.User.Tag.Value; if (!string.IsNullOrEmpty(presence.User.AvatarHash)) member.User.avatarHash = presence.User.AvatarHash; 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 (ChannelManager.Exists(channel.Id)) continue; ChannelManager.Add(channel); } foreach (Guild guild in ready.UnavailableGuilds) { DiscordServer server = new DiscordServer(this, guild); if (ServerManager.Exists(server.Id)) continue; ServerManager.Add(server); } DiscordUser user = new DiscordUser(this, ready.User); UserManager.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 = UserManager.Id(sUser.Id); //Debug.Assert(user != null, "user is null"); if (user == null) { user = new DiscordUser(this, sUser); UserManager.Add(user); } OnUserUpdate?.Invoke(user); } private void ShardManager_OnSocketError(GatewayShard shard, Exception ex) { } #endregion #region IDisposable private bool IsDisposed = false; /// /// Disconnects and releases all unmanaged objects /// private void Dispose(bool disposing) { if (IsDisposed) return; IsDisposed = true; Disconnect(); ServerManager.Dispose(); MessageManager.Dispose(); ChannelManager.Dispose(); MemberManager.Dispose(); RoleManager.Dispose(); UserManager.Dispose(); if (disposing) GC.SuppressFinalize(this); } ~Discord() => Dispose(false); public void Dispose() => Dispose(true); #endregion } }