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 System;
using System.Collections.Generic;
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();
private DiscordParams Params;
///
/// Constructor
///
public Discord(DiscordParams parameters = null)
{
Params = parameters ?? new DiscordParams();
RestClient = new RestClient(this);
ShardClient = new GatewayShardClient(this);
#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;
ShardClient.OnSocketOpen += ShardManager_OnSocketOpen;
ShardClient.OnSocketClose += ShardManager_OnSocketClose;
ShardClient.OnSocketError += ShardManager_OnSocketError;
ShardClient.OnSocketMessage += ShardManager_OnSocketMessage;
#endregion
}
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 DiscordException($"{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 DiscordException($"{login.ErrorCode}: {login.ErrorMessage}");
if (login.Response.UsernameError?.Length > 0)
throw new DiscordException(login.Response.UsernameError.FirstOrDefault());
if (login.Response.PasswordError?.Length > 0)
throw new DiscordException(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 DiscordException($"{totp.ErrorCode}: {totp.ErrorMessage}");
Token = totp.Response.Token;
}
else
throw new DiscordException("Token was null but MFA is false and/or ticket is empty?");
}
if (string.IsNullOrEmpty(Token))
throw new DiscordException("Authentication failed!");
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;
}
#region Event Handlers
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);
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 (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)
{
}
private void ShardManager_OnSocketClose(GatewayShard shard, bool wasClean, ushort code, string reason)
{
}
private void ShardManager_OnSocketError(GatewayShard shard, Exception ex)
{
}
private void ShardManager_OnSocketMessage(GatewayShard shard, string text)
{
}
#endregion
#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
}
}