@@ -34,7 +34,7 @@ namespace Discord.Modules | |||||
public event EventHandler<UserEventArgs> UserUpdated; | public event EventHandler<UserEventArgs> UserUpdated; | ||||
public event EventHandler<UserEventArgs> UserPresenceUpdated; | public event EventHandler<UserEventArgs> UserPresenceUpdated; | ||||
public event EventHandler<UserEventArgs> UserVoiceStateUpdated; | public event EventHandler<UserEventArgs> UserVoiceStateUpdated; | ||||
public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated; | |||||
public event EventHandler<ChannelEventArgs> UserIsTypingUpdated; | |||||
public event EventHandler<MessageEventArgs> MessageReceived; | public event EventHandler<MessageEventArgs> MessageReceived; | ||||
public event EventHandler<MessageEventArgs> MessageSent; | 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))] | [JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))] | ||||
public ulong? LastMessageId { get; set; } | public ulong? LastMessageId { get; set; } | ||||
[JsonProperty("is_private")] | [JsonProperty("is_private")] | ||||
public bool IsPrivate { get; set; } | |||||
public bool? IsPrivate { get; set; } | |||||
[JsonProperty("position")] | [JsonProperty("position")] | ||||
public int Position { get; set; } | |||||
public int? Position { get; set; } | |||||
[JsonProperty("topic")] | [JsonProperty("topic")] | ||||
public string Topic { get; set; } | public string Topic { get; set; } | ||||
[JsonProperty("permission_overwrites")] | [JsonProperty("permission_overwrites")] | ||||
@@ -6,7 +6,7 @@ namespace Discord.API.Client | |||||
public class MemberPresence : MemberReference | public class MemberPresence : MemberReference | ||||
{ | { | ||||
[JsonProperty("game_id")] | [JsonProperty("game_id")] | ||||
public int? GameId { get; set; } | |||||
public string GameId { get; set; } | |||||
[JsonProperty("status")] | [JsonProperty("status")] | ||||
public string Status { get; set; } | public string Status { get; set; } | ||||
[JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | ||||
@@ -6,7 +6,7 @@ namespace Discord.API.Client | |||||
public class MemberReference | public class MemberReference | ||||
{ | { | ||||
[JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | ||||
public ulong GuildId { get; set; } | |||||
public ulong? GuildId { get; set; } | |||||
[JsonProperty("user")] | [JsonProperty("user")] | ||||
public UserReference User { get; set; } | public UserReference User { get; set; } | ||||
} | } | ||||
@@ -14,7 +14,7 @@ namespace Discord.API.Client.Rest | |||||
StringBuilder query = new StringBuilder(); | StringBuilder query = new StringBuilder(); | ||||
this.AddQueryParam(query, "limit", Limit.ToString()); | this.AddQueryParam(query, "limit", Limit.ToString()); | ||||
if (RelativeDir != null) | if (RelativeDir != null) | ||||
this.AddQueryParam(query, RelativeDir, RelativeId.Value.ToString()); | |||||
this.AddQueryParam(query, RelativeDir, RelativeId.ToString()); | |||||
return $"channels/{ChannelId}/messages{query}"; | return $"channels/{ChannelId}/messages{query}"; | ||||
} | } | ||||
} | } | ||||
@@ -25,7 +25,7 @@ namespace Discord.API.Client.Rest | |||||
public int Limit { get; set; } = 100; | public int Limit { get; set; } = 100; | ||||
public string RelativeDir { get; set; } = null; | public string RelativeDir { get; set; } = null; | ||||
public ulong? RelativeId { get; set; } = 0; | |||||
public ulong RelativeId { get; set; } = 0; | |||||
public GetMessagesRequest(ulong channelId) | public GetMessagesRequest(ulong channelId) | ||||
{ | { | ||||
@@ -9,9 +9,9 @@ namespace Discord.API.Converters | |||||
public override bool CanConvert(Type objectType) | public override bool CanConvert(Type objectType) | ||||
=> objectType == typeof(ulong); | => objectType == typeof(ulong); | ||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | 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) | 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 | public class NullableLongStringConverter : JsonConverter | ||||
@@ -19,9 +19,9 @@ namespace Discord.API.Converters | |||||
public override bool CanConvert(Type objectType) | public override bool CanConvert(Type objectType) | ||||
=> objectType == typeof(ulong?); | => objectType == typeof(ulong?); | ||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | 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) | 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 | /*public class LongStringEnumerableConverter : JsonConverter | ||||
@@ -66,7 +66,7 @@ namespace Discord.API.Converters | |||||
reader.Read(); | reader.Read(); | ||||
while (reader.TokenType != JsonToken.EndArray) | while (reader.TokenType != JsonToken.EndArray) | ||||
{ | { | ||||
result.Add(IdConvert.ToLong((string)reader.Value)); | |||||
result.Add(((string)reader.Value).ToId()); | |||||
reader.Read(); | reader.Read(); | ||||
} | } | ||||
} | } | ||||
@@ -81,7 +81,7 @@ namespace Discord.API.Converters | |||||
writer.WriteStartArray(); | writer.WriteStartArray(); | ||||
var a = (ulong[])value; | var a = (ulong[])value; | ||||
for (int i = 0; i < a.Length; i++) | for (int i = 0; i < a.Length; i++) | ||||
writer.WriteValue(IdConvert.ToString(a[i])); | |||||
writer.WriteValue(a[i].ToIdString()); | |||||
writer.WriteEndArray(); | 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 | Debug = 5 | ||||
} | } | ||||
public abstract class BaseConfig<T> | |||||
where T : BaseConfig<T> | |||||
public abstract class Config<T> | |||||
where T : Config<T> | |||||
{ | { | ||||
protected bool _isLocked; | protected bool _isLocked; | ||||
protected internal void Lock() { _isLocked = true; } | 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 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 | //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(); } } | public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } | ||||
private string _appName = null; | private string _appName = null; | ||||
/// <summary> Version of your application. </summary> | /// <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() | static Format() | ||||
{ | { | ||||
_patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; | _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; | ||||
_builder = new StringBuilder(DiscordClient.MaxMessageSize); | |||||
_builder = new StringBuilder(DiscordConfig.MaxMessageSize); | |||||
} | } | ||||
/// <summary> Removes all special formatting characters from the provided text. </summary> | /// <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) | lock (_builder) | ||||
{ | { | ||||
@@ -84,10 +84,7 @@ namespace Discord | |||||
} | } | ||||
return -1; | 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> | /// <summary> Returns a markdown-formatted string with bold formatting, optionally escaping the contents. </summary> | ||||
public static string Bold(string text, bool escape = true) | public static string Bold(string text, bool escape = true) | ||||
=> escape ? $"**{Escape(text)}**" : $"**{text}**"; | => escape ? $"**{Escape(text)}**" : $"**{text}**"; | ||||
@@ -109,20 +106,5 @@ namespace Discord | |||||
else | else | ||||
return $"`{text}`"; | 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")] | [Obsolete("Use User.Mention instead")] | ||||
public static string User(User user) | public static string User(User user) | ||||
=> $"<@{user.Id}>"; | => $"<@{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> | /// <summary> Returns the string used to create a channel mention. </summary> | ||||
[Obsolete("Use Channel.Mention instead")] | [Obsolete("Use Channel.Mention instead")] | ||||
public static string Channel(Channel channel) | public static string Channel(Channel channel) | ||||
@@ -27,12 +23,12 @@ namespace Discord | |||||
public static string Everyone() | public static string Everyone() | ||||
=> $"@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 => | 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 (user != null) | ||||
{ | { | ||||
if (users != null) | if (users != null) | ||||
@@ -43,54 +39,57 @@ namespace Discord | |||||
return '@' + e.Value; | 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 => | 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) | if (channels != null) | ||||
channels.Add(channel); | |||||
return '#' + channel.Name; | |||||
channels.Add(mentionedChannel); | |||||
return '#' + mentionedChannel.Name; | |||||
} | } | ||||
else //Channel not found | else //Channel not found | ||||
return '#' + e.Value; | 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 => | return _roleRegex.Replace(text, new MatchEvaluator(e => | ||||
{ | { | ||||
if (roles != null && user.GetPermissions(channel).MentionEveryone) | if (roles != null && user.GetPermissions(channel).MentionEveryone) | ||||
roles.Add(channel.Server.EveryoneRole); | |||||
roles.Add(server.EveryoneRole); | |||||
return e.Value; | 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 (source == null) throw new ArgumentNullException(nameof(source)); | ||||
if (text == null) throw new ArgumentNullException(nameof(text)); | 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> | /// <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)); | 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 Discord.API.Client; | ||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -7,315 +7,292 @@ using APIChannel = Discord.API.Client.Channel; | |||||
namespace Discord | 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) | 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) | 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) | 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 | else | ||||
{ | { | ||||
if (Type == ChannelType.Text) | if (Type == ChannelType.Text) | ||||
{ | { | ||||
ChannelPermissions perms = new ChannelPermissions(); | ChannelPermissions perms = new ChannelPermissions(); | ||||
return Server.Members.Where(x => | |||||
return Server.Users.Where(x => | |||||
{ | { | ||||
UpdatePermissions(x, perms); | UpdatePermissions(x, perms); | ||||
return perms.ReadMessages == true; | return perms.ReadMessages == true; | ||||
}); | }); | ||||
} | } | ||||
else if (Type == ChannelType.Voice) | 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; | 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; | 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; | return; | ||||
foreach (var pair in _members) | |||||
foreach (var pair in _users) | |||||
{ | { | ||||
ChannelMember member = pair.Value; | |||||
Member member = pair.Value; | |||||
UpdatePermissions(member.User, member.Permissions); | UpdatePermissions(member.User, member.Permissions); | ||||
} | } | ||||
} | |||||
} | |||||
internal void UpdatePermissions(User user) | internal void UpdatePermissions(User user) | ||||
{ | { | ||||
if (!_client.Config.UsePermissionsCache) | |||||
if (!Client.Config.UsePermissionsCache) | |||||
return; | return; | ||||
ChannelMember member; | |||||
if (_members.TryGetValue(user.Id, out member)) | |||||
Member member; | |||||
if (_users.TryGetValue(user.Id, out member)) | |||||
UpdatePermissions(member.User, member.Permissions); | UpdatePermissions(member.User, member.Permissions); | ||||
} | } | ||||
internal void UpdatePermissions(User user, ChannelPermissions 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) | if (newPermissions != permissions.RawValue) | ||||
permissions.SetRawValueInternal(newPermissions); | 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 | 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> | /// <summary> Returns the email for this user. Note: this field is only ever populated for the current logged in user. </summary> | ||||
[JsonIgnore] | [JsonIgnore] | ||||
@@ -75,5 +75,5 @@ namespace Discord | |||||
public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id; | public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id; | ||||
public override int GetHashCode() => unchecked(Id.GetHashCode() + 7891); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 7891); | ||||
public override string ToString() => IdConvert.ToString(Id); | public override string ToString() => IdConvert.ToString(Id); | ||||
} | |||||
}*/ | |||||
} | } |
@@ -42,6 +42,7 @@ namespace Discord | |||||
public ushort Discriminator { get; } | public ushort Discriminator { get; } | ||||
/// <summary> Returns the unique identifier for this user's avatar. </summary> | /// <summary> Returns the unique identifier for this user's avatar. </summary> | ||||
public string AvatarId { get; } | public string AvatarId { get; } | ||||
/// <summary> Returns the full path to this user's avatar. </summary> | /// <summary> Returns the full path to this user's avatar. </summary> | ||||
public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId); | 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; } | 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; } | 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; } | 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; } | 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> | /// <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; } | public bool IsTemporary { get; private set; } | ||||
/// <summary> Gets when this invite was created. </summary> | |||||
public DateTime CreatedAt { get; private set; } | public DateTime CreatedAt { get; private set; } | ||||
/// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | /// <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) | if (model.IsTemporary != null) | ||||
IsTemporary = model.IsTemporary.Value; | IsTemporary = model.IsTemporary.Value; | ||||
if (model.MaxAge != null) | if (model.MaxAge != null) | ||||
MaxAge = model.MaxAge.Value; | |||||
MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null; | |||||
if (model.MaxUses != null) | if (model.MaxUses != null) | ||||
MaxUses = model.MaxUses.Value; | MaxUses = model.MaxUses.Value; | ||||
if (model.Uses != null) | if (model.Uses != null) | ||||
@@ -15,9 +15,9 @@ namespace Discord | |||||
Failed | 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) | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | ||||
{ | { | ||||
@@ -31,7 +31,7 @@ namespace Discord | |||||
} | } | ||||
return property; | return property; | ||||
} | } | ||||
} | |||||
}*/ | |||||
public sealed class Attachment : File | public sealed class Attachment : File | ||||
{ | { | ||||
@@ -89,17 +89,24 @@ namespace Discord | |||||
internal File() { } | 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> | /// <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; } | public bool IsTTS { get; private set; } | ||||
/// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary> | /// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary> | ||||
public MessageState State { get; internal set; } | public MessageState State { get; internal set; } | ||||
/// <summary> Returns the raw content of this message as it was received from the server. </summary> | /// <summary> Returns the raw content of this message as it was received from the server. </summary> | ||||
public string RawText { get; private set; } | public string RawText { get; private set; } | ||||
[JsonIgnore] | |||||
/// <summary> Returns the content of this message with any special references such as mentions converted. </summary> | /// <summary> Returns the content of this message with any special references such as mentions converted. </summary> | ||||
public string Text { get; internal set; } | public string Text { get; internal set; } | ||||
/// <summary> Returns the timestamp for when this message was sent. </summary> | /// <summary> Returns the timestamp for when this message was sent. </summary> | ||||
@@ -108,89 +115,26 @@ namespace Discord | |||||
public DateTime? EditedTimestamp { get; private set; } | public DateTime? EditedTimestamp { get; private set; } | ||||
/// <summary> Returns the attachments included in this message. </summary> | /// <summary> Returns the attachments included in this message. </summary> | ||||
public Attachment[] Attachments { get; private set; } | 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> | /// <summary> Returns a collection of all embeded content in this message. </summary> | ||||
public Embed[] Embeds { get; private set; } | 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> | /// <summary> Returns a collection of all users mentioned in this message. </summary> | ||||
[JsonIgnore] | |||||
public IEnumerable<User> MentionedUsers { get; internal set; } | 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> | /// <summary> Returns a collection of all channels mentioned in this message. </summary> | ||||
[JsonIgnore] | |||||
public IEnumerable<Channel> MentionedChannels { get; internal set; } | 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> | /// <summary> Returns a collection of all roles mentioned in this message. </summary> | ||||
[JsonIgnore] | |||||
public IEnumerable<Role> MentionedRoles { get; internal set; } | 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> | /// <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; | Attachments = _initialAttachments; | ||||
Embeds = _initialEmbeds; | Embeds = _initialEmbeds; | ||||
} | } | ||||
internal override bool LoadReferences() | |||||
{ | |||||
return _channel.Load() && _user.Load(); | |||||
} | |||||
internal override void UnloadReferences() | |||||
{ | |||||
_channel.Unload(); | |||||
_user.Unload(); | |||||
} | |||||
internal void Update(APIMessage model) | internal void Update(APIMessage model) | ||||
{ | { | ||||
@@ -247,7 +191,7 @@ namespace Discord | |||||
if (model.Mentions != null) | if (model.Mentions != null) | ||||
{ | { | ||||
MentionedUsers = model.Mentions | MentionedUsers = model.Mentions | ||||
.Select(x => _client.Users[x.Id, Channel.Server?.Id]) | |||||
.Select(x => Channel.GetUser(x.Id)) | |||||
.Where(x => x != null) | .Where(x => x != null) | ||||
.ToArray(); | .ToArray(); | ||||
} | } | ||||
@@ -266,10 +210,10 @@ namespace Discord | |||||
//var mentionedUsers = new List<User>(); | //var mentionedUsers = new List<User>(); | ||||
var mentionedChannels = new List<Channel>(); | var mentionedChannels = new List<Channel>(); | ||||
//var mentionedRoles = new List<Role>(); | //var mentionedRoles = new List<Role>(); | ||||
text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/); | |||||
text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/); | |||||
if (server != null) | 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 = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); | ||||
} | } | ||||
Text = text; | Text = text; | ||||
@@ -287,7 +231,7 @@ namespace Discord | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
var me = _client.PrivateUser; | |||||
var me = Channel.Client.PrivateUser; | |||||
IsMentioningMe = MentionedUsers?.Contains(me) ?? false; | IsMentioningMe = MentionedUsers?.Contains(me) ?? false; | ||||
} | } | ||||
} | } | ||||
@@ -127,9 +127,15 @@ namespace Discord | |||||
_rawValue = rawValue; | _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; | internal void Lock() => _isLocked = true; | ||||
protected void CheckLock() | 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 bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue; | ||||
public override int GetHashCode() => unchecked(_rawValue.GetHashCode() + 393); | public override int GetHashCode() => unchecked(_rawValue.GetHashCode() + 393); | ||||
} | |||||
} | |||||
public sealed class DualChannelPermissions | 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.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using APIRole = Discord.API.Client.Role; | using APIRole = Discord.API.Client.Role; | ||||
namespace Discord | 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 = new ServerPermissions(0); | ||||
Permissions.Lock(); | Permissions.Lock(); | ||||
Color = new Color(0); | Color = new Color(0); | ||||
Color.Lock(); | Color.Lock(); | ||||
} | } | ||||
internal override bool LoadReferences() | |||||
{ | |||||
return _server.Load(); | |||||
} | |||||
internal override void UnloadReferences() | |||||
{ | |||||
_server.Unload(); | |||||
} | |||||
internal void Update(APIRole model) | 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 bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; | ||||
public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); | 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 Discord.API.Client; | ||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using APIGuild = Discord.API.Client.Guild; | |||||
namespace Discord | 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) | 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) | 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) | 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; | return member.Permissions; | ||||
else | else | ||||
return null; | return null; | ||||
} | } | ||||
internal void UpdatePermissions(User user) | 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); | UpdatePermissions(member.User, member.Permissions); | ||||
} | } | ||||
private void UpdatePermissions(User user, ServerPermissions permissions) | private void UpdatePermissions(User user, ServerPermissions permissions) | ||||
{ | { | ||||
uint newPermissions = 0; | uint newPermissions = 0; | ||||
if (user.IsOwner) | |||||
if (user.Id == _ownerId) | |||||
newPermissions = ServerPermissions.All.RawValue; | newPermissions = ServerPermissions.All.RawValue; | ||||
else | else | ||||
{ | { | ||||
@@ -285,7 +231,7 @@ namespace Discord | |||||
newPermissions |= serverRole.Permissions.RawValue; | newPermissions |= serverRole.Permissions.RawValue; | ||||
} | } | ||||
if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions)) | |||||
if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions)) | |||||
newPermissions = ServerPermissions.All.RawValue; | newPermissions = ServerPermissions.All.RawValue; | ||||
if (newPermissions != permissions.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 bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; | ||||
public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); | 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 Discord.API.Client; | ||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -7,8 +6,19 @@ using APIMember = Discord.API.Client.Member; | |||||
namespace Discord | 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> | internal struct CompositeKey : IEquatable<CompositeKey> | ||||
{ | { | ||||
public ulong ServerId, UserId; | public ulong ServerId, UserId; | ||||
@@ -24,92 +34,71 @@ namespace Discord | |||||
=> unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); | => 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> | /// <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 | 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)); | (x.Type == ChannelType.Voice && x.GetPermissions(this).Connect)); | ||||
} | } | ||||
else | else | ||||
@@ -120,63 +109,37 @@ namespace Discord | |||||
{ | { | ||||
x.UpdatePermissions(this, perms); | x.UpdatePermissions(this, perms); | ||||
return (x.Type == ChannelType.Text && perms.ReadMessages) || | return (x.Type == ChannelType.Text && perms.ReadMessages) || | ||||
(x.Type == ChannelType.Voice && perms.Connect); | |||||
(x.Type == ChannelType.Voice && perms.Connect); | |||||
}); | }); | ||||
} | } | ||||
} | } | ||||
else | 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>(); | _roles = new Dictionary<ulong, Role>(); | ||||
Status = UserStatus.Offline; | Status = UserStatus.Offline; | ||||
if (serverId == null) | |||||
if (server == null) | |||||
UpdateRoles(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) | internal void Update(UserReference model) | ||||
{ | { | ||||
@@ -195,24 +158,29 @@ namespace Discord | |||||
if (model.JoinedAt.HasValue) | if (model.JoinedAt.HasValue) | ||||
JoinedAt = model.JoinedAt.Value; | JoinedAt = model.JoinedAt.Value; | ||||
if (model.Roles != null) | 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) | internal void Update(ExtendedGuild.ExtendedMemberInfo model) | ||||
{ | { | ||||
Update(model as APIMember); | 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) | internal void Update(MemberPresence model) | ||||
{ | { | ||||
if (model.User != null) | if (model.User != null) | ||||
Update(model.User as UserReference); | 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) | if (model.Status != null && Status != model.Status) | ||||
{ | { | ||||
Status = UserStatus.FromString(model.Status); | Status = UserStatus.FromString(model.Status); | ||||
@@ -223,42 +191,55 @@ namespace Discord | |||||
GameId = model.GameId; //Allows null | GameId = model.GameId; //Allows null | ||||
} | } | ||||
internal void Update(MemberVoiceState model) | 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; | SessionId = model.SessionId; | ||||
if (model.Token != null) | 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) | private void UpdateRoles(IEnumerable<Role> roles) | ||||
{ | { | ||||
var newRoles = new Dictionary<ulong, Role>(); | var newRoles = new Dictionary<ulong, Role>(); | ||||
if (roles != null) | 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; | var everyone = Server.EveryoneRole; | ||||
newRoles.Add(everyone.Id, everyone); | |||||
newRoles[everyone.Id] = everyone; | |||||
} | } | ||||
_roles = newRoles; | _roles = newRoles; | ||||
if (!IsPrivate) | |||||
if (Server != null) | |||||
Server.UpdatePermissions(this); | 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 bool Equals(object obj) => obj is User && (obj as User).Id == Id; | ||||
public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); | 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 | namespace Discord.Net | ||||
{ | { | ||||
#if NET45 | |||||
#if NET46 | |||||
[Serializable] | [Serializable] | ||||
#endif | #endif | ||||
public class HttpException : Exception | public class HttpException : Exception | ||||
@@ -16,7 +16,7 @@ namespace Discord.Net | |||||
{ | { | ||||
StatusCode = statusCode; | StatusCode = statusCode; | ||||
} | } | ||||
#if NET45 | |||||
#if NET46 | |||||
public override void GetObjectData(SerializationInfo info, StreamingContext context) | public override void GetObjectData(SerializationInfo info, StreamingContext context) | ||||
=> base.GetObjectData(info, context); | => base.GetObjectData(info, context); | ||||
#endif | #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.API; | ||||
using Discord.Logging; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System; | using System; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
@@ -7,25 +8,53 @@ using System.Threading.Tasks; | |||||
namespace Discord.Net.Rest | 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 DiscordConfig _config; | ||||
private readonly IRestEngine _engine; | 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; | _config = config; | ||||
Logger = logger; | |||||
#if !DOTNET5_4 | #if !DOTNET5_4 | ||||
_engine = new RestSharpEngine(config, logger, baseUrl); | |||||
_engine = new RestSharpEngine(config, baseUrl); | |||||
#else | #else | ||||
//_engine = new BuiltInRestEngine(config, logger, baseUrl); | |||||
//_engine = new BuiltInRestEngine(config, baseUrl); | |||||
#endif | #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) | public async Task<ResponseT> Send<ResponseT>(IRestRequest<ResponseT> request) | ||||
where ResponseT : class | where ResponseT : class | ||||
{ | { | ||||
@@ -69,24 +98,26 @@ namespace Discord.Net.Rest | |||||
requestJson = JsonConvert.SerializeObject(payload); | requestJson = JsonConvert.SerializeObject(payload); | ||||
Stopwatch stopwatch = null; | Stopwatch stopwatch = null; | ||||
if (_config.LogLevel >= LogSeverity.Verbose) | |||||
if (Logger.Level >= LogSeverity.Verbose) | |||||
stopwatch = Stopwatch.StartNew(); | 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(); | 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) | 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; | return responseJson; | ||||
} | } | ||||
@@ -100,19 +131,21 @@ namespace Discord.Net.Rest | |||||
var isPrivate = request.IsPrivate; | var isPrivate = request.IsPrivate; | ||||
Stopwatch stopwatch = null; | Stopwatch stopwatch = null; | ||||
if (_config.LogLevel >= LogSeverity.Verbose) | |||||
if (Logger.Level >= LogSeverity.Verbose) | |||||
stopwatch = Stopwatch.StartNew(); | 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(); | 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; | return responseJson; | ||||
} | } | ||||
@@ -12,15 +12,13 @@ namespace Discord.Net.Rest | |||||
{ | { | ||||
private readonly DiscordConfig _config; | private readonly DiscordConfig _config; | ||||
private readonly RestSharp.RestClient _client; | private readonly RestSharp.RestClient _client; | ||||
private readonly Logger _logger; | |||||
private readonly object _rateLimitLock; | private readonly object _rateLimitLock; | ||||
private DateTime _rateLimitTime; | private DateTime _rateLimitTime; | ||||
public RestSharpEngine(DiscordConfig config, Logger logger, string baseUrl) | |||||
public RestSharpEngine(DiscordConfig config, string baseUrl) | |||||
{ | { | ||||
_config = config; | _config = config; | ||||
_logger = logger; | |||||
_rateLimitLock = new object(); | _rateLimitLock = new object(); | ||||
_client = new RestSharp.RestClient(baseUrl) | _client = new RestSharp.RestClient(baseUrl) | ||||
{ | { | ||||
@@ -28,9 +26,6 @@ namespace Discord.Net.Rest | |||||
ReadWriteTimeout = _config.RestTimeout, | ReadWriteTimeout = _config.RestTimeout, | ||||
UserAgent = config.UserAgent | UserAgent = config.UserAgent | ||||
}; | }; | ||||
/*if (_config.ProxyUrl != null) | |||||
_client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials); | |||||
else*/ | |||||
_client.Proxy = null; | _client.Proxy = null; | ||||
_client.RemoveDefaultParameter("Accept"); | _client.RemoveDefaultParameter("Accept"); | ||||
_client.AddDefaultHeader("accept", "*/*"); | _client.AddDefaultHeader("accept", "*/*"); | ||||
@@ -83,21 +78,18 @@ namespace Discord.Net.Rest | |||||
int milliseconds; | int milliseconds; | ||||
if (retryAfter != null && int.TryParse((string)retryAfter.Value, out 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); | await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); | ||||
continue; | continue; | ||||
} | } | ||||
@@ -2,7 +2,7 @@ | |||||
namespace Discord.Net | namespace Discord.Net | ||||
{ | { | ||||
#if NET45 | |||||
#if NET46 | |||||
[Serializable] | [Serializable] | ||||
#endif | #endif | ||||
public sealed class TimeoutException : OperationCanceledException | public sealed class TimeoutException : OperationCanceledException | ||||
@@ -1,5 +1,6 @@ | |||||
using Discord.API.Client; | using Discord.API.Client; | ||||
using Discord.API.Client.GatewaySocket; | using Discord.API.Client.GatewaySocket; | ||||
using Discord.Logging; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using System; | using System; | ||||
@@ -10,14 +11,13 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
public partial class GatewaySocket : WebSocket | public partial class GatewaySocket : WebSocket | ||||
{ | { | ||||
public int LastSequence => _lastSeq; | |||||
private int _lastSeq; | |||||
public string SessionId => _sessionId; | |||||
private int _lastSequence; | |||||
private string _sessionId; | 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) => | 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() | private async Task Redirect() | ||||
{ | { | ||||
@@ -46,13 +47,13 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
await Connect().ConfigureAwait(false); | |||||
await Connect(Token).ConfigureAwait(false); | |||||
break; | break; | ||||
} | } | ||||
catch (OperationCanceledException) { throw; } | catch (OperationCanceledException) { throw; } | ||||
catch (Exception ex) | 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() | //Net is down? We can keep trying to reconnect until the user runs Disconnect() | ||||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | ||||
} | } | ||||
@@ -60,13 +61,13 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
} | } | ||||
public Task Disconnect() => TaskManager.Stop(); | |||||
public Task Disconnect() => _taskManager.Stop(); | |||||
protected override async Task Run() | protected override async Task Run() | ||||
{ | { | ||||
List<Task> tasks = new List<Task>(); | 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); | await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | ||||
} | } | ||||
@@ -75,7 +76,7 @@ namespace Discord.Net.WebSockets | |||||
await base.ProcessMessage(json).ConfigureAwait(false); | await base.ProcessMessage(json).ConfigureAwait(false); | ||||
var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | ||||
if (msg.Sequence.HasValue) | if (msg.Sequence.HasValue) | ||||
_lastSeq = msg.Sequence.Value; | |||||
_lastSequence = msg.Sequence.Value; | |||||
var opCode = (OpCodes)msg.Operation; | var opCode = (OpCodes)msg.Operation; | ||||
switch (opCode) | switch (opCode) | ||||
@@ -105,20 +106,18 @@ namespace Discord.Net.WebSockets | |||||
if (payload.Url != null) | if (payload.Url != null) | ||||
{ | { | ||||
Host = payload.Url; | Host = payload.Url; | ||||
if (_logger.Level >= LogSeverity.Info) | |||||
_logger.Info("Redirected to " + payload.Url); | |||||
Logger.Info("Redirected to " + payload.Url); | |||||
await Redirect().ConfigureAwait(false); | await Redirect().ConfigureAwait(false); | ||||
} | } | ||||
} | } | ||||
break; | break; | ||||
default: | default: | ||||
if (_logger.Level >= LogSeverity.Warning) | |||||
_logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}"); | |||||
Logger.Warning($"Unknown Opcode: {opCode}"); | |||||
break; | break; | ||||
} | } | ||||
} | } | ||||
public void SendIdentify() | |||||
public void SendIdentify(string token) | |||||
{ | { | ||||
var props = new Dictionary<string, string> | var props = new Dictionary<string, string> | ||||
{ | { | ||||
@@ -127,7 +126,7 @@ namespace Discord.Net.WebSockets | |||||
var msg = new IdentifyCommand() | var msg = new IdentifyCommand() | ||||
{ | { | ||||
Version = 3, | Version = 3, | ||||
Token = _client.Token, | |||||
Token = token, | |||||
Properties = props, | Properties = props, | ||||
LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null, | LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null, | ||||
UseCompression = true | UseCompression = true | ||||
@@ -136,7 +135,7 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
public void SendResume() | public void SendResume() | ||||
=> QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSeq }); | |||||
=> QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSequence }); | |||||
public override void SendHeartbeat() | public override void SendHeartbeat() | ||||
=> QueueMessage(new HeartbeatCommand()); | => QueueMessage(new HeartbeatCommand()); | ||||
public void SendUpdateStatus(long? idleSince, int? gameId) | public void SendUpdateStatus(long? idleSince, int? gameId) | ||||
@@ -13,30 +13,22 @@ namespace Discord.Net.WebSockets | |||||
internal class WS4NetEngine : IWebSocketEngine | internal class WS4NetEngine : IWebSocketEngine | ||||
{ | { | ||||
private readonly DiscordConfig _config; | private readonly DiscordConfig _config; | ||||
private readonly Logger _logger; | |||||
private readonly ConcurrentQueue<string> _sendQueue; | private readonly ConcurrentQueue<string> _sendQueue; | ||||
private readonly WebSocket _parent; | |||||
private readonly TaskManager _taskManager; | |||||
private WS4NetWebSocket _webSocket; | private WS4NetWebSocket _webSocket; | ||||
private ManualResetEventSlim _waitUntilConnect; | 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; | _config = config; | ||||
_logger = logger; | |||||
_taskManager = taskManager; | |||||
_sendQueue = new ConcurrentQueue<string>(); | _sendQueue = new ConcurrentQueue<string>(); | ||||
_waitUntilConnect = new ManualResetEventSlim(); | _waitUntilConnect = new ManualResetEventSlim(); | ||||
} | } | ||||
@@ -57,7 +49,7 @@ namespace Discord.Net.WebSockets | |||||
_waitUntilConnect.Reset(); | _waitUntilConnect.Reset(); | ||||
_webSocket.Open(); | _webSocket.Open(); | ||||
_waitUntilConnect.Wait(cancelToken); | _waitUntilConnect.Wait(cancelToken); | ||||
_parent.TaskManager.ThrowException(); //In case our connection failed | |||||
_taskManager.ThrowException(); //In case our connection failed | |||||
return TaskHelper.CompletedTask; | return TaskHelper.CompletedTask; | ||||
} | } | ||||
@@ -84,27 +76,25 @@ namespace Discord.Net.WebSockets | |||||
private void OnWebSocketError(object sender, ErrorEventArgs e) | private void OnWebSocketError(object sender, ErrorEventArgs e) | ||||
{ | { | ||||
_parent.TaskManager.SignalError(e.Exception); | |||||
_taskManager.SignalError(e.Exception); | |||||
_waitUntilConnect.Set(); | _waitUntilConnect.Set(); | ||||
} | } | ||||
private void OnWebSocketClosed(object sender, EventArgs e) | 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(); | _waitUntilConnect.Set(); | ||||
} | } | ||||
private void OnWebSocketOpened(object sender, EventArgs e) | private void OnWebSocketOpened(object sender, EventArgs e) | ||||
{ | |||||
_waitUntilConnect.Set(); | |||||
} | |||||
=> _waitUntilConnect.Set(); | |||||
private void OnWebSocketText(object sender, MessageReceivedEventArgs e) | private void OnWebSocketText(object sender, MessageReceivedEventArgs e) | ||||
{ | |||||
RaiseTextMessage(e.Message); | |||||
} | |||||
=> OnTextMessage(e.Message); | |||||
private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) | private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) | ||||
{ | |||||
RaiseBinaryMessage(e.Data); | |||||
} | |||||
=> OnBinaryMessage(e.Data); | |||||
public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; | public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; | ||||
@@ -128,9 +118,7 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
public void QueueMessage(string message) | public void QueueMessage(string message) | ||||
{ | |||||
_sendQueue.Enqueue(message); | |||||
} | |||||
=> _sendQueue.Enqueue(message); | |||||
} | } | ||||
} | } | ||||
#endif | #endif |
@@ -1,4 +1,5 @@ | |||||
using Discord.API.Client; | using Discord.API.Client; | ||||
using Discord.Logging; | |||||
using Newtonsoft.Json; | using Newtonsoft.Json; | ||||
using System; | using System; | ||||
using System.IO; | using System.IO; | ||||
@@ -14,88 +15,59 @@ namespace Discord.Net.WebSockets | |||||
protected readonly IWebSocketEngine _engine; | protected readonly IWebSocketEngine _engine; | ||||
protected readonly DiscordClient _client; | protected readonly DiscordClient _client; | ||||
protected readonly ManualResetEventSlim _connectedEvent; | 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 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; | _client = client; | ||||
_logger = logger; | |||||
Logger = logger; | |||||
_serializer = serializer; | |||||
_lock = new Semaphore(1, 1); | _lock = new Semaphore(1, 1); | ||||
_taskManager = new TaskManager(Cleanup); | _taskManager = new TaskManager(Cleanup); | ||||
_cancelToken = new CancellationToken(true); | |||||
CancelToken = new CancellationToken(true); | |||||
_connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
#if !DOTNET5_4 | #if !DOTNET5_4 | ||||
_engine = new WS4NetEngine(this, client.Config, _logger); | |||||
_engine = new WS4NetEngine(client.Config, _taskManager); | |||||
#else | #else | ||||
//_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); | |||||
//_engine = new BuiltInWebSocketEngine(this, client.Config); | |||||
#endif | #endif | ||||
_engine.BinaryMessage += (s, e) => | _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)) | using (var reader = new StreamReader(decompressed)) | ||||
ProcessMessage(reader.ReadToEnd()).Wait(); | |||||
} | |||||
ProcessMessage(reader.ReadToEnd()).Wait(); | |||||
} | |||||
}; | }; | ||||
_engine.TextMessage += (s, e) => ProcessMessage(e.Message).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() | protected async Task BeginConnect() | ||||
@@ -107,13 +79,13 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
await _taskManager.Stop().ConfigureAwait(false); | await _taskManager.Stop().ConfigureAwait(false); | ||||
_taskManager.ClearException(); | _taskManager.ClearException(); | ||||
_state = ConnectionState.Connecting; | |||||
State = ConnectionState.Connecting; | |||||
_cancelTokenSource = new CancellationTokenSource(); | _cancelTokenSource = new CancellationTokenSource(); | ||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||||
CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||||
_lastHeartbeat = DateTime.UtcNow; | _lastHeartbeat = DateTime.UtcNow; | ||||
await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||||
await _engine.Connect(Host, CancelToken).ConfigureAwait(false); | |||||
await Run().ConfigureAwait(false); | await Run().ConfigureAwait(false); | ||||
} | } | ||||
finally | finally | ||||
@@ -131,10 +103,11 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
_state = ConnectionState.Connected; | |||||
State = ConnectionState.Connected; | |||||
_connectedEvent.Set(); | _connectedEvent.Set(); | ||||
RaiseConnected(); | |||||
Logger.Info($"Connected"); | |||||
OnConnected(); | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
@@ -145,29 +118,32 @@ namespace Discord.Net.WebSockets | |||||
protected abstract Task Run(); | protected abstract Task Run(); | ||||
protected virtual async Task Cleanup() | protected virtual async Task Cleanup() | ||||
{ | { | ||||
var oldState = _state; | |||||
_state = ConnectionState.Disconnecting; | |||||
var oldState = State; | |||||
State = ConnectionState.Disconnecting; | |||||
await _engine.Disconnect().ConfigureAwait(false); | await _engine.Disconnect().ConfigureAwait(false); | ||||
_cancelTokenSource = null; | _cancelTokenSource = null; | ||||
_connectedEvent.Reset(); | _connectedEvent.Reset(); | ||||
if (oldState == ConnectionState.Connected) | 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) | 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; | return TaskHelper.CompletedTask; | ||||
} | } | ||||
protected void QueueMessage(IWebSocketMessage message) | protected void QueueMessage(IWebSocketMessage message) | ||||
{ | { | ||||
string json = JsonConvert.SerializeObject(new WebSocketMessage(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); | _engine.QueueMessage(json); | ||||
} | } | ||||
@@ -179,9 +155,9 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
{ | { | ||||
if (_state == ConnectionState.Connected) | |||||
if (this.State == ConnectionState.Connected) | |||||
{ | { | ||||
SendHeartbeat(); | |||||
SendHeartbeat(); | |||||
await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | ||||
} | } | ||||
else | else | ||||
@@ -192,5 +168,20 @@ namespace Discord.Net.WebSockets | |||||
}); | }); | ||||
} | } | ||||
public abstract void SendHeartbeat(); | 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() | public void ThrowException() | ||||
{ | { | ||||
lock (_lock) | lock (_lock) | ||||
{ | |||||
if (_stopReason != null) | |||||
_stopReason.Throw(); | |||||
} | |||||
_stopReason?.Throw(); | |||||
} | } | ||||
public void ClearException() | public void ClearException() | ||||
{ | { |
@@ -29,12 +29,11 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"Newtonsoft.Json": "7.0.1", | |||||
"StyleCop.Analyzers": "1.0.0-rc2" | |||||
"Newtonsoft.Json": "7.0.1" | |||||
}, | }, | ||||
"frameworks": { | "frameworks": { | ||||
"net45": { | |||||
"net46": { | |||||
"dependencies": { | "dependencies": { | ||||
"WebSocket4Net": "0.14.1", | "WebSocket4Net": "0.14.1", | ||||
"RestSharp": "105.2.3" | "RestSharp": "105.2.3" | ||||