@@ -28,7 +28,7 @@ namespace Discord | |||||
MessageCreated += async (s, e) => | MessageCreated += async (s, e) => | ||||
{ | { | ||||
//Ignore messages from ourselves | //Ignore messages from ourselves | ||||
if (e.Message.UserId == UserId) | |||||
if (e.Message.UserId == _myId) | |||||
return; | return; | ||||
//Check for the command character | //Check for the command character | ||||
@@ -80,6 +80,12 @@ | |||||
<Compile Include="..\Discord.Net\ChannelTypes.cs"> | <Compile Include="..\Discord.Net\ChannelTypes.cs"> | ||||
<Link>ChannelTypes.cs</Link> | <Link>ChannelTypes.cs</Link> | ||||
</Compile> | </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"> | <Compile Include="..\Discord.Net\DiscordClient.cs"> | ||||
<Link>DiscordClient.cs</Link> | <Link>DiscordClient.cs</Link> | ||||
</Compile> | </Compile> | ||||
@@ -107,6 +113,9 @@ | |||||
<Compile Include="..\Discord.Net\Helpers\AsyncCache.cs"> | <Compile Include="..\Discord.Net\Helpers\AsyncCache.cs"> | ||||
<Link>Helpers\AsyncCache.cs</Link> | <Link>Helpers\AsyncCache.cs</Link> | ||||
</Compile> | </Compile> | ||||
<Compile Include="..\Discord.Net\Helpers\Extensions.cs"> | |||||
<Link>Helpers\Extensions.cs</Link> | |||||
</Compile> | |||||
<Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | <Compile Include="..\Discord.Net\Helpers\JsonHttpClient.cs"> | ||||
<Link>Helpers\JsonHttpClient.cs</Link> | <Link>Helpers\JsonHttpClient.cs</Link> | ||||
</Compile> | </Compile> | ||||
@@ -141,10 +141,10 @@ namespace Discord.API | |||||
var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | var request = new APIRequests.ChangePassword { NewPassword = newPassword, CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | ||||
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | 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 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 }; | var request = new APIRequests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword }; | ||||
return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | return _http.Patch<SelfUserInfo>(Endpoints.UserMe, request); | ||||
} | } | ||||
@@ -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); | |||||
} | |||||
} | |||||
} |
@@ -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]; | |||||
} | |||||
} |
@@ -2,6 +2,12 @@ | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
public class DisconnectedEventArgs : EventArgs | |||||
{ | |||||
public readonly bool WasUnexpected; | |||||
internal DisconnectedEventArgs(bool wasUnexpected) { WasUnexpected = wasUnexpected; } | |||||
} | |||||
internal abstract partial class DiscordWebSocket | internal abstract partial class DiscordWebSocket | ||||
{ | { | ||||
//Debug | //Debug | ||||
@@ -19,11 +25,11 @@ namespace Discord | |||||
if (Connected != null) | if (Connected != null) | ||||
Connected(this, EventArgs.Empty); | Connected(this, EventArgs.Empty); | ||||
} | } | ||||
public event EventHandler Disconnected; | |||||
private void RaiseDisconnected() | |||||
public event EventHandler<DisconnectedEventArgs> Disconnected; | |||||
private void RaiseDisconnected(bool wasUnexpected) | |||||
{ | { | ||||
if (Disconnected != null) | if (Disconnected != null) | ||||
Disconnected(this, EventArgs.Empty); | |||||
Disconnected(this, new DisconnectedEventArgs(wasUnexpected)); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -18,13 +18,13 @@ namespace Discord | |||||
protected readonly bool _isDebug; | protected readonly bool _isDebug; | ||||
private readonly ConcurrentQueue<byte[]> _sendQueue; | 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 string _host; | ||||
protected int _timeout, _heartbeatInterval; | protected int _timeout, _heartbeatInterval; | ||||
private DateTime _lastHeartbeat; | |||||
private bool _isConnected; | |||||
private bool _isConnected, _wasDisconnectedUnexpected; | |||||
public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug) | public DiscordWebSocket(DiscordClient client, int timeout, int interval, bool isDebug) | ||||
{ | { | ||||
@@ -54,7 +54,7 @@ namespace Discord | |||||
OnConnect(); | OnConnect(); | ||||
_lastHeartbeat = DateTime.UtcNow; | _lastHeartbeat = DateTime.UtcNow; | ||||
_tasks = Task.Factory.ContinueWhenAll(CreateTasks(), x => | |||||
_task = Task.Factory.ContinueWhenAll(CreateTasks(), x => | |||||
{ | { | ||||
if (_isDebug) | if (_isDebug) | ||||
RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected."); | RaiseOnDebugMessage(DebugMessageType.Connection, $"Disconnected."); | ||||
@@ -64,6 +64,7 @@ namespace Discord | |||||
_disconnectToken.Dispose(); | _disconnectToken.Dispose(); | ||||
_disconnectToken = null; | _disconnectToken = null; | ||||
_wasDisconnectedUnexpected = false; | |||||
//Clear send queue | //Clear send queue | ||||
_heartbeatInterval = 0; | _heartbeatInterval = 0; | ||||
@@ -76,20 +77,20 @@ namespace Discord | |||||
if (_isConnected) | if (_isConnected) | ||||
{ | { | ||||
_isConnected = false; | _isConnected = false; | ||||
RaiseDisconnected(); | |||||
RaiseDisconnected(_wasDisconnectedUnexpected); | |||||
} | } | ||||
_tasks = null; | |||||
_task = null; | |||||
}); | }); | ||||
} | } | ||||
public Task ReconnectAsync() | public Task ReconnectAsync() | ||||
=> ConnectAsync(_host); | => ConnectAsync(_host); | ||||
public async Task DisconnectAsync() | public async Task DisconnectAsync() | ||||
{ | { | ||||
if (_tasks != null) | |||||
if (_task != null) | |||||
{ | { | ||||
try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | try { _disconnectToken.Cancel(); } catch (NullReferenceException) { } | ||||
try { await _tasks; } catch (NullReferenceException) { } | |||||
try { await _task; } catch (NullReferenceException) { } | |||||
} | } | ||||
} | } | ||||
protected virtual void OnConnect() { } | protected virtual void OnConnect() { } | ||||
@@ -130,6 +131,7 @@ namespace Discord | |||||
if (result.MessageType == WebSocketMessageType.Close) | if (result.MessageType == WebSocketMessageType.Close) | ||||
{ | { | ||||
_wasDisconnectedUnexpected = true; | |||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); | await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); | ||||
RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})"); | RaiseOnDebugMessage(DebugMessageType.Connection, $"Got Close Message ({result.CloseStatus?.ToString() ?? "Unexpected"}, {result.CloseStatusDescription ?? "No Reason"})"); | ||||
return; | return; | ||||
@@ -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) { } | |||||
} | |||||
} | |||||
} |
@@ -103,7 +103,7 @@ namespace Discord | |||||
[JsonIgnore] | [JsonIgnore] | ||||
public User User => _client.GetUser(UserId); | public User User => _client.GetUser(UserId); | ||||
/// <summary> Returns true if the current user created this message. </summary> | /// <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) | internal Message(string id, string channelId, DiscordClient client) | ||||
{ | { | ||||
@@ -29,7 +29,7 @@ namespace Discord | |||||
/// <summary> Returns the user that first created this server. </summary> | /// <summary> Returns the user that first created this server. </summary> | ||||
public User Owner => _client.GetUser(OwnerId); | public User Owner => _client.GetUser(OwnerId); | ||||
/// <summary> Returns true if the current user created this server. </summary> | /// <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> | /// <summary> Returns the id of the AFK voice channel for this server (see AFKTimeout). </summary> | ||||
public string AFKChannelId { get; internal set; } | public string AFKChannelId { get; internal set; } | ||||
@@ -69,7 +69,7 @@ namespace Discord | |||||
_members = new AsyncCache<Membership, API.Models.MemberInfo>( | _members = new AsyncCache<Membership, API.Models.MemberInfo>( | ||||
(key, parentKey) => | (key, parentKey) => | ||||
{ | { | ||||
if (_client.Config.EnableDebug) | |||||
if (_client.IsDebugMode) | |||||
_client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Created user {key} in server {parentKey}."); | ||||
return new Membership(parentKey, key, _client); | return new Membership(parentKey, key, _client); | ||||
}, | }, | ||||
@@ -108,12 +108,12 @@ namespace Discord | |||||
member.IsDeafened = extendedModel.IsDeafened; | member.IsDeafened = extendedModel.IsDeafened; | ||||
member.IsMuted = extendedModel.IsMuted; | 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})."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Updated user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId})."); | ||||
}, | }, | ||||
(member) => | (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})."); | _client.RaiseOnDebugMessage(DebugMessageType.Cache, $"Destroyed user {member.User?.Name} ({member.UserId}) in server {member.Server?.Name} ({member.ServerId})."); | ||||
} | } | ||||
); | ); | ||||