| @@ -34,7 +34,7 @@ namespace Discord.Modules | |||
| public event EventHandler<UserEventArgs> UserUpdated; | |||
| public event EventHandler<UserEventArgs> UserPresenceUpdated; | |||
| public event EventHandler<UserEventArgs> UserVoiceStateUpdated; | |||
| public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated; | |||
| public event EventHandler<ChannelEventArgs> UserIsTypingUpdated; | |||
| public event EventHandler<MessageEventArgs> MessageReceived; | |||
| public event EventHandler<MessageEventArgs> MessageSent; | |||
| @@ -1,17 +0,0 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| internal static class EventHelper | |||
| { | |||
| public static void Raise(Logger logger, string name, Action action) | |||
| { | |||
| try { action(); } | |||
| catch (Exception ex) | |||
| { | |||
| var ex2 = ex.GetBaseException(); | |||
| logger.Error($"{name}'s handler raised {ex2.GetType().Name}: ${ex2.Message}", ex); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,20 +0,0 @@ | |||
| using System; | |||
| using System.Globalization; | |||
| namespace Discord | |||
| { | |||
| internal static class IdConvert | |||
| { | |||
| internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; | |||
| public static ulong ToLong(string value) | |||
| => ulong.Parse(value, NumberStyles.None, _format); | |||
| public static ulong? ToNullableLong(string value) | |||
| => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); | |||
| public static string ToString(ulong value) | |||
| => value.ToString(_format); | |||
| public static string ToString(ulong? value) | |||
| => value?.ToString(_format); | |||
| } | |||
| } | |||
| @@ -21,9 +21,9 @@ namespace Discord.API.Client | |||
| [JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))] | |||
| public ulong? LastMessageId { get; set; } | |||
| [JsonProperty("is_private")] | |||
| public bool IsPrivate { get; set; } | |||
| public bool? IsPrivate { get; set; } | |||
| [JsonProperty("position")] | |||
| public int Position { get; set; } | |||
| public int? Position { get; set; } | |||
| [JsonProperty("topic")] | |||
| public string Topic { get; set; } | |||
| [JsonProperty("permission_overwrites")] | |||
| @@ -6,7 +6,7 @@ namespace Discord.API.Client | |||
| public class MemberPresence : MemberReference | |||
| { | |||
| [JsonProperty("game_id")] | |||
| public int? GameId { get; set; } | |||
| public string GameId { get; set; } | |||
| [JsonProperty("status")] | |||
| public string Status { get; set; } | |||
| [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] | |||
| @@ -6,7 +6,7 @@ namespace Discord.API.Client | |||
| public class MemberReference | |||
| { | |||
| [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] | |||
| public ulong GuildId { get; set; } | |||
| public ulong? GuildId { get; set; } | |||
| [JsonProperty("user")] | |||
| public UserReference User { get; set; } | |||
| } | |||
| @@ -14,7 +14,7 @@ namespace Discord.API.Client.Rest | |||
| StringBuilder query = new StringBuilder(); | |||
| this.AddQueryParam(query, "limit", Limit.ToString()); | |||
| if (RelativeDir != null) | |||
| this.AddQueryParam(query, RelativeDir, RelativeId.Value.ToString()); | |||
| this.AddQueryParam(query, RelativeDir, RelativeId.ToString()); | |||
| return $"channels/{ChannelId}/messages{query}"; | |||
| } | |||
| } | |||
| @@ -25,7 +25,7 @@ namespace Discord.API.Client.Rest | |||
| public int Limit { get; set; } = 100; | |||
| public string RelativeDir { get; set; } = null; | |||
| public ulong? RelativeId { get; set; } = 0; | |||
| public ulong RelativeId { get; set; } = 0; | |||
| public GetMessagesRequest(ulong channelId) | |||
| { | |||
| @@ -9,9 +9,9 @@ namespace Discord.API.Converters | |||
| public override bool CanConvert(Type objectType) | |||
| => objectType == typeof(ulong); | |||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||
| => IdConvert.ToLong((string)reader.Value); | |||
| => ((string)reader.Value).ToId(); | |||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||
| => writer.WriteValue(IdConvert.ToString((ulong)value)); | |||
| => writer.WriteValue(((ulong)value).ToIdString()); | |||
| } | |||
| public class NullableLongStringConverter : JsonConverter | |||
| @@ -19,9 +19,9 @@ namespace Discord.API.Converters | |||
| public override bool CanConvert(Type objectType) | |||
| => objectType == typeof(ulong?); | |||
| public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||
| => IdConvert.ToNullableLong((string)reader.Value); | |||
| => ((string)reader.Value).ToNullableId(); | |||
| public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||
| => writer.WriteValue(IdConvert.ToString((ulong?)value)); | |||
| => writer.WriteValue(((ulong?)value).ToIdString()); | |||
| } | |||
| /*public class LongStringEnumerableConverter : JsonConverter | |||
| @@ -66,7 +66,7 @@ namespace Discord.API.Converters | |||
| reader.Read(); | |||
| while (reader.TokenType != JsonToken.EndArray) | |||
| { | |||
| result.Add(IdConvert.ToLong((string)reader.Value)); | |||
| result.Add(((string)reader.Value).ToId()); | |||
| reader.Read(); | |||
| } | |||
| } | |||
| @@ -81,7 +81,7 @@ namespace Discord.API.Converters | |||
| writer.WriteStartArray(); | |||
| var a = (ulong[])value; | |||
| for (int i = 0; i < a.Length; i++) | |||
| writer.WriteValue(IdConvert.ToString(a[i])); | |||
| writer.WriteValue(a[i].ToIdString()); | |||
| writer.WriteEndArray(); | |||
| } | |||
| } | |||
| @@ -1,223 +0,0 @@ | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal sealed class Channels : AsyncCollection<ulong, Channel> | |||
| { | |||
| public IEnumerable<Channel> PrivateChannels => _privateChannels.Select(x => x.Value); | |||
| private ConcurrentDictionary<ulong, Channel> _privateChannels; | |||
| public Channels(DiscordClient client, object writerLock) | |||
| : base(client, writerLock) | |||
| { | |||
| _privateChannels = new ConcurrentDictionary<ulong, Channel>(); | |||
| ItemCreated += (s, e) => | |||
| { | |||
| if (e.Item.IsPrivate) | |||
| _privateChannels.TryAdd(e.Item.Id, e.Item); | |||
| }; | |||
| ItemDestroyed += (s, e) => | |||
| { | |||
| if (e.Item.IsPrivate) | |||
| { | |||
| Channel ignored; | |||
| _privateChannels.TryRemove(e.Item.Id, out ignored); | |||
| } | |||
| }; | |||
| Cleared += (s, e) => _privateChannels.Clear(); | |||
| } | |||
| public Channel GetOrAdd(ulong id, ulong? serverId, ulong? recipientId = null) | |||
| => GetOrAdd(id, () => new Channel(_client, id, serverId, recipientId)); | |||
| } | |||
| public class ChannelEventArgs : EventArgs | |||
| { | |||
| public Channel Channel { get; } | |||
| public Server Server => Channel.Server; | |||
| public ChannelEventArgs(Channel channel) { Channel = channel; } | |||
| } | |||
| public partial class DiscordClient | |||
| { | |||
| public event EventHandler<ChannelEventArgs> ChannelCreated; | |||
| private void RaiseChannelCreated(Channel channel) | |||
| { | |||
| if (ChannelCreated != null) | |||
| EventHelper.Raise(_logger, nameof(ChannelCreated), () => ChannelCreated(this, new ChannelEventArgs(channel))); | |||
| } | |||
| public event EventHandler<ChannelEventArgs> ChannelDestroyed; | |||
| private void RaiseChannelDestroyed(Channel channel) | |||
| { | |||
| if (ChannelDestroyed != null) | |||
| EventHelper.Raise(_logger, nameof(ChannelDestroyed), () => ChannelDestroyed(this, new ChannelEventArgs(channel))); | |||
| } | |||
| public event EventHandler<ChannelEventArgs> ChannelUpdated; | |||
| private void RaiseChannelUpdated(Channel channel) | |||
| { | |||
| if (ChannelUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(ChannelUpdated), () => ChannelUpdated(this, new ChannelEventArgs(channel))); | |||
| } | |||
| /// <summary> Returns a collection of all servers this client is a member of. </summary> | |||
| public IEnumerable<Channel> PrivateChannels { get { CheckReady(); return _channels.PrivateChannels; } } | |||
| internal Channels Channels => _channels; | |||
| private readonly Channels _channels; | |||
| /// <summary> Returns the channel with the specified id, or null if none was found. </summary> | |||
| public Channel GetChannel(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _channels[id]; | |||
| } | |||
| /// <summary> Returns all channels with the specified server and name. </summary> | |||
| /// <remarks> Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||
| if (!exactMatch && name.Length >= 2) | |||
| { | |||
| if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention | |||
| { | |||
| var id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); | |||
| var channel = _channels[id]; | |||
| if (channel != null) | |||
| query = query.Concat(new Channel[] { channel }); | |||
| } | |||
| else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention | |||
| { | |||
| string name2 = name.Substring(1); | |||
| query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||
| } | |||
| } | |||
| if (type != null) | |||
| query = query.Where(x => x.Type == type); | |||
| return query; | |||
| } | |||
| /// <summary> Creates a new channel with the provided name and type. </summary> | |||
| public async Task<Channel> CreateChannel(Server server, string name, ChannelType type) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| if (type == null) throw new ArgumentNullException(nameof(type)); | |||
| CheckReady(); | |||
| var request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); | |||
| channel.Update(response); | |||
| return channel; | |||
| } | |||
| /// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary> | |||
| public async Task<Channel> CreatePMChannel(User user) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| Channel channel = null; | |||
| if (user != null) | |||
| channel = user.Global.PrivateChannel; | |||
| if (channel == null) | |||
| { | |||
| var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var recipient = _users.GetOrAdd(response.Recipient.Id, null); | |||
| recipient.Update(response.Recipient); | |||
| channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient.Id); | |||
| channel.Update(response); | |||
| } | |||
| return channel; | |||
| } | |||
| /// <summary> Edits the provided channel, changing only non-null attributes. </summary> | |||
| public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| CheckReady(); | |||
| if (name != null || topic != null) | |||
| { | |||
| var request = new UpdateChannelRequest(channel.Id) | |||
| { | |||
| Name = name ?? channel.Name, | |||
| Topic = topic ?? channel.Topic, | |||
| Position = channel.Position | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| if (position != null) | |||
| { | |||
| Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray(); | |||
| int oldPos = Array.IndexOf(channels, channel); | |||
| var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); | |||
| int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; | |||
| if (newPos < 0) | |||
| newPos = 0; | |||
| int minPos; | |||
| if (oldPos < newPos) //Moving Down | |||
| { | |||
| minPos = oldPos; | |||
| for (int i = oldPos; i < newPos; i++) | |||
| channels[i] = channels[i + 1]; | |||
| channels[newPos] = channel; | |||
| } | |||
| else //(oldPos > newPos) Moving Up | |||
| { | |||
| minPos = newPos; | |||
| for (int i = oldPos; i > newPos; i--) | |||
| channels[i] = channels[i - 1]; | |||
| channels[newPos] = channel; | |||
| } | |||
| Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; | |||
| await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false); | |||
| } | |||
| } | |||
| /// <summary> Reorders the provided channels in the server's channel list and places them after a certain channel. </summary> | |||
| public Task ReorderChannels(Server server, IEnumerable<Channel> channels, Channel after = null) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (channels == null) throw new ArgumentNullException(nameof(channels)); | |||
| CheckReady(); | |||
| var request = new ReorderChannelsRequest(server.Id) | |||
| { | |||
| ChannelIds = channels.Select(x => x.Id).ToArray(), | |||
| StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| /// <summary> Destroys the provided channel. </summary> | |||
| public async Task DeleteChannel(Channel channel) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,114 @@ | |||
| using System; | |||
| using System.Runtime.CompilerServices; | |||
| namespace Discord | |||
| { | |||
| public partial class DiscordClient | |||
| { | |||
| public event EventHandler Connected = delegate { }; | |||
| public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { }; | |||
| public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { }; | |||
| public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { }; | |||
| public event EventHandler<ChannelEventArgs> ChannelUpdated = delegate { }; | |||
| public event EventHandler<MessageEventArgs> MessageAcknowledged = delegate { }; | |||
| public event EventHandler<MessageEventArgs> MessageDeleted = delegate { }; | |||
| public event EventHandler<MessageEventArgs> MessageReceived = delegate { }; | |||
| public event EventHandler<MessageEventArgs> MessageSent = delegate { }; | |||
| public event EventHandler<MessageEventArgs> MessageUpdated = delegate { }; | |||
| public event EventHandler<ProfileEventArgs> ProfileUpdated = delegate { }; | |||
| public event EventHandler<RoleEventArgs> RoleCreated = delegate { }; | |||
| public event EventHandler<RoleEventArgs> RoleUpdated = delegate { }; | |||
| public event EventHandler<RoleEventArgs> RoleDeleted = delegate { }; | |||
| public event EventHandler<ServerEventArgs> JoinedServer = delegate { }; | |||
| public event EventHandler<ServerEventArgs> LeftServer = delegate { }; | |||
| public event EventHandler<ServerEventArgs> ServerAvailable = delegate { }; | |||
| public event EventHandler<ServerEventArgs> ServerUpdated = delegate { }; | |||
| public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { }; | |||
| public event EventHandler<BanEventArgs> UserBanned = delegate { }; | |||
| public event EventHandler<ChannelUserEventArgs> UserIsTypingUpdated = delegate { }; | |||
| public event EventHandler<UserEventArgs> UserJoined = delegate { }; | |||
| public event EventHandler<UserEventArgs> UserLeft = delegate { }; | |||
| public event EventHandler<UserEventArgs> UserPresenceUpdated = delegate { }; | |||
| public event EventHandler<UserEventArgs> UserUpdated = delegate { }; | |||
| public event EventHandler<BanEventArgs> UserUnbanned = delegate { }; | |||
| public event EventHandler<UserEventArgs> UserVoiceStateUpdated = delegate { }; | |||
| private void OnConnected() | |||
| => OnEvent(Connected); | |||
| private void OnDisconnected(bool wasUnexpected, Exception ex) | |||
| => OnEvent(Disconnected, new DisconnectedEventArgs(wasUnexpected, ex)); | |||
| private void OnChannelCreated(Channel channel) | |||
| => OnEvent(ChannelCreated, new ChannelEventArgs(channel)); | |||
| private void OnChannelDestroyed(Channel channel) | |||
| => OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); | |||
| private void OnChannelUpdated(Channel channel) | |||
| => OnEvent(ChannelUpdated, new ChannelEventArgs(channel)); | |||
| private void OnMessageAcknowledged(Message msg) | |||
| => OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); | |||
| private void OnMessageDeleted(Message msg) | |||
| => OnEvent(MessageDeleted, new MessageEventArgs(msg)); | |||
| private void OnMessageReceived(Message msg) | |||
| => OnEvent(MessageReceived, new MessageEventArgs(msg)); | |||
| /*private void OnMessageSent(Message msg) | |||
| => OnEvent(MessageSent, new MessageEventArgs(msg));*/ | |||
| private void OnMessageUpdated(Message msg) | |||
| => OnEvent(MessageUpdated, new MessageEventArgs(msg)); | |||
| private void OnProfileUpdated(Profile profile) | |||
| => OnEvent(ProfileUpdated, new ProfileEventArgs(profile)); | |||
| private void OnRoleCreated(Role role) | |||
| => OnEvent(RoleCreated, new RoleEventArgs(role)); | |||
| private void OnRoleDeleted(Role role) | |||
| => OnEvent(RoleDeleted, new RoleEventArgs(role)); | |||
| private void OnRoleUpdated(Role role) | |||
| => OnEvent(RoleUpdated, new RoleEventArgs(role)); | |||
| private void OnJoinedServer(Server server) | |||
| => OnEvent(JoinedServer, new ServerEventArgs(server)); | |||
| private void OnLeftServer(Server server) | |||
| => OnEvent(LeftServer, new ServerEventArgs(server)); | |||
| private void OnServerAvailable(Server server) | |||
| => OnEvent(ServerAvailable, new ServerEventArgs(server)); | |||
| private void OnServerUpdated(Server server) | |||
| => OnEvent(ServerUpdated, new ServerEventArgs(server)); | |||
| private void OnServerUnavailable(Server server) | |||
| => OnEvent(ServerUnavailable, new ServerEventArgs(server)); | |||
| private void OnUserBanned(Server server, ulong userId) | |||
| => OnEvent(UserBanned, new BanEventArgs(server, userId)); | |||
| private void OnUserIsTypingUpdated(Channel channel, User user) | |||
| => OnEvent(UserIsTypingUpdated, new ChannelUserEventArgs(channel, user)); | |||
| private void OnUserJoined(User user) | |||
| => OnEvent(UserJoined, new UserEventArgs(user)); | |||
| private void OnUserLeft(User user) | |||
| => OnEvent(UserLeft, new UserEventArgs(user)); | |||
| private void OnUserPresenceUpdated(User user) | |||
| => OnEvent(UserPresenceUpdated, new UserEventArgs(user)); | |||
| private void OnUserUnbanned(Server server, ulong userId) | |||
| => OnEvent(UserUnbanned, new BanEventArgs(server, userId)); | |||
| private void OnUserUpdated(User user) | |||
| => OnEvent(UserUpdated, new UserEventArgs(user)); | |||
| private void OnUserVoiceStateUpdated(User user) | |||
| => OnEvent(UserVoiceStateUpdated, new UserEventArgs(user)); | |||
| private void OnEvent<T>(EventHandler<T> handler, T eventArgs, [CallerMemberName] string callerName = null) | |||
| { | |||
| try { handler(this, eventArgs); } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex); | |||
| } | |||
| } | |||
| private void OnEvent(EventHandler handler, [CallerMemberName] string callerName = null) | |||
| { | |||
| try { handler(this, EventArgs.Empty); } | |||
| catch (Exception ex) | |||
| { | |||
| Logger.Error($"{callerName.Substring(2)}'s handler encountered error {ex.GetType().Name}: ${ex.Message}", ex); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,104 +0,0 @@ | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public partial class DiscordClient | |||
| { | |||
| /// <summary> Gets more info about the provided invite code. </summary> | |||
| /// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks> | |||
| public async Task<Invite> GetInvite(string inviteIdOrXkcd) | |||
| { | |||
| if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); | |||
| CheckReady(); | |||
| //Remove trailing slash | |||
| if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') | |||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); | |||
| //Remove leading URL | |||
| int index = inviteIdOrXkcd.LastIndexOf('/'); | |||
| if (index >= 0) | |||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); | |||
| var response = await _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); | |||
| var invite = new Invite(response.Code, response.XkcdPass); | |||
| invite.Update(response); | |||
| return invite; | |||
| } | |||
| /// <summary> Gets all active (non-expired) invites to a provided server. </summary> | |||
| public async Task<Invite[]> GetInvites(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false); | |||
| return response.Select(x => | |||
| { | |||
| var invite = new Invite(x.Code, x.XkcdPass); | |||
| invite.Update(x); | |||
| return invite; | |||
| }).ToArray(); | |||
| } | |||
| /// <summary> Creates a new invite to the default channel of the provided server. </summary> | |||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||
| /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> | |||
| public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd); | |||
| } | |||
| /// <summary> Creates a new invite to the provided channel. </summary> | |||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||
| /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> | |||
| public async Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); | |||
| if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | |||
| CheckReady(); | |||
| var request = new CreateInviteRequest(channel.Id) | |||
| { | |||
| MaxAge = maxAge, | |||
| MaxUses = maxUses, | |||
| IsTemporary = isTemporary, | |||
| WithXkcdPass = withXkcd | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var invite = new Invite(response.Code, response.XkcdPass); | |||
| return invite; | |||
| } | |||
| /// <summary> Deletes the provided invite. </summary> | |||
| public async Task DeleteInvite(Invite invite) | |||
| { | |||
| if (invite == null) throw new ArgumentNullException(nameof(invite)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| /// <summary> Accepts the provided invite. </summary> | |||
| public Task AcceptInvite(Invite invite) | |||
| { | |||
| if (invite == null) throw new ArgumentNullException(nameof(invite)); | |||
| CheckReady(); | |||
| return _clientRest.Send(new AcceptInviteRequest(invite.Code)); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,429 +0,0 @@ | |||
| using Discord.API; | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| using APIMessage = Discord.API.Client.Message; | |||
| using APIUser = Discord.API.Client.User; | |||
| namespace Discord | |||
| { | |||
| public enum RelativeDirection { Before, After} | |||
| internal sealed class Messages : AsyncCollection<ulong, Message> | |||
| { | |||
| private bool _isEnabled; | |||
| public Messages(DiscordClient client, object writerLock, bool isEnabled) | |||
| : base(client, writerLock) | |||
| { | |||
| _isEnabled = isEnabled; | |||
| } | |||
| public Message GetOrAdd(ulong id, ulong channelId, ulong userId) | |||
| { | |||
| if (_isEnabled) | |||
| return GetOrAdd(id, () => new Message(_client, id, channelId, userId)); | |||
| else | |||
| { | |||
| var msg = new Message(_client, id, channelId, userId); | |||
| msg.Cache(); //Builds references | |||
| return msg; | |||
| } | |||
| } | |||
| public void Import(Dictionary<ulong, Message> messages) | |||
| => base.Import(messages); | |||
| } | |||
| internal class MessageQueueItem | |||
| { | |||
| public readonly Message Message; | |||
| public readonly string Text; | |||
| public readonly ulong[] MentionedUsers; | |||
| public MessageQueueItem(Message msg, string text, ulong[] userIds) | |||
| { | |||
| Message = msg; | |||
| Text = text; | |||
| MentionedUsers = userIds; | |||
| } | |||
| } | |||
| public class MessageEventArgs : EventArgs | |||
| { | |||
| public Message Message { get; } | |||
| public User User => Message.User; | |||
| public Channel Channel => Message.Channel; | |||
| public Server Server => Message.Server; | |||
| public MessageEventArgs(Message msg) { Message = msg; } | |||
| } | |||
| public partial class DiscordClient | |||
| { | |||
| public const int MaxMessageSize = 2000; | |||
| public event EventHandler<MessageEventArgs> MessageReceived; | |||
| private void RaiseMessageReceived(Message msg) | |||
| { | |||
| if (MessageReceived != null) | |||
| EventHelper.Raise(_logger, nameof(MessageReceived), () => MessageReceived(this, new MessageEventArgs(msg))); | |||
| } | |||
| public event EventHandler<MessageEventArgs> MessageSent; | |||
| private void RaiseMessageSent(Message msg) | |||
| { | |||
| if (MessageSent != null) | |||
| EventHelper.Raise(_logger, nameof(MessageSent), () => MessageSent(this, new MessageEventArgs(msg))); | |||
| } | |||
| public event EventHandler<MessageEventArgs> MessageDeleted; | |||
| private void RaiseMessageDeleted(Message msg) | |||
| { | |||
| if (MessageDeleted != null) | |||
| EventHelper.Raise(_logger, nameof(MessageDeleted), () => MessageDeleted(this, new MessageEventArgs(msg))); | |||
| } | |||
| public event EventHandler<MessageEventArgs> MessageUpdated; | |||
| private void RaiseMessageUpdated(Message msg) | |||
| { | |||
| if (MessageUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(MessageUpdated), () => MessageUpdated(this, new MessageEventArgs(msg))); | |||
| } | |||
| public event EventHandler<MessageEventArgs> MessageAcknowledged; | |||
| private void RaiseMessageAcknowledged(Message msg) | |||
| { | |||
| if (MessageAcknowledged != null) | |||
| EventHelper.Raise(_logger, nameof(MessageAcknowledged), () => MessageAcknowledged(this, new MessageEventArgs(msg))); | |||
| } | |||
| internal Messages Messages => _messages; | |||
| private readonly Random _nonceRand; | |||
| private readonly Messages _messages; | |||
| private readonly JsonSerializer _messageImporter; | |||
| private readonly ConcurrentQueue<MessageQueueItem> _pendingMessages; | |||
| /// <summary> Returns the message with the specified id, or null if none was found. </summary> | |||
| public Message GetMessage(ulong id) | |||
| { | |||
| if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); | |||
| CheckReady(); | |||
| return _messages[id]; | |||
| } | |||
| /// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary> | |||
| public Task<Message> SendMessage(Channel channel, string text) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| return SendMessageInternal(channel, text, false); | |||
| } | |||
| /// <summary> Sends a private message to the provided user. </summary> | |||
| public async Task<Message> SendMessage(User user, string text) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendMessageInternal(channel, text, false).ConfigureAwait(false); | |||
| } | |||
| /// <summary> Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. </summary> | |||
| public Task<Message> SendTTSMessage(Channel channel, string text) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| return SendMessageInternal(channel, text, true); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public Task<Message> SendFile(Channel channel, string filePath) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (filePath == null) throw new ArgumentNullException(nameof(filePath)); | |||
| CheckReady(); | |||
| return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(Channel channel, string filename, Stream stream) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
| CheckReady(); | |||
| var request = new SendFileRequest(channel.Id) | |||
| { | |||
| Filename = filename, | |||
| Stream = stream | |||
| }; | |||
| var model = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | |||
| msg.Update(model); | |||
| RaiseMessageSent(msg); | |||
| return msg; | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(User user, string filePath) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (filePath == null) throw new ArgumentNullException(nameof(filePath)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(User user, string filename, Stream stream) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendFile(channel, filename, stream).ConfigureAwait(false); | |||
| } | |||
| private async Task<Message> SendMessageInternal(Channel channel, string text, bool isTextToSpeech) | |||
| { | |||
| Message msg; | |||
| var server = channel.Server; | |||
| var mentionedUsers = new List<User>(); | |||
| text = Mention.CleanUserMentions(this, server, text, mentionedUsers); | |||
| if (text.Length > MaxMessageSize) | |||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); | |||
| if (Config.UseMessageQueue) | |||
| { | |||
| var nonce = GenerateNonce(); | |||
| msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id); | |||
| var currentUser = msg.User; | |||
| msg.Update(new APIMessage | |||
| { | |||
| Content = text, | |||
| Timestamp = DateTime.UtcNow, | |||
| Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name }, | |||
| ChannelId = channel.Id, | |||
| Nonce = IdConvert.ToString(nonce), | |||
| IsTextToSpeech = isTextToSpeech | |||
| }); | |||
| msg.State = MessageState.Queued; | |||
| _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); | |||
| } | |||
| else | |||
| { | |||
| var request = new SendMessageRequest(channel.Id) | |||
| { | |||
| Content = text, | |||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), | |||
| Nonce = null, | |||
| IsTTS = isTextToSpeech | |||
| }; | |||
| var model = await _clientRest.Send(request).ConfigureAwait(false); | |||
| msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | |||
| msg.Update(model); | |||
| RaiseMessageSent(msg); | |||
| } | |||
| return msg; | |||
| } | |||
| /// <summary> Edits the provided message, changing only non-null attributes. </summary> | |||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks> | |||
| public async Task EditMessage(Message message, string text) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| var channel = message.Channel; | |||
| var mentionedUsers = new List<User>(); | |||
| if (!channel.IsPrivate) | |||
| text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers); | |||
| if (text.Length > MaxMessageSize) | |||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); | |||
| if (Config.UseMessageQueue) | |||
| _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); | |||
| else | |||
| { | |||
| var request = new UpdateMessageRequest(message.Channel.Id, message.Id) | |||
| { | |||
| Content = text, | |||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| } | |||
| /// <summary> Deletes the provided message. </summary> | |||
| public async Task DeleteMessage(Message message) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| CheckReady(); | |||
| var request = new DeleteMessageRequest(message.Id, message.Channel.Id); | |||
| try { await _clientRest.Send(request).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task DeleteMessages(IEnumerable<Message> messages) | |||
| { | |||
| if (messages == null) throw new ArgumentNullException(nameof(messages)); | |||
| CheckReady(); | |||
| foreach (var message in messages) | |||
| { | |||
| var request = new DeleteMessageRequest(message.Id, message.Channel.Id); | |||
| try { await _clientRest.Send(request).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| } | |||
| /// <summary> Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. </summary> | |||
| public async Task<Message[]> DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (limit < 0) throw new ArgumentNullException(nameof(limit)); | |||
| CheckReady(); | |||
| if (limit == 0) return new Message[0]; | |||
| if (channel != null && channel.Type == ChannelType.Text) | |||
| { | |||
| try | |||
| { | |||
| var request = new GetMessagesRequest(channel.Id) | |||
| { | |||
| Limit = limit, | |||
| RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after", | |||
| RelativeId = relativeMessageId | |||
| }; | |||
| var msgs = await _clientRest.Send(request).ConfigureAwait(false); | |||
| return msgs.Select(x => | |||
| { | |||
| Message msg = null; | |||
| if (useCache) | |||
| { | |||
| msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id); | |||
| var user = msg.User; | |||
| if (user != null) | |||
| user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); | |||
| } | |||
| else | |||
| msg = /*_messages[x.Id] ??*/ new Message(this, x.Id, x.ChannelId, x.Author.Id); | |||
| msg.Update(x); | |||
| return msg; | |||
| }) | |||
| .ToArray(); | |||
| } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden){ } //Bad Permissions | |||
| } | |||
| return new Message[0]; | |||
| } | |||
| /// <summary> Marks a given message as read. </summary> | |||
| public void AckMessage(Message message) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| if (!message.IsAuthor) | |||
| _clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id)); | |||
| } | |||
| /// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary> | |||
| public IEnumerable<Message> ImportMessages(Channel channel, string json) | |||
| { | |||
| if (json == null) throw new ArgumentNullException(nameof(json)); | |||
| var dic = JArray.Parse(json) | |||
| .Select(x => | |||
| { | |||
| var msg = new Message(this, | |||
| x["Id"].Value<ulong>(), | |||
| channel.Id, | |||
| x["UserId"].Value<ulong>()); | |||
| var reader = x.CreateReader(); | |||
| _messageImporter.Populate(reader, msg); | |||
| msg.Text = Mention.Resolve(msg, msg.RawText); | |||
| return msg; | |||
| }) | |||
| .ToDictionary(x => x.Id); | |||
| _messages.Import(dic); | |||
| foreach (var msg in dic.Values) | |||
| { | |||
| var user = msg.User; | |||
| if (user != null) | |||
| user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); | |||
| } | |||
| return dic.Values; | |||
| } | |||
| /// <summary> Serializes the message cache for a given channel to JSON.</summary> | |||
| public string ExportMessages(Channel channel) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| return JsonConvert.SerializeObject(channel.Messages); | |||
| } | |||
| private Task MessageQueueAsync() | |||
| { | |||
| var cancelToken = _cancelToken; | |||
| int interval = Config.MessageQueueInterval; | |||
| return Task.Run(async () => | |||
| { | |||
| MessageQueueItem queuedMessage; | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| while (_pendingMessages.TryDequeue(out queuedMessage)) | |||
| { | |||
| var msg = queuedMessage.Message; | |||
| try | |||
| { | |||
| if (msg.Id == 0) | |||
| { | |||
| var request = new SendMessageRequest(msg.Channel.Id) | |||
| { | |||
| Content = queuedMessage.Text, | |||
| MentionedUserIds = queuedMessage.MentionedUsers, | |||
| Nonce = IdConvert.ToString(msg.Id), //Nonce | |||
| IsTTS = msg.IsTTS | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id) | |||
| { | |||
| Content = queuedMessage.Text, | |||
| MentionedUserIds = queuedMessage.MentionedUsers | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| } | |||
| catch (WebException) { break; } | |||
| catch (HttpException) { msg.State = MessageState.Failed; } | |||
| } | |||
| await Task.Delay(interval).ConfigureAwait(false); | |||
| } | |||
| }); | |||
| } | |||
| private ulong GenerateNonce() | |||
| { | |||
| lock (_nonceRand) | |||
| return (ulong)_nonceRand.Next(1, int.MaxValue); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,972 @@ | |||
| namespace Discord | |||
| { | |||
| /*public enum RelativeDirection { Before, After } | |||
| public partial class DiscordClient | |||
| { | |||
| /// <summary> Returns the channel with the specified id, or null if none was found. </summary> | |||
| public Channel GetChannel(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _channels[id]; | |||
| } | |||
| /// <summary> Returns all channels with the specified server and name. </summary> | |||
| /// <remarks> Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| var query = server.Channels.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||
| if (!exactMatch && name.Length >= 2) | |||
| { | |||
| if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention | |||
| { | |||
| var id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); | |||
| var channel = _channels[id]; | |||
| if (channel != null) | |||
| query = query.Concat(new Channel[] { channel }); | |||
| } | |||
| else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention | |||
| { | |||
| string name2 = name.Substring(1); | |||
| query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||
| } | |||
| } | |||
| if (type != null) | |||
| query = query.Where(x => x.Type == type); | |||
| return query; | |||
| } | |||
| /// <summary> Creates a new channel with the provided name and type. </summary> | |||
| public async Task<Channel> CreateChannel(Server server, string name, ChannelType type) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| if (type == null) throw new ArgumentNullException(nameof(type)); | |||
| CheckReady(); | |||
| var request = new CreateChannelRequest(server.Id) { Name = name, Type = type.Value }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient?.Id); | |||
| channel.Update(response); | |||
| return channel; | |||
| } | |||
| /// <summary> Returns the private channel with the provided user, creating one if it does not currently exist. </summary> | |||
| public async Task<Channel> CreatePMChannel(User user) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| Channel channel = null; | |||
| if (user != null) | |||
| channel = user.Global.PrivateChannel; | |||
| if (channel == null) | |||
| { | |||
| var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var recipient = _users.GetOrAdd(response.Recipient.Id, null); | |||
| recipient.Update(response.Recipient); | |||
| channel = _channels.GetOrAdd(response.Id, response.GuildId, response.Recipient.Id); | |||
| channel.Update(response); | |||
| } | |||
| return channel; | |||
| } | |||
| /// <summary> Edits the provided channel, changing only non-null attributes. </summary> | |||
| public async Task EditChannel(Channel channel, string name = null, string topic = null, int? position = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| CheckReady(); | |||
| if (name != null || topic != null) | |||
| { | |||
| var request = new UpdateChannelRequest(channel.Id) | |||
| { | |||
| Name = name ?? channel.Name, | |||
| Topic = topic ?? channel.Topic, | |||
| Position = channel.Position | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| if (position != null) | |||
| { | |||
| Channel[] channels = channel.Server.Channels.Where(x => x.Type == channel.Type).OrderBy(x => x.Position).ToArray(); | |||
| int oldPos = Array.IndexOf(channels, channel); | |||
| var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); | |||
| int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; | |||
| if (newPos < 0) | |||
| newPos = 0; | |||
| int minPos; | |||
| if (oldPos < newPos) //Moving Down | |||
| { | |||
| minPos = oldPos; | |||
| for (int i = oldPos; i < newPos; i++) | |||
| channels[i] = channels[i + 1]; | |||
| channels[newPos] = channel; | |||
| } | |||
| else //(oldPos > newPos) Moving Up | |||
| { | |||
| minPos = newPos; | |||
| for (int i = oldPos; i > newPos; i--) | |||
| channels[i] = channels[i - 1]; | |||
| channels[newPos] = channel; | |||
| } | |||
| Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; | |||
| await ReorderChannels(channel.Server, channels.Skip(minPos), after).ConfigureAwait(false); | |||
| } | |||
| } | |||
| /// <summary> Reorders the provided channels in the server's channel list and places them after a certain channel. </summary> | |||
| public Task ReorderChannels(Server server, IEnumerable<Channel> channels, Channel after = null) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (channels == null) throw new ArgumentNullException(nameof(channels)); | |||
| CheckReady(); | |||
| var request = new ReorderChannelsRequest(server.Id) | |||
| { | |||
| ChannelIds = channels.Select(x => x.Id).ToArray(), | |||
| StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| /// <summary> Destroys the provided channel. </summary> | |||
| public async Task DeleteChannel(Channel channel) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteChannelRequest(channel.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| /// <summary> Gets more info about the provided invite code. </summary> | |||
| /// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks> | |||
| public async Task<Invite> GetInvite(string inviteIdOrXkcd) | |||
| { | |||
| if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); | |||
| CheckReady(); | |||
| //Remove trailing slash | |||
| if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') | |||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); | |||
| //Remove leading URL | |||
| int index = inviteIdOrXkcd.LastIndexOf('/'); | |||
| if (index >= 0) | |||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); | |||
| var response = await _clientRest.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); | |||
| var invite = new Invite(response.Code, response.XkcdPass); | |||
| invite.Update(response); | |||
| return invite; | |||
| } | |||
| /// <summary> Gets all active (non-expired) invites to a provided server. </summary> | |||
| public async Task<Invite[]> GetInvites(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| var response = await _clientRest.Send(new GetInvitesRequest(server.Id)).ConfigureAwait(false); | |||
| return response.Select(x => | |||
| { | |||
| var invite = new Invite(x.Code, x.XkcdPass); | |||
| invite.Update(x); | |||
| return invite; | |||
| }).ToArray(); | |||
| } | |||
| /// <summary> Creates a new invite to the default channel of the provided server. </summary> | |||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||
| /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> | |||
| public Task<Invite> CreateInvite(Server server, int maxAge = 1800, int maxUses = 0, bool tempMembership = false, bool hasXkcd = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| return CreateInvite(server.DefaultChannel, maxAge, maxUses, tempMembership, hasXkcd); | |||
| } | |||
| /// <summary> Creates a new invite to the provided channel. </summary> | |||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to 0 to never expire. </param> | |||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||
| /// <param name="hasXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to 0. </param> | |||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to 0 to have unlimited uses. </param> | |||
| public async Task<Invite> CreateInvite(Channel channel, int maxAge = 1800, int maxUses = 0, bool isTemporary = false, bool withXkcd = false) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); | |||
| if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | |||
| CheckReady(); | |||
| var request = new CreateInviteRequest(channel.Id) | |||
| { | |||
| MaxAge = maxAge, | |||
| MaxUses = maxUses, | |||
| IsTemporary = isTemporary, | |||
| WithXkcdPass = withXkcd | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var invite = new Invite(response.Code, response.XkcdPass); | |||
| return invite; | |||
| } | |||
| /// <summary> Deletes the provided invite. </summary> | |||
| public async Task DeleteInvite(Invite invite) | |||
| { | |||
| if (invite == null) throw new ArgumentNullException(nameof(invite)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteInviteRequest(invite.Code)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| /// <summary> Accepts the provided invite. </summary> | |||
| public Task AcceptInvite(Invite invite) | |||
| { | |||
| if (invite == null) throw new ArgumentNullException(nameof(invite)); | |||
| CheckReady(); | |||
| return _clientRest.Send(new AcceptInviteRequest(invite.Code)); | |||
| } | |||
| /// <summary> Returns the message with the specified id, or null if none was found. </summary> | |||
| public Message GetMessage(ulong id) | |||
| { | |||
| if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); | |||
| CheckReady(); | |||
| return _messages[id]; | |||
| } | |||
| /// <summary> Sends a message to the provided channel. To include a mention, see the Mention static helper class. </summary> | |||
| public Task<Message> SendMessage(Channel channel, string text) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| return SendMessageInternal(channel, text, false); | |||
| } | |||
| /// <summary> Sends a private message to the provided user. </summary> | |||
| public async Task<Message> SendMessage(User user, string text) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendMessageInternal(channel, text, false).ConfigureAwait(false); | |||
| } | |||
| /// <summary> Sends a text-to-speech message to the provided channel. To include a mention, see the Mention static helper class. </summary> | |||
| public Task<Message> SendTTSMessage(Channel channel, string text) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| return SendMessageInternal(channel, text, true); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public Task<Message> SendFile(Channel channel, string filePath) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (filePath == null) throw new ArgumentNullException(nameof(filePath)); | |||
| CheckReady(); | |||
| return SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(Channel channel, string filename, Stream stream) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
| CheckReady(); | |||
| var request = new SendFileRequest(channel.Id) | |||
| { | |||
| Filename = filename, | |||
| Stream = stream | |||
| }; | |||
| var model = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | |||
| msg.Update(model); | |||
| RaiseMessageSent(msg); | |||
| return msg; | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(User user, string filePath) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (filePath == null) throw new ArgumentNullException(nameof(filePath)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendFile(channel, Path.GetFileName(filePath), File.OpenRead(filePath)).ConfigureAwait(false); | |||
| } | |||
| /// <summary> Sends a file to the provided channel. </summary> | |||
| public async Task<Message> SendFile(User user, string filename, Stream stream) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||
| CheckReady(); | |||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | |||
| return await SendFile(channel, filename, stream).ConfigureAwait(false); | |||
| } | |||
| private async Task<Message> SendMessageInternal(Channel channel, string text, bool isTextToSpeech) | |||
| { | |||
| Message msg; | |||
| var server = channel.Server; | |||
| var mentionedUsers = new List<User>(); | |||
| text = Mention.CleanUserMentions(this, server, text, mentionedUsers); | |||
| if (text.Length > MaxMessageSize) | |||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less."); | |||
| if (Config.UseMessageQueue) | |||
| { | |||
| var nonce = GenerateNonce(); | |||
| msg = new Message(this, 0, channel.Id, _currentUser.Id); //_messages.GetOrAdd(nonce, channel.Id, _privateUser.Id); | |||
| var currentUser = msg.User; | |||
| msg.Update(new APIMessage | |||
| { | |||
| Content = text, | |||
| Timestamp = DateTime.UtcNow, | |||
| Author = new APIUser { Avatar = currentUser.AvatarId, Discriminator = currentUser.Discriminator, Id = _currentUser.Id, Username = currentUser.Name }, | |||
| ChannelId = channel.Id, | |||
| Nonce = IdConvert.ToString(nonce), | |||
| IsTextToSpeech = isTextToSpeech | |||
| }); | |||
| msg.State = MessageState.Queued; | |||
| _pendingMessages.Enqueue(new MessageQueueItem(msg, text, mentionedUsers.Select(x => x.Id).ToArray())); | |||
| } | |||
| else | |||
| { | |||
| var request = new SendMessageRequest(channel.Id) | |||
| { | |||
| Content = text, | |||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), | |||
| Nonce = null, | |||
| IsTTS = isTextToSpeech | |||
| }; | |||
| var model = await _clientRest.Send(request).ConfigureAwait(false); | |||
| msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | |||
| msg.Update(model); | |||
| RaiseMessageSent(msg); | |||
| } | |||
| return msg; | |||
| } | |||
| /// <summary> Edits the provided message, changing only non-null attributes. </summary> | |||
| /// <remarks> While not required, it is recommended to include a mention reference in the text (see Mention.User). </remarks> | |||
| public async Task EditMessage(Message message, string text) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| CheckReady(); | |||
| var channel = message.Channel; | |||
| var mentionedUsers = new List<User>(); | |||
| if (!channel.IsPrivate) | |||
| text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers); | |||
| if (text.Length > DiscordConfig.MaxMessageSize) | |||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||
| if (Config.UseMessageQueue) | |||
| _pendingMessages.Enqueue(new MessageQueueItem(message, text, mentionedUsers.Select(x => x.Id).ToArray())); | |||
| else | |||
| { | |||
| var request = new UpdateMessageRequest(message.Channel.Id, message.Id) | |||
| { | |||
| Content = text, | |||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| } | |||
| } | |||
| /// <summary> Deletes the provided message. </summary> | |||
| public async Task DeleteMessage(Message message) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| CheckReady(); | |||
| var request = new DeleteMessageRequest(message.Id, message.Channel.Id); | |||
| try { await _clientRest.Send(request).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task DeleteMessages(IEnumerable<Message> messages) | |||
| { | |||
| if (messages == null) throw new ArgumentNullException(nameof(messages)); | |||
| CheckReady(); | |||
| foreach (var message in messages) | |||
| { | |||
| var request = new DeleteMessageRequest(message.Id, message.Channel.Id); | |||
| try { await _clientRest.Send(request).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| } | |||
| /// <summary> Downloads messages from the server, returning all messages before or after relativeMessageId, if it's provided. </summary> | |||
| public async Task<Message[]> DownloadMessages(Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (limit < 0) throw new ArgumentNullException(nameof(limit)); | |||
| CheckReady(); | |||
| if (limit == 0) return new Message[0]; | |||
| if (channel != null && channel.Type == ChannelType.Text) | |||
| { | |||
| try | |||
| { | |||
| var request = new GetMessagesRequest(channel.Id) | |||
| { | |||
| Limit = limit, | |||
| RelativeDir = relativeDir == RelativeDirection.Before ? "before" : "after", | |||
| RelativeId = relativeMessageId | |||
| }; | |||
| var msgs = await _clientRest.Send(request).ConfigureAwait(false); | |||
| return msgs.Select(x => | |||
| { | |||
| Message msg = null; | |||
| if (useCache) | |||
| { | |||
| msg = _messages.GetOrAdd(x.Id, x.ChannelId, x.Author.Id); | |||
| var user = msg.User; | |||
| if (user != null) | |||
| user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); | |||
| } | |||
| else | |||
| msg = new Message(this, x.Id, x.ChannelId, x.Author.Id); | |||
| msg.Update(x); | |||
| return msg; | |||
| }) | |||
| .ToArray(); | |||
| } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) { } //Bad Permissions | |||
| } | |||
| return new Message[0]; | |||
| } | |||
| /// <summary> Marks a given message as read. </summary> | |||
| public void AckMessage(Message message) | |||
| { | |||
| if (message == null) throw new ArgumentNullException(nameof(message)); | |||
| if (!message.IsAuthor) | |||
| _clientRest.Send(new AckMessageRequest(message.Id, message.Channel.Id)); | |||
| } | |||
| /// <summary> Deserializes messages from JSON format and imports them into the message cache.</summary> | |||
| public IEnumerable<Message> ImportMessages(Channel channel, string json) | |||
| { | |||
| if (json == null) throw new ArgumentNullException(nameof(json)); | |||
| var dic = JArray.Parse(json) | |||
| .Select(x => | |||
| { | |||
| var msg = new Message(this, | |||
| x["Id"].Value<ulong>(), | |||
| channel.Id, | |||
| x["UserId"].Value<ulong>()); | |||
| var reader = x.CreateReader(); | |||
| _messageImporter.Populate(reader, msg); | |||
| msg.Text = Mention.Resolve(msg, msg.RawText); | |||
| return msg; | |||
| }) | |||
| .ToDictionary(x => x.Id); | |||
| _messages.Import(dic); | |||
| foreach (var msg in dic.Values) | |||
| { | |||
| var user = msg.User; | |||
| if (user != null) | |||
| user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); | |||
| } | |||
| return dic.Values; | |||
| } | |||
| /// <summary> Serializes the message cache for a given channel to JSON.</summary> | |||
| public string ExportMessages(Channel channel) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| return JsonConvert.SerializeObject(channel.Messages); | |||
| } | |||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||
| public User GetUser(Server server, ulong userId) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| return _users[userId, server.Id]; | |||
| } | |||
| /// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary> | |||
| public User GetUser(Server server, string username, ushort discriminator) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (username == null) throw new ArgumentNullException(nameof(username)); | |||
| CheckReady(); | |||
| return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault(); | |||
| } | |||
| /// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary> | |||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<User> FindUsers(Server server, string name, bool exactMatch = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch); | |||
| } | |||
| /// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary> | |||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<User> FindUsers(Channel channel, string name, bool exactMatch = false) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch); | |||
| } | |||
| private IEnumerable<User> FindUsers(IEnumerable<User> users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false) | |||
| { | |||
| var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||
| if (!exactMatch && name.Length >= 2) | |||
| { | |||
| if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention | |||
| { | |||
| ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); | |||
| var user = _users[id, serverId]; | |||
| if (user != null) | |||
| query = query.Concat(new User[] { user }); | |||
| } | |||
| else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention | |||
| { | |||
| string name2 = name.Substring(1); | |||
| query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||
| } | |||
| } | |||
| if (discriminator != null) | |||
| query = query.Where(x => x.Discriminator == discriminator.Value); | |||
| return query; | |||
| } | |||
| public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel"); | |||
| CheckReady(); | |||
| //Modify the roles collection and filter out the everyone role | |||
| var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone).Select(x => x.Id); | |||
| var request = new UpdateMemberRequest(user.Server.Id, user.Id) | |||
| { | |||
| IsMuted = isMuted ?? user.IsServerMuted, | |||
| IsDeafened = isDeafened ?? user.IsServerDeafened, | |||
| VoiceChannelId = voiceChannel?.Id, | |||
| RoleIds = roleIds.ToArray() | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task KickUser(User user) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel"); | |||
| CheckReady(); | |||
| var request = new KickMemberRequest(user.Server.Id, user.Id); | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task BanUser(User user, int pruneDays = 0) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel"); | |||
| CheckReady(); | |||
| var request = new AddGuildBanRequest(user.Server.Id, user.Id); | |||
| request.PruneDays = pruneDays; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public async Task UnbanUser(Server server, ulong userId) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task<int> PruneUsers(Server server, int days, bool simulate = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); | |||
| CheckReady(); | |||
| var request = new PruneMembersRequest(server.Id) | |||
| { | |||
| Days = days, | |||
| IsSimulation = simulate | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| return response.Pruned; | |||
| } | |||
| /// <summary>When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server.</summary> | |||
| public void RequestOfflineUsers(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| _webSocket.SendRequestMembers(server.Id, "", 0); | |||
| } | |||
| public async Task EditProfile(string currentPassword = "", | |||
| string username = null, string email = null, string password = null, | |||
| Stream avatar = null, ImageType avatarType = ImageType.Png) | |||
| { | |||
| if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); | |||
| CheckReady(); | |||
| var request = new UpdateProfileRequest() | |||
| { | |||
| CurrentPassword = currentPassword, | |||
| Email = email ?? _currentUser?.Email, | |||
| Password = password, | |||
| Username = username ?? _privateUser?.Name, | |||
| AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId) | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| if (password != null) | |||
| { | |||
| var loginRequest = new LoginRequest() | |||
| { | |||
| Email = _currentUser.Email, | |||
| Password = password | |||
| }; | |||
| var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false); | |||
| _clientRest.SetToken(loginResponse.Token); | |||
| } | |||
| } | |||
| /// <summary> Returns the role with the specified id, or null if none was found. </summary> | |||
| public Role GetRole(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _roles[id]; | |||
| } | |||
| /// <summary> Returns all roles with the specified server and name. </summary> | |||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||
| public IEnumerable<Role> FindRoles(Server server, string name) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| // if (name.StartsWith("@")) | |||
| // { | |||
| // string name2 = name.Substring(1); | |||
| // return _roles.Where(x => x.Server.Id == server.Id && | |||
| // string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || | |||
| // string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||
| // } | |||
| // else | |||
| // { | |||
| return _roles.Where(x => x.Server.Id == server.Id && | |||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||
| // } | |||
| } | |||
| /// <summary> Note: due to current API limitations, the created role cannot be returned. </summary> | |||
| public async Task<Role> CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| var request1 = new CreateRoleRequest(server.Id); | |||
| var response1 = await _clientRest.Send(request1).ConfigureAwait(false); | |||
| var role = _roles.GetOrAdd(response1.Id, server.Id); | |||
| role.Update(response1); | |||
| var request2 = new UpdateRoleRequest(role.Server.Id, role.Id) | |||
| { | |||
| Name = name, | |||
| Permissions = (permissions ?? role.Permissions).RawValue, | |||
| Color = (color ?? Color.Default).RawValue, | |||
| IsHoisted = isHoisted | |||
| }; | |||
| var response2 = await _clientRest.Send(request2).ConfigureAwait(false); | |||
| role.Update(response2); | |||
| return role; | |||
| } | |||
| public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) | |||
| { | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| var request1 = new UpdateRoleRequest(role.Server.Id, role.Id) | |||
| { | |||
| Name = name ?? role.Name, | |||
| Permissions = (permissions ?? role.Permissions).RawValue, | |||
| Color = (color ?? role.Color).RawValue, | |||
| IsHoisted = isHoisted ?? role.IsHoisted | |||
| }; | |||
| var response = await _clientRest.Send(request1).ConfigureAwait(false); | |||
| if (position != null) | |||
| { | |||
| int oldPos = role.Position; | |||
| int newPos = position.Value; | |||
| int minPos; | |||
| Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray(); | |||
| if (oldPos < newPos) //Moving Down | |||
| { | |||
| minPos = oldPos; | |||
| for (int i = oldPos; i < newPos; i++) | |||
| roles[i] = roles[i + 1]; | |||
| roles[newPos] = role; | |||
| } | |||
| else //(oldPos > newPos) Moving Up | |||
| { | |||
| minPos = newPos; | |||
| for (int i = oldPos; i > newPos; i--) | |||
| roles[i] = roles[i - 1]; | |||
| roles[newPos] = role; | |||
| } | |||
| var request2 = new ReorderRolesRequest(role.Server.Id) | |||
| { | |||
| RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), | |||
| StartPos = minPos | |||
| }; | |||
| await _clientRest.Send(request2).ConfigureAwait(false); | |||
| } | |||
| } | |||
| public async Task DeleteRole(Role role) | |||
| { | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public Task ReorderRoles(Server server, IEnumerable<Role> roles, int startPos = 0) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (roles == null) throw new ArgumentNullException(nameof(roles)); | |||
| if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); | |||
| CheckReady(); | |||
| return _clientRest.Send(new ReorderRolesRequest(server.Id) | |||
| { | |||
| RoleIds = roles.Select(x => x.Id).ToArray(), | |||
| StartPos = startPos | |||
| }); | |||
| } | |||
| /// <summary> Returns the server with the specified id, or null if none was found. </summary> | |||
| public Server GetServer(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _servers[id]; | |||
| } | |||
| /// <summary> Returns all servers with the specified name. </summary> | |||
| /// <remarks> Search is case-insensitive. </remarks> | |||
| public IEnumerable<Server> FindServers(string name) | |||
| { | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||
| } | |||
| /// <summary> Creates a new server with the provided name and region (see Regions). </summary> | |||
| public async Task<Server> CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) | |||
| { | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| if (region == null) throw new ArgumentNullException(nameof(region)); | |||
| CheckReady(); | |||
| var request = new CreateGuildRequest() | |||
| { | |||
| Name = name, | |||
| Region = region.Id, | |||
| IconBase64 = Base64Image(iconType, icon, null) | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var server = _servers.GetOrAdd(response.Id); | |||
| server.Update(response); | |||
| return server; | |||
| } | |||
| /// <summary> Edits the provided server, changing only non-null attributes. </summary> | |||
| public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| var request = new UpdateGuildRequest(server.Id) | |||
| { | |||
| Name = name ?? server.Name, | |||
| Region = region ?? server.Region, | |||
| IconBase64 = Base64Image(iconType, icon, server.IconId), | |||
| AFKChannelId = server.AFKChannel?.Id, | |||
| AFKTimeout = server.AFKTimeout | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| server.Update(response); | |||
| } | |||
| /// <summary> Leaves the provided server, destroying it if you are the owner. </summary> | |||
| public async Task LeaveServer(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task<IEnumerable<Region>> GetVoiceRegions() | |||
| { | |||
| CheckReady(); | |||
| var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); | |||
| return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)); | |||
| } | |||
| public DualChannelPermissions GetChannelPermissions(Channel channel, User user) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return channel.PermissionOverwrites | |||
| .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) | |||
| .Select(x => x.Permissions) | |||
| .FirstOrDefault(); | |||
| } | |||
| public DualChannelPermissions GetChannelPermissions(Channel channel, Role role) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return channel.PermissionOverwrites | |||
| .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) | |||
| .Select(x => x.Permissions) | |||
| .FirstOrDefault(); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); | |||
| } | |||
| private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| var request = new AddChannelPermissionsRequest(channel.Id) | |||
| { | |||
| TargetId = targetId, | |||
| TargetType = targetType.Value, | |||
| Allow = allow?.RawValue ?? 0, | |||
| Deny = deny?.RawValue ?? 0 | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task RemoveChannelPermissions(Channel channel, User user) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User); | |||
| } | |||
| public Task RemoveChannelPermissions(Channel channel, Role role) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role); | |||
| } | |||
| private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType) | |||
| { | |||
| try | |||
| { | |||
| var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); | |||
| await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false); | |||
| } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| }*/ | |||
| } | |||
| @@ -1,105 +0,0 @@ | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| public partial class DiscordClient | |||
| { | |||
| public DualChannelPermissions GetChannelPermissions(Channel channel, User user) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return channel.PermissionOverwrites | |||
| .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) | |||
| .Select(x => x.Permissions) | |||
| .FirstOrDefault(); | |||
| } | |||
| public DualChannelPermissions GetChannelPermissions(Channel channel, Role role) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return channel.PermissionOverwrites | |||
| .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) | |||
| .Select(x => x.Permissions) | |||
| .FirstOrDefault(); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, User user, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, user.Id, PermissionTarget.User, allow, deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, User user, DualChannelPermissions permissions = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, allow, deny); | |||
| } | |||
| public Task SetChannelPermissions(Channel channel, Role role, DualChannelPermissions permissions = null) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return SetChannelPermissions(channel, role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); | |||
| } | |||
| private Task SetChannelPermissions(Channel channel, ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||
| { | |||
| var request = new AddChannelPermissionsRequest(channel.Id) | |||
| { | |||
| TargetId = targetId, | |||
| TargetType = targetType.Value, | |||
| Allow = allow?.RawValue ?? 0, | |||
| Deny = deny?.RawValue ?? 0 | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task RemoveChannelPermissions(Channel channel, User user) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| CheckReady(); | |||
| return RemoveChannelPermissions(channel, user.Id, PermissionTarget.User); | |||
| } | |||
| public Task RemoveChannelPermissions(Channel channel, Role role) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| return RemoveChannelPermissions(channel, role.Id, PermissionTarget.Role); | |||
| } | |||
| private async Task RemoveChannelPermissions(Channel channel, ulong userOrRoleId, PermissionTarget targetType) | |||
| { | |||
| try | |||
| { | |||
| var perms = channel.PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); | |||
| await _clientRest.Send(new RemoveChannelPermissionsRequest(channel.Id, userOrRoleId)).ConfigureAwait(false); | |||
| } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,176 +0,0 @@ | |||
| using Discord.API; | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal sealed class Roles : AsyncCollection<ulong, Role> | |||
| { | |||
| public Roles(DiscordClient client, object writerLock) | |||
| : base(client, writerLock) { } | |||
| public Role GetOrAdd(ulong id, ulong serverId) | |||
| => GetOrAdd(id, () => new Role(_client, id, serverId)); | |||
| } | |||
| public class RoleEventArgs : EventArgs | |||
| { | |||
| public Role Role { get; } | |||
| public Server Server => Role.Server; | |||
| public RoleEventArgs(Role role) { Role = role; } | |||
| } | |||
| public partial class DiscordClient | |||
| { | |||
| public event EventHandler<RoleEventArgs> RoleCreated; | |||
| private void RaiseRoleCreated(Role role) | |||
| { | |||
| if (RoleCreated != null) | |||
| EventHelper.Raise(_logger, nameof(RoleCreated), () => RoleCreated(this, new RoleEventArgs(role))); | |||
| } | |||
| public event EventHandler<RoleEventArgs> RoleUpdated; | |||
| private void RaiseRoleDeleted(Role role) | |||
| { | |||
| if (RoleDeleted != null) | |||
| EventHelper.Raise(_logger, nameof(RoleDeleted), () => RoleDeleted(this, new RoleEventArgs(role))); | |||
| } | |||
| public event EventHandler<RoleEventArgs> RoleDeleted; | |||
| private void RaiseRoleUpdated(Role role) | |||
| { | |||
| if (RoleUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(RoleUpdated), () => RoleUpdated(this, new RoleEventArgs(role))); | |||
| } | |||
| internal Roles Roles => _roles; | |||
| private readonly Roles _roles; | |||
| /// <summary> Returns the role with the specified id, or null if none was found. </summary> | |||
| public Role GetRole(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _roles[id]; | |||
| } | |||
| /// <summary> Returns all roles with the specified server and name. </summary> | |||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive. </remarks> | |||
| public IEnumerable<Role> FindRoles(Server server, string name) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| /*if (name.StartsWith("@")) | |||
| { | |||
| string name2 = name.Substring(1); | |||
| return _roles.Where(x => x.Server.Id == server.Id && | |||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || | |||
| string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||
| } | |||
| else | |||
| {*/ | |||
| return _roles.Where(x => x.Server.Id == server.Id && | |||
| string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||
| //} | |||
| } | |||
| /// <summary> Note: due to current API limitations, the created role cannot be returned. </summary> | |||
| public async Task<Role> CreateRole(Server server, string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| var request1 = new CreateRoleRequest(server.Id); | |||
| var response1 = await _clientRest.Send(request1).ConfigureAwait(false); | |||
| var role = _roles.GetOrAdd(response1.Id, server.Id); | |||
| role.Update(response1); | |||
| var request2 = new UpdateRoleRequest(role.Server.Id, role.Id) | |||
| { | |||
| Name = name, | |||
| Permissions = (permissions ?? role.Permissions).RawValue, | |||
| Color = (color ?? Color.Default).RawValue, | |||
| IsHoisted = isHoisted | |||
| }; | |||
| var response2 = await _clientRest.Send(request2).ConfigureAwait(false); | |||
| role.Update(response2); | |||
| return role; | |||
| } | |||
| public async Task EditRole(Role role, string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) | |||
| { | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| var request1 = new UpdateRoleRequest(role.Server.Id, role.Id) | |||
| { | |||
| Name = name ?? role.Name, | |||
| Permissions = (permissions ?? role.Permissions).RawValue, | |||
| Color = (color ?? role.Color).RawValue, | |||
| IsHoisted = isHoisted ?? role.IsHoisted | |||
| }; | |||
| var response = await _clientRest.Send(request1).ConfigureAwait(false); | |||
| if (position != null) | |||
| { | |||
| int oldPos = role.Position; | |||
| int newPos = position.Value; | |||
| int minPos; | |||
| Role[] roles = role.Server.Roles.OrderBy(x => x.Position).ToArray(); | |||
| if (oldPos < newPos) //Moving Down | |||
| { | |||
| minPos = oldPos; | |||
| for (int i = oldPos; i < newPos; i++) | |||
| roles[i] = roles[i + 1]; | |||
| roles[newPos] = role; | |||
| } | |||
| else //(oldPos > newPos) Moving Up | |||
| { | |||
| minPos = newPos; | |||
| for (int i = oldPos; i > newPos; i--) | |||
| roles[i] = roles[i - 1]; | |||
| roles[newPos] = role; | |||
| } | |||
| var request2 = new ReorderRolesRequest(role.Server.Id) | |||
| { | |||
| RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), | |||
| StartPos = minPos | |||
| }; | |||
| await _clientRest.Send(request2).ConfigureAwait(false); | |||
| } | |||
| } | |||
| public async Task DeleteRole(Role role) | |||
| { | |||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new DeleteRoleRequest(role.Server.Id, role.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public Task ReorderRoles(Server server, IEnumerable<Role> roles, int startPos = 0) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (roles == null) throw new ArgumentNullException(nameof(roles)); | |||
| if (startPos < 0) throw new ArgumentOutOfRangeException(nameof(startPos), "startPos must be a positive integer."); | |||
| CheckReady(); | |||
| return _clientRest.Send(new ReorderRolesRequest(server.Id) | |||
| { | |||
| RoleIds = roles.Select(x => x.Id).ToArray(), | |||
| StartPos = startPos | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,140 +0,0 @@ | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal sealed class Servers : AsyncCollection<ulong, Server> | |||
| { | |||
| public Servers(DiscordClient client, object writerLock) | |||
| : base(client, writerLock) { } | |||
| public Server GetOrAdd(ulong id) | |||
| => GetOrAdd(id, () => new Server(_client, id)); | |||
| } | |||
| public class ServerEventArgs : EventArgs | |||
| { | |||
| public Server Server { get; } | |||
| public ServerEventArgs(Server server) { Server = server; } | |||
| } | |||
| public partial class DiscordClient | |||
| { | |||
| public event EventHandler<ServerEventArgs> JoinedServer; | |||
| private void RaiseJoinedServer(Server server) | |||
| { | |||
| if (JoinedServer != null) | |||
| EventHelper.Raise(_logger, nameof(JoinedServer), () => JoinedServer(this, new ServerEventArgs(server))); | |||
| } | |||
| public event EventHandler<ServerEventArgs> LeftServer; | |||
| private void RaiseLeftServer(Server server) | |||
| { | |||
| if (LeftServer != null) | |||
| EventHelper.Raise(_logger, nameof(LeftServer), () => LeftServer(this, new ServerEventArgs(server))); | |||
| } | |||
| public event EventHandler<ServerEventArgs> ServerUpdated; | |||
| private void RaiseServerUpdated(Server server) | |||
| { | |||
| if (ServerUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(ServerUpdated), () => ServerUpdated(this, new ServerEventArgs(server))); | |||
| } | |||
| public event EventHandler<ServerEventArgs> ServerUnavailable; | |||
| private void RaiseServerUnavailable(Server server) | |||
| { | |||
| if (ServerUnavailable != null) | |||
| EventHelper.Raise(_logger, nameof(ServerUnavailable), () => ServerUnavailable(this, new ServerEventArgs(server))); | |||
| } | |||
| public event EventHandler<ServerEventArgs> ServerAvailable; | |||
| private void RaiseServerAvailable(Server server) | |||
| { | |||
| if (ServerAvailable != null) | |||
| EventHelper.Raise(_logger, nameof(ServerAvailable), () => ServerAvailable(this, new ServerEventArgs(server))); | |||
| } | |||
| /// <summary> Returns a collection of all servers this client is a member of. </summary> | |||
| public IEnumerable<Server> AllServers { get { CheckReady(); return _servers; } } | |||
| internal Servers Servers => _servers; | |||
| private readonly Servers _servers; | |||
| /// <summary> Returns the server with the specified id, or null if none was found. </summary> | |||
| public Server GetServer(ulong id) | |||
| { | |||
| CheckReady(); | |||
| return _servers[id]; | |||
| } | |||
| /// <summary> Returns all servers with the specified name. </summary> | |||
| /// <remarks> Search is case-insensitive. </remarks> | |||
| public IEnumerable<Server> FindServers(string name) | |||
| { | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return _servers.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||
| } | |||
| /// <summary> Creates a new server with the provided name and region (see Regions). </summary> | |||
| public async Task<Server> CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) | |||
| { | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| if (region == null) throw new ArgumentNullException(nameof(region)); | |||
| CheckReady(); | |||
| var request = new CreateGuildRequest() | |||
| { | |||
| Name = name, | |||
| Region = region.Id, | |||
| IconBase64 = Base64Image(iconType, icon, null) | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| var server = _servers.GetOrAdd(response.Id); | |||
| server.Update(response); | |||
| return server; | |||
| } | |||
| /// <summary> Edits the provided server, changing only non-null attributes. </summary> | |||
| public async Task EditServer(Server server, string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| var request = new UpdateGuildRequest(server.Id) | |||
| { | |||
| Name = name ?? server.Name, | |||
| Region = region ?? server.Region, | |||
| IconBase64 = Base64Image(iconType, icon, server.IconId), | |||
| AFKChannelId = server.AFKChannel?.Id, | |||
| AFKTimeout = server.AFKTimeout | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| server.Update(response); | |||
| } | |||
| /// <summary> Leaves the provided server, destroying it if you are the owner. </summary> | |||
| public async Task LeaveServer(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new LeaveGuildRequest(server.Id)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task<IEnumerable<Region>> GetVoiceRegions() | |||
| { | |||
| CheckReady(); | |||
| var regions = await _clientRest.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); | |||
| return regions.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port)); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,329 +0,0 @@ | |||
| using Discord.API.Client.Rest; | |||
| using Discord.Net; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Net; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| internal sealed class GlobalUsers : AsyncCollection<ulong, GlobalUser> | |||
| { | |||
| public GlobalUsers(DiscordClient client, object writerLock) | |||
| : base(client, writerLock) { } | |||
| public GlobalUser GetOrAdd(ulong id) => GetOrAdd(id, () => new GlobalUser(_client, id)); | |||
| } | |||
| internal sealed class Users : AsyncCollection<User.CompositeKey, User> | |||
| { | |||
| public Users(DiscordClient client, object writerLock) | |||
| : base(client, writerLock) | |||
| { } | |||
| public User this[ulong userId, ulong? serverId] | |||
| => base[new User.CompositeKey(userId, serverId)]; | |||
| public User GetOrAdd(ulong userId, ulong? serverId) | |||
| => GetOrAdd(new User.CompositeKey(userId, serverId), () => new User(_client, userId, serverId)); | |||
| public User TryRemove(ulong userId, ulong? serverId) | |||
| => TryRemove(new User.CompositeKey(userId, serverId)); | |||
| } | |||
| public class UserEventArgs : EventArgs | |||
| { | |||
| public User User { get; } | |||
| public Server Server => User.Server; | |||
| public UserEventArgs(User user) { User = user; } | |||
| } | |||
| public class UserChannelEventArgs : UserEventArgs | |||
| { | |||
| public Channel Channel { get; } | |||
| public UserChannelEventArgs(User user, Channel channel) | |||
| : base(user) | |||
| { | |||
| Channel = channel; | |||
| } | |||
| } | |||
| public class BanEventArgs : EventArgs | |||
| { | |||
| public ulong UserId { get; } | |||
| public Server Server { get; } | |||
| public BanEventArgs(ulong userId, Server server) | |||
| { | |||
| UserId = userId; | |||
| Server = server; | |||
| } | |||
| } | |||
| public partial class DiscordClient : IDisposable | |||
| { | |||
| public event EventHandler<UserEventArgs> UserJoined; | |||
| private void RaiseUserJoined(User user) | |||
| { | |||
| if (UserJoined != null) | |||
| EventHelper.Raise(_logger, nameof(UserJoined), () => UserJoined(this, new UserEventArgs(user))); | |||
| } | |||
| public event EventHandler<UserEventArgs> UserLeft; | |||
| private void RaiseUserLeft(User user) | |||
| { | |||
| if (UserLeft != null) | |||
| EventHelper.Raise(_logger, nameof(UserLeft), () => UserLeft(this, new UserEventArgs(user))); | |||
| } | |||
| public event EventHandler<UserEventArgs> UserUpdated; | |||
| private void RaiseUserUpdated(User user) | |||
| { | |||
| if (UserUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(UserUpdated), () => UserUpdated(this, new UserEventArgs(user))); | |||
| } | |||
| public event EventHandler<UserEventArgs> UserPresenceUpdated; | |||
| private void RaiseUserPresenceUpdated(User user) | |||
| { | |||
| if (UserPresenceUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(UserPresenceUpdated), () => UserPresenceUpdated(this, new UserEventArgs(user))); | |||
| } | |||
| public event EventHandler<UserEventArgs> UserVoiceStateUpdated; | |||
| private void RaiseUserVoiceStateUpdated(User user) | |||
| { | |||
| if (UserVoiceStateUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(UserVoiceStateUpdated), () => UserVoiceStateUpdated(this, new UserEventArgs(user))); | |||
| } | |||
| public event EventHandler<UserChannelEventArgs> UserIsTypingUpdated; | |||
| private void RaiseUserIsTyping(User user, Channel channel) | |||
| { | |||
| if (UserIsTypingUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(UserIsTypingUpdated), () => UserIsTypingUpdated(this, new UserChannelEventArgs(user, channel))); | |||
| } | |||
| public event EventHandler ProfileUpdated; | |||
| private void RaiseProfileUpdated() | |||
| { | |||
| if (ProfileUpdated != null) | |||
| EventHelper.Raise(_logger, nameof(ProfileUpdated), () => ProfileUpdated(this, EventArgs.Empty)); | |||
| } | |||
| public event EventHandler<BanEventArgs> UserBanned; | |||
| private void RaiseUserBanned(ulong userId, Server server) | |||
| { | |||
| if (UserBanned != null) | |||
| EventHelper.Raise(_logger, nameof(UserBanned), () => UserBanned(this, new BanEventArgs(userId, server))); | |||
| } | |||
| public event EventHandler<BanEventArgs> UserUnbanned; | |||
| private void RaiseUserUnbanned(ulong userId, Server server) | |||
| { | |||
| if (UserUnbanned != null) | |||
| EventHelper.Raise(_logger, nameof(UserUnbanned), () => UserUnbanned(this, new BanEventArgs(userId, server))); | |||
| } | |||
| /// <summary> Returns the current logged-in user used in private channels. </summary> | |||
| internal User PrivateUser => _privateUser; | |||
| private User _privateUser; | |||
| /// <summary> Returns information about the currently logged-in account. </summary> | |||
| public GlobalUser CurrentUser => _currentUser; | |||
| private GlobalUser _currentUser; | |||
| /// <summary> Returns a collection of all unique users this client can currently see. </summary> | |||
| public IEnumerable<GlobalUser> AllUsers { get { CheckReady(); return _globalUsers; } } | |||
| internal GlobalUsers GlobalUsers => _globalUsers; | |||
| private readonly GlobalUsers _globalUsers; | |||
| internal Users Users => _users; | |||
| private readonly Users _users; | |||
| public GlobalUser GetUser(ulong userId) | |||
| { | |||
| CheckReady(); | |||
| return _globalUsers[userId]; | |||
| } | |||
| /// <summary> Returns the user with the specified id, along with their server-specific data, or null if none was found. </summary> | |||
| public User GetUser(Server server, ulong userId) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| CheckReady(); | |||
| return _users[userId, server.Id]; | |||
| } | |||
| /// <summary> Returns the user with the specified name and discriminator, along withtheir server-specific data, or null if they couldn't be found. </summary> | |||
| public User GetUser(Server server, string username, ushort discriminator) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (username == null) throw new ArgumentNullException(nameof(username)); | |||
| CheckReady(); | |||
| return FindUsers(server.Members, server.Id, username, discriminator, true).FirstOrDefault(); | |||
| } | |||
| /// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary> | |||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<User> FindUsers(Server server, string name, bool exactMatch = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return FindUsers(server.Members, server.Id, name, exactMatch: exactMatch); | |||
| } | |||
| /// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary> | |||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||
| public IEnumerable<User> FindUsers(Channel channel, string name, bool exactMatch = false) | |||
| { | |||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||
| CheckReady(); | |||
| return FindUsers(channel.Members, channel.IsPrivate ? (ulong?)null : channel.Server.Id, name, exactMatch: exactMatch); | |||
| } | |||
| private IEnumerable<User> FindUsers(IEnumerable<User> users, ulong? serverId, string name, ushort? discriminator = null, bool exactMatch = false) | |||
| { | |||
| var query = users.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||
| if (!exactMatch && name.Length >= 2) | |||
| { | |||
| if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention | |||
| { | |||
| ulong id = IdConvert.ToLong(name.Substring(2, name.Length - 3)); | |||
| var user = _users[id, serverId]; | |||
| if (user != null) | |||
| query = query.Concat(new User[] { user }); | |||
| } | |||
| else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention | |||
| { | |||
| string name2 = name.Substring(1); | |||
| query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||
| } | |||
| } | |||
| if (discriminator != null) | |||
| query = query.Where(x => x.Discriminator == discriminator.Value); | |||
| return query; | |||
| } | |||
| public Task EditUser(User user, bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to edit users in a private channel"); | |||
| CheckReady(); | |||
| //Modify the roles collection and filter out the everyone role | |||
| var roleIds = roles == null ? null : user.Roles.Where(x => !x.IsEveryone) .Select(x => x.Id); | |||
| var request = new UpdateMemberRequest(user.Server.Id, user.Id) | |||
| { | |||
| IsMuted = isMuted ?? user.IsServerMuted, | |||
| IsDeafened = isDeafened ?? user.IsServerDeafened, | |||
| VoiceChannelId = voiceChannel?.Id, | |||
| RoleIds = roleIds.ToArray() | |||
| }; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task KickUser(User user) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to kick users from a private channel"); | |||
| CheckReady(); | |||
| var request = new KickMemberRequest(user.Server.Id, user.Id); | |||
| return _clientRest.Send(request); | |||
| } | |||
| public Task BanUser(User user, int pruneDays = 0) | |||
| { | |||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||
| if (user.IsPrivate) throw new InvalidOperationException("Unable to ban users from a private channel"); | |||
| CheckReady(); | |||
| var request = new AddGuildBanRequest(user.Server.Id, user.Id); | |||
| request.PruneDays = pruneDays; | |||
| return _clientRest.Send(request); | |||
| } | |||
| public async Task UnbanUser(Server server, ulong userId) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); | |||
| CheckReady(); | |||
| try { await _clientRest.Send(new RemoveGuildBanRequest(server.Id, userId)).ConfigureAwait(false); } | |||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||
| } | |||
| public async Task<int> PruneUsers(Server server, int days, bool simulate = false) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); | |||
| CheckReady(); | |||
| var request = new PruneMembersRequest(server.Id) | |||
| { | |||
| Days = days, | |||
| IsSimulation = simulate | |||
| }; | |||
| var response = await _clientRest.Send(request).ConfigureAwait(false); | |||
| return response.Pruned; | |||
| } | |||
| /// <summary>When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for a particular server.</summary> | |||
| public void RequestOfflineUsers(Server server) | |||
| { | |||
| if (server == null) throw new ArgumentNullException(nameof(server)); | |||
| _webSocket.SendRequestMembers(server.Id, "", 0); | |||
| } | |||
| public async Task EditProfile(string currentPassword = "", | |||
| string username = null, string email = null, string password = null, | |||
| Stream avatar = null, ImageType avatarType = ImageType.Png) | |||
| { | |||
| if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); | |||
| CheckReady(); | |||
| var request = new UpdateProfileRequest() | |||
| { | |||
| CurrentPassword = currentPassword, | |||
| Email = email ?? _currentUser?.Email, | |||
| Password = password, | |||
| Username = username ?? _privateUser?.Name, | |||
| AvatarBase64 = Base64Image(avatarType, avatar, _privateUser?.AvatarId) | |||
| }; | |||
| await _clientRest.Send(request).ConfigureAwait(false); | |||
| if (password != null) | |||
| { | |||
| var loginRequest = new LoginRequest() | |||
| { | |||
| Email = _currentUser.Email, | |||
| Password = password | |||
| }; | |||
| var loginResponse = await _clientRest.Send(loginRequest).ConfigureAwait(false); | |||
| _clientRest.SetToken(loginResponse.Token); | |||
| } | |||
| } | |||
| public Task SetStatus(UserStatus status) | |||
| { | |||
| if (status == null) throw new ArgumentNullException(nameof(status)); | |||
| if (status != UserStatus.Online && status != UserStatus.Idle) | |||
| throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | |||
| CheckReady(); | |||
| _status = status; | |||
| return SendStatus(); | |||
| } | |||
| public Task SetGame(int? gameId) | |||
| { | |||
| CheckReady(); | |||
| _gameId = gameId; | |||
| return SendStatus(); | |||
| } | |||
| private Task SendStatus() | |||
| { | |||
| _webSocket.SendUpdateStatus(_status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, _gameId); | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -14,8 +14,8 @@ namespace Discord | |||
| Debug = 5 | |||
| } | |||
| public abstract class BaseConfig<T> | |||
| where T : BaseConfig<T> | |||
| public abstract class Config<T> | |||
| where T : Config<T> | |||
| { | |||
| protected bool _isLocked; | |||
| protected internal void Lock() { _isLocked = true; } | |||
| @@ -34,20 +34,22 @@ namespace Discord | |||
| } | |||
| } | |||
| public class DiscordConfig : BaseConfig<DiscordConfig> | |||
| { | |||
| public static string LibName => "Discord.Net"; | |||
| public class DiscordConfig : Config<DiscordConfig> | |||
| { | |||
| public const int MaxMessageSize = 2000; | |||
| public const string LibName = "Discord.Net"; | |||
| public static string LibVersion => typeof(DiscordClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); | |||
| public static string LibUrl => "https://github.com/RogueException/Discord.Net"; | |||
| public const string LibUrl = "https://github.com/RogueException/Discord.Net"; | |||
| public static string ClientAPIUrl => "https://discordapp.com/api/"; | |||
| public static string StatusAPIUrl => "https://status.discordapp.com/api/v2/"; | |||
| public static string CDNUrl => "https://cdn.discordapp.com/"; | |||
| public static string InviteUrl => "https://discord.gg/"; | |||
| public const string ClientAPIUrl = "https://discordapp.com/api/"; | |||
| public const string StatusAPIUrl = "https://status.discordapp.com/api/v2/"; | |||
| public const string CDNUrl = "https://cdn.discordapp.com/"; | |||
| public const string InviteUrl = "https://discord.gg/"; | |||
| //Global | |||
| /// <summary> Name of your application. </summary> | |||
| /// <summary> Name of your application. This is used both for the token cache directory and user agent. </summary> | |||
| public string AppName { get { return _appName; } set { SetValue(ref _appName, value); UpdateUserAgent(); } } | |||
| private string _appName = null; | |||
| /// <summary> Version of your application. </summary> | |||
| @@ -0,0 +1,10 @@ | |||
| namespace Discord | |||
| { | |||
| public enum ConnectionState : byte | |||
| { | |||
| Disconnected, | |||
| Connecting, | |||
| Connected, | |||
| Disconnecting | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class ChannelEventArgs : EventArgs | |||
| { | |||
| public Channel Channel { get; } | |||
| public Server Server => Channel.Server; | |||
| public ChannelEventArgs(Channel channel) { Channel = channel; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| namespace Discord | |||
| { | |||
| public class ChannelUserEventArgs | |||
| { | |||
| public Channel Channel { get; } | |||
| public User User { get; } | |||
| public ChannelUserEventArgs(Channel channel, User user) | |||
| { | |||
| Channel = channel; | |||
| User = user; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class DisconnectedEventArgs : EventArgs | |||
| { | |||
| public bool WasUnexpected { get; } | |||
| public Exception Exception { get; } | |||
| public DisconnectedEventArgs(bool wasUnexpected, Exception ex) | |||
| { | |||
| WasUnexpected = wasUnexpected; | |||
| Exception = ex; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public sealed class LogMessageEventArgs : EventArgs | |||
| { | |||
| public LogSeverity Severity { get; } | |||
| public string Source { get; } | |||
| public string Message { get; } | |||
| public Exception Exception { get; } | |||
| public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) | |||
| { | |||
| Severity = severity; | |||
| Source = source; | |||
| Message = msg; | |||
| Exception = exception; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class MessageEventArgs : EventArgs | |||
| { | |||
| public Message Message { get; } | |||
| public User User => Message.User; | |||
| public Channel Channel => Message.Channel; | |||
| public Server Server => Message.Server; | |||
| public MessageEventArgs(Message msg) { Message = msg; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class ProfileEventArgs : EventArgs | |||
| { | |||
| public Profile Profile { get; } | |||
| public ProfileEventArgs(Profile profile) | |||
| { | |||
| Profile = profile; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class RoleEventArgs : EventArgs | |||
| { | |||
| public Role Role { get; } | |||
| public Server Server => Role.Server; | |||
| public RoleEventArgs(Role role) { Role = role; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class ServerEventArgs : EventArgs | |||
| { | |||
| public Server Server { get; } | |||
| public ServerEventArgs(Server server) { Server = server; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class UserEventArgs : EventArgs | |||
| { | |||
| public User User { get; } | |||
| public Server Server => User.Server; | |||
| public UserEventArgs(User user) { User = user; } | |||
| } | |||
| } | |||
| @@ -10,11 +10,11 @@ namespace Discord | |||
| static Format() | |||
| { | |||
| _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; | |||
| _builder = new StringBuilder(DiscordClient.MaxMessageSize); | |||
| _builder = new StringBuilder(DiscordConfig.MaxMessageSize); | |||
| } | |||
| /// <summary> Removes all special formatting characters from the provided text. </summary> | |||
| private static string Escape(string text) | |||
| public static string Escape(string text) | |||
| { | |||
| lock (_builder) | |||
| { | |||
| @@ -84,10 +84,7 @@ namespace Discord | |||
| } | |||
| return -1; | |||
| } | |||
| /// <summary> Returns a markdown-formatted string with no formatting, optionally escaping the contents. </summary> | |||
| public static string Normal(string text, bool escape = true) | |||
| => escape ? Escape(text) : text; | |||
| /// <summary> Returns a markdown-formatted string with bold formatting, optionally escaping the contents. </summary> | |||
| public static string Bold(string text, bool escape = true) | |||
| => escape ? $"**{Escape(text)}**" : $"**{text}**"; | |||
| @@ -109,20 +106,5 @@ namespace Discord | |||
| else | |||
| return $"`{text}`"; | |||
| } | |||
| /// <summary> Returns a markdown-formatted string with multiple formatting, optionally escaping the contents. </summary> | |||
| public static string Multiple(string text, bool escape = true, | |||
| bool bold = false, bool italics = false, bool underline = false, bool strikeout = false, | |||
| bool code = false, string codeLanguage = null) | |||
| { | |||
| string result = text; | |||
| if (escape) result = Escape(result); | |||
| if (bold) result = Bold(result, false); | |||
| if (italics) result = Italics(result, false); | |||
| if (underline) result = Underline(result, false); | |||
| if (strikeout) result = Strikeout(result, false); | |||
| if (code) result = Code(result, codeLanguage); | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,166 +0,0 @@ | |||
| using System; | |||
| using System.Collections; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| internal abstract class AsyncCollection<TKey, TValue> : IEnumerable<TValue> | |||
| where TKey : struct, IEquatable<TKey> | |||
| where TValue : CachedObject | |||
| { | |||
| private readonly object _writerLock; | |||
| public class CollectionItemEventArgs : EventArgs | |||
| { | |||
| public TValue Item { get; } | |||
| public CollectionItemEventArgs(TValue item) { Item = item; } | |||
| } | |||
| public class CollectionItemRemappedEventArgs : EventArgs | |||
| { | |||
| public TValue Item { get; } | |||
| public TKey OldId { get; } | |||
| public TKey NewId { get; } | |||
| public CollectionItemRemappedEventArgs(TValue item, TKey oldId, TKey newId) { Item = item; OldId = oldId; NewId = newId; } | |||
| } | |||
| public EventHandler<CollectionItemEventArgs> ItemCreated; | |||
| private void RaiseItemCreated(TValue item) | |||
| { | |||
| if (ItemCreated != null) | |||
| ItemCreated(this, new CollectionItemEventArgs(item)); | |||
| } | |||
| public EventHandler<CollectionItemEventArgs> ItemDestroyed; | |||
| private void RaiseItemDestroyed(TValue item) | |||
| { | |||
| if (ItemDestroyed != null) | |||
| ItemDestroyed(this, new CollectionItemEventArgs(item)); | |||
| } | |||
| public EventHandler<CollectionItemRemappedEventArgs> ItemRemapped; | |||
| private void RaiseItemRemapped(TValue item, TKey oldId, TKey newId) | |||
| { | |||
| if (ItemRemapped != null) | |||
| ItemRemapped(this, new CollectionItemRemappedEventArgs(item, oldId, newId)); | |||
| } | |||
| public EventHandler Cleared; | |||
| private void RaiseCleared() | |||
| { | |||
| if (Cleared != null) | |||
| Cleared(this, EventArgs.Empty); | |||
| } | |||
| protected readonly DiscordClient _client; | |||
| protected readonly ConcurrentDictionary<TKey, TValue> _dictionary; | |||
| public int Count => _dictionary.Count; | |||
| protected AsyncCollection(DiscordClient client, object writerLock) | |||
| { | |||
| _client = client; | |||
| _writerLock = writerLock; | |||
| _dictionary = new ConcurrentDictionary<TKey, TValue>(); | |||
| } | |||
| public TValue this[TKey? key] | |||
| => key == null ? null : this[key.Value]; | |||
| public TValue this[TKey key] | |||
| { | |||
| get | |||
| { | |||
| if (key.Equals(default(TKey))) | |||
| return null; | |||
| TValue result; | |||
| if (!_dictionary.TryGetValue(key, out result)) | |||
| return null; | |||
| return result; | |||
| } | |||
| } | |||
| protected TValue GetOrAdd(TKey key, Func<TValue> createFunc) | |||
| { | |||
| TValue result; | |||
| if (_dictionary.TryGetValue(key, out result)) | |||
| return result; | |||
| lock (_writerLock) | |||
| { | |||
| if (!_dictionary.ContainsKey(key)) | |||
| { | |||
| result = createFunc(); | |||
| if (result.Cache()) | |||
| { | |||
| _dictionary.TryAdd(key, result); | |||
| RaiseItemCreated(result); | |||
| } | |||
| else | |||
| result.Uncache(); | |||
| return result; | |||
| } | |||
| else | |||
| return _dictionary[key]; | |||
| } | |||
| } | |||
| protected void Import(IEnumerable<KeyValuePair<TKey, TValue>> items) | |||
| { | |||
| lock (_writerLock) | |||
| { | |||
| foreach (var pair in items) | |||
| { | |||
| var value = pair.Value; | |||
| if (value.Cache()) | |||
| { | |||
| _dictionary.TryAdd(pair.Key, value); | |||
| RaiseItemCreated(value); | |||
| } | |||
| else | |||
| value.Uncache(); | |||
| } | |||
| } | |||
| } | |||
| public TValue TryRemove(TKey key) | |||
| { | |||
| if (_dictionary.ContainsKey(key)) | |||
| { | |||
| lock (_writerLock) | |||
| { | |||
| TValue result; | |||
| if (_dictionary.TryRemove(key, out result)) | |||
| { | |||
| result.Uncache(); //TODO: If this object is accessed before OnRemoved finished firing, properties such as Server.Channels will have null elements | |||
| return result; | |||
| } | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| public void Clear() | |||
| { | |||
| lock (_writerLock) | |||
| { | |||
| _dictionary.Clear(); | |||
| RaiseCleared(); | |||
| } | |||
| } | |||
| public TValue Remap(TKey oldKey, TKey newKey) | |||
| { | |||
| if (_dictionary.ContainsKey(oldKey)) | |||
| { | |||
| lock (_writerLock) | |||
| { | |||
| TValue result; | |||
| if (_dictionary.TryRemove(oldKey, out result)) | |||
| _dictionary[newKey] = result; | |||
| return result; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| public IEnumerator<TValue> GetEnumerator() => _dictionary.Select(x => x.Value).GetEnumerator(); | |||
| IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |||
| } | |||
| } | |||
| @@ -1,14 +0,0 @@ | |||
| namespace Discord | |||
| { | |||
| internal static class BitHelper | |||
| { | |||
| public static bool GetBit(uint value, int pos) => ((value >> (byte)pos) & 1U) == 1; | |||
| public static void SetBit(ref uint value, int pos, bool bitValue) | |||
| { | |||
| if (bitValue) | |||
| value |= (1U << pos); | |||
| else | |||
| value &= ~(1U << pos); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,54 +0,0 @@ | |||
| using System.Globalization; | |||
| namespace Discord | |||
| { | |||
| public abstract class CachedObject<TKey> : CachedObject | |||
| { | |||
| private TKey _id; | |||
| internal CachedObject(DiscordClient client, TKey id) | |||
| : base(client) | |||
| { | |||
| _id = id; | |||
| } | |||
| /// <summary> Returns the unique identifier for this object. </summary> | |||
| public TKey Id { get { return _id; } internal set { _id = value; } } | |||
| public override string ToString() => $"{this.GetType().Name} {Id}"; | |||
| } | |||
| public abstract class CachedObject | |||
| { | |||
| protected readonly DiscordClient _client; | |||
| private bool _isCached; | |||
| internal DiscordClient Client => _client; | |||
| internal bool IsCached => _isCached; | |||
| internal CachedObject(DiscordClient client) | |||
| { | |||
| _client = client; | |||
| } | |||
| internal bool Cache() | |||
| { | |||
| if (LoadReferences()) | |||
| { | |||
| _isCached = true; | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| internal void Uncache() | |||
| { | |||
| if (_isCached) | |||
| { | |||
| UnloadReferences(); | |||
| _isCached = false; | |||
| } | |||
| } | |||
| internal abstract bool LoadReferences(); | |||
| internal abstract void UnloadReferences(); | |||
| } | |||
| } | |||
| @@ -1,30 +0,0 @@ | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| namespace Discord | |||
| { | |||
| public enum EditMode : byte | |||
| { | |||
| Set, | |||
| Add, | |||
| Remove | |||
| } | |||
| internal static class Extensions | |||
| { | |||
| public static IEnumerable<T> Modify<T>(this IEnumerable<T> original, IEnumerable<T> modified, EditMode mode) | |||
| { | |||
| if (original == null) return null; | |||
| switch (mode) | |||
| { | |||
| case EditMode.Set: | |||
| default: | |||
| return modified; | |||
| case EditMode.Add: | |||
| return original.Concat(modified); | |||
| case EditMode.Remove: | |||
| return original.Except(modified); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,70 +0,0 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| internal class Reference<T> | |||
| where T : CachedObject<ulong> | |||
| { | |||
| private Action<T> _onCache, _onUncache; | |||
| private Func<ulong, T> _getItem; | |||
| private ulong? _id; | |||
| public ulong? Id | |||
| { | |||
| get { return _id; } | |||
| set | |||
| { | |||
| _id = value; | |||
| _value = null; | |||
| } | |||
| } | |||
| private T _value; | |||
| public T Value | |||
| { | |||
| get | |||
| { | |||
| var v = _value; //A little trickery to make this threadsafe | |||
| var id = _id; | |||
| if (v != null && !_value.IsCached) | |||
| { | |||
| v = null; | |||
| _value = null; | |||
| } | |||
| if (v == null && id != null) | |||
| { | |||
| v = _getItem(id.Value); | |||
| if (v != null && _onCache != null) | |||
| _onCache(v); | |||
| _value = v; | |||
| } | |||
| return v; | |||
| } | |||
| } | |||
| public bool Load() | |||
| { | |||
| return Value != null; //Used for precaching | |||
| } | |||
| public void Unload() | |||
| { | |||
| if (_onUncache != null) | |||
| { | |||
| var v = _value; | |||
| if (v != null && _onUncache != null) | |||
| _onUncache(v); | |||
| } | |||
| } | |||
| public Reference(Func<ulong, T> onUpdate, Action<T> onCache = null, Action<T> onUncache = null) | |||
| : this(null, onUpdate, onCache, onUncache) { } | |||
| public Reference(ulong? id, Func<ulong, T> getItem, Action<T> onCache = null, Action<T> onUncache = null) | |||
| { | |||
| _id = id; | |||
| _getItem = getItem; | |||
| _onCache = onCache; | |||
| _onUncache = onUncache; | |||
| _value = null; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| using System; | |||
| namespace Discord.Logging | |||
| { | |||
| public class LogManager | |||
| { | |||
| private readonly DiscordClient _client; | |||
| public LogSeverity Level { get; } | |||
| public event EventHandler<LogMessageEventArgs> Message = delegate { }; | |||
| internal LogManager(DiscordClient client) | |||
| { | |||
| _client = client; | |||
| Level = client.Config.LogLevel; | |||
| } | |||
| public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null) | |||
| { | |||
| if (severity <= Level) | |||
| { | |||
| try { Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } | |||
| catch { } //We dont want to log on log errors | |||
| } | |||
| } | |||
| public void Log(LogSeverity severity, string source, string message, Exception exception = null) | |||
| { | |||
| if (severity <= Level) | |||
| { | |||
| try { Message(this, new LogMessageEventArgs(severity, source, message, exception)); } | |||
| catch { } //We dont want to log on log errors | |||
| } | |||
| } | |||
| public void Error(string source, string message, Exception ex = null) | |||
| => Log(LogSeverity.Error, source, message, ex); | |||
| public void Error(string source, FormattableString message, Exception ex = null) | |||
| => Log(LogSeverity.Error, source, message, ex); | |||
| public void Warning(string source, string message, Exception ex = null) | |||
| => Log(LogSeverity.Warning, source, message, ex); | |||
| public void Warning(string source, FormattableString message, Exception ex = null) | |||
| => Log(LogSeverity.Warning, source, message, ex); | |||
| public void Info(string source, string message, Exception ex = null) | |||
| => Log(LogSeverity.Info, source, message, ex); | |||
| public void Info(string source, FormattableString message, Exception ex = null) | |||
| => Log(LogSeverity.Info, source, message, ex); | |||
| public void Verbose(string source, string message, Exception ex = null) | |||
| => Log(LogSeverity.Verbose, source, message, ex); | |||
| public void Verbose(string source, FormattableString message, Exception ex = null) | |||
| => Log(LogSeverity.Verbose, source, message, ex); | |||
| public void Debug(string source, string message, Exception ex = null) | |||
| => Log(LogSeverity.Debug, source, message, ex); | |||
| public void Debug(string source, FormattableString message, Exception ex = null) | |||
| => Log(LogSeverity.Debug, source, message, ex); | |||
| public Logger CreateLogger(string name) => new Logger(this, name); | |||
| } | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| using Discord.Net.WebSockets; | |||
| using System; | |||
| namespace Discord.Logging | |||
| { | |||
| public class Logger | |||
| { | |||
| private readonly LogManager _manager; | |||
| public string Name { get; } | |||
| public LogSeverity Level => _manager.Level; | |||
| internal Logger(LogManager manager, string name) | |||
| { | |||
| _manager = manager; | |||
| Name = name; | |||
| } | |||
| public void Log(LogSeverity severity, string message, Exception exception = null) | |||
| => _manager.Log(severity, Name, message, exception); | |||
| public void Log(LogSeverity severity, FormattableString message, Exception exception = null) | |||
| => _manager.Log(severity, Name, message, exception); | |||
| public void Error(string message, Exception exception = null) | |||
| => _manager.Error(Name, message, exception); | |||
| public void Error(FormattableString message, Exception exception = null) | |||
| => _manager.Error(Name, message, exception); | |||
| public void Warning(string message, Exception exception = null) | |||
| => _manager.Warning(Name, message, exception); | |||
| public void Warning(FormattableString message, Exception exception = null) | |||
| => _manager.Warning(Name, message, exception); | |||
| public void Info(string message, Exception exception = null) | |||
| => _manager.Info(Name, message, exception); | |||
| public void Info(FormattableString message, Exception exception = null) | |||
| => _manager.Info(Name, message, exception); | |||
| public void Verbose(string message, Exception exception = null) | |||
| => _manager.Verbose(Name, message, exception); | |||
| public void Verbose(FormattableString message, Exception exception = null) | |||
| => _manager.Verbose(Name, message, exception); | |||
| public void Debug(string message, Exception exception = null) | |||
| => _manager.Debug(Name, message, exception); | |||
| public void Debug(FormattableString message, Exception exception = null) | |||
| => _manager.Debug(Name, message, exception); | |||
| } | |||
| } | |||
| @@ -14,10 +14,6 @@ namespace Discord | |||
| [Obsolete("Use User.Mention instead")] | |||
| public static string User(User user) | |||
| => $"<@{user.Id}>"; | |||
| /// <summary> Returns the string used to create a user mention. </summary> | |||
| [Obsolete("Use GlobalUser.Mention instead")] | |||
| public static string User(GlobalUser user) | |||
| => $"<@{user.Id}>"; | |||
| /// <summary> Returns the string used to create a channel mention. </summary> | |||
| [Obsolete("Use Channel.Mention instead")] | |||
| public static string Channel(Channel channel) | |||
| @@ -27,12 +23,12 @@ namespace Discord | |||
| public static string Everyone() | |||
| => $"@everyone"; | |||
| internal static string CleanUserMentions(DiscordClient client, Server server, string text, List<User> users = null) | |||
| internal static string CleanUserMentions(DiscordClient client, Channel channel, string text, List<User> users = null) | |||
| { | |||
| return _userRegex.Replace(text, new MatchEvaluator(e => | |||
| { | |||
| var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3)); | |||
| var user = client.Users[id, server?.Id]; | |||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||
| var user = channel.GetUser(id); | |||
| if (user != null) | |||
| { | |||
| if (users != null) | |||
| @@ -43,54 +39,57 @@ namespace Discord | |||
| return '@' + e.Value; | |||
| })); | |||
| } | |||
| internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List<Channel> channels = null) | |||
| internal static string CleanChannelMentions(DiscordClient client, Channel channel, string text, List<Channel> channels = null) | |||
| { | |||
| var server = channel.Server; | |||
| if (server == null) return text; | |||
| return _channelRegex.Replace(text, new MatchEvaluator(e => | |||
| { | |||
| var id = IdConvert.ToLong(e.Value.Substring(2, e.Value.Length - 3)); | |||
| var channel = client.Channels[id]; | |||
| if (channel != null && channel.Server.Id == server.Id) | |||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||
| var mentionedChannel = server.GetChannel(id); | |||
| if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) | |||
| { | |||
| if (channels != null) | |||
| channels.Add(channel); | |||
| return '#' + channel.Name; | |||
| channels.Add(mentionedChannel); | |||
| return '#' + mentionedChannel.Name; | |||
| } | |||
| else //Channel not found | |||
| return '#' + e.Value; | |||
| })); | |||
| } | |||
| /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null) | |||
| /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null) | |||
| { | |||
| var server = channel.Server; | |||
| if (server == null) return text; | |||
| return _roleRegex.Replace(text, new MatchEvaluator(e => | |||
| { | |||
| if (roles != null && user.GetPermissions(channel).MentionEveryone) | |||
| roles.Add(channel.Server.EveryoneRole); | |||
| roles.Add(server.EveryoneRole); | |||
| return e.Value; | |||
| })); | |||
| }*/ | |||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||
| public static string Resolve(Message source, string text) | |||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||
| public static string Resolve(Message source, string text) | |||
| { | |||
| if (source == null) throw new ArgumentNullException(nameof(source)); | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| return Resolve(source.Server, text); | |||
| return Resolve(source.Channel, text); | |||
| } | |||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||
| public static string Resolve(Server server, string text) | |||
| public static string Resolve(Channel channel, string text) | |||
| { | |||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||
| var client = server?.Client; | |||
| text = CleanUserMentions(client, server, text); | |||
| if (server != null) | |||
| { | |||
| text = CleanChannelMentions(client, server, text); | |||
| //text = CleanRoleMentions(_client, User, channel, text); | |||
| } | |||
| return text; | |||
| var client = channel.Client; | |||
| text = CleanUserMentions(client, channel, text); | |||
| text = CleanChannelMentions(client, channel, text); | |||
| //text = CleanRoleMentions(_client, channel, text); | |||
| return text; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,102 @@ | |||
| using Discord.API.Client.Rest; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Net; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Net | |||
| { | |||
| public class MessageQueue | |||
| { | |||
| private class MessageQueueItem | |||
| { | |||
| public readonly ulong Id, ChannelId; | |||
| public readonly string Text; | |||
| public readonly ulong[] MentionedUsers; | |||
| public readonly bool IsTTS; | |||
| public MessageQueueItem(ulong id, ulong channelId, string text, ulong[] userIds, bool isTTS) | |||
| { | |||
| Id = id; | |||
| ChannelId = channelId; | |||
| Text = text; | |||
| MentionedUsers = userIds; | |||
| IsTTS = isTTS; | |||
| } | |||
| } | |||
| private readonly Random _nonceRand; | |||
| private readonly DiscordClient _client; | |||
| private readonly ConcurrentQueue<MessageQueueItem> _pending; | |||
| internal MessageQueue(DiscordClient client) | |||
| { | |||
| _client = client; | |||
| _nonceRand = new Random(); | |||
| _pending = new ConcurrentQueue<MessageQueueItem>(); | |||
| } | |||
| public void QueueSend(ulong channelId, string text, ulong[] userIds, bool isTTS) | |||
| { | |||
| _pending.Enqueue(new MessageQueueItem(0, channelId, text, userIds, isTTS)); | |||
| } | |||
| public void QueueEdit(ulong channelId, ulong messageId, string text, ulong[] userIds) | |||
| { | |||
| _pending.Enqueue(new MessageQueueItem(channelId, messageId, text, userIds, false)); | |||
| } | |||
| internal Task Run(CancellationToken cancelToken, int interval) | |||
| { | |||
| return Task.Run(async () => | |||
| { | |||
| MessageQueueItem queuedMessage; | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| await Task.Delay(interval).ConfigureAwait(false); | |||
| while (_pending.TryDequeue(out queuedMessage)) | |||
| { | |||
| try | |||
| { | |||
| if (queuedMessage.Id == 0) | |||
| { | |||
| var request = new SendMessageRequest(queuedMessage.ChannelId) | |||
| { | |||
| Content = queuedMessage.Text, | |||
| MentionedUserIds = queuedMessage.MentionedUsers, | |||
| Nonce = GenerateNonce().ToIdString(), | |||
| IsTTS = queuedMessage.IsTTS | |||
| }; | |||
| await _client.ClientAPI.Send(request).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| var request = new UpdateMessageRequest(queuedMessage.ChannelId, queuedMessage.Id) | |||
| { | |||
| Content = queuedMessage.Text, | |||
| MentionedUserIds = queuedMessage.MentionedUsers | |||
| }; | |||
| await _client.ClientAPI.Send(request).ConfigureAwait(false); | |||
| } | |||
| } | |||
| catch (WebException) { break; } | |||
| catch (HttpException) { /*msg.State = MessageState.Failed;*/ } | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| public void Clear() | |||
| { | |||
| MessageQueueItem ignored; | |||
| while (_pending.TryDequeue(out ignored)) { } | |||
| } | |||
| private ulong GenerateNonce() | |||
| { | |||
| lock (_nonceRand) | |||
| return (ulong)_nonceRand.Next(1, int.MaxValue); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| using Discord.API.Client; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| @@ -7,315 +7,292 @@ using APIChannel = Discord.API.Client.Channel; | |||
| namespace Discord | |||
| { | |||
| public sealed class Channel : CachedObject<ulong> | |||
| { | |||
| private struct ChannelMember | |||
| { | |||
| public readonly User User; | |||
| public readonly ChannelPermissions Permissions; | |||
| public ChannelMember(User user) | |||
| { | |||
| User = user; | |||
| Permissions = new ChannelPermissions(); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| public sealed class PermissionOverwrite | |||
| { | |||
| public PermissionTarget TargetType { get; } | |||
| public ulong TargetId { get; } | |||
| public DualChannelPermissions Permissions { get; } | |||
| public sealed class Channel | |||
| { | |||
| private struct Member | |||
| { | |||
| public readonly User User; | |||
| public readonly ChannelPermissions Permissions; | |||
| public Member(User user) | |||
| { | |||
| User = user; | |||
| Permissions = new ChannelPermissions(); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny) | |||
| { | |||
| TargetType = targetType; | |||
| TargetId = targetId; | |||
| Permissions = new DualChannelPermissions(allow, deny); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| public sealed class PermissionOverwrite | |||
| { | |||
| public PermissionTarget TargetType { get; } | |||
| public ulong TargetId { get; } | |||
| public DualChannelPermissions Permissions { get; } | |||
| internal PermissionOverwrite(PermissionTarget targetType, ulong targetId, uint allow, uint deny) | |||
| { | |||
| TargetType = targetType; | |||
| TargetId = targetId; | |||
| Permissions = new DualChannelPermissions(allow, deny); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| private readonly ConcurrentDictionary<ulong, Member> _users; | |||
| private readonly ConcurrentDictionary<ulong, Message> _messages; | |||
| private Dictionary<ulong, PermissionOverwrite> _permissionOverwrites; | |||
| /// <summary> Returns the name of this channel. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Returns the topic associated with this channel. </summary> | |||
| public string Topic { get; private set; } | |||
| /// <summary> Returns the position of this channel in the channel list for this server. </summary> | |||
| public int Position { get; private set; } | |||
| /// <summary> Returns false is this is a public chat and true if this is a private chat with another user (see Recipient). </summary> | |||
| public bool IsPrivate => _recipient.Id != null; | |||
| /// <summary> Returns the type of this channel (see ChannelTypes). </summary> | |||
| public string Type { get; private set; } | |||
| /// <summary> Gets the client that generated this channel object. </summary> | |||
| internal DiscordClient Client { get; } | |||
| /// <summary> Gets the unique identifier for this channel. </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> Gets the server owning this channel, if this is a public chat. </summary> | |||
| public Server Server { get; } | |||
| /// <summary> Gets the target user, if this is a private chat. </summary> | |||
| public User Recipient { get; } | |||
| /// <summary> Returns the server containing this channel. </summary> | |||
| [JsonIgnore] | |||
| public Server Server => _server.Value; | |||
| [JsonProperty] | |||
| private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } | |||
| private readonly Reference<Server> _server; | |||
| /// <summary> Gets the name of this channel. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Gets the topic of this channel. </summary> | |||
| public string Topic { get; private set; } | |||
| /// <summary> Gets the position of this channel relative to other channels in this server. </summary> | |||
| public int Position { get; private set; } | |||
| /// <summary> Gets the type of this channel (see ChannelTypes). </summary> | |||
| public string Type { get; private set; } | |||
| /// For private chats, returns the target user, otherwise null. | |||
| [JsonIgnore] | |||
| public User Recipient => _recipient.Value; | |||
| [JsonProperty] | |||
| private ulong? RecipientId { get { return _recipient.Id; } set { _recipient.Id = value; } } | |||
| private readonly Reference<User> _recipient; | |||
| /// <summary> Gets true if this is a private chat with another user. </summary> | |||
| public bool IsPrivate => Recipient != null; | |||
| /// <summary> Gets the string used to mention this channel. </summary> | |||
| public string Mention => $"<#{Id}>"; | |||
| /// <summary> Gets a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary> | |||
| public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>(); | |||
| /// <summary> Gets a collection of all custom permissions used for this channel. </summary> | |||
| public IEnumerable<PermissionOverwrite> PermissionOverwrites => _permissionOverwrites.Select(x => x.Value); | |||
| //Collections | |||
| /// <summary> Returns a collection of all users with read access to this channel. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> Members | |||
| { | |||
| get | |||
| { | |||
| /// <summary> Gets a collection of all users with read access to this channel. </summary> | |||
| public IEnumerable<User> Users | |||
| { | |||
| get | |||
| { | |||
| if (IsPrivate) | |||
| return _members.Values.Select(x => x.User); | |||
| if (_client.Config.UsePermissionsCache) | |||
| return _users.Values.Select(x => x.User); | |||
| if (Client.Config.UsePermissionsCache) | |||
| { | |||
| if (Type == ChannelType.Text) | |||
| return _members.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); | |||
| return _users.Values.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); | |||
| else if (Type == ChannelType.Voice) | |||
| return _members.Values.Select(x => x.User).Where(x => x.VoiceChannel == this); | |||
| return _users.Values.Select(x => x.User).Where(x => x.VoiceChannel == this); | |||
| } | |||
| else | |||
| { | |||
| if (Type == ChannelType.Text) | |||
| { | |||
| ChannelPermissions perms = new ChannelPermissions(); | |||
| return Server.Members.Where(x => | |||
| return Server.Users.Where(x => | |||
| { | |||
| UpdatePermissions(x, perms); | |||
| return perms.ReadMessages == true; | |||
| }); | |||
| } | |||
| else if (Type == ChannelType.Voice) | |||
| return Server.Members.Where(x => x.VoiceChannel == this); | |||
| return Server.Users.Where(x => x.VoiceChannel == this); | |||
| } | |||
| return Enumerable.Empty<User>(); | |||
| return Enumerable.Empty<User>(); | |||
| } | |||
| } | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id); | |||
| private ConcurrentDictionary<ulong, ChannelMember> _members; | |||
| /// <summary> Returns a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Message> Messages => _messages?.Values ?? Enumerable.Empty<Message>(); | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MessageIds => Messages.Select(x => x.Id); | |||
| private readonly ConcurrentDictionary<ulong, Message> _messages; | |||
| } | |||
| /// <summary> Returns a collection of all custom permissions used for this channel. </summary> | |||
| private PermissionOverwrite[] _permissionOverwrites; | |||
| public IEnumerable<PermissionOverwrite> PermissionOverwrites { get { return _permissionOverwrites; } internal set { _permissionOverwrites = value.ToArray(); } } | |||
| internal Channel(DiscordClient client, ulong id, Server server) | |||
| : this(client, id) | |||
| { | |||
| Server = server; | |||
| } | |||
| internal Channel(DiscordClient client, ulong id, User recipient) | |||
| : this(client, id) | |||
| { | |||
| Recipient = recipient; | |||
| Name = $"@{recipient}"; | |||
| AddUser(client.PrivateUser); | |||
| AddUser(recipient); | |||
| } | |||
| private Channel(DiscordClient client, ulong id) | |||
| { | |||
| Client = client; | |||
| Id = id; | |||
| /// <summary> Returns the string used to mention this channel. </summary> | |||
| public string Mention => $"<#{Id}>"; | |||
| _permissionOverwrites = new Dictionary<ulong, PermissionOverwrite>(); | |||
| _users = new ConcurrentDictionary<ulong, Member>(); | |||
| if (client.Config.MessageCacheSize > 0) | |||
| _messages = new ConcurrentDictionary<ulong, Message>(); | |||
| } | |||
| internal Channel(DiscordClient client, ulong id, ulong? serverId, ulong? recipientId) | |||
| : base(client, id) | |||
| { | |||
| _server = new Reference<Server>(serverId, | |||
| x => _client.Servers[x], | |||
| x => x.AddChannel(this), | |||
| x => x.RemoveChannel(this)); | |||
| _recipient = new Reference<User>(recipientId, | |||
| x => _client.Users.GetOrAdd(x, _server.Id), | |||
| x => | |||
| { | |||
| Name = $"@{x}"; | |||
| if (_server.Id == null) | |||
| x.Global.PrivateChannel = this; | |||
| }, | |||
| x => | |||
| { | |||
| if (_server.Id == null) | |||
| x.Global.PrivateChannel = null; | |||
| }); | |||
| _permissionOverwrites = new PermissionOverwrite[0]; | |||
| _members = new ConcurrentDictionary<ulong, ChannelMember>(); | |||
| internal void Update(ChannelReference model) | |||
| { | |||
| if (!IsPrivate && model.Name != null) | |||
| Name = model.Name; | |||
| if (model.Type != null) | |||
| Type = model.Type; | |||
| } | |||
| internal void Update(APIChannel model) | |||
| { | |||
| Update(model as ChannelReference); | |||
| if (recipientId != null) | |||
| { | |||
| AddMember(client.PrivateUser); | |||
| AddMember(Recipient); | |||
| } | |||
| if (model.Position != null) | |||
| Position = model.Position.Value; | |||
| if (model.Topic != null) | |||
| Topic = model.Topic; | |||
| if (model.Recipient != null) | |||
| Recipient.Update(model.Recipient); | |||
| //Local Cache | |||
| if (client.Config.MessageCacheSize > 0) | |||
| _messages = new ConcurrentDictionary<ulong, Message>(); | |||
| } | |||
| internal override bool LoadReferences() | |||
| { | |||
| if (IsPrivate) | |||
| return _recipient.Load(); | |||
| else | |||
| return _server.Load(); | |||
| } | |||
| internal override void UnloadReferences() | |||
| { | |||
| _server.Unload(); | |||
| _recipient.Unload(); | |||
| var globalMessages = _client.Messages; | |||
| if (_client.Config.MessageCacheSize > 0) | |||
| if (model.PermissionOverwrites != null) | |||
| { | |||
| var messages = _messages; | |||
| foreach (var message in messages) | |||
| globalMessages.TryRemove(message.Key); | |||
| messages.Clear(); | |||
| _permissionOverwrites = model.PermissionOverwrites | |||
| .Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny)) | |||
| .ToDictionary(x => x.TargetId); | |||
| UpdatePermissions(); | |||
| } | |||
| } | |||
| internal void Update(ChannelReference model) | |||
| { | |||
| if (!IsPrivate && model.Name != null) | |||
| Name = model.Name; | |||
| if (model.Type != null) | |||
| Type = model.Type; | |||
| } | |||
| internal void Update(APIChannel model) | |||
| { | |||
| Update(model as ChannelReference); | |||
| if (model.Position != null) | |||
| Position = model.Position; | |||
| if (model.Topic != null) | |||
| Topic = model.Topic; | |||
| if (model.PermissionOverwrites != null) | |||
| { | |||
| _permissionOverwrites = model.PermissionOverwrites | |||
| .Select(x => new PermissionOverwrite(PermissionTarget.FromString(x.Type), x.Id, x.Allow, x.Deny)) | |||
| .ToArray(); | |||
| UpdatePermissions(); | |||
| } | |||
| } | |||
| internal void AddMessage(Message message) | |||
| { | |||
| //Race conditions are okay here - it just means the queue will occasionally go higher than the requested cache size, and fixed later. | |||
| var cacheLength = _client.Config.MessageCacheSize; | |||
| if (cacheLength > 0) | |||
| { | |||
| var oldestIds = _messages.Where(x => x.Value.Timestamp < message.Timestamp).Select(x => x.Key).OrderBy(x => x).Take(_messages.Count - cacheLength); | |||
| foreach (var id in oldestIds) | |||
| { | |||
| Message removed; | |||
| if (_messages.TryRemove(id, out removed)) | |||
| _client.Messages.TryRemove(id); | |||
| } | |||
| _messages.TryAdd(message.Id, message); | |||
| } | |||
| } | |||
| internal void RemoveMessage(Message message) | |||
| { | |||
| if (_client.Config.MessageCacheSize > 0) | |||
| _messages.TryRemove(message.Id, out message); | |||
| } | |||
| internal void AddMember(User user) | |||
| //Members | |||
| internal void AddUser(User user) | |||
| { | |||
| if (!_client.Config.UsePermissionsCache) | |||
| if (!Client.Config.UsePermissionsCache) | |||
| return; | |||
| var member = new ChannelMember(user); | |||
| if (_members.TryAdd(user.Id, member)) | |||
| UpdatePermissions(user, member.Permissions); | |||
| var member = new Member(user); | |||
| if (_users.TryAdd(user.Id, member)) | |||
| UpdatePermissions(user, member.Permissions); | |||
| } | |||
| internal void RemoveMember(User user) | |||
| internal void RemoveUser(ulong id) | |||
| { | |||
| if (!_client.Config.UsePermissionsCache) | |||
| if (!Client.Config.UsePermissionsCache) | |||
| return; | |||
| ChannelMember ignored; | |||
| _members.TryRemove(user.Id, out ignored); | |||
| } | |||
| Member ignored; | |||
| _users.TryRemove(id, out ignored); | |||
| } | |||
| public User GetUser(ulong id) | |||
| { | |||
| Member result; | |||
| _users.TryGetValue(id, out result); | |||
| return result.User; | |||
| } | |||
| internal ChannelPermissions GetPermissions(User user) | |||
| //Messages | |||
| internal Message AddMessage(ulong id, ulong userId, DateTime timestamp) | |||
| { | |||
| if (_client.Config.UsePermissionsCache) | |||
| Message message = new Message(id, this, userId); | |||
| var cacheLength = Client.Config.MessageCacheSize; | |||
| if (cacheLength > 0) | |||
| { | |||
| ChannelMember member; | |||
| if (_members.TryGetValue(user.Id, out member)) | |||
| return member.Permissions; | |||
| else | |||
| return null; | |||
| var oldestIds = _messages | |||
| .Where(x => x.Value.Timestamp < timestamp) | |||
| .Select(x => x.Key).OrderBy(x => x) | |||
| .Take(_messages.Count - cacheLength); | |||
| Message removed; | |||
| foreach (var removeId in oldestIds) | |||
| _messages.TryRemove(removeId, out removed); | |||
| return _messages.GetOrAdd(message.Id, message); | |||
| } | |||
| else | |||
| return message; | |||
| } | |||
| internal Message RemoveMessage(ulong id) | |||
| { | |||
| if (Client.Config.MessageCacheSize > 0) | |||
| { | |||
| ChannelPermissions perms = new ChannelPermissions(); | |||
| UpdatePermissions(user, perms); | |||
| return perms; | |||
| Message msg; | |||
| _messages.TryRemove(id, out msg); | |||
| return msg; | |||
| } | |||
| } | |||
| internal void UpdatePermissions() | |||
| return null; | |||
| } | |||
| public Message GetMessage(ulong id) | |||
| { | |||
| if (!_client.Config.UsePermissionsCache) | |||
| Message result; | |||
| _messages.TryGetValue(id, out result); | |||
| return result; | |||
| } | |||
| //Permissions | |||
| internal void UpdatePermissions() | |||
| { | |||
| if (!Client.Config.UsePermissionsCache) | |||
| return; | |||
| foreach (var pair in _members) | |||
| foreach (var pair in _users) | |||
| { | |||
| ChannelMember member = pair.Value; | |||
| Member member = pair.Value; | |||
| UpdatePermissions(member.User, member.Permissions); | |||
| } | |||
| } | |||
| } | |||
| internal void UpdatePermissions(User user) | |||
| { | |||
| if (!_client.Config.UsePermissionsCache) | |||
| if (!Client.Config.UsePermissionsCache) | |||
| return; | |||
| ChannelMember member; | |||
| if (_members.TryGetValue(user.Id, out member)) | |||
| Member member; | |||
| if (_users.TryGetValue(user.Id, out member)) | |||
| UpdatePermissions(member.User, member.Permissions); | |||
| } | |||
| internal void UpdatePermissions(User user, ChannelPermissions permissions) | |||
| { | |||
| uint newPermissions = 0; | |||
| var server = Server; | |||
| { | |||
| uint newPermissions = 0; | |||
| var server = Server; | |||
| //Load the mask of all permissions supported by this channel type | |||
| var mask = ChannelPermissions.All(this).RawValue; | |||
| //Load the mask of all permissions supported by this channel type | |||
| var mask = ChannelPermissions.All(this).RawValue; | |||
| if (server != null) | |||
| { | |||
| //Start with this user's server permissions | |||
| newPermissions = server.GetPermissions(user).RawValue; | |||
| if (server != null) | |||
| { | |||
| //Start with this user's server permissions | |||
| newPermissions = server.GetPermissions(user).RawValue; | |||
| if (IsPrivate || user.IsOwner) | |||
| newPermissions = mask; //Owners always have all permissions | |||
| else | |||
| { | |||
| var channelOverwrites = PermissionOverwrites; | |||
| if (IsPrivate || user == Server.Owner) | |||
| newPermissions = mask; //Owners always have all permissions | |||
| else | |||
| { | |||
| var channelOverwrites = PermissionOverwrites; | |||
| var roles = user.Roles; | |||
| foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) | |||
| newPermissions &= ~denyRole.Permissions.Deny.RawValue; | |||
| foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) | |||
| newPermissions |= allowRole.Permissions.Allow.RawValue; | |||
| foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0)) | |||
| newPermissions &= ~denyUser.Permissions.Deny.RawValue; | |||
| foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0)) | |||
| newPermissions |= allowUser.Permissions.Allow.RawValue; | |||
| var roles = user.Roles; | |||
| foreach (var denyRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Deny.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) | |||
| newPermissions &= ~denyRole.Permissions.Deny.RawValue; | |||
| foreach (var allowRole in channelOverwrites.Where(x => x.TargetType == PermissionTarget.Role && x.Permissions.Allow.RawValue != 0 && roles.Any(y => y.Id == x.TargetId))) | |||
| newPermissions |= allowRole.Permissions.Allow.RawValue; | |||
| foreach (var denyUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Deny.RawValue != 0)) | |||
| newPermissions &= ~denyUser.Permissions.Deny.RawValue; | |||
| foreach (var allowUser in channelOverwrites.Where(x => x.TargetType == PermissionTarget.User && x.TargetId == Id && x.Permissions.Allow.RawValue != 0)) | |||
| newPermissions |= allowUser.Permissions.Allow.RawValue; | |||
| if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions)) | |||
| newPermissions = mask; //ManageRolesOrPermissions gives all permisions | |||
| else if (Type == ChannelType.Text && !BitHelper.GetBit(newPermissions, (int)PermissionsBits.ReadMessages)) | |||
| newPermissions = 0; //No read permission on a text channel removes all other permissions | |||
| else | |||
| newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) | |||
| } | |||
| } | |||
| else | |||
| newPermissions = mask; //Private messages always have all permissions | |||
| if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions)) | |||
| newPermissions = mask; //ManageRolesOrPermissions gives all permisions | |||
| else if (Type == ChannelType.Text && !newPermissions.HasBit((byte)PermissionsBits.ReadMessages)) | |||
| newPermissions = 0; //No read permission on a text channel removes all other permissions | |||
| else | |||
| newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) | |||
| } | |||
| } | |||
| else | |||
| newPermissions = mask; //Private messages always have all permissions | |||
| if (newPermissions != permissions.RawValue) | |||
| permissions.SetRawValueInternal(newPermissions); | |||
| } | |||
| } | |||
| internal ChannelPermissions GetPermissions(User user) | |||
| { | |||
| if (Client.Config.UsePermissionsCache) | |||
| { | |||
| Member member; | |||
| if (_users.TryGetValue(user.Id, out member)) | |||
| return member.Permissions; | |||
| else | |||
| return null; | |||
| } | |||
| else | |||
| { | |||
| ChannelPermissions perms = new ChannelPermissions(); | |||
| UpdatePermissions(user, perms); | |||
| return perms; | |||
| } | |||
| } | |||
| public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); | |||
| public override string ToString() => Name ?? IdConvert.ToString(Id); | |||
| } | |||
| public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); | |||
| public override string ToString() => Name ?? Id.ToIdString(); | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ using APIUser = Discord.API.Client.User; | |||
| namespace Discord | |||
| { | |||
| public sealed class GlobalUser : CachedObject<ulong> | |||
| /*public sealed class GlobalUser : CachedObject<ulong> | |||
| { | |||
| /// <summary> Returns the email for this user. Note: this field is only ever populated for the current logged in user. </summary> | |||
| [JsonIgnore] | |||
| @@ -75,5 +75,5 @@ namespace Discord | |||
| public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 7891); | |||
| public override string ToString() => IdConvert.ToString(Id); | |||
| } | |||
| }*/ | |||
| } | |||
| @@ -42,6 +42,7 @@ namespace Discord | |||
| public ushort Discriminator { get; } | |||
| /// <summary> Returns the unique identifier for this user's avatar. </summary> | |||
| public string AvatarId { get; } | |||
| /// <summary> Returns the full path to this user's avatar. </summary> | |||
| public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId); | |||
| @@ -54,24 +55,26 @@ namespace Discord | |||
| } | |||
| } | |||
| /// <summary> Returns information about the server this invite is attached to. </summary> | |||
| public ServerInfo Server { get; private set; } | |||
| /// <summary> Returns information about the channel this invite is attached to. </summary> | |||
| public ChannelInfo Channel { get; private set; } | |||
| /// <summary> Gets the unique code for this invite. </summary> | |||
| public string Code { get; } | |||
| /// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary> | |||
| /// <summary> Gets, if enabled, an alternative human-readable invite code. </summary> | |||
| public string XkcdCode { get; } | |||
| /// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary> | |||
| public int MaxAge { get; private set; } | |||
| /// <summary> The amount of times this invite has been used. </summary> | |||
| /// <summary> Gets information about the server this invite is attached to. </summary> | |||
| public ServerInfo Server { get; private set; } | |||
| /// <summary> Gets information about the channel this invite is attached to. </summary> | |||
| public ChannelInfo Channel { get; private set; } | |||
| /// <summary> Gets the time (in seconds) until the invite expires. </summary> | |||
| public int? MaxAge { get; private set; } | |||
| /// <summary> Gets the amount of times this invite has been used. </summary> | |||
| public int Uses { get; private set; } | |||
| /// <summary> The max amount of times this invite may be used. </summary> | |||
| public int MaxUses { get; private set; } | |||
| /// <summary> Returns true if this invite has been destroyed, or you are banned from that server. </summary> | |||
| /// <summary> Gets the max amount of times this invite may be used. </summary> | |||
| public int? MaxUses { get; private set; } | |||
| /// <summary> Returns true if this invite has expired, been destroyed, or you are banned from that server. </summary> | |||
| public bool IsRevoked { get; private set; } | |||
| /// <summary> If true, a user accepting this invite will be kicked from the server after closing their client. </summary> | |||
| public bool IsTemporary { get; private set; } | |||
| /// <summary> Gets when this invite was created. </summary> | |||
| public DateTime CreatedAt { get; private set; } | |||
| /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | |||
| @@ -99,7 +102,7 @@ namespace Discord | |||
| if (model.IsTemporary != null) | |||
| IsTemporary = model.IsTemporary.Value; | |||
| if (model.MaxAge != null) | |||
| MaxAge = model.MaxAge.Value; | |||
| MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null; | |||
| if (model.MaxUses != null) | |||
| MaxUses = model.MaxUses.Value; | |||
| if (model.Uses != null) | |||
| @@ -15,9 +15,9 @@ namespace Discord | |||
| Failed | |||
| } | |||
| public sealed class Message : CachedObject<ulong> | |||
| public sealed class Message | |||
| { | |||
| internal class ImportResolver : DefaultContractResolver | |||
| /*internal class ImportResolver : DefaultContractResolver | |||
| { | |||
| protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | |||
| { | |||
| @@ -31,7 +31,7 @@ namespace Discord | |||
| } | |||
| return property; | |||
| } | |||
| } | |||
| }*/ | |||
| public sealed class Attachment : File | |||
| { | |||
| @@ -89,17 +89,24 @@ namespace Discord | |||
| internal File() { } | |||
| } | |||
| /// <summary> Returns true if the logged-in user was mentioned. </summary> | |||
| public bool IsMentioningMe { get; private set; } | |||
| /// <summary> Returns true if the current user created this message. </summary> | |||
| public bool IsAuthor => _client.CurrentUser.Id == _user.Id; | |||
| private static readonly Attachment[] _initialAttachments = new Attachment[0]; | |||
| private static readonly Embed[] _initialEmbeds = new Embed[0]; | |||
| private readonly ulong _userId; | |||
| /// <summary> Returns the unique identifier for this message. </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> Returns the channel this message was sent to. </summary> | |||
| public Channel Channel { get; } | |||
| /// <summary> Returns true if the logged-in user was mentioned. </summary> | |||
| public bool IsMentioningMe { get; private set; } | |||
| /// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary> | |||
| public bool IsTTS { get; private set; } | |||
| /// <summary> Returns the state of this message. Only useful if UseMessageQueue is true. </summary> | |||
| public MessageState State { get; internal set; } | |||
| /// <summary> Returns the raw content of this message as it was received from the server. </summary> | |||
| public string RawText { get; private set; } | |||
| [JsonIgnore] | |||
| /// <summary> Returns the content of this message with any special references such as mentions converted. </summary> | |||
| public string Text { get; internal set; } | |||
| /// <summary> Returns the timestamp for when this message was sent. </summary> | |||
| @@ -108,89 +115,26 @@ namespace Discord | |||
| public DateTime? EditedTimestamp { get; private set; } | |||
| /// <summary> Returns the attachments included in this message. </summary> | |||
| public Attachment[] Attachments { get; private set; } | |||
| private static readonly Attachment[] _initialAttachments = new Attachment[0]; | |||
| /// <summary> Returns a collection of all embeded content in this message. </summary> | |||
| public Embed[] Embeds { get; private set; } | |||
| private static readonly Embed[] _initialEmbeds = new Embed[0]; | |||
| /// <summary> Returns a collection of all users mentioned in this message. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> MentionedUsers { get; internal set; } | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MentionedUserIds | |||
| { | |||
| get { return MentionedUsers?.Select(x => x.Id); } | |||
| set { MentionedUsers = value.Select(x => _client.GetUser(Server, x)).Where(x => x != null); } | |||
| } | |||
| /// <summary> Returns a collection of all channels mentioned in this message. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> MentionedChannels { get; internal set; } | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MentionedChannelIds | |||
| { | |||
| get { return MentionedChannels?.Select(x => x.Id); } | |||
| set { MentionedChannels = value.Select(x => _client.GetChannel(x)).Where(x => x != null); } | |||
| } | |||
| /// <summary> Returns a collection of all roles mentioned in this message. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Role> MentionedRoles { get; internal set; } | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MentionedRoleIds | |||
| { | |||
| get { return MentionedRoles?.Select(x => x.Id); } | |||
| set { MentionedRoles = value.Select(x => _client.GetRole(x)).Where(x => x != null); } | |||
| } | |||
| /// <summary> Returns the server containing the channel this message was sent to. </summary> | |||
| [JsonIgnore] | |||
| public Server Server => _channel.Value.Server; | |||
| /// <summary> Returns the channel this message was sent to. </summary> | |||
| [JsonIgnore] | |||
| public Channel Channel => _channel.Value; | |||
| [JsonProperty] | |||
| private ulong? ChannelId => _channel.Id; | |||
| private readonly Reference<Channel> _channel; | |||
| public Server Server => Channel.Server; | |||
| /// <summary> Returns the author of this message. </summary> | |||
| public User User => Channel.GetUser(_userId); | |||
| /// <summary> Returns the author of this message. </summary> | |||
| [JsonIgnore] | |||
| public User User => _user.Value; | |||
| [JsonProperty] | |||
| private ulong? UserId => _user.Id; | |||
| private readonly Reference<User> _user; | |||
| internal Message(DiscordClient client, ulong id, ulong channelId, ulong userId) | |||
| : base(client, id) | |||
| internal Message(ulong id, Channel channel, ulong userId) | |||
| { | |||
| _channel = new Reference<Channel>(channelId, | |||
| x => _client.Channels[x], | |||
| x => x.AddMessage(this), | |||
| x => x.RemoveMessage(this)); | |||
| _user = new Reference<User>(userId, | |||
| x => | |||
| { | |||
| var channel = Channel; | |||
| if (channel == null) return null; | |||
| if (!channel.IsPrivate) | |||
| return _client.Users[x, channel.Server.Id]; | |||
| else | |||
| return _client.Users[x, null]; | |||
| }); | |||
| Attachments = _initialAttachments; | |||
| Embeds = _initialEmbeds; | |||
| } | |||
| internal override bool LoadReferences() | |||
| { | |||
| return _channel.Load() && _user.Load(); | |||
| } | |||
| internal override void UnloadReferences() | |||
| { | |||
| _channel.Unload(); | |||
| _user.Unload(); | |||
| } | |||
| internal void Update(APIMessage model) | |||
| { | |||
| @@ -247,7 +191,7 @@ namespace Discord | |||
| if (model.Mentions != null) | |||
| { | |||
| MentionedUsers = model.Mentions | |||
| .Select(x => _client.Users[x.Id, Channel.Server?.Id]) | |||
| .Select(x => Channel.GetUser(x.Id)) | |||
| .Where(x => x != null) | |||
| .ToArray(); | |||
| } | |||
| @@ -266,10 +210,10 @@ namespace Discord | |||
| //var mentionedUsers = new List<User>(); | |||
| var mentionedChannels = new List<Channel>(); | |||
| //var mentionedRoles = new List<Role>(); | |||
| text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/); | |||
| text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/); | |||
| if (server != null) | |||
| { | |||
| text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels); | |||
| text = Mention.CleanChannelMentions(Channel.Client, channel, text, mentionedChannels); | |||
| //text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); | |||
| } | |||
| Text = text; | |||
| @@ -287,7 +231,7 @@ namespace Discord | |||
| } | |||
| else | |||
| { | |||
| var me = _client.PrivateUser; | |||
| var me = Channel.Client.PrivateUser; | |||
| IsMentioningMe = MentionedUsers?.Contains(me) ?? false; | |||
| } | |||
| } | |||
| @@ -127,9 +127,15 @@ namespace Discord | |||
| _rawValue = rawValue; | |||
| } | |||
| internal bool GetBit(PermissionsBits pos) => BitHelper.GetBit(_rawValue, (int)pos); | |||
| internal void SetBit(PermissionsBits pos, bool value) { CheckLock(); SetBitInternal((byte)pos, value); } | |||
| internal void SetBitInternal(int pos, bool value) => BitHelper.SetBit(ref _rawValue, pos, value); | |||
| internal bool GetBit(PermissionsBits bit) => _rawValue.HasBit((byte)bit); | |||
| internal void SetBit(PermissionsBits bit, bool value) { CheckLock(); SetBitInternal((byte)bit, value); } | |||
| internal void SetBitInternal(int pos, bool value) | |||
| { | |||
| if (value) | |||
| _rawValue |= (1U << pos); | |||
| else | |||
| _rawValue &= ~(1U << pos); | |||
| } | |||
| internal void Lock() => _isLocked = true; | |||
| protected void CheckLock() | |||
| @@ -140,7 +146,7 @@ namespace Discord | |||
| public override bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue; | |||
| public override int GetHashCode() => unchecked(_rawValue.GetHashCode() + 393); | |||
| } | |||
| } | |||
| public sealed class DualChannelPermissions | |||
| { | |||
| @@ -0,0 +1,29 @@ | |||
| using Newtonsoft.Json; | |||
| using APIUser = Discord.API.Client.User; | |||
| namespace Discord | |||
| { | |||
| public sealed class Profile | |||
| { | |||
| /// <summary> Gets the unique identifier for this user. </summary> | |||
| public ulong Id { get; private set; } | |||
| /// <summary> Gets the email for this user. </summary> | |||
| public string Email { get; private set; } | |||
| /// <summary> Gets if the email for this user has been verified. </summary> | |||
| public bool? IsVerified { get; private set; } | |||
| internal Profile() { } | |||
| internal void Update(APIUser model) | |||
| { | |||
| Id = model.Id; | |||
| Email = model.Email; | |||
| IsVerified = model.IsVerified; | |||
| } | |||
| public override bool Equals(object obj) | |||
| => (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id); | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061); | |||
| public override string ToString() => Id.ToIdString(); | |||
| } | |||
| } | |||
| @@ -1,64 +1,58 @@ | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using APIRole = Discord.API.Client.Role; | |||
| namespace Discord | |||
| { | |||
| public sealed class Role : CachedObject<ulong> | |||
| { | |||
| /// <summary> Returns the name of this role. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> If true, this role is displayed isolated from other users. </summary> | |||
| public bool IsHoisted { get; private set; } | |||
| /// <summary> Returns the position of this channel in the role list for this server. </summary> | |||
| public int Position { get; private set; } | |||
| /// <summary> Returns the color of this role. </summary> | |||
| public Color Color { get; private set; } | |||
| /// <summary> Returns whether this role is managed by server (e.g. for Twitch integration) </summary> | |||
| public bool IsManaged { get; private set; } | |||
| /// <summary> Returns the the permissions contained by this role. </summary> | |||
| public ServerPermissions Permissions { get; } | |||
| public sealed class Role | |||
| { | |||
| private readonly DiscordClient _client; | |||
| /// <summary> Returns the server this role is a member of. </summary> | |||
| [JsonIgnore] | |||
| public Server Server => _server.Value; | |||
| [JsonProperty] | |||
| private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } | |||
| private readonly Reference<Server> _server; | |||
| /// <summary> Gets the unique identifier for this role. </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> Gets the server this role is a member of. </summary> | |||
| public Server Server { get; } | |||
| /// <summary> Gets the the permissions contained by this role. </summary> | |||
| public ServerPermissions Permissions { get; } | |||
| /// <summary> Gets the color of this role. </summary> | |||
| public Color Color { get; } | |||
| /// <summary> Returns true if this is the role representing all users in a server. </summary> | |||
| public bool IsEveryone => _server.Id == null || Id == _server.Id; | |||
| /// <summary> Gets the name of this role. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> If true, this role is displayed isolated from other users. </summary> | |||
| public bool IsHoisted { get; private set; } | |||
| /// <summary> Gets the position of this channel relative to other channels in this server. </summary> | |||
| public int Position { get; private set; } | |||
| /// <summary> Gets whether this role is managed by server (e.g. for Twitch integration) </summary> | |||
| public bool IsManaged { get; private set; } | |||
| /// <summary> Returns a list of all members in this role. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> Members => _server.Id != null ? (IsEveryone ? Server.Members : Server.Members.Where(x => x.HasRole(this))) : new User[0]; | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id); | |||
| //TODO: Add local members cache | |||
| /// <summary> Gets true if this is the role representing all users in a server. </summary> | |||
| public bool IsEveryone => Id == Server.Id; | |||
| /// <summary> Gets a list of all members in this role. </summary> | |||
| public IEnumerable<User> Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this)); | |||
| /// <summary> Returns the string used to mention this role. </summary> | |||
| public string Mention { get { if (IsEveryone) return "@everyone"; else throw new InvalidOperationException("Discord currently only supports mentioning the everyone role"); } } | |||
| /// <summary> Gets the string used to mention this role. </summary> | |||
| public string Mention | |||
| { | |||
| get | |||
| { | |||
| if (IsEveryone) | |||
| return "@everyone"; | |||
| else | |||
| throw new InvalidOperationException("Roles may only be mentioned if IsEveryone is true"); | |||
| } | |||
| } | |||
| internal Role(DiscordClient client, ulong id, ulong serverId) | |||
| : base(client, id) | |||
| internal Role(ulong id, Server server) | |||
| { | |||
| _server = new Reference<Server>(serverId, x => _client.Servers[x], x => x.AddRole(this), x => x.RemoveRole(this)); | |||
| Id = id; | |||
| Server = server; | |||
| Permissions = new ServerPermissions(0); | |||
| Permissions.Lock(); | |||
| Color = new Color(0); | |||
| Color.Lock(); | |||
| } | |||
| internal override bool LoadReferences() | |||
| { | |||
| return _server.Load(); | |||
| } | |||
| internal override void UnloadReferences() | |||
| { | |||
| _server.Unload(); | |||
| } | |||
| internal void Update(APIRole model) | |||
| { | |||
| @@ -81,6 +75,6 @@ namespace Discord | |||
| public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); | |||
| public override string ToString() => Name ?? IdConvert.ToString(Id); | |||
| public override string ToString() => Name ?? Id.ToIdString(); | |||
| } | |||
| } | |||
| @@ -1,283 +1,229 @@ | |||
| using Discord.API.Client; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using APIGuild = Discord.API.Client.Guild; | |||
| namespace Discord | |||
| { | |||
| public sealed class Server : CachedObject<ulong> | |||
| { | |||
| private struct ServerMember | |||
| { | |||
| public readonly User User; | |||
| public readonly ServerPermissions Permissions; | |||
| public ServerMember(User user) | |||
| { | |||
| User = user; | |||
| Permissions = new ServerPermissions(); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| /// <summary> Returns the name of this channel. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Returns the current logged-in user's data for this server. </summary> | |||
| public User CurrentUser { get; internal set; } | |||
| /// <summary> Returns the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel (see AFKChannel). </summary> | |||
| public int AFKTimeout { get; private set; } | |||
| /// <summary> Returns the date and time your joined this server. </summary> | |||
| public DateTime JoinedAt { get; private set; } | |||
| /// <summary> Returns the region for this server (see Regions). </summary> | |||
| public string Region { get; private set; } | |||
| /// <summary> Returns the unique identifier for this user's current avatar. </summary> | |||
| public string IconId { get; private set; } | |||
| /// <summary> Returns the URL to this user's current avatar. </summary> | |||
| public string IconUrl => IconId != null ? $"{DiscordConfig.CDNUrl}/icons/{Id}/{IconId}.jpg" : null; | |||
| /// <summary> Returns the user that first created this server. </summary> | |||
| [JsonIgnore] | |||
| public User Owner => _owner.Value; | |||
| [JsonProperty] | |||
| internal ulong? OwnerId => _owner.Id; | |||
| private Reference<User> _owner; | |||
| /// <summary> Returns the AFK voice channel for this server (see AFKTimeout). </summary> | |||
| [JsonIgnore] | |||
| public Channel AFKChannel => _afkChannel.Value; | |||
| [JsonProperty] | |||
| private ulong? AFKChannelId => _afkChannel.Id; | |||
| private Reference<Channel> _afkChannel; | |||
| /// <summary> Returns the default channel for this server. </summary> | |||
| [JsonIgnore] | |||
| public Channel DefaultChannel { get; private set; } | |||
| /// <summary> Returns a collection of the ids of all users banned on this server. </summary> | |||
| public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key); | |||
| private ConcurrentDictionary<ulong, bool> _bans; | |||
| /// <summary> Returns a collection of all channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | |||
| /// <summary> Returns a collection of all text channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> TextChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Text); | |||
| /// <summary> Returns a collection of all voice channels within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> VoiceChannels => _channels.Select(x => x.Value).Where(x => x.Type == ChannelType.Voice); | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> ChannelIds => Channels.Select(x => x.Id); | |||
| private ConcurrentDictionary<ulong, Channel> _channels; | |||
| /// <summary> Returns a collection of all users within this server with their server-specific data. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<User> Members => _members.Select(x => x.Value.User); | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> MemberIds => Members.Select(x => x.Id); | |||
| private ConcurrentDictionary<ulong, ServerMember> _members; | |||
| /// <summary> Return the the role representing all users in a server. </summary> | |||
| [JsonIgnore] | |||
| public Role EveryoneRole { get; private set; } | |||
| /// <summary> Returns a collection of all roles within this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> RoleIds => Roles.Select(x => x.Id); | |||
| private ConcurrentDictionary<ulong, Role> _roles; | |||
| internal Server(DiscordClient client, ulong id) | |||
| : base(client, id) | |||
| { | |||
| _owner = new Reference<User>(x => _client.Users[x, Id]); | |||
| _afkChannel = new Reference<Channel>(x => _client.Channels[x]); | |||
| //Global Cache | |||
| _channels = new ConcurrentDictionary<ulong, Channel>(); | |||
| _roles = new ConcurrentDictionary<ulong, Role>(); | |||
| _members = new ConcurrentDictionary<ulong, ServerMember>(); | |||
| //Local Cache | |||
| _bans = new ConcurrentDictionary<ulong, bool>(); | |||
| EveryoneRole = _client.Roles.GetOrAdd(id, id); | |||
| } | |||
| internal override bool LoadReferences() | |||
| { | |||
| _afkChannel.Load(); | |||
| _owner.Load(); | |||
| return true; | |||
| /// <summary> Represents a Discord server (also known as a guild). </summary> | |||
| public sealed class Server | |||
| { | |||
| private struct Member | |||
| { | |||
| public readonly User User; | |||
| public readonly ServerPermissions Permissions; | |||
| public Member(User user) | |||
| { | |||
| User = user; | |||
| Permissions = new ServerPermissions(); | |||
| Permissions.Lock(); | |||
| } | |||
| } | |||
| internal override void UnloadReferences() | |||
| { | |||
| //Global Cache | |||
| var globalChannels = _client.Channels; | |||
| var channels = _channels; | |||
| foreach (var channel in channels) | |||
| globalChannels.TryRemove(channel.Key); | |||
| channels.Clear(); | |||
| var globalUsers = _client.Users; | |||
| var members = _members; | |||
| foreach (var member in members) | |||
| globalUsers.TryRemove(member.Key, Id); | |||
| members.Clear(); | |||
| var globalRoles = _client.Roles; | |||
| var roles = _roles; | |||
| foreach (var role in roles) | |||
| globalRoles.TryRemove(role.Key); | |||
| roles.Clear(); | |||
| //Local Cache | |||
| _bans.Clear(); | |||
| _afkChannel.Unload(); | |||
| private readonly ConcurrentDictionary<ulong, Role> _roles; | |||
| private readonly ConcurrentDictionary<ulong, Member> _users; | |||
| private readonly ConcurrentDictionary<ulong, Channel> _channels; | |||
| private readonly ConcurrentDictionary<ulong, bool> _bans; | |||
| private ulong _ownerId; | |||
| private ulong? _afkChannelId; | |||
| /// <summary> Gets the client that generated this server object. </summary> | |||
| internal DiscordClient Client { get; } | |||
| /// <summary> Gets the unique identifier for this server. </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> Gets the default channel for this server. </summary> | |||
| public Channel DefaultChannel { get; } | |||
| /// <summary> Gets the the role representing all users in a server. </summary> | |||
| public Role EveryoneRole { get; } | |||
| /// <summary> Gets the name of this server. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel, if one is set. </summary> | |||
| public int AFKTimeout { get; private set; } | |||
| /// <summary> Gets the date and time you joined this server. </summary> | |||
| public DateTime JoinedAt { get; private set; } | |||
| /// <summary> Gets the voice region for this server. </summary> | |||
| public Region Region { get; private set; } | |||
| /// <summary> Gets the unique identifier for this user's current avatar. </summary> | |||
| public string IconId { get; private set; } | |||
| /// <summary> Gets the URL to this user's current avatar. </summary> | |||
| public string IconUrl => GetIconUrl(Id, IconId); | |||
| internal static string GetIconUrl(ulong serverId, string iconId) | |||
| => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; | |||
| /// <summary> Gets the user that created this server. </summary> | |||
| public User Owner => GetUser(_ownerId); | |||
| /// <summary> Gets the AFK voice channel for this server. </summary> | |||
| public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; | |||
| /// <summary> Gets the current user in this server. </summary> | |||
| public User CurrentUser => GetUser(Client.CurrentUser.Id); | |||
| /// <summary> Gets a collection of the ids of all users banned on this server. </summary> | |||
| public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key); | |||
| /// <summary> Gets a collection of all channels within this server. </summary> | |||
| public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | |||
| /// <summary> Gets a collection of all users within this server with their server-specific data. </summary> | |||
| public IEnumerable<User> Users => _users.Select(x => x.Value.User); | |||
| /// <summary> Gets a collection of all roles within this server. </summary> | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| internal Server(DiscordClient client, ulong id) | |||
| { | |||
| Client = client; | |||
| Id = id; | |||
| _channels = new ConcurrentDictionary<ulong, Channel>(); | |||
| _roles = new ConcurrentDictionary<ulong, Role>(); | |||
| _users = new ConcurrentDictionary<ulong, Member>(); | |||
| _bans = new ConcurrentDictionary<ulong, bool>(); | |||
| DefaultChannel = AddChannel(id); | |||
| EveryoneRole = AddRole(id); | |||
| } | |||
| internal void Update(GuildReference model) | |||
| { | |||
| if (model.Name != null) | |||
| Name = model.Name; | |||
| } | |||
| internal void Update(GuildReference model) | |||
| { | |||
| if (model.Name != null) | |||
| Name = model.Name; | |||
| } | |||
| internal void Update(Guild model) | |||
| { | |||
| Update(model as GuildReference); | |||
| if (model.AFKTimeout != null) | |||
| AFKTimeout = model.AFKTimeout.Value; | |||
| if (model.AFKChannelId != null) | |||
| if (model.JoinedAt != null) | |||
| JoinedAt = model.JoinedAt.Value; | |||
| if (model.OwnerId != null) | |||
| _owner.Id = model.OwnerId.Value; | |||
| if (model.Region != null) | |||
| Region = model.Region; | |||
| if (model.Icon != null) | |||
| IconId = model.Icon; | |||
| if (model.Roles != null) | |||
| { | |||
| var roleCache = _client.Roles; | |||
| foreach (var x in model.Roles) | |||
| { | |||
| var role = roleCache.GetOrAdd(x.Id, Id); | |||
| role.Update(x); | |||
| } | |||
| { | |||
| Update(model as GuildReference); | |||
| if (model.AFKTimeout != null) | |||
| AFKTimeout = model.AFKTimeout.Value; | |||
| _afkChannelId = model.AFKChannelId.Value; //Can be null | |||
| if (model.JoinedAt != null) | |||
| JoinedAt = model.JoinedAt.Value; | |||
| if (model.OwnerId != null) | |||
| _ownerId = model.OwnerId.Value; | |||
| if (model.Region != null) | |||
| Region = Client.GetRegion(model.Region); | |||
| if (model.Icon != null) | |||
| IconId = model.Icon; | |||
| if (model.Roles != null) | |||
| { | |||
| foreach (var x in model.Roles) | |||
| AddRole(x.Id).Update(x); | |||
| } | |||
| _afkChannel.Id = model.AFKChannelId; //Can be null | |||
| } | |||
| internal void Update(ExtendedGuild model) | |||
| { | |||
| Update(model as APIGuild); | |||
| var channels = _client.Channels; | |||
| foreach (var subModel in model.Channels) | |||
| { | |||
| var channel = channels.GetOrAdd(subModel.Id, Id); | |||
| channel.Update(subModel); | |||
| } | |||
| var usersCache = _client.Users; | |||
| foreach (var subModel in model.Members) | |||
| { | |||
| var user = usersCache.GetOrAdd(subModel.User.Id, Id); | |||
| user.Update(subModel); | |||
| } | |||
| foreach (var subModel in model.VoiceStates) | |||
| { | |||
| var user = usersCache[subModel.UserId, Id]; | |||
| if (user != null) | |||
| user.Update(subModel); | |||
| } | |||
| foreach (var subModel in model.Presences) | |||
| { | |||
| var user = usersCache[subModel.User.Id, Id]; | |||
| if (user != null) | |||
| user.Update(subModel); | |||
| } | |||
| } | |||
| } | |||
| internal void Update(ExtendedGuild model) | |||
| { | |||
| Update(model as Guild); | |||
| if (model.Channels != null) | |||
| { | |||
| foreach (var subModel in model.Channels) | |||
| AddChannel(subModel.Id).Update(subModel); | |||
| } | |||
| if (model.Members != null) | |||
| { | |||
| foreach (var subModel in model.Members) | |||
| AddMember(subModel.User.Id).Update(subModel); | |||
| } | |||
| if (model.VoiceStates != null) | |||
| { | |||
| foreach (var subModel in model.VoiceStates) | |||
| GetUser(subModel.UserId)?.Update(subModel); | |||
| } | |||
| if (model.Presences != null) | |||
| { | |||
| foreach (var subModel in model.Presences) | |||
| GetUser(subModel.User.Id)?.Update(subModel); | |||
| } | |||
| } | |||
| internal void AddBan(ulong banId) | |||
| { | |||
| _bans.TryAdd(banId, true); | |||
| } | |||
| internal bool RemoveBan(ulong banId) | |||
| { | |||
| bool ignored; | |||
| return _bans.TryRemove(banId, out ignored); | |||
| } | |||
| //Bans | |||
| internal void AddBan(ulong banId) | |||
| => _bans.TryAdd(banId, true); | |||
| internal bool RemoveBan(ulong banId) | |||
| { | |||
| bool ignored; | |||
| return _bans.TryRemove(banId, out ignored); | |||
| } | |||
| internal void AddChannel(Channel channel) | |||
| { | |||
| if (_channels.TryAdd(channel.Id, channel)) | |||
| { | |||
| if (channel.Id == Id) | |||
| DefaultChannel = channel; | |||
| } | |||
| } | |||
| internal void RemoveChannel(Channel channel) | |||
| { | |||
| _channels.TryRemove(channel.Id, out channel); | |||
| } | |||
| //Channels | |||
| internal Channel AddChannel(ulong id) | |||
| => _channels.GetOrAdd(id, x => new Channel(Client, x, this)); | |||
| internal Channel RemoveChannel(ulong id) | |||
| { | |||
| Channel channel; | |||
| _channels.TryRemove(id, out channel); | |||
| return channel; | |||
| } | |||
| public Channel GetChannel(ulong id) | |||
| { | |||
| Channel result; | |||
| _channels.TryGetValue(id, out result); | |||
| return result; | |||
| } | |||
| internal void AddMember(User user) | |||
| { | |||
| if (_members.TryAdd(user.Id, new ServerMember(user))) | |||
| //Members | |||
| internal User AddMember(ulong id) | |||
| { | |||
| User newUser = null; | |||
| var user = _users.GetOrAdd(id, x => new Member(new User(id, this))); | |||
| if (user.User == newUser) | |||
| { | |||
| foreach (var channel in Channels) | |||
| channel.AddMember(user); | |||
| channel.AddUser(newUser); | |||
| } | |||
| return user.User; | |||
| } | |||
| internal void RemoveMember(User user) | |||
| internal User RemoveMember(ulong id) | |||
| { | |||
| ServerMember ignored; | |||
| if (_members.TryRemove(user.Id, out ignored)) | |||
| Member member; | |||
| if (_users.TryRemove(id, out member)) | |||
| { | |||
| foreach (var channel in Channels) | |||
| channel.RemoveMember(user); | |||
| channel.RemoveUser(id); | |||
| } | |||
| } | |||
| internal void HasMember(User user) => _members.ContainsKey(user.Id); | |||
| return member.User; | |||
| } | |||
| public User GetUser(ulong id) | |||
| { | |||
| Member result; | |||
| _users.TryGetValue(id, out result); | |||
| return result.User; | |||
| } | |||
| internal void AddRole(Role role) | |||
| { | |||
| if (_roles.TryAdd(role.Id, role)) | |||
| { | |||
| if (role.Id == Id) | |||
| EveryoneRole = role; | |||
| } | |||
| } | |||
| internal void RemoveRole(Role role) | |||
| { | |||
| _roles.TryRemove(role.Id, out role); | |||
| } | |||
| //Roles | |||
| internal Role AddRole(ulong id) | |||
| => _roles.GetOrAdd(id, x => new Role(x, this)); | |||
| internal Role RemoveRole(ulong id) | |||
| { | |||
| Role role; | |||
| _roles.TryRemove(id, out role); | |||
| return role; | |||
| } | |||
| public Role GetRole(ulong id) | |||
| { | |||
| Role result; | |||
| _roles.TryGetValue(id, out result); | |||
| return result; | |||
| } | |||
| internal ServerPermissions GetPermissions(User user) | |||
| //Permissions | |||
| internal ServerPermissions GetPermissions(User user) | |||
| { | |||
| ServerMember member; | |||
| if (_members.TryGetValue(user.Id, out member)) | |||
| Member member; | |||
| if (_users.TryGetValue(user.Id, out member)) | |||
| return member.Permissions; | |||
| else | |||
| return null; | |||
| } | |||
| internal void UpdatePermissions(User user) | |||
| { | |||
| ServerMember member; | |||
| if (_members.TryGetValue(user.Id, out member)) | |||
| Member member; | |||
| if (_users.TryGetValue(user.Id, out member)) | |||
| UpdatePermissions(member.User, member.Permissions); | |||
| } | |||
| private void UpdatePermissions(User user, ServerPermissions permissions) | |||
| { | |||
| uint newPermissions = 0; | |||
| if (user.IsOwner) | |||
| if (user.Id == _ownerId) | |||
| newPermissions = ServerPermissions.All.RawValue; | |||
| else | |||
| { | |||
| @@ -285,7 +231,7 @@ namespace Discord | |||
| newPermissions |= serverRole.Permissions.RawValue; | |||
| } | |||
| if (BitHelper.GetBit(newPermissions, (int)PermissionsBits.ManageRolesOrPermissions)) | |||
| if (newPermissions.HasBit((byte)PermissionsBits.ManageRolesOrPermissions)) | |||
| newPermissions = ServerPermissions.All.RawValue; | |||
| if (newPermissions != permissions.RawValue) | |||
| @@ -298,6 +244,6 @@ namespace Discord | |||
| public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); | |||
| public override string ToString() => Name ?? IdConvert.ToString(Id); | |||
| public override string ToString() => Name ?? Id.ToIdString(); | |||
| } | |||
| } | |||
| @@ -1,5 +1,4 @@ | |||
| using Discord.API.Client; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| @@ -7,8 +6,19 @@ using APIMember = Discord.API.Client.Member; | |||
| namespace Discord | |||
| { | |||
| public class User : CachedObject<ulong> | |||
| public class User | |||
| { | |||
| [Flags] | |||
| private enum VoiceState : byte | |||
| { | |||
| None = 0x0, | |||
| SelfMuted = 0x01, | |||
| SelfDeafened = 0x02, | |||
| ServerMuted = 0x04, | |||
| ServerDeafened = 0x08, | |||
| ServerSuppressed = 0x10, | |||
| } | |||
| internal struct CompositeKey : IEquatable<CompositeKey> | |||
| { | |||
| public ulong ServerId, UserId; | |||
| @@ -24,92 +34,71 @@ namespace Discord | |||
| => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); | |||
| } | |||
| public static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; | |||
| /// <summary> Returns a unique identifier combining this user's id with its server's. </summary> | |||
| internal CompositeKey UniqueId => new CompositeKey(_server.Id ?? 0, Id); | |||
| /// <summary> Returns the name of this user on this server. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Returns a by-name unique identifier separating this user from others with the same name. </summary> | |||
| public ushort Discriminator { get; private set; } | |||
| /// <summary> Returns the unique identifier for this user's current avatar. </summary> | |||
| public string AvatarId { get; private set; } | |||
| /// <summary> Returns the URL to this user's current avatar. </summary> | |||
| public string AvatarUrl => GetAvatarUrl(Id, AvatarId); | |||
| /// <summary> Returns the datetime that this user joined this server. </summary> | |||
| public DateTime JoinedAt { get; private set; } | |||
| internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; | |||
| private VoiceState _voiceState; | |||
| private DateTime? _lastOnline; | |||
| private ulong? _voiceChannelId; | |||
| private Dictionary<ulong, Role> _roles; | |||
| public bool IsSelfMuted { get; private set; } | |||
| public bool IsSelfDeafened { get; private set; } | |||
| public bool IsServerMuted { get; private set; } | |||
| public bool IsServerDeafened { get; private set; } | |||
| public bool IsServerSuppressed { get; private set; } | |||
| public bool IsPrivate => _server.Id == null; | |||
| public bool IsOwner => _server.Value.OwnerId == Id; | |||
| /// <summary> Gets the client that generated this user object. </summary> | |||
| internal DiscordClient Client { get; } | |||
| /// <summary> Gets the unique identifier for this user. </summary> | |||
| public ulong Id { get; } | |||
| /// <summary> Gets the server this user is a member of. </summary> | |||
| public Server Server { get; } | |||
| public string SessionId { get; private set; } | |||
| public string Token { get; private set; } | |||
| /// <summary> Gets the name of this user. </summary> | |||
| public string Name { get; private set; } | |||
| /// <summary> Gets an id uniquely identifying from others with the same name. </summary> | |||
| public ushort Discriminator { get; private set; } | |||
| /// <summary> Gets the unique identifier for this user's current avatar. </summary> | |||
| public string AvatarId { get; private set; } | |||
| /// <summary> Gets the id for the game this user is currently playing. </summary> | |||
| public string GameId { get; private set; } | |||
| /// <summary> Gets the current status for this user. </summary> | |||
| public UserStatus Status { get; private set; } | |||
| /// <summary> Gets the datetime that this user joined this server. </summary> | |||
| public DateTime JoinedAt { get; private set; } | |||
| /// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary> | |||
| public DateTime? LastActivityAt { get; private set; } | |||
| // /// <summary> Gets this user's voice session id. </summary> | |||
| // public string SessionId { get; private set; } | |||
| // /// <summary> Gets this user's voice token. </summary> | |||
| // public string Token { get; private set; } | |||
| /// <summary> Returns the id for the game this user is currently playing. </summary> | |||
| public int? GameId { get; private set; } | |||
| /// <summary> Returns the current status for this user. </summary> | |||
| public UserStatus Status { get; private set; } | |||
| /// <summary> Returns the time this user last sent/edited a message, started typing or sent voice data in this server. </summary> | |||
| public DateTime? LastActivityAt { get; private set; } | |||
| /// <summary> Returns the string used to mention this user. </summary> | |||
| public string Mention => $"<@{Id}>"; | |||
| /// <summary> Returns true if this user has marked themselves as muted. </summary> | |||
| public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0; | |||
| /// <summary> Returns true if this user has marked themselves as deafened. </summary> | |||
| public bool IsSelfDeafened => (_voiceState & VoiceState.SelfDeafened) != 0; | |||
| /// <summary> Returns true if the server is blocking audio from this user. </summary> | |||
| public bool IsServerMuted => (_voiceState & VoiceState.ServerMuted) != 0; | |||
| /// <summary> Returns true if the server is blocking audio to this user. </summary> | |||
| public bool IsServerDeafened => (_voiceState & VoiceState.ServerDeafened) != 0; | |||
| /// <summary> Returns true if the server is temporarily blocking audio to/from this user. </summary> | |||
| public bool IsServerSuppressed => (_voiceState & VoiceState.ServerSuppressed) != 0; | |||
| /// <summary> Returns the time this user was last seen online in this server. </summary> | |||
| public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; | |||
| private DateTime? _lastOnline; | |||
| //References | |||
| [JsonIgnore] | |||
| public GlobalUser Global => _globalUser.Value; | |||
| private readonly Reference<GlobalUser> _globalUser; | |||
| [JsonIgnore] | |||
| public Server Server => _server.Value; | |||
| private readonly Reference<Server> _server; | |||
| [JsonProperty] | |||
| private ulong? ServerId { get { return _server.Id; } set { _server.Id = value; } } | |||
| [JsonIgnore] | |||
| public Channel VoiceChannel => _voiceChannel.Value; | |||
| private Reference<Channel> _voiceChannel; | |||
| [JsonProperty] | |||
| private ulong? VoiceChannelId { get { return _voiceChannel.Id; } set { _voiceChannel.Id = value; } } | |||
| //Collections | |||
| [JsonIgnore] | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| private Dictionary<ulong, Role> _roles; | |||
| [JsonProperty] | |||
| private IEnumerable<ulong> RoleIds => _roles.Select(x => x.Key); | |||
| /// <summary> Returns a collection of all messages this user has sent on this server that are still in cache. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Message> Messages | |||
| { | |||
| get | |||
| { | |||
| if (_server.Id != null) | |||
| return Server.Channels.SelectMany(x => x.Messages.Where(y => y.User.Id == Id)); | |||
| else | |||
| return Global.PrivateChannel.Messages.Where(x => x.User.Id == Id); | |||
| } | |||
| } | |||
| public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; | |||
| /// <summary> Gets this user's </summary> | |||
| public Channel VoiceChannel => _voiceChannelId != null ? Server.GetChannel(_voiceChannelId.Value) : null; | |||
| /// <summary> Gets the URL to this user's current avatar. </summary> | |||
| public string AvatarUrl => GetAvatarUrl(Id, AvatarId); | |||
| /// <summary> Gets all roles that have been assigned to this user, including the everyone role. </summary> | |||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | |||
| /// <summary> Returns a collection of all channels this user has permissions to join on this server. </summary> | |||
| [JsonIgnore] | |||
| public IEnumerable<Channel> Channels | |||
| /// <summary> Returns a collection of all channels this user has permissions to join on this server. </summary> | |||
| public IEnumerable<Channel> Channels | |||
| { | |||
| get | |||
| { | |||
| if (_server.Id != null) | |||
| if (Server != null) | |||
| { | |||
| if (_client.Config.UsePermissionsCache) | |||
| if (Client.Config.UsePermissionsCache) | |||
| { | |||
| return Server.Channels | |||
| .Where(x => (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || | |||
| return Server.Channels.Where(x => | |||
| (x.Type == ChannelType.Text && x.GetPermissions(this).ReadMessages) || | |||
| (x.Type == ChannelType.Voice && x.GetPermissions(this).Connect)); | |||
| } | |||
| else | |||
| @@ -120,63 +109,37 @@ namespace Discord | |||
| { | |||
| x.UpdatePermissions(this, perms); | |||
| return (x.Type == ChannelType.Text && perms.ReadMessages) || | |||
| (x.Type == ChannelType.Voice && perms.Connect); | |||
| (x.Type == ChannelType.Voice && perms.Connect); | |||
| }); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| var privateChannel = Global.PrivateChannel; | |||
| if (privateChannel != null) | |||
| return new Channel[] { privateChannel }; | |||
| else | |||
| return new Channel[0]; | |||
| if (this == Client.PrivateUser) | |||
| return Client.PrivateChannels; | |||
| else | |||
| { | |||
| var privateChannel = Client.GetPrivateChannel(Id); | |||
| if (privateChannel != null) | |||
| return new Channel[] { privateChannel }; | |||
| else | |||
| return new Channel[0]; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /// <summary> Returns the string used to mention this user. </summary> | |||
| public string Mention => $"<@{Id}>"; | |||
| internal User(DiscordClient client, ulong id, ulong? serverId) | |||
| : base(client, id) | |||
| internal User(ulong id, Server server) | |||
| { | |||
| _globalUser = new Reference<GlobalUser>(id, | |||
| x => _client.GlobalUsers.GetOrAdd(x), | |||
| x => x.AddUser(this), | |||
| x => x.RemoveUser(this)); | |||
| _server = new Reference<Server>(serverId, | |||
| x => _client.Servers[x], | |||
| x => | |||
| { | |||
| x.AddMember(this); | |||
| if (Id == _client.CurrentUser.Id) | |||
| x.CurrentUser = this; | |||
| }, | |||
| x => | |||
| { | |||
| x.RemoveMember(this); | |||
| if (Id == _client.CurrentUser.Id) | |||
| x.CurrentUser = null; | |||
| }); | |||
| _voiceChannel = new Reference<Channel>(x => _client.Channels[x]); | |||
| Server = server; | |||
| _roles = new Dictionary<ulong, Role>(); | |||
| Status = UserStatus.Offline; | |||
| if (serverId == null) | |||
| if (server == null) | |||
| UpdateRoles(null); | |||
| } | |||
| internal override bool LoadReferences() | |||
| { | |||
| return _globalUser.Load() && | |||
| (IsPrivate || _server.Load()); | |||
| } | |||
| internal override void UnloadReferences() | |||
| { | |||
| _globalUser.Unload(); | |||
| _server.Unload(); | |||
| } | |||
| internal void Update(UserReference model) | |||
| { | |||
| @@ -195,24 +158,29 @@ namespace Discord | |||
| if (model.JoinedAt.HasValue) | |||
| JoinedAt = model.JoinedAt.Value; | |||
| if (model.Roles != null) | |||
| UpdateRoles(model.Roles.Select(x => _client.Roles[x])); | |||
| UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); | |||
| } | |||
| internal void Update(ExtendedGuild.ExtendedMemberInfo model) | |||
| { | |||
| Update(model as APIMember); | |||
| if (model.IsServerMuted == true) | |||
| _voiceState |= VoiceState.ServerMuted; | |||
| else if (model.IsServerMuted == false) | |||
| _voiceState &= ~VoiceState.ServerMuted; | |||
| if (model.IsServerDeafened != null) | |||
| IsServerDeafened = model.IsServerDeafened.Value; | |||
| if (model.IsServerMuted != null) | |||
| IsServerMuted = model.IsServerMuted.Value; | |||
| } | |||
| if (model.IsServerDeafened.Value == true) | |||
| _voiceState |= VoiceState.ServerDeafened; | |||
| else if (model.IsServerDeafened.Value == false) | |||
| _voiceState &= ~VoiceState.ServerDeafened; | |||
| } | |||
| internal void Update(MemberPresence model) | |||
| { | |||
| if (model.User != null) | |||
| Update(model.User as UserReference); | |||
| if (model.Roles != null) | |||
| UpdateRoles(model.Roles.Select(x => _client.Roles[x])); | |||
| if (model.Roles != null) | |||
| UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); | |||
| if (model.Status != null && Status != model.Status) | |||
| { | |||
| Status = UserStatus.FromString(model.Status); | |||
| @@ -223,42 +191,55 @@ namespace Discord | |||
| GameId = model.GameId; //Allows null | |||
| } | |||
| internal void Update(MemberVoiceState model) | |||
| { | |||
| if (model.IsServerDeafened != null) | |||
| IsServerDeafened = model.IsServerDeafened.Value; | |||
| if (model.IsServerMuted != null) | |||
| IsServerMuted = model.IsServerMuted.Value; | |||
| if (model.SessionId != null) | |||
| { | |||
| if (model.IsSelfMuted.Value == true) | |||
| _voiceState |= VoiceState.SelfMuted; | |||
| else if (model.IsSelfMuted.Value == false) | |||
| _voiceState &= ~VoiceState.SelfMuted; | |||
| if (model.IsSelfDeafened.Value == true) | |||
| _voiceState |= VoiceState.SelfDeafened; | |||
| else if (model.IsSelfDeafened.Value == false) | |||
| _voiceState &= ~VoiceState.SelfDeafened; | |||
| if (model.IsServerMuted == true) | |||
| _voiceState |= VoiceState.ServerMuted; | |||
| else if (model.IsServerMuted == false) | |||
| _voiceState &= ~VoiceState.ServerMuted; | |||
| if (model.IsServerDeafened.Value == true) | |||
| _voiceState |= VoiceState.ServerDeafened; | |||
| else if (model.IsServerDeafened.Value == false) | |||
| _voiceState &= ~VoiceState.ServerDeafened; | |||
| if (model.IsServerSuppressed.Value == true) | |||
| _voiceState |= VoiceState.ServerSuppressed; | |||
| else if (model.IsServerSuppressed.Value == false) | |||
| _voiceState &= ~VoiceState.ServerSuppressed; | |||
| /*if (model.SessionId != null) | |||
| SessionId = model.SessionId; | |||
| if (model.Token != null) | |||
| Token = model.Token; | |||
| if (model.IsSelfDeafened != null) | |||
| IsSelfDeafened = model.IsSelfDeafened.Value; | |||
| if (model.IsSelfMuted != null) | |||
| IsSelfMuted = model.IsSelfMuted.Value; | |||
| if (model.IsServerSuppressed != null) | |||
| IsServerSuppressed = model.IsServerSuppressed.Value; | |||
| Token = model.Token;*/ | |||
| _voiceChannel.Id = model.ChannelId; //Allows null | |||
| _voiceChannelId = model.ChannelId; //Allows null | |||
| } | |||
| private void UpdateRoles(IEnumerable<Role> roles) | |||
| { | |||
| var newRoles = new Dictionary<ulong, Role>(); | |||
| if (roles != null) | |||
| { | |||
| foreach (var r in roles) | |||
| newRoles[r.Id] = r; | |||
| foreach (var r in roles) | |||
| { | |||
| if (r != null) | |||
| newRoles[r.Id] = r; | |||
| } | |||
| } | |||
| if (_server.Id != null) | |||
| if (Server != null) | |||
| { | |||
| var everyone = Server.EveryoneRole; | |||
| newRoles.Add(everyone.Id, everyone); | |||
| newRoles[everyone.Id] = everyone; | |||
| } | |||
| _roles = newRoles; | |||
| if (!IsPrivate) | |||
| if (Server != null) | |||
| Server.UpdatePermissions(this); | |||
| } | |||
| @@ -285,6 +266,6 @@ namespace Discord | |||
| public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; | |||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); | |||
| public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : IdConvert.ToString(Id); | |||
| public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); | |||
| } | |||
| } | |||
| @@ -4,7 +4,7 @@ using System.Runtime.Serialization; | |||
| namespace Discord.Net | |||
| { | |||
| #if NET45 | |||
| #if NET46 | |||
| [Serializable] | |||
| #endif | |||
| public class HttpException : Exception | |||
| @@ -16,7 +16,7 @@ namespace Discord.Net | |||
| { | |||
| StatusCode = statusCode; | |||
| } | |||
| #if NET45 | |||
| #if NET46 | |||
| public override void GetObjectData(SerializationInfo info, StreamingContext context) | |||
| => base.GetObjectData(info, context); | |||
| #endif | |||
| @@ -1,29 +0,0 @@ | |||
| using System; | |||
| namespace Discord.Net.Rest | |||
| { | |||
| public sealed partial class RestClient | |||
| { | |||
| public class RequestEventArgs : EventArgs | |||
| { | |||
| public string Method { get; } | |||
| public string Path { get; } | |||
| public string Payload { get; } | |||
| public double ElapsedMilliseconds { get; } | |||
| public RequestEventArgs(string method, string path, string payload, double milliseconds) | |||
| { | |||
| Method = method; | |||
| Path = path; | |||
| Payload = payload; | |||
| ElapsedMilliseconds = milliseconds; | |||
| } | |||
| } | |||
| public event EventHandler<RequestEventArgs> OnRequest; | |||
| private void RaiseOnRequest(string method, string path, string payload, double milliseconds) | |||
| { | |||
| if (OnRequest != null) | |||
| OnRequest(this, new RequestEventArgs(method, path, payload, milliseconds)); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using Discord.API; | |||
| using Discord.Logging; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Diagnostics; | |||
| @@ -7,25 +8,53 @@ using System.Threading.Tasks; | |||
| namespace Discord.Net.Rest | |||
| { | |||
| public sealed partial class RestClient | |||
| public class RequestEventArgs : EventArgs | |||
| { | |||
| public string Method { get; } | |||
| public string Path { get; } | |||
| public string Payload { get; } | |||
| public double ElapsedMilliseconds { get; } | |||
| public RequestEventArgs(string method, string path, string payload, double milliseconds) | |||
| { | |||
| Method = method; | |||
| Path = path; | |||
| Payload = payload; | |||
| ElapsedMilliseconds = milliseconds; | |||
| } | |||
| } | |||
| public sealed partial class RestClient | |||
| { | |||
| private readonly DiscordConfig _config; | |||
| private readonly IRestEngine _engine; | |||
| private CancellationToken _cancelToken; | |||
| private string _token; | |||
| internal Logger Logger { get; } | |||
| public CancellationToken CancelToken { get; set; } | |||
| public string Token | |||
| { | |||
| get { return _token; } | |||
| set | |||
| { | |||
| _token = value; | |||
| _engine.SetToken(value); | |||
| } | |||
| } | |||
| public RestClient(DiscordConfig config, Logger logger, string baseUrl) | |||
| public RestClient(DiscordConfig config, string baseUrl, Logger logger) | |||
| { | |||
| _config = config; | |||
| Logger = logger; | |||
| #if !DOTNET5_4 | |||
| _engine = new RestSharpEngine(config, logger, baseUrl); | |||
| _engine = new RestSharpEngine(config, baseUrl); | |||
| #else | |||
| //_engine = new BuiltInRestEngine(config, logger, baseUrl); | |||
| //_engine = new BuiltInRestEngine(config, baseUrl); | |||
| #endif | |||
| } | |||
| public void SetToken(string token) => _engine.SetToken(token); | |||
| public void SetCancelToken(CancellationToken token) => _cancelToken = token; | |||
| public async Task<ResponseT> Send<ResponseT>(IRestRequest<ResponseT> request) | |||
| where ResponseT : class | |||
| { | |||
| @@ -69,24 +98,26 @@ namespace Discord.Net.Rest | |||
| requestJson = JsonConvert.SerializeObject(payload); | |||
| Stopwatch stopwatch = null; | |||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||
| if (Logger.Level >= LogSeverity.Verbose) | |||
| stopwatch = Stopwatch.StartNew(); | |||
| string responseJson = await _engine.Send(method, path, requestJson, _cancelToken).ConfigureAwait(false); | |||
| string responseJson = await _engine.Send(method, path, requestJson, CancelToken).ConfigureAwait(false); | |||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||
| if (Logger.Level >= LogSeverity.Verbose) | |||
| { | |||
| stopwatch.Stop(); | |||
| if (payload != null && _config.LogLevel >= LogSeverity.Debug) | |||
| double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2); | |||
| string log = $"{method} {path}: {milliseconds} ms"; | |||
| if (payload != null && _config.LogLevel >= LogSeverity.Debug) | |||
| { | |||
| if (isPrivate) | |||
| RaiseOnRequest(method, path, "[Hidden]", stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
| else | |||
| RaiseOnRequest(method, path, requestJson, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
| log += $" [Hidden]"; | |||
| else | |||
| log += $" {requestJson}"; | |||
| } | |||
| else | |||
| RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
| } | |||
| Logger.Verbose(log); | |||
| } | |||
| return responseJson; | |||
| } | |||
| @@ -100,19 +131,21 @@ namespace Discord.Net.Rest | |||
| var isPrivate = request.IsPrivate; | |||
| Stopwatch stopwatch = null; | |||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||
| if (Logger.Level >= LogSeverity.Verbose) | |||
| stopwatch = Stopwatch.StartNew(); | |||
| string responseJson = await _engine.SendFile(method, path, filename, stream, _cancelToken).ConfigureAwait(false); | |||
| string responseJson = await _engine.SendFile(method, path, filename, stream, CancelToken).ConfigureAwait(false); | |||
| if (_config.LogLevel >= LogSeverity.Verbose) | |||
| if (Logger.Level >= LogSeverity.Verbose) | |||
| { | |||
| stopwatch.Stop(); | |||
| if (_config.LogLevel >= LogSeverity.Debug && !isPrivate) | |||
| RaiseOnRequest(method, path, filename, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
| else | |||
| RaiseOnRequest(method, path, null, stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond); | |||
| } | |||
| double milliseconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerMillisecond, 2); | |||
| string log = $"{method} {path}: {milliseconds} ms"; | |||
| if (_config.LogLevel >= LogSeverity.Debug && !isPrivate) | |||
| log += $" {filename}"; | |||
| Logger.Verbose(log); | |||
| } | |||
| return responseJson; | |||
| } | |||
| @@ -12,15 +12,13 @@ namespace Discord.Net.Rest | |||
| { | |||
| private readonly DiscordConfig _config; | |||
| private readonly RestSharp.RestClient _client; | |||
| private readonly Logger _logger; | |||
| private readonly object _rateLimitLock; | |||
| private DateTime _rateLimitTime; | |||
| public RestSharpEngine(DiscordConfig config, Logger logger, string baseUrl) | |||
| public RestSharpEngine(DiscordConfig config, string baseUrl) | |||
| { | |||
| _config = config; | |||
| _logger = logger; | |||
| _rateLimitLock = new object(); | |||
| _client = new RestSharp.RestClient(baseUrl) | |||
| { | |||
| @@ -28,9 +26,6 @@ namespace Discord.Net.Rest | |||
| ReadWriteTimeout = _config.RestTimeout, | |||
| UserAgent = config.UserAgent | |||
| }; | |||
| /*if (_config.ProxyUrl != null) | |||
| _client.Proxy = new WebProxy(_config.ProxyUrl, true, new string[0], _config.ProxyCredentials); | |||
| else*/ | |||
| _client.Proxy = null; | |||
| _client.RemoveDefaultParameter("Accept"); | |||
| _client.AddDefaultHeader("accept", "*/*"); | |||
| @@ -83,21 +78,18 @@ namespace Discord.Net.Rest | |||
| int milliseconds; | |||
| if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) | |||
| { | |||
| if (_logger != null) | |||
| /*var now = DateTime.UtcNow; | |||
| if (now >= _rateLimitTime) | |||
| { | |||
| var now = DateTime.UtcNow; | |||
| if (now >= _rateLimitTime) | |||
| lock (_rateLimitLock) | |||
| { | |||
| lock (_rateLimitLock) | |||
| if (now >= _rateLimitTime) | |||
| { | |||
| if (now >= _rateLimitTime) | |||
| { | |||
| _rateLimitTime = now.AddMilliseconds(milliseconds); | |||
| _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); | |||
| } | |||
| _rateLimitTime = now.AddMilliseconds(milliseconds); | |||
| _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); | |||
| } | |||
| } | |||
| } | |||
| }*/ | |||
| await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); | |||
| continue; | |||
| } | |||
| @@ -2,7 +2,7 @@ | |||
| namespace Discord.Net | |||
| { | |||
| #if NET45 | |||
| #if NET46 | |||
| [Serializable] | |||
| #endif | |||
| public sealed class TimeoutException : OperationCanceledException | |||
| @@ -1,5 +1,6 @@ | |||
| using Discord.API.Client; | |||
| using Discord.API.Client.GatewaySocket; | |||
| using Discord.Logging; | |||
| using Newtonsoft.Json; | |||
| using Newtonsoft.Json.Linq; | |||
| using System; | |||
| @@ -10,14 +11,13 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| public partial class GatewaySocket : WebSocket | |||
| { | |||
| public int LastSequence => _lastSeq; | |||
| private int _lastSeq; | |||
| public string SessionId => _sessionId; | |||
| private int _lastSequence; | |||
| private string _sessionId; | |||
| public GatewaySocket(DiscordClient client, Logger logger) | |||
| : base(client, logger) | |||
| public string Token { get; private set; } | |||
| public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger) | |||
| : base(client, serializer, logger) | |||
| { | |||
| Disconnected += async (s, e) => | |||
| { | |||
| @@ -26,10 +26,11 @@ namespace Discord.Net.WebSockets | |||
| }; | |||
| } | |||
| public async Task Connect() | |||
| public async Task Connect(string token) | |||
| { | |||
| await BeginConnect().ConfigureAwait(false); | |||
| SendIdentify(); | |||
| Token = token; | |||
| await BeginConnect().ConfigureAwait(false); | |||
| SendIdentify(token); | |||
| } | |||
| private async Task Redirect() | |||
| { | |||
| @@ -46,13 +47,13 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| try | |||
| { | |||
| await Connect().ConfigureAwait(false); | |||
| await Connect(Token).ConfigureAwait(false); | |||
| break; | |||
| } | |||
| catch (OperationCanceledException) { throw; } | |||
| catch (Exception ex) | |||
| { | |||
| _logger.Log(LogSeverity.Error, $"Reconnect failed", ex); | |||
| Logger.Error("Reconnect failed", ex); | |||
| //Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||
| await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||
| } | |||
| @@ -60,13 +61,13 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| } | |||
| public Task Disconnect() => TaskManager.Stop(); | |||
| public Task Disconnect() => _taskManager.Stop(); | |||
| protected override async Task Run() | |||
| { | |||
| List<Task> tasks = new List<Task>(); | |||
| tasks.AddRange(_engine.GetTasks(_cancelToken)); | |||
| tasks.Add(HeartbeatAsync(_cancelToken)); | |||
| tasks.AddRange(_engine.GetTasks(CancelToken)); | |||
| tasks.Add(HeartbeatAsync(CancelToken)); | |||
| await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); | |||
| } | |||
| @@ -75,7 +76,7 @@ namespace Discord.Net.WebSockets | |||
| await base.ProcessMessage(json).ConfigureAwait(false); | |||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(json); | |||
| if (msg.Sequence.HasValue) | |||
| _lastSeq = msg.Sequence.Value; | |||
| _lastSequence = msg.Sequence.Value; | |||
| var opCode = (OpCodes)msg.Operation; | |||
| switch (opCode) | |||
| @@ -105,20 +106,18 @@ namespace Discord.Net.WebSockets | |||
| if (payload.Url != null) | |||
| { | |||
| Host = payload.Url; | |||
| if (_logger.Level >= LogSeverity.Info) | |||
| _logger.Info("Redirected to " + payload.Url); | |||
| Logger.Info("Redirected to " + payload.Url); | |||
| await Redirect().ConfigureAwait(false); | |||
| } | |||
| } | |||
| break; | |||
| default: | |||
| if (_logger.Level >= LogSeverity.Warning) | |||
| _logger.Log(LogSeverity.Warning, $"Unknown Opcode: {opCode}"); | |||
| Logger.Warning($"Unknown Opcode: {opCode}"); | |||
| break; | |||
| } | |||
| } | |||
| public void SendIdentify() | |||
| public void SendIdentify(string token) | |||
| { | |||
| var props = new Dictionary<string, string> | |||
| { | |||
| @@ -127,7 +126,7 @@ namespace Discord.Net.WebSockets | |||
| var msg = new IdentifyCommand() | |||
| { | |||
| Version = 3, | |||
| Token = _client.Token, | |||
| Token = token, | |||
| Properties = props, | |||
| LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null, | |||
| UseCompression = true | |||
| @@ -136,7 +135,7 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| public void SendResume() | |||
| => QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSeq }); | |||
| => QueueMessage(new ResumeCommand { SessionId = _sessionId, Sequence = _lastSequence }); | |||
| public override void SendHeartbeat() | |||
| => QueueMessage(new HeartbeatCommand()); | |||
| public void SendUpdateStatus(long? idleSince, int? gameId) | |||
| @@ -13,30 +13,22 @@ namespace Discord.Net.WebSockets | |||
| internal class WS4NetEngine : IWebSocketEngine | |||
| { | |||
| private readonly DiscordConfig _config; | |||
| private readonly Logger _logger; | |||
| private readonly ConcurrentQueue<string> _sendQueue; | |||
| private readonly WebSocket _parent; | |||
| private readonly TaskManager _taskManager; | |||
| private WS4NetWebSocket _webSocket; | |||
| private ManualResetEventSlim _waitUntilConnect; | |||
| public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage; | |||
| public event EventHandler<WebSocketTextMessageEventArgs> TextMessage; | |||
| private void RaiseBinaryMessage(byte[] data) | |||
| { | |||
| if (BinaryMessage != null) | |||
| BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data)); | |||
| } | |||
| private void RaiseTextMessage(string msg) | |||
| { | |||
| if (TextMessage != null) | |||
| TextMessage(this, new WebSocketTextMessageEventArgs(msg)); | |||
| } | |||
| public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage = delegate { }; | |||
| public event EventHandler<WebSocketTextMessageEventArgs> TextMessage = delegate { }; | |||
| private void OnBinaryMessage(byte[] data) | |||
| => BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data)); | |||
| private void OnTextMessage(string msg) | |||
| => TextMessage(this, new WebSocketTextMessageEventArgs(msg)); | |||
| internal WS4NetEngine(WebSocket parent, DiscordConfig config, Logger logger) | |||
| internal WS4NetEngine(DiscordConfig config, TaskManager taskManager) | |||
| { | |||
| _parent = parent; | |||
| _config = config; | |||
| _logger = logger; | |||
| _taskManager = taskManager; | |||
| _sendQueue = new ConcurrentQueue<string>(); | |||
| _waitUntilConnect = new ManualResetEventSlim(); | |||
| } | |||
| @@ -57,7 +49,7 @@ namespace Discord.Net.WebSockets | |||
| _waitUntilConnect.Reset(); | |||
| _webSocket.Open(); | |||
| _waitUntilConnect.Wait(cancelToken); | |||
| _parent.TaskManager.ThrowException(); //In case our connection failed | |||
| _taskManager.ThrowException(); //In case our connection failed | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| @@ -84,27 +76,25 @@ namespace Discord.Net.WebSockets | |||
| private void OnWebSocketError(object sender, ErrorEventArgs e) | |||
| { | |||
| _parent.TaskManager.SignalError(e.Exception); | |||
| _taskManager.SignalError(e.Exception); | |||
| _waitUntilConnect.Set(); | |||
| } | |||
| private void OnWebSocketClosed(object sender, EventArgs e) | |||
| { | |||
| var ex = new Exception($"Connection lost or close message received."); | |||
| _parent.TaskManager.SignalError(ex, isUnexpected: true); | |||
| Exception ex; | |||
| if (e is ClosedEventArgs) | |||
| ex = new Exception($"Received close code {(e as ClosedEventArgs).Code}: {(e as ClosedEventArgs).Reason ?? "No reason"}"); | |||
| else | |||
| ex = new Exception($"Connection lost"); | |||
| _taskManager.SignalError(ex, isUnexpected: true); | |||
| _waitUntilConnect.Set(); | |||
| } | |||
| private void OnWebSocketOpened(object sender, EventArgs e) | |||
| { | |||
| _waitUntilConnect.Set(); | |||
| } | |||
| => _waitUntilConnect.Set(); | |||
| private void OnWebSocketText(object sender, MessageReceivedEventArgs e) | |||
| { | |||
| RaiseTextMessage(e.Message); | |||
| } | |||
| => OnTextMessage(e.Message); | |||
| private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) | |||
| { | |||
| RaiseBinaryMessage(e.Data); | |||
| } | |||
| => OnBinaryMessage(e.Data); | |||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; | |||
| @@ -128,9 +118,7 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| public void QueueMessage(string message) | |||
| { | |||
| _sendQueue.Enqueue(message); | |||
| } | |||
| => _sendQueue.Enqueue(message); | |||
| } | |||
| } | |||
| #endif | |||
| @@ -1,4 +1,5 @@ | |||
| using Discord.API.Client; | |||
| using Discord.Logging; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.IO; | |||
| @@ -14,88 +15,59 @@ namespace Discord.Net.WebSockets | |||
| protected readonly IWebSocketEngine _engine; | |||
| protected readonly DiscordClient _client; | |||
| protected readonly ManualResetEventSlim _connectedEvent; | |||
| protected int _heartbeatInterval; | |||
| private DateTime _lastHeartbeat; | |||
| public CancellationToken? ParentCancelToken { get; set; } | |||
| public CancellationToken CancelToken => _cancelToken; | |||
| protected readonly TaskManager _taskManager; | |||
| protected readonly JsonSerializer _serializer; | |||
| protected CancellationTokenSource _cancelTokenSource; | |||
| protected CancellationToken _cancelToken; | |||
| public JsonSerializer Serializer => _serializer; | |||
| protected JsonSerializer _serializer; | |||
| protected int _heartbeatInterval; | |||
| private DateTime _lastHeartbeat; | |||
| /// <summary> Gets the logger used for this client. </summary> | |||
| internal Logger Logger { get; } | |||
| internal TaskManager TaskManager => _taskManager; | |||
| protected readonly TaskManager _taskManager; | |||
| public CancellationToken CancelToken { get; private set; } | |||
| public Logger Logger => _logger; | |||
| protected readonly Logger _logger; | |||
| public CancellationToken? ParentCancelToken { get; set; } | |||
| public string Host { get { return _host; } set { _host = value; } } | |||
| private string _host; | |||
| public string Host { get; set; } | |||
| /// <summary> Gets the current connection state of this client. </summary> | |||
| public ConnectionState State { get; private set; } | |||
| public ConnectionState State => _state; | |||
| protected ConnectionState _state; | |||
| public event EventHandler Connected = delegate { }; | |||
| private void OnConnected() | |||
| => Connected(this, EventArgs.Empty); | |||
| public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { }; | |||
| private void OnDisconnected(bool wasUnexpected, Exception error) | |||
| => Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | |||
| public event EventHandler Connected; | |||
| private void RaiseConnected() | |||
| { | |||
| if (_logger.Level >= LogSeverity.Info) | |||
| _logger.Info( "Connected"); | |||
| if (Connected != null) | |||
| Connected(this, EventArgs.Empty); | |||
| } | |||
| public event EventHandler<DisconnectedEventArgs> Disconnected; | |||
| private void RaiseDisconnected(bool wasUnexpected, Exception error) | |||
| { | |||
| if (_logger.Level >= LogSeverity.Info) | |||
| _logger.Info( "Disconnected"); | |||
| if (Disconnected != null) | |||
| Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); | |||
| } | |||
| public WebSocket(DiscordClient client, Logger logger) | |||
| public WebSocket(DiscordClient client, JsonSerializer serializer, Logger logger) | |||
| { | |||
| _client = client; | |||
| _logger = logger; | |||
| Logger = logger; | |||
| _serializer = serializer; | |||
| _lock = new Semaphore(1, 1); | |||
| _taskManager = new TaskManager(Cleanup); | |||
| _cancelToken = new CancellationToken(true); | |||
| CancelToken = new CancellationToken(true); | |||
| _connectedEvent = new ManualResetEventSlim(false); | |||
| #if !DOTNET5_4 | |||
| _engine = new WS4NetEngine(this, client.Config, _logger); | |||
| _engine = new WS4NetEngine(client.Config, _taskManager); | |||
| #else | |||
| //_engine = new BuiltInWebSocketEngine(this, client.Config, _logger); | |||
| //_engine = new BuiltInWebSocketEngine(this, client.Config); | |||
| #endif | |||
| _engine.BinaryMessage += (s, e) => | |||
| { | |||
| using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | |||
| using (var decompressed = new MemoryStream()) | |||
| { | |||
| using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
| zlib.CopyTo(decompressed); | |||
| decompressed.Position = 0; | |||
| { | |||
| using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) | |||
| using (var decompressed = new MemoryStream()) | |||
| { | |||
| using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) | |||
| zlib.CopyTo(decompressed); | |||
| decompressed.Position = 0; | |||
| using (var reader = new StreamReader(decompressed)) | |||
| ProcessMessage(reader.ReadToEnd()).Wait(); | |||
| } | |||
| ProcessMessage(reader.ReadToEnd()).Wait(); | |||
| } | |||
| }; | |||
| _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); | |||
| _serializer = new JsonSerializer(); | |||
| _serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; | |||
| #if TEST_RESPONSES | |||
| _serializer.CheckAdditionalContent = true; | |||
| _serializer.MissingMemberHandling = MissingMemberHandling.Error; | |||
| #else | |||
| _serializer.Error += (s, e) => | |||
| { | |||
| e.ErrorContext.Handled = true; | |||
| _logger.Log(LogSeverity.Error, "Serialization Failed", e.ErrorContext.Error); | |||
| }; | |||
| #endif | |||
| } | |||
| protected async Task BeginConnect() | |||
| @@ -107,13 +79,13 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| await _taskManager.Stop().ConfigureAwait(false); | |||
| _taskManager.ClearException(); | |||
| _state = ConnectionState.Connecting; | |||
| State = ConnectionState.Connecting; | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||
| CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; | |||
| _lastHeartbeat = DateTime.UtcNow; | |||
| await _engine.Connect(Host, _cancelToken).ConfigureAwait(false); | |||
| await _engine.Connect(Host, CancelToken).ConfigureAwait(false); | |||
| await Run().ConfigureAwait(false); | |||
| } | |||
| finally | |||
| @@ -131,10 +103,11 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| try | |||
| { | |||
| _state = ConnectionState.Connected; | |||
| State = ConnectionState.Connected; | |||
| _connectedEvent.Set(); | |||
| RaiseConnected(); | |||
| Logger.Info($"Connected"); | |||
| OnConnected(); | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| @@ -145,29 +118,32 @@ namespace Discord.Net.WebSockets | |||
| protected abstract Task Run(); | |||
| protected virtual async Task Cleanup() | |||
| { | |||
| var oldState = _state; | |||
| _state = ConnectionState.Disconnecting; | |||
| var oldState = State; | |||
| State = ConnectionState.Disconnecting; | |||
| await _engine.Disconnect().ConfigureAwait(false); | |||
| _cancelTokenSource = null; | |||
| _connectedEvent.Reset(); | |||
| if (oldState == ConnectionState.Connected) | |||
| RaiseDisconnected(_taskManager.WasUnexpected, _taskManager.Exception); | |||
| _state = ConnectionState.Disconnected; | |||
| { | |||
| Logger.Info("Disconnected"); | |||
| OnDisconnected(_taskManager.WasUnexpected, _taskManager.Exception); | |||
| } | |||
| State = ConnectionState.Disconnected; | |||
| } | |||
| protected virtual Task ProcessMessage(string json) | |||
| { | |||
| if (_logger.Level >= LogSeverity.Debug) | |||
| _logger.Debug( $"In: {json}"); | |||
| if (Logger.Level >= LogSeverity.Debug) | |||
| Logger.Debug( $"In: {json}"); | |||
| return TaskHelper.CompletedTask; | |||
| } | |||
| protected void QueueMessage(IWebSocketMessage message) | |||
| { | |||
| string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); | |||
| if (_logger.Level >= LogSeverity.Debug) | |||
| _logger.Debug( $"Out: " + json); | |||
| if (Logger.Level >= LogSeverity.Debug) | |||
| Logger.Debug( $"Out: {json}"); | |||
| _engine.QueueMessage(json); | |||
| } | |||
| @@ -179,9 +155,9 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| while (!cancelToken.IsCancellationRequested) | |||
| { | |||
| if (_state == ConnectionState.Connected) | |||
| if (this.State == ConnectionState.Connected) | |||
| { | |||
| SendHeartbeat(); | |||
| SendHeartbeat(); | |||
| await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); | |||
| } | |||
| else | |||
| @@ -192,5 +168,20 @@ namespace Discord.Net.WebSockets | |||
| }); | |||
| } | |||
| public abstract void SendHeartbeat(); | |||
| public void WaitForConnection(CancellationToken cancelToken) | |||
| { | |||
| try | |||
| { | |||
| //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached | |||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token; | |||
| _connectedEvent.Wait(cancelToken); | |||
| } | |||
| catch (OperationCanceledException) | |||
| { | |||
| _taskManager.ThrowException(); //Throws data socket's internal error if any occured | |||
| throw; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,71 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| /*internal class Reference<T> | |||
| where T : CachedObject<ulong> | |||
| { | |||
| private Action<T> _onCache, _onUncache; | |||
| private Func<ulong, T> _getItem; | |||
| private ulong? _id; | |||
| public ulong? Id | |||
| { | |||
| get { return _id; } | |||
| set | |||
| { | |||
| _id = value; | |||
| _value = null; | |||
| } | |||
| } | |||
| private T _value; | |||
| public T Value | |||
| { | |||
| get | |||
| { | |||
| } | |||
| } | |||
| public T Load() | |||
| { | |||
| var v = _value; //A little trickery to make this threadsafe | |||
| var id = _id; | |||
| if (v != null && !_value.IsCached) | |||
| { | |||
| v = null; | |||
| _value = null; | |||
| } | |||
| if (v == null && id != null) | |||
| { | |||
| v = _getItem(id.Value); | |||
| if (v != null && _onCache != null) | |||
| _onCache(v); | |||
| _value = v; | |||
| } | |||
| return v; | |||
| return Value != null; //Used for precaching | |||
| } | |||
| public void Unload() | |||
| { | |||
| if (_onUncache != null) | |||
| { | |||
| var v = _value; | |||
| if (v != null && _onUncache != null) | |||
| _onUncache(v); | |||
| } | |||
| } | |||
| public Reference(Func<ulong, T> onUpdate, Action<T> onCache = null, Action<T> onUncache = null) | |||
| : this(null, onUpdate, onCache, onUncache) | |||
| { } | |||
| public Reference(ulong? id, Func<ulong, T> getItem, Action<T> onCache = null, Action<T> onUncache = null) | |||
| { | |||
| _id = id; | |||
| _getItem = getItem; | |||
| _onCache = onCache; | |||
| _onUncache = onUncache; | |||
| _value = null; | |||
| } | |||
| }*/ | |||
| } | |||
| @@ -1,7 +0,0 @@ | |||
| namespace Discord | |||
| { | |||
| public interface IService | |||
| { | |||
| void Install(DiscordClient client); | |||
| } | |||
| } | |||
| @@ -1,8 +0,0 @@ | |||
| namespace Discord | |||
| { | |||
| public static class LogExtensions | |||
| { | |||
| public static LogService Log(this DiscordClient client, bool required = true) | |||
| => client.GetService<LogService>(required); | |||
| } | |||
| } | |||
| @@ -1,71 +0,0 @@ | |||
| using System; | |||
| namespace Discord | |||
| { | |||
| public class LogService : IService | |||
| { | |||
| public DiscordClient Client => _client; | |||
| private DiscordClient _client; | |||
| public LogSeverity Level => _level; | |||
| private LogSeverity _level; | |||
| public event EventHandler<LogMessageEventArgs> LogMessage; | |||
| internal void RaiseLogMessage(LogMessageEventArgs e) | |||
| { | |||
| if (LogMessage != null) | |||
| { | |||
| try | |||
| { | |||
| LogMessage(this, e); | |||
| } | |||
| catch { } //We dont want to log on log errors | |||
| } | |||
| } | |||
| void IService.Install(DiscordClient client) | |||
| { | |||
| _client = client; | |||
| _level = client.Config.LogLevel; | |||
| } | |||
| public Logger CreateLogger(string source) | |||
| { | |||
| return new Logger(this, source); | |||
| } | |||
| } | |||
| public class Logger | |||
| { | |||
| private LogService _service; | |||
| public LogSeverity Level => _level; | |||
| private LogSeverity _level; | |||
| public string Source => _source; | |||
| private string _source; | |||
| internal Logger(LogService service, string source) | |||
| { | |||
| _service = service; | |||
| _level = service.Level; | |||
| _source = source; | |||
| } | |||
| public void Log(LogSeverity severity, string message, Exception exception = null) | |||
| { | |||
| if (severity <= _service.Level) | |||
| _service.RaiseLogMessage(new LogMessageEventArgs(severity, _source, message, exception)); | |||
| } | |||
| public void Error(string message, Exception exception = null) | |||
| => Log(LogSeverity.Error, message, exception); | |||
| public void Warning(string message, Exception exception = null) | |||
| => Log(LogSeverity.Warning, message, exception); | |||
| public void Info(string message, Exception exception = null) | |||
| => Log(LogSeverity.Info, message, exception); | |||
| public void Verbose(string message, Exception exception = null) | |||
| => Log(LogSeverity.Verbose, message, exception); | |||
| public void Debug(string message, Exception exception = null) | |||
| => Log(LogSeverity.Debug, message, exception); | |||
| } | |||
| } | |||
| @@ -146,10 +146,7 @@ namespace Discord | |||
| public void ThrowException() | |||
| { | |||
| lock (_lock) | |||
| { | |||
| if (_stopReason != null) | |||
| _stopReason.Throw(); | |||
| } | |||
| _stopReason?.Throw(); | |||
| } | |||
| public void ClearException() | |||
| { | |||
| @@ -29,12 +29,11 @@ | |||
| }, | |||
| "dependencies": { | |||
| "Newtonsoft.Json": "7.0.1", | |||
| "StyleCop.Analyzers": "1.0.0-rc2" | |||
| "Newtonsoft.Json": "7.0.1" | |||
| }, | |||
| "frameworks": { | |||
| "net45": { | |||
| "net46": { | |||
| "dependencies": { | |||
| "WebSocket4Net": "0.14.1", | |||
| "RestSharp": "105.2.3" | |||