Browse Source

Refactored DiscordClient, added argument validations to API and connection functions

tags/docs-0.9
RogueException 9 years ago
parent
commit
597462857c
11 changed files with 1178 additions and 1049 deletions
  1. +1
    -1
      src/Discord.Net.Commands/DiscordBotClient.cs
  2. +9
    -0
      src/Discord.Net.Net45/Discord.Net.csproj
  3. +2
    -2
      src/Discord.Net/API/DiscordAPI.cs
  4. +559
    -0
      src/Discord.Net/DiscordClient.API.cs
  5. +457
    -0
      src/Discord.Net/DiscordClient.Cache.cs
  6. +103
    -1028
      src/Discord.Net/DiscordClient.cs
  7. +9
    -3
      src/Discord.Net/DiscordWebSocket.Events.cs
  8. +12
    -10
      src/Discord.Net/DiscordWebSocket.cs
  9. +21
    -0
      src/Discord.Net/Helpers/Extensions.cs
  10. +1
    -1
      src/Discord.Net/Message.cs
  11. +4
    -4
      src/Discord.Net/Server.cs

+ 1
- 1
src/Discord.Net.Commands/DiscordBotClient.cs View File

@@ -28,7 +28,7 @@ namespace Discord
MessageCreated += async (s, e) =>
{
//Ignore messages from ourselves
if (e.Message.UserId == UserId)
if (e.Message.UserId == _myId)
return;

//Check for the command character


+ 9
- 0
src/Discord.Net.Net45/Discord.Net.csproj View File

@@ -80,6 +80,12 @@
<Compile Include="..\Discord.Net\ChannelTypes.cs">
<Link>ChannelTypes.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.API.cs">
<Link>DiscordClient.API.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.Cache.cs">
<Link>DiscordClient.Cache.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\DiscordClient.cs">
<Link>DiscordClient.cs</Link>
</Compile>
@@ -107,6 +113,9 @@
<Compile Include="..\Discord.Net\Helpers\AsyncCache.cs">
<Link>Helpers\AsyncCache.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\Extensions.cs">
<Link>Helpers\Extensions.cs</Link>
</Compile>
<Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs">
<Link>Helpers\JsonHttpClient.cs</Link>
</Compile>


+ 2
- 2
src/Discord.Net/API/DiscordAPI.cs View File

@@ -141,10 +141,10 @@ namespace Discord.API
var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}
public Task<SelfUserInfo> ChangeAvatar(DiscordClient.AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
public Task<SelfUserInfo> ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
{
string base64 = Convert.ToBase64String(bytes);
string type = imageType == DiscordClient.AvatarImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64";
string type = imageType == AvatarImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64";
var request = new APIRequests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword };
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request);
}


+ 559
- 0
src/Discord.Net/DiscordClient.API.cs View File

@@ -0,0 +1,559 @@
using Discord.API;
using Discord.API.Models;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
public enum AvatarImageType
{
Jpeg,
Png
}
public partial class DiscordClient
{
//Servers
/// <summary> Creates a new server with the provided name and region (see Regions). </summary>
public async Task<Server> CreateServer(string name, string region)
{
CheckReady();
if (name == null) throw new ArgumentNullException(nameof(name));
if (region == null) throw new ArgumentNullException(nameof(region));

var response = await _api.CreateServer(name, region);
return _servers.Update(response.Id, response);
}

/// <summary> Leaves the provided server, destroying it if you are the owner. </summary>
public Task<Server> LeaveServer(Server server)
=> LeaveServer(server?.Id);
/// <summary> Leaves the provided server, destroying it if you are the owner. </summary>
public async Task<Server> LeaveServer(string serverId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));

try { await _api.LeaveServer(serverId); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
return _servers.Remove(serverId);
}

//Channels
/// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary>
public Task<Channel> CreateChannel(Server server, string name, string type)
=> CreateChannel(server?.Id, name, type);
/// <summary> Creates a new channel with the provided name and type (see ChannelTypes). </summary>
public async Task<Channel> CreateChannel(string serverId, string name, string type)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (name == null) throw new ArgumentNullException(nameof(name));
if (type == null) throw new ArgumentNullException(nameof(type));

var response = await _api.CreateChannel(serverId, name, type);
return _channels.Update(response.Id, serverId, response);
}

/// <summary> Creates a new private channel with the provided user. </summary>
public Task<Channel> CreatePMChannel(User user)
=> CreatePMChannel(user?.Id);
/// <summary> Creates a new private channel with the provided user. </summary>
public async Task<Channel> CreatePMChannel(string userId)
{
CheckReady();
if (userId == null) throw new ArgumentNullException(nameof(userId));

var response = await _api.CreatePMChannel(_myId, userId);
return _channels.Update(response.Id, response);
}

/// <summary> Destroys the provided channel. </summary>
public Task<Channel> DestroyChannel(Channel channel)
=> DestroyChannel(channel?.Id);
/// <summary> Destroys the provided channel. </summary>
public async Task<Channel> DestroyChannel(string channelId)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));

try { await _api.DestroyChannel(channelId); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
return _channels.Remove(channelId);
}

//Bans
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(Server server, User user)
=> Ban(server?.Id, user?.Id);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(Server server, string userId)
=> Ban(server?.Id, userId);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(string server, User user)
=> Ban(server, user?.Id);
/// <summary> Bans a user from the provided server. </summary>
public Task Ban(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return _api.Ban(serverId, userId);
}

/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(Server server, User user)
=> Unban(server?.Id, user?.Id);
/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(Server server, string userId)
=> Unban(server?.Id, userId);
/// <summary> Unbans a user from the provided server. </summary>
public Task Unban(string server, User user)
=> Unban(server, user?.Id);
/// <summary> Unbans a user from the provided server. </summary>
public async Task Unban(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

try { await _api.Unban(serverId, userId); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

//Invites
/// <summary> Creates a new invite to the default channel of the provided server. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. </param>
public Task<Invite> CreateInvite(Server server, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
=> CreateInvite(server?.DefaultChannelId, maxAge, maxUses, isTemporary, hasXkcdPass);
/// <summary> Creates a new invite to the provided channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. </param>
public Task<Invite> CreateInvite(Channel channel, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
=> CreateInvite(channel?.Id, maxAge, maxUses, isTemporary, hasXkcdPass);
/// <summary> Creates a new invite to the provided channel. </summary>
/// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcdPass"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. </param>
public async Task<Invite> CreateInvite(string channelId, int maxAge, int maxUses, bool isTemporary, bool hasXkcdPass)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses));

var response = await _api.CreateInvite(channelId, maxAge, maxUses, isTemporary, hasXkcdPass);
_channels.Update(response.Channel.Id, response.Server.Id, response.Channel);
_servers.Update(response.Server.Id, response.Server);
_users.Update(response.Inviter.Id, response.Inviter);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id,
IsRevoked = response.IsRevoked,
IsTemporary = response.IsTemporary,
MaxAge = response.MaxAge,
MaxUses = response.MaxUses,
Uses = response.Uses
};
}

/// <summary> Gets more info about the provided invite. </summary>
/// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks>
public async Task<Invite> GetInvite(string id)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));

var response = await _api.GetInvite(id);
return new Invite(response.Code, response.XkcdPass, this)
{
ChannelId = response.Channel.Id,
InviterId = response.Inviter.Id,
ServerId = response.Server.Id
};
}

/// <summary> Accepts the provided invite. </summary>
public Task AcceptInvite(Invite invite)
{
CheckReady();
if (invite == null) throw new ArgumentNullException(nameof(invite));

return _api.AcceptInvite(invite.Id);
}
/// <summary> Accepts the provided invite. </summary>
public async Task AcceptInvite(string id)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));

//Remove Url Parts
if (id.StartsWith(Endpoints.BaseShortHttps))
id = id.Substring(Endpoints.BaseShortHttps.Length);
if (id.Length > 0 && id[0] == '/')
id = id.Substring(1);
if (id.Length > 0 && id[id.Length - 1] == '/')
id = id.Substring(0, id.Length - 1);

//Check if this is a human-readable link and get its ID
var response = await _api.GetInvite(id);
await _api.AcceptInvite(response.Code);
}

/// <summary> Deletes the provided invite. </summary>
public async Task DeleteInvite(string id)
{
CheckReady();
if (id == null) throw new ArgumentNullException(nameof(id));

try
{
//Check if this is a human-readable link and get its ID
var response = await _api.GetInvite(id);
await _api.DeleteInvite(response.Code);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

//Chat
/// <summary> Sends a message to the provided channel. </summary>
public Task<Message[]> SendMessage(Channel channel, string text)
=> SendMessage(channel?.Id, text, new string[0]);
/// <summary> Sends a message to the provided channel. </summary>
public Task<Message[]> SendMessage(string channelId, string text)
=> SendMessage(channelId, text, new string[0]);
/// <summary> Sends a message to the provided channel, mentioning certain users. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks>
public Task<Message[]> SendMessage(Channel channel, string text, string[] mentions)
=> SendMessage(channel?.Id, text, mentions);
/// <summary> Sends a message to the provided channel, mentioning certain users. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks>
public async Task<Message[]> SendMessage(string channelId, string text, string[] mentions)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (text == null) throw new ArgumentNullException(nameof(text));
if (mentions == null) throw new ArgumentNullException(nameof(mentions));

int blockCount = (int)Math.Ceiling(text.Length / (double)DiscordAPI.MaxMessageSize);
Message[] result = new Message[blockCount];
for (int i = 0; i < blockCount; i++)
{
int index = i * DiscordAPI.MaxMessageSize;
string blockText = text.Substring(index, Math.Min(2000, text.Length - index));
var nonce = GenerateNonce();
if (_config.UseMessageQueue)
{
var msg = _messages.Update("nonce_" + nonce, channelId, new API.Models.Message
{
Content = blockText,
Timestamp = DateTime.UtcNow,
Author = new UserReference { Avatar = User.AvatarId, Discriminator = User.Discriminator, Id = User.Id, Username = User.Name },
ChannelId = channelId
});
msg.IsQueued = true;
msg.Nonce = nonce;
result[i] = msg;
_pendingMessages.Enqueue(msg);
}
else
{
var msg = await _api.SendMessage(channelId, blockText, mentions, nonce);
result[i] = _messages.Update(msg.Id, channelId, msg);
result[i].Nonce = nonce;
try { RaiseMessageSent(result[i]); } catch { }
}
await Task.Delay(1000);
}
return result;
}

/// <summary> Edits a message the provided message. </summary>
public Task EditMessage(Message message, string text)
=> EditMessage(message?.ChannelId, message?.Id, text, new string[0]);
/// <summary> Edits a message the provided message. </summary>
public Task EditMessage(Channel channel, string messageId, string text)
=> EditMessage(channel?.Id, messageId, text, new string[0]);
/// <summary> Edits a message the provided message. </summary>
public Task EditMessage(string channelId, string messageId, string text)
=> EditMessage(channelId, messageId, text, new string[0]);
/// <summary> Edits a message the provided message, mentioning certain users. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks>
public Task EditMessage(Message message, string text, string[] mentions)
=> EditMessage(message?.ChannelId, message?.Id, text, mentions);
/// <summary> Edits a message the provided message, mentioning certain users. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks>
public Task EditMessage(Channel channel, string messageId, string text, string[] mentions)
=> EditMessage(channel?.Id, messageId, text, mentions);
/// <summary> Edits a message the provided message, mentioning certain users. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see User.Mention). </remarks>
public async Task EditMessage(string channelId, string messageId, string text, string[] mentions)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (messageId == null) throw new ArgumentNullException(nameof(messageId));
if (text == null) throw new ArgumentNullException(nameof(text));
if (mentions == null) throw new ArgumentNullException(nameof(mentions));

if (text.Length > DiscordAPI.MaxMessageSize)
text = text.Substring(0, DiscordAPI.MaxMessageSize);

var msg = await _api.EditMessage(channelId, messageId, text, mentions);
_messages.Update(msg.Id, channelId, msg);
}

/// <summary> Deletes the provided message. </summary>
public Task DeleteMessage(Message msg)
=> DeleteMessage(msg?.ChannelId, msg?.Id);
/// <summary> Deletes the provided message. </summary>
public async Task<Message> DeleteMessage(string channelId, string msgId)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (msgId == null) throw new ArgumentNullException(nameof(msgId));

try
{
await _api.DeleteMessage(channelId, msgId);
return _messages.Remove(msgId);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError) { } //TODO: Remove me - temporary fix for deleting nonexisting messages
return null;
}

/// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(Channel channel, string path)
=> SendFile(channel?.Id, path);
/// <summary> Sends a file to the provided channel. </summary>
public Task SendFile(string channelId, string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
return SendFile(channelId, File.OpenRead(path), Path.GetFileName(path));
}
/// <summary> Reads a stream and sends it to the provided channel as a file. </summary>
/// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks>
public Task SendFile(Channel channel, Stream stream, string filename = null)
=> SendFile(channel?.Id, stream, filename);
/// <summary> Reads a stream and sends it to the provided channel as a file. </summary>
/// <remarks> It is highly recommended that this stream be cached in memory or on disk, or the request may time out. </remarks>
public Task SendFile(string channelId, Stream stream, string filename = null)
{
CheckReady();
if (channelId == null) throw new ArgumentNullException(nameof(channelId));
if (stream == null) throw new ArgumentNullException(nameof(stream));
if (filename == null) throw new ArgumentNullException(nameof(filename));

return _api.SendFile(channelId, stream, filename);
}

/// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary>
public Task<Message[]> DownloadMessages(Channel channel, int count, string beforeMessageId = null)
=> DownloadMessages(channel.Id, count);
/// <summary> Downloads last count messages from the server, starting at beforeMessageId if it's provided. </summary>
public async Task<Message[]> DownloadMessages(string channelId, int count, string beforeMessageId = null)
{
CheckReady();
if (channelId == null) throw new NullReferenceException(nameof(channelId));
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0) return new Message[0];

Channel channel = GetChannel(channelId);
if (channel != null && channel.Type == ChannelTypes.Text)
{
try
{
var msgs = await _api.GetMessages(channel.Id, count);
return msgs.OrderBy(x => x.Timestamp)
.Select(x =>
{
var msg = _messages.Update(x.Id, x.ChannelId, x);
var user = msg.User;
if (user != null)
user.UpdateActivity(x.Timestamp);
return msg;
})
.ToArray();
}
catch (HttpException) { } //Bad Permissions?
}
return null;
}

//Voice
/// <summary> Mutes a user on the provided server. </summary>
public Task Mute(Server server, User user)
=> Mute(server?.Id, user?.Id);
/// <summary> Mutes a user on the provided server. </summary>
public Task Mute(Server server, string userId)
=> Mute(server?.Id, userId);
/// <summary> Mutes a user on the provided server. </summary>
public Task Mute(string server, User user)
=> Mute(server, user?.Id);
/// <summary> Mutes a user on the provided server. </summary>
public Task Mute(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return _api.Mute(serverId, userId);
}

/// <summary> Unmutes a user on the provided server. </summary>
public Task Unmute(Server server, User user)
=> Unmute(server?.Id, user?.Id);
/// <summary> Unmutes a user on the provided server. </summary>
public Task Unmute(Server server, string userId)
=> Unmute(server?.Id, userId);
/// <summary> Unmutes a user on the provided server. </summary>
public Task Unmute(string server, User user)
=> Unmute(server, user?.Id);
/// <summary> Unmutes a user on the provided server. </summary>
public Task Unmute(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return _api.Unmute(serverId, userId);
}

/// <summary> Deafens a user on the provided server. </summary>
public Task Deafen(Server server, User user)
=> Deafen(server?.Id, user?.Id);
/// <summary> Deafens a user on the provided server. </summary>
public Task Deafen(Server server, string userId)
=> Deafen(server?.Id, userId);
/// <summary> Deafens a user on the provided server. </summary>
public Task Deafen(string server, User user)
=> Deafen(server, user?.Id);
/// <summary> Deafens a user on the provided server. </summary>
public Task Deafen(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return _api.Deafen(serverId, userId);
}

/// <summary> Undeafens a user on the provided server. </summary>
public Task Undeafen(Server server, User user)
=> Undeafen(server?.Id, user?.Id);
/// <summary> Undeafens a user on the provided server. </summary>
public Task Undeafen(Server server, string userId)
=> Undeafen(server?.Id, userId);
/// <summary> Undeafens a user on the provided server. </summary>
public Task Undeafen(string server, User user)
=> Undeafen(server, user?.Id);
/// <summary> Undeafens a user on the provided server. </summary>
public Task Undeafen(string serverId, string userId)
{
CheckReady();
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (userId == null) throw new ArgumentNullException(nameof(userId));

return _api.Undeafen(serverId, userId);
}

#if !DNXCORE50
public Task JoinVoiceServer(Server server, Channel channel)
=> JoinVoiceServer(server?.Id, channel?.Id);
public Task JoinVoiceServer(Server server, string channelId)
=> JoinVoiceServer(server?.Id, channelId);
public Task JoinVoiceServer(string serverId, Channel channel)
=> JoinVoiceServer(serverId, channel?.Id);
public async Task JoinVoiceServer(string serverId, string channelId)
{
CheckReady();
if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
if (serverId == null) throw new ArgumentNullException(nameof(serverId));
if (channelId == null) throw new ArgumentNullException(nameof(channelId));

await LeaveVoiceServer();
_currentVoiceServerId = serverId;
_webSocket.JoinVoice(serverId, channelId);
}

public async Task LeaveVoiceServer()
{
if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");

await _voiceWebSocket.DisconnectAsync();
if (_currentVoiceServerId != null)
_webSocket.LeaveVoice();
_currentVoiceServerId = null;
_currentVoiceToken = null;
}

/// <summary> Sends a PCM frame to the voice server. </summary>
/// <param name="data">PCM frame to send.</param>
/// <param name="count">Number of bytes in this frame. </param>
public void SendVoicePCM(byte[] data, int count)
{
CheckReady();
if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");
if (count == 0) return;

if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Queued {count} bytes for voice output.");
_voiceWebSocket.SendPCMFrame(data, count);
}

/// <summary> Clears the PCM buffer. </summary>
public void ClearVoicePCM()
{
CheckReady();
if (!_config.EnableVoice) throw new InvalidOperationException("Voice is not enabled for this client.");

if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.VoiceOutput, $"Cleared the voice buffer.");
_voiceWebSocket.ClearPCMFrames();
}
#endif

//Profile
/// <summary> Changes your username to newName. </summary>
public async Task ChangeUsername(string newName, string currentEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangeUsername(newName, currentEmail, currentPassword);
_users.Update(response.Id, response);
}
/// <summary> Changes your email to newEmail. </summary>
public async Task ChangeEmail(string newEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangeEmail(newEmail, currentPassword);
_users.Update(response.Id, response);
}
/// <summary> Changes your password to newPassword. </summary>
public async Task ChangePassword(string newPassword, string currentEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangePassword(newPassword, currentEmail, currentPassword);
_users.Update(response.Id, response);
}

/// <summary> Changes your avatar. </summary>
/// <remarks>Only supports PNG and JPEG (see AvatarImageType)</remarks>
public async Task ChangeAvatar(AvatarImageType imageType, byte[] bytes, string currentEmail, string currentPassword)
{
CheckReady();
var response = await _api.ChangeAvatar(imageType, bytes, currentEmail, currentPassword);
_users.Update(response.Id, response);
}
}
}

+ 457
- 0
src/Discord.Net/DiscordClient.Cache.cs View File

@@ -0,0 +1,457 @@
using Discord.API.Models;
using Discord.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Discord
{
public partial class DiscordClient
{
/// <summary> Returns a collection of all users the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<User> Users => _users;
private AsyncCache<User, API.Models.UserReference> _users;

/// <summary> Returns a collection of all servers the client is a member of. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Server> Servers => _servers;
private AsyncCache<Server, API.Models.ServerReference> _servers;

/// <summary> Returns a collection of all channels the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Channel> Channels => _channels;
private AsyncCache<Channel, API.Models.ChannelReference> _channels;

/// <summary> Returns a collection of all messages the client has in cache. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Message> Messages => _messages;
private AsyncCache<Message, API.Models.MessageReference> _messages;

/// <summary> Returns a collection of all roles the client can see across all servers. </summary>
/// <remarks> This collection does not guarantee any ordering. </remarks>
public IEnumerable<Role> Roles => _roles;
private AsyncCache<Role, API.Models.Role> _roles;

private void CreateCaches()
{
_servers = new AsyncCache<Server, API.Models.ServerReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created server {key}.");
return new Server(key, this);
},
(server, model) =>
{
server.Name = model.Name;
_channels.Update(server.DefaultChannelId, server.Id, null);
if (model is ExtendedServerInfo)
{
var extendedModel = model as ExtendedServerInfo;
server.AFKChannelId = extendedModel.AFKChannelId;
server.AFKTimeout = extendedModel.AFKTimeout;
server.JoinedAt = extendedModel.JoinedAt ?? DateTime.MinValue;
server.OwnerId = extendedModel.OwnerId;
server.Region = extendedModel.Region;

foreach (var role in extendedModel.Roles)
_roles.Update(role.Id, model.Id, role);
foreach (var channel in extendedModel.Channels)
_channels.Update(channel.Id, model.Id, channel);
foreach (var membership in extendedModel.Members)
{
_users.Update(membership.User.Id, membership.User);
server.UpdateMember(membership);
}
foreach (var membership in extendedModel.VoiceStates)
server.UpdateMember(membership);
foreach (var membership in extendedModel.Presences)
server.UpdateMember(membership);
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated server {server.Name} ({server.Id}).");
},
server =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed server {server.Name} ({server.Id}).");
}
);

_channels = new AsyncCache<Channel, API.Models.ChannelReference>(
(key, parentKey) =>
{
if (_isDebugMode)
{
if (parentKey != null)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created channel {key} in server {parentKey}.");
else
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created private channel {key}.");
}
return new Channel(key, parentKey, this);
},
(channel, model) =>
{
channel.Name = model.Name;
channel.Type = model.Type;
if (model is ChannelInfo)
{
var extendedModel = model as ChannelInfo;
channel.Position = extendedModel.Position;

if (extendedModel.IsPrivate)
{
var user = _users.Update(extendedModel.Recipient.Id, extendedModel.Recipient);
channel.RecipientId = user.Id;
user.PrivateChannelId = channel.Id;
}

if (extendedModel.PermissionOverwrites != null)
{
channel.PermissionOverwrites = extendedModel.PermissionOverwrites.Select(x => new Channel.PermissionOverwrite
{
Type = x.Type,
Id = x.Id,
Deny = new PackedPermissions(x.Deny),
Allow = new PackedPermissions(x.Allow)
}).ToArray();
}
else
channel.PermissionOverwrites = null;
}
if (_isDebugMode)
{
if (channel.IsPrivate)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated private channel {channel.Name} ({channel.Id}).");
else
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId}).");
}
},
channel =>
{
if (channel.IsPrivate)
{
var user = channel.Recipient;
if (user.PrivateChannelId == channel.Id)
user.PrivateChannelId = null;
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed private channel {channel.Name} ({channel.Id}).");
}
else
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed channel {channel.Name} ({channel.Id}) in server {channel.Server?.Name} ({channel.ServerId}).");
}
});

_messages = new AsyncCache<Message, API.Models.MessageReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created message {key} in channel {parentKey}.");
return new Message(key, parentKey, this);
},
(message, model) =>
{
if (model is API.Models.Message)
{
var extendedModel = model as API.Models.Message;
if (extendedModel.Attachments != null)
{
message.Attachments = extendedModel.Attachments.Select(x => new Message.Attachment
{
Id = x.Id,
Url = x.Url,
ProxyUrl = x.ProxyUrl,
Size = x.Size,
Filename = x.Filename,
Width = x.Width,
Height = x.Height
}).ToArray();
}
else
message.Attachments = new Message.Attachment[0];
if (extendedModel.Embeds != null)
{
message.Embeds = extendedModel.Embeds.Select(x =>
{
var embed = new Message.Embed
{
Url = x.Url,
Type = x.Type,
Description = x.Description,
Title = x.Title
};
if (x.Provider != null)
{
embed.Provider = new Message.EmbedReference
{
Url = x.Provider.Url,
Name = x.Provider.Name
};
}
if (x.Author != null)
{
embed.Author = new Message.EmbedReference
{
Url = x.Author.Url,
Name = x.Author.Name
};
}
if (x.Thumbnail != null)
{
embed.Thumbnail = new Message.File
{
Url = x.Thumbnail.Url,
ProxyUrl = x.Thumbnail.ProxyUrl,
Width = x.Thumbnail.Width,
Height = x.Thumbnail.Height
};
}
return embed;
}).ToArray();
}
else
message.Embeds = new Message.Embed[0];
message.IsMentioningEveryone = extendedModel.IsMentioningEveryone;
message.IsTTS = extendedModel.IsTextToSpeech;
message.MentionIds = extendedModel.Mentions?.Select(x => x.Id)?.ToArray() ?? new string[0];
message.IsMentioningMe = message.MentionIds.Contains(_myId);
message.RawText = extendedModel.Content;
message.Timestamp = extendedModel.Timestamp;
message.EditedTimestamp = extendedModel.EditedTimestamp;
if (extendedModel.Author != null)
message.UserId = extendedModel.Author.Id;
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
},
message =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed message {message.Id} in channel {message.Channel?.Name} ({message.ChannelId}).");
}
);

_roles = new AsyncCache<Role, API.Models.Role>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created role {key} in server {parentKey}.");
return new Role(key, parentKey, this);
},
(role, model) =>
{
role.Name = model.Name;
role.Permissions.RawValue = (uint)model.Permissions;
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
},
role =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed role {role.Name} ({role.Id}) in server {role.Server?.Name} ({role.ServerId}).");
}
);

_users = new AsyncCache<User, API.Models.UserReference>(
(key, parentKey) =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key}.");
return new User(key, this);
},
(user, model) =>
{
user.AvatarId = model.Avatar;
user.Discriminator = model.Discriminator;
user.Name = model.Username;
if (model is SelfUserInfo)
{
var extendedModel = model as SelfUserInfo;
user.Email = extendedModel.Email;
user.IsVerified = extendedModel.IsVerified;
}
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {user?.Name} ({user.Id}).");
},
user =>
{
if (_isDebugMode)
RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {user?.Name} ({user.Id}).");
}
);
}

/// <summary> Returns the user with the specified id, or null if none was found. </summary>
public User GetUser(string id) => _users[id];
/// <summary> Returns the user with the specified name and discriminator, or null if none was found. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public User GetUser(string name, string discriminator)
{
if (name == null || discriminator == null)
return null;

if (name.StartsWith("@"))
name = name.Substring(1);

return _users
.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) &&
x.Discriminator == discriminator
)
.FirstOrDefault();
}
/// <summary> Returns all users with the specified name across all servers. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<User> FindUsers(string name)
{
if (name == null)
return new User[0];

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _users.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}

/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Membership GetMember(string serverId, User user)
=> GetMember(_servers[serverId], user?.Id);
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Membership GetMember(string serverId, string userId)
=> GetMember(_servers[serverId], userId);
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Membership GetMember(Server server, User user)
=> GetMember(server, user?.Id);
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public Membership GetMember(Server server, string userId)
{
if (server == null || userId == null)
return null;
return server.GetMember(userId);
}

/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Membership> FindMembers(string serverId, string name)
=> FindMembers(GetServer(serverId), name);
/// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks>
public IEnumerable<Membership> FindMembers(Server server, string name)
{
if (server == null || name == null)
return new Membership[0];

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return server.Members.Where(x =>
{
var user = x.User;
return string.Equals(user.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(user.Name, name2, StringComparison.OrdinalIgnoreCase);
});
}
else
{
return server.Members.Where(x =>
string.Equals(x.User.Name, name, StringComparison.OrdinalIgnoreCase));
}
}

/// <summary> Returns the server with the specified id, or null if none was found. </summary>
public Server GetServer(string id) => _servers[id];
/// <summary> Returns all servers with the specified name. </summary>
/// <remarks> Search is case-insensitive. </remarks>
public IEnumerable<Server> FindServers(string name)
{
if (name == null)
return new Server[0];
return _servers.Where(x =>
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}

/// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(string id) => _channels[id];
/// <summary> Returns a private channel with the provided user. </summary>
public Task<Channel> GetPMChannel(string userId, bool createIfNotExists = false)
=> GetPMChannel(_users[userId], createIfNotExists);
/// <summary> Returns a private channel with the provided user. </summary>
public async Task<Channel> GetPMChannel(User user, bool createIfNotExists = false)
{
if (user == null)
{
if (createIfNotExists)
throw new ArgumentNullException(nameof(user));
else
return null;
}

var channel = user.PrivateChannel;
if (channel == null && createIfNotExists)
await CreatePMChannel(user);
return channel;
}
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(Server server, string name)
=> FindChannels(server?.Id, name);
/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and #Name. Search is case-insensitive. </remarks>
public IEnumerable<Channel> FindChannels(string serverId, string name)
{
if (serverId == null || name == null)
return new Channel[0];

if (name.StartsWith("#"))
{
string name2 = name.Substring(1);
return _channels.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _channels.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}

/// <summary> Returns the role with the specified id, or null if none was found. </summary>
public Role GetRole(string id) => _roles[id];
/// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(Server server, string name)
=> FindRoles(server?.Id, name);
/// <summary> Returns all roles with the specified server and name. </summary>
/// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks>
public IEnumerable<Role> FindRoles(string serverId, string name)
{
if (serverId == null || name == null)
return new Role[0];

if (name.StartsWith("@"))
{
string name2 = name.Substring(1);
return _roles.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
}
else
{
return _roles.Where(x => x.ServerId == serverId &&
string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}
}

/// <summary> Returns the message with the specified id, or null if none was found. </summary>
public Message GetMessage(string id) => _messages[id];
}
}

+ 103
- 1028
src/Discord.Net/DiscordClient.cs
File diff suppressed because it is too large
View File


+ 9
- 3
src/Discord.Net/DiscordWebSocket.Events.cs View File

@@ -2,6 +2,12 @@

namespace Discord
{
public class DisconnectedEventArgs : EventArgs
{
public readonly bool WasUnexpected;
internal DisconnectedEventArgs(bool wasUnexpected) { WasUnexpected = wasUnexpected; }
}

internal abstract partial class DiscordWebSocket
{
//Debug
@@ -19,11 +25,11 @@ namespace Discord
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler Disconnected;
private void RaiseDisconnected()
public event EventHandler<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(bool wasUnexpected)
{
if (Disconnected != null)
Disconnected(this, EventArgs.Empty);
Disconnected(this, new DisconnectedEventArgs(wasUnexpected));
}
}
}

+ 12
- 10
src/Discord.Net/DiscordWebSocket.cs View File

@@ -18,13 +18,13 @@ namespace Discord
protected readonly bool _isDebug;
private readonly ConcurrentQueue<byte[]> _sendQueue;

protected volatile CancellationTokenSource _disconnectToken;
private volatile ClientWebSocket _webSocket;
private volatile Task _tasks;
protected CancellationTokenSource _disconnectToken;
private ClientWebSocket _webSocket;
private DateTime _lastHeartbeat;
private Task _task;
protected string _host;
protected int _timeout, _heartbeatInterval;
private DateTime _lastHeartbeat;
private bool _isConnected;
private bool _isConnected, _wasDisconnectedUnexpected;

public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug)
{
@@ -54,7 +54,7 @@ namespace Discord
OnConnect();

_lastHeartbeat = DateTime.UtcNow;
_tasks = Task.Factory.ContinueWhenAll(CreateTasks(), x =>
_task = Task.Factory.ContinueWhenAll(CreateTasks(), x =>
{
if (_isDebug)
RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected.");
@@ -64,6 +64,7 @@ namespace Discord

_disconnectToken.Dispose();
_disconnectToken = null;
_wasDisconnectedUnexpected = false;

//Clear send queue
_heartbeatInterval = 0;
@@ -76,20 +77,20 @@ namespace Discord
if (_isConnected)
{
_isConnected = false;
RaiseDisconnected();
RaiseDisconnected(_wasDisconnectedUnexpected);
}

_tasks = null;
_task = null;
});
}
public Task ReconnectAsync()
=> ConnectAsync(_host);
public async Task DisconnectAsync()
{
if (_tasks != null)
if (_task != null)
{
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { }
try { await _tasks; } catch (NullReferenceException) { }
try { await _task; } catch (NullReferenceException) { }
}
}
protected virtual void OnConnect() { }
@@ -130,6 +131,7 @@ namespace Discord

if (result.MessageType == WebSocketMessageType.Close)
{
_wasDisconnectedUnexpected = true;
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})");
return;


+ 21
- 0
src/Discord.Net/Helpers/Extensions.cs View File

@@ -0,0 +1,21 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Helpers
{
internal static class Extensions
{
public static async Task Wait(this CancellationTokenSource tokenSource)
{
var token = tokenSource.Token;
try { await Task.Delay(-1, token); }
catch (OperationCanceledException) { }
}
public static async Task Wait(this CancellationToken token)
{
try { await Task.Delay(-1, token); }
catch (OperationCanceledException) { }
}
}
}

+ 1
- 1
src/Discord.Net/Message.cs View File

@@ -103,7 +103,7 @@ namespace Discord
[JsonIgnore]
public User User => _client.GetUser(UserId);
/// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.UserId == UserId;
public bool IsAuthor => _client.User?.Id == UserId;

internal Message(string id, string channelId, DiscordClient client)
{


+ 4
- 4
src/Discord.Net/Server.cs View File

@@ -29,7 +29,7 @@ namespace Discord
/// <summary> Returns the user that first created this server. </summary>
public User Owner => _client.GetUser(OwnerId);
/// <summary> Returns true if the current user created this server. </summary>
public bool IsOwner => _client.UserId == OwnerId;
public bool IsOwner => _client.User?.Id == OwnerId;

/// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary>
public string AFKChannelId { get; internal set; }
@@ -69,7 +69,7 @@ namespace Discord
_members = new AsyncCache<Membership, API.Models.MemberInfo>(
(key, parentKey) =>
{
if (_client.Config.EnableDebug)
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}.");
return new Membership(parentKey, key, _client);
},
@@ -108,12 +108,12 @@ namespace Discord
member.IsDeafened = extendedModel.IsDeafened;
member.IsMuted = extendedModel.IsMuted;
}
if (_client.Config.EnableDebug)
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
},
(member) =>
{
if (_client.Config.EnableDebug)
if (_client.IsDebugMode)
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId}).");
}
);


Loading…
Cancel
Save