Archived
1
0
Fork 0

Compare commits

...

10 commits

32 changed files with 1438 additions and 1405 deletions

64
Maki/BaseManager.cs Normal file
View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Maki
{
internal abstract class BaseManager<T> : IDisposable
{
protected readonly HashSet<T> Collection = new HashSet<T>();
public IEnumerable<T> Items => Collection;
public int Count => Collection.Count;
public event Action<T> OnAdd;
public event Action<T> OnRemove;
protected virtual bool DisposeOnRemove => true;
protected bool IsDisposed = false;
~BaseManager()
=> Dispose(false);
public void Dispose()
=> Dispose(true);
protected void Dispose(bool disposing)
{
if (IsDisposed)
return;
IsDisposed = true;
Collection.ToList().ForEach(x => Remove(x));
Collection.Clear();
if (disposing)
GC.SuppressFinalize(this);
}
public virtual void Add(T item)
{
if (item == null)
return;
lock (Collection)
Collection.Add(item);
OnAdd?.Invoke(item);
}
public virtual void Remove(T item)
{
if (item == null)
return;
lock (Collection)
Collection.Remove(item);
OnRemove?.Invoke(item);
if (DisposeOnRemove && item is IDisposable)
(item as IDisposable).Dispose();
}
}
}

30
Maki/ChannelManager.cs Normal file
View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
namespace Maki
{
internal sealed class ChannelManager : BaseManager<DiscordChannel>
{
internal ChannelManager()
{
}
public DiscordChannel Id(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).FirstOrDefault();
}
public bool Exists(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).Count() > 0;
}
public IEnumerable<DiscordChannel> Server(DiscordServer server)
{
lock (Collection)
return Collection.Where(x => x.Server == server).OrderBy(x => x.Position);
}
}
}

View file

@ -1,6 +1,5 @@
using Maki.Gateway;
using Maki.Rest;
using Maki.Structures.Auth;
using Maki.Structures.Channels;
using Maki.Structures.Gateway;
using Maki.Structures.Guilds;
@ -10,6 +9,7 @@ using Maki.Structures.Roles;
using Maki.Structures.Users;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Maki
@ -64,35 +64,49 @@ namespace Maki
internal readonly GatewayShardClient ShardClient;
#region Containers
internal readonly ServerManager ServerManager;
/// <summary>
/// Servers we're in
/// </summary>
internal readonly List<DiscordServer> servers = new List<DiscordServer>();
public IEnumerable<DiscordServer> Servers => ServerManager.Items;
internal readonly UserManager UserManager;
/// <summary>
/// Users we are affiliated with
/// </summary>
internal readonly List<DiscordUser> users = new List<DiscordUser>();
public IEnumerable<DiscordUser> Users => UserManager.Items;
internal readonly MemberManager MemberManager;
/// <summary>
/// Members of servers, different from users!
/// </summary>
internal readonly List<DiscordMember> members = new List<DiscordMember>();
public IEnumerable<DiscordMember> Members => MemberManager.Items;
internal readonly RoleManager RoleManager;
/// <summary>
/// Roles in servers
/// </summary>
internal readonly List<DiscordRole> roles = new List<DiscordRole>();
public IEnumerable<DiscordRole> Roles => RoleManager.Items;
internal readonly ChannelManager ChannelManager;
/// <summary>
/// Channels
/// </summary>
internal readonly List<DiscordChannel> channels = new List<DiscordChannel>();
public IEnumerable<DiscordChannel> Channels => ChannelManager.Items;
internal readonly MessageManager MessageManager;
/// <summary>
/// Messages
/// </summary>
internal readonly List<DiscordMessage> messages = new List<DiscordMessage>();
public IEnumerable<DiscordMessage> Messages => MessageManager.Items;
#endregion
#region Events
@ -207,8 +221,7 @@ namespace Maki
public event Action<DiscordUser> OnUserUpdate;
#endregion
public DiscordServer[] Servers => servers.ToArray();
public DiscordUser Me => users.FirstOrDefault();
public DiscordUser Me => UserManager.Items.FirstOrDefault();
private DiscordParams Params;
@ -220,6 +233,13 @@ namespace Maki
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;
@ -251,31 +271,14 @@ namespace Maki
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();
}
/// <summary>
/// Connects to Discord using the token assigned to Token
/// </summary>
public void Connect()
{
ClearContainers();
int shards = 1;
using (WebRequest wr = new WebRequest(HttpMethod.GET, IsBot ? RestEndpoints.GatewayBot : RestEndpoints.Gateway))
@ -292,87 +295,11 @@ namespace Maki
if (gi.Shards.HasValue)
shards = gi.Shards.Value;
}
if (Gateway.Contains("?"))
Gateway = Gateway.Substring(0, Gateway.IndexOf('?'));
if (!Gateway.EndsWith("/"))
Gateway += "/";
for (int i = 0; i < shards; i++)
ShardClient.Create(i);
}
/// <summary>
/// Connects to Discord with the provided email, password and, optionally, mfa token
/// </summary>
/// <param name="email">Discord account email</param>
/// <param name="password">Discord account password</param>
/// <param name="code">Multi factor authentication token</param>
public void Connect(string email, string password, string code = null)
{
// TODO: not tested with new request backend yet
TokenType = DiscordTokenType.User;
LoginResponse login;
using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.AuthLogin))
{
wr.AddJson(new LoginRequest
{
Email = email,
Password = password,
});
wr.Perform();
if (wr.Response.Length < 1)
throw new DiscordException("Login failed");
login = wr.ResponseJson<LoginResponse>();
}
if (login.UsernameError?.Length > 0)
throw new DiscordException(login.UsernameError.FirstOrDefault());
if (login.PasswordError?.Length > 0)
throw new DiscordException(login.PasswordError.FirstOrDefault());
Token = login.Token;
if (string.IsNullOrEmpty(Token))
{
if (login.MFA == true && !string.IsNullOrEmpty(login.Ticket))
{
using (WebRequest wr = new WebRequest(HttpMethod.POST, RestEndpoints.AuthMfaTotp))
{
wr.AddJson(new LoginMultiFactorAuth
{
Code = code,
Ticket = login.Ticket,
});
wr.Perform();
if (wr.Response.Length < 1)
throw new DiscordException("Multi factor auth failed");
login = wr.ResponseJson<LoginResponse>();
}
//if (totp.ErrorCode != RestErrorCode.Ok)
// throw new DiscordException($"{totp.ErrorCode}: {totp.ErrorMessage}");
Token = login.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();
}
/// <summary>
/// Connects to Discord using a provided token and token type
/// </summary>
@ -391,23 +318,27 @@ namespace Maki
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);
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);
channels.Add(chan);
ChannelManager.Add(chan);
OnChannelCreate?.Invoke(chan);
}
private void ShardManager_OnChannelUpdate(GatewayShard shard, Channel channel)
{
DiscordChannel chan = channels.Find(x => x.Id == channel.Id);
DiscordChannel chan = ChannelManager.Id(channel.Id);
Debug.Assert(chan != null, "channel is null");
chan.Name = channel.Name;
@ -435,8 +366,10 @@ namespace Maki
private void ShardManager_OnChannelDelete(GatewayShard shard, Channel channel)
{
DiscordChannel chan = channels.Find(x => x.Id == channel.Id);
channels.Remove(chan);
DiscordChannel chan = ChannelManager.Id(channel.Id);
Debug.Assert(chan != null, "channel is null");
ChannelManager.Remove(chan);
OnChannelDelete?.Invoke(chan);
}
@ -452,9 +385,15 @@ namespace Maki
$"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);
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);
}
@ -470,24 +409,28 @@ namespace Maki
$"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);
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 = servers.Where(x => x.Id == guild.Id).FirstOrDefault();
DiscordServer server = ServerManager.Id(guild.Id);
if (server == default(DiscordServer))
if (server == null)
{
server = new DiscordServer(this, guild);
servers.Add(server);
ServerManager.Add(server);
}
else
{
server = servers.Find(x => x.Id == guild.Id);
server.Name = guild.Name;
server.OwnerId = guild.OwnerId ?? 0;
server.IconHash = guild.IconHash;
@ -500,7 +443,7 @@ namespace Maki
Channel channel = guild.Channels[i];
channel.GuildId = server.Id;
if (channels.Where(x => x.Id == channel.Id).Count() > 0)
if (ChannelManager.Exists(channel.Id))
ShardManager_OnChannelUpdate(shard, channel);
else
ShardManager_OnChannelCreate(shard, channel);
@ -515,7 +458,7 @@ namespace Maki
RoleId = role.Id
};
if (roles.Where(x => x.Id == role.Id).Count() > 0)
if (RoleManager.Exists(role.Id))
ShardManager_OnGuildRoleUpdate(shard, gRole);
else
ShardManager_OnGuildRoleCreate(shard, gRole);
@ -532,7 +475,8 @@ namespace Maki
private void ShardManager_OnGuildDelete(GatewayShard shard, Guild guild)
{
DiscordServer server = servers.Find(x => x.Id == guild.Id);
DiscordServer server = ServerManager.Id(guild.Id);
Debug.Assert(server != null, "server is null");
server.Name = guild.Name;
server.OwnerId = guild.OwnerId ?? 0;
@ -549,16 +493,20 @@ namespace Maki
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);
// 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 = servers.Find(x => x.Id == guild.Id);
DiscordServer server = ServerManager.Id(guild.Id);
Debug.Assert(server != null, "server is null");
server.Name = guild.Name;
server.OwnerId = guild.OwnerId ?? 0;
@ -580,31 +528,41 @@ namespace Maki
private void ShardManager_OnGuildEmojisUpdate(GatewayShard shard, Guild guild)
{
DiscordServer server = servers.Find(x => x.Id == guild.Id);
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)
{
DiscordServer server = servers.Find(x => x.Id == sMember.GuildId);
DiscordUser user = users.Where(x => x.Id == sMember.User.Id).FirstOrDefault();
Debug.Assert(sMember.GuildId.HasValue, "GuildId has no value");
if (user == default(DiscordUser))
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);
users.Add(user);
UserManager.Add(user);
}
DiscordMember member = new DiscordMember(this, sMember, user, server);
members.Add(member);
MemberManager.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);
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;
@ -615,8 +573,12 @@ namespace Maki
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);
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);
}
@ -632,13 +594,20 @@ namespace Maki
$"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();
DiscordServer server = ServerManager.Id(sRole.Guild);
Debug.Assert(server != null, "server is null");
if (role == default(DiscordRole))
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);
roles.Add(role);
RoleManager.Add(role);
} else
{
role.Colour.Raw = sRole.Role.Value.Colour.Value;
@ -649,17 +618,25 @@ namespace Maki
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);
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 = roles.Find(x => x.Id == dRole.Id && x.Server.Id == sRole.Guild);
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;
@ -697,40 +674,52 @@ namespace Maki
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();
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);
messages.Add(msg);
MessageManager.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);
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 = messages.Where(x => x.Id == message.Id).FirstOrDefault();
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.Response.Length < 1)
if (wr.Status != 200 || wr.ResponseString.Length < 1)
throw new DiscordException("Failed to load message from API");
message = wr.ResponseJson<Message>();
}
DiscordChannel channel = channels.Where(x => x.Id == message.ChannelId).FirstOrDefault();
DiscordMember member = members.Where(x => x.User.Id == message.User.Id && (channel.Server == null || channel.Server == x.Server)).FirstOrDefault();
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);
messages.Add(msg);
MessageManager.Add(msg);
} else
{
if (!string.IsNullOrEmpty(message.Content))
@ -746,14 +735,19 @@ namespace Maki
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);
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 = members.Find(x => x.User.Id == presence.User.Id && x.Server.Id == presence.Guild);
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;
@ -771,20 +765,20 @@ namespace Maki
switch (presence.Status.ToLower())
{
case @"online":
case "online":
member.User.Status = DiscordUserStatus.Online;
break;
case @"away":
case @"idle":
case "away":
case "idle":
member.User.Status = DiscordUserStatus.Idle;
break;
case @"dnd":
case "dnd":
member.User.Status = DiscordUserStatus.DoNotDisturb;
break;
case @"offline":
case "offline":
default:
member.User.Status = DiscordUserStatus.Offline;
break;
@ -800,24 +794,24 @@ namespace Maki
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)
if (ChannelManager.Exists(channel.Id))
continue;
channels.Add(channel);
ChannelManager.Add(channel);
}
foreach (Guild guild in ready.UnavailableGuilds)
{
DiscordServer server = new DiscordServer(this, guild);
if (servers.Where(x => x.Id == server.Id).Count() > 0)
if (ServerManager.Exists(server.Id))
continue;
servers.Add(server);
ServerManager.Add(server);
}
DiscordUser user = new DiscordUser(this, ready.User);
users.Add(user);
UserManager.Add(user);
OnReady?.Invoke(user);
}
@ -838,58 +832,56 @@ namespace Maki
$"Is verified?: {user.IsVerified}",
$"E-mail: {user.EMail}"*/
DiscordUser user = users.Where(x => x.Id == sUser.Id).FirstOrDefault();
DiscordUser user = UserManager.Id(sUser.Id);
//Debug.Assert(user != null, "user is null");
if (user == default(DiscordUser))
if (user == null)
{
user = new DiscordUser(this, sUser);
users.Add(user);
UserManager.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);
}
private bool IsDisposed = false;
/// <summary>
/// Disconnects and releases all unmanaged objects
/// </summary>
public void Dispose()
private void Dispose(bool disposing)
{
Dispose(true);
GC.SuppressFinalize(true);
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
}
}

View file

@ -3,6 +3,7 @@ using Maki.Structures.Channels;
using Maki.Structures.Messages;
using Maki.Structures.Rest;
using Newtonsoft.Json;
using System.Diagnostics;
using System.IO;
namespace Maki
@ -39,14 +40,7 @@ namespace Maki
UserLimit = channel.UserLimit ?? 0;
Server = server;
}
public DiscordMessage Send(Stream stream, string filename, string text = "", DiscordEmbed embed = null)
{
byte[] bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
return Send(text, embed, filename, bytes);
}
public DiscordMessage Send(string text = "", DiscordEmbed embed = null, string filename = null, byte[] file = null)
{
Message? msg = null;
@ -58,25 +52,36 @@ namespace Maki
Text = text,
Embed = embed?.ToStruct(),
}));
if (file != null && !string.IsNullOrEmpty(filename))
wr.AddFile(filename, file);
wr.Perform();
if (wr.Status != 200 || wr.Response.Length < 1)
if (wr.Status != 200 || wr.ResponseString.Length < 1)
// TODO: elaborate
throw new DiscordException("Failed to send message");
//throw new DiscordException("Failed to send message");
return null;
msg = wr.ResponseJson<Message>();
}
if (!msg.HasValue)
throw new DiscordException("Empty response?");
DiscordMessage message = new DiscordMessage(client, msg.Value, this, client.members.Find(x => x.User.Id == msg.Value.User.Id));
client.messages.Add(message);
DiscordMember member = client.MemberManager.Id(Server, msg.Value.User.Id);
Debug.Assert(member != null, "member is null");
DiscordMessage message = new DiscordMessage(client, msg.Value, this, member);
client.MessageManager.Add(message);
return message;
}
public DiscordMessage Send(Stream stream, string filename, string text = "", DiscordEmbed embed = null)
{
byte[] bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
return Send(text, embed, filename, bytes);
}
}
}

View file

@ -5,6 +5,41 @@ namespace Maki
{
public class DiscordColour
{
public int Raw = 0;
public byte Red
{
get => (byte)(Raw >> 16 & 0xFF);
set
{
Raw &= ~0xFF0000;
Raw |= value << 16;
}
}
public byte Green
{
get => (byte)(Raw >> 8 & 0xFF);
set
{
Raw &= ~0xFF00;
Raw |= value << 8;
}
}
public byte Blue
{
get => (byte)(Raw & 0xFF);
set
{
Raw &= ~0xFF;
Raw |= value;
}
}
public string Hex => $"{Red:X2}{Green:X2}{Blue:X2}";
public override string ToString() => $"#{Hex}";
public DiscordColour(int raw)
{
Raw = raw;
@ -12,7 +47,9 @@ namespace Maki
public DiscordColour(byte red, byte green, byte blue)
{
Raw = (red << 16) + (green << 8) + blue;
Raw |= red << 16;
Raw |= green << 8;
Raw |= blue;
}
public DiscordColour(string hex)
@ -24,32 +61,10 @@ namespace Maki
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 int Raw = int.MinValue;
public byte Red
{
get => (byte)(Raw >> 16 & 0xFF);
set => Raw = (value << 16) + (Green << 8) + Blue;
}
public byte Green
{
get => (byte)(Raw >> 8 & 0xFF);
set => Raw = (Red << 16) + (value << 8) + Blue;
}
public byte Blue
{
get => (byte)(Raw & 0xFF);
set => Raw = (Red << 16) + (Green << 8) + value;
}
public string Hex => $"{Red:X2}{Green:X2}{Blue:X2}";
}
}

View file

@ -12,7 +12,7 @@ namespace Maki
public readonly DiscordServer Server;
private readonly Discord client;
public DiscordRole[] Roles => client.roles.Where(x => HasRole(x.Id) || x.Id == Server.Id).OrderByDescending(x => x.Position).ToArray();
public IEnumerable<DiscordRole> Roles => client.RoleManager.Server(Server).Where(x => HasRole(x.Id));
public DateTime Joined { get; internal set; }
public string Nickname { get; internal set; }

View file

@ -2,6 +2,8 @@
using Maki.Structures.Messages;
using Maki.Structures.Rest;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Maki
@ -17,18 +19,20 @@ namespace Maki
public string Text { get; internal set; }
public DateTime Edited { get; internal set; }
public DiscordMember[] MentionsUsers { get; internal set; }
public DiscordRole[] MentionsRoles { get; internal set; }
public IEnumerable<DiscordMember> MentionsUsers { get; internal set; }
public IEnumerable<DiscordRole> MentionsRoles { get; internal set; }
public bool MentionsEveryone { get; internal set; }
public bool IsPinned { get; internal set; }
public bool MentionsMe(bool everyone = false, bool roles = true) =>
(everyone && MentionsEveryone)
|| (Channel.Type != DiscordChannelType.Private && roles && client.members.Where(x => x.User == client.Me && x.Server == Channel.Server).First().Roles.Where(x => MentionsRoles.Contains(x)).Count() > 0)
|| (Channel.Type != DiscordChannelType.Private && roles && client.MemberManager.Id(Channel.Server, client.Me)?.Roles.Where(x => MentionsRoles.Contains(x)).Count() > 0)
|| MentionsUsers.Select(x => x.User).Contains(client.Me);
public bool IsMe => client.Me == User;
public DiscordServer Server => Channel.Server;
internal DiscordMessage(Discord discord, Message msg, DiscordChannel channel, DiscordMember member = null)
{
client = discord;
@ -43,12 +47,13 @@ namespace Maki
} else
{
member = null;
User = client.users.Where(x => x.Id == msg.User.Id).FirstOrDefault();
User = client.UserManager.Id(msg.User.Id);
Debug.Assert(User != null, "user is null");
}
Channel = channel;
MentionsUsers = client.members.Where(x => x.Server == channel.Server && msg.Mentions.Select(y => y.Id).Contains(x.User.Id)).ToArray();
MentionsRoles = client.roles.Where(x => x.Server == channel.Server && msg.MentionsRoles.Contains(x.Id)).ToArray();
MentionsUsers = client.MemberManager.Server(channel.Server).Where(x => msg.Mentions.Select(y => y.Id).Contains(x.User.Id));
MentionsRoles = client.RoleManager .Server(channel.Server).Where(x => msg.MentionsRoles.Contains(x.Id));
MentionsEveryone = msg.MentioningEveryone;
IsPinned = msg.IsPinned == true;
}

View file

@ -6,171 +6,151 @@ namespace Maki
/// Discord Permission Flags
/// </summary>
[Flags]
public enum DiscordPermission
public enum DiscordPermission : long
{
/// <summary>
/// Allows creation of instant invites
/// </summary>
CreateInstantInvite = 1,
CreateInstantInvite = 0x1,
/// <summary>
/// Allows kicking members
/// </summary>
KickMembers = 1 << 1,
KickMembers = 0x2,
/// <summary>
/// Allows banning members
/// </summary>
BanMembers = 1 << 2,
BanMembers = 0x4,
/// <summary>
/// Allows all permissions and bypasses channel permission overwrites
/// </summary>
Administrator = 1 << 3,
Administrator = 0x8,
/// <summary>
/// Allows management and editing of channels
/// </summary>
ManageChannels = 1 << 4,
ManageChannels = 0x10,
/// <summary>
/// Allows management and editing of the guild
/// </summary>
ManageGuild = 1 << 5,
ManageGuild = 0x20,
/// <summary>
/// Allows for the addition of reactions to messages
/// </summary>
AddReactions = 1 << 6,
AddReactions = 0x40,
/// <summary>
/// Allows viewing the audit log
/// </summary>
ViewAuditLog = 1 << 7,
ViewAuditLog = 0x80,
/// <summary>
/// Allows reading messages in a channel. The channel will not appear for users without this permission
/// </summary>
ReadMessages = 1 << 10,
ReadMessages = 0x400,
/// <summary>
/// Allows for sending messages in a channel.
/// </summary>
SendMessages = 1 << 11,
SendMessages = 0x800,
/// <summary>
/// Allows for sending of /tts messages
/// </summary>
SendTTSMessages = 1 << 12,
SendTTSMessages = 0x1000,
/// <summary>
/// Allows for deletion of other users messages
/// </summary>
ManageMessages = 1 << 13,
ManageMessages = 0x2000,
/// <summary>
/// Links sent by this user will be auto-embedded
/// </summary>
EmbedLinks = 1 << 14,
EmbedLinks = 0x4000,
/// <summary>
/// Allows for uploading images and files
/// </summary>
AttachFiles = 1 << 15,
AttachFiles = 0x8000,
/// <summary>
/// Allows for reading of message history
/// </summary>
ReadMessageHistory = 1 << 16,
ReadMessageHistory = 0x10000,
/// <summary>
/// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel
/// </summary>
MentionEveryone = 1 << 17,
MentionEveryone = 0x20000,
/// <summary>
/// Allows the usage of custom emojis from other servers
/// </summary>
ExternalEmojis = 1 << 18,
ExternalEmojis = 0x40000,
/// <summary>
/// Allows for joining of a voice channel
/// </summary>
VoiceConnect = 1 << 20,
VoiceConnect = 0x100000,
/// <summary>
/// Allows for speaking in a voice channel
/// </summary>
VoiceSpeak = 1 << 21,
VoiceSpeak = 0x200000,
/// <summary>
/// Allows for muting members in a voice channel
/// </summary>
VoiceMuteMembers = 1 << 22,
VoiceMuteMembers = 0x400000,
/// <summary>
/// Allows for deafening of members in a voice channel
/// </summary>
VoiceDeafenMembers = 1 << 23,
VoiceDeafenMembers = 0x800000,
/// <summary>
/// Allows for moving of members between voice channels
/// </summary>
VoiceMoveMembers = 1 << 24,
VoiceMoveMembers = 0x1000000,
/// <summary>
/// Allows for using voice-activity-detection in a voice channel
/// </summary>
VoiceUseVAD = 1 << 25,
VoiceUseVAD = 0x2000000,
/// <summary>
/// Allows for modification of own nickname
/// </summary>
ChangeNickname = 1 << 26,
ChangeNickname = 0x4000000,
/// <summary>
/// Allows for modification of other users nicknames
/// </summary>
ManageNicknames = 1 << 27,
ManageNicknames = 0x8000000,
/// <summary>
/// Allows management and editing of roles
/// </summary>
ManageRoles = 1 << 28,
ManageRoles = 0x10000000,
/// <summary>
/// Allows management and editing of webhooks
/// </summary>
ManageWebhooks = 1 << 29,
ManageWebhooks = 0x20000000,
/// <summary>
/// Allows management and editing of emojis
/// </summary>
ManageEmojis = 1 << 30,
ManageEmojis = 0x40000000,
/// <summary>
/// Blank permissions
/// </summary>
None = 0,
/// <summary>
/// All Guild related permissions
/// </summary>
AllGuild = CreateInstantInvite | KickMembers | BanMembers | Administrator | ManageChannels | ManageGuild | AddReactions | ViewAuditLog | ChangeNickname | ManageNicknames | ManageRoles | ManageWebhooks | ManageEmojis,
/// <summary>
/// All text channel permissions
/// </summary>
AllText = CreateInstantInvite | ManageChannels | ViewAuditLog | ReadMessages | SendMessages | SendTTSMessages | ManageMessages | EmbedLinks | AttachFiles | ReadMessageHistory | MentionEveryone | ExternalEmojis | ManageRoles | ManageWebhooks | ManageEmojis,
/// <summary>
/// All voice channel permissions
/// </summary>
AllVoice = CreateInstantInvite | ManageChannels | ViewAuditLog | VoiceConnect | VoiceSpeak | VoiceMuteMembers | VoiceDeafenMembers | VoiceMoveMembers | VoiceUseVAD | ManageRoles,
/// <summary>
/// All permissions
/// </summary>
All = CreateInstantInvite | KickMembers | BanMembers | Administrator | ManageChannels | ManageGuild | AddReactions | ViewAuditLog | ReadMessages | SendMessages | SendTTSMessages | ManageMessages | EmbedLinks | AttachFiles | ReadMessageHistory | MentionEveryone | ExternalEmojis | VoiceConnect | VoiceSpeak | VoiceMuteMembers | VoiceDeafenMembers | VoiceMoveMembers | VoiceUseVAD | ChangeNickname | ManageNicknames | ManageRoles | ManageWebhooks | ManageEmojis,
}
}

View file

@ -60,7 +60,7 @@ namespace Maki
});
wr.Perform();
if (wr.Status != 200 || wr.Response.Length < 1)
if (wr.Status != 200 || wr.ResponseString.Length < 1)
// TODO: elaborate
throw new DiscordException("Failed to edit role");

View file

@ -3,6 +3,7 @@ using Maki.Structures.Guilds;
using Maki.Structures.Rest;
using Maki.Structures.Roles;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Maki
@ -17,13 +18,17 @@ namespace Maki
public DateTime Created { get; internal set; }
internal string IconHash;
public DiscordMember[] Members => client.members.Where(x => x.Server == this).ToArray();
public IEnumerable<DiscordMember> Members => client.MemberManager.Server(this);
public DiscordMember Owner => Members.Where(x => x.User.Id == OwnerId).FirstOrDefault();
public DiscordMember Me => Members.Where(x => x.User == client.Me).FirstOrDefault();
public DiscordChannel[] TextChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Text).OrderBy(x => x.Position).ToArray();
public DiscordChannel[] VoiceChannels => client.channels.Where(x => x.Server == this && x.Type == DiscordChannelType.Voice).OrderBy(x => x.Position).ToArray();
public DiscordRole[] Roles => client.roles.Where(x => x.Server == this).OrderByDescending(x => x.Position).ToArray();
public string Icon(string ext = @"png", int size = 128) => RestEndpoints.CDN_URL + $@"/icons/{Id}/{IconHash}.{ext}?size={size}";
public IEnumerable<DiscordChannel> Channels => client.ChannelManager.Server(this);
public IEnumerable<DiscordChannel> TextChannels => Channels.Where(x => x.Type == DiscordChannelType.Text);
public IEnumerable<DiscordChannel> VoiceChannels => Channels.Where(x => x.Type == DiscordChannelType.Voice);
public IEnumerable<DiscordRole> Roles => client.RoleManager.Server(this);
public string Icon(string ext = "png", int size = 128) => RestEndpoints.CDN_URL + $@"/icons/{Id}/{IconHash}.{ext}?size={size}";
internal DiscordServer(Discord discord, Guild guild)
{
@ -51,7 +56,7 @@ namespace Maki
});
wr.Perform();
if (wr.Status != 200 || wr.Response.Length < 1)
if (wr.Status != 200 || wr.ResponseString.Length < 1)
throw new DiscordException("Failed to create role");
roleStruct = wr.ResponseJson<Role>();
@ -60,14 +65,15 @@ namespace Maki
if (!roleStruct.HasValue)
throw new DiscordException("Empty response?");
DiscordRole role = client.roles.Where(x => x.Id == roleStruct.Value.Id).FirstOrDefault();
DiscordRole role = client.RoleManager.Id(roleStruct.Value.Id);
//Debug.Assert(role != null, "role is null");
if (role == default(DiscordRole))
if (role == null)
{
role = new DiscordRole(client, roleStruct.Value, this);
client.roles.Add(role);
client.RoleManager.Add(role);
}
return role;
}
}

View file

@ -1,192 +0,0 @@
namespace Maki.Gateway
{
/// <summary>
/// Gateway Dispatch Events
/// </summary>
enum GatewayEvent
{
#region Voice Events
CALL_CREATE,
CALL_DELETE,
CALL_UPDATE,
/// <summary>
/// Sent when a guild's voice server is updated. This is sent when initially connecting to voice, and when the current voice instance fails over to a new server.
/// </summary>
VOICE_SERVER_UPDATE,
/// <summary>
/// Sent when someone joins/leaves/moves voice channels. Inner payload is a voice state object.
/// </summary>
VOICE_STATE_UPDATE,
#endregion
#region Channel Events
/// <summary>
/// Sent when a new channel is created, relevant to the current user. The inner payload is a DM or Guild channel object.
/// </summary>
CHANNEL_CREATE,
/// <summary>
/// Sent when a channel relevant to the current user is deleted. The inner payload is a DM or Guild channel object.
/// </summary>
CHANNEL_DELETE,
CHANNEL_PINS_ACK,
CHANNEL_PINS_UPDATE,
CHANNEL_RECIPIENT_ADD,
CHANNEL_RECIPIENT_REMOVE,
/// <summary>
/// Sent when a channel is updated. The inner payload is a guild channel object.
/// </summary>
CHANNEL_UPDATE,
#endregion
#region Guild Events
/// <summary>
/// Sent when a user is banned from a guild. The inner payload is a user object, with an extra guild_id key.
/// </summary>
GUILD_BAN_ADD,
/// <summary>
/// Sent when a user is unbanned from a guild. The inner payload is a user object, with an extra guild_id key.
/// </summary>
GUILD_BAN_REMOVE,
/// <summary>
/// This event can be sent in three different scenarios:
/// 1. When a user is initially connecting, to lazily load and backfill information for all unavailable guilds sent in the ready event.
/// 2. When a Guild becomes available again to the client.
/// 3. When the current user joins a new Guild.
/// The inner payload is a guild object, with all the extra fields specified.
/// </summary>
GUILD_CREATE,
/// <summary>
/// Sent when a guild becomes unavailable during a guild outage, or when the user leaves or is removed from a guild. See GUILD_CREATE for more information about how to handle this event.
/// </summary>
GUILD_DELETE,
/// <summary>
/// Sent when a guild's emojis have been updated.
/// </summary>
GUILD_EMOJIS_UPDATE,
/// <summary>
/// Sent when a guild integration is updated.
/// </summary>
GUILD_INTEGRATIONS_UPDATE,
/// <summary>
/// Sent when a new user joins a guild. The inner payload is a guild member objec, with an extra guild_id key.
/// </summary>
GUILD_MEMBER_ADD,
/// <summary>
/// Sent when a guild member is updated.
/// </summary>
GUILD_MEMBER_UPDATE,
/// <summary>
/// Sent when a user is removed from a guild (leave/kick/ban).
/// </summary>
GUILD_MEMBER_REMOVE,
/// <summary>
/// Sent in response to Gateway Request Guild Members.
/// </summary>
GUILD_MEMBERS_CHUNK,
/// <summary>
/// Sent when a guild role is created.
/// </summary>
GUILD_ROLE_CREATE,
/// <summary>
/// Sent when a guild role is updated.
/// </summary>
GUILD_ROLE_UPDATE,
/// <summary>
/// Sent when a guild role is deleted.
/// </summary>
GUILD_ROLE_DELETE,
/// <summary>
/// Sent when a guild is updated. The inner payload is a guild object.
/// </summary>
GUILD_UPDATE,
#endregion
#region Message Events
MESSAGE_ACK,
/// <summary>
/// Sent when a message is created. The inner payload is a message object.
/// </summary>
MESSAGE_CREATE,
/// <summary>
/// Sent when a message is deleted.
/// </summary>
MESSAGE_DELETE,
/// <summary>
/// Sent when multiple messages are deleted at once.
/// </summary>
MESSAGE_DELETE_BULK,
MESSAGE_REACTION_ADD,
MESSAGE_REACTION_REMOVE,
MESSAGE_REACTIONS_REMOVE_ALL,
/// <summary>
/// Sent when a message is updated. The inner payload is a message object.
/// </summary>
MESSAGE_UPDATE,
#endregion
#region Presence Events
/// <summary>
/// Sent when a user starts typing in a channel.
/// </summary>
TYPING_START,
/// <summary>
/// A user's presence is their current state on a guild. This event is sent when a user's presence is updated for a guild.
/// </summary>
PRESENCE_UPDATE,
PRESENCES_REPLACE,
#endregion
#region Relation Events
FRIEND_SUGGESTION_DELETE,
RELATIONSHIP_ADD,
RELATIONSHIP_REMOVE,
#endregion
#region State Events
/// <summary>
/// The ready event is dispatched when a client has completed the initial handshake with the gateway (for new sessions). The ready event can be the largest and most complex event the gateway will send, as it contains all the state required for a client to begin interacting with the rest of the platform.
/// </summary>
READY,
/// <summary>
/// The resumed event is dispatched when a client has sent a resume payload to the gateway (for resuming existing sessions).
/// </summary>
RESUMED,
#endregion
#region User Events
/// <summary>
/// Sent when properties about the user change. Inner payload is a user object.
/// </summary>
USER_UPDATE,
USER_NOTE_UPDATE,
/// <summary>
/// Sent when the current user updates their settings. Inner payload is a user settings object.
/// </summary>
USER_SETTINGS_UPDATE,
#endregion
}
}

View file

@ -11,20 +11,20 @@ namespace Maki.Gateway
/// <summary>
/// Subject shard
/// </summary>
private readonly GatewayShard shard;
private readonly GatewayShard Shard;
/// <summary>
/// Interval timer for repeating the request
/// </summary>
private Timer timer = null;
private Timer Timer = null;
/// <summary>
/// Constructor
/// </summary>
/// <param name="s">Parent GatewayShard instance</param>
public GatewayHeartbeatManager(GatewayShard s)
/// <param name="shard">Parent GatewayShard instance</param>
public GatewayHeartbeatManager(GatewayShard shard)
{
shard = s;
Shard = shard;
}
/// <summary>
@ -32,10 +32,10 @@ namespace Maki.Gateway
/// </summary>
public void Start()
{
if (timer != null)
if (Timer != null)
Stop();
timer = new Timer(Handler, null, TimeSpan.Zero, shard.HeartbeatInterval);
Timer = new Timer(Handler, null, TimeSpan.Zero, Shard.HeartbeatInterval);
}
/// <summary>
@ -44,7 +44,7 @@ namespace Maki.Gateway
/// <param name="state"></param>
private void Handler(object state)
{
shard.Send(GatewayOPCode.Heartbeat, shard.LastSequence);
Shard.Send(GatewayOPCode.Heartbeat, Shard.LastSequence);
}
/// <summary>
@ -52,11 +52,11 @@ namespace Maki.Gateway
/// </summary>
public void Stop()
{
if (timer == null)
if (Timer == null)
return;
timer.Dispose();
timer = null;
Timer.Dispose();
Timer = null;
}
}
}

View file

@ -1,9 +1,4 @@
using Maki.Structures.Channels;
using Maki.Structures.Gateway;
using Maki.Structures.Guilds;
using Maki.Structures.Messages;
using Maki.Structures.Presences;
using Maki.Structures.Users;
using Maki.Structures.Gateway;
using Newtonsoft.Json;
using System;
using WebSocketSharp;
@ -15,21 +10,20 @@ namespace Maki.Gateway
/// </summary>
class GatewayShard : IDisposable
{
private const string GATEWAY_URL = "{0}?v={1}&encoding=json";
private readonly string GatewayUrl;
/// <summary>
/// Session key for continuing a resuming after disconnecting
/// </summary>
private string session;
private string Session;
/// <summary>
/// Websocket container
/// </summary>
private WebSocket webSocket;
/// <summary>
/// Active gateway version
/// </summary>
private int gatewayVersion = Discord.GATEWAY_VERSION;
private WebSocket WebSocket;
/// <summary>
/// Interval at which heartbeats are sent
/// </summary>
@ -48,111 +42,46 @@ namespace Maki.Gateway
/// <summary>
/// Parent DiscordClient instance
/// </summary>
private readonly Discord client;
private readonly Discord Client;
/// <summary>
/// Heartbeat handler
/// </summary>
private readonly GatewayHeartbeatManager heartbeatHandler;
#region Events
public event Action<GatewayShard> OnCallCreate;
public event Action<GatewayShard> OnCallDelete;
public event Action<GatewayShard> OnCallUpdate;
public event Action<GatewayShard> OnVoiceServerUpdate;
public event Action<GatewayShard> OnVoiceStateUpdate;
public event Action<GatewayShard, Channel> OnChannelCreate;
public event Action<GatewayShard, Channel> OnChannelDelete;
public event Action<GatewayShard> OnChannelPinsAck;
public event Action<GatewayShard> OnChannelPinsUpdate;
public event Action<GatewayShard> OnChannelRecipientAdd;
public event Action<GatewayShard> OnChannelRecipientRemove;
public event Action<GatewayShard, Channel> OnChannelUpdate;
public event Action<GatewayShard, User> OnGuildBanAdd;
public event Action<GatewayShard, User> OnGuildBanRemove;
public event Action<GatewayShard, Guild> OnGuildCreate;
public event Action<GatewayShard, Guild> OnGuildDelete;
public event Action<GatewayShard, Guild> OnGuildEmojisUpdate;
public event Action<GatewayShard, GuildIntegration> OnGuildIntegrationsUpdate;
public event Action<GatewayShard, GuildMember> OnGuildMemberAdd;
public event Action<GatewayShard, GuildMember> OnGuildMemberUpdate;
public event Action<GatewayShard, GuildMember> OnGuildMemberRemove;
public event Action<GatewayShard, GuildMembersChunk> OnGuildMembersChunk;
public event Action<GatewayShard, GuildRole> OnGuildRoleCreate;
public event Action<GatewayShard, GuildRole> OnGuildRoleUpdate;
public event Action<GatewayShard, GuildRole> OnGuildRoleDelete;
public event Action<GatewayShard, Guild> OnGuildUpdate;
public event Action<GatewayShard> OnMessageAck;
public event Action<GatewayShard, Message> OnMessageCreate;
public event Action<GatewayShard, Message> OnMessageDelete;
public event Action<GatewayShard> OnMessageDeleteBulk;
public event Action<GatewayShard> OnMessageReactionAdd;
public event Action<GatewayShard> OnMessageReactionRemove;
public event Action<GatewayShard> OnMessageReactionsRemoveAll;
public event Action<GatewayShard, Message> OnMessageUpdate;
public event Action<GatewayShard, TypingStart> OnTypingStart;
public event Action<GatewayShard, Presence> OnPresenceUpdate;
public event Action<GatewayShard> OnPresencesReplace;
public event Action<GatewayShard> OnFriendSuggestionDelete;
public event Action<GatewayShard> OnRelationshipAdd;
public event Action<GatewayShard> OnRelationshipRemove;
public event Action<GatewayShard, GatewayReady> OnReady;
public event Action<GatewayShard> OnResumed;
public event Action<GatewayShard, User> OnUserUpdate;
public event Action<GatewayShard> OnUserNoteUpdate;
public event Action<GatewayShard> OnUserSettingsUpdate;
public event Action<GatewayShard> OnSocketOpen;
public event Action<GatewayShard, bool, ushort, string> OnSocketClose;
public event Action<GatewayShard, Exception> OnSocketError;
public event Action<GatewayShard, string> OnSocketMessage;
#endregion
private readonly GatewayHeartbeatManager HeartbeatHandler;
/// <summary>
/// Fires when a payload is received.
/// </summary>
public event Action<GatewayShard, GatewayPayload> OnSocketPayload;
/// <summary>
/// Constructor
/// </summary>
/// <param name="id">Shard Id</param>
/// <param name="c">Parent DiscordClient instance</param>
public GatewayShard(int id, Discord c)
/// <param name="client">Parent DiscordClient instance</param>
/// <param name="connect">Whether to immediately call Connect()</param>
public GatewayShard(int id, Discord client, bool connect = true)
{
Id = id;
client = c;
heartbeatHandler = new GatewayHeartbeatManager(this);
Connect();
Client = client;
HeartbeatHandler = new GatewayHeartbeatManager(this);
GatewayUrl = string.Format(GATEWAY_URL, Client.Gateway, Discord.GATEWAY_VERSION);
if (connect)
Connect();
}
/// <summary>
/// Event handler for WebSocketSharp's OnOpen event, forwards to our OnSocketOpen event
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="e">Event arguments</param>
private void WebSocket_OnOpen(object sender, EventArgs e) => OnSocketOpen?.Invoke(this);
/// <summary>
/// Event handler for WebSocketSharp's OnError event, forwards to our OnSocketError event
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="e">Event arguments</param>
private void WebSocket_OnError(object sender, ErrorEventArgs e) => OnSocketError?.Invoke(this, e.Exception);
/// <summary>
/// Event handler for WebSocketSharp's OnClose event, stops heartbeats, forwards to our OnSocketClose event and reconnects if the close wasn't clean
/// Event handler for WebSocketSharp's OnClose event, stops heartbeats and reconnects if the close wasn't clean
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="e">Event arguments</param>
private void WebSocket_OnClose(object sender, CloseEventArgs e)
{
heartbeatHandler.Stop();
OnSocketClose?.Invoke(this, e.WasClean, e.Code, e.Reason);
HeartbeatHandler.Stop();
if (!e.WasClean)
if (!e.WasClean || e.Code != 1000)
Connect();
}
@ -167,218 +96,19 @@ namespace Maki.Gateway
if (!e.IsText)
return;
//Console.WriteLine(e.Data.Replace(client.Token, new string('*', client.Token.Length)));
OnSocketMessage?.Invoke(this, e.Data);
Console.WriteLine(e.Data.Replace(Client.Token, new string('*', Client.Token.Length)));
GatewayPayload payload = JsonConvert.DeserializeObject<GatewayPayload>(e.Data);
OnSocketPayload?.Invoke(this, payload);
switch (payload.OPCode) {
case GatewayOPCode.Dispatch:
LastSequence = payload.Sequence;
Enum.TryParse(payload.Name, out GatewayEvent evt);
switch (evt) {
#region Call
case GatewayEvent.CALL_CREATE:
OnCallCreate?.Invoke(this);
break;
case GatewayEvent.CALL_DELETE:
OnCallDelete?.Invoke(this);
break;
case GatewayEvent.CALL_UPDATE:
OnCallUpdate?.Invoke(this);
break;
case GatewayEvent.VOICE_SERVER_UPDATE:
OnVoiceServerUpdate?.Invoke(this);
break;
case GatewayEvent.VOICE_STATE_UPDATE:
OnVoiceStateUpdate?.Invoke(this);
break;
#endregion
#region Channel
case GatewayEvent.CHANNEL_CREATE:
OnChannelCreate?.Invoke(this, payload.DataAs<Channel>());
break;
case GatewayEvent.CHANNEL_DELETE:
OnChannelDelete?.Invoke(this, payload.DataAs<Channel>());
break;
case GatewayEvent.CHANNEL_PINS_ACK:
OnChannelPinsAck?.Invoke(this);
break;
case GatewayEvent.CHANNEL_PINS_UPDATE:
OnChannelPinsUpdate?.Invoke(this);
break;
case GatewayEvent.CHANNEL_RECIPIENT_ADD:
OnChannelRecipientAdd?.Invoke(this);
break;
case GatewayEvent.CHANNEL_RECIPIENT_REMOVE:
OnChannelRecipientRemove?.Invoke(this);
break;
case GatewayEvent.CHANNEL_UPDATE:
OnChannelUpdate?.Invoke(this, payload.DataAs<Channel>());
break;
#endregion
#region Guild
case GatewayEvent.GUILD_BAN_ADD:
OnGuildBanAdd?.Invoke(this, payload.DataAs<User>());
break;
case GatewayEvent.GUILD_BAN_REMOVE:
OnGuildBanRemove?.Invoke(this, payload.DataAs<User>());
break;
case GatewayEvent.GUILD_CREATE:
OnGuildCreate?.Invoke(this, payload.DataAs<Guild>());
break;
case GatewayEvent.GUILD_DELETE:
OnGuildDelete?.Invoke(this, payload.DataAs<Guild>());
break;
case GatewayEvent.GUILD_EMOJIS_UPDATE:
OnGuildEmojisUpdate?.Invoke(this, payload.DataAs<Guild>());
break;
case GatewayEvent.GUILD_INTEGRATIONS_UPDATE:
OnGuildIntegrationsUpdate?.Invoke(this, payload.DataAs<GuildIntegration>());
break;
case GatewayEvent.GUILD_MEMBER_ADD:
OnGuildMemberAdd?.Invoke(this, payload.DataAs<GuildMember>());
break;
case GatewayEvent.GUILD_MEMBER_UPDATE:
OnGuildMemberUpdate?.Invoke(this, payload.DataAs<GuildMember>());
break;
case GatewayEvent.GUILD_MEMBER_REMOVE:
OnGuildMemberRemove?.Invoke(this, payload.DataAs<GuildMember>());
break;
case GatewayEvent.GUILD_MEMBERS_CHUNK:
OnGuildMembersChunk?.Invoke(this, payload.DataAs<GuildMembersChunk>());
break;
case GatewayEvent.GUILD_ROLE_CREATE:
OnGuildRoleCreate?.Invoke(this, payload.DataAs<GuildRole>());
break;
case GatewayEvent.GUILD_ROLE_UPDATE:
OnGuildRoleUpdate?.Invoke(this, payload.DataAs<GuildRole>());
break;
case GatewayEvent.GUILD_ROLE_DELETE:
OnGuildRoleDelete?.Invoke(this, payload.DataAs<GuildRole>());
break;
case GatewayEvent.GUILD_UPDATE:
OnGuildUpdate?.Invoke(this, payload.DataAs<Guild>());
break;
#endregion
#region Message
case GatewayEvent.MESSAGE_ACK:
OnMessageAck?.Invoke(this);
break;
case GatewayEvent.MESSAGE_CREATE:
OnMessageCreate?.Invoke(this, payload.DataAs<Message>());
break;
case GatewayEvent.MESSAGE_DELETE:
OnMessageDelete?.Invoke(this, payload.DataAs<Message>());
break;
case GatewayEvent.MESSAGE_DELETE_BULK:
OnMessageDeleteBulk?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTION_ADD:
OnMessageReactionAdd?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTION_REMOVE:
OnMessageReactionRemove?.Invoke(this);
break;
case GatewayEvent.MESSAGE_REACTIONS_REMOVE_ALL:
OnMessageReactionsRemoveAll?.Invoke(this);
break;
case GatewayEvent.MESSAGE_UPDATE:
OnMessageUpdate?.Invoke(this, payload.DataAs<Message>());
break;
#endregion
#region Presence
case GatewayEvent.TYPING_START:
OnTypingStart?.Invoke(this, payload.DataAs<TypingStart>());
break;
case GatewayEvent.PRESENCE_UPDATE:
OnPresenceUpdate?.Invoke(this, payload.DataAs<Presence>());
break;
case GatewayEvent.PRESENCES_REPLACE:
OnPresencesReplace?.Invoke(this);
break;
#endregion
#region Relations
case GatewayEvent.FRIEND_SUGGESTION_DELETE:
OnFriendSuggestionDelete?.Invoke(this);
break;
case GatewayEvent.RELATIONSHIP_ADD:
OnRelationshipAdd?.Invoke(this);
break;
case GatewayEvent.RELATIONSHIP_REMOVE:
OnRelationshipRemove?.Invoke(this);
break;
#endregion
#region State
case GatewayEvent.READY:
GatewayReady ready = payload.DataAs<GatewayReady>();
gatewayVersion = ready.Version;
session = ready.Session;
OnReady?.Invoke(this, ready);
break;
case GatewayEvent.RESUMED:
OnResumed?.Invoke(this);
break;
#endregion
#region User
case GatewayEvent.USER_UPDATE:
OnUserUpdate?.Invoke(this, payload.DataAs<User>());
break;
case GatewayEvent.USER_NOTE_UPDATE:
OnUserNoteUpdate?.Invoke(this);
break;
case GatewayEvent.USER_SETTINGS_UPDATE:
OnUserSettingsUpdate?.Invoke(this);
break;
#endregion
default:
Console.WriteLine($"Unknown payload type: {payload.Name}");
break;
if (payload.Name == "READY")
{
GatewayReady ready = payload.DataAs<GatewayReady>();
Session = ready.Session;
}
break;
@ -386,27 +116,30 @@ namespace Maki.Gateway
GatewayHello hello = payload.DataAs<GatewayHello>();
HeartbeatInterval = TimeSpan.FromMilliseconds(hello.HeartbeatInterval);
heartbeatHandler.Start();
HeartbeatHandler.Start();
if (string.IsNullOrEmpty(session))
Send(GatewayOPCode.Identify, new GatewayIdentification {
Token = client.Token,
if (string.IsNullOrEmpty(Session))
Send(GatewayOPCode.Identify, new GatewayIdentification
{
Token = Client.Token,
Compress = false,
LargeThreshold = 250,
Shard = new int[2] { Id, client.ShardClient.ShardCount },
Properties = new GatewayIdentificationProperties {
OperatingSystem = @"windows",
Browser = @"Maki",
Device = @"Maki",
Shard = new int[] { Id, Client.ShardClient.ShardCount },
Properties = new GatewayIdentificationProperties
{
OperatingSystem = "windows",
Browser = "Maki",
Device = "Maki",
Referrer = string.Empty,
ReferringDomain = string.Empty,
}
});
else
Send(GatewayOPCode.Resume, new GatewayResume {
Send(GatewayOPCode.Resume, new GatewayResume
{
LastSequence = LastSequence ?? default(int),
Token = client.Token,
Session = session,
Token = Client.Token,
Session = Session,
});
break;
}
@ -418,23 +151,21 @@ namespace Maki.Gateway
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
IsDisposed = true;
Disconnect();
}
if (IsDisposed)
return;
IsDisposed = true;
Disconnect();
if (disposing)
GC.SuppressFinalize(this);
}
~GatewayShard()
{
Dispose(false);
}
=> Dispose(false);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(true);
}
=> Dispose(true);
#endregion
@ -443,17 +174,15 @@ namespace Maki.Gateway
/// </summary>
public void Connect()
{
webSocket = new WebSocket($"{client.Gateway}?v={gatewayVersion}&encoding=json");
WebSocket = new WebSocket(GatewayUrl);
// make wss not log anything on its own
webSocket.Log.Output = (LogData logData, string path) => { };
WebSocket.Log.Output = (LogData logData, string path) => { };
webSocket.OnOpen += WebSocket_OnOpen;
webSocket.OnClose += WebSocket_OnClose;
webSocket.OnError += WebSocket_OnError;
webSocket.OnMessage += WebSocket_OnMessage;
WebSocket.OnClose += WebSocket_OnClose;
WebSocket.OnMessage += WebSocket_OnMessage;
webSocket.Connect();
WebSocket.Connect();
}
/// <summary>
@ -461,10 +190,10 @@ namespace Maki.Gateway
/// </summary>
public void Disconnect()
{
heartbeatHandler.Stop();
HeartbeatHandler.Stop();
if (webSocket.ReadyState != WebSocketState.Closed)
webSocket?.Close(CloseStatusCode.Normal);
if (WebSocket.ReadyState != WebSocketState.Closed)
WebSocket?.Close(CloseStatusCode.Normal);
}
/// <summary>
@ -474,20 +203,11 @@ namespace Maki.Gateway
/// <param name="data">Data to serialise and send</param>
public void Send<T>(T data)
{
/*Console.WriteLine(
JsonConvert.SerializeObject(
data,
typeof(T),
new JsonSerializerSettings()
).Replace(client.Token, new string('*', client.Token.Length)));*/
string json = JsonConvert.SerializeObject(data, typeof(T), new JsonSerializerSettings());
webSocket.Send(
JsonConvert.SerializeObject(
data,
typeof(T),
new JsonSerializerSettings()
)
);
Console.WriteLine(json.Replace(Client.Token, new string('*', Client.Token.Length)));
WebSocket.Send(json);
}
/// <summary>
@ -496,12 +216,10 @@ namespace Maki.Gateway
/// <param name="opcode">Opcode to use</param>
/// <param name="data">Data to send</param>
public void Send(GatewayOPCode opcode, object data)
{
Send(new GatewayPayload
=> Send(new GatewayPayload
{
OPCode = opcode,
Data = data
});
}
}
}

View file

@ -18,26 +18,27 @@ namespace Maki.Gateway
/// <summary>
/// Managed (active) shards
/// </summary>
private readonly Dictionary<int, GatewayShard> shards = new Dictionary<int, GatewayShard>();
private readonly List<GatewayShard> Shards = new List<GatewayShard>();
private readonly object Lock = new object();
/// <summary>
/// Parent DiscordClient instance
/// </summary>
private Discord client;
private Discord Client;
/// <summary>
/// Number of active managed shards
/// </summary>
public int ShardCount => shards.Count;
public int ShardCount
=> Shards.Count;
/// <summary>
/// Constructor
/// </summary>
/// <param name="c">Parent DiscordClient instance</param>
public GatewayShardClient(Discord c)
{
client = c;
}
/// <param name="client">Parent DiscordClient instance</param>
public GatewayShardClient(Discord client)
=> Client = client;
/// <summary>
/// Creates a new Gateway Shard
@ -46,17 +47,227 @@ namespace Maki.Gateway
/// <returns>New Gateway Shard</returns>
public GatewayShard Create(int id)
{
GatewayShard shard = new GatewayShard(id, client);
ApplyEvents(shard);
shards.Add(id, shard);
GatewayShard shard = new GatewayShard(id, Client);
shard.OnSocketPayload += Shard_OnSocketPayload;
lock (Lock)
Shards.Add(shard);
return shard;
}
/// <summary>
/// Destroys a Gateway Shard using its id
/// </summary>
/// <param name="id">Id of the shard to destroy</param>
public void Destroy(int id) => Destroy(shards[id]);
private void Shard_OnSocketPayload(GatewayShard shard, GatewayPayload payload)
{
switch (payload.OPCode)
{
case GatewayOPCode.Dispatch:
switch (payload.Name)
{
#region Call
case "CALL_CREATE":
OnCallCreate?.Invoke(shard);
break;
case "CALL_DELETE":
OnCallDelete?.Invoke(shard);
break;
case "CALL_UPDATE":
OnCallUpdate?.Invoke(shard);
break;
case "VOICE_SERVER_UPDATE":
OnVoiceServerUpdate?.Invoke(shard);
break;
case "VOICE_STATE_UPDATE":
OnVoiceStateUpdate?.Invoke(shard);
break;
#endregion
#region Channel
case "CHANNEL_CREATE":
OnChannelCreate?.Invoke(shard, payload.DataAs<Channel>());
break;
case "CHANNEL_DELETE":
OnChannelDelete?.Invoke(shard, payload.DataAs<Channel>());
break;
case "CHANNEL_PINS_ACK":
OnChannelPinsAck?.Invoke(shard);
break;
case "CHANNEL_PINS_UPDATE":
OnChannelPinsUpdate?.Invoke(shard);
break;
case "CHANNEL_RECIPIENT_ADD":
OnChannelRecipientAdd?.Invoke(shard);
break;
case "CHANNEL_RECIPIENT_REMOVE":
OnChannelRecipientRemove?.Invoke(shard);
break;
case "CHANNEL_UPDATE":
OnChannelUpdate?.Invoke(shard, payload.DataAs<Channel>());
break;
#endregion
#region Guild
case "GUILD_BAN_ADD":
OnGuildBanAdd?.Invoke(shard, payload.DataAs<User>());
break;
case "GUILD_BAN_REMOVE":
OnGuildBanRemove?.Invoke(shard, payload.DataAs<User>());
break;
case "GUILD_CREATE":
OnGuildCreate?.Invoke(shard, payload.DataAs<Guild>());
break;
case "GUILD_DELETE":
OnGuildDelete?.Invoke(shard, payload.DataAs<Guild>());
break;
case "GUILD_EMOJIS_UPDATE":
OnGuildEmojisUpdate?.Invoke(shard, payload.DataAs<Guild>());
break;
case "GUILD_INTEGRATIONS_UPDATE":
OnGuildIntegrationsUpdate?.Invoke(shard, payload.DataAs<GuildIntegration>());
break;
case "GUILD_MEMBER_ADD":
OnGuildMemberAdd?.Invoke(shard, payload.DataAs<GuildMember>());
break;
case "GUILD_MEMBER_UPDATE":
OnGuildMemberUpdate?.Invoke(shard, payload.DataAs<GuildMember>());
break;
case "GUILD_MEMBER_REMOVE":
OnGuildMemberRemove?.Invoke(shard, payload.DataAs<GuildMember>());
break;
case "GUILD_MEMBERS_CHUNK":
OnGuildMembersChunk?.Invoke(shard, payload.DataAs<GuildMembersChunk>());
break;
case "GUILD_ROLE_CREATE":
OnGuildRoleCreate?.Invoke(shard, payload.DataAs<GuildRole>());
break;
case "GUILD_ROLE_UPDATE":
OnGuildRoleUpdate?.Invoke(shard, payload.DataAs<GuildRole>());
break;
case "GUILD_ROLE_DELETE":
OnGuildRoleDelete?.Invoke(shard, payload.DataAs<GuildRole>());
break;
case "GUILD_UPDATE":
OnGuildUpdate?.Invoke(shard, payload.DataAs<Guild>());
break;
#endregion
#region Message
case "MESSAGE_ACK":
OnMessageAck?.Invoke(shard);
break;
case "MESSAGE_CREATE":
OnMessageCreate?.Invoke(shard, payload.DataAs<Message>());
break;
case "MESSAGE_DELETE":
OnMessageDelete?.Invoke(shard, payload.DataAs<Message>());
break;
case "MESSAGE_DELETE_BULK":
OnMessageDeleteBulk?.Invoke(shard);
break;
case "MESSAGE_REACTION_ADD":
OnMessageReactionAdd?.Invoke(shard);
break;
case "MESSAGE_REACTION_REMOVE":
OnMessageReactionRemove?.Invoke(shard);
break;
case "MESSAGE_REACTIONS_REMOVE_ALL":
OnMessageReactionsRemoveAll?.Invoke(shard);
break;
case "MESSAGE_UPDATE":
OnMessageUpdate?.Invoke(shard, payload.DataAs<Message>());
break;
#endregion
#region Presence
case "TYPING_START":
OnTypingStart?.Invoke(shard, payload.DataAs<TypingStart>());
break;
case "PRESENCE_UPDATE":
OnPresenceUpdate?.Invoke(shard, payload.DataAs<Presence>());
break;
case "PRESENCES_REPLACE":
OnPresencesReplace?.Invoke(shard);
break;
#endregion
#region Relations
case "FRIEND_SUGGESTION_DELETE":
OnFriendSuggestionDelete?.Invoke(shard);
break;
case "RELATIONSHIP_ADD":
OnRelationshipAdd?.Invoke(shard);
break;
case "RELATIONSHIP_REMOVE":
OnRelationshipRemove?.Invoke(shard);
break;
#endregion
#region State
case "READY":
OnReady?.Invoke(shard, payload.DataAs<GatewayReady>());
break;
case "RESUMED":
OnResumed?.Invoke(shard);
break;
#endregion
#region User
case "USER_UPDATE":
OnUserUpdate?.Invoke(shard, payload.DataAs<User>());
break;
case "USER_NOTE_UPDATE":
OnUserNoteUpdate?.Invoke(shard);
break;
case "USER_SETTINGS_UPDATE":
OnUserSettingsUpdate?.Invoke(shard);
break;
#endregion
#if DEBUG
default:
Console.WriteLine($"Unknown payload type: {payload.Name}");
break;
#endif
}
break;
}
}
/// <summary>
/// Destroys a Gateway Shard
@ -64,150 +275,26 @@ namespace Maki.Gateway
/// <param name="shard">Shard to destroy</param>
public void Destroy(GatewayShard shard)
{
shards.Remove(shard.Id);
lock (Lock)
Shards.Remove(shard);
// Disconnect and Dispose are called separately so OnSocketClose is called properly
shard.Disconnect();
RemoveEvents(shard);
shard.OnSocketPayload -= Shard_OnSocketPayload;
shard.Dispose();
}
/// <summary>
/// Destroys all shards
/// </summary>
public void Disconnect() => shards.Keys.ToList().ForEach(x => Destroy(x));
/// <summary>
/// Links all gateway events handlers to the shard
/// </summary>
/// <param name="shard">Shard to link events to</param>
private void ApplyEvents(GatewayShard shard)
public void Disconnect()
{
shard.OnCallCreate += OnCallCreate;
shard.OnCallDelete += OnCallDelete;
shard.OnCallUpdate += OnCallUpdate;
shard.OnVoiceServerUpdate += OnVoiceServerUpdate;
shard.OnVoiceStateUpdate += OnVoiceStateUpdate;
shard.OnChannelCreate += OnChannelCreate;
shard.OnChannelDelete += OnChannelDelete;
shard.OnChannelPinsAck += OnChannelPinsAck;
shard.OnChannelPinsUpdate += OnChannelPinsUpdate;
shard.OnChannelRecipientAdd += OnChannelRecipientAdd;
shard.OnChannelRecipientRemove += OnChannelRecipientRemove;
shard.OnChannelUpdate += OnChannelUpdate;
shard.OnGuildBanAdd += OnGuildBanAdd;
shard.OnGuildBanRemove += OnGuildBanRemove;
shard.OnGuildCreate += OnGuildCreate;
shard.OnGuildDelete += OnGuildDelete;
shard.OnGuildEmojisUpdate += OnGuildEmojisUpdate;
shard.OnGuildIntegrationsUpdate += OnGuildIntegrationsUpdate;
shard.OnGuildMemberAdd += OnGuildMemberAdd;
shard.OnGuildMemberUpdate += OnGuildMemberUpdate;
shard.OnGuildMemberRemove += OnGuildMemberRemove;
shard.OnGuildMembersChunk += OnGuildMembersChunk;
shard.OnGuildRoleCreate += OnGuildRoleCreate;
shard.OnGuildRoleUpdate += OnGuildRoleUpdate;
shard.OnGuildRoleDelete += OnGuildRoleDelete;
shard.OnGuildUpdate += OnGuildUpdate;
shard.OnMessageAck += OnMessageAck;
shard.OnMessageCreate += OnMessageCreate;
shard.OnMessageDelete += OnMessageDelete;
shard.OnMessageDeleteBulk += OnMessageDeleteBulk;
shard.OnMessageReactionAdd += OnMessageReactionAdd;
shard.OnMessageReactionRemove += OnMessageReactionRemove;
shard.OnMessageReactionsRemoveAll += OnMessageReactionsRemoveAll;
shard.OnMessageUpdate += OnMessageUpdate;
shard.OnTypingStart += OnTypingStart;
shard.OnPresenceUpdate += OnPresenceUpdate;
shard.OnPresencesReplace += OnPresencesReplace;
shard.OnFriendSuggestionDelete += OnFriendSuggestionDelete;
shard.OnRelationshipAdd += OnRelationshipAdd;
shard.OnRelationshipRemove += OnRelationshipRemove;
shard.OnReady += OnReady;
shard.OnResumed += OnResumed;
shard.OnUserUpdate += OnUserUpdate;
shard.OnUserNoteUpdate += OnUserNoteUpdate;
shard.OnUserSettingsUpdate += OnUserSettingsUpdate;
shard.OnSocketOpen += OnSocketOpen;
shard.OnSocketClose += OnSocketClose;
shard.OnSocketError += OnSocketError;
shard.OnSocketMessage += OnSocketMessage;
}
/// <summary>
/// Unlinks all gateway events handlers from the shard
/// </summary>
/// <param name="shard">Shard to unlink events from</param>
public void RemoveEvents(GatewayShard shard)
{
shard.OnCallCreate -= OnCallCreate;
shard.OnCallDelete -= OnCallDelete;
shard.OnCallUpdate -= OnCallUpdate;
shard.OnVoiceServerUpdate -= OnVoiceServerUpdate;
shard.OnVoiceStateUpdate -= OnVoiceStateUpdate;
shard.OnChannelCreate -= OnChannelCreate;
shard.OnChannelDelete -= OnChannelDelete;
shard.OnChannelPinsAck -= OnChannelPinsAck;
shard.OnChannelPinsUpdate -= OnChannelPinsUpdate;
shard.OnChannelRecipientAdd -= OnChannelRecipientAdd;
shard.OnChannelRecipientRemove -= OnChannelRecipientRemove;
shard.OnChannelUpdate -= OnChannelUpdate;
shard.OnGuildBanAdd -= OnGuildBanAdd;
shard.OnGuildBanRemove -= OnGuildBanRemove;
shard.OnGuildCreate -= OnGuildCreate;
shard.OnGuildDelete -= OnGuildDelete;
shard.OnGuildEmojisUpdate -= OnGuildEmojisUpdate;
shard.OnGuildIntegrationsUpdate -= OnGuildIntegrationsUpdate;
shard.OnGuildMemberAdd -= OnGuildMemberAdd;
shard.OnGuildMemberUpdate -= OnGuildMemberUpdate;
shard.OnGuildMemberRemove -= OnGuildMemberRemove;
shard.OnGuildMembersChunk -= OnGuildMembersChunk;
shard.OnGuildRoleCreate -= OnGuildRoleCreate;
shard.OnGuildRoleUpdate -= OnGuildRoleUpdate;
shard.OnGuildRoleDelete -= OnGuildRoleDelete;
shard.OnGuildUpdate -= OnGuildUpdate;
shard.OnMessageAck -= OnMessageAck;
shard.OnMessageCreate -= OnMessageCreate;
shard.OnMessageDelete -= OnMessageDelete;
shard.OnMessageDeleteBulk -= OnMessageDeleteBulk;
shard.OnMessageReactionAdd -= OnMessageReactionAdd;
shard.OnMessageReactionRemove -= OnMessageReactionRemove;
shard.OnMessageReactionsRemoveAll -= OnMessageReactionsRemoveAll;
shard.OnMessageUpdate -= OnMessageUpdate;
shard.OnTypingStart -= OnTypingStart;
shard.OnPresenceUpdate -= OnPresenceUpdate;
shard.OnPresencesReplace -= OnPresencesReplace;
shard.OnFriendSuggestionDelete -= OnFriendSuggestionDelete;
shard.OnRelationshipAdd -= OnRelationshipAdd;
shard.OnRelationshipRemove -= OnRelationshipRemove;
shard.OnReady -= OnReady;
shard.OnResumed -= OnResumed;
shard.OnUserUpdate -= OnUserUpdate;
shard.OnUserNoteUpdate -= OnUserNoteUpdate;
shard.OnUserSettingsUpdate -= OnUserSettingsUpdate;
shard.OnSocketOpen -= OnSocketOpen;
shard.OnSocketClose -= OnSocketClose;
shard.OnSocketError -= OnSocketError;
shard.OnSocketMessage -= OnSocketMessage;
lock (Lock)
Shards.ToList().ForEach(x => Destroy(x));
}
#region Events
public event Action<GatewayShard> OnCallCreate;
public event Action<GatewayShard> OnCallDelete;
public event Action<GatewayShard> OnCallUpdate;
@ -261,39 +348,33 @@ namespace Maki.Gateway
public event Action<GatewayShard> OnUserNoteUpdate;
public event Action<GatewayShard> OnUserSettingsUpdate;
public event Action<GatewayShard> OnSocketOpen;
public event Action<GatewayShard, bool, ushort, string> OnSocketClose;
public event Action<GatewayShard, Exception> OnSocketError;
public event Action<GatewayShard, string> OnSocketMessage;
#endregion
#region IDisposable
private bool IsDisposed = false;
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
IsDisposed = true;
Disconnect();
}
}
~GatewayShardClient()
{
Dispose(false);
}
/// <summary>
/// Releases all unmanaged resources used by this object
/// </summary>
public void Dispose()
private void Dispose(bool disposing)
{
Dispose(true);
GC.SuppressFinalize(true);
if (IsDisposed)
return;
IsDisposed = true;
Disconnect();
if (disposing)
GC.SuppressFinalize(this);
}
~GatewayShardClient()
=> Dispose(false);
public void Dispose()
=> Dispose(true);
#endregion
}
}

View file

@ -35,21 +35,48 @@
<NoWarn>0649</NoWarn>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath>
<Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="System.IO, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll</HintPath>
</Reference>
<Reference Include="System.Net" />
<Reference Include="System.Net.Http, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.Extensions, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.Primitives, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.WebRequest, Version=2.2.29.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.2.29\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
</Reference>
<Reference Include="System.Runtime, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks, Version=2.6.10.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Threading.Tasks.dll</HintPath>
</Reference>
<Reference Include="websocket-sharp, Version=1.0.2.59611, Culture=neutral, PublicKeyToken=5660b08a1845a91e, processorArchitecture=MSIL">
<HintPath>..\..\packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll</HintPath>
<HintPath>$(SolutionDir)\packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="BaseManager.cs" />
<Compile Include="ChannelManager.cs" />
<Compile Include="DiscordChannel.cs" />
<Compile Include="Discord.cs" />
<Compile Include="DiscordChannelType.cs" />
@ -68,7 +95,8 @@
<Compile Include="DiscordServer.cs" />
<Compile Include="DiscordUserStatus.cs" />
<Compile Include="DiscordUser.cs" />
<Compile Include="Gateway\GatewayEvent.cs" />
<Compile Include="MemberManager.cs" />
<Compile Include="MessageManager.cs" />
<Compile Include="Rest\RestErrorCode.cs" />
<Compile Include="Rest\HttpMethod.cs" />
<Compile Include="Rest\RestEndpoints.cs" />
@ -78,9 +106,8 @@
<Compile Include="DiscordTokenType.cs" />
<Compile Include="Gateway\GatewayCloseCode.cs" />
<Compile Include="Rest\WebRequest.cs" />
<Compile Include="Structures\Auth\LoginMultiFactorAuth.cs" />
<Compile Include="Structures\Auth\LoginRequest.cs" />
<Compile Include="Structures\Auth\LoginResponse.cs" />
<Compile Include="RoleManager.cs" />
<Compile Include="ServerManager.cs" />
<Compile Include="Structures\Channels\Channel.cs" />
<Compile Include="Structures\Channels\ChannelType.cs" />
<Compile Include="Structures\Embeds\Embed.cs" />
@ -125,10 +152,17 @@
<Compile Include="Structures\Roles\Role.cs" />
<Compile Include="Structures\Users\PermissionOverwrite.cs" />
<Compile Include="Structures\Users\User.cs" />
<Compile Include="UserManager.cs" />
<Compile Include="Utility.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets" Condition="Exists('..\..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" />
<Target Name="EnsureBclBuildImported" BeforeTargets="BeforeBuild" Condition="'$(BclBuildImported)' == ''">
<Error Condition="!Exists('..\..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" Text="This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=317567." HelpKeyword="BCLBUILD2001" />
<Error Condition="Exists('..\..\packages\Microsoft.Bcl.Build.1.0.14\tools\Microsoft.Bcl.Build.targets')" Text="The build restored NuGet packages. Build the project again to include these packages in the build. For more information, see http://go.microsoft.com/fwlink/?LinkID=317568." HelpKeyword="BCLBUILD2002" />
</Target>
</Project>

48
Maki/MemberManager.cs Normal file
View file

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
namespace Maki
{
internal sealed class MemberManager : BaseManager<DiscordMember>
{
internal MemberManager()
{
}
public DiscordMember Id(ulong user)
{
lock (Collection)
return Collection.Where(x => x.Server == null && x.User.Id == user).FirstOrDefault();
}
public DiscordMember Id(ulong server, ulong user)
{
lock (Collection)
return Collection.Where(x => x.Server.Id == server && x.User.Id == user).FirstOrDefault();
}
public DiscordMember Id(DiscordServer server, ulong user)
{
lock (Collection)
return Collection.Where(x => x.Server == server && x.User.Id == user).FirstOrDefault();
}
public DiscordMember Id(DiscordServer server, DiscordUser user)
{
lock (Collection)
return Collection.Where(x => x.Server == server && x.User == user).FirstOrDefault();
}
public IEnumerable<DiscordMember> Server(DiscordServer server)
{
lock (Collection)
return Collection.Where(x => x.Server == server);
}
public IEnumerable<DiscordMember> Role(DiscordRole role)
{
lock (Collection)
return Collection.Where(x => x.HasRole(role.Id));
}
}
}

30
Maki/MessageManager.cs Normal file
View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
namespace Maki
{
internal sealed class MessageManager : BaseManager<DiscordMessage>
{
internal MessageManager()
{
}
public DiscordMessage Id(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).FirstOrDefault();
}
public IEnumerable<DiscordMessage> Channel(DiscordChannel channel)
{
lock (Collection)
return Collection.Where(x => x.Channel == channel);
}
public IEnumerable<DiscordMessage> Server(DiscordServer server)
{
lock (Collection)
return Collection.Where(x => x.Server == server);
}
}
}

View file

@ -1,36 +1,10 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Maki")]
[assembly: AssemblyDescription("Discord Client library")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Maki")]
[assembly: AssemblyCopyright("flash.moe 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("97523aed-b694-42c2-96ac-86a1d65109f7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.*")]

View file

@ -3,7 +3,7 @@
/// <summary>
/// Discord Rest request methods
/// </summary>
enum HttpMethod
public enum HttpMethod
{
/// <summary>
/// GET, does not send additional data

View file

@ -8,17 +8,17 @@
/// <summary>
/// Base URL of Discord
/// </summary>
public const string BASE_URL = @"https://discordapp.com";
public const string BASE_URL = "https://discordapp.com";
/// <summary>
/// Path to the REST API
/// </summary>
public static string BASE_PATH => @"/api/v" + Discord.GATEWAY_VERSION;
public static string BASE_PATH => "/api/v" + Discord.GATEWAY_VERSION;
/// <summary>
/// Url of Discord's CDN
/// </summary>
public const string CDN_URL = @"https://cdn.discordapp.com";
public const string CDN_URL = "https://cdn.discordapp.com";
#region Channels
/// <summary>
@ -349,18 +349,6 @@
public static string OAuth2Application(ulong appId) => $@"/oauth2/applications/{appId}";
#endregion
#region Auth
/// <summary>
/// Auth Login endpoint
/// </summary>
public static string AuthLogin => @"/auth/login";
/// <summary>
/// Auth MFA TOTP endpoint
/// </summary>
public static string AuthMfaTotp => @"/auth/mfa/totp";
#endregion
#region User
/// <summary>
/// Gets a User endpoint

View file

@ -4,257 +4,495 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Maki.Rest
{
internal class WebRequest : IDisposable
public class WebRequest : IDisposable
{
private const string USER_AGENT = @"DiscordBot (https://github.com/flashwave/maki, 1.0.0.0)";
private const string GENERIC_CONTENT_TYPE = @"application/octet-stream";
private const string JSON_CONTENT_TYPE = @"application/json";
private const string FORM_CONTENT_TYPE = @"multipart/form-data";
internal readonly HttpMethod Method;
internal readonly string Url;
private const int MAX_RETRIES = 1;
private const int TIMEOUT = 10000;
internal string ContentType { get; set; } = GENERIC_CONTENT_TYPE;
public int Timeout { get; set; } = TIMEOUT;
// TODO: make this not static
public event Action Started;
public event Action Finished;
public event Action<Exception> Failed;
public event Action<long, long> DownloadProgress;
public event Action<long, long> UploadProgress;
public bool IsAborted { get; private set; }
public string Accept { get; set; }
private bool PrivateCompleted;
public bool IsCompleted
{
get => PrivateCompleted;
private set
{
PrivateCompleted = value;
if (!PrivateCompleted)
return;
Started = null;
Finished = null;
DownloadProgress = null;
UploadProgress = null;
}
}
private string PrivateUrl;
private string Url
{
get => PrivateUrl;
set
{
if (!value.StartsWith(@"http://") && !value.StartsWith(@"https://"))
value = RestEndpoints.BASE_URL + RestEndpoints.BASE_PATH + value;
PrivateUrl = value;
}
}
private const int BUFFER_SIZE = 4096;
private byte[] Buffer;
private static HttpClient HttpClient;
public readonly HttpMethod Method;
private bool HasBody => Method == HttpMethod.PUT || Method == HttpMethod.POST || Method == HttpMethod.PATCH;
public string ContentType { get; set; } = GENERIC_CONTENT_TYPE;
[Obsolete]
public long ContentLength => Response.Content.Headers.ContentLength ?? BytesDownloaded;
[Obsolete]
internal static string Authorisation { get; set; }
private readonly Dictionary<string, string> headers = new Dictionary<string, string>();
private readonly Dictionary<string, string> parameters = new Dictionary<string, string>();
private readonly Dictionary<string, byte[]> files = new Dictionary<string, byte[]>();
private readonly Dictionary<string, string> Headers = new Dictionary<string, string>();
private readonly Dictionary<string, string> Parameters = new Dictionary<string, string>();
private readonly Dictionary<string, byte[]> Files = new Dictionary<string, byte[]>();
private readonly Dictionary<string, string> mimeTypes = new Dictionary<string, string>()
{
{ "png", "image/png" },
{ "jpg", "image/jpeg" },
{ "jpeg", "image/jpeg" },
{ "gif", "image/gif" },
};
private long BytesUploaded = 0;
private long BytesDownloaded = 0;
private byte[] rawContent = new byte[0];
private HttpWebRequest wRequest;
private Stream requestStream;
private HttpWebResponse wResponse;
private Stream responseStream;
private HttpResponseMessage Response;
private byte[] rawResponse;
internal byte[] RawResponse
private MemoryStream RawRequestBody;
private Stream RequestStream;
private Stream ResponseStream;
private CancellationTokenSource AbortToken;
private CancellationTokenSource TimeoutToken;
public int Retries { get; private set; } = 0;
private byte[] PrivateResponseBytes;
public byte[] ResponseBytes
{
get
{
if (rawResponse == null)
{
rawResponse = new byte[4096];
responseStream.Read(rawResponse, 0, rawResponse.Length);
}
if (PrivateResponseBytes == null)
using (MemoryStream ms = new MemoryStream())
{
byte[] bytes = new byte[4096];
int read = 0;
return rawResponse;
while ((read = ResponseStream.Read(bytes, 0, bytes.Length)) > 0)
ms.Write(bytes, 0, read);
ms.Seek(0, SeekOrigin.Begin);
PrivateResponseBytes = new byte[ms.Length];
ms.Read(PrivateResponseBytes, 0, PrivateResponseBytes.Length);
}
return PrivateResponseBytes;
}
}
private string responseString = string.Empty;
internal string Response
private string PrivateResponseString = string.Empty;
public string ResponseString
{
get
{
if (string.IsNullOrEmpty(responseString))
responseString = Encoding.UTF8.GetString(RawResponse).Trim('\0');
if (string.IsNullOrEmpty(PrivateResponseString))
PrivateResponseString = Encoding.UTF8.GetString(ResponseBytes);
return responseString;
return PrivateResponseString;
}
}
internal T ResponseJson<T>() =>
JsonConvert.DeserializeObject<T>(Response);
public T ResponseJson<T>() =>
JsonConvert.DeserializeObject<T>(ResponseString);
internal short Status =>
(short)wResponse?.StatusCode;
[Obsolete]
public short Status => (short)Response?.StatusCode;
static WebRequest()
{
ServicePointManager.Expect100Continue = false;
}
=> CreateHttpClientInstance();
internal WebRequest(HttpMethod method, string url)
public WebRequest(HttpMethod method, string url)
{
Method = method;
Url = url;
}
internal void AddRaw(byte[] bytes) =>
rawContent = bytes;
private static void CreateHttpClientInstance()
{
HttpClient?.Dispose();
internal void AddRaw(string str) =>
HttpClient = new HttpClient(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
HttpClient.DefaultRequestHeaders.ExpectContinue = true;
HttpClient.Timeout = new TimeSpan(0, 0, 0, 0, System.Threading.Timeout.Infinite);
}
public void AddRaw(Stream stream)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
RawRequestBody?.Dispose();
RawRequestBody = new MemoryStream();
stream.CopyTo(RawRequestBody);
}
public void AddRaw(byte[] bytes)
{
using (MemoryStream ms = new MemoryStream(bytes))
AddRaw(ms);
}
public void AddRaw(string str) =>
AddRaw(Encoding.UTF8.GetBytes(str));
internal void AddJson(object obj)
public void AddJson(object obj)
{
ContentType = JSON_CONTENT_TYPE;
AddRaw(JsonConvert.SerializeObject(obj));
}
internal void AddParam(string name, string contents) =>
parameters.Add(name, contents);
public void AddParam(string name, string contents) =>
Parameters.Add(name, contents);
internal void AddFile(string name, byte[] bytes) =>
files.Add(name, bytes);
public void AddFile(string name, byte[] bytes) =>
Files.Add(name, bytes);
internal void Perform()
public void AddHeader(string name, string value)
{
StringBuilder urlBuilder = new StringBuilder();
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
if (!Url.StartsWith("http://") || !Url.StartsWith("https://"))
{
urlBuilder.Append(RestEndpoints.BASE_URL);
urlBuilder.Append(RestEndpoints.BASE_PATH);
}
if (value == null)
throw new ArgumentNullException(nameof(value));
urlBuilder.Append(Url);
if (Headers.ContainsKey(name))
Headers[name] = value;
else
Headers.Add(name, value);
}
if (Method == HttpMethod.GET
|| Method == HttpMethod.DELETE)
if (parameters.Count > 1)
{
if (!Url.Contains('?'))
urlBuilder.Append(@"?");
foreach (KeyValuePair<string, string> param in parameters)
urlBuilder.Append($@"{param.Key}={param.Value}&");
}
string url = urlBuilder.ToString().TrimEnd('&');
wRequest = System.Net.WebRequest.Create(url) as HttpWebRequest;
wRequest.Method = Method.ToString();
wRequest.UserAgent = USER_AGENT;
wRequest.KeepAlive = true;
//wRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
wRequest.ReadWriteTimeout = Timeout.Infinite;
wRequest.Timeout = Timeout.Infinite;
if (!string.IsNullOrEmpty(Authorisation))
wRequest.Headers[HttpRequestHeader.Authorization] = Authorisation;
foreach (KeyValuePair<string, string> header in headers)
wRequest.Headers[header.Key] = header.Value;
if (Method == HttpMethod.POST
|| Method == HttpMethod.PUT
|| Method == HttpMethod.PATCH)
{
requestStream = wRequest.GetRequestStream();
if (parameters.Count + files.Count < 1)
requestStream.Write(rawContent, 0, rawContent.Length);
else
{
string boundary = $@"-----------------------------{DateTime.Now.Ticks}";
ContentType = $@"{FORM_CONTENT_TYPE}; boundary={boundary}";
if (parameters.Count >= 1)
{
StringBuilder postBodyBuilder = new StringBuilder();
byte[] postBody = new byte[0];
foreach (KeyValuePair<string, string> param in parameters)
{
postBodyBuilder.AppendLine($@"--{boundary}");
postBodyBuilder.AppendLine($@"Content-Disposition: form-data; name=""{param.Key}""");
postBodyBuilder.AppendLine();
postBodyBuilder.AppendLine(param.Value);
}
postBody = Encoding.UTF8.GetBytes(postBodyBuilder.ToString());
requestStream.Write(postBody, 0, postBody.Length);
}
if (files.Count >= 1)
{
byte[] boundaryBytes = Encoding.UTF8.GetBytes($@"--{boundary}");
byte[] newLineBytes = Encoding.UTF8.GetBytes("\r\n");
foreach (KeyValuePair<string, byte[]> file in files)
{
string cType = GENERIC_CONTENT_TYPE;
string fileExt = Path.GetExtension(file.Key).ToLower().TrimStart('.');
if (mimeTypes.ContainsKey(fileExt))
cType = mimeTypes[fileExt];
byte[] cDisposBytes = Encoding.UTF8.GetBytes($@"Content-Disposition: form-data; name=""{file.Key}""; filename=""{file.Key}""");
byte[] cTypeBytes = Encoding.UTF8.GetBytes($@"Content-Type: {cType}");
// Boundary + newline
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Write(newLineBytes, 0, newLineBytes.Length);
// Disposition header + newline
requestStream.Write(cDisposBytes, 0, cDisposBytes.Length);
requestStream.Write(newLineBytes, 0, newLineBytes.Length);
// Type header + newline
requestStream.Write(cTypeBytes, 0, cTypeBytes.Length);
requestStream.Write(newLineBytes, 0, newLineBytes.Length);
// newline + contents + newline
requestStream.Write(newLineBytes, 0, newLineBytes.Length);
requestStream.Write(file.Value, 0, file.Value.Length);
requestStream.Write(newLineBytes, 0, newLineBytes.Length);
}
}
byte[] closingBound = Encoding.UTF8.GetBytes($@"--{boundary}--");
requestStream.Write(closingBound, 0, closingBound.Length);
}
}
wRequest.ContentType = ContentType;
public void Abort()
{
IsAborted = true;
IsCompleted = true;
try
{
wResponse = wRequest.GetResponse() as HttpWebResponse;
} catch (WebException ex)
{
wResponse = ex.Response as HttpWebResponse;
AbortToken?.Cancel();
}
catch (ObjectDisposedException)
{
// just do nothign in this case
}
responseStream = wResponse.GetResponseStream();
}
#region IDisposable
private bool isDisposed = false;
private System.Net.Http.HttpMethod FromInternalHttpMethod(HttpMethod method)
{
switch (method)
{
case HttpMethod.GET:
return System.Net.Http.HttpMethod.Get;
case HttpMethod.DELETE:
return System.Net.Http.HttpMethod.Delete;
case HttpMethod.POST:
return System.Net.Http.HttpMethod.Post;
case HttpMethod.PATCH:
return new System.Net.Http.HttpMethod(@"PATCH");
case HttpMethod.PUT:
return System.Net.Http.HttpMethod.Put;
}
throw new InvalidOperationException($"Unsupported HTTP method {method}.");
}
private void PrivatePerform()
{
using (AbortToken = new CancellationTokenSource())
using (TimeoutToken = new CancellationTokenSource())
using (CancellationTokenSource linkedToken = CancellationTokenSource.CreateLinkedTokenSource(AbortToken.Token, TimeoutToken.Token))
{
try
{
string requestUri = Url;
HttpRequestMessage request = new HttpRequestMessage(FromInternalHttpMethod(Method), requestUri);
foreach (KeyValuePair<string, string> h in Headers)
request.Headers.Add(h.Key, h.Value);
if (!string.IsNullOrEmpty(Accept))
request.Headers.Accept.TryParseAdd(Accept);
if (HasBody)
{
Stream bodyContent;
if (RawRequestBody == null)
{
MultipartFormDataContent formData = new MultipartFormDataContent();
foreach (KeyValuePair<string, string> p in Parameters)
formData.Add(new StringContent(p.Value), p.Key);
foreach (KeyValuePair<string, byte[]> f in Files)
{
ByteArrayContent bac = new ByteArrayContent(f.Value);
bac.Headers.Add("Content-Type", GENERIC_CONTENT_TYPE);
formData.Add(bac, f.Key, f.Key);
}
bodyContent = formData.ReadAsStreamAsync().Result;
} else
{
if (Parameters.Count > 0 || Files.Count > 0)
throw new InvalidOperationException($"You cannot use {nameof(AddRaw)} at the same time as {nameof(AddParam)} or {nameof(AddFile)}");
bodyContent = new MemoryStream();
RawRequestBody.Seek(0, SeekOrigin.Begin);
RawRequestBody.CopyTo(bodyContent);
bodyContent.Seek(0, SeekOrigin.Begin);
}
request.Content = new StreamContent(RequestStream);
if (!string.IsNullOrEmpty(ContentType))
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ContentType);
}
else
{
if (Parameters.Count > 1)
{
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.Append(Url);
if (!Url.Contains('?'))
urlBuilder.Append('?');
foreach (KeyValuePair<string, string> param in Parameters)
{
urlBuilder.Append(param.Key);
urlBuilder.Append('=');
urlBuilder.Append(param.Value);
urlBuilder.Append('&');
}
urlBuilder.Length -= 1;
requestUri = urlBuilder.ToString();
}
}
ReportProgress();
using (request)
Response = HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, linkedToken.Token).Result;
ResponseStream = new MemoryStream();
if (HasBody)
{
ReportProgress();
UploadProgress?.Invoke(0, BytesUploaded);
}
HandleResponse(linkedToken.Token);
} catch (Exception) when (AbortToken.IsCancellationRequested)
{
Complete(new WebException(string.Format("Request to {0} was aborted by the user.", Url), WebExceptionStatus.RequestCanceled));
} catch (Exception) when (TimeoutToken.IsCancellationRequested)
{
Complete(new WebException(string.Format("Request to {0} timed out after {1:N0} seconds idle (read {2:N0} bytes).", Url, TimeSinceLastAction / 1000, BytesDownloaded), WebExceptionStatus.Timeout));
} catch (Exception ex)
{
if (IsCompleted)
throw;
Complete(ex);
}
}
}
public async Task PerformAsync()
{
if (IsCompleted)
throw new InvalidOperationException($"{nameof(WebRequest)} has already been run, you can't reuse WebRequest objects.");
try
{
await Task.Factory.StartNew(PrivatePerform, TaskCreationOptions.LongRunning);
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.Count != 1)
throw ex;
while (ex.InnerExceptions.Count == 1)
{
AggregateException innerEx = ex.InnerException as AggregateException;
ex = innerEx ?? throw innerEx.InnerException;
}
throw ex;
}
}
public void Perform()
=> PerformAsync().Wait();
private void HandleResponse(CancellationToken cancellationToken)
{
using (Stream responseStream = Response.Content.ReadAsStreamAsync().Result)
{
Started?.Invoke();
Buffer = new byte[BUFFER_SIZE];
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
int read = responseStream.Read(Buffer, 0, BUFFER_SIZE);
ReportProgress();
if (read > 0)
{
ResponseStream.Write(Buffer, 0, read);
BytesDownloaded += read;
DownloadProgress?.Invoke(BytesDownloaded, Response.Content.Headers.ContentLength ?? BytesDownloaded);
} else
{
ResponseStream.Seek(0, SeekOrigin.Begin);
break;
}
}
}
}
private void Complete(Exception exception = null)
{
if (IsAborted || IsCompleted)
return;
bool allowRetry = true;
if (exception != null)
{
allowRetry = exception is WebException && (exception as WebException)?.Status == WebExceptionStatus.Timeout;
} else if (!Response.IsSuccessStatusCode)
{
exception = new WebException($@"HTTP {Response.StatusCode}");
switch (Response.StatusCode)
{
case HttpStatusCode.NotFound:
case HttpStatusCode.MethodNotAllowed:
case HttpStatusCode.Forbidden:
case HttpStatusCode.Unauthorized:
allowRetry = false;
break;
}
}
if (exception != null)
if (allowRetry && Retries < MAX_RETRIES && BytesDownloaded < 1)
{
++Retries;
PrivatePerform();
}
try
{
// process
} catch (Exception ex)
{
exception = exception == null ? ex : new AggregateException(exception, ex);
}
IsCompleted = true;
if (exception != null)
{
IsAborted = true;
Failed?.Invoke(exception);
throw exception;
}
Finished?.Invoke();
}
#region Timeout
private long LastReportedAction = 0;
private long TimeSinceLastAction => (DateTime.Now.Ticks - LastReportedAction) / TimeSpan.TicksPerMillisecond;
private void ReportProgress()
{
LastReportedAction = DateTime.Now.Ticks;
TimeoutToken.CancelAfter(Timeout);
}
#endregion
#region Disposal
public bool IsDisposed { get; private set; } = false;
private void Dispose(bool disposing)
{
if (!isDisposed)
{
isDisposed = true;
requestStream?.Dispose();
wRequest?.Abort();
responseStream?.Dispose();
wResponse?.Close();
}
if (IsDisposed)
return;
IsDisposed = true;
// TODO: reimplement disposal
if (disposing)
GC.SuppressFinalize(this);
}
~WebRequest()
{
Dispose(false);
}
=> Dispose(false);
/// <summary>
/// Disconnects and releases all unmanaged objects
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(true);
}
=> Dispose(true);
#endregion
}
}

30
Maki/RoleManager.cs Normal file
View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
namespace Maki
{
internal sealed class RoleManager : BaseManager<DiscordRole>
{
internal RoleManager()
{
}
public DiscordRole Id(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).FirstOrDefault();
}
public bool Exists(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).Count() > 0;
}
public IEnumerable<DiscordRole> Server(DiscordServer server)
{
lock (Collection)
return Collection.Where(x => x.Server == server).OrderByDescending(x => x.Position);
}
}
}

23
Maki/ServerManager.cs Normal file
View file

@ -0,0 +1,23 @@
using System.Linq;
namespace Maki
{
internal sealed class ServerManager : BaseManager<DiscordServer>
{
internal ServerManager()
{
}
public DiscordServer Id(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).FirstOrDefault();
}
public bool Exists(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).Count() > 0;
}
}
}

View file

@ -1,22 +0,0 @@
using Newtonsoft.Json;
namespace Maki.Structures.Auth
{
/// <summary>
/// Multi Factor Auth login request
/// </summary>
internal struct LoginMultiFactorAuth
{
/// <summary>
/// Multi factor auth code
/// </summary>
[JsonProperty("code")]
public string Code;
/// <summary>
/// Multi factor auth ticket
/// </summary>
[JsonProperty("ticket")]
public string Ticket;
}
}

View file

@ -1,22 +0,0 @@
using Newtonsoft.Json;
namespace Maki.Structures.Auth
{
/// <summary>
/// Login request
/// </summary>
internal struct LoginRequest
{
/// <summary>
/// E-mail address
/// </summary>
[JsonProperty("email")]
public string Email;
/// <summary>
/// Password
/// </summary>
[JsonProperty("password")]
public string Password;
}
}

View file

@ -1,40 +0,0 @@
using Newtonsoft.Json;
namespace Maki.Structures.Auth
{
/// <summary>
/// Login response
/// </summary>
internal struct LoginResponse
{
/// <summary>
/// Indicates whether we require multi factor, is usually either null or true
/// </summary>
[JsonProperty("mfa")]
public bool? MFA;
/// <summary>
/// Token
/// </summary>
[JsonProperty("token")]
public string Token;
/// <summary>
/// MFA Ticket, to be bundled with the mfa request
/// </summary>
[JsonProperty("ticket")]
public string Ticket;
/// <summary>
/// Username errors
/// </summary>
[JsonProperty("username")]
public string[] UsernameError;
/// <summary>
/// Password errors
/// </summary>
[JsonProperty("password")]
public string[] PasswordError;
}
}

17
Maki/UserManager.cs Normal file
View file

@ -0,0 +1,17 @@
using System.Linq;
namespace Maki
{
internal sealed class UserManager : BaseManager<DiscordUser>
{
internal UserManager()
{
}
public DiscordUser Id(ulong id)
{
lock (Collection)
return Collection.Where(x => x.Id == id).FirstOrDefault();
}
}
}

View file

@ -4,11 +4,8 @@ namespace Maki
{
internal static class Utility
{
public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static readonly DateTime DiscordEpoch = new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static DateTime FromUnixTimeMilliseconds(long ms) => UnixEpoch.AddMilliseconds(ms);
public static DateTime FromDiscordTimeMilliseconds(long ms) => DiscordEpoch.AddMilliseconds(ms);
public static DateTime FromDiscordSnowflake(ulong snowflake) => FromDiscordTimeMilliseconds((long)snowflake >> 22);

19
Maki/app.config Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.2.29.0" newVersion="2.2.29.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.6.10.0" newVersion="2.6.10.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="Microsoft.Bcl" version="1.1.10" targetFramework="net40" />
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net40" />
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net40" />
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="net40" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40" />
<package id="WebSocketSharp" version="1.0.3-rc11" targetFramework="net40" />
</packages>

View file

@ -34,12 +34,6 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />

View file

@ -1,28 +1,51 @@
using Maki;
using Maki.Rest;
using System;
using System.IO;
using System.Threading;
namespace MakiTest
namespace Maki.Testing
{
class Program
{
static void Main(string[] args)
public static void Main(string[] args)
{
TestWebRequest(false, HttpMethod.GET, WEB_REQUEST_GET);
TestWebRequest(true, HttpMethod.GET, WEB_REQUEST_GET);
//TestMainLibrary();
Console.WriteLine("Stopped, press any key to close the program...");
Console.ReadKey();
}
#region WebRequest tests
private const string WEB_REQUEST_SECURE = @"https://";
private const string WEB_REQUEST_INSECURE = @"http://";
private const string WEB_REQUEST_BASE = @"httpbin.org";
private const string WEB_REQUEST_GET = WEB_REQUEST_BASE + @"/get";
private const string WEB_REQUEST_FAIL = @"thisshouldneveresolve.flash.moe";
private static void TestWebRequest(bool secure, HttpMethod method, string url)
{
url = (secure ? WEB_REQUEST_SECURE : WEB_REQUEST_INSECURE) + url;
using (WebRequest wr = new WebRequest(method, url))
{
wr.Perform();
Console.WriteLine(wr.ResponseString);
}
}
#endregion
private static void TestMainLibrary()
{
string[] tokenInfo = File.ReadAllLines("token.txt");
string token = tokenInfo[0] ?? string.Empty;
DiscordTokenType type = (tokenInfo[1] ?? string.Empty) == "user" ? DiscordTokenType.User : DiscordTokenType.Bot;
string username = string.Empty;
string password = string.Empty;
string mfaCode = null;
if (tokenInfo.Length >= 5)
{
username = tokenInfo[2];
password = tokenInfo[3];
mfaCode = tokenInfo[4];
}
else if (string.IsNullOrEmpty(token))
if (string.IsNullOrEmpty(token))
throw new Exception("Please set a token or login details.");
using (ManualResetEvent mre = new ManualResetEvent(false))
@ -30,39 +53,33 @@ namespace MakiTest
{
Console.CancelKeyPress += (s, e) => { e.Cancel = true; mre.Set(); };
if (!string.IsNullOrEmpty(token))
client.Connect(token, type);
else
client.Connect(username, password, mfaCode);
client.Connect(token, type);
client.OnReady += (me) => Console.WriteLine($"Connected as {me.NameWithTag} ({me.Id})!");
client.OnServerCreate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) created.");
client.OnServerUpdate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) updated.");
client.OnServerDelete += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) deleted.");
client.OnEmojisUpdate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) emojis updated.");
client.OnChannelCreate += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) created.");
client.OnChannelUpdate += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) updated.");
client.OnChannelDelete += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) deleted.");
client.OnBanAdd += (user, serv) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) been banned from {serv.Name} ({serv.Id}).");
client.OnBanRemove += (user, serv) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) been unbanned from {serv.Name} ({serv.Id}).");
client.OnMemberAdd += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) joined {member.Server.Name} ({member.Server.Id}).");
client.OnMemberRemove += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) left {member.Server.Name} ({member.Server.Id}).");
client.OnMemberUpdate += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) was updated for {member.Server.Name} ({member.Server.Id}).");
client.OnMessageCreate += (msg) => Console.WriteLine($"{msg.Sender.NameWithTag} ({msg.Sender.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}: {msg.Text}");
client.OnMessageUpdate += (msg) => Console.WriteLine($"{msg.Sender.NameWithTag} ({msg.Sender.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}: {msg.Text}");
client.OnMessageDelete += (msg) => Console.WriteLine($"{msg.Sender.NameWithTag} ({msg.Sender.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}");
client.OnRoleCreate += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) created in {role.Server.Name} ({role.Server.Id}).");
client.OnRoleUpdate += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) updated in {role.Server.Name} ({role.Server.Id}).");
client.OnRoleDelete += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) deleted in {role.Server.Name} ({role.Server.Id}).");
client.OnTypingStart += (user, chan) => Console.WriteLine($"{user.NameWithTag} ({user.User.Id}) started typing in #{chan.Name} ({chan.Id}) in {user.Server.Name} ({user.Server.Id})");
client.OnPresenceUpdate += (user) => Console.WriteLine($"Presence of {user.NameWithTag} ({user.User.Id}) update for {user.Server.Name} ({user.Server.Id}).");
client.OnUserUpdate += (user) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) updated.");
client.OnReady += (me) => Console.WriteLine($"Connected as {me.NameWithTag} ({me.Id})!");
client.OnServerCreate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) created.");
client.OnServerUpdate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) updated.");
client.OnServerDelete += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) deleted.");
client.OnEmojisUpdate += (serv) => Console.WriteLine($"Server {serv.Name} ({serv.Id}) emojis updated.");
client.OnChannelCreate += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) created.");
client.OnChannelUpdate += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) updated.");
client.OnChannelDelete += (chan) => Console.WriteLine($"Channel #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id}) deleted.");
client.OnBanAdd += (user, serv) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) been banned from {serv.Name} ({serv.Id}).");
client.OnBanRemove += (user, serv) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) been unbanned from {serv.Name} ({serv.Id}).");
client.OnMemberAdd += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) joined {member.Server.Name} ({member.Server.Id}).");
client.OnMemberRemove += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) left {member.Server.Name} ({member.Server.Id}).");
client.OnMemberUpdate += (member) => Console.WriteLine($"{member.NameWithTag} ({member.User.Id}) was updated for {member.Server.Name} ({member.Server.Id}).");
client.OnMessageCreate += (msg) => Console.WriteLine($"{msg.User.NameWithTag} ({msg.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}: {msg.Text}");
client.OnMessageUpdate += (msg) => Console.WriteLine($"{msg.User.NameWithTag} ({msg.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}: {msg.Text}");
client.OnMessageDelete += (msg) => Console.WriteLine($"{msg.User.NameWithTag} ({msg.User.Id}) in {msg.Channel.Server.Name} ({msg.Channel.Server.Id}) #{msg.Channel.Name} ({msg.Channel.Id}) {msg.Id}");
client.OnRoleCreate += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) created in {role.Server.Name} ({role.Server.Id}).");
client.OnRoleUpdate += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) updated in {role.Server.Name} ({role.Server.Id}).");
client.OnRoleDelete += (role) => Console.WriteLine($"Role {role.Name} ({role.Id}) deleted in {role.Server.Name} ({role.Server.Id}).");
client.OnTypingStart += (user, chan) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) started typing in #{chan.Name} ({chan.Id}) in {chan.Server.Name} ({chan.Server.Id})");
client.OnPresenceUpdate += (user) => Console.WriteLine($"Presence of {user.NameWithTag} ({user.User.Id}) update for {user.Server.Name} ({user.Server.Id}).");
client.OnUserUpdate += (user) => Console.WriteLine($"{user.NameWithTag} ({user.Id}) updated.");
mre.WaitOne();
}
Console.WriteLine("Stopped, press any key to close the program...");
Console.ReadKey();
}
}
}