@@ -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; | |||
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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")] | |||
@@ -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))] | |||
@@ -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; } | |||
} | |||
@@ -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) | |||
{ | |||
@@ -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(); | |||
} | |||
} | |||
@@ -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 <#Id>. 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) { } | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 <#Id>. 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 <@Id>. 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 <@Id>. 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) { } | |||
} | |||
}*/ | |||
} |
@@ -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) { } | |||
} | |||
} | |||
} |
@@ -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 | |||
}); | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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 <@Id>. 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 <@Id>. 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; | |||
} | |||
} | |||
} |
@@ -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> | |||
@@ -0,0 +1,10 @@ | |||
namespace Discord | |||
{ | |||
public enum ConnectionState : byte | |||
{ | |||
Disconnected, | |||
Connecting, | |||
Connected, | |||
Disconnecting | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -0,0 +1,14 @@ | |||
using System; | |||
namespace Discord | |||
{ | |||
public class ProfileEventArgs : EventArgs | |||
{ | |||
public Profile Profile { get; } | |||
public ProfileEventArgs(Profile profile) | |||
{ | |||
Profile = profile; | |||
} | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
using System; | |||
namespace Discord | |||
{ | |||
public class ServerEventArgs : EventArgs | |||
{ | |||
public Server Server { get; } | |||
public ServerEventArgs(Server server) { Server = server; } | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
}*/ | |||
} |
@@ -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) | |||
@@ -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; | |||
} | |||
} | |||
@@ -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 | |||
{ | |||
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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 | |||
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
@@ -2,7 +2,7 @@ | |||
namespace Discord.Net | |||
{ | |||
#if NET45 | |||
#if NET46 | |||
[Serializable] | |||
#endif | |||
public sealed class TimeoutException : OperationCanceledException | |||
@@ -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) | |||
@@ -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 |
@@ -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; | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
}*/ | |||
} |
@@ -1,7 +0,0 @@ | |||
namespace Discord | |||
{ | |||
public interface IService | |||
{ | |||
void Install(DiscordClient client); | |||
} | |||
} |
@@ -1,8 +0,0 @@ | |||
namespace Discord | |||
{ | |||
public static class LogExtensions | |||
{ | |||
public static LogService Log(this DiscordClient client, bool required = true) | |||
=> client.GetService<LogService>(required); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -146,10 +146,7 @@ namespace Discord | |||
public void ThrowException() | |||
{ | |||
lock (_lock) | |||
{ | |||
if (_stopReason != null) | |||
_stopReason.Throw(); | |||
} | |||
_stopReason?.Throw(); | |||
} | |||
public void ClearException() | |||
{ |
@@ -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" | |||