Browse Source

Cleaned up all the models

tags/docs-0.9
RogueException 9 years ago
parent
commit
0d1e69600c
63 changed files with 2872 additions and 3495 deletions
  1. +1
    -1
      src/Discord.Net.Modules/ModuleManager.cs
  2. +0
    -17
      src/Discord.Net.Shared/EventHelper.cs
  3. +0
    -20
      src/Discord.Net.Shared/IdConvert.cs
  4. +2
    -2
      src/Discord.Net/API/Client/Common/Channel.cs
  5. +1
    -1
      src/Discord.Net/API/Client/Common/MemberPresence.cs
  6. +1
    -1
      src/Discord.Net/API/Client/Common/MemberReference.cs
  7. +2
    -2
      src/Discord.Net/API/Client/Rest/GetMessages.cs
  8. +6
    -6
      src/Discord.Net/API/Converters.cs
  9. +0
    -223
      src/Discord.Net/DiscordClient.Channels.cs
  10. +114
    -0
      src/Discord.Net/DiscordClient.Events.cs
  11. +0
    -104
      src/Discord.Net/DiscordClient.Invites.cs
  12. +0
    -429
      src/Discord.Net/DiscordClient.Messages.cs
  13. +972
    -0
      src/Discord.Net/DiscordClient.Obsolete.cs
  14. +0
    -105
      src/Discord.Net/DiscordClient.Permissions.cs
  15. +0
    -176
      src/Discord.Net/DiscordClient.Roles.cs
  16. +0
    -140
      src/Discord.Net/DiscordClient.Servers.cs
  17. +0
    -329
      src/Discord.Net/DiscordClient.Users.cs
  18. +481
    -471
      src/Discord.Net/DiscordClient.cs
  19. +13
    -11
      src/Discord.Net/DiscordConfig.cs
  20. +10
    -0
      src/Discord.Net/Enums/ConnectionState.cs
  21. +0
    -0
      src/Discord.Net/Enums/ImageType.cs
  22. +12
    -0
      src/Discord.Net/Events/ChannelEventArgs.cs
  23. +14
    -0
      src/Discord.Net/Events/ChannelUserEventArgs.cs
  24. +16
    -0
      src/Discord.Net/Events/DisconnectedEventArgs.cs
  25. +20
    -0
      src/Discord.Net/Events/LogMessageEventArgs.cs
  26. +14
    -0
      src/Discord.Net/Events/MessageEventArgs.cs
  27. +14
    -0
      src/Discord.Net/Events/ProfileEventArgs.cs
  28. +12
    -0
      src/Discord.Net/Events/RoleEventArgs.cs
  29. +11
    -0
      src/Discord.Net/Events/ServerEventArgs.cs
  30. +11
    -0
      src/Discord.Net/Events/UserEventArgs.cs
  31. +3
    -21
      src/Discord.Net/Format.cs
  32. +0
    -166
      src/Discord.Net/Helpers/AsyncCollection.cs
  33. +0
    -14
      src/Discord.Net/Helpers/BitHelper.cs
  34. +0
    -54
      src/Discord.Net/Helpers/CachedObject.cs
  35. +0
    -30
      src/Discord.Net/Helpers/CollectionExtensions.cs
  36. +0
    -70
      src/Discord.Net/Helpers/Reference.cs
  37. +59
    -0
      src/Discord.Net/Logging/LogManager.cs
  38. +45
    -0
      src/Discord.Net/Logging/Logger.cs
  39. +26
    -27
      src/Discord.Net/Mention.cs
  40. +102
    -0
      src/Discord.Net/MessageQueue.cs
  41. +222
    -245
      src/Discord.Net/Models/Channel.cs
  42. +2
    -2
      src/Discord.Net/Models/GlobalUser.cs
  43. +16
    -13
      src/Discord.Net/Models/Invite.cs
  44. +23
    -79
      src/Discord.Net/Models/Message.cs
  45. +10
    -4
      src/Discord.Net/Models/Permissions.cs
  46. +29
    -0
      src/Discord.Net/Models/Profile.cs
  47. +39
    -45
      src/Discord.Net/Models/Role.cs
  48. +188
    -242
      src/Discord.Net/Models/Server.cs
  49. +127
    -146
      src/Discord.Net/Models/User.cs
  50. +2
    -2
      src/Discord.Net/Net/HttpException.cs
  51. +0
    -29
      src/Discord.Net/Net/Rest/RestClient.Events.cs
  52. +59
    -26
      src/Discord.Net/Net/Rest/RestClient.cs
  53. +8
    -16
      src/Discord.Net/Net/Rest/SharpRestEngine.cs
  54. +1
    -1
      src/Discord.Net/Net/TimeoutException.cs
  55. +21
    -22
      src/Discord.Net/Net/WebSockets/GatewaySocket.cs
  56. +21
    -33
      src/Discord.Net/Net/WebSockets/WS4NetEngine.cs
  57. +68
    -77
      src/Discord.Net/Net/WebSockets/WebSocket.cs
  58. +71
    -0
      src/Discord.Net/Reference.cs
  59. +0
    -7
      src/Discord.Net/Services/IService.cs
  60. +0
    -8
      src/Discord.Net/Services/LogExtensions.cs
  61. +0
    -71
      src/Discord.Net/Services/LogService.cs
  62. +1
    -4
      src/Discord.Net/TaskManager.cs
  63. +2
    -3
      src/Discord.Net/project.json

+ 1
- 1
src/Discord.Net.Modules/ModuleManager.cs View File

@@ -34,7 +34,7 @@ namespace Discord.Modules
public event EventHandler<UserEventArgs> UserUpdated;
public event EventHandler<UserEventArgs> UserPresenceUpdated;
public event EventHandler<UserEventArgs> UserVoiceStateUpdated;
public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated;
public event EventHandler<ChannelEventArgs> UserIsTypingUpdated;

public event EventHandler<MessageEventArgs> MessageReceived;
public event EventHandler<MessageEventArgs> MessageSent;


+ 0
- 17
src/Discord.Net.Shared/EventHelper.cs View File

@@ -1,17 +0,0 @@
using System;

namespace Discord
{
internal static class EventHelper
{
public static void Raise(Logger logger, string name, Action action)
{
try { action(); }
catch (Exception ex)
{
var ex2 = ex.GetBaseException();
logger.Error($"{name}'s handler raised {ex2.GetType().Name}: ${ex2.Message}", ex);
}
}
}
}

+ 0
- 20
src/Discord.Net.Shared/IdConvert.cs View File

@@ -1,20 +0,0 @@
using System;
using System.Globalization;

namespace Discord
{
internal static class IdConvert
{
internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture;
public static ulong ToLong(string value)
=> ulong.Parse(value, NumberStyles.None, _format);
public static ulong? ToNullableLong(string value)
=> value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format);
public static string ToString(ulong value)
=> value.ToString(_format);
public static string ToString(ulong? value)
=> value?.ToString(_format);
}
}

+ 2
- 2
src/Discord.Net/API/Client/Common/Channel.cs View File

@@ -21,9 +21,9 @@ namespace Discord.API.Client
[JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))]
public ulong? LastMessageId { get; set; }
[JsonProperty("is_private")]
public bool IsPrivate { get; set; }
public bool? IsPrivate { get; set; }
[JsonProperty("position")]
public int Position { get; set; }
public int? Position { get; set; }
[JsonProperty("topic")]
public string Topic { get; set; }
[JsonProperty("permission_overwrites")]


+ 1
- 1
src/Discord.Net/API/Client/Common/MemberPresence.cs View File

@@ -6,7 +6,7 @@ namespace Discord.API.Client
public class MemberPresence : MemberReference
{
[JsonProperty("game_id")]
public int? GameId { get; set; }
public string GameId { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))]


+ 1
- 1
src/Discord.Net/API/Client/Common/MemberReference.cs View File

@@ -6,7 +6,7 @@ namespace Discord.API.Client
public class MemberReference
{
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))]
public ulong GuildId { get; set; }
public ulong? GuildId { get; set; }
[JsonProperty("user")]
public UserReference User { get; set; }
}


+ 2
- 2
src/Discord.Net/API/Client/Rest/GetMessages.cs View File

@@ -14,7 +14,7 @@ namespace Discord.API.Client.Rest
StringBuilder query = new StringBuilder();
this.AddQueryParam(query, "limit", Limit.ToString());
if (RelativeDir != null)
this.AddQueryParam(query, RelativeDir, RelativeId.Value.ToString());
this.AddQueryParam(query, RelativeDir, RelativeId.ToString());
return $"channels/{ChannelId}/messages{query}";
}
}
@@ -25,7 +25,7 @@ namespace Discord.API.Client.Rest

public int Limit { get; set; } = 100;
public string RelativeDir { get; set; } = null;
public ulong? RelativeId { get; set; } = 0;
public ulong RelativeId { get; set; } = 0;

public GetMessagesRequest(ulong channelId)
{


+ 6
- 6
src/Discord.Net/API/Converters.cs View File

@@ -9,9 +9,9 @@ namespace Discord.API.Converters
public override bool CanConvert(Type objectType)
=> objectType == typeof(ulong);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> IdConvert.ToLong((string)reader.Value);
=> ((string)reader.Value).ToId();
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> writer.WriteValue(IdConvert.ToString((ulong)value));
=> writer.WriteValue(((ulong)value).ToIdString());
}

public class NullableLongStringConverter : JsonConverter
@@ -19,9 +19,9 @@ namespace Discord.API.Converters
public override bool CanConvert(Type objectType)
=> objectType == typeof(ulong?);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> IdConvert.ToNullableLong((string)reader.Value);
=> ((string)reader.Value).ToNullableId();
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> writer.WriteValue(IdConvert.ToString((ulong?)value));
=> writer.WriteValue(((ulong?)value).ToIdString());
}

/*public class LongStringEnumerableConverter : JsonConverter
@@ -66,7 +66,7 @@ namespace Discord.API.Converters
reader.Read();
while (reader.TokenType != JsonToken.EndArray)
{
result.Add(IdConvert.ToLong((string)reader.Value));
result.Add(((string)reader.Value).ToId());
reader.Read();
}
}
@@ -81,7 +81,7 @@ namespace Discord.API.Converters
writer.WriteStartArray();
var a = (ulong[])value;
for (int i = 0; i < a.Length; i++)
writer.WriteValue(IdConvert.ToString(a[i]));
writer.WriteValue(a[i].ToIdString());
writer.WriteEndArray();
}
}


+ 0
- 223
src/Discord.Net/DiscordClient.Channels.cs View File

@@ -1,223 +0,0 @@
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
internal sealed class Channels : AsyncCollection<ulong, Channel>
{
public IEnumerable<Channel> PrivateChannels => _privateChannels.Select(x => x.Value);
private ConcurrentDictionary<ulong, Channel> _privateChannels;

public Channels(DiscordClient client, object writerLock)
: base(client, writerLock)
{
_privateChannels = new ConcurrentDictionary<ulong, Channel>();
ItemCreated += (s, e) =>
{
if (e.Item.IsPrivate)
_privateChannels.TryAdd(e.Item.Id, e.Item);
};
ItemDestroyed += (s, e) =>
{
if (e.Item.IsPrivate)
{
Channel ignored;
_privateChannels.TryRemove(e.Item.Id, out ignored);
}
};
Cleared += (s, e) => _privateChannels.Clear();
}
public Channel GetOrAdd(ulong id, ulong? serverId, ulong? recipientId = null)
=> GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId));
}

public class ChannelEventArgs : EventArgs
{
public Channel Channel { get; }
public Server Server => Channel.Server;

public ChannelEventArgs(Channel channel) { Channel = channel; }
}

public partial class DiscordClient
{
public event EventHandler<ChannelEventArgs> ChannelCreated;
private void RaiseChannelCreated(Channel channel)
{
if (ChannelCreated != null)
EventHelper.Raise(_logger, nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel)));
}
public event EventHandler<ChannelEventArgs> ChannelDestroyed;
private void RaiseChannelDestroyed(Channel channel)
{
if (ChannelDestroyed != null)
EventHelper.Raise(_logger, nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel)));
}
public event EventHandler<ChannelEventArgs> ChannelUpdated;
private void RaiseChannelUpdated(Channel channel)
{
if (ChannelUpdated != null)
EventHelper.Raise(_logger, nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel)));
}

/// <summary> Returns a collection of all servers this client is a member of. </summary>
public IEnumerable<Channel> PrivateChannels { get { CheckReady(); return _channels.PrivateChannels; } }
internal Channels Channels => _channels;
private readonly Channels _channels;

/// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(ulong id)
{
CheckReady();

return _channels[id];
}

/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name, #Name and &lt;#Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();
var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));

if (!exactMatch && name.Length >= 2)
{
if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention
{
var id = IdConvert.ToLong(name.Substring(2, name.Length - 3));
var channel = _channels[id];
if (channel != null)
query = query.Concat(new Channel[] { channel });
}
else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}

if (type != null)
query = query.Where(x => x.Type == type);
return query;
}

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

var request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value };
var response = await _clientRest.Send(request).ConfigureAwait(false);

var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response);
return channel;
}
/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public async Task<Channel> CreatePMChannel(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

Channel channel = null;
if (user != null)
channel = user.Global.PrivateChannel;
if (channel == null)
{
var request = new CreatePrivateChannelRequest() { RecipientId = user.Id };
var response = await _clientRest.Send(request).ConfigureAwait(false);

var recipient = _users.GetOrAdd(response.Recipient.Id, null);
recipient.Update(response.Recipient);
channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient.Id);
channel.Update(response);
}
return channel;
}
/// <summary> Edits the provided channel, changing only non-null attributes. </summary>
public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
CheckReady();

if (name != null || topic != null)
{
var request = new UpdateChannelRequest(channel.Id)
{
Name = name ?? channel.Name,
Topic = topic ?? channel.Topic,
Position = channel.Position
};
await _clientRest.Send(request).ConfigureAwait(false);
}

if (position != null)
{
Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray();
int oldPos = Array.IndexOf(channels, channel);
var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault();
int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1;
if (newPos < 0)
newPos = 0;
int minPos;

if (oldPos < newPos) //Moving Down
{
minPos = oldPos;
for (int i = oldPos; i < newPos; i++)
channels[i] = channels[i + 1];
channels[newPos] = channel;
}
else //(oldPos > newPos) Moving Up
{
minPos = newPos;
for (int i = oldPos; i > newPos; i--)
channels[i] = channels[i - 1];
channels[newPos] = channel;
}
Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null;
await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false);
}
}
/// <summary> Reorders the provided channels in the server's channel list and places them after a certain channel. </summary>
public Task ReorderChannels(Server server, IEnumerable<Channel> channels, Channel after = null)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (channels == null) throw new ArgumentNullException(nameof(channels));
CheckReady();

var request = new ReorderChannelsRequest(server.Id)
{
ChannelIds = channels.Select(x => x.Id).ToArray(),
StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position)
};
return _clientRest.Send(request);
}
/// <summary> Destroys the provided channel. </summary>
public async Task DeleteChannel(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
CheckReady();

try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}
}

+ 114
- 0
src/Discord.Net/DiscordClient.Events.cs View File

@@ -0,0 +1,114 @@
using System;
using System.Runtime.CompilerServices;

namespace Discord
{
public partial class DiscordClient
{
public event EventHandler Connected = delegate { };
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { };
public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { };
public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { };
public event EventHandler<ChannelEventArgs> ChannelUpdated = delegate { };
public event EventHandler<MessageEventArgs> MessageAcknowledged = delegate { };
public event EventHandler<MessageEventArgs> MessageDeleted = delegate { };
public event EventHandler<MessageEventArgs> MessageReceived = delegate { };
public event EventHandler<MessageEventArgs> MessageSent = delegate { };
public event EventHandler<MessageEventArgs> MessageUpdated = delegate { };
public event EventHandler<ProfileEventArgs> ProfileUpdated = delegate { };
public event EventHandler<RoleEventArgs> RoleCreated = delegate { };
public event EventHandler<RoleEventArgs> RoleUpdated = delegate { };
public event EventHandler<RoleEventArgs> RoleDeleted = delegate { };
public event EventHandler<ServerEventArgs> JoinedServer = delegate { };
public event EventHandler<ServerEventArgs> LeftServer = delegate { };
public event EventHandler<ServerEventArgs> ServerAvailable = delegate { };
public event EventHandler<ServerEventArgs> ServerUpdated = delegate { };
public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { };
public event EventHandler<BanEventArgs> UserBanned = delegate { };
public event EventHandler<ChannelUserEventArgs> UserIsTypingUpdated = delegate { };
public event EventHandler<UserEventArgs> UserJoined = delegate { };
public event EventHandler<UserEventArgs> UserLeft = delegate { };
public event EventHandler<UserEventArgs> UserPresenceUpdated = delegate { };
public event EventHandler<UserEventArgs> UserUpdated = delegate { };
public event EventHandler<BanEventArgs> UserUnbanned = delegate { };
public event EventHandler<UserEventArgs> UserVoiceStateUpdated = delegate { };

private void OnConnected()
=> OnEvent(Connected);
private void OnDisconnected(bool wasUnexpected, Exception ex)
=> OnEvent(Disconnected, new DisconnectedEventArgs(wasUnexpected, ex));

private void OnChannelCreated(Channel channel)
=> OnEvent(ChannelCreated, new ChannelEventArgs(channel));
private void OnChannelDestroyed(Channel channel)
=> OnEvent(ChannelDestroyed, new ChannelEventArgs(channel));
private void OnChannelUpdated(Channel channel)
=> OnEvent(ChannelUpdated, new ChannelEventArgs(channel));
private void OnMessageAcknowledged(Message msg)
=> OnEvent(MessageAcknowledged, new MessageEventArgs(msg));
private void OnMessageDeleted(Message msg)
=> OnEvent(MessageDeleted, new MessageEventArgs(msg));
private void OnMessageReceived(Message msg)
=> OnEvent(MessageReceived, new MessageEventArgs(msg));
/*private void OnMessageSent(Message msg)
=> OnEvent(MessageSent, new MessageEventArgs(msg));*/
private void OnMessageUpdated(Message msg)
=> OnEvent(MessageUpdated, new MessageEventArgs(msg));

private void OnProfileUpdated(Profile profile)
=> OnEvent(ProfileUpdated, new ProfileEventArgs(profile));

private void OnRoleCreated(Role role)
=> OnEvent(RoleCreated, new RoleEventArgs(role));
private void OnRoleDeleted(Role role)
=> OnEvent(RoleDeleted, new RoleEventArgs(role));
private void OnRoleUpdated(Role role)
=> OnEvent(RoleUpdated, new RoleEventArgs(role));

private void OnJoinedServer(Server server)
=> OnEvent(JoinedServer, new ServerEventArgs(server));
private void OnLeftServer(Server server)
=> OnEvent(LeftServer, new ServerEventArgs(server));
private void OnServerAvailable(Server server)
=> OnEvent(ServerAvailable, new ServerEventArgs(server));
private void OnServerUpdated(Server server)
=> OnEvent(ServerUpdated, new ServerEventArgs(server));
private void OnServerUnavailable(Server server)
=> OnEvent(ServerUnavailable, new ServerEventArgs(server));

private void OnUserBanned(Server server, ulong userId)
=> OnEvent(UserBanned, new BanEventArgs(server, userId));
private void OnUserIsTypingUpdated(Channel channel, User user)
=> OnEvent(UserIsTypingUpdated, new ChannelUserEventArgs(channel, user));
private void OnUserJoined(User user)
=> OnEvent(UserJoined, new UserEventArgs(user));
private void OnUserLeft(User user)
=> OnEvent(UserLeft, new UserEventArgs(user));
private void OnUserPresenceUpdated(User user)
=> OnEvent(UserPresenceUpdated, new UserEventArgs(user));
private void OnUserUnbanned(Server server, ulong userId)
=> OnEvent(UserUnbanned, new BanEventArgs(server, userId));
private void OnUserUpdated(User user)
=> OnEvent(UserUpdated, new UserEventArgs(user));
private void OnUserVoiceStateUpdated(User user)
=> OnEvent(UserVoiceStateUpdated, new UserEventArgs(user));

private void OnEvent<T>(EventHandler<T> handler, T eventArgs, [CallerMemberName] string callerName = null)
{
try { handler(this, eventArgs); }
catch (Exception ex)
{
Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex);
}
}
private void OnEvent(EventHandler handler, [CallerMemberName] string callerName = null)
{
try { handler(this, EventArgs.Empty); }
catch (Exception ex)
{
Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex);
}
}
}
}

+ 0
- 104
src/Discord.Net/DiscordClient.Invites.cs View File

@@ -1,104 +0,0 @@
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

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

//Remove trailing slash
if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/')
inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1);
//Remove leading URL
int index = inviteIdOrXkcd.LastIndexOf('/');
if (index >= 0)
inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1);

var response = await _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false);
var invite = new Invite(response.Code, response.XkcdPass);
invite.Update(response);
return invite;
}

/// <summary> Gets all active (non-expired) invites to a provided server. </summary>
public async Task<Invite[]> GetInvites(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false);
return response.Select(x =>
{
var invite = new Invite(x.Code, x.XkcdPass);
invite.Update(x);
return invite;
}).ToArray();
}

/// <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="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> 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. Set to 0 to have unlimited uses. </param>
public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd);
}
/// <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="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> 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. Set to 0 to have unlimited uses. </param>
public async Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
CheckReady();

var request = new CreateInviteRequest(channel.Id)
{
MaxAge = maxAge,
MaxUses = maxUses,
IsTemporary = isTemporary,
WithXkcdPass = withXkcd
};

var response = await _clientRest.Send(request).ConfigureAwait(false);
var invite = new Invite(response.Code, response.XkcdPass);
return invite;
}

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

try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
/// <summary> Accepts the provided invite. </summary>
public Task AcceptInvite(Invite invite)
{
if (invite == null) throw new ArgumentNullException(nameof(invite));
CheckReady();

return _clientRest.Send(new AcceptInviteRequest(invite.Code));
}
}
}

+ 0
- 429
src/Discord.Net/DiscordClient.Messages.cs View File

@@ -1,429 +0,0 @@
using Discord.API;
using Discord.API.Client.Rest;
using Discord.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using APIMessage = Discord.API.Client.Message;
using APIUser = Discord.API.Client.User;

namespace Discord
{
public enum RelativeDirection { Before, After}
internal sealed class Messages : AsyncCollection<ulong, Message>
{
private bool _isEnabled;

public Messages(DiscordClient client, object writerLock, bool isEnabled)
: base(client, writerLock)
{
_isEnabled = isEnabled;
}
public Message GetOrAdd(ulong id, ulong channelId, ulong userId)
{
if (_isEnabled)
return GetOrAdd(id, () => new Message(_client, id, channelId, userId));
else
{
var msg = new Message(_client, id, channelId, userId);
msg.Cache(); //Builds references
return msg;
}
}
public void Import(Dictionary<ulong, Message> messages)
=> base.Import(messages);
}

internal class MessageQueueItem
{
public readonly Message Message;
public readonly string Text;
public readonly ulong[] MentionedUsers;
public MessageQueueItem(Message msg, string text, ulong[] userIds)
{
Message = msg;
Text = text;
MentionedUsers = userIds;
}
}

public class MessageEventArgs : EventArgs
{
public Message Message { get; }
public User User => Message.User;
public Channel Channel => Message.Channel;
public Server Server => Message.Server;

public MessageEventArgs(Message msg) { Message = msg; }
}

public partial class DiscordClient
{
public const int MaxMessageSize = 2000;

public event EventHandler<MessageEventArgs> MessageReceived;
private void RaiseMessageReceived(Message msg)
{
if (MessageReceived != null)
EventHelper.Raise(_logger, nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg)));
}
public event EventHandler<MessageEventArgs> MessageSent;
private void RaiseMessageSent(Message msg)
{
if (MessageSent != null)
EventHelper.Raise(_logger, nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg)));
}
public event EventHandler<MessageEventArgs> MessageDeleted;
private void RaiseMessageDeleted(Message msg)
{
if (MessageDeleted != null)
EventHelper.Raise(_logger, nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg)));
}
public event EventHandler<MessageEventArgs> MessageUpdated;
private void RaiseMessageUpdated(Message msg)
{
if (MessageUpdated != null)
EventHelper.Raise(_logger, nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg)));
}
public event EventHandler<MessageEventArgs> MessageAcknowledged;
private void RaiseMessageAcknowledged(Message msg)
{
if (MessageAcknowledged != null)
EventHelper.Raise(_logger, nameof(MessageAcknowledged), () => MessageAcknowledged(this, new MessageEventArgs(msg)));
}
internal Messages Messages => _messages;
private readonly Random _nonceRand;
private readonly Messages _messages;
private readonly JsonSerializer _messageImporter;
private readonly ConcurrentQueue<MessageQueueItem> _pendingMessages;

/// <summary> Returns the message with the specified id, or null if none was found. </summary>
public Message GetMessage(ulong id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id));
CheckReady();

return _messages[id];
}

/// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message> SendMessage(Channel channel, string text)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

return SendMessageInternal(channel, text, false);
}
/// <summary> Sends a private message to the provided user. </summary>
public async Task<Message> SendMessage(User user, string text)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendMessageInternal(channel, text, false).ConfigureAwait(false);
}
/// <summary> Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message> SendTTSMessage(Channel channel, string text)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

return SendMessageInternal(channel, text, true);
}
/// <summary> Sends a file to the provided channel. </summary>
public Task<Message> SendFile(Channel channel, string filePath)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (filePath == null) throw new ArgumentNullException(nameof(filePath));
CheckReady();

return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath));
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(Channel channel, string filename, Stream stream)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (filename == null) throw new ArgumentNullException(nameof(filename));
if (stream == null) throw new ArgumentNullException(nameof(stream));
CheckReady();

var request = new SendFileRequest(channel.Id)
{
Filename = filename,
Stream = stream
};
var model = await _clientRest.Send(request).ConfigureAwait(false);

var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
return msg;
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(User user, string filePath)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (filePath == null) throw new ArgumentNullException(nameof(filePath));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false);
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(User user, string filename, Stream stream)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (filename == null) throw new ArgumentNullException(nameof(filename));
if (stream == null) throw new ArgumentNullException(nameof(stream));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendFile(channel, filename, stream).ConfigureAwait(false);
}
private async Task<Message> SendMessageInternal(Channel channel, string text, bool isTextToSpeech)
{
Message msg;
var server = channel.Server;

var mentionedUsers = new List<User>();
text = Mention.CleanUserMentions(this, server, text, mentionedUsers);
if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

if (Config.UseMessageQueue)
{
var nonce = GenerateNonce();
msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id);
var currentUser = msg.User;
msg.Update(new APIMessage
{
Content = text,
Timestamp = DateTime.UtcNow,
Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name },
ChannelId = channel.Id,
Nonce = IdConvert.ToString(nonce),
IsTextToSpeech = isTextToSpeech
});
msg.State = MessageState.Queued;
_pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray()));
}
else
{
var request = new SendMessageRequest(channel.Id)
{
Content = text,
MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(),
Nonce = null,
IsTTS = isTextToSpeech
};
var model = await _clientRest.Send(request).ConfigureAwait(false);
msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
}
return msg;
}

/// <summary> Edits the provided message, changing only non-null attributes. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks>
public async Task EditMessage(Message message, string text)
{
if (message == null) throw new ArgumentNullException(nameof(message));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

var channel = message.Channel;
var mentionedUsers = new List<User>();
if (!channel.IsPrivate)
text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers);

if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

if (Config.UseMessageQueue)
_pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray()));
else
{
var request = new UpdateMessageRequest(message.Channel.Id, message.Id)
{
Content = text,
MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray()
};
await _clientRest.Send(request).ConfigureAwait(false);
}
}

/// <summary> Deletes the provided message. </summary>
public async Task DeleteMessage(Message message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
CheckReady();

var request = new DeleteMessageRequest(message.Id, message.Channel.Id);
try { await _clientRest.Send(request).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
public async Task DeleteMessages(IEnumerable<Message> messages)
{
if (messages == null) throw new ArgumentNullException(nameof(messages));
CheckReady();

foreach (var message in messages)
{
var request = new DeleteMessageRequest(message.Id, message.Channel.Id);
try { await _clientRest.Send(request).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}

/// <summary> Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. </summary>
public async Task<Message[]> DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (limit < 0) throw new ArgumentNullException(nameof(limit));
CheckReady();
if (limit == 0) return new Message[0];
if (channel != null && channel.Type == ChannelType.Text)
{
try
{
var request = new GetMessagesRequest(channel.Id)
{
Limit = limit,
RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after",
RelativeId = relativeMessageId
};
var msgs = await _clientRest.Send(request).ConfigureAwait(false);
return msgs.Select(x =>
{
Message msg = null;
if (useCache)
{
msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id);
var user = msg.User;
if (user != null)
user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp);
}
else
msg = /*_messages[x.Id] ??*/ new Message(this, x.Id, x.ChannelId, x.Author.Id);
msg.Update(x);
return msg;
})
.ToArray();
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden){ } //Bad Permissions
}
return new Message[0];
}

/// <summary> Marks a given message as read. </summary>
public void AckMessage(Message message)
{
if (message == null) throw new ArgumentNullException(nameof(message));

if (!message.IsAuthor)
_clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id));
}
/// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary>
public IEnumerable<Message> ImportMessages(Channel channel, string json)
{
if (json == null) throw new ArgumentNullException(nameof(json));
var dic = JArray.Parse(json)
.Select(x =>
{
var msg = new Message(this,
x["Id"].Value<ulong>(),
channel.Id,
x["UserId"].Value<ulong>());

var reader = x.CreateReader();
_messageImporter.Populate(reader, msg);
msg.Text = Mention.Resolve(msg, msg.RawText);
return msg;
})
.ToDictionary(x => x.Id);
_messages.Import(dic);
foreach (var msg in dic.Values)
{
var user = msg.User;
if (user != null)
user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp);
}
return dic.Values;
}

/// <summary> Serializes the message cache for a given channel to JSON.</summary>
public string ExportMessages(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));

return JsonConvert.SerializeObject(channel.Messages);
}

private Task MessageQueueAsync()
{
var cancelToken = _cancelToken;
int interval = Config.MessageQueueInterval;

return Task.Run(async () =>
{
MessageQueueItem queuedMessage;

while (!cancelToken.IsCancellationRequested)
{
while (_pendingMessages.TryDequeue(out queuedMessage))
{
var msg = queuedMessage.Message;
try
{
if (msg.Id == 0)
{
var request = new SendMessageRequest(msg.Channel.Id)
{
Content = queuedMessage.Text,
MentionedUserIds = queuedMessage.MentionedUsers,
Nonce = IdConvert.ToString(msg.Id), //Nonce
IsTTS = msg.IsTTS
};
await _clientRest.Send(request).ConfigureAwait(false);
}
else
{
var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id)
{
Content = queuedMessage.Text,
MentionedUserIds = queuedMessage.MentionedUsers
};
await _clientRest.Send(request).ConfigureAwait(false);
}
}
catch (WebException) { break; }
catch (HttpException) { msg.State = MessageState.Failed; }
}
await Task.Delay(interval).ConfigureAwait(false);
}
});
}
private ulong GenerateNonce()
{
lock (_nonceRand)
return (ulong)_nonceRand.Next(1, int.MaxValue);
}
}
}

+ 972
- 0
src/Discord.Net/DiscordClient.Obsolete.cs View File

@@ -0,0 +1,972 @@
namespace Discord
{
/*public enum RelativeDirection { Before, After }
public partial class DiscordClient
{
/// <summary> Returns the channel with the specified id, or null if none was found. </summary>
public Channel GetChannel(ulong id)
{
CheckReady();

return _channels[id];
}

/// <summary> Returns all channels with the specified server and name. </summary>
/// <remarks> Name formats supported: Name, #Name and &lt;#Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));

if (!exactMatch && name.Length >= 2)
{
if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention
{
var id = IdConvert.ToLong(name.Substring(2, name.Length - 3));
var channel = _channels[id];
if (channel != null)
query = query.Concat(new Channel[] { channel });
}
else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}

if (type != null)
query = query.Where(x => x.Type == type);
return query;
}

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

var request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value };
var response = await _clientRest.Send(request).ConfigureAwait(false);

var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id);
channel.Update(response);
return channel;
}

/// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary>
public async Task<Channel> CreatePMChannel(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

Channel channel = null;
if (user != null)
channel = user.Global.PrivateChannel;
if (channel == null)
{
var request = new CreatePrivateChannelRequest() { RecipientId = user.Id };
var response = await _clientRest.Send(request).ConfigureAwait(false);

var recipient = _users.GetOrAdd(response.Recipient.Id, null);
recipient.Update(response.Recipient);
channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient.Id);
channel.Update(response);
}
return channel;
}

/// <summary> Edits the provided channel, changing only non-null attributes. </summary>
public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
CheckReady();

if (name != null || topic != null)
{
var request = new UpdateChannelRequest(channel.Id)
{
Name = name ?? channel.Name,
Topic = topic ?? channel.Topic,
Position = channel.Position
};
await _clientRest.Send(request).ConfigureAwait(false);
}

if (position != null)
{
Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray();
int oldPos = Array.IndexOf(channels, channel);
var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault();
int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1;
if (newPos < 0)
newPos = 0;
int minPos;

if (oldPos < newPos) //Moving Down
{
minPos = oldPos;
for (int i = oldPos; i < newPos; i++)
channels[i] = channels[i + 1];
channels[newPos] = channel;
}
else //(oldPos > newPos) Moving Up
{
minPos = newPos;
for (int i = oldPos; i > newPos; i--)
channels[i] = channels[i - 1];
channels[newPos] = channel;
}
Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null;
await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false);
}
}

/// <summary> Reorders the provided channels in the server's channel list and places them after a certain channel. </summary>
public Task ReorderChannels(Server server, IEnumerable<Channel> channels, Channel after = null)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (channels == null) throw new ArgumentNullException(nameof(channels));
CheckReady();

var request = new ReorderChannelsRequest(server.Id)
{
ChannelIds = channels.Select(x => x.Id).ToArray(),
StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position)
};
return _clientRest.Send(request);
}

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

try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

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

//Remove trailing slash
if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/')
inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1);
//Remove leading URL
int index = inviteIdOrXkcd.LastIndexOf('/');
if (index >= 0)
inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1);

var response = await _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false);
var invite = new Invite(response.Code, response.XkcdPass);
invite.Update(response);
return invite;
}

/// <summary> Gets all active (non-expired) invites to a provided server. </summary>
public async Task<Invite[]> GetInvites(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false);
return response.Select(x =>
{
var invite = new Invite(x.Code, x.XkcdPass);
invite.Update(x);
return invite;
}).ToArray();
}

/// <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="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> 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. Set to 0 to have unlimited uses. </param>
public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd);
}
/// <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="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param>
/// <param name="hasXkcd"> 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. Set to 0 to have unlimited uses. </param>
public async Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge));
if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses));
CheckReady();

var request = new CreateInviteRequest(channel.Id)
{
MaxAge = maxAge,
MaxUses = maxUses,
IsTemporary = isTemporary,
WithXkcdPass = withXkcd
};

var response = await _clientRest.Send(request).ConfigureAwait(false);
var invite = new Invite(response.Code, response.XkcdPass);
return invite;
}

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

try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

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

return _clientRest.Send(new AcceptInviteRequest(invite.Code));
}


/// <summary> Returns the message with the specified id, or null if none was found. </summary>
public Message GetMessage(ulong id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id));
CheckReady();

return _messages[id];
}

/// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message> SendMessage(Channel channel, string text)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

return SendMessageInternal(channel, text, false);
}
/// <summary> Sends a private message to the provided user. </summary>
public async Task<Message> SendMessage(User user, string text)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendMessageInternal(channel, text, false).ConfigureAwait(false);
}
/// <summary> Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. </summary>
public Task<Message> SendTTSMessage(Channel channel, string text)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

return SendMessageInternal(channel, text, true);
}
/// <summary> Sends a file to the provided channel. </summary>
public Task<Message> SendFile(Channel channel, string filePath)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (filePath == null) throw new ArgumentNullException(nameof(filePath));
CheckReady();

return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath));
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(Channel channel, string filename, Stream stream)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (filename == null) throw new ArgumentNullException(nameof(filename));
if (stream == null) throw new ArgumentNullException(nameof(stream));
CheckReady();

var request = new SendFileRequest(channel.Id)
{
Filename = filename,
Stream = stream
};
var model = await _clientRest.Send(request).ConfigureAwait(false);

var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
return msg;
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(User user, string filePath)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (filePath == null) throw new ArgumentNullException(nameof(filePath));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false);
}
/// <summary> Sends a file to the provided channel. </summary>
public async Task<Message> SendFile(User user, string filename, Stream stream)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (filename == null) throw new ArgumentNullException(nameof(filename));
if (stream == null) throw new ArgumentNullException(nameof(stream));
CheckReady();

var channel = await CreatePMChannel(user).ConfigureAwait(false);
return await SendFile(channel, filename, stream).ConfigureAwait(false);
}
private async Task<Message> SendMessageInternal(Channel channel, string text, bool isTextToSpeech)
{
Message msg;
var server = channel.Server;

var mentionedUsers = new List<User>();
text = Mention.CleanUserMentions(this, server, text, mentionedUsers);
if (text.Length > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");

if (Config.UseMessageQueue)
{
var nonce = GenerateNonce();
msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id);
var currentUser = msg.User;
msg.Update(new APIMessage
{
Content = text,
Timestamp = DateTime.UtcNow,
Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name },
ChannelId = channel.Id,
Nonce = IdConvert.ToString(nonce),
IsTextToSpeech = isTextToSpeech
});
msg.State = MessageState.Queued;

_pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray()));
}
else
{
var request = new SendMessageRequest(channel.Id)
{
Content = text,
MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(),
Nonce = null,
IsTTS = isTextToSpeech
};
var model = await _clientRest.Send(request).ConfigureAwait(false);
msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
}
return msg;
}

/// <summary> Edits the provided message, changing only non-null attributes. </summary>
/// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks>
public async Task EditMessage(Message message, string text)
{
if (message == null) throw new ArgumentNullException(nameof(message));
if (text == null) throw new ArgumentNullException(nameof(text));
CheckReady();

var channel = message.Channel;
var mentionedUsers = new List<User>();
if (!channel.IsPrivate)
text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers);

if (text.Length > DiscordConfig.MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less.");

if (Config.UseMessageQueue)
_pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray()));
else
{
var request = new UpdateMessageRequest(message.Channel.Id, message.Id)
{
Content = text,
MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray()
};
await _clientRest.Send(request).ConfigureAwait(false);
}
}

/// <summary> Deletes the provided message. </summary>
public async Task DeleteMessage(Message message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
CheckReady();

var request = new DeleteMessageRequest(message.Id, message.Channel.Id);
try { await _clientRest.Send(request).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
public async Task DeleteMessages(IEnumerable<Message> messages)
{
if (messages == null) throw new ArgumentNullException(nameof(messages));
CheckReady();

foreach (var message in messages)
{
var request = new DeleteMessageRequest(message.Id, message.Channel.Id);
try { await _clientRest.Send(request).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}

/// <summary> Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. </summary>
public async Task<Message[]> DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (limit < 0) throw new ArgumentNullException(nameof(limit));
CheckReady();

if (limit == 0) return new Message[0];
if (channel != null && channel.Type == ChannelType.Text)
{
try
{
var request = new GetMessagesRequest(channel.Id)
{
Limit = limit,
RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after",
RelativeId = relativeMessageId
};
var msgs = await _clientRest.Send(request).ConfigureAwait(false);
return msgs.Select(x =>
{
Message msg = null;
if (useCache)
{
msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id);
var user = msg.User;
if (user != null)
user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp);
}
else
msg = new Message(this, x.Id, x.ChannelId, x.Author.Id);
msg.Update(x);
return msg;
})
.ToArray();
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) { } //Bad Permissions
}
return new Message[0];
}

/// <summary> Marks a given message as read. </summary>
public void AckMessage(Message message)
{
if (message == null) throw new ArgumentNullException(nameof(message));

if (!message.IsAuthor)
_clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id));
}

/// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary>
public IEnumerable<Message> ImportMessages(Channel channel, string json)
{
if (json == null) throw new ArgumentNullException(nameof(json));

var dic = JArray.Parse(json)
.Select(x =>
{
var msg = new Message(this,
x["Id"].Value<ulong>(),
channel.Id,
x["UserId"].Value<ulong>());

var reader = x.CreateReader();
_messageImporter.Populate(reader, msg);
msg.Text = Mention.Resolve(msg, msg.RawText);
return msg;
})
.ToDictionary(x => x.Id);
_messages.Import(dic);
foreach (var msg in dic.Values)
{
var user = msg.User;
if (user != null)
user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp);
}
return dic.Values;
}

/// <summary> Serializes the message cache for a given channel to JSON.</summary>
public string ExportMessages(Channel channel)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));

return JsonConvert.SerializeObject(channel.Messages);
}

/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public User GetUser(Server server, ulong userId)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

return _users[userId, server.Id];
}
/// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary>
public User GetUser(Server server, string username, ushort discriminator)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (username == null) throw new ArgumentNullException(nameof(username));
CheckReady();

return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault();
}

/// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name, @Name and &lt;@Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<User> FindUsers(Server server, string name, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch);
}
/// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name, @Name and &lt;@Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<User> FindUsers(Channel channel, string name, bool exactMatch = false)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch);
}

private IEnumerable<User> FindUsers(IEnumerable<User> users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false)
{
var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));

if (!exactMatch && name.Length >= 2)
{
if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention
{
ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3));
var user = _users[id, serverId];
if (user != null)
query = query.Concat(new User[] { user });
}
else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}

if (discriminator != null)
query = query.Where(x => x.Discriminator == discriminator.Value);
return query;
}

public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel");
CheckReady();

//Modify the roles collection and filter out the everyone role
var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone).Select(x => x.Id);

var request = new UpdateMemberRequest(user.Server.Id, user.Id)
{
IsMuted = isMuted ?? user.IsServerMuted,
IsDeafened = isDeafened ?? user.IsServerDeafened,
VoiceChannelId = voiceChannel?.Id,
RoleIds = roleIds.ToArray()
};
return _clientRest.Send(request);
}

public Task KickUser(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel");
CheckReady();

var request = new KickMemberRequest(user.Server.Id, user.Id);
return _clientRest.Send(request);
}
public Task BanUser(User user, int pruneDays = 0)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel");
CheckReady();

var request = new AddGuildBanRequest(user.Server.Id, user.Id);
request.PruneDays = pruneDays;
return _clientRest.Send(request);
}
public async Task UnbanUser(Server server, ulong userId)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId));
CheckReady();

try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public async Task<int> PruneUsers(Server server, int days, bool simulate = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days));
CheckReady();

var request = new PruneMembersRequest(server.Id)
{
Days = days,
IsSimulation = simulate
};
var response = await _clientRest.Send(request).ConfigureAwait(false);
return response.Pruned;
}

/// <summary>When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server.</summary>
public void RequestOfflineUsers(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));

_webSocket.SendRequestMembers(server.Id, "", 0);
}

public async Task EditProfile(string currentPassword = "",
string username = null, string email = null, string password = null,
Stream avatar = null, ImageType avatarType = ImageType.Png)
{
if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword));
CheckReady();

var request = new UpdateProfileRequest()
{
CurrentPassword = currentPassword,
Email = email ?? _currentUser?.Email,
Password = password,
Username = username ?? _privateUser?.Name,
AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId)
};

await _clientRest.Send(request).ConfigureAwait(false);

if (password != null)
{
var loginRequest = new LoginRequest()
{
Email = _currentUser.Email,
Password = password
};
var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false);
_clientRest.SetToken(loginResponse.Token);
}
}
/// <summary> Returns the role with the specified id, or null if none was found. </summary>
public Role GetRole(ulong id)
{
CheckReady();

return _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)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

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

/// <summary> Note: due to current API limitations, the created role cannot be returned. </summary>
public async Task<Role> CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

var request1 = new CreateRoleRequest(server.Id);
var response1 = await _clientRest.Send(request1).ConfigureAwait(false);
var role = _roles.GetOrAdd(response1.Id, server.Id);
role.Update(response1);

var request2 = new UpdateRoleRequest(role.Server.Id, role.Id)
{
Name = name,
Permissions = (permissions ?? role.Permissions).RawValue,
Color = (color ?? Color.Default).RawValue,
IsHoisted = isHoisted
};
var response2 = await _clientRest.Send(request2).ConfigureAwait(false);
role.Update(response2);

return role;
}

public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null)
{
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

var request1 = new UpdateRoleRequest(role.Server.Id, role.Id)
{
Name = name ?? role.Name,
Permissions = (permissions ?? role.Permissions).RawValue,
Color = (color ?? role.Color).RawValue,
IsHoisted = isHoisted ?? role.IsHoisted
};

var response = await _clientRest.Send(request1).ConfigureAwait(false);

if (position != null)
{
int oldPos = role.Position;
int newPos = position.Value;
int minPos;
Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray();

if (oldPos < newPos) //Moving Down
{
minPos = oldPos;
for (int i = oldPos; i < newPos; i++)
roles[i] = roles[i + 1];
roles[newPos] = role;
}
else //(oldPos > newPos) Moving Up
{
minPos = newPos;
for (int i = oldPos; i > newPos; i--)
roles[i] = roles[i - 1];
roles[newPos] = role;
}

var request2 = new ReorderRolesRequest(role.Server.Id)
{
RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(),
StartPos = minPos
};
await _clientRest.Send(request2).ConfigureAwait(false);
}
}

public async Task DeleteRole(Role role)
{
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public Task ReorderRoles(Server server, IEnumerable<Role> roles, int startPos = 0)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (roles == null) throw new ArgumentNullException(nameof(roles));
if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer.");
CheckReady();

return _clientRest.Send(new ReorderRolesRequest(server.Id)
{
RoleIds = roles.Select(x => x.Id).ToArray(),
StartPos = startPos
});
}

/// <summary> Returns the server with the specified id, or null if none was found. </summary>
public Server GetServer(ulong id)
{
CheckReady();

return _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) throw new ArgumentNullException(nameof(name));
CheckReady();

return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}

/// <summary> Creates a new server with the provided name and region (see Regions). </summary>
public async Task<Server> CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (region == null) throw new ArgumentNullException(nameof(region));
CheckReady();

var request = new CreateGuildRequest()
{
Name = name,
Region = region.Id,
IconBase64 = Base64Image(iconType, icon, null)
};
var response = await _clientRest.Send(request).ConfigureAwait(false);

var server = _servers.GetOrAdd(response.Id);
server.Update(response);
return server;
}

/// <summary> Edits the provided server, changing only non-null attributes. </summary>
public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

var request = new UpdateGuildRequest(server.Id)
{
Name = name ?? server.Name,
Region = region ?? server.Region,
IconBase64 = Base64Image(iconType, icon, server.IconId),
AFKChannelId = server.AFKChannel?.Id,
AFKTimeout = server.AFKTimeout
};
var response = await _clientRest.Send(request).ConfigureAwait(false);
server.Update(response);
}

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

try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public async Task<IEnumerable<Region>> GetVoiceRegions()
{
CheckReady();

var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false);
return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port));
}
public DualChannelPermissions GetChannelPermissions(Channel channel, User user)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return channel.PermissionOverwrites
.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id)
.Select(x => x.Permissions)
.FirstOrDefault();
}
public DualChannelPermissions GetChannelPermissions(Channel channel, Role role)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return channel.PermissionOverwrites
.Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id)
.Select(x => x.Permissions)
.FirstOrDefault();
}

public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny);
}
public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny);
}
public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny);
}
public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);
}
private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
var request = new AddChannelPermissionsRequest(channel.Id)
{
TargetId = targetId,
TargetType = targetType.Value,
Allow = allow?.RawValue ?? 0,
Deny = deny?.RawValue ?? 0
};
return _clientRest.Send(request);
}

public Task RemoveChannelPermissions(Channel channel, User user)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User);
}
public Task RemoveChannelPermissions(Channel channel, Role role)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role);
}
private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType)
{
try
{
var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault();
await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}*/
}

+ 0
- 105
src/Discord.Net/DiscordClient.Permissions.cs View File

@@ -1,105 +0,0 @@
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
public partial class DiscordClient
{
public DualChannelPermissions GetChannelPermissions(Channel channel, User user)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return channel.PermissionOverwrites
.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id)
.Select(x => x.Permissions)
.FirstOrDefault();
}
public DualChannelPermissions GetChannelPermissions(Channel channel, Role role)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return channel.PermissionOverwrites
.Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id)
.Select(x => x.Permissions)
.FirstOrDefault();
}

public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny);
}
public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny);
}
public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny);
}
public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny);
}
private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null)
{
var request = new AddChannelPermissionsRequest(channel.Id)
{
TargetId = targetId,
TargetType = targetType.Value,
Allow = allow?.RawValue ?? 0,
Deny = deny?.RawValue ?? 0
};
return _clientRest.Send(request);
}

public Task RemoveChannelPermissions(Channel channel, User user)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (user == null) throw new ArgumentNullException(nameof(user));
CheckReady();

return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User);
}
public Task RemoveChannelPermissions(Channel channel, Role role)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role);
}
private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType)
{
try
{
var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault();
await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}
}
}

+ 0
- 176
src/Discord.Net/DiscordClient.Roles.cs View File

@@ -1,176 +0,0 @@
using Discord.API;
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
internal sealed class Roles : AsyncCollection<ulong, Role>
{
public Roles(DiscordClient client, object writerLock)
: base(client, writerLock) { }
public Role GetOrAdd(ulong id, ulong serverId)
=> GetOrAdd(id, () => new Role(_client, id, serverId));
}

public class RoleEventArgs : EventArgs
{
public Role Role { get; }
public Server Server => Role.Server;

public RoleEventArgs(Role role) { Role = role; }
}

public partial class DiscordClient
{
public event EventHandler<RoleEventArgs> RoleCreated;
private void RaiseRoleCreated(Role role)
{
if (RoleCreated != null)
EventHelper.Raise(_logger, nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role)));
}
public event EventHandler<RoleEventArgs> RoleUpdated;
private void RaiseRoleDeleted(Role role)
{
if (RoleDeleted != null)
EventHelper.Raise(_logger, nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role)));
}
public event EventHandler<RoleEventArgs> RoleDeleted;
private void RaiseRoleUpdated(Role role)
{
if (RoleUpdated != null)
EventHelper.Raise(_logger, nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role)));
}
internal Roles Roles => _roles;
private readonly Roles _roles;

/// <summary> Returns the role with the specified id, or null if none was found. </summary>
public Role GetRole(ulong id)
{
CheckReady();

return _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)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

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

/// <summary> Note: due to current API limitations, the created role cannot be returned. </summary>
public async Task<Role> CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

var request1 = new CreateRoleRequest(server.Id);
var response1 = await _clientRest.Send(request1).ConfigureAwait(false);
var role = _roles.GetOrAdd(response1.Id, server.Id);
role.Update(response1);
var request2 = new UpdateRoleRequest(role.Server.Id, role.Id)
{
Name = name,
Permissions = (permissions ?? role.Permissions).RawValue,
Color = (color ?? Color.Default).RawValue,
IsHoisted = isHoisted
};
var response2 = await _clientRest.Send(request2).ConfigureAwait(false);
role.Update(response2);

return role;
}

public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null)
{
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();
var request1 = new UpdateRoleRequest(role.Server.Id, role.Id)
{
Name = name ?? role.Name,
Permissions = (permissions ?? role.Permissions).RawValue,
Color = (color ?? role.Color).RawValue,
IsHoisted = isHoisted ?? role.IsHoisted
};

var response = await _clientRest.Send(request1).ConfigureAwait(false);

if (position != null)
{
int oldPos = role.Position;
int newPos = position.Value;
int minPos;
Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray();

if (oldPos < newPos) //Moving Down
{
minPos = oldPos;
for (int i = oldPos; i < newPos; i++)
roles[i] = roles[i + 1];
roles[newPos] = role;
}
else //(oldPos > newPos) Moving Up
{
minPos = newPos;
for (int i = oldPos; i > newPos; i--)
roles[i] = roles[i - 1];
roles[newPos] = role;
}

var request2 = new ReorderRolesRequest(role.Server.Id)
{
RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(),
StartPos = minPos
};
await _clientRest.Send(request2).ConfigureAwait(false);
}
}

public async Task DeleteRole(Role role)
{
if (role == null) throw new ArgumentNullException(nameof(role));
CheckReady();

try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public Task ReorderRoles(Server server, IEnumerable<Role> roles, int startPos = 0)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (roles == null) throw new ArgumentNullException(nameof(roles));
if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer.");
CheckReady();

return _clientRest.Send(new ReorderRolesRequest(server.Id)
{
RoleIds = roles.Select(x => x.Id).ToArray(),
StartPos = startPos
});
}
}
}

+ 0
- 140
src/Discord.Net/DiscordClient.Servers.cs View File

@@ -1,140 +0,0 @@
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
internal sealed class Servers : AsyncCollection<ulong, Server>
{
public Servers(DiscordClient client, object writerLock)
: base(client, writerLock) { }
public Server GetOrAdd(ulong id)
=> GetOrAdd(id, () => new Server(_client, id));
}

public class ServerEventArgs : EventArgs
{
public Server Server { get; }

public ServerEventArgs(Server server) { Server = server; }
}

public partial class DiscordClient
{
public event EventHandler<ServerEventArgs> JoinedServer;
private void RaiseJoinedServer(Server server)
{
if (JoinedServer != null)
EventHelper.Raise(_logger, nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server)));
}
public event EventHandler<ServerEventArgs> LeftServer;
private void RaiseLeftServer(Server server)
{
if (LeftServer != null)
EventHelper.Raise(_logger, nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server)));
}
public event EventHandler<ServerEventArgs> ServerUpdated;
private void RaiseServerUpdated(Server server)
{
if (ServerUpdated != null)
EventHelper.Raise(_logger, nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server)));
}
public event EventHandler<ServerEventArgs> ServerUnavailable;
private void RaiseServerUnavailable(Server server)
{
if (ServerUnavailable != null)
EventHelper.Raise(_logger, nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server)));
}
public event EventHandler<ServerEventArgs> ServerAvailable;
private void RaiseServerAvailable(Server server)
{
if (ServerAvailable != null)
EventHelper.Raise(_logger, nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server)));
}

/// <summary> Returns a collection of all servers this client is a member of. </summary>
public IEnumerable<Server> AllServers { get { CheckReady(); return _servers; } }
internal Servers Servers => _servers;
private readonly Servers _servers;

/// <summary> Returns the server with the specified id, or null if none was found. </summary>
public Server GetServer(ulong id)
{
CheckReady();

return _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) throw new ArgumentNullException(nameof(name));
CheckReady();

return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
}

/// <summary> Creates a new server with the provided name and region (see Regions). </summary>
public async Task<Server> CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (region == null) throw new ArgumentNullException(nameof(region));
CheckReady();

var request = new CreateGuildRequest()
{
Name = name,
Region = region.Id,
IconBase64 = Base64Image(iconType, icon, null)
};
var response = await _clientRest.Send(request).ConfigureAwait(false);

var server = _servers.GetOrAdd(response.Id);
server.Update(response);
return server;
}

/// <summary> Edits the provided server, changing only non-null attributes. </summary>
public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

var request = new UpdateGuildRequest(server.Id)
{
Name = name ?? server.Name,
Region = region ?? server.Region,
IconBase64 = Base64Image(iconType, icon, server.IconId),
AFKChannelId = server.AFKChannel?.Id,
AFKTimeout = server.AFKTimeout
};
var response = await _clientRest.Send(request).ConfigureAwait(false);
server.Update(response);
}
/// <summary> Leaves the provided server, destroying it if you are the owner. </summary>
public async Task LeaveServer(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public async Task<IEnumerable<Region>> GetVoiceRegions()
{
CheckReady();

var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false);
return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port));
}
}
}

+ 0
- 329
src/Discord.Net/DiscordClient.Users.cs View File

@@ -1,329 +0,0 @@
using Discord.API.Client.Rest;
using Discord.Net;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Discord
{
internal sealed class GlobalUsers : AsyncCollection<ulong, GlobalUser>
{
public GlobalUsers(DiscordClient client, object writerLock)
: base(client, writerLock) { }

public GlobalUser GetOrAdd(ulong id) => GetOrAdd(id, () => new GlobalUser(_client, id));
}
internal sealed class Users : AsyncCollection<User.CompositeKey, User>
{
public Users(DiscordClient client, object writerLock)
: base(client, writerLock)
{ }

public User this[ulong userId, ulong? serverId]
=> base[new User.CompositeKey(userId, serverId)];
public User GetOrAdd(ulong userId, ulong? serverId)
=> GetOrAdd(new User.CompositeKey(userId, serverId), () => new User(_client, userId, serverId));
public User TryRemove(ulong userId, ulong? serverId)
=> TryRemove(new User.CompositeKey(userId, serverId));
}

public class UserEventArgs : EventArgs
{
public User User { get; }
public Server Server => User.Server;

public UserEventArgs(User user) { User = user; }
}
public class UserChannelEventArgs : UserEventArgs
{
public Channel Channel { get; }

public UserChannelEventArgs(User user, Channel channel)
: base(user)
{
Channel = channel;
}
}
public class BanEventArgs : EventArgs
{
public ulong UserId { get; }
public Server Server { get; }

public BanEventArgs(ulong userId, Server server)
{
UserId = userId;
Server = server;
}
}

public partial class DiscordClient : IDisposable
{
public event EventHandler<UserEventArgs> UserJoined;
private void RaiseUserJoined(User user)
{
if (UserJoined != null)
EventHelper.Raise(_logger, nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user)));
}
public event EventHandler<UserEventArgs> UserLeft;
private void RaiseUserLeft(User user)
{
if (UserLeft != null)
EventHelper.Raise(_logger, nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user)));
}
public event EventHandler<UserEventArgs> UserUpdated;
private void RaiseUserUpdated(User user)
{
if (UserUpdated != null)
EventHelper.Raise(_logger, nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user)));
}
public event EventHandler<UserEventArgs> UserPresenceUpdated;
private void RaiseUserPresenceUpdated(User user)
{
if (UserPresenceUpdated != null)
EventHelper.Raise(_logger, nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user)));
}
public event EventHandler<UserEventArgs> UserVoiceStateUpdated;
private void RaiseUserVoiceStateUpdated(User user)
{
if (UserVoiceStateUpdated != null)
EventHelper.Raise(_logger, nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user)));
}
public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated;
private void RaiseUserIsTyping(User user, Channel channel)
{
if (UserIsTypingUpdated != null)
EventHelper.Raise(_logger, nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel)));
}
public event EventHandler ProfileUpdated;
private void RaiseProfileUpdated()
{
if (ProfileUpdated != null)
EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty));
}
public event EventHandler<BanEventArgs> UserBanned;
private void RaiseUserBanned(ulong userId, Server server)
{
if (UserBanned != null)
EventHelper.Raise(_logger, nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server)));
}
public event EventHandler<BanEventArgs> UserUnbanned;
private void RaiseUserUnbanned(ulong userId, Server server)
{
if (UserUnbanned != null)
EventHelper.Raise(_logger, nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server)));
}

/// <summary> Returns the current logged-in user used in private channels. </summary>
internal User PrivateUser => _privateUser;
private User _privateUser;

/// <summary> Returns information about the currently logged-in account. </summary>
public GlobalUser CurrentUser => _currentUser;
private GlobalUser _currentUser;

/// <summary> Returns a collection of all unique users this client can currently see. </summary>
public IEnumerable<GlobalUser> AllUsers { get { CheckReady(); return _globalUsers; } }
internal GlobalUsers GlobalUsers => _globalUsers;
private readonly GlobalUsers _globalUsers;

internal Users Users => _users;
private readonly Users _users;

public GlobalUser GetUser(ulong userId)
{
CheckReady();

return _globalUsers[userId];
}
/// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary>
public User GetUser(Server server, ulong userId)
{
if (server == null) throw new ArgumentNullException(nameof(server));
CheckReady();

return _users[userId, server.Id];
}
/// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary>
public User GetUser(Server server, string username, ushort discriminator)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (username == null) throw new ArgumentNullException(nameof(username));
CheckReady();

return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault();
}

/// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name, @Name and &lt;@Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<User> FindUsers(Server server, string name, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch);
}
/// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary>
/// <remarks> Name formats supported: Name, @Name and &lt;@Id&gt;. Search is case-insensitive if exactMatch is false.</remarks>
public IEnumerable<User> FindUsers(Channel channel, string name, bool exactMatch = false)
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();

return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch);
}

private IEnumerable<User> FindUsers(IEnumerable<User> users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false)
{
var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));

if (!exactMatch && name.Length >= 2)
{
if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention
{
ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3));
var user = _users[id, serverId];
if (user != null)
query = query.Concat(new User[] { user });
}
else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention
{
string name2 = name.Substring(1);
query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
}
}

if (discriminator != null)
query = query.Where(x => x.Discriminator == discriminator.Value);
return query;
}

public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel");
CheckReady();

//Modify the roles collection and filter out the everyone role
var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone) .Select(x => x.Id);

var request = new UpdateMemberRequest(user.Server.Id, user.Id)
{
IsMuted = isMuted ?? user.IsServerMuted,
IsDeafened = isDeafened ?? user.IsServerDeafened,
VoiceChannelId = voiceChannel?.Id,
RoleIds = roleIds.ToArray()
};
return _clientRest.Send(request);
}

public Task KickUser(User user)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel");
CheckReady();

var request = new KickMemberRequest(user.Server.Id, user.Id);
return _clientRest.Send(request);
}
public Task BanUser(User user, int pruneDays = 0)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel");
CheckReady();

var request = new AddGuildBanRequest(user.Server.Id, user.Id);
request.PruneDays = pruneDays;
return _clientRest.Send(request);
}
public async Task UnbanUser(Server server, ulong userId)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId));
CheckReady();

try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); }
catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { }
}

public async Task<int> PruneUsers(Server server, int days, bool simulate = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days));
CheckReady();

var request = new PruneMembersRequest(server.Id)
{
Days = days,
IsSimulation = simulate
};
var response = await _clientRest.Send(request).ConfigureAwait(false);
return response.Pruned;
}

/// <summary>When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server.</summary>
public void RequestOfflineUsers(Server server)
{
if (server == null) throw new ArgumentNullException(nameof(server));

_webSocket.SendRequestMembers(server.Id, "", 0);
}

public async Task EditProfile(string currentPassword = "",
string username = null, string email = null, string password = null,
Stream avatar = null, ImageType avatarType = ImageType.Png)
{
if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword));
CheckReady();

var request = new UpdateProfileRequest()
{
CurrentPassword = currentPassword,
Email = email ?? _currentUser?.Email,
Password = password,
Username = username ?? _privateUser?.Name,
AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId)
};

await _clientRest.Send(request).ConfigureAwait(false);

if (password != null)
{
var loginRequest = new LoginRequest()
{
Email = _currentUser.Email,
Password = password
};
var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false);
_clientRest.SetToken(loginResponse.Token);
}
}

public Task SetStatus(UserStatus status)
{
if (status == null) throw new ArgumentNullException(nameof(status));
if (status != UserStatus.Online && status != UserStatus.Idle)
throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status));
CheckReady();
_status = status;
return SendStatus();
}
public Task SetGame(int? gameId)
{
CheckReady();

_gameId = gameId;
return SendStatus();
}
private Task SendStatus()
{
_webSocket.SendUpdateStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId);
return TaskHelper.CompletedTask;
}
}
}

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


+ 13
- 11
src/Discord.Net/DiscordConfig.cs View File

@@ -14,8 +14,8 @@ namespace Discord
Debug = 5
}

public abstract class BaseConfig<T>
where T : BaseConfig<T>
public abstract class Config<T>
where T : Config<T>
{
protected bool _isLocked;
protected internal void Lock() { _isLocked = true; }
@@ -34,20 +34,22 @@ namespace Discord
}
}
public class DiscordConfig : BaseConfig<DiscordConfig>
{
public static string LibName => "Discord.Net";
public class DiscordConfig : Config<DiscordConfig>
{
public const int MaxMessageSize = 2000;

public const string LibName = "Discord.Net";
public static string LibVersion => typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3);
public static string LibUrl => "https://github.com/RogueException/Discord.Net";
public const string LibUrl = "https://github.com/RogueException/Discord.Net";

public static string ClientAPIUrl => "https://discordapp.com/api/";
public static string StatusAPIUrl => "https://status.discordapp.com/api/v2/";
public static string CDNUrl => "https://cdn.discordapp.com/";
public static string InviteUrl => "https://discord.gg/";
public const string ClientAPIUrl = "https://discordapp.com/api/";
public const string StatusAPIUrl = "https://status.discordapp.com/api/v2/";
public const string CDNUrl = "https://cdn.discordapp.com/";
public const string InviteUrl = "https://discord.gg/";

//Global

/// <summary> Name of your application. </summary>
/// <summary> Name of your application. This is used both for the token cache directory and user agent. </summary>
public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } }
private string _appName = null;
/// <summary> Version of your application. </summary>


+ 10
- 0
src/Discord.Net/Enums/ConnectionState.cs View File

@@ -0,0 +1,10 @@
namespace Discord
{
public enum ConnectionState : byte
{
Disconnected,
Connecting,
Connected,
Disconnecting
}
}

src/Discord.Net/ImageType.cs → src/Discord.Net/Enums/ImageType.cs View File


+ 12
- 0
src/Discord.Net/Events/ChannelEventArgs.cs View File

@@ -0,0 +1,12 @@
using System;

namespace Discord
{
public class ChannelEventArgs : EventArgs
{
public Channel Channel { get; }
public Server Server => Channel.Server;

public ChannelEventArgs(Channel channel) { Channel = channel; }
}
}

+ 14
- 0
src/Discord.Net/Events/ChannelUserEventArgs.cs View File

@@ -0,0 +1,14 @@
namespace Discord
{
public class ChannelUserEventArgs
{
public Channel Channel { get; }
public User User { get; }

public ChannelUserEventArgs(Channel channel, User user)
{
Channel = channel;
User = user;
}
}
}

+ 16
- 0
src/Discord.Net/Events/DisconnectedEventArgs.cs View File

@@ -0,0 +1,16 @@
using System;

namespace Discord
{
public class DisconnectedEventArgs : EventArgs
{
public bool WasUnexpected { get; }
public Exception Exception { get; }

public DisconnectedEventArgs(bool wasUnexpected, Exception ex)
{
WasUnexpected = wasUnexpected;
Exception = ex;
}
}
}

+ 20
- 0
src/Discord.Net/Events/LogMessageEventArgs.cs View File

@@ -0,0 +1,20 @@
using System;

namespace Discord
{
public sealed class LogMessageEventArgs : EventArgs
{
public LogSeverity Severity { get; }
public string Source { get; }
public string Message { get; }
public Exception Exception { get; }

public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception)
{
Severity = severity;
Source = source;
Message = msg;
Exception = exception;
}
}
}

+ 14
- 0
src/Discord.Net/Events/MessageEventArgs.cs View File

@@ -0,0 +1,14 @@
using System;

namespace Discord
{
public class MessageEventArgs : EventArgs
{
public Message Message { get; }
public User User => Message.User;
public Channel Channel => Message.Channel;
public Server Server => Message.Server;

public MessageEventArgs(Message msg) { Message = msg; }
}
}

+ 14
- 0
src/Discord.Net/Events/ProfileEventArgs.cs View File

@@ -0,0 +1,14 @@
using System;

namespace Discord
{
public class ProfileEventArgs : EventArgs
{
public Profile Profile { get; }

public ProfileEventArgs(Profile profile)
{
Profile = profile;
}
}
}

+ 12
- 0
src/Discord.Net/Events/RoleEventArgs.cs View File

@@ -0,0 +1,12 @@
using System;

namespace Discord
{
public class RoleEventArgs : EventArgs
{
public Role Role { get; }
public Server Server => Role.Server;

public RoleEventArgs(Role role) { Role = role; }
}
}

+ 11
- 0
src/Discord.Net/Events/ServerEventArgs.cs View File

@@ -0,0 +1,11 @@
using System;

namespace Discord
{
public class ServerEventArgs : EventArgs
{
public Server Server { get; }

public ServerEventArgs(Server server) { Server = server; }
}
}

+ 11
- 0
src/Discord.Net/Events/UserEventArgs.cs View File

@@ -0,0 +1,11 @@
using System;
namespace Discord
{
public class UserEventArgs : EventArgs
{
public User User { get; }
public Server Server => User.Server;

public UserEventArgs(User user) { User = user; }
}
}

src/Discord.Net/Helpers/Format.cs → src/Discord.Net/Format.cs View File

@@ -10,11 +10,11 @@ namespace Discord
static Format()
{
_patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"};
_builder = new StringBuilder(DiscordClient.MaxMessageSize);
_builder = new StringBuilder(DiscordConfig.MaxMessageSize);
}

/// <summary> Removes all special formatting characters from the provided text. </summary>
private static string Escape(string text)
public static string Escape(string text)
{
lock (_builder)
{
@@ -84,10 +84,7 @@ namespace Discord
}
return -1;
}

/// <summary> Returns a markdown-formatted string with no formatting, optionally escaping the contents. </summary>
public static string Normal(string text, bool escape = true)
=> escape ? Escape(text) : text;
/// <summary> Returns a markdown-formatted string with bold formatting, optionally escaping the contents. </summary>
public static string Bold(string text, bool escape = true)
=> escape ? $"**{Escape(text)}**" : $"**{text}**";
@@ -109,20 +106,5 @@ namespace Discord
else
return $"`{text}`";
}

/// <summary> Returns a markdown-formatted string with multiple formatting, optionally escaping the contents. </summary>
public static string Multiple(string text, bool escape = true,
bool bold = false, bool italics = false, bool underline = false, bool strikeout = false,
bool code = false, string codeLanguage = null)
{
string result = text;
if (escape) result = Escape(result);
if (bold) result = Bold(result, false);
if (italics) result = Italics(result, false);
if (underline) result = Underline(result, false);
if (strikeout) result = Strikeout(result, false);
if (code) result = Code(result, codeLanguage);
return result;
}
}
}

+ 0
- 166
src/Discord.Net/Helpers/AsyncCollection.cs View File

@@ -1,166 +0,0 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
internal abstract class AsyncCollection<TKey, TValue> : IEnumerable<TValue>
where TKey : struct, IEquatable<TKey>
where TValue : CachedObject
{
private readonly object _writerLock;

public class CollectionItemEventArgs : EventArgs
{
public TValue Item { get; }
public CollectionItemEventArgs(TValue item) { Item = item; }
}
public class CollectionItemRemappedEventArgs : EventArgs
{
public TValue Item { get; }
public TKey OldId { get; }
public TKey NewId { get; }
public CollectionItemRemappedEventArgs(TValue item, TKey oldId, TKey newId) { Item = item; OldId = oldId; NewId = newId; }
}
public EventHandler<CollectionItemEventArgs> ItemCreated;
private void RaiseItemCreated(TValue item)
{
if (ItemCreated != null)
ItemCreated(this, new CollectionItemEventArgs(item));
}
public EventHandler<CollectionItemEventArgs> ItemDestroyed;
private void RaiseItemDestroyed(TValue item)
{
if (ItemDestroyed != null)
ItemDestroyed(this, new CollectionItemEventArgs(item));
}
public EventHandler<CollectionItemRemappedEventArgs> ItemRemapped;
private void RaiseItemRemapped(TValue item, TKey oldId, TKey newId)
{
if (ItemRemapped != null)
ItemRemapped(this, new CollectionItemRemappedEventArgs(item, oldId, newId));
}

public EventHandler Cleared;
private void RaiseCleared()
{
if (Cleared != null)
Cleared(this, EventArgs.Empty);
}

protected readonly DiscordClient _client;
protected readonly ConcurrentDictionary<TKey, TValue> _dictionary;

public int Count => _dictionary.Count;

protected AsyncCollection(DiscordClient client, object writerLock)
{
_client = client;
_writerLock = writerLock;
_dictionary = new ConcurrentDictionary<TKey, TValue>();
}

public TValue this[TKey? key]
=> key == null ? null : this[key.Value];
public TValue this[TKey key]
{
get
{
if (key.Equals(default(TKey)))
return null;

TValue result;
if (!_dictionary.TryGetValue(key, out result))
return null;
return result;
}
}
protected TValue GetOrAdd(TKey key, Func<TValue> createFunc)
{
TValue result;
if (_dictionary.TryGetValue(key, out result))
return result;

lock (_writerLock)
{
if (!_dictionary.ContainsKey(key))
{
result = createFunc();
if (result.Cache())
{
_dictionary.TryAdd(key, result);
RaiseItemCreated(result);
}
else
result.Uncache();
return result;
}
else
return _dictionary[key];
}
}
protected void Import(IEnumerable<KeyValuePair<TKey, TValue>> items)
{
lock (_writerLock)
{
foreach (var pair in items)
{
var value = pair.Value;
if (value.Cache())
{
_dictionary.TryAdd(pair.Key, value);
RaiseItemCreated(value);
}
else
value.Uncache();
}
}
}

public TValue TryRemove(TKey key)
{
if (_dictionary.ContainsKey(key))
{
lock (_writerLock)
{
TValue result;
if (_dictionary.TryRemove(key, out result))
{
result.Uncache(); //TODO: If this object is accessed before OnRemoved finished firing, properties such as Server.Channels will have null elements
return result;
}
}
}
return null;
}
public void Clear()
{
lock (_writerLock)
{
_dictionary.Clear();
RaiseCleared();
}
}

public TValue Remap(TKey oldKey, TKey newKey)
{
if (_dictionary.ContainsKey(oldKey))
{
lock (_writerLock)
{
TValue result;
if (_dictionary.TryRemove(oldKey, out result))
_dictionary[newKey] = result;
return result;
}
}
return null;
}

public IEnumerator<TValue> GetEnumerator() => _dictionary.Select(x => x.Value).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

+ 0
- 14
src/Discord.Net/Helpers/BitHelper.cs View File

@@ -1,14 +0,0 @@
namespace Discord
{
internal static class BitHelper
{
public static bool GetBit(uint value, int pos) => ((value >> (byte)pos) & 1U) == 1;
public static void SetBit(ref uint value, int pos, bool bitValue)
{
if (bitValue)
value |= (1U << pos);
else
value &= ~(1U << pos);
}
}
}

+ 0
- 54
src/Discord.Net/Helpers/CachedObject.cs View File

@@ -1,54 +0,0 @@
using System.Globalization;

namespace Discord
{
public abstract class CachedObject<TKey> : CachedObject
{
private TKey _id;

internal CachedObject(DiscordClient client, TKey id)
: base(client)
{
_id = id;
}

/// <summary> Returns the unique identifier for this object. </summary>
public TKey Id { get { return _id; } internal set { _id = value; } }

public override string ToString() => $"{this.GetType().Name} {Id}";
}

public abstract class CachedObject
{
protected readonly DiscordClient _client;
private bool _isCached;

internal DiscordClient Client => _client;
internal bool IsCached => _isCached;

internal CachedObject(DiscordClient client)
{
_client = client;
}

internal bool Cache()
{
if (LoadReferences())
{
_isCached = true;
return true;
}
return false;
}
internal void Uncache()
{
if (_isCached)
{
UnloadReferences();
_isCached = false;
}
}
internal abstract bool LoadReferences();
internal abstract void UnloadReferences();
}
}

+ 0
- 30
src/Discord.Net/Helpers/CollectionExtensions.cs View File

@@ -1,30 +0,0 @@
using System.Collections.Generic;
using System.Linq;

namespace Discord
{
public enum EditMode : byte
{
Set,
Add,
Remove
}

internal static class Extensions
{
public static IEnumerable<T> Modify<T>(this IEnumerable<T> original, IEnumerable<T> modified, EditMode mode)
{
if (original == null) return null;
switch (mode)
{
case EditMode.Set:
default:
return modified;
case EditMode.Add:
return original.Concat(modified);
case EditMode.Remove:
return original.Except(modified);
}
}
}
}

+ 0
- 70
src/Discord.Net/Helpers/Reference.cs View File

@@ -1,70 +0,0 @@
using System;

namespace Discord
{
internal class Reference<T>
where T : CachedObject<ulong>
{
private Action<T> _onCache, _onUncache;
private Func<ulong, T> _getItem;
private ulong? _id;
public ulong? Id
{
get { return _id; }
set
{
_id = value;
_value = null;
}
}

private T _value;
public T Value
{
get
{
var v = _value; //A little trickery to make this threadsafe
var id = _id;
if (v != null && !_value.IsCached)
{
v = null;
_value = null;
}
if (v == null && id != null)
{
v = _getItem(id.Value);
if (v != null && _onCache != null)
_onCache(v);
_value = v;
}
return v;
}
}

public bool Load()
{
return Value != null; //Used for precaching
}

public void Unload()
{
if (_onUncache != null)
{
var v = _value;
if (v != null && _onUncache != null)
_onUncache(v);
}
}

public Reference(Func<ulong, T> onUpdate, Action<T> onCache = null, Action<T> onUncache = null)
: this(null, onUpdate, onCache, onUncache) { }
public Reference(ulong? id, Func<ulong, T> getItem, Action<T> onCache = null, Action<T> onUncache = null)
{
_id = id;
_getItem = getItem;
_onCache = onCache;
_onUncache = onUncache;
_value = null;
}
}
}

+ 59
- 0
src/Discord.Net/Logging/LogManager.cs View File

@@ -0,0 +1,59 @@
using System;

namespace Discord.Logging
{
public class LogManager
{
private readonly DiscordClient _client;

public LogSeverity Level { get; }

public event EventHandler<LogMessageEventArgs> Message = delegate { };

internal LogManager(DiscordClient client)
{
_client = client;
Level = client.Config.LogLevel;
}

public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null)
{
if (severity <= Level)
{
try { Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); }
catch { } //We dont want to log on log errors
}
}
public void Log(LogSeverity severity, string source, string message, Exception exception = null)
{
if (severity <= Level)
{
try { Message(this, new LogMessageEventArgs(severity, source, message, exception)); }
catch { } //We dont want to log on log errors
}
}

public void Error(string source, string message, Exception ex = null)
=> Log(LogSeverity.Error, source, message, ex);
public void Error(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Error, source, message, ex);
public void Warning(string source, string message, Exception ex = null)
=> Log(LogSeverity.Warning, source, message, ex);
public void Warning(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Warning, source, message, ex);
public void Info(string source, string message, Exception ex = null)
=> Log(LogSeverity.Info, source, message, ex);
public void Info(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Info, source, message, ex);
public void Verbose(string source, string message, Exception ex = null)
=> Log(LogSeverity.Verbose, source, message, ex);
public void Verbose(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Verbose, source, message, ex);
public void Debug(string source, string message, Exception ex = null)
=> Log(LogSeverity.Debug, source, message, ex);
public void Debug(string source, FormattableString message, Exception ex = null)
=> Log(LogSeverity.Debug, source, message, ex);

public Logger CreateLogger(string name) => new Logger(this, name);
}
}

+ 45
- 0
src/Discord.Net/Logging/Logger.cs View File

@@ -0,0 +1,45 @@
using Discord.Net.WebSockets;
using System;

namespace Discord.Logging
{
public class Logger
{
private readonly LogManager _manager;

public string Name { get; }
public LogSeverity Level => _manager.Level;

internal Logger(LogManager manager, string name)
{
_manager = manager;
Name = name;
}

public void Log(LogSeverity severity, string message, Exception exception = null)
=> _manager.Log(severity, Name, message, exception);
public void Log(LogSeverity severity, FormattableString message, Exception exception = null)
=> _manager.Log(severity, Name, message, exception);

public void Error(string message, Exception exception = null)
=> _manager.Error(Name, message, exception);
public void Error(FormattableString message, Exception exception = null)
=> _manager.Error(Name, message, exception);
public void Warning(string message, Exception exception = null)
=> _manager.Warning(Name, message, exception);
public void Warning(FormattableString message, Exception exception = null)
=> _manager.Warning(Name, message, exception);
public void Info(string message, Exception exception = null)
=> _manager.Info(Name, message, exception);
public void Info(FormattableString message, Exception exception = null)
=> _manager.Info(Name, message, exception);
public void Verbose(string message, Exception exception = null)
=> _manager.Verbose(Name, message, exception);
public void Verbose(FormattableString message, Exception exception = null)
=> _manager.Verbose(Name, message, exception);
public void Debug(string message, Exception exception = null)
=> _manager.Debug(Name, message, exception);
public void Debug(FormattableString message, Exception exception = null)
=> _manager.Debug(Name, message, exception);
}
}

src/Discord.Net/Helpers/Mention.cs → src/Discord.Net/Mention.cs View File

@@ -14,10 +14,6 @@ namespace Discord
[Obsolete("Use User.Mention instead")]
public static string User(User user)
=> $"<@{user.Id}>";
/// <summary> Returns the string used to create a user mention. </summary>
[Obsolete("Use GlobalUser.Mention instead")]
public static string User(GlobalUser user)
=> $"<@{user.Id}>";
/// <summary> Returns the string used to create a channel mention. </summary>
[Obsolete("Use Channel.Mention instead")]
public static string Channel(Channel channel)
@@ -27,12 +23,12 @@ namespace Discord
public static string Everyone()
=> $"@everyone";

internal static string CleanUserMentions(DiscordClient client, Server server, string text, List<User> users = null)
internal static string CleanUserMentions(DiscordClient client, Channel channel, string text, List<User> users = null)
{
return _userRegex.Replace(text, new MatchEvaluator(e =>
{
var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3));
var user = client.Users[id, server?.Id];
var id = e.Value.Substring(2, e.Value.Length - 3).ToId();
var user = channel.GetUser(id);
if (user != null)
{
if (users != null)
@@ -43,54 +39,57 @@ namespace Discord
return '@' + e.Value;
}));
}
internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List<Channel> channels = null)
internal static string CleanChannelMentions(DiscordClient client, Channel channel, string text, List<Channel> channels = null)
{
var server = channel.Server;
if (server == null) return text;

return _channelRegex.Replace(text, new MatchEvaluator(e =>
{
var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3));
var channel = client.Channels[id];
if (channel != null && channel.Server.Id == server.Id)
var id = e.Value.Substring(2, e.Value.Length - 3).ToId();
var mentionedChannel = server.GetChannel(id);
if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id)
{
if (channels != null)
channels.Add(channel);
return '#' + channel.Name;
channels.Add(mentionedChannel);
return '#' + mentionedChannel.Name;
}
else //Channel not found
return '#' + e.Value;
}));
}
/*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null)
/*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null)
{
var server = channel.Server;
if (server == null) return text;

return _roleRegex.Replace(text, new MatchEvaluator(e =>
{
if (roles != null && user.GetPermissions(channel).MentionEveryone)
roles.Add(channel.Server.EveryoneRole);
roles.Add(server.EveryoneRole);
return e.Value;
}));
}*/

/// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary>
public static string Resolve(Message source, string text)
/// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary>
public static string Resolve(Message source, string text)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (text == null) throw new ArgumentNullException(nameof(text));

return Resolve(source.Server, text);
return Resolve(source.Channel, text);
}

/// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary>
public static string Resolve(Server server, string text)
public static string Resolve(Channel channel, string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));

var client = server?.Client;
text = CleanUserMentions(client, server, text);
if (server != null)
{
text = CleanChannelMentions(client, server, text);
//text = CleanRoleMentions(_client, User, channel, text);
}
return text;
var client = channel.Client;
text = CleanUserMentions(client, channel, text);
text = CleanChannelMentions(client, channel, text);
//text = CleanRoleMentions(_client, channel, text);
return text;
}
}
}

+ 102
- 0
src/Discord.Net/MessageQueue.cs View File

@@ -0,0 +1,102 @@
using Discord.API.Client.Rest;
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Net
{
public class MessageQueue
{
private class MessageQueueItem
{
public readonly ulong Id, ChannelId;
public readonly string Text;
public readonly ulong[] MentionedUsers;
public readonly bool IsTTS;

public MessageQueueItem(ulong id, ulong channelId, string text, ulong[] userIds, bool isTTS)
{
Id = id;
ChannelId = channelId;
Text = text;
MentionedUsers = userIds;
IsTTS = isTTS;
}
}

private readonly Random _nonceRand;
private readonly DiscordClient _client;
private readonly ConcurrentQueue<MessageQueueItem> _pending;

internal MessageQueue(DiscordClient client)
{
_client = client;
_nonceRand = new Random();
_pending = new ConcurrentQueue<MessageQueueItem>();
}

public void QueueSend(ulong channelId, string text, ulong[] userIds, bool isTTS)
{
_pending.Enqueue(new MessageQueueItem(0, channelId, text, userIds, isTTS));
}
public void QueueEdit(ulong channelId, ulong messageId, string text, ulong[] userIds)
{
_pending.Enqueue(new MessageQueueItem(channelId, messageId, text, userIds, false));
}

internal Task Run(CancellationToken cancelToken, int interval)
{
return Task.Run(async () =>
{
MessageQueueItem queuedMessage;

while (!cancelToken.IsCancellationRequested)
{
await Task.Delay(interval).ConfigureAwait(false);
while (_pending.TryDequeue(out queuedMessage))
{
try
{
if (queuedMessage.Id == 0)
{
var request = new SendMessageRequest(queuedMessage.ChannelId)
{
Content = queuedMessage.Text,
MentionedUserIds = queuedMessage.MentionedUsers,
Nonce = GenerateNonce().ToIdString(),
IsTTS = queuedMessage.IsTTS
};
await _client.ClientAPI.Send(request).ConfigureAwait(false);
}
else
{
var request = new UpdateMessageRequest(queuedMessage.ChannelId, queuedMessage.Id)
{
Content = queuedMessage.Text,
MentionedUserIds = queuedMessage.MentionedUsers
};
await _client.ClientAPI.Send(request).ConfigureAwait(false);
}
}
catch (WebException) { break; }
catch (HttpException) { /*msg.State = MessageState.Failed;*/ }
}
}
});
}

public void Clear()
{
MessageQueueItem ignored;
while (_pending.TryDequeue(out ignored)) { }
}

private ulong GenerateNonce()
{
lock (_nonceRand)
return (ulong)_nonceRand.Next(1, int.MaxValue);
}
}
}

+ 222
- 245
src/Discord.Net/Models/Channel.cs View File

@@ -1,5 +1,5 @@
using Discord.API.Client;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -7,315 +7,292 @@ using APIChannel = Discord.API.Client.Channel;

namespace Discord
{
public sealed class Channel : CachedObject<ulong>
{
private struct ChannelMember
{
public readonly User User;
public readonly ChannelPermissions Permissions;

public ChannelMember(User user)
{
User = user;
Permissions = new ChannelPermissions();
Permissions.Lock();
}
}

public sealed class PermissionOverwrite
{
public PermissionTarget TargetType { get; }
public ulong TargetId { get; }
public DualChannelPermissions Permissions { get; }
public sealed class Channel
{
private struct Member
{
public readonly User User;
public readonly ChannelPermissions Permissions;
public Member(User user)
{
User = user;
Permissions = new ChannelPermissions();
Permissions.Lock();
}
}

internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny)
{
TargetType = targetType;
TargetId = targetId;
Permissions = new DualChannelPermissions(allow, deny);
Permissions.Lock();
}
}
public sealed class PermissionOverwrite
{
public PermissionTarget TargetType { get; }
public ulong TargetId { get; }
public DualChannelPermissions Permissions { get; }
internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny)
{
TargetType = targetType;
TargetId = targetId;
Permissions = new DualChannelPermissions(allow, deny);
Permissions.Lock();
}
}
private readonly ConcurrentDictionary<ulong, Member> _users;
private readonly ConcurrentDictionary<ulong, Message> _messages;
private Dictionary<ulong, PermissionOverwrite> _permissionOverwrites;

/// <summary> Returns the name of this channel. </summary>
public string Name { get; private set; }
/// <summary> Returns the topic associated with this channel. </summary>
public string Topic { get; private set; }
/// <summary> Returns the position of this channel in the channel list for this server. </summary>
public int Position { get; private set; }
/// <summary> Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). </summary>
public bool IsPrivate => _recipient.Id != null;
/// <summary> Returns the type of this channel (see ChannelTypes). </summary>
public string Type { get; private set; }
/// <summary> Gets the client that generated this channel object. </summary>
internal DiscordClient Client { get; }
/// <summary> Gets the unique identifier for this channel. </summary>
public ulong Id { get; }
/// <summary> Gets the server owning this channel, if this is a public chat. </summary>
public Server Server { get; }
/// <summary> Gets the target user, if this is a private chat. </summary>
public User Recipient { get; }

/// <summary> Returns the server containing this channel. </summary>
[JsonIgnore]
public Server Server => _server.Value;
[JsonProperty]
private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } }
private readonly Reference<Server> _server;
/// <summary> Gets the name of this channel. </summary>
public string Name { get; private set; }
/// <summary> Gets the topic of this channel. </summary>
public string Topic { get; private set; }
/// <summary> Gets the position of this channel relative to other channels in this server. </summary>
public int Position { get; private set; }
/// <summary> Gets the type of this channel (see ChannelTypes). </summary>
public string Type { get; private set; }

/// For private chats, returns the target user, otherwise null.
[JsonIgnore]
public User Recipient => _recipient.Value;
[JsonProperty]
private ulong? RecipientId { get { return _recipient.Id; } set { _recipient.Id = value; } }
private readonly Reference<User> _recipient;
/// <summary> Gets true if this is a private chat with another user. </summary>
public bool IsPrivate => Recipient != null;
/// <summary> Gets the string used to mention this channel. </summary>
public string Mention => $"<#{Id}>";
/// <summary> Gets a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary>
public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>();
/// <summary> Gets a collection of all custom permissions used for this channel. </summary>
public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites.Select(x => x.Value);

//Collections
/// <summary> Returns a collection of all users with read access to this channel. </summary>
[JsonIgnore]
public IEnumerable<User> Members
{
get
{
/// <summary> Gets a collection of all users with read access to this channel. </summary>
public IEnumerable<User> Users
{
get
{
if (IsPrivate)
return _members.Values.Select(x => x.User);
if (_client.Config.UsePermissionsCache)
return _users.Values.Select(x => x.User);
if (Client.Config.UsePermissionsCache)
{
if (Type == ChannelType.Text)
return _members.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User);
return _users.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User);
else if (Type == ChannelType.Voice)
return _members.Values.Select(x => x.User).Where(x => x.VoiceChannel == this);
return _users.Values.Select(x => x.User).Where(x => x.VoiceChannel == this);
}
else
{
if (Type == ChannelType.Text)
{
ChannelPermissions perms = new ChannelPermissions();
return Server.Members.Where(x =>
return Server.Users.Where(x =>
{
UpdatePermissions(x, perms);
return perms.ReadMessages == true;
});
}
else if (Type == ChannelType.Voice)
return Server.Members.Where(x => x.VoiceChannel == this);
return Server.Users.Where(x => x.VoiceChannel == this);
}
return Enumerable.Empty<User>();
return Enumerable.Empty<User>();
}
}
[JsonProperty]
private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id);
private ConcurrentDictionary<ulong, ChannelMember> _members;

/// <summary> Returns a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary>
[JsonIgnore]
public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>();
[JsonProperty]
private IEnumerable<ulong> MessageIds => Messages.Select(x => x.Id);
private readonly ConcurrentDictionary<ulong, Message> _messages;
}

/// <summary> Returns a collection of all custom permissions used for this channel. </summary>
private PermissionOverwrite[] _permissionOverwrites;
public IEnumerable<PermissionOverwrite> PermissionOverwrites { get { return _permissionOverwrites; } internal set { _permissionOverwrites = value.ToArray(); } }
internal Channel(DiscordClient client, ulong id, Server server)
: this(client, id)
{
Server = server;
}
internal Channel(DiscordClient client, ulong id, User recipient)
: this(client, id)
{
Recipient = recipient;
Name = $"@{recipient}";
AddUser(client.PrivateUser);
AddUser(recipient);
}
private Channel(DiscordClient client, ulong id)
{
Client = client;
Id = id;

/// <summary> Returns the string used to mention this channel. </summary>
public string Mention => $"<#{Id}>";
_permissionOverwrites = new Dictionary<ulong, PermissionOverwrite>();
_users = new ConcurrentDictionary<ulong, Member>();
if (client.Config.MessageCacheSize > 0)
_messages = new ConcurrentDictionary<ulong, Message>();
}

internal Channel(DiscordClient client, ulong id, ulong? serverId, ulong? recipientId)
: base(client, id)
{
_server = new Reference<Server>(serverId,
x => _client.Servers[x],
x => x.AddChannel(this),
x => x.RemoveChannel(this));
_recipient = new Reference<User>(recipientId,
x => _client.Users.GetOrAdd(x, _server.Id),
x =>
{
Name = $"@{x}";
if (_server.Id == null)
x.Global.PrivateChannel = this;
},
x =>
{
if (_server.Id == null)
x.Global.PrivateChannel = null;
});
_permissionOverwrites = new PermissionOverwrite[0];
_members = new ConcurrentDictionary<ulong, ChannelMember>();
internal void Update(ChannelReference model)
{
if (!IsPrivate && model.Name != null)
Name = model.Name;
if (model.Type != null)
Type = model.Type;
}
internal void Update(APIChannel model)
{
Update(model as ChannelReference);

if (recipientId != null)
{
AddMember(client.PrivateUser);
AddMember(Recipient);
}
if (model.Position != null)
Position = model.Position.Value;
if (model.Topic != null)
Topic = model.Topic;
if (model.Recipient != null)
Recipient.Update(model.Recipient);

//Local Cache
if (client.Config.MessageCacheSize > 0)
_messages = new ConcurrentDictionary<ulong, Message>();
}
internal override bool LoadReferences()
{
if (IsPrivate)
return _recipient.Load();
else
return _server.Load();
}
internal override void UnloadReferences()
{
_server.Unload();
_recipient.Unload();
var globalMessages = _client.Messages;
if (_client.Config.MessageCacheSize > 0)
if (model.PermissionOverwrites != null)
{
var messages = _messages;
foreach (var message in messages)
globalMessages.TryRemove(message.Key);
messages.Clear();
_permissionOverwrites = model.PermissionOverwrites
.Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny))
.ToDictionary(x => x.TargetId);
UpdatePermissions();
}
}

internal void Update(ChannelReference model)
{
if (!IsPrivate && model.Name != null)
Name = model.Name;
if (model.Type != null)
Type = model.Type;
}
internal void Update(APIChannel model)
{
Update(model as ChannelReference);
if (model.Position != null)
Position = model.Position;
if (model.Topic != null)
Topic = model.Topic;

if (model.PermissionOverwrites != null)
{
_permissionOverwrites = model.PermissionOverwrites
.Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny))
.ToArray();
UpdatePermissions();
}
}

internal void AddMessage(Message message)
{
//Race conditions are okay here - it just means the queue will occasionally go higher than the requested cache size, and fixed later.
var cacheLength = _client.Config.MessageCacheSize;
if (cacheLength > 0)
{
var oldestIds = _messages.Where(x => x.Value.Timestamp < message.Timestamp).Select(x => x.Key).OrderBy(x => x).Take(_messages.Count - cacheLength);
foreach (var id in oldestIds)
{
Message removed;
if (_messages.TryRemove(id, out removed))
_client.Messages.TryRemove(id);
}
_messages.TryAdd(message.Id, message);
}
}
internal void RemoveMessage(Message message)
{
if (_client.Config.MessageCacheSize > 0)
_messages.TryRemove(message.Id, out message);
}
internal void AddMember(User user)
//Members
internal void AddUser(User user)
{
if (!_client.Config.UsePermissionsCache)
if (!Client.Config.UsePermissionsCache)
return;

var member = new ChannelMember(user);
if (_members.TryAdd(user.Id, member))
UpdatePermissions(user, member.Permissions);
var member = new Member(user);
if (_users.TryAdd(user.Id, member))
UpdatePermissions(user, member.Permissions);
}
internal void RemoveMember(User user)
internal void RemoveUser(ulong id)
{
if (!_client.Config.UsePermissionsCache)
if (!Client.Config.UsePermissionsCache)
return;

ChannelMember ignored;
_members.TryRemove(user.Id, out ignored);
}
Member ignored;
_users.TryRemove(id, out ignored);
}
public User GetUser(ulong id)
{
Member result;
_users.TryGetValue(id, out result);
return result.User;
}

internal ChannelPermissions GetPermissions(User user)
//Messages
internal Message AddMessage(ulong id, ulong userId, DateTime timestamp)
{
if (_client.Config.UsePermissionsCache)
Message message = new Message(id, this, userId);
var cacheLength = Client.Config.MessageCacheSize;
if (cacheLength > 0)
{
ChannelMember member;
if (_members.TryGetValue(user.Id, out member))
return member.Permissions;
else
return null;
var oldestIds = _messages
.Where(x => x.Value.Timestamp < timestamp)
.Select(x => x.Key).OrderBy(x => x)
.Take(_messages.Count - cacheLength);
Message removed;
foreach (var removeId in oldestIds)
_messages.TryRemove(removeId, out removed);
return _messages.GetOrAdd(message.Id, message);
}
else
return message;
}
internal Message RemoveMessage(ulong id)
{
if (Client.Config.MessageCacheSize > 0)
{
ChannelPermissions perms = new ChannelPermissions();
UpdatePermissions(user, perms);
return perms;
Message msg;
_messages.TryRemove(id, out msg);
return msg;
}
}
internal void UpdatePermissions()
return null;
}
public Message GetMessage(ulong id)
{
if (!_client.Config.UsePermissionsCache)
Message result;
_messages.TryGetValue(id, out result);
return result;
}

//Permissions
internal void UpdatePermissions()
{
if (!Client.Config.UsePermissionsCache)
return;

foreach (var pair in _members)
foreach (var pair in _users)
{
ChannelMember member = pair.Value;
Member member = pair.Value;
UpdatePermissions(member.User, member.Permissions);
}
}
}
internal void UpdatePermissions(User user)
{
if (!_client.Config.UsePermissionsCache)
if (!Client.Config.UsePermissionsCache)
return;

ChannelMember member;
if (_members.TryGetValue(user.Id, out member))
Member member;
if (_users.TryGetValue(user.Id, out member))
UpdatePermissions(member.User, member.Permissions);
}
internal void UpdatePermissions(User user, ChannelPermissions permissions)
{
uint newPermissions = 0;
var server = Server;
{
uint newPermissions = 0;
var server = Server;

//Load the mask of all permissions supported by this channel type
var mask = ChannelPermissions.All(this).RawValue;
//Load the mask of all permissions supported by this channel type
var mask = ChannelPermissions.All(this).RawValue;

if (server != null)
{
//Start with this user's server permissions
newPermissions = server.GetPermissions(user).RawValue;
if (server != null)
{
//Start with this user's server permissions
newPermissions = server.GetPermissions(user).RawValue;

if (IsPrivate || user.IsOwner)
newPermissions = mask; //Owners always have all permissions
else
{
var channelOverwrites = PermissionOverwrites;
if (IsPrivate || user == Server.Owner)
newPermissions = mask; //Owners always have all permissions
else
{
var channelOverwrites = PermissionOverwrites;

var roles = user.Roles;
foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions &= ~denyRole.Permissions.Deny.RawValue;
foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions |= allowRole.Permissions.Allow.RawValue;
foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0))
newPermissions &= ~denyUser.Permissions.Deny.RawValue;
foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0))
newPermissions |= allowUser.Permissions.Allow.RawValue;
var roles = user.Roles;
foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions &= ~denyRole.Permissions.Deny.RawValue;
foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId)))
newPermissions |= allowRole.Permissions.Allow.RawValue;
foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0))
newPermissions &= ~denyUser.Permissions.Deny.RawValue;
foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0))
newPermissions |= allowUser.Permissions.Allow.RawValue;

if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions))
newPermissions = mask; //ManageRolesOrPermissions gives all permisions
else if (Type == ChannelType.Text && !BitHelper.GetBit(newPermissions, (int)PermissionsBits.ReadMessages))
newPermissions = 0; //No read permission on a text channel removes all other permissions
else
newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example)
}
}
else
newPermissions = mask; //Private messages always have all permissions
if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions))
newPermissions = mask; //ManageRolesOrPermissions gives all permisions
else if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionsBits.ReadMessages))
newPermissions = 0; //No read permission on a text channel removes all other permissions
else
newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example)
}
}
else
newPermissions = mask; //Private messages always have all permissions

if (newPermissions != permissions.RawValue)
permissions.SetRawValueInternal(newPermissions);
}
}
internal ChannelPermissions GetPermissions(User user)
{
if (Client.Config.UsePermissionsCache)
{
Member member;
if (_users.TryGetValue(user.Id, out member))
return member.Permissions;
else
return null;
}
else
{
ChannelPermissions perms = new ChannelPermissions();
UpdatePermissions(user, perms);
return perms;
}
}

public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658);
public override string ToString() => Name ?? IdConvert.ToString(Id);
}
public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658);
public override string ToString() => Name ?? Id.ToIdString();
}
}

+ 2
- 2
src/Discord.Net/Models/GlobalUser.cs View File

@@ -6,7 +6,7 @@ using APIUser = Discord.API.Client.User;

namespace Discord
{
public sealed class GlobalUser : CachedObject<ulong>
/*public sealed class GlobalUser : CachedObject<ulong>
{
/// <summary> Returns the email for this user. Note: this field is only ever populated for the current logged in user. </summary>
[JsonIgnore]
@@ -75,5 +75,5 @@ namespace Discord
public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 7891);
public override string ToString() => IdConvert.ToString(Id);
}
}*/
}

+ 16
- 13
src/Discord.Net/Models/Invite.cs View File

@@ -42,6 +42,7 @@ namespace Discord
public ushort Discriminator { get; }
/// <summary> Returns the unique identifier for this user's avatar. </summary>
public string AvatarId { get; }

/// <summary> Returns the full path to this user's avatar. </summary>
public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId);

@@ -54,24 +55,26 @@ namespace Discord
}
}

/// <summary> Returns information about the server this invite is attached to. </summary>
public ServerInfo Server { get; private set; }
/// <summary> Returns information about the channel this invite is attached to. </summary>
public ChannelInfo Channel { get; private set; }
/// <summary> Gets the unique code for this invite. </summary>
public string Code { get; }
/// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary>
/// <summary> Gets, if enabled, an alternative human-readable invite code. </summary>
public string XkcdCode { get; }
/// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary>
public int MaxAge { get; private set; }
/// <summary> The amount of times this invite has been used. </summary>

/// <summary> Gets information about the server this invite is attached to. </summary>
public ServerInfo Server { get; private set; }
/// <summary> Gets information about the channel this invite is attached to. </summary>
public ChannelInfo Channel { get; private set; }
/// <summary> Gets the time (in seconds) until the invite expires. </summary>
public int? MaxAge { get; private set; }
/// <summary> Gets the amount of times this invite has been used. </summary>
public int Uses { get; private set; }
/// <summary> The max amount of times this invite may be used. </summary>
public int MaxUses { get; private set; }
/// <summary> Returns true if this invite has been destroyed, or you are banned from that server. </summary>
/// <summary> Gets the max amount of times this invite may be used. </summary>
public int? MaxUses { get; private set; }
/// <summary> Returns true if this invite has expired, been destroyed, or you are banned from that server. </summary>
public bool IsRevoked { get; private set; }
/// <summary> If true, a user accepting this invite will be kicked from the server after closing their client. </summary>
public bool IsTemporary { get; private set; }
/// <summary> Gets when this invite was created. </summary>
public DateTime CreatedAt { get; private set; }

/// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary>
@@ -99,7 +102,7 @@ namespace Discord
if (model.IsTemporary != null)
IsTemporary = model.IsTemporary.Value;
if (model.MaxAge != null)
MaxAge = model.MaxAge.Value;
MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null;
if (model.MaxUses != null)
MaxUses = model.MaxUses.Value;
if (model.Uses != null)


+ 23
- 79
src/Discord.Net/Models/Message.cs View File

@@ -15,9 +15,9 @@ namespace Discord
Failed
}

public sealed class Message : CachedObject<ulong>
public sealed class Message
{
internal class ImportResolver : DefaultContractResolver
/*internal class ImportResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
@@ -31,7 +31,7 @@ namespace Discord
}
return property;
}
}
}*/

public sealed class Attachment : File
{
@@ -89,17 +89,24 @@ namespace Discord
internal File() { }
}

/// <summary> Returns true if the logged-in user was mentioned. </summary>
public bool IsMentioningMe { get; private set; }
/// <summary> Returns true if the current user created this message. </summary>
public bool IsAuthor => _client.CurrentUser.Id == _user.Id;
private static readonly Attachment[] _initialAttachments = new Attachment[0];
private static readonly Embed[] _initialEmbeds = new Embed[0];

private readonly ulong _userId;

/// <summary> Returns the unique identifier for this message. </summary>
public ulong Id { get; }
/// <summary> Returns the channel this message was sent to. </summary>
public Channel Channel { get; }

/// <summary> Returns true if the logged-in user was mentioned. </summary>
public bool IsMentioningMe { get; private set; }
/// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary>
public bool IsTTS { get; private set; }
/// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary>
public MessageState State { get; internal set; }
/// <summary> Returns the raw content of this message as it was received from the server. </summary>
public string RawText { get; private set; }
[JsonIgnore]
/// <summary> Returns the content of this message with any special references such as mentions converted. </summary>
public string Text { get; internal set; }
/// <summary> Returns the timestamp for when this message was sent. </summary>
@@ -108,89 +115,26 @@ namespace Discord
public DateTime? EditedTimestamp { get; private set; }
/// <summary> Returns the attachments included in this message. </summary>
public Attachment[] Attachments { get; private set; }
private static readonly Attachment[] _initialAttachments = new Attachment[0];
/// <summary> Returns a collection of all embeded content in this message. </summary>
public Embed[] Embeds { get; private set; }
private static readonly Embed[] _initialEmbeds = new Embed[0];
/// <summary> Returns a collection of all users mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<User> MentionedUsers { get; internal set; }
[JsonProperty]
private IEnumerable<ulong> MentionedUserIds
{
get { return MentionedUsers?.Select(x => x.Id); }
set { MentionedUsers = value.Select(x => _client.GetUser(Server, x)).Where(x => x != null); }
}

/// <summary> Returns a collection of all channels mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<Channel> MentionedChannels { get; internal set; }
[JsonProperty]
private IEnumerable<ulong> MentionedChannelIds
{
get { return MentionedChannels?.Select(x => x.Id); }
set { MentionedChannels = value.Select(x => _client.GetChannel(x)).Where(x => x != null); }
}

/// <summary> Returns a collection of all roles mentioned in this message. </summary>
[JsonIgnore]
public IEnumerable<Role> MentionedRoles { get; internal set; }
[JsonProperty]
private IEnumerable<ulong> MentionedRoleIds
{
get { return MentionedRoles?.Select(x => x.Id); }
set { MentionedRoles = value.Select(x => _client.GetRole(x)).Where(x => x != null); }
}

/// <summary> Returns the server containing the channel this message was sent to. </summary>
[JsonIgnore]
public Server Server => _channel.Value.Server;

/// <summary> Returns the channel this message was sent to. </summary>
[JsonIgnore]
public Channel Channel => _channel.Value;
[JsonProperty]
private ulong? ChannelId => _channel.Id;
private readonly Reference<Channel> _channel;
public Server Server => Channel.Server;
/// <summary> Returns the author of this message. </summary>
public User User => Channel.GetUser(_userId);

/// <summary> Returns the author of this message. </summary>
[JsonIgnore]
public User User => _user.Value;
[JsonProperty]
private ulong? UserId => _user.Id;
private readonly Reference<User> _user;

internal Message(DiscordClient client, ulong id, ulong channelId, ulong userId)
: base(client, id)
internal Message(ulong id, Channel channel, ulong userId)
{
_channel = new Reference<Channel>(channelId,
x => _client.Channels[x],
x => x.AddMessage(this),
x => x.RemoveMessage(this));
_user = new Reference<User>(userId,
x =>
{
var channel = Channel;
if (channel == null) return null;

if (!channel.IsPrivate)
return _client.Users[x, channel.Server.Id];
else
return _client.Users[x, null];
});
Attachments = _initialAttachments;
Embeds = _initialEmbeds;
}
internal override bool LoadReferences()
{
return _channel.Load() && _user.Load();
}
internal override void UnloadReferences()
{
_channel.Unload();
_user.Unload();
}

internal void Update(APIMessage model)
{
@@ -247,7 +191,7 @@ namespace Discord
if (model.Mentions != null)
{
MentionedUsers = model.Mentions
.Select(x => _client.Users[x.Id, Channel.Server?.Id])
.Select(x => Channel.GetUser(x.Id))
.Where(x => x != null)
.ToArray();
}
@@ -266,10 +210,10 @@ namespace Discord
//var mentionedUsers = new List<User>();
var mentionedChannels = new List<Channel>();
//var mentionedRoles = new List<Role>();
text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/);
text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/);
if (server != null)
{
text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels);
text = Mention.CleanChannelMentions(Channel.Client, channel, text, mentionedChannels);
//text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles);
}
Text = text;
@@ -287,7 +231,7 @@ namespace Discord
}
else
{
var me = _client.PrivateUser;
var me = Channel.Client.PrivateUser;
IsMentioningMe = MentionedUsers?.Contains(me) ?? false;
}
}


+ 10
- 4
src/Discord.Net/Models/Permissions.cs View File

@@ -127,9 +127,15 @@ namespace Discord
_rawValue = rawValue;
}

internal bool GetBit(PermissionsBits pos) => BitHelper.GetBit(_rawValue, (int)pos);
internal void SetBit(PermissionsBits pos, bool value) { CheckLock(); SetBitInternal((byte)pos, value); }
internal void SetBitInternal(int pos, bool value) => BitHelper.SetBit(ref _rawValue, pos, value);
internal bool GetBit(PermissionsBits bit) => _rawValue.HasBit((byte)bit);
internal void SetBit(PermissionsBits bit, bool value) { CheckLock(); SetBitInternal((byte)bit, value); }
internal void SetBitInternal(int pos, bool value)
{
if (value)
_rawValue |= (1U << pos);
else
_rawValue &= ~(1U << pos);
}

internal void Lock() => _isLocked = true;
protected void CheckLock()
@@ -140,7 +146,7 @@ namespace Discord

public override bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue;
public override int GetHashCode() => unchecked(_rawValue.GetHashCode() + 393);
}
}

public sealed class DualChannelPermissions
{


+ 29
- 0
src/Discord.Net/Models/Profile.cs View File

@@ -0,0 +1,29 @@
using Newtonsoft.Json;
using APIUser = Discord.API.Client.User;

namespace Discord
{
public sealed class Profile
{
/// <summary> Gets the unique identifier for this user. </summary>
public ulong Id { get; private set; }
/// <summary> Gets the email for this user. </summary>
public string Email { get; private set; }
/// <summary> Gets if the email for this user has been verified. </summary>
public bool? IsVerified { get; private set; }

internal Profile() { }

internal void Update(APIUser model)
{
Id = model.Id;
Email = model.Email;
IsVerified = model.IsVerified;
}

public override bool Equals(object obj)
=> (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id);
public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061);
public override string ToString() => Id.ToIdString();
}
}

+ 39
- 45
src/Discord.Net/Models/Role.cs View File

@@ -1,64 +1,58 @@
using Newtonsoft.Json;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using APIRole = Discord.API.Client.Role;

namespace Discord
{
public sealed class Role : CachedObject<ulong>
{
/// <summary> Returns the name of this role. </summary>
public string Name { get; private set; }
/// <summary> If true, this role is displayed isolated from other users. </summary>
public bool IsHoisted { get; private set; }
/// <summary> Returns the position of this channel in the role list for this server. </summary>
public int Position { get; private set; }
/// <summary> Returns the color of this role. </summary>
public Color Color { get; private set; }
/// <summary> Returns whether this role is managed by server (e.g. for Twitch integration) </summary>
public bool IsManaged { get; private set; }

/// <summary> Returns the the permissions contained by this role. </summary>
public ServerPermissions Permissions { get; }
public sealed class Role
{
private readonly DiscordClient _client;

/// <summary> Returns the server this role is a member of. </summary>
[JsonIgnore]
public Server Server => _server.Value;
[JsonProperty]
private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } }
private readonly Reference<Server> _server;
/// <summary> Gets the unique identifier for this role. </summary>
public ulong Id { get; }
/// <summary> Gets the server this role is a member of. </summary>
public Server Server { get; }
/// <summary> Gets the the permissions contained by this role. </summary>
public ServerPermissions Permissions { get; }
/// <summary> Gets the color of this role. </summary>
public Color Color { get; }

/// <summary> Returns true if this is the role representing all users in a server. </summary>
public bool IsEveryone => _server.Id == null || Id == _server.Id;
/// <summary> Gets the name of this role. </summary>
public string Name { get; private set; }
/// <summary> If true, this role is displayed isolated from other users. </summary>
public bool IsHoisted { get; private set; }
/// <summary> Gets the position of this channel relative to other channels in this server. </summary>
public int Position { get; private set; }
/// <summary> Gets whether this role is managed by server (e.g. for Twitch integration) </summary>
public bool IsManaged { get; private set; }

/// <summary> Returns a list of all members in this role. </summary>
[JsonIgnore]
public IEnumerable<User> Members => _server.Id != null ? (IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this))) : new User[0];
[JsonProperty]
private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id);
//TODO: Add local members cache
/// <summary> Gets true if this is the role representing all users in a server. </summary>
public bool IsEveryone => Id == Server.Id;
/// <summary> Gets a list of all members in this role. </summary>
public IEnumerable<User> Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this));

/// <summary> Returns the string used to mention this role. </summary>
public string Mention { get { if (IsEveryone) return "@everyone"; else throw new InvalidOperationException("Discord currently only supports mentioning the everyone role"); } }
/// <summary> Gets the string used to mention this role. </summary>
public string Mention
{
get
{
if (IsEveryone)
return "@everyone";
else
throw new InvalidOperationException("Roles may only be mentioned if IsEveryone is true");
}
}

internal Role(DiscordClient client, ulong id, ulong serverId)
: base(client, id)
internal Role(ulong id, Server server)
{
_server = new Reference<Server>(serverId, x => _client.Servers[x], x => x.AddRole(this), x => x.RemoveRole(this));
Id = id;
Server = server;
Permissions = new ServerPermissions(0);
Permissions.Lock();
Color = new Color(0);
Color.Lock();
}
internal override bool LoadReferences()
{
return _server.Load();
}
internal override void UnloadReferences()
{
_server.Unload();
}

internal void Update(APIRole model)
{
@@ -81,6 +75,6 @@ namespace Discord

public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653);
public override string ToString() => Name ?? IdConvert.ToString(Id);
public override string ToString() => Name ?? Id.ToIdString();
}
}

+ 188
- 242
src/Discord.Net/Models/Server.cs View File

@@ -1,283 +1,229 @@
using Discord.API.Client;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using APIGuild = Discord.API.Client.Guild;

namespace Discord
{
public sealed class Server : CachedObject<ulong>
{
private struct ServerMember
{
public readonly User User;
public readonly ServerPermissions Permissions;

public ServerMember(User user)
{
User = user;
Permissions = new ServerPermissions();
Permissions.Lock();
}
}
/// <summary> Returns the name of this channel. </summary>
public string Name { get; private set; }
/// <summary> Returns the current logged-in user's data for this server. </summary>
public User CurrentUser { get; internal set; }

/// <summary> Returns the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel (see AFKChannel). </summary>
public int AFKTimeout { get; private set; }
/// <summary> Returns the date and time your joined this server. </summary>
public DateTime JoinedAt { get; private set; }
/// <summary> Returns the region for this server (see Regions). </summary>
public string Region { get; private set; }
/// <summary> Returns the unique identifier for this user's current avatar. </summary>
public string IconId { get; private set; }
/// <summary> Returns the URL to this user's current avatar. </summary>
public string IconUrl => IconId != null ? $"{DiscordConfig.CDNUrl}/icons/{Id}/{IconId}.jpg" : null;

/// <summary> Returns the user that first created this server. </summary>
[JsonIgnore]
public User Owner => _owner.Value;
[JsonProperty]
internal ulong? OwnerId => _owner.Id;
private Reference<User> _owner;

/// <summary> Returns the AFK voice channel for this server (see AFKTimeout). </summary>
[JsonIgnore]
public Channel AFKChannel => _afkChannel.Value;
[JsonProperty]
private ulong? AFKChannelId => _afkChannel.Id;
private Reference<Channel> _afkChannel;

/// <summary> Returns the default channel for this server. </summary>
[JsonIgnore]
public Channel DefaultChannel { get; private set; }

/// <summary> Returns a collection of the ids of all users banned on this server. </summary>
public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key);
private ConcurrentDictionary<ulong, bool> _bans;
/// <summary> Returns a collection of all channels within this server. </summary>
[JsonIgnore]
public IEnumerable<Channel> Channels => _channels.Select(x => x.Value);
/// <summary> Returns a collection of all text channels within this server. </summary>
[JsonIgnore]
public IEnumerable<Channel> TextChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Text);
/// <summary> Returns a collection of all voice channels within this server. </summary>
[JsonIgnore]
public IEnumerable<Channel> VoiceChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Voice);
[JsonProperty]
private IEnumerable<ulong> ChannelIds => Channels.Select(x => x.Id);
private ConcurrentDictionary<ulong, Channel> _channels;

/// <summary> Returns a collection of all users within this server with their server-specific data. </summary>
[JsonIgnore]
public IEnumerable<User> Members => _members.Select(x => x.Value.User);
[JsonProperty]
private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id);
private ConcurrentDictionary<ulong, ServerMember> _members;

/// <summary> Return the the role representing all users in a server. </summary>
[JsonIgnore]
public Role EveryoneRole { get; private set; }
/// <summary> Returns a collection of all roles within this server. </summary>
[JsonIgnore]
public IEnumerable<Role> Roles => _roles.Select(x => x.Value);
[JsonProperty]
private IEnumerable<ulong> RoleIds => Roles.Select(x => x.Id);
private ConcurrentDictionary<ulong, Role> _roles;

internal Server(DiscordClient client, ulong id)
: base(client, id)
{
_owner = new Reference<User>(x => _client.Users[x, Id]);
_afkChannel = new Reference<Channel>(x => _client.Channels[x]);

//Global Cache
_channels = new ConcurrentDictionary<ulong, Channel>();
_roles = new ConcurrentDictionary<ulong, Role>();
_members = new ConcurrentDictionary<ulong, ServerMember>();

//Local Cache
_bans = new ConcurrentDictionary<ulong, bool>();
EveryoneRole = _client.Roles.GetOrAdd(id, id);
}
internal override bool LoadReferences()
{
_afkChannel.Load();
_owner.Load();
return true;
/// <summary> Represents a Discord server (also known as a guild). </summary>
public sealed class Server
{
private struct Member
{
public readonly User User;
public readonly ServerPermissions Permissions;
public Member(User user)
{
User = user;
Permissions = new ServerPermissions();
Permissions.Lock();
}
}
internal override void UnloadReferences()
{
//Global Cache
var globalChannels = _client.Channels;
var channels = _channels;
foreach (var channel in channels)
globalChannels.TryRemove(channel.Key);
channels.Clear();

var globalUsers = _client.Users;
var members = _members;
foreach (var member in members)
globalUsers.TryRemove(member.Key, Id);
members.Clear();

var globalRoles = _client.Roles;
var roles = _roles;
foreach (var role in roles)
globalRoles.TryRemove(role.Key);
roles.Clear();

//Local Cache
_bans.Clear();

_afkChannel.Unload();
private readonly ConcurrentDictionary<ulong, Role> _roles;
private readonly ConcurrentDictionary<ulong, Member> _users;
private readonly ConcurrentDictionary<ulong, Channel> _channels;
private readonly ConcurrentDictionary<ulong, bool> _bans;
private ulong _ownerId;
private ulong? _afkChannelId;

/// <summary> Gets the client that generated this server object. </summary>
internal DiscordClient Client { get; }
/// <summary> Gets the unique identifier for this server. </summary>
public ulong Id { get; }
/// <summary> Gets the default channel for this server. </summary>
public Channel DefaultChannel { get; }
/// <summary> Gets the the role representing all users in a server. </summary>
public Role EveryoneRole { get; }

/// <summary> Gets the name of this server. </summary>
public string Name { get; private set; }

/// <summary> Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel, if one is set. </summary>
public int AFKTimeout { get; private set; }
/// <summary> Gets the date and time you joined this server. </summary>
public DateTime JoinedAt { get; private set; }
/// <summary> Gets the voice region for this server. </summary>
public Region Region { get; private set; }
/// <summary> Gets the unique identifier for this user's current avatar. </summary>
public string IconId { get; private set; }
/// <summary> Gets the URL to this user's current avatar. </summary>
public string IconUrl => GetIconUrl(Id, IconId);
internal static string GetIconUrl(ulong serverId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null;

/// <summary> Gets the user that created this server. </summary>
public User Owner => GetUser(_ownerId);
/// <summary> Gets the AFK voice channel for this server. </summary>
public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null;
/// <summary> Gets the current user in this server. </summary>
public User CurrentUser => GetUser(Client.CurrentUser.Id);

/// <summary> Gets a collection of the ids of all users banned on this server. </summary>
public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key);
/// <summary> Gets a collection of all channels within this server. </summary>
public IEnumerable<Channel> Channels => _channels.Select(x => x.Value);
/// <summary> Gets a collection of all users within this server with their server-specific data. </summary>
public IEnumerable<User> Users => _users.Select(x => x.Value.User);
/// <summary> Gets a collection of all roles within this server. </summary>
public IEnumerable<Role> Roles => _roles.Select(x => x.Value);

internal Server(DiscordClient client, ulong id)
{
Client = client;
Id = id;
_channels = new ConcurrentDictionary<ulong, Channel>();
_roles = new ConcurrentDictionary<ulong, Role>();
_users = new ConcurrentDictionary<ulong, Member>();
_bans = new ConcurrentDictionary<ulong, bool>();
DefaultChannel = AddChannel(id);
EveryoneRole = AddRole(id);
}

internal void Update(GuildReference model)
{
if (model.Name != null)
Name = model.Name;
}

internal void Update(GuildReference model)
{
if (model.Name != null)
Name = model.Name;
}
internal void Update(Guild model)
{
Update(model as GuildReference);

if (model.AFKTimeout != null)
AFKTimeout = model.AFKTimeout.Value;
if (model.AFKChannelId != null)
if (model.JoinedAt != null)
JoinedAt = model.JoinedAt.Value;
if (model.OwnerId != null)
_owner.Id = model.OwnerId.Value;
if (model.Region != null)
Region = model.Region;
if (model.Icon != null)
IconId = model.Icon;

if (model.Roles != null)
{
var roleCache = _client.Roles;
foreach (var x in model.Roles)
{
var role = roleCache.GetOrAdd(x.Id, Id);
role.Update(x);
}
{
Update(model as GuildReference);

if (model.AFKTimeout != null)
AFKTimeout = model.AFKTimeout.Value;
_afkChannelId = model.AFKChannelId.Value; //Can be null
if (model.JoinedAt != null)
JoinedAt = model.JoinedAt.Value;
if (model.OwnerId != null)
_ownerId = model.OwnerId.Value;
if (model.Region != null)
Region = Client.GetRegion(model.Region);
if (model.Icon != null)
IconId = model.Icon;

if (model.Roles != null)
{
foreach (var x in model.Roles)
AddRole(x.Id).Update(x);
}
_afkChannel.Id = model.AFKChannelId; //Can be null
}
internal void Update(ExtendedGuild model)
{
Update(model as APIGuild);
var channels = _client.Channels;
foreach (var subModel in model.Channels)
{
var channel = channels.GetOrAdd(subModel.Id, Id);
channel.Update(subModel);
}
var usersCache = _client.Users;
foreach (var subModel in model.Members)
{
var user = usersCache.GetOrAdd(subModel.User.Id, Id);
user.Update(subModel);
}
foreach (var subModel in model.VoiceStates)
{
var user = usersCache[subModel.UserId, Id];
if (user != null)
user.Update(subModel);
}
foreach (var subModel in model.Presences)
{
var user = usersCache[subModel.User.Id, Id];
if (user != null)
user.Update(subModel);
}
}
}
internal void Update(ExtendedGuild model)
{
Update(model as Guild);

if (model.Channels != null)
{
foreach (var subModel in model.Channels)
AddChannel(subModel.Id).Update(subModel);
}
if (model.Members != null)
{
foreach (var subModel in model.Members)
AddMember(subModel.User.Id).Update(subModel);
}
if (model.VoiceStates != null)
{
foreach (var subModel in model.VoiceStates)
GetUser(subModel.UserId)?.Update(subModel);
}
if (model.Presences != null)
{
foreach (var subModel in model.Presences)
GetUser(subModel.User.Id)?.Update(subModel);
}
}

internal void AddBan(ulong banId)
{
_bans.TryAdd(banId, true);
}
internal bool RemoveBan(ulong banId)
{
bool ignored;
return _bans.TryRemove(banId, out ignored);
}
//Bans
internal void AddBan(ulong banId)
=> _bans.TryAdd(banId, true);
internal bool RemoveBan(ulong banId)
{
bool ignored;
return _bans.TryRemove(banId, out ignored);
}

internal void AddChannel(Channel channel)
{
if (_channels.TryAdd(channel.Id, channel))
{
if (channel.Id == Id)
DefaultChannel = channel;
}
}
internal void RemoveChannel(Channel channel)
{
_channels.TryRemove(channel.Id, out channel);
}
//Channels
internal Channel AddChannel(ulong id)
=> _channels.GetOrAdd(id, x => new Channel(Client, x, this));
internal Channel RemoveChannel(ulong id)
{
Channel channel;
_channels.TryRemove(id, out channel);
return channel;
}
public Channel GetChannel(ulong id)
{
Channel result;
_channels.TryGetValue(id, out result);
return result;
}

internal void AddMember(User user)
{
if (_members.TryAdd(user.Id, new ServerMember(user)))
//Members
internal User AddMember(ulong id)
{
User newUser = null;
var user = _users.GetOrAdd(id, x => new Member(new User(id, this)));
if (user.User == newUser)
{
foreach (var channel in Channels)
channel.AddMember(user);
channel.AddUser(newUser);
}
return user.User;
}
internal void RemoveMember(User user)
internal User RemoveMember(ulong id)
{
ServerMember ignored;
if (_members.TryRemove(user.Id, out ignored))
Member member;
if (_users.TryRemove(id, out member))
{
foreach (var channel in Channels)
channel.RemoveMember(user);
channel.RemoveUser(id);
}
}
internal void HasMember(User user) => _members.ContainsKey(user.Id);
return member.User;
}
public User GetUser(ulong id)
{
Member result;
_users.TryGetValue(id, out result);
return result.User;
}

internal void AddRole(Role role)
{
if (_roles.TryAdd(role.Id, role))
{
if (role.Id == Id)
EveryoneRole = role;
}
}
internal void RemoveRole(Role role)
{
_roles.TryRemove(role.Id, out role);
}
//Roles
internal Role AddRole(ulong id)
=> _roles.GetOrAdd(id, x => new Role(x, this));
internal Role RemoveRole(ulong id)
{
Role role;
_roles.TryRemove(id, out role);
return role;
}
public Role GetRole(ulong id)
{
Role result;
_roles.TryGetValue(id, out result);
return result;
}

internal ServerPermissions GetPermissions(User user)
//Permissions
internal ServerPermissions GetPermissions(User user)
{
ServerMember member;
if (_members.TryGetValue(user.Id, out member))
Member member;
if (_users.TryGetValue(user.Id, out member))
return member.Permissions;
else
return null;
}
internal void UpdatePermissions(User user)
{
ServerMember member;
if (_members.TryGetValue(user.Id, out member))
Member member;
if (_users.TryGetValue(user.Id, out member))
UpdatePermissions(member.User, member.Permissions);
}
private void UpdatePermissions(User user, ServerPermissions permissions)
{
uint newPermissions = 0;

if (user.IsOwner)
if (user.Id == _ownerId)
newPermissions = ServerPermissions.All.RawValue;
else
{
@@ -285,7 +231,7 @@ namespace Discord
newPermissions |= serverRole.Permissions.RawValue;
}

if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions))
if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions))
newPermissions = ServerPermissions.All.RawValue;

if (newPermissions != permissions.RawValue)
@@ -298,6 +244,6 @@ namespace Discord

public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175);
public override string ToString() => Name ?? IdConvert.ToString(Id);
public override string ToString() => Name ?? Id.ToIdString();
}
}

+ 127
- 146
src/Discord.Net/Models/User.cs View File

@@ -1,5 +1,4 @@
using Discord.API.Client;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,8 +6,19 @@ using APIMember = Discord.API.Client.Member;

namespace Discord
{
public class User : CachedObject<ulong>
public class User
{
[Flags]
private enum VoiceState : byte
{
None = 0x0,
SelfMuted = 0x01,
SelfDeafened = 0x02,
ServerMuted = 0x04,
ServerDeafened = 0x08,
ServerSuppressed = 0x10,
}

internal struct CompositeKey : IEquatable<CompositeKey>
{
public ulong ServerId, UserId;
@@ -24,92 +34,71 @@ namespace Discord
=> unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23);
}

public static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null;

/// <summary> Returns a unique identifier combining this user's id with its server's. </summary>
internal CompositeKey UniqueId => new CompositeKey(_server.Id ?? 0, Id);
/// <summary> Returns the name of this user on this server. </summary>
public string Name { get; private set; }
/// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary>
public ushort Discriminator { get; private set; }
/// <summary> Returns the unique identifier for this user's current avatar. </summary>
public string AvatarId { get; private set; }
/// <summary> Returns the URL to this user's current avatar. </summary>
public string AvatarUrl => GetAvatarUrl(Id, AvatarId);
/// <summary> Returns the datetime that this user joined this server. </summary>
public DateTime JoinedAt { get; private set; }
internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null;

private VoiceState _voiceState;
private DateTime? _lastOnline;
private ulong? _voiceChannelId;
private Dictionary<ulong, Role> _roles;

public bool IsSelfMuted { get; private set; }
public bool IsSelfDeafened { get; private set; }
public bool IsServerMuted { get; private set; }
public bool IsServerDeafened { get; private set; }
public bool IsServerSuppressed { get; private set; }
public bool IsPrivate => _server.Id == null;
public bool IsOwner => _server.Value.OwnerId == Id;
/// <summary> Gets the client that generated this user object. </summary>
internal DiscordClient Client { get; }
/// <summary> Gets the unique identifier for this user. </summary>
public ulong Id { get; }
/// <summary> Gets the server this user is a member of. </summary>
public Server Server { get; }

public string SessionId { get; private set; }
public string Token { get; private set; }
/// <summary> Gets the name of this user. </summary>
public string Name { get; private set; }
/// <summary> Gets an id uniquely identifying from others with the same name. </summary>
public ushort Discriminator { get; private set; }
/// <summary> Gets the unique identifier for this user's current avatar. </summary>
public string AvatarId { get; private set; }
/// <summary> Gets the id for the game this user is currently playing. </summary>
public string GameId { get; private set; }
/// <summary> Gets the current status for this user. </summary>
public UserStatus Status { get; private set; }
/// <summary> Gets the datetime that this user joined this server. </summary>
public DateTime JoinedAt { get; private set; }
/// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary>
public DateTime? LastActivityAt { get; private set; }
// /// <summary> Gets this user's voice session id. </summary>
// public string SessionId { get; private set; }
// /// <summary> Gets this user's voice token. </summary>
// public string Token { get; private set; }

/// <summary> Returns the id for the game this user is currently playing. </summary>
public int? GameId { get; private set; }
/// <summary> Returns the current status for this user. </summary>
public UserStatus Status { get; private set; }
/// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary>
public DateTime? LastActivityAt { get; private set; }
/// <summary> Returns the string used to mention this user. </summary>
public string Mention => $"<@{Id}>";
/// <summary> Returns true if this user has marked themselves as muted. </summary>
public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0;
/// <summary> Returns true if this user has marked themselves as deafened. </summary>
public bool IsSelfDeafened => (_voiceState & VoiceState.SelfDeafened) != 0;
/// <summary> Returns true if the server is blocking audio from this user. </summary>
public bool IsServerMuted => (_voiceState & VoiceState.ServerMuted) != 0;
/// <summary> Returns true if the server is blocking audio to this user. </summary>
public bool IsServerDeafened => (_voiceState & VoiceState.ServerDeafened) != 0;
/// <summary> Returns true if the server is temporarily blocking audio to/from this user. </summary>
public bool IsServerSuppressed => (_voiceState & VoiceState.ServerSuppressed) != 0;
/// <summary> Returns the time this user was last seen online in this server. </summary>
public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline;
private DateTime? _lastOnline;

//References
[JsonIgnore]
public GlobalUser Global => _globalUser.Value;
private readonly Reference<GlobalUser> _globalUser;

[JsonIgnore]
public Server Server => _server.Value;
private readonly Reference<Server> _server;
[JsonProperty]
private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } }

[JsonIgnore]
public Channel VoiceChannel => _voiceChannel.Value;
private Reference<Channel> _voiceChannel;
[JsonProperty]
private ulong? VoiceChannelId { get { return _voiceChannel.Id; } set { _voiceChannel.Id = value; } }

//Collections
[JsonIgnore]
public IEnumerable<Role> Roles => _roles.Select(x => x.Value);
private Dictionary<ulong, Role> _roles;
[JsonProperty]
private IEnumerable<ulong> RoleIds => _roles.Select(x => x.Key);

/// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary>
[JsonIgnore]
public IEnumerable<Message> Messages
{
get
{
if (_server.Id != null)
return Server.Channels.SelectMany(x => x.Messages.Where(y => y.User.Id == Id));
else
return Global.PrivateChannel.Messages.Where(x => x.User.Id == Id);
}
}
public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline;
/// <summary> Gets this user's </summary>
public Channel VoiceChannel => _voiceChannelId != null ? Server.GetChannel(_voiceChannelId.Value) : null;
/// <summary> Gets the URL to this user's current avatar. </summary>
public string AvatarUrl => GetAvatarUrl(Id, AvatarId);
/// <summary> Gets all roles that have been assigned to this user, including the everyone role. </summary>
public IEnumerable<Role> Roles => _roles.Select(x => x.Value);

/// <summary> Returns a collection of all channels this user has permissions to join on this server. </summary>
[JsonIgnore]
public IEnumerable<Channel> Channels
/// <summary> Returns a collection of all channels this user has permissions to join on this server. </summary>
public IEnumerable<Channel> Channels
{
get
{
if (_server.Id != null)
if (Server != null)
{
if (_client.Config.UsePermissionsCache)
if (Client.Config.UsePermissionsCache)
{
return Server.Channels
.Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) ||
return Server.Channels.Where(x =>
(x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) ||
(x.Type == ChannelType.Voice && x.GetPermissions(this).Connect));
}
else
@@ -120,63 +109,37 @@ namespace Discord
{
x.UpdatePermissions(this, perms);
return (x.Type == ChannelType.Text && perms.ReadMessages) ||
(x.Type == ChannelType.Voice && perms.Connect);
(x.Type == ChannelType.Voice && perms.Connect);
});
}
}
else
{
var privateChannel = Global.PrivateChannel;
if (privateChannel != null)
return new Channel[] { privateChannel };
else
return new Channel[0];
if (this == Client.PrivateUser)
return Client.PrivateChannels;
else
{
var privateChannel = Client.GetPrivateChannel(Id);
if (privateChannel != null)
return new Channel[] { privateChannel };
else
return new Channel[0];
}
}
}
}

/// <summary> Returns the string used to mention this user. </summary>
public string Mention => $"<@{Id}>";

internal User(DiscordClient client, ulong id, ulong? serverId)
: base(client, id)
internal User(ulong id, Server server)
{
_globalUser = new Reference<GlobalUser>(id,
x => _client.GlobalUsers.GetOrAdd(x),
x => x.AddUser(this),
x => x.RemoveUser(this));
_server = new Reference<Server>(serverId,
x => _client.Servers[x],
x =>
{
x.AddMember(this);
if (Id == _client.CurrentUser.Id)
x.CurrentUser = this;
},
x =>
{
x.RemoveMember(this);
if (Id == _client.CurrentUser.Id)
x.CurrentUser = null;
});
_voiceChannel = new Reference<Channel>(x => _client.Channels[x]);
Server = server;
_roles = new Dictionary<ulong, Role>();

Status = UserStatus.Offline;

if (serverId == null)
if (server == null)
UpdateRoles(null);
}
internal override bool LoadReferences()
{
return _globalUser.Load() &&
(IsPrivate || _server.Load());
}
internal override void UnloadReferences()
{
_globalUser.Unload();
_server.Unload();
}

internal void Update(UserReference model)
{
@@ -195,24 +158,29 @@ namespace Discord
if (model.JoinedAt.HasValue)
JoinedAt = model.JoinedAt.Value;
if (model.Roles != null)
UpdateRoles(model.Roles.Select(x => _client.Roles[x]));
UpdateRoles(model.Roles.Select(x => Server.GetRole(x)));
}
internal void Update(ExtendedGuild.ExtendedMemberInfo model)
{
Update(model as APIMember);
if (model.IsServerMuted == true)
_voiceState |= VoiceState.ServerMuted;
else if (model.IsServerMuted == false)
_voiceState &= ~VoiceState.ServerMuted;

if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
}
if (model.IsServerDeafened.Value == true)
_voiceState |= VoiceState.ServerDeafened;
else if (model.IsServerDeafened.Value == false)
_voiceState &= ~VoiceState.ServerDeafened;
}
internal void Update(MemberPresence model)
{
if (model.User != null)
Update(model.User as UserReference);

if (model.Roles != null)
UpdateRoles(model.Roles.Select(x => _client.Roles[x]));
if (model.Roles != null)
UpdateRoles(model.Roles.Select(x => Server.GetRole(x)));
if (model.Status != null && Status != model.Status)
{
Status = UserStatus.FromString(model.Status);
@@ -223,42 +191,55 @@ namespace Discord
GameId = model.GameId; //Allows null
}
internal void Update(MemberVoiceState model)
{
if (model.IsServerDeafened != null)
IsServerDeafened = model.IsServerDeafened.Value;
if (model.IsServerMuted != null)
IsServerMuted = model.IsServerMuted.Value;
if (model.SessionId != null)
{
if (model.IsSelfMuted.Value == true)
_voiceState |= VoiceState.SelfMuted;
else if (model.IsSelfMuted.Value == false)
_voiceState &= ~VoiceState.SelfMuted;
if (model.IsSelfDeafened.Value == true)
_voiceState |= VoiceState.SelfDeafened;
else if (model.IsSelfDeafened.Value == false)
_voiceState &= ~VoiceState.SelfDeafened;
if (model.IsServerMuted == true)
_voiceState |= VoiceState.ServerMuted;
else if (model.IsServerMuted == false)
_voiceState &= ~VoiceState.ServerMuted;
if (model.IsServerDeafened.Value == true)
_voiceState |= VoiceState.ServerDeafened;
else if (model.IsServerDeafened.Value == false)
_voiceState &= ~VoiceState.ServerDeafened;
if (model.IsServerSuppressed.Value == true)
_voiceState |= VoiceState.ServerSuppressed;
else if (model.IsServerSuppressed.Value == false)
_voiceState &= ~VoiceState.ServerSuppressed;
/*if (model.SessionId != null)
SessionId = model.SessionId;
if (model.Token != null)
Token = model.Token;
if (model.IsSelfDeafened != null)
IsSelfDeafened = model.IsSelfDeafened.Value;
if (model.IsSelfMuted != null)
IsSelfMuted = model.IsSelfMuted.Value;
if (model.IsServerSuppressed != null)
IsServerSuppressed = model.IsServerSuppressed.Value;
Token = model.Token;*/
_voiceChannel.Id = model.ChannelId; //Allows null
_voiceChannelId = model.ChannelId; //Allows null
}
private void UpdateRoles(IEnumerable<Role> roles)
{
var newRoles = new Dictionary<ulong, Role>();
if (roles != null)
{
foreach (var r in roles)
newRoles[r.Id] = r;
foreach (var r in roles)
{
if (r != null)
newRoles[r.Id] = r;
}
}

if (_server.Id != null)
if (Server != null)
{
var everyone = Server.EveryoneRole;
newRoles.Add(everyone.Id, everyone);
newRoles[everyone.Id] = everyone;
}
_roles = newRoles;

if (!IsPrivate)
if (Server != null)
Server.UpdatePermissions(this);
}

@@ -285,6 +266,6 @@ namespace Discord

public override bool Equals(object obj) => obj is User && (obj as User).Id == Id;
public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230);
public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : IdConvert.ToString(Id);
public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString();
}
}

+ 2
- 2
src/Discord.Net/Net/HttpException.cs View File

@@ -4,7 +4,7 @@ using System.Runtime.Serialization;

namespace Discord.Net
{
#if NET45
#if NET46
[Serializable]
#endif
public class HttpException : Exception
@@ -16,7 +16,7 @@ namespace Discord.Net
{
StatusCode = statusCode;
}
#if NET45
#if NET46
public override void GetObjectData(SerializationInfo info, StreamingContext context)
=> base.GetObjectData(info, context);
#endif


+ 0
- 29
src/Discord.Net/Net/Rest/RestClient.Events.cs View File

@@ -1,29 +0,0 @@
using System;

namespace Discord.Net.Rest
{
public sealed partial class RestClient
{
public class RequestEventArgs : EventArgs
{
public string Method { get; }
public string Path { get; }
public string Payload { get; }
public double ElapsedMilliseconds { get; }
public RequestEventArgs(string method, string path, string payload, double milliseconds)
{
Method = method;
Path = path;
Payload = payload;
ElapsedMilliseconds = milliseconds;
}
}

public event EventHandler<RequestEventArgs> OnRequest;
private void RaiseOnRequest(string method, string path, string payload, double milliseconds)
{
if (OnRequest != null)
OnRequest(this, new RequestEventArgs(method, path, payload, milliseconds));
}
}
}

+ 59
- 26
src/Discord.Net/Net/Rest/RestClient.cs View File

@@ -1,4 +1,5 @@
using Discord.API;
using Discord.Logging;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
@@ -7,25 +8,53 @@ using System.Threading.Tasks;

namespace Discord.Net.Rest
{
public sealed partial class RestClient
public class RequestEventArgs : EventArgs
{
public string Method { get; }
public string Path { get; }
public string Payload { get; }
public double ElapsedMilliseconds { get; }
public RequestEventArgs(string method, string path, string payload, double milliseconds)
{
Method = method;
Path = path;
Payload = payload;
ElapsedMilliseconds = milliseconds;
}
}

public sealed partial class RestClient
{
private readonly DiscordConfig _config;
private readonly IRestEngine _engine;
private CancellationToken _cancelToken;
private string _token;

internal Logger Logger { get; }

public CancellationToken CancelToken { get; set; }

public string Token
{
get { return _token; }
set
{
_token = value;
_engine.SetToken(value);
}
}

public RestClient(DiscordConfig config, Logger logger, string baseUrl)
public RestClient(DiscordConfig config, string baseUrl, Logger logger)
{
_config = config;
Logger = logger;

#if !DOTNET5_4
_engine = new RestSharpEngine(config, logger, baseUrl);
_engine = new RestSharpEngine(config, baseUrl);
#else
//_engine = new BuiltInRestEngine(config, logger, baseUrl);
//_engine = new BuiltInRestEngine(config, baseUrl);
#endif
}

public void SetToken(string token) => _engine.SetToken(token);
public void SetCancelToken(CancellationToken token) => _cancelToken = token;

public async Task<ResponseT> Send<ResponseT>(IRestRequest<ResponseT> request)
where ResponseT : class
{
@@ -69,24 +98,26 @@ namespace Discord.Net.Rest
requestJson = JsonConvert.SerializeObject(payload);

Stopwatch stopwatch = null;
if (_config.LogLevel >= LogSeverity.Verbose)
if (Logger.Level >= LogSeverity.Verbose)
stopwatch = Stopwatch.StartNew();

string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false);
string responseJson = await _engine.Send(method, path, requestJson, CancelToken).ConfigureAwait(false);

if (_config.LogLevel >= LogSeverity.Verbose)
if (Logger.Level >= LogSeverity.Verbose)
{
stopwatch.Stop();
if (payload != null && _config.LogLevel >= LogSeverity.Debug)
double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2);

string log = $"{method} {path}: {milliseconds} ms";
if (payload != null && _config.LogLevel >= LogSeverity.Debug)
{
if (isPrivate)
RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
else
RaiseOnRequest(method, path, requestJson, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
log += $" [Hidden]";
else
log += $" {requestJson}";
}
else
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
}
Logger.Verbose(log);
}

return responseJson;
}
@@ -100,19 +131,21 @@ namespace Discord.Net.Rest
var isPrivate = request.IsPrivate;

Stopwatch stopwatch = null;
if (_config.LogLevel >= LogSeverity.Verbose)
if (Logger.Level >= LogSeverity.Verbose)
stopwatch = Stopwatch.StartNew();
string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false);
string responseJson = await _engine.SendFile(method, path, filename, stream, CancelToken).ConfigureAwait(false);

if (_config.LogLevel >= LogSeverity.Verbose)
if (Logger.Level >= LogSeverity.Verbose)
{
stopwatch.Stop();
if (_config.LogLevel >= LogSeverity.Debug && !isPrivate)
RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
else
RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond);
}
double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2);

string log = $"{method} {path}: {milliseconds} ms";
if (_config.LogLevel >= LogSeverity.Debug && !isPrivate)
log += $" {filename}";
Logger.Verbose(log);
}

return responseJson;
}


+ 8
- 16
src/Discord.Net/Net/Rest/SharpRestEngine.cs View File

@@ -12,15 +12,13 @@ namespace Discord.Net.Rest
{
private readonly DiscordConfig _config;
private readonly RestSharp.RestClient _client;
private readonly Logger _logger;

private readonly object _rateLimitLock;
private DateTime _rateLimitTime;

public RestSharpEngine(DiscordConfig config, Logger logger, string baseUrl)
public RestSharpEngine(DiscordConfig config, string baseUrl)
{
_config = config;
_logger = logger;
_rateLimitLock = new object();
_client = new RestSharp.RestClient(baseUrl)
{
@@ -28,9 +26,6 @@ namespace Discord.Net.Rest
ReadWriteTimeout = _config.RestTimeout,
UserAgent = config.UserAgent
};
/*if (_config.ProxyUrl != null)
_client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials);
else*/
_client.Proxy = null;
_client.RemoveDefaultParameter("Accept");
_client.AddDefaultHeader("accept", "*/*");
@@ -83,21 +78,18 @@ namespace Discord.Net.Rest
int milliseconds;
if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds))
{
if (_logger != null)
/*var now = DateTime.UtcNow;
if (now >= _rateLimitTime)
{
var now = DateTime.UtcNow;
if (now >= _rateLimitTime)
lock (_rateLimitLock)
{
lock (_rateLimitLock)
if (now >= _rateLimitTime)
{
if (now >= _rateLimitTime)
{
_rateLimitTime = now.AddMilliseconds(milliseconds);
_logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds");
}
_rateLimitTime = now.AddMilliseconds(milliseconds);
_logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds");
}
}
}
}*/
await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false);
continue;
}


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

@@ -2,7 +2,7 @@

namespace Discord.Net
{
#if NET45
#if NET46
[Serializable]
#endif
public sealed class TimeoutException : OperationCanceledException


+ 21
- 22
src/Discord.Net/Net/WebSockets/GatewaySocket.cs View File

@@ -1,5 +1,6 @@
using Discord.API.Client;
using Discord.API.Client.GatewaySocket;
using Discord.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -10,14 +11,13 @@ namespace Discord.Net.WebSockets
{
public partial class GatewaySocket : WebSocket
{
public int LastSequence => _lastSeq;
private int _lastSeq;

public string SessionId => _sessionId;
private int _lastSequence;
private string _sessionId;

public GatewaySocket(DiscordClient client, Logger logger)
: base(client, logger)
public string Token { get; private set; }

public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger)
: base(client, serializer, logger)
{
Disconnected += async (s, e) =>
{
@@ -26,10 +26,11 @@ namespace Discord.Net.WebSockets
};
}

public async Task Connect()
public async Task Connect(string token)
{
await BeginConnect().ConfigureAwait(false);
SendIdentify();
Token = token;
await BeginConnect().ConfigureAwait(false);
SendIdentify(token);
}
private async Task Redirect()
{
@@ -46,13 +47,13 @@ namespace Discord.Net.WebSockets
{
try
{
await Connect().ConfigureAwait(false);
await Connect(Token).ConfigureAwait(false);
break;
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
_logger.Log(LogSeverity.Error, $"Reconnect failed", ex);
Logger.Error("Reconnect failed", ex);
//Net is down? We can keep trying to reconnect until the user runs Disconnect()
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false);
}
@@ -60,13 +61,13 @@ namespace Discord.Net.WebSockets
}
catch (OperationCanceledException) { }
}
public Task Disconnect() => TaskManager.Stop();
public Task Disconnect() => _taskManager.Stop();

protected override async Task Run()
{
List<Task> tasks = new List<Task>();
tasks.AddRange(_engine.GetTasks(_cancelToken));
tasks.Add(HeartbeatAsync(_cancelToken));
tasks.AddRange(_engine.GetTasks(CancelToken));
tasks.Add(HeartbeatAsync(CancelToken));
await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false);
}

@@ -75,7 +76,7 @@ namespace Discord.Net.WebSockets
await base.ProcessMessage(json).ConfigureAwait(false);
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json);
if (msg.Sequence.HasValue)
_lastSeq = msg.Sequence.Value;
_lastSequence = msg.Sequence.Value;

var opCode = (OpCodes)msg.Operation;
switch (opCode)
@@ -105,20 +106,18 @@ namespace Discord.Net.WebSockets
if (payload.Url != null)
{
Host = payload.Url;
if (_logger.Level >= LogSeverity.Info)
_logger.Info("Redirected to " + payload.Url);
Logger.Info("Redirected to " + payload.Url);
await Redirect().ConfigureAwait(false);
}
}
break;
default:
if (_logger.Level >= LogSeverity.Warning)
_logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}");
Logger.Warning($"Unknown Opcode: {opCode}");
break;
}
}

public void SendIdentify()
public void SendIdentify(string token)
{
var props = new Dictionary<string, string>
{
@@ -127,7 +126,7 @@ namespace Discord.Net.WebSockets
var msg = new IdentifyCommand()
{
Version = 3,
Token = _client.Token,
Token = token,
Properties = props,
LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null,
UseCompression = true
@@ -136,7 +135,7 @@ namespace Discord.Net.WebSockets
}

public void SendResume()
=> QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSeq });
=> QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSequence });
public override void SendHeartbeat()
=> QueueMessage(new HeartbeatCommand());
public void SendUpdateStatus(long? idleSince, int? gameId)


+ 21
- 33
src/Discord.Net/Net/WebSockets/WS4NetEngine.cs View File

@@ -13,30 +13,22 @@ namespace Discord.Net.WebSockets
internal class WS4NetEngine : IWebSocketEngine
{
private readonly DiscordConfig _config;
private readonly Logger _logger;
private readonly ConcurrentQueue<string> _sendQueue;
private readonly WebSocket _parent;
private readonly TaskManager _taskManager;
private WS4NetWebSocket _webSocket;
private ManualResetEventSlim _waitUntilConnect;

public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage;
public event EventHandler<WebSocketTextMessageEventArgs> TextMessage;
private void RaiseBinaryMessage(byte[] data)
{
if (BinaryMessage != null)
BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data));
}
private void RaiseTextMessage(string msg)
{
if (TextMessage != null)
TextMessage(this, new WebSocketTextMessageEventArgs(msg));
}
public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage = delegate { };
public event EventHandler<WebSocketTextMessageEventArgs> TextMessage = delegate { };
private void OnBinaryMessage(byte[] data)
=> BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data));
private void OnTextMessage(string msg)
=> TextMessage(this, new WebSocketTextMessageEventArgs(msg));

internal WS4NetEngine(WebSocket parent, DiscordConfig config, Logger logger)
internal WS4NetEngine(DiscordConfig config, TaskManager taskManager)
{
_parent = parent;
_config = config;
_logger = logger;
_taskManager = taskManager;
_sendQueue = new ConcurrentQueue<string>();
_waitUntilConnect = new ManualResetEventSlim();
}
@@ -57,7 +49,7 @@ namespace Discord.Net.WebSockets
_waitUntilConnect.Reset();
_webSocket.Open();
_waitUntilConnect.Wait(cancelToken);
_parent.TaskManager.ThrowException(); //In case our connection failed
_taskManager.ThrowException(); //In case our connection failed
return TaskHelper.CompletedTask;
}

@@ -84,27 +76,25 @@ namespace Discord.Net.WebSockets

private void OnWebSocketError(object sender, ErrorEventArgs e)
{
_parent.TaskManager.SignalError(e.Exception);
_taskManager.SignalError(e.Exception);
_waitUntilConnect.Set();
}
private void OnWebSocketClosed(object sender, EventArgs e)
{
var ex = new Exception($"Connection lost or close message received.");
_parent.TaskManager.SignalError(ex, isUnexpected: true);
Exception ex;
if (e is ClosedEventArgs)
ex = new Exception($"Received close code {(e as ClosedEventArgs).Code}: {(e as ClosedEventArgs).Reason ?? "No reason"}");
else
ex = new Exception($"Connection lost");
_taskManager.SignalError(ex, isUnexpected: true);
_waitUntilConnect.Set();
}
private void OnWebSocketOpened(object sender, EventArgs e)
{
_waitUntilConnect.Set();
}
=> _waitUntilConnect.Set();
private void OnWebSocketText(object sender, MessageReceivedEventArgs e)
{
RaiseTextMessage(e.Message);
}
=> OnTextMessage(e.Message);
private void OnWebSocketBinary(object sender, DataReceivedEventArgs e)
{
RaiseBinaryMessage(e.Data);
}
=> OnBinaryMessage(e.Data);

public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) };

@@ -128,9 +118,7 @@ namespace Discord.Net.WebSockets
}

public void QueueMessage(string message)
{
_sendQueue.Enqueue(message);
}
=> _sendQueue.Enqueue(message);
}
}
#endif

+ 68
- 77
src/Discord.Net/Net/WebSockets/WebSocket.cs View File

@@ -1,4 +1,5 @@
using Discord.API.Client;
using Discord.Logging;
using Newtonsoft.Json;
using System;
using System.IO;
@@ -14,88 +15,59 @@ namespace Discord.Net.WebSockets
protected readonly IWebSocketEngine _engine;
protected readonly DiscordClient _client;
protected readonly ManualResetEventSlim _connectedEvent;

protected int _heartbeatInterval;
private DateTime _lastHeartbeat;

public CancellationToken? ParentCancelToken { get; set; }
public CancellationToken CancelToken => _cancelToken;
protected readonly TaskManager _taskManager;
protected readonly JsonSerializer _serializer;
protected CancellationTokenSource _cancelTokenSource;
protected CancellationToken _cancelToken;

public JsonSerializer Serializer => _serializer;
protected JsonSerializer _serializer;
protected int _heartbeatInterval;
private DateTime _lastHeartbeat;
/// <summary> Gets the logger used for this client. </summary>
internal Logger Logger { get; }

internal TaskManager TaskManager => _taskManager;
protected readonly TaskManager _taskManager;
public CancellationToken CancelToken { get; private set; }

public Logger Logger => _logger;
protected readonly Logger _logger;
public CancellationToken? ParentCancelToken { get; set; }

public string Host { get { return _host; } set { _host = value; } }
private string _host;
public string Host { get; set; }
/// <summary> Gets the current connection state of this client. </summary>
public ConnectionState State { get; private set; }

public ConnectionState State => _state;
protected ConnectionState _state;
public event EventHandler Connected = delegate { };
private void OnConnected()
=> Connected(this, EventArgs.Empty);
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { };
private void OnDisconnected(bool wasUnexpected, Exception error)
=> Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error));

public event EventHandler Connected;
private void RaiseConnected()
{
if (_logger.Level >= LogSeverity.Info)
_logger.Info( "Connected");
if (Connected != null)
Connected(this, EventArgs.Empty);
}
public event EventHandler<DisconnectedEventArgs> Disconnected;
private void RaiseDisconnected(bool wasUnexpected, Exception error)
{
if (_logger.Level >= LogSeverity.Info)
_logger.Info( "Disconnected");
if (Disconnected != null)
Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error));
}

public WebSocket(DiscordClient client, Logger logger)
public WebSocket(DiscordClient client, JsonSerializer serializer, Logger logger)
{
_client = client;
_logger = logger;
Logger = logger;
_serializer = serializer;

_lock = new Semaphore(1, 1);
_taskManager = new TaskManager(Cleanup);
_cancelToken = new CancellationToken(true);
CancelToken = new CancellationToken(true);
_connectedEvent = new ManualResetEventSlim(false);

#if !DOTNET5_4
_engine = new WS4NetEngine(this, client.Config, _logger);
_engine = new WS4NetEngine(client.Config, _taskManager);
#else
//_engine = new BuiltInWebSocketEngine(this, client.Config, _logger);
//_engine = new BuiltInWebSocketEngine(this, client.Config);
#endif
_engine.BinaryMessage += (s, e) =>
{
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
{
using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2))
using (var decompressed = new MemoryStream())
{
using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress))
zlib.CopyTo(decompressed);
decompressed.Position = 0;
using (var reader = new StreamReader(decompressed))
ProcessMessage(reader.ReadToEnd()).Wait();
}
ProcessMessage(reader.ReadToEnd()).Wait();
}
};
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait();

_serializer = new JsonSerializer();
_serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
#if TEST_RESPONSES
_serializer.CheckAdditionalContent = true;
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#else
_serializer.Error += (s, e) =>
{
e.ErrorContext.Handled = true;
_logger.Log(LogSeverity.Error, "Serialization Failed", e.ErrorContext.Error);
};
#endif
}

protected async Task BeginConnect()
@@ -107,13 +79,13 @@ namespace Discord.Net.WebSockets
{
await _taskManager.Stop().ConfigureAwait(false);
_taskManager.ClearException();
_state = ConnectionState.Connecting;
State = ConnectionState.Connecting;
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token;
CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token;
_lastHeartbeat = DateTime.UtcNow;

await _engine.Connect(Host, _cancelToken).ConfigureAwait(false);
await _engine.Connect(Host, CancelToken).ConfigureAwait(false);
await Run().ConfigureAwait(false);
}
finally
@@ -131,10 +103,11 @@ namespace Discord.Net.WebSockets
{
try
{
_state = ConnectionState.Connected;
State = ConnectionState.Connected;

_connectedEvent.Set();
RaiseConnected();
Logger.Info($"Connected");
OnConnected();
}
catch (Exception ex)
{
@@ -145,29 +118,32 @@ namespace Discord.Net.WebSockets
protected abstract Task Run();
protected virtual async Task Cleanup()
{
var oldState = _state;
_state = ConnectionState.Disconnecting;
var oldState = State;
State = ConnectionState.Disconnecting;

await _engine.Disconnect().ConfigureAwait(false);
_cancelTokenSource = null;
_connectedEvent.Reset();

if (oldState == ConnectionState.Connected)
RaiseDisconnected(_taskManager.WasUnexpected, _taskManager.Exception);
_state = ConnectionState.Disconnected;
{
Logger.Info("Disconnected");
OnDisconnected(_taskManager.WasUnexpected, _taskManager.Exception);
}
State = ConnectionState.Disconnected;
}

protected virtual Task ProcessMessage(string json)
{
if (_logger.Level >= LogSeverity.Debug)
_logger.Debug( $"In: {json}");
if (Logger.Level >= LogSeverity.Debug)
Logger.Debug( $"In: {json}");
return TaskHelper.CompletedTask;
}
protected void QueueMessage(IWebSocketMessage message)
{
string json = JsonConvert.SerializeObject(new WebSocketMessage(message));
if (_logger.Level >= LogSeverity.Debug)
_logger.Debug( $"Out: " + json);
if (Logger.Level >= LogSeverity.Debug)
Logger.Debug( $"Out: {json}");
_engine.QueueMessage(json);
}

@@ -179,9 +155,9 @@ namespace Discord.Net.WebSockets
{
while (!cancelToken.IsCancellationRequested)
{
if (_state == ConnectionState.Connected)
if (this.State == ConnectionState.Connected)
{
SendHeartbeat();
SendHeartbeat();
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false);
}
else
@@ -192,5 +168,20 @@ namespace Discord.Net.WebSockets
});
}
public abstract void SendHeartbeat();

public void WaitForConnection(CancellationToken cancelToken)
{
try
{
//Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token;
_connectedEvent.Wait(cancelToken);
}
catch (OperationCanceledException)
{
_taskManager.ThrowException(); //Throws data socket's internal error if any occured
throw;
}
}
}
}

+ 71
- 0
src/Discord.Net/Reference.cs View File

@@ -0,0 +1,71 @@
using System;

namespace Discord
{
/*internal class Reference<T>
where T : CachedObject<ulong>
{
private Action<T> _onCache, _onUncache;
private Func<ulong, T> _getItem;
private ulong? _id;
public ulong? Id
{
get { return _id; }
set
{
_id = value;
_value = null;
}
}

private T _value;
public T Value
{
get
{
}
}

public T Load()
{
var v = _value; //A little trickery to make this threadsafe
var id = _id;
if (v != null && !_value.IsCached)
{
v = null;
_value = null;
}
if (v == null && id != null)
{
v = _getItem(id.Value);
if (v != null && _onCache != null)
_onCache(v);
_value = v;
}
return v;
return Value != null; //Used for precaching
}

public void Unload()
{
if (_onUncache != null)
{
var v = _value;
if (v != null && _onUncache != null)
_onUncache(v);
}
}

public Reference(Func<ulong, T> onUpdate, Action<T> onCache = null, Action<T> onUncache = null)
: this(null, onUpdate, onCache, onUncache)
{ }
public Reference(ulong? id, Func<ulong, T> getItem, Action<T> onCache = null, Action<T> onUncache = null)
{
_id = id;
_getItem = getItem;
_onCache = onCache;
_onUncache = onUncache;
_value = null;
}
}*/
}

+ 0
- 7
src/Discord.Net/Services/IService.cs View File

@@ -1,7 +0,0 @@
namespace Discord
{
public interface IService
{
void Install(DiscordClient client);
}
}

+ 0
- 8
src/Discord.Net/Services/LogExtensions.cs View File

@@ -1,8 +0,0 @@
namespace Discord
{
public static class LogExtensions
{
public static LogService Log(this DiscordClient client, bool required = true)
=> client.GetService<LogService>(required);
}
}

+ 0
- 71
src/Discord.Net/Services/LogService.cs View File

@@ -1,71 +0,0 @@
using System;

namespace Discord
{
public class LogService : IService
{
public DiscordClient Client => _client;
private DiscordClient _client;

public LogSeverity Level => _level;
private LogSeverity _level;

public event EventHandler<LogMessageEventArgs> LogMessage;
internal void RaiseLogMessage(LogMessageEventArgs e)
{
if (LogMessage != null)
{
try
{
LogMessage(this, e);
}
catch { } //We dont want to log on log errors
}
}

void IService.Install(DiscordClient client)
{
_client = client;
_level = client.Config.LogLevel;
}

public Logger CreateLogger(string source)
{
return new Logger(this, source);
}
}

public class Logger
{
private LogService _service;

public LogSeverity Level => _level;
private LogSeverity _level;

public string Source => _source;
private string _source;

internal Logger(LogService service, string source)
{
_service = service;
_level = service.Level;
_source = source;
}

public void Log(LogSeverity severity, string message, Exception exception = null)
{
if (severity <= _service.Level)
_service.RaiseLogMessage(new LogMessageEventArgs(severity, _source, message, exception));
}
public void Error(string message, Exception exception = null)
=> Log(LogSeverity.Error, message, exception);
public void Warning(string message, Exception exception = null)
=> Log(LogSeverity.Warning, message, exception);
public void Info(string message, Exception exception = null)
=> Log(LogSeverity.Info, message, exception);
public void Verbose(string message, Exception exception = null)
=> Log(LogSeverity.Verbose, message, exception);
public void Debug(string message, Exception exception = null)
=> Log(LogSeverity.Debug, message, exception);
}
}

src/Discord.Net/Helpers/TaskManager.cs → src/Discord.Net/TaskManager.cs View File

@@ -146,10 +146,7 @@ namespace Discord
public void ThrowException()
{
lock (_lock)
{
if (_stopReason != null)
_stopReason.Throw();
}
_stopReason?.Throw();
}
public void ClearException()
{

+ 2
- 3
src/Discord.Net/project.json View File

@@ -29,12 +29,11 @@
},

"dependencies": {
"Newtonsoft.Json": "7.0.1",
"StyleCop.Analyzers": "1.0.0-rc2"
"Newtonsoft.Json": "7.0.1"
},

"frameworks": {
"net45": {
"net46": {
"dependencies": {
"WebSocket4Net": "0.14.1",
"RestSharp": "105.2.3"


Loading…
Cancel
Save