| @@ -5,9 +5,6 @@ An unofficial .Net API Wrapper for the Discord client (http://discordapp.com). | |||||
| Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). | Check out the [documentation](http://rtd.discord.foxbot.me/en/docs-dev/index.html) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). | ||||
| ##### Warning: Some of the documentation is outdated. | |||||
| It's current being rewritten. Until that's done, feel free to use my [DiscordBot](https://github.com/RogueException/DiscordBot) repo for reference. | |||||
| ### Installation | ### Installation | ||||
| You can download Discord.Net and its extensions from NuGet: | You can download Discord.Net and its extensions from NuGet: | ||||
| - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) | - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) | ||||
| @@ -16,9 +13,10 @@ You can download Discord.Net and its extensions from NuGet: | |||||
| - [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) | - [Discord.Net.Audio](https://www.nuget.org/packages/Discord.Net.Audio/) | ||||
| ### Compiling | ### Compiling | ||||
| In order to compile Discord.Net, you require at least the following: | |||||
| - [Visual Studio 2015](https://www.visualstudio.com/downloads/download-visual-studio-vs) | |||||
| - [Visual Studio 2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) | |||||
| - [Visual Studio .Net Core Plugin](https://www.microsoft.com/net/core#windows) | |||||
| In order to compile Discord.Net, you require the following: | |||||
| #### Visual Studio 2015 | |||||
| - [VS2015 Update 2](https://www.visualstudio.com/en-us/news/vs2015-update2-vs.aspx) | |||||
| - [.Net Core SDK + VS Plugin](https://www.microsoft.com/net/core#windows) | |||||
| - NuGet 3.3+ (available through Visual Studio) | - NuGet 3.3+ (available through Visual Studio) | ||||
| #### CLI | |||||
| - [.Net Core SDK](https://www.microsoft.com/net/core#windows) | |||||
| @@ -15,6 +15,6 @@ namespace Discord.API | |||||
| public bool Revoked { get; set; } | public bool Revoked { get; set; } | ||||
| [JsonProperty("integrations")] | [JsonProperty("integrations")] | ||||
| public IEnumerable<ulong> Integrations { get; set; } | |||||
| public IReadOnlyCollection<ulong> Integrations { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -9,6 +9,6 @@ namespace Discord.API | |||||
| [JsonProperty("url")] | [JsonProperty("url")] | ||||
| public string StreamUrl { get; set; } | public string StreamUrl { get; set; } | ||||
| [JsonProperty("type")] | [JsonProperty("type")] | ||||
| public StreamType StreamType { get; set; } | |||||
| public StreamType? StreamType { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,14 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | |||||
| { | |||||
| public class Presence | |||||
| { | |||||
| [JsonProperty("user")] | |||||
| public User User { get; set; } | |||||
| [JsonProperty("status")] | |||||
| public UserStatus Status { get; set; } | |||||
| [JsonProperty("game")] | |||||
| public Game Game { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | |||||
| { | |||||
| public class Relationship | |||||
| { | |||||
| [JsonProperty("id")] | |||||
| public ulong Id { get; set; } | |||||
| [JsonProperty("user")] | |||||
| public User User { get; set; } | |||||
| [JsonProperty("type")] | |||||
| public RelationshipType Type { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| namespace Discord.API | |||||
| { | |||||
| public enum RelationshipType | |||||
| { | |||||
| Friend = 1, | |||||
| Blocked = 2, | |||||
| Pending = 4 | |||||
| } | |||||
| } | |||||
| @@ -4,8 +4,6 @@ namespace Discord.API | |||||
| { | { | ||||
| public class VoiceState | public class VoiceState | ||||
| { | { | ||||
| [JsonProperty("guild_id")] | |||||
| public ulong? GuildId { get; set; } | |||||
| [JsonProperty("channel_id")] | [JsonProperty("channel_id")] | ||||
| public ulong ChannelId { get; set; } | public ulong ChannelId { get; set; } | ||||
| [JsonProperty("user_id")] | [JsonProperty("user_id")] | ||||
| @@ -0,0 +1,24 @@ | |||||
| using Newtonsoft.Json; | |||||
| using System; | |||||
| namespace Discord.API.Gateway | |||||
| { | |||||
| public class ExtendedGuild : Guild | |||||
| { | |||||
| [JsonProperty("unavailable")] | |||||
| public bool? Unavailable { get; set; } | |||||
| [JsonProperty("member_count")] | |||||
| public int MemberCount { get; set; } | |||||
| [JsonProperty("large")] | |||||
| public bool Large { get; set; } | |||||
| [JsonProperty("presences")] | |||||
| public Presence[] Presences { get; set; } | |||||
| [JsonProperty("members")] | |||||
| public GuildMember[] Members { get; set; } | |||||
| [JsonProperty("channels")] | |||||
| public Channel[] Channels { get; set; } | |||||
| [JsonProperty("joined_at")] | |||||
| public DateTime JoinedAt { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -12,13 +12,19 @@ | |||||
| StatusUpdate = 3, | StatusUpdate = 3, | ||||
| /// <summary> C→S - Used to join a particular voice channel. </summary> | /// <summary> C→S - Used to join a particular voice channel. </summary> | ||||
| VoiceStateUpdate = 4, | VoiceStateUpdate = 4, | ||||
| /// <summary> C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. </summary> | |||||
| /// <summary> C→S - Used to ensure the guild's voice server is alive. </summary> | |||||
| VoiceServerPing = 5, | VoiceServerPing = 5, | ||||
| /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | ||||
| Resume = 6, | Resume = 6, | ||||
| /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | ||||
| Reconnect = 7, | Reconnect = 7, | ||||
| /// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | /// <summary> C→S - Used to request all members that were withheld by large_threshold </summary> | ||||
| RequestGuildMembers = 8 | |||||
| RequestGuildMembers = 8, | |||||
| /// <summary> S→C - Used to notify the client that their session has expired and cannot be resumed. </summary> | |||||
| InvalidSession = 9, | |||||
| /// <summary> S→C - Used to provide information to the client immediately on connection. </summary> | |||||
| Hello = 10, | |||||
| /// <summary> S→C - Used to reply to a client's heartbeat. </summary> | |||||
| HeartbeatAck = 11 | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,10 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Gateway | |||||
| { | |||||
| public class GuildBanEvent : User | |||||
| { | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong GuildId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Gateway | |||||
| { | |||||
| public class GuildRoleDeleteEvent | |||||
| { | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong GuildId { get; set; } | |||||
| [JsonProperty("role_id")] | |||||
| public ulong RoleId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -23,11 +23,13 @@ namespace Discord.API.Gateway | |||||
| [JsonProperty("read_state")] | [JsonProperty("read_state")] | ||||
| public ReadState[] ReadStates { get; set; } | public ReadState[] ReadStates { get; set; } | ||||
| [JsonProperty("guilds")] | [JsonProperty("guilds")] | ||||
| public Guild[] Guilds { get; set; } | |||||
| public ExtendedGuild[] Guilds { get; set; } | |||||
| [JsonProperty("private_channels")] | [JsonProperty("private_channels")] | ||||
| public Channel[] PrivateChannels { get; set; } | public Channel[] PrivateChannels { get; set; } | ||||
| [JsonProperty("heartbeat_interval")] | [JsonProperty("heartbeat_interval")] | ||||
| public int HeartbeatInterval { get; set; } | public int HeartbeatInterval { get; set; } | ||||
| [JsonProperty("relationships")] | |||||
| public Relationship[] Relationships { get; set; } | |||||
| //Ignored | //Ignored | ||||
| [JsonProperty("user_settings")] | [JsonProperty("user_settings")] | ||||
| @@ -3,7 +3,7 @@ using System.Collections.Generic; | |||||
| namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
| { | { | ||||
| public class DeleteMessagesParam | |||||
| public class DeleteMessagesParams | |||||
| { | { | ||||
| [JsonProperty("messages")] | [JsonProperty("messages")] | ||||
| public IEnumerable<ulong> MessageIds { get; set; } | public IEnumerable<ulong> MessageIds { get; set; } | ||||
| @@ -1,12 +0,0 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rest | |||||
| { | |||||
| public class LoginParams | |||||
| { | |||||
| [JsonProperty("email")] | |||||
| public string Email { get; set; } | |||||
| [JsonProperty("password")] | |||||
| public string Password { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -1,10 +0,0 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API.Rest | |||||
| { | |||||
| public class LoginResponse | |||||
| { | |||||
| [JsonProperty("token")] | |||||
| public string Token { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -1,5 +1,4 @@ | |||||
| using Discord.Net.Converters; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json; | |||||
| using System.IO; | using System.IO; | ||||
| namespace Discord.API.Rest | namespace Discord.API.Rest | ||||
| @@ -8,12 +7,6 @@ namespace Discord.API.Rest | |||||
| { | { | ||||
| [JsonProperty("username")] | [JsonProperty("username")] | ||||
| public Optional<string> Username { get; set; } | public Optional<string> Username { get; set; } | ||||
| [JsonProperty("email")] | |||||
| public Optional<string> Email { get; set; } | |||||
| [JsonProperty("password")] | |||||
| public Optional<string> Password { get; set; } | |||||
| [JsonProperty("new_password")] | |||||
| public Optional<string> NewPassword { get; set; } | |||||
| [JsonProperty("avatar"), Image] | [JsonProperty("avatar"), Image] | ||||
| public Optional<Stream> Avatar { get; set; } | public Optional<Stream> Avatar { get; set; } | ||||
| } | } | ||||
| @@ -0,0 +1,4 @@ | |||||
| namespace Discord.Data | |||||
| { | |||||
| public delegate DataStore DataStoreProvider(int shardId, int totalShards, int guildCount, int dmCount); | |||||
| } | |||||
| @@ -0,0 +1,90 @@ | |||||
| using Discord.Extensions; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| namespace Discord.Data | |||||
| { | |||||
| public class DefaultDataStore : DataStore | |||||
| { | |||||
| private const double AverageChannelsPerGuild = 10.22; //Source: Googie2149 | |||||
| private const double AverageUsersPerGuild = 47.78; //Source: Googie2149 | |||||
| private const double CollectionMultiplier = 1.05; //Add buffer to handle growth | |||||
| private const double CollectionConcurrencyLevel = 1; //WebSocket updater/event handler. //TODO: Needs profiling, increase to 2? | |||||
| private readonly ConcurrentDictionary<ulong, ICachedChannel> _channels; | |||||
| private readonly ConcurrentDictionary<ulong, CachedGuild> _guilds; | |||||
| private readonly ConcurrentDictionary<ulong, CachedPublicUser> _users; | |||||
| internal override IReadOnlyCollection<ICachedChannel> Channels => _channels.ToReadOnlyCollection(); | |||||
| internal override IReadOnlyCollection<CachedGuild> Guilds => _guilds.ToReadOnlyCollection(); | |||||
| internal override IReadOnlyCollection<CachedPublicUser> Users => _users.ToReadOnlyCollection(); | |||||
| public DefaultDataStore(int guildCount, int dmChannelCount) | |||||
| { | |||||
| double estimatedChannelCount = guildCount * AverageChannelsPerGuild + dmChannelCount; | |||||
| double estimatedUsersCount = guildCount * AverageUsersPerGuild; | |||||
| _channels = new ConcurrentDictionary<ulong, ICachedChannel>(1, (int)(estimatedChannelCount * CollectionMultiplier)); | |||||
| _guilds = new ConcurrentDictionary<ulong, CachedGuild>(1, (int)(guildCount * CollectionMultiplier)); | |||||
| _users = new ConcurrentDictionary<ulong, CachedPublicUser>(1, (int)(estimatedUsersCount * CollectionMultiplier)); | |||||
| } | |||||
| internal override ICachedChannel GetChannel(ulong id) | |||||
| { | |||||
| ICachedChannel channel; | |||||
| if (_channels.TryGetValue(id, out channel)) | |||||
| return channel; | |||||
| return null; | |||||
| } | |||||
| internal override void AddChannel(ICachedChannel channel) | |||||
| { | |||||
| _channels[channel.Id] = channel; | |||||
| } | |||||
| internal override ICachedChannel RemoveChannel(ulong id) | |||||
| { | |||||
| ICachedChannel channel; | |||||
| if (_channels.TryRemove(id, out channel)) | |||||
| return channel; | |||||
| return null; | |||||
| } | |||||
| internal override CachedGuild GetGuild(ulong id) | |||||
| { | |||||
| CachedGuild guild; | |||||
| if (_guilds.TryGetValue(id, out guild)) | |||||
| return guild; | |||||
| return null; | |||||
| } | |||||
| internal override void AddGuild(CachedGuild guild) | |||||
| { | |||||
| _guilds[guild.Id] = guild; | |||||
| } | |||||
| internal override CachedGuild RemoveGuild(ulong id) | |||||
| { | |||||
| CachedGuild guild; | |||||
| if (_guilds.TryRemove(id, out guild)) | |||||
| return guild; | |||||
| return null; | |||||
| } | |||||
| internal override CachedPublicUser GetUser(ulong id) | |||||
| { | |||||
| CachedPublicUser user; | |||||
| if (_users.TryGetValue(id, out user)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| internal override CachedPublicUser GetOrAddUser(ulong id, Func<ulong, CachedPublicUser> userFactory) | |||||
| { | |||||
| return _users.GetOrAdd(id, userFactory); | |||||
| } | |||||
| internal override CachedPublicUser RemoveUser(ulong id) | |||||
| { | |||||
| CachedPublicUser user; | |||||
| if (_users.TryRemove(id, out user)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Data | |||||
| { | |||||
| public abstract class DataStore | |||||
| { | |||||
| internal abstract IReadOnlyCollection<ICachedChannel> Channels { get; } | |||||
| internal abstract IReadOnlyCollection<CachedGuild> Guilds { get; } | |||||
| internal abstract IReadOnlyCollection<CachedPublicUser> Users { get; } | |||||
| internal abstract ICachedChannel GetChannel(ulong id); | |||||
| internal abstract void AddChannel(ICachedChannel channel); | |||||
| internal abstract ICachedChannel RemoveChannel(ulong id); | |||||
| internal abstract CachedGuild GetGuild(ulong id); | |||||
| internal abstract void AddGuild(CachedGuild guild); | |||||
| internal abstract CachedGuild RemoveGuild(ulong id); | |||||
| internal abstract CachedPublicUser GetUser(ulong id); | |||||
| internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func<ulong, CachedPublicUser> userFactory); | |||||
| internal abstract CachedPublicUser RemoveUser(ulong id); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| namespace Discord.Data | |||||
| { | |||||
| //TODO: Implement | |||||
| //TODO: CachedPublicUser's GuildCount system is not at all multi-writer threadsafe! | |||||
| //TODO: CachedPublicUser's Update method is not multi-writer threadsafe! | |||||
| //TODO: Are there other multiwriters across shards? | |||||
| /*public class SharedDataStore | |||||
| { | |||||
| }*/ | |||||
| } | |||||
| @@ -1,43 +1,38 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Extensions; | |||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net; | using Discord.Net; | ||||
| using Discord.Net.Queue; | using Discord.Net.Queue; | ||||
| using Discord.Net.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | |||||
| using System.IO; | using System.IO; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| //TODO: Docstrings | |||||
| //TODO: Log Internal/External REST Rate Limits, 502s | |||||
| //TODO: Log Logins/Logouts | |||||
| public sealed class DiscordClient : IDiscordClient, IDisposable | |||||
| public class DiscordClient : IDiscordClient | |||||
| { | { | ||||
| public event Func<LogMessage, Task> Log; | public event Func<LogMessage, Task> Log; | ||||
| public event Func<Task> LoggedIn, LoggedOut; | public event Func<Task> LoggedIn, LoggedOut; | ||||
| private readonly Logger _discordLogger, _restLogger; | |||||
| private readonly SemaphoreSlim _connectionLock; | |||||
| private readonly RestClientProvider _restClientProvider; | |||||
| private readonly LogManager _log; | |||||
| private readonly RequestQueue _requestQueue; | |||||
| private bool _isDisposed; | |||||
| private SelfUser _currentUser; | |||||
| internal readonly Logger _discordLogger, _restLogger; | |||||
| internal readonly SemaphoreSlim _connectionLock; | |||||
| internal readonly LogManager _log; | |||||
| internal readonly RequestQueue _requestQueue; | |||||
| internal bool _isDisposed; | |||||
| internal SelfUser _currentUser; | |||||
| public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
| public API.DiscordApiClient ApiClient { get; private set; } | public API.DiscordApiClient ApiClient { get; private set; } | ||||
| public IRequestQueue RequestQueue => _requestQueue; | |||||
| public DiscordClient(DiscordConfig config = null) | public DiscordClient(DiscordConfig config = null) | ||||
| { | { | ||||
| if (config == null) | if (config == null) | ||||
| config = new DiscordConfig(); | config = new DiscordConfig(); | ||||
| _log = new LogManager(config.LogLevel); | _log = new LogManager(config.LogLevel); | ||||
| _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); | _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); | ||||
| _discordLogger = _log.CreateLogger("Discord"); | _discordLogger = _log.CreateLogger("Discord"); | ||||
| @@ -49,26 +44,17 @@ namespace Discord.Rest | |||||
| ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); | ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); | ||||
| ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task Login(string email, string password) | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await LoginInternal(TokenType.User, null, email, password, true, false).ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| public async Task Login(TokenType tokenType, string token, bool validateToken = true) | public async Task Login(TokenType tokenType, string token, bool validateToken = true) | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); | |||||
| await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); | |||||
| } | } | ||||
| finally { _connectionLock.Release(); } | finally { _connectionLock.Release(); } | ||||
| } | } | ||||
| private async Task LoginInternal(TokenType tokenType, string token, string email, string password, bool useEmail, bool validateToken) | |||||
| private async Task LoginInternal(TokenType tokenType, string token, bool validateToken) | |||||
| { | { | ||||
| if (LoginState != LoginState.LoggedOut) | if (LoginState != LoginState.LoggedOut) | ||||
| await LogoutInternal().ConfigureAwait(false); | await LogoutInternal().ConfigureAwait(false); | ||||
| @@ -76,13 +62,7 @@ namespace Discord.Rest | |||||
| try | try | ||||
| { | { | ||||
| if (useEmail) | |||||
| { | |||||
| var args = new LoginParams { Email = email, Password = password }; | |||||
| await ApiClient.Login(args).ConfigureAwait(false); | |||||
| } | |||||
| else | |||||
| await ApiClient.Login(tokenType, token).ConfigureAwait(false); | |||||
| await ApiClient.Login(tokenType, token).ConfigureAwait(false); | |||||
| if (validateToken) | if (validateToken) | ||||
| { | { | ||||
| @@ -96,6 +76,8 @@ namespace Discord.Rest | |||||
| } | } | ||||
| } | } | ||||
| await OnLogin().ConfigureAwait(false); | |||||
| LoginState = LoginState.LoggedIn; | LoginState = LoginState.LoggedIn; | ||||
| } | } | ||||
| catch (Exception) | catch (Exception) | ||||
| @@ -106,6 +88,7 @@ namespace Discord.Rest | |||||
| await LoggedIn.Raise().ConfigureAwait(false); | await LoggedIn.Raise().ConfigureAwait(false); | ||||
| } | } | ||||
| protected virtual Task OnLogin() => Task.CompletedTask; | |||||
| public async Task Logout() | public async Task Logout() | ||||
| { | { | ||||
| @@ -122,6 +105,8 @@ namespace Discord.Rest | |||||
| LoginState = LoginState.LoggingOut; | LoginState = LoginState.LoggingOut; | ||||
| await ApiClient.Logout().ConfigureAwait(false); | await ApiClient.Logout().ConfigureAwait(false); | ||||
| await OnLogout().ConfigureAwait(false); | |||||
| _currentUser = null; | _currentUser = null; | ||||
| @@ -129,14 +114,15 @@ namespace Discord.Rest | |||||
| await LoggedOut.Raise().ConfigureAwait(false); | await LoggedOut.Raise().ConfigureAwait(false); | ||||
| } | } | ||||
| protected virtual Task OnLogout() => Task.CompletedTask; | |||||
| public async Task<IEnumerable<Connection>> GetConnections() | |||||
| public async Task<IReadOnlyCollection<IConnection>> GetConnections() | |||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); | ||||
| return models.Select(x => new Connection(x)); | |||||
| return models.Select(x => new Connection(x)).ToImmutableArray(); | |||||
| } | } | ||||
| public async Task<IChannel> GetChannel(ulong id) | |||||
| public virtual async Task<IChannel> GetChannel(ulong id) | |||||
| { | { | ||||
| var model = await ApiClient.GetChannel(id).ConfigureAwait(false); | var model = await ApiClient.GetChannel(id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| @@ -151,17 +137,17 @@ namespace Discord.Rest | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| return new DMChannel(this, model); | |||||
| return new DMChannel(this, new User(this, model.Recipient), model); | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<IEnumerable<DMChannel>> GetDMChannels() | |||||
| public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels() | |||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); | ||||
| return models.Select(x => new DMChannel(this, x)); | |||||
| return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); | |||||
| } | } | ||||
| public async Task<Invite> GetInvite(string inviteIdOrXkcd) | |||||
| public virtual async Task<IInvite> GetInvite(string inviteIdOrXkcd) | |||||
| { | { | ||||
| var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); | var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| @@ -169,48 +155,48 @@ namespace Discord.Rest | |||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<Guild> GetGuild(ulong id) | |||||
| public virtual async Task<IGuild> GetGuild(ulong id) | |||||
| { | { | ||||
| var model = await ApiClient.GetGuild(id).ConfigureAwait(false); | var model = await ApiClient.GetGuild(id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return new Guild(this, model); | return new Guild(this, model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<GuildEmbed> GetGuildEmbed(ulong id) | |||||
| public virtual async Task<GuildEmbed?> GetGuildEmbed(ulong id) | |||||
| { | { | ||||
| var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); | var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return new GuildEmbed(model); | return new GuildEmbed(model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<IEnumerable<UserGuild>> GetGuilds() | |||||
| public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds() | |||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); | ||||
| return models.Select(x => new UserGuild(this, x)); | |||||
| return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); | |||||
| } | } | ||||
| public async Task<Guild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) | |||||
| public virtual async Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) | |||||
| { | { | ||||
| var args = new CreateGuildParams(); | var args = new CreateGuildParams(); | ||||
| var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); | var model = await ApiClient.CreateGuild(args).ConfigureAwait(false); | ||||
| return new Guild(this, model); | return new Guild(this, model); | ||||
| } | } | ||||
| public async Task<User> GetUser(ulong id) | |||||
| public virtual async Task<IUser> GetUser(ulong id) | |||||
| { | { | ||||
| var model = await ApiClient.GetUser(id).ConfigureAwait(false); | var model = await ApiClient.GetUser(id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return new PublicUser(this, model); | |||||
| return new User(this, model); | |||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<User> GetUser(string username, ushort discriminator) | |||||
| public virtual async Task<IUser> GetUser(string username, ushort discriminator) | |||||
| { | { | ||||
| var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); | var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return new PublicUser(this, model); | |||||
| return new User(this, model); | |||||
| return null; | return null; | ||||
| } | } | ||||
| public async Task<SelfUser> GetCurrentUser() | |||||
| public virtual async Task<ISelfUser> GetCurrentUser() | |||||
| { | { | ||||
| var user = _currentUser; | var user = _currentUser; | ||||
| if (user == null) | if (user == null) | ||||
| @@ -221,60 +207,32 @@ namespace Discord.Rest | |||||
| } | } | ||||
| return user; | return user; | ||||
| } | } | ||||
| public async Task<IEnumerable<User>> QueryUsers(string query, int limit) | |||||
| public virtual async Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit) | |||||
| { | { | ||||
| var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); | var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); | ||||
| return models.Select(x => new PublicUser(this, x)); | |||||
| return models.Select(x => new User(this, x)).ToImmutableArray(); | |||||
| } | } | ||||
| public async Task<IEnumerable<VoiceRegion>> GetVoiceRegions() | |||||
| public virtual async Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions() | |||||
| { | { | ||||
| var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | ||||
| return models.Select(x => new VoiceRegion(x)); | |||||
| return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); | |||||
| } | } | ||||
| public async Task<VoiceRegion> GetVoiceRegion(string id) | |||||
| public virtual async Task<IVoiceRegion> GetVoiceRegion(string id) | |||||
| { | { | ||||
| var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | ||||
| return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); | return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); | ||||
| } | } | ||||
| void Dispose(bool disposing) | |||||
| internal void Dispose(bool disposing) | |||||
| { | { | ||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
| ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ||||
| WebSocket.Data.IDataStore IDiscordClient.DataStore => null; | |||||
| Task IDiscordClient.Connect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); } | |||||
| Task IDiscordClient.Disconnect() { return Task.FromException(new NotSupportedException("This client does not support websocket connections.")); } | |||||
| async Task<IChannel> IDiscordClient.GetChannel(ulong id) | |||||
| => await GetChannel(id).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IDMChannel>> IDiscordClient.GetDMChannels() | |||||
| => await GetDMChannels().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IConnection>> IDiscordClient.GetConnections() | |||||
| => await GetConnections().ConfigureAwait(false); | |||||
| async Task<IInvite> IDiscordClient.GetInvite(string inviteIdOrXkcd) | |||||
| => await GetInvite(inviteIdOrXkcd).ConfigureAwait(false); | |||||
| async Task<IGuild> IDiscordClient.GetGuild(ulong id) | |||||
| => await GetGuild(id).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IUserGuild>> IDiscordClient.GetGuilds() | |||||
| => await GetGuilds().ConfigureAwait(false); | |||||
| async Task<IGuild> IDiscordClient.CreateGuild(string name, IVoiceRegion region, Stream jpegIcon) | |||||
| => await CreateGuild(name, region, jpegIcon).ConfigureAwait(false); | |||||
| async Task<IUser> IDiscordClient.GetUser(ulong id) | |||||
| => await GetUser(id).ConfigureAwait(false); | |||||
| async Task<IUser> IDiscordClient.GetUser(string username, ushort discriminator) | |||||
| => await GetUser(username, discriminator).ConfigureAwait(false); | |||||
| async Task<ISelfUser> IDiscordClient.GetCurrentUser() | |||||
| => await GetCurrentUser().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IUser>> IDiscordClient.QueryUsers(string query, int limit) | |||||
| => await QueryUsers(query, limit).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IVoiceRegion>> IDiscordClient.GetVoiceRegions() | |||||
| => await GetVoiceRegions().ConfigureAwait(false); | |||||
| async Task<IVoiceRegion> IDiscordClient.GetVoiceRegion(string id) | |||||
| => await GetVoiceRegion(id).ConfigureAwait(false); | |||||
| Task IDiscordClient.Connect() { throw new NotSupportedException(); } | |||||
| Task IDiscordClient.Disconnect() { throw new NotSupportedException(); } | |||||
| } | } | ||||
| } | } | ||||
| @@ -10,7 +10,7 @@ namespace Discord | |||||
| public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; | ||||
| public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; | public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; | ||||
| public const int GatewayAPIVersion = 3; //TODO: Upgrade to 4 | |||||
| public const int GatewayAPIVersion = 5; | |||||
| public const string GatewayEncoding = "json"; | public const string GatewayEncoding = "json"; | ||||
| public const string ClientAPIUrl = "https://discordapp.com/api/"; | public const string ClientAPIUrl = "https://discordapp.com/api/"; | ||||
| @@ -0,0 +1,708 @@ | |||||
| using Discord.API; | |||||
| using Discord.API.Gateway; | |||||
| using Discord.Data; | |||||
| using Discord.Extensions; | |||||
| using Discord.Logging; | |||||
| using Discord.Net.Converters; | |||||
| using Discord.Net.WebSockets; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| //TODO: Remove unnecessary `as` casts | |||||
| //TODO: Add docstrings | |||||
| public class DiscordSocketClient : DiscordClient, IDiscordClient | |||||
| { | |||||
| public event Func<Task> Connected, Disconnected; | |||||
| public event Func<Task> Ready; | |||||
| //public event Func<Channel> VoiceConnected, VoiceDisconnected; | |||||
| /*public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed; | |||||
| public event Func<IChannel, IChannel, Task> ChannelUpdated; | |||||
| public event Func<IMessage, Task> MessageReceived, MessageDeleted; | |||||
| public event Func<IMessage, IMessage, Task> MessageUpdated; | |||||
| public event Func<IRole, Task> RoleCreated, RoleDeleted; | |||||
| public event Func<IRole, IRole, Task> RoleUpdated; | |||||
| public event Func<IGuild, Task> JoinedGuild, LeftGuild, GuildAvailable, GuildUnavailable; | |||||
| public event Func<IGuild, IGuild, Task> GuildUpdated; | |||||
| public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | |||||
| public event Func<IUser, IUser, Task> UserUpdated; | |||||
| public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated; | |||||
| public event Func<IChannel, IUser, Task> UserIsTyping;*/ | |||||
| private readonly ConcurrentQueue<ulong> _largeGuilds; | |||||
| private readonly Logger _gatewayLogger; | |||||
| private readonly DataStoreProvider _dataStoreProvider; | |||||
| private readonly JsonSerializer _serializer; | |||||
| private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; | |||||
| private readonly bool _enablePreUpdateEvents; | |||||
| private readonly int _largeThreshold; | |||||
| private readonly int _totalShards; | |||||
| private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | |||||
| private string _sessionId; | |||||
| public int ShardId { get; } | |||||
| public ConnectionState ConnectionState { get; private set; } | |||||
| public IWebSocketClient GatewaySocket { get; private set; } | |||||
| internal int MessageCacheSize { get; private set; } | |||||
| //internal bool UsePermissionCache { get; private set; } | |||||
| internal DataStore DataStore { get; private set; } | |||||
| internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; | |||||
| internal IReadOnlyCollection<CachedGuild> Guilds | |||||
| { | |||||
| get | |||||
| { | |||||
| var guilds = DataStore.Guilds; | |||||
| return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds); | |||||
| } | |||||
| } | |||||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels | |||||
| { | |||||
| get | |||||
| { | |||||
| var users = DataStore.Users; | |||||
| return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users); | |||||
| } | |||||
| } | |||||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | |||||
| public DiscordSocketClient(DiscordSocketConfig config = null) | |||||
| { | |||||
| if (config == null) | |||||
| config = new DiscordSocketConfig(); | |||||
| ShardId = config.ShardId; | |||||
| _totalShards = config.TotalShards; | |||||
| _connectionTimeout = config.ConnectionTimeout; | |||||
| _reconnectDelay = config.ReconnectDelay; | |||||
| _failedReconnectDelay = config.FailedReconnectDelay; | |||||
| _dataStoreProvider = config.DataStoreProvider; | |||||
| MessageCacheSize = config.MessageCacheSize; | |||||
| //UsePermissionCache = config.UsePermissionsCache; | |||||
| _enablePreUpdateEvents = config.EnablePreUpdateEvents; | |||||
| _largeThreshold = config.LargeThreshold; | |||||
| _gatewayLogger = _log.CreateLogger("Gateway"); | |||||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||||
| ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.Verbose($"Sent Op {opCode}"); | |||||
| ApiClient.ReceivedGatewayEvent += ProcessMessage; | |||||
| GatewaySocket = config.WebSocketProvider(); | |||||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||||
| _largeGuilds = new ConcurrentQueue<ulong>(); | |||||
| } | |||||
| protected override async Task OnLogin() | |||||
| { | |||||
| var voiceRegions = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | |||||
| _voiceRegions = voiceRegions.Select(x => new VoiceRegion(x)).ToImmutableDictionary(x => x.Id); | |||||
| } | |||||
| protected override async Task OnLogout() | |||||
| { | |||||
| if (ConnectionState != ConnectionState.Disconnected) | |||||
| await DisconnectInternal().ConfigureAwait(false); | |||||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | |||||
| } | |||||
| public async Task Connect() | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await ConnectInternal().ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| private async Task ConnectInternal() | |||||
| { | |||||
| if (LoginState != LoginState.LoggedIn) | |||||
| throw new InvalidOperationException("You must log in before connecting."); | |||||
| ConnectionState = ConnectionState.Connecting; | |||||
| try | |||||
| { | |||||
| await ApiClient.Connect().ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Connected; | |||||
| } | |||||
| catch (Exception) | |||||
| { | |||||
| await DisconnectInternal().ConfigureAwait(false); | |||||
| throw; | |||||
| } | |||||
| await Connected.Raise().ConfigureAwait(false); | |||||
| } | |||||
| public async Task Disconnect() | |||||
| { | |||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await DisconnectInternal().ConfigureAwait(false); | |||||
| } | |||||
| finally { _connectionLock.Release(); } | |||||
| } | |||||
| private async Task DisconnectInternal() | |||||
| { | |||||
| ulong guildId; | |||||
| if (ConnectionState == ConnectionState.Disconnected) return; | |||||
| ConnectionState = ConnectionState.Disconnecting; | |||||
| await ApiClient.Disconnect().ConfigureAwait(false); | |||||
| while (_largeGuilds.TryDequeue(out guildId)) { } | |||||
| ConnectionState = ConnectionState.Disconnected; | |||||
| await Disconnected.Raise().ConfigureAwait(false); | |||||
| } | |||||
| public override Task<IVoiceRegion> GetVoiceRegion(string id) | |||||
| { | |||||
| VoiceRegion region; | |||||
| if (_voiceRegions.TryGetValue(id, out region)) | |||||
| return Task.FromResult<IVoiceRegion>(region); | |||||
| return Task.FromResult<IVoiceRegion>(null); | |||||
| } | |||||
| public override Task<IGuild> GetGuild(ulong id) | |||||
| { | |||||
| return Task.FromResult<IGuild>(DataStore.GetGuild(id)); | |||||
| } | |||||
| internal CachedGuild AddCachedGuild(API.Gateway.ExtendedGuild model, DataStore dataStore = null) | |||||
| { | |||||
| var guild = new CachedGuild(this, model); | |||||
| for (int i = 0; i < model.Channels.Length; i++) | |||||
| AddCachedChannel(model.Channels[i], dataStore); | |||||
| DataStore.AddGuild(guild); | |||||
| if (model.Large) | |||||
| _largeGuilds.Enqueue(model.Id); | |||||
| return guild; | |||||
| } | |||||
| internal CachedGuild RemoveCachedGuild(ulong id, DataStore dataStore = null) | |||||
| { | |||||
| var guild = DataStore.RemoveGuild(id) as CachedGuild; | |||||
| foreach (var channel in guild.Channels) | |||||
| guild.RemoveCachedChannel(channel.Id); | |||||
| foreach (var user in guild.Members) | |||||
| guild.RemoveCachedUser(user.Id); | |||||
| return guild; | |||||
| } | |||||
| internal CachedGuild GetCachedGuild(ulong id) => DataStore.GetGuild(id) as CachedGuild; | |||||
| public override Task<IChannel> GetChannel(ulong id) | |||||
| { | |||||
| return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | |||||
| } | |||||
| internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) | |||||
| { | |||||
| if (model.IsPrivate) | |||||
| { | |||||
| var recipient = AddCachedUser(model.Recipient); | |||||
| return recipient.SetDMChannel(model); | |||||
| } | |||||
| else | |||||
| { | |||||
| var guild = GetCachedGuild(model.GuildId.Value); | |||||
| return guild.AddCachedChannel(model); | |||||
| } | |||||
| } | |||||
| internal ICachedChannel RemoveCachedChannel(ulong id, DataStore dataStore = null) | |||||
| { | |||||
| var channel = DataStore.RemoveChannel(id) as ICachedChannel; | |||||
| var dmChannel = channel as CachedDMChannel; | |||||
| if (dmChannel != null) | |||||
| { | |||||
| var recipient = dmChannel.Recipient; | |||||
| recipient.RemoveDMChannel(id); | |||||
| } | |||||
| return channel; | |||||
| } | |||||
| internal ICachedChannel GetCachedChannel(ulong id) => DataStore.GetChannel(id) as ICachedChannel; | |||||
| public override Task<IUser> GetUser(ulong id) | |||||
| { | |||||
| return Task.FromResult<IUser>(DataStore.GetUser(id)); | |||||
| } | |||||
| public override Task<IUser> GetUser(string username, ushort discriminator) | |||||
| { | |||||
| return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | |||||
| } | |||||
| internal CachedPublicUser AddCachedUser(API.User model, DataStore dataStore = null) | |||||
| { | |||||
| var user = DataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; | |||||
| user.AddRef(); | |||||
| return user; | |||||
| } | |||||
| internal CachedPublicUser RemoveCachedUser(ulong id, DataStore dataStore = null) | |||||
| { | |||||
| var user = DataStore.GetUser(id) as CachedPublicUser; | |||||
| user.RemoveRef(); | |||||
| return user; | |||||
| } | |||||
| private async Task ProcessMessage(GatewayOpCodes opCode, string type, JToken payload) | |||||
| { | |||||
| try | |||||
| { | |||||
| switch (opCode) | |||||
| { | |||||
| case GatewayOpCodes.Dispatch: | |||||
| switch (type) | |||||
| { | |||||
| //Global | |||||
| case "READY": | |||||
| { | |||||
| //TODO: Make downloading large guilds optional | |||||
| var data = payload.ToObject<ReadyEvent>(_serializer); | |||||
| var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); | |||||
| _currentUser = new CachedSelfUser(this,data.User); | |||||
| for (int i = 0; i < data.Guilds.Length; i++) | |||||
| AddCachedGuild(data.Guilds[i], dataStore); | |||||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | |||||
| AddCachedChannel(data.PrivateChannels[i], dataStore); | |||||
| _sessionId = data.SessionId; | |||||
| DataStore = dataStore; | |||||
| await Ready().ConfigureAwait(false); | |||||
| } | |||||
| break; | |||||
| //Guilds | |||||
| /*case "GUILD_CREATE": | |||||
| { | |||||
| var data = payload.ToObject<ExtendedGuild>(_serializer); | |||||
| var guild = new CachedGuild(this, data); | |||||
| DataStore.AddGuild(guild); | |||||
| if (data.Unavailable == false) | |||||
| type = "GUILD_AVAILABLE"; | |||||
| else | |||||
| await JoinedGuild.Raise(guild).ConfigureAwait(false); | |||||
| if (!data.Large) | |||||
| await GuildAvailable.Raise(guild); | |||||
| else | |||||
| _largeGuilds.Enqueue(data.Id); | |||||
| } | |||||
| break; | |||||
| case "GUILD_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Guild>(_serializer); | |||||
| var guild = DataStore.GetGuild(data.Id); | |||||
| if (guild != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? guild.Clone() : null; | |||||
| guild.Update(data); | |||||
| await GuildUpdated.Raise(before, guild); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_UPDATE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_DELETE": | |||||
| { | |||||
| var data = payload.ToObject<ExtendedGuild>(_serializer); | |||||
| var guild = DataStore.RemoveGuild(data.Id); | |||||
| if (guild != null) | |||||
| { | |||||
| if (data.Unavailable == true) | |||||
| type = "GUILD_UNAVAILABLE"; | |||||
| await GuildUnavailable.Raise(guild); | |||||
| if (data.Unavailable != true) | |||||
| await LeftGuild.Raise(guild); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_DELETE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| //Channels | |||||
| case "CHANNEL_CREATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| IChannel channel = null; | |||||
| if (data.GuildId != null) | |||||
| { | |||||
| var guild = GetCachedGuild(data.GuildId.Value); | |||||
| if (guild != null) | |||||
| channel = guild.AddCachedChannel(data.Id, true); | |||||
| else | |||||
| await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); | |||||
| } | |||||
| else | |||||
| channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id); | |||||
| if (channel != null) | |||||
| { | |||||
| channel.Update(data); | |||||
| await ChannelCreated.Raise(channel); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "CHANNEL_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| var channel = DataStore.GetChannel(data.Id) as Channel; | |||||
| if (channel != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? channel.Clone() : null; | |||||
| channel.Update(data); | |||||
| await ChannelUpdated.Raise(before, channel); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("CHANNEL_UPDATE referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| case "CHANNEL_DELETE": | |||||
| { | |||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| var channel = RemoveCachedChannel(data.Id); | |||||
| if (channel != null) | |||||
| await ChannelDestroyed.Raise(channel); | |||||
| else | |||||
| await _gatewayLogger.Warning("CHANNEL_DELETE referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| //Members | |||||
| case "GUILD_MEMBER_ADD": | |||||
| { | |||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | |||||
| if (guild != null) | |||||
| { | |||||
| var user = guild.AddCachedUser(data.User.Id, true, true); | |||||
| user.Update(data); | |||||
| user.UpdateActivity(); | |||||
| UserJoined.Raise(user); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | |||||
| if (guild != null) | |||||
| { | |||||
| var user = guild.GetCachedUser(data.User.Id); | |||||
| if (user != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? user.Clone() : null; | |||||
| user.Update(data); | |||||
| await UserUpdated.Raise(before, user); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_REMOVE": | |||||
| { | |||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | |||||
| if (guild != null) | |||||
| { | |||||
| var user = guild.RemoveCachedUser(data.User.Id); | |||||
| if (user != null) | |||||
| { | |||||
| user.GlobalUser.RemoveGuild(); | |||||
| if (user.GuildCount == 0 && user.DMChannel == null) | |||||
| DataStore.RemoveUser(user.Id); | |||||
| await UserLeft.Raise(user); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBERS_CHUNK": | |||||
| { | |||||
| var data = payload.ToObject<GuildMembersChunkEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| { | |||||
| foreach (var memberData in data.Members) | |||||
| { | |||||
| var user = guild.AddCachedUser(memberData.User.Id, true, false); | |||||
| user.Update(memberData); | |||||
| } | |||||
| if (guild.CurrentUserCount >= guild.UserCount) //Finished downloading for there | |||||
| await GuildAvailable.Raise(guild); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| //Roles | |||||
| case "GUILD_ROLE_CREATE": | |||||
| { | |||||
| var data = payload.ToObject<GuildRoleCreateEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| { | |||||
| var role = guild.AddCachedRole(data.Data.Id); | |||||
| role.Update(data.Data, false); | |||||
| RoleCreated.Raise(role); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<GuildRoleUpdateEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| { | |||||
| var role = guild.GetRole(data.Data.Id); | |||||
| if (role != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? role.Clone() : null; | |||||
| role.Update(data.Data, true); | |||||
| RoleUpdated.Raise(before, role); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_DELETE": | |||||
| { | |||||
| var data = payload.ToObject<GuildRoleDeleteEvent>(_serializer); | |||||
| var guild = DataStore.GetGuild(data.GuildId) as CachedGuild; | |||||
| if (guild != null) | |||||
| { | |||||
| var role = guild.RemoveRole(data.RoleId); | |||||
| if (role != null) | |||||
| RoleDeleted.Raise(role); | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| //Bans | |||||
| case "GUILD_BAN_ADD": | |||||
| { | |||||
| var data = payload.ToObject<GuildBanEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| await UserBanned.Raise(new User(this, data)); | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_BAN_ADD referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| case "GUILD_BAN_REMOVE": | |||||
| { | |||||
| var data = payload.ToObject<GuildBanEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| await UserUnbanned.Raise(new User(this, data)); | |||||
| else | |||||
| await _gatewayLogger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| //Messages | |||||
| case "MESSAGE_CREATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var channel = DataStore.GetChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| var user = channel.GetUser(data.Author.Id); | |||||
| if (user != null) | |||||
| { | |||||
| bool isAuthor = data.Author.Id == CurrentUser.Id; | |||||
| var msg = channel.AddMessage(data.Id, user, data.Timestamp.Value); | |||||
| msg.Update(data); | |||||
| MessageReceived.Raise(msg); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown user."); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("MESSAGE_CREATE referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| case "MESSAGE_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| var msg = channel.GetMessage(data.Id, data.Author?.Id); | |||||
| var before = _enablePreUpdateEvents ? msg.Clone() : null; | |||||
| msg.Update(data); | |||||
| MessageUpdated.Raise(before, msg); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("MESSAGE_UPDATE referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| case "MESSAGE_DELETE": | |||||
| { | |||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| var msg = channel.RemoveMessage(data.Id); | |||||
| MessageDeleted.Raise(msg); | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("MESSAGE_DELETE referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| //Statuses | |||||
| case "PRESENCE_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.Presence>(_serializer); | |||||
| User user; | |||||
| Guild guild; | |||||
| if (data.GuildId == null) | |||||
| { | |||||
| guild = null; | |||||
| user = GetPrivateChannel(data.User.Id)?.Recipient; | |||||
| } | |||||
| else | |||||
| { | |||||
| guild = GetGuild(data.GuildId.Value); | |||||
| if (guild == null) | |||||
| { | |||||
| await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown guild."); | |||||
| break; | |||||
| } | |||||
| else | |||||
| user = guild.GetUser(data.User.Id); | |||||
| } | |||||
| if (user != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? user.Clone() : null; | |||||
| user.Update(data); | |||||
| UserUpdated.Raise(before, user); | |||||
| } | |||||
| else | |||||
| { | |||||
| //Occurs when a user leaves a guild | |||||
| //await _gatewayLogger.Warning("PRESENCE_UPDATE referenced an unknown user."); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "TYPING_START": | |||||
| { | |||||
| var data = payload.ToObject<TypingStartEvent>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| var user = channel.GetUser(data.UserId); | |||||
| if (user != null) | |||||
| { | |||||
| await UserIsTyping.Raise(channel, user); | |||||
| user.UpdateActivity(); | |||||
| } | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("TYPING_START referenced an unknown channel."); | |||||
| } | |||||
| break; | |||||
| //Voice | |||||
| case "VOICE_STATE_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<API.VoiceState>(_serializer); | |||||
| var guild = GetGuild(data.GuildId); | |||||
| if (guild != null) | |||||
| { | |||||
| var user = guild.GetUser(data.UserId); | |||||
| if (user != null) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? user.Clone() : null; | |||||
| user.Update(data); | |||||
| UserUpdated.Raise(before, user); | |||||
| } | |||||
| else | |||||
| { | |||||
| //Occurs when a user leaves a guild | |||||
| //await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown user."); | |||||
| } | |||||
| } | |||||
| else | |||||
| await _gatewayLogger.Warning("VOICE_STATE_UPDATE referenced an unknown guild."); | |||||
| } | |||||
| break; | |||||
| //Settings | |||||
| case "USER_UPDATE": | |||||
| { | |||||
| var data = payload.ToObject<SelfUser>(_serializer); | |||||
| if (data.Id == CurrentUser.Id) | |||||
| { | |||||
| var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; | |||||
| CurrentUser.Update(data); | |||||
| await CurrentUserUpdated.Raise(before, CurrentUser).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| break;*/ | |||||
| //Ignored | |||||
| case "USER_SETTINGS_UPDATE": | |||||
| case "MESSAGE_ACK": //TODO: Add (User only) | |||||
| case "GUILD_EMOJIS_UPDATE": //TODO: Add | |||||
| case "GUILD_INTEGRATIONS_UPDATE": //TODO: Add | |||||
| case "VOICE_SERVER_UPDATE": //TODO: Add | |||||
| case "RESUMED": //TODO: Add | |||||
| await _gatewayLogger.Debug($"Ignored message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); | |||||
| return; | |||||
| //Others | |||||
| default: | |||||
| await _gatewayLogger.Warning($"Unknown message {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); | |||||
| return; | |||||
| } | |||||
| break; | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| await _gatewayLogger.Error($"Error handling msg {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); | |||||
| return; | |||||
| } | |||||
| await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,7 +1,7 @@ | |||||
| using Discord.Net.WebSockets; | |||||
| using Discord.WebSocket.Data; | |||||
| using Discord.Data; | |||||
| using Discord.Net.WebSockets; | |||||
| namespace Discord.WebSocket | |||||
| namespace Discord | |||||
| { | { | ||||
| public class DiscordSocketConfig : DiscordConfig | public class DiscordSocketConfig : DiscordConfig | ||||
| { | { | ||||
| @@ -15,15 +15,15 @@ namespace Discord.WebSocket | |||||
| /// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | /// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | ||||
| public int ReconnectDelay { get; set; } = 1000; | public int ReconnectDelay { get; set; } = 1000; | ||||
| /// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | /// <summary> Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. </summary> | ||||
| public int FailedReconnectDelay { get; set; } = 15000; | |||||
| public int FailedReconnectDelay { get; set; } = 15000; | |||||
| /// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary> | /// <summary> Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. </summary> | ||||
| public int MessageCacheSize { get; set; } = 100; | public int MessageCacheSize { get; set; } = 100; | ||||
| /// <summary> | |||||
| /*/// <summary> | |||||
| /// Gets or sets whether the permissions cache should be used. | /// Gets or sets whether the permissions cache should be used. | ||||
| /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. | |||||
| /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster at the expense of increased memory usage. | |||||
| /// </summary> | /// </summary> | ||||
| public bool UsePermissionsCache { get; set; } = true; | |||||
| public bool UsePermissionsCache { get; set; } = false;*/ | |||||
| /// <summary> Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. </summary> | /// <summary> Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. </summary> | ||||
| public bool EnablePreUpdateEvents { get; set; } = true; | public bool EnablePreUpdateEvents { get; set; } = true; | ||||
| /// <summary> | /// <summary> | ||||
| @@ -0,0 +1,126 @@ | |||||
| using Discord.API.Rest; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | |||||
| using System.IO; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| internal class DMChannel : SnowflakeEntity, IDMChannel | |||||
| { | |||||
| public override DiscordClient Discord { get; } | |||||
| public User Recipient { get; private set; } | |||||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||||
| public DMChannel(DiscordClient discord, User recipient, Model model) | |||||
| : base(model.Id) | |||||
| { | |||||
| Discord = discord; | |||||
| Recipient = recipient; | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | |||||
| protected void Update(Model model, UpdateSource source) | |||||
| { | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Recipient.Update(model.Recipient, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Update() | |||||
| { | |||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Close() | |||||
| { | |||||
| await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); | |||||
| } | |||||
| public virtual async Task<IUser> GetUser(ulong id) | |||||
| { | |||||
| var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | |||||
| if (id == Recipient.Id) | |||||
| return Recipient; | |||||
| else if (id == currentUser.Id) | |||||
| return currentUser; | |||||
| else | |||||
| return null; | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IUser>> GetUsers() | |||||
| { | |||||
| var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | |||||
| return ImmutableArray.Create<IUser>(currentUser, Recipient); | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset) | |||||
| { | |||||
| var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | |||||
| return new IUser[] { currentUser, Recipient }.Skip(offset).Take(limit).ToImmutableArray(); | |||||
| } | |||||
| public async Task<IMessage> SendMessage(string text, bool isTTS) | |||||
| { | |||||
| var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.CreateDMMessage(Id, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| public async Task<IMessage> SendFile(string filePath, string text, bool isTTS) | |||||
| { | |||||
| string filename = Path.GetFileName(filePath); | |||||
| using (var file = File.OpenRead(filePath)) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadDMFile(Id, file, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| } | |||||
| public async Task<IMessage> SendFile(Stream stream, string filename, string text, bool isTTS) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadDMFile(Id, stream, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| public virtual async Task<IMessage> GetMessage(ulong id) | |||||
| { | |||||
| var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); | |||||
| if (model != null) | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| return null; | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); | |||||
| } | |||||
| public async Task DeleteMessages(IEnumerable<IMessage> messages) | |||||
| { | |||||
| await Discord.ApiClient.DeleteDMMessages(Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); | |||||
| } | |||||
| public async Task TriggerTyping() | |||||
| { | |||||
| await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); | |||||
| } | |||||
| public override string ToString() => '@' + Recipient.ToString(); | |||||
| private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; | |||||
| IUser IDMChannel.Recipient => Recipient; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||||
| } | |||||
| } | |||||
| @@ -1,42 +1,39 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Extensions; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Channel; | using Model = Discord.API.Channel; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public abstract class GuildChannel : IGuildChannel | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel | |||||
| { | { | ||||
| private ConcurrentDictionary<ulong, Overwrite> _overwrites; | private ConcurrentDictionary<ulong, Overwrite> _overwrites; | ||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| /// <summary> Gets the guild this channel is a member of. </summary> | |||||
| public Guild Guild { get; } | |||||
| /// <inheritdoc /> | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int Position { get; private set; } | public int Position { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites => _overwrites; | |||||
| internal DiscordClient Discord => Guild.Discord; | |||||
| public Guild Guild { get; private set; } | |||||
| public override DiscordClient Discord => Guild.Discord; | |||||
| internal GuildChannel(Guild guild, Model model) | |||||
| public GuildChannel(Guild guild, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| Guild = guild; | Guild = guild; | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| internal virtual void Update(Model model) | |||||
| protected virtual void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Name = model.Name; | Name = model.Name; | ||||
| Position = model.Position; | Position = model.Position; | ||||
| @@ -49,6 +46,13 @@ namespace Discord.Rest | |||||
| _overwrites = newOverwrites; | _overwrites = newOverwrites; | ||||
| } | } | ||||
| public async Task Update() | |||||
| { | |||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Modify(Action<ModifyGuildChannelParams> func) | public async Task Modify(Action<ModifyGuildChannelParams> func) | ||||
| { | { | ||||
| if (func != null) throw new NullReferenceException(nameof(func)); | if (func != null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -56,10 +60,35 @@ namespace Discord.Rest | |||||
| var args = new ModifyGuildChannelParams(); | var args = new ModifyGuildChannelParams(); | ||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Delete() | |||||
| { | |||||
| await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); | |||||
| } | |||||
| public abstract Task<IGuildUser> GetUser(ulong id); | |||||
| public abstract Task<IReadOnlyCollection<IGuildUser>> GetUsers(); | |||||
| public abstract Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset); | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvites() | |||||
| { | |||||
| var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); | |||||
| return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); | |||||
| } | |||||
| public async Task<IInviteMetadata> CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) | |||||
| { | |||||
| var args = new CreateChannelInviteParams | |||||
| { | |||||
| MaxAge = maxAge ?? 0, | |||||
| MaxUses = maxUses ?? 0, | |||||
| Temporary = isTemporary, | |||||
| XkcdPass = withXkcd | |||||
| }; | |||||
| var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); | |||||
| return new InviteMetadata(Discord, model); | |||||
| } | |||||
| public OverwritePermissions? GetPermissionOverwrite(IUser user) | public OverwritePermissions? GetPermissionOverwrite(IUser user) | ||||
| { | { | ||||
| Overwrite value; | Overwrite value; | ||||
| @@ -67,7 +96,6 @@ namespace Discord.Rest | |||||
| return value.Permissions; | return value.Permissions; | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public OverwritePermissions? GetPermissionOverwrite(IRole role) | public OverwritePermissions? GetPermissionOverwrite(IRole role) | ||||
| { | { | ||||
| Overwrite value; | Overwrite value; | ||||
| @@ -75,28 +103,19 @@ namespace Discord.Rest | |||||
| return value.Permissions; | return value.Permissions; | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <summary> Downloads a collection of all invites to this channel. </summary> | |||||
| public async Task<IEnumerable<InviteMetadata>> GetInvites() | |||||
| { | |||||
| var models = await Discord.ApiClient.GetChannelInvites(Id).ConfigureAwait(false); | |||||
| return models.Select(x => new InviteMetadata(Discord, x)); | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) | public async Task AddPermissionOverwrite(IUser user, OverwritePermissions perms) | ||||
| { | { | ||||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | ||||
| await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); | await Discord.ApiClient.ModifyChannelPermissions(Id, user.Id, args).ConfigureAwait(false); | ||||
| _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); | _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) | public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) | ||||
| { | { | ||||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | ||||
| await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); | await Discord.ApiClient.ModifyChannelPermissions(Id, role.Id, args).ConfigureAwait(false); | ||||
| _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); | _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task RemovePermissionOverwrite(IUser user) | public async Task RemovePermissionOverwrite(IUser user) | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); | ||||
| @@ -104,7 +123,6 @@ namespace Discord.Rest | |||||
| Overwrite value; | Overwrite value; | ||||
| _overwrites.TryRemove(user.Id, out value); | _overwrites.TryRemove(user.Id, out value); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task RemovePermissionOverwrite(IRole role) | public async Task RemovePermissionOverwrite(IRole role) | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); | ||||
| @@ -112,58 +130,15 @@ namespace Discord.Rest | |||||
| Overwrite value; | Overwrite value; | ||||
| _overwrites.TryRemove(role.Id, out value); | _overwrites.TryRemove(role.Id, out value); | ||||
| } | } | ||||
| /// <summary> Creates a new invite to this channel. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> | |||||
| /// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param> | |||||
| /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | |||||
| public async Task<InviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) | |||||
| { | |||||
| var args = new CreateChannelInviteParams | |||||
| { | |||||
| MaxAge = maxAge ?? 0, | |||||
| MaxUses = maxUses ?? 0, | |||||
| Temporary = isTemporary, | |||||
| XkcdPass = withXkcd | |||||
| }; | |||||
| var model = await Discord.ApiClient.CreateChannelInvite(Id, args).ConfigureAwait(false); | |||||
| return new InviteMetadata(Discord, model); | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public async Task Delete() | |||||
| { | |||||
| await Discord.ApiClient.DeleteChannel(Id).ConfigureAwait(false); | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public async Task Update() | |||||
| { | |||||
| var model = await Discord.ApiClient.GetChannel(Id).ConfigureAwait(false); | |||||
| Update(model); | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| protected abstract Task<GuildUser> GetUserInternal(ulong id); | |||||
| protected abstract Task<IEnumerable<GuildUser>> GetUsersInternal(); | |||||
| protected abstract Task<IEnumerable<GuildUser>> GetUsersInternal(int limit, int offset); | |||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | |||||
| IGuild IGuildChannel.Guild => Guild; | IGuild IGuildChannel.Guild => Guild; | ||||
| async Task<IInviteMetadata> IGuildChannel.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) | |||||
| => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IInviteMetadata>> IGuildChannel.GetInvites() | |||||
| => await GetInvites().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IGuildUser>> IGuildChannel.GetUsers() | |||||
| => await GetUsersInternal().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IUser>> IChannel.GetUsers() | |||||
| => await GetUsersInternal().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IUser>> IChannel.GetUsers(int limit, int offset) | |||||
| => await GetUsersInternal(limit, offset).ConfigureAwait(false); | |||||
| async Task<IGuildUser> IGuildChannel.GetUser(ulong id) | |||||
| => await GetUserInternal(id).ConfigureAwait(false); | |||||
| async Task<IUser> IChannel.GetUser(ulong id) | |||||
| => await GetUserInternal(id).ConfigureAwait(false); | |||||
| IReadOnlyCollection<Overwrite> IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); | |||||
| async Task<IUser> IChannel.GetUser(ulong id) => await GetUser(id).ConfigureAwait(false); | |||||
| async Task<IReadOnlyCollection<IUser>> IChannel.GetUsers() => await GetUsers().ConfigureAwait(false); | |||||
| async Task<IReadOnlyCollection<IUser>> IChannel.GetUsers(int limit, int offset) => await GetUsers(limit, offset).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| @@ -6,9 +6,9 @@ namespace Discord | |||||
| public interface IChannel : ISnowflakeEntity | public interface IChannel : ISnowflakeEntity | ||||
| { | { | ||||
| /// <summary> Gets a collection of all users in this channel. </summary> | /// <summary> Gets a collection of all users in this channel. </summary> | ||||
| Task<IEnumerable<IUser>> GetUsers(); | |||||
| Task<IReadOnlyCollection<IUser>> GetUsers(); | |||||
| /// <summary> Gets a paginated collection of all users in this channel. </summary> | /// <summary> Gets a paginated collection of all users in this channel. </summary> | ||||
| Task<IEnumerable<IUser>> GetUsers(int limit, int offset = 0); | |||||
| Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset = 0); | |||||
| /// <summary> Gets a user in this channel with the provided id.</summary> | /// <summary> Gets a user in this channel with the provided id.</summary> | ||||
| Task<IUser> GetUser(ulong id); | Task<IUser> GetUser(ulong id); | ||||
| } | } | ||||
| @@ -22,11 +22,11 @@ namespace Discord | |||||
| /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | ||||
| Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); | Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); | ||||
| /// <summary> Returns a collection of all invites to this channel. </summary> | /// <summary> Returns a collection of all invites to this channel. </summary> | ||||
| Task<IEnumerable<IInviteMetadata>> GetInvites(); | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> GetInvites(); | |||||
| /// <summary> Gets a collection of permission overwrites for this channel. </summary> | /// <summary> Gets a collection of permission overwrites for this channel. </summary> | ||||
| IReadOnlyDictionary<ulong, Overwrite> PermissionOverwrites { get; } | |||||
| IReadOnlyCollection<Overwrite> PermissionOverwrites { get; } | |||||
| /// <summary> Modifies this guild channel. </summary> | /// <summary> Modifies this guild channel. </summary> | ||||
| Task Modify(Action<ModifyGuildChannelParams> func); | Task Modify(Action<ModifyGuildChannelParams> func); | ||||
| @@ -44,7 +44,7 @@ namespace Discord | |||||
| Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions); | Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions); | ||||
| /// <summary> Gets a collection of all users in this channel. </summary> | /// <summary> Gets a collection of all users in this channel. </summary> | ||||
| new Task<IEnumerable<IGuildUser>> GetUsers(); | |||||
| new Task<IReadOnlyCollection<IGuildUser>> GetUsers(); | |||||
| /// <summary> Gets a user in this channel with the provided id.</summary> | /// <summary> Gets a user in this channel with the provided id.</summary> | ||||
| new Task<IGuildUser> GetUser(ulong id); | new Task<IGuildUser> GetUser(ulong id); | ||||
| } | } | ||||
| @@ -7,25 +7,25 @@ namespace Discord | |||||
| public interface IMessageChannel : IChannel | public interface IMessageChannel : IChannel | ||||
| { | { | ||||
| /// <summary> Gets all messages in this channel's cache. </summary> | /// <summary> Gets all messages in this channel's cache. </summary> | ||||
| IEnumerable<IMessage> CachedMessages { get; } | |||||
| IReadOnlyCollection<IMessage> CachedMessages { get; } | |||||
| /// <summary> Gets the message from this channel's cache with the given id, or null if none was found. </summary> | |||||
| Task<IMessage> GetCachedMessage(ulong id); | |||||
| /// <summary> Gets the last N messages from this message channel. </summary> | |||||
| Task<IEnumerable<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Gets a collection of messages in this channel. </summary> | |||||
| Task<IEnumerable<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Sends a message to this text channel. </summary> | |||||
| /// <summary> Sends a message to this message channel. </summary> | |||||
| Task<IMessage> SendMessage(string text, bool isTTS = false); | Task<IMessage> SendMessage(string text, bool isTTS = false); | ||||
| /// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
| Task<IMessage> SendFile(string filePath, string text = null, bool isTTS = false); | Task<IMessage> SendFile(string filePath, string text = null, bool isTTS = false); | ||||
| /// <summary> Sends a file to this text channel, with an optional caption. </summary> | /// <summary> Sends a file to this text channel, with an optional caption. </summary> | ||||
| Task<IMessage> SendFile(Stream stream, string filename, string text = null, bool isTTS = false); | Task<IMessage> SendFile(Stream stream, string filename, string text = null, bool isTTS = false); | ||||
| /// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> | |||||
| Task<IMessage> GetMessage(ulong id); | |||||
| /// <summary> Gets the message from this channel's cache with the given id, or null if not found. </summary> | |||||
| IMessage GetCachedMessage(ulong id); | |||||
| /// <summary> Gets the last N messages from this message channel. </summary> | |||||
| Task<IReadOnlyCollection<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Gets a collection of messages in this channel. </summary> | |||||
| Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); | |||||
| /// <summary> Bulk deletes multiple messages. </summary> | /// <summary> Bulk deletes multiple messages. </summary> | ||||
| Task DeleteMessages(IEnumerable<IMessage> messages); | Task DeleteMessages(IEnumerable<IMessage> messages); | ||||
| /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.</summary> | /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.</summary> | ||||
| Task TriggerTyping(); | Task TriggerTyping(); | ||||
| @@ -0,0 +1,116 @@ | |||||
| using Discord.API.Rest; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | |||||
| using System.IO; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| internal class TextChannel : GuildChannel, ITextChannel | |||||
| { | |||||
| public string Topic { get; private set; } | |||||
| public string Mention => MentionUtils.Mention(this); | |||||
| public virtual IReadOnlyCollection<IMessage> CachedMessages => ImmutableArray.Create<IMessage>(); | |||||
| public TextChannel(Guild guild, Model model) | |||||
| : base(guild, model) | |||||
| { | |||||
| } | |||||
| protected override void Update(Model model, UpdateSource source) | |||||
| { | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Topic = model.Topic; | |||||
| base.Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Modify(Action<ModifyTextChannelParams> func) | |||||
| { | |||||
| if (func != null) throw new NullReferenceException(nameof(func)); | |||||
| var args = new ModifyTextChannelParams(); | |||||
| func(args); | |||||
| var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public override async Task<IGuildUser> GetUser(ulong id) | |||||
| { | |||||
| var user = await Guild.GetUser(id).ConfigureAwait(false); | |||||
| if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IGuildUser>> GetUsers() | |||||
| { | |||||
| var users = await Guild.GetUsers().ConfigureAwait(false); | |||||
| return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| { | |||||
| var users = await Guild.GetUsers(limit, offset).ConfigureAwait(false); | |||||
| return users.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); | |||||
| } | |||||
| public async Task<IMessage> SendMessage(string text, bool isTTS) | |||||
| { | |||||
| var args = new CreateMessageParams { Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.CreateMessage(Guild.Id, Id, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| public async Task<IMessage> SendFile(string filePath, string text, bool isTTS) | |||||
| { | |||||
| string filename = Path.GetFileName(filePath); | |||||
| using (var file = File.OpenRead(filePath)) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, file, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| } | |||||
| public async Task<IMessage> SendFile(Stream stream, string filename, string text, bool isTTS) | |||||
| { | |||||
| var args = new UploadFileParams { Filename = filename, Content = text, IsTTS = isTTS }; | |||||
| var model = await Discord.ApiClient.UploadFile(Guild.Id, Id, stream, args).ConfigureAwait(false); | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| } | |||||
| public virtual async Task<IMessage> GetMessage(ulong id) | |||||
| { | |||||
| var model = await Discord.ApiClient.GetChannelMessage(Id, id).ConfigureAwait(false); | |||||
| if (model != null) | |||||
| return new Message(this, new User(Discord, model.Author), model); | |||||
| return null; | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit) | |||||
| { | |||||
| var args = new GetChannelMessagesParams { Limit = limit }; | |||||
| var models = await Discord.ApiClient.GetChannelMessages(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new Message(this, new User(Discord, x.Author), x)).ToImmutableArray(); | |||||
| } | |||||
| public async Task DeleteMessages(IEnumerable<IMessage> messages) | |||||
| { | |||||
| await Discord.ApiClient.DeleteMessages(Guild.Id, Id, new DeleteMessagesParams { MessageIds = messages.Select(x => x.Id) }).ConfigureAwait(false); | |||||
| } | |||||
| public async Task TriggerTyping() | |||||
| { | |||||
| await Discord.ApiClient.TriggerTypingIndicator(Id).ConfigureAwait(false); | |||||
| } | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => null; | |||||
| } | |||||
| } | |||||
| @@ -5,28 +5,27 @@ using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Channel; | using Model = Discord.API.Channel; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class VoiceChannel : GuildChannel, IVoiceChannel | |||||
| internal class VoiceChannel : GuildChannel, IVoiceChannel | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public int Bitrate { get; private set; } | public int Bitrate { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int UserLimit { get; private set; } | public int UserLimit { get; private set; } | ||||
| internal VoiceChannel(Guild guild, Model model) | |||||
| public VoiceChannel(Guild guild, Model model) | |||||
| : base(guild, model) | : base(guild, model) | ||||
| { | { | ||||
| } | } | ||||
| internal override void Update(Model model) | |||||
| protected override void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| base.Update(model); | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| base.Update(model, UpdateSource.Rest); | |||||
| Bitrate = model.Bitrate; | Bitrate = model.Bitrate; | ||||
| UserLimit = model.UserLimit; | UserLimit = model.UserLimit; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Modify(Action<ModifyVoiceChannelParams> func) | public async Task Modify(Action<ModifyVoiceChannelParams> func) | ||||
| { | { | ||||
| if (func != null) throw new NullReferenceException(nameof(func)); | if (func != null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -34,12 +33,21 @@ namespace Discord.Rest | |||||
| var args = new ModifyVoiceChannelParams(); | var args = new ModifyVoiceChannelParams(); | ||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyGuildChannel(Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| protected override Task<GuildUser> GetUserInternal(ulong id) { throw new NotSupportedException(); } | |||||
| protected override Task<IEnumerable<GuildUser>> GetUsersInternal() { throw new NotSupportedException(); } | |||||
| protected override Task<IEnumerable<GuildUser>> GetUsersInternal(int limit, int offset) { throw new NotSupportedException(); } | |||||
| public override Task<IGuildUser> GetUser(ulong id) | |||||
| { | |||||
| throw new NotSupportedException(); | |||||
| } | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers() | |||||
| { | |||||
| throw new NotSupportedException(); | |||||
| } | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| { | |||||
| throw new NotSupportedException(); | |||||
| } | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | ||||
| } | } | ||||
| @@ -0,0 +1,16 @@ | |||||
| namespace Discord | |||||
| { | |||||
| internal abstract class Entity<T> : IEntity<T> | |||||
| { | |||||
| public T Id { get; } | |||||
| public abstract DiscordClient Discord { get; } | |||||
| public bool IsAttached => this is ICachedEntity<T>; | |||||
| public Entity(T id) | |||||
| { | |||||
| Id = id; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -11,7 +11,7 @@ namespace Discord | |||||
| public bool RequireColons { get; } | public bool RequireColons { get; } | ||||
| public IImmutableList<ulong> RoleIds { get; } | public IImmutableList<ulong> RoleIds { get; } | ||||
| internal Emoji(Model model) | |||||
| public Emoji(Model model) | |||||
| { | { | ||||
| Id = model.Id; | Id = model.Id; | ||||
| Name = model.Name; | Name = model.Name; | ||||
| @@ -1,77 +1,60 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using Discord.Extensions; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Guild; | |||||
| using EmbedModel = Discord.API.GuildEmbed; | using EmbedModel = Discord.API.GuildEmbed; | ||||
| using Model = Discord.API.Guild; | |||||
| using RoleModel = Discord.API.Role; | using RoleModel = Discord.API.Role; | ||||
| using System.Diagnostics; | |||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| /// <summary> Represents a Discord guild (called a server in the official client). </summary> | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Guild : IGuild | |||||
| internal class Guild : SnowflakeEntity, IGuild | |||||
| { | { | ||||
| private ConcurrentDictionary<ulong, Role> _roles; | |||||
| private string _iconId, _splashId; | |||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| internal DiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | |||||
| protected ConcurrentDictionary<ulong, Role> _roles; | |||||
| protected string _iconId, _splashId; | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int AFKTimeout { get; private set; } | public int AFKTimeout { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsEmbeddable { get; private set; } | public bool IsEmbeddable { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int VerificationLevel { get; private set; } | public int VerificationLevel { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public ulong? AFKChannelId { get; private set; } | public ulong? AFKChannelId { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public ulong? EmbedChannelId { get; private set; } | public ulong? EmbedChannelId { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public ulong OwnerId { get; private set; } | public ulong OwnerId { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string VoiceRegionId { get; private set; } | public string VoiceRegionId { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<Emoji> Emojis { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<string> Features { get; private set; } | |||||
| public override DiscordClient Discord { get; } | |||||
| public ImmutableArray<Emoji> Emojis { get; protected set; } | |||||
| public ImmutableArray<string> Features { get; protected set; } | |||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc /> | |||||
| public ulong DefaultChannelId => Id; | |||||
| public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | ||||
| /// <inheritdoc /> | |||||
| public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); | public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); | ||||
| /// <inheritdoc /> | |||||
| public ulong DefaultChannelId => Id; | |||||
| /// <inheritdoc /> | |||||
| public Role EveryoneRole => GetRole(Id); | public Role EveryoneRole => GetRole(Id); | ||||
| /// <summary> Gets a collection of all roles in this guild. </summary> | |||||
| public IEnumerable<Role> Roles => _roles?.Select(x => x.Value) ?? Enumerable.Empty<Role>(); | |||||
| public IReadOnlyCollection<IRole> Roles => _roles.ToReadOnlyCollection(); | |||||
| internal Guild(DiscordClient discord, Model model) | |||||
| public Guild(DiscordClient discord, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| Discord = discord; | Discord = discord; | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| private void Update(Model model) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| AFKChannelId = model.AFKChannelId; | AFKChannelId = model.AFKChannelId; | ||||
| AFKTimeout = model.AFKTimeout; | |||||
| EmbedChannelId = model.EmbedChannelId; | EmbedChannelId = model.EmbedChannelId; | ||||
| AFKTimeout = model.AFKTimeout; | |||||
| IsEmbeddable = model.EmbedEnabled; | IsEmbeddable = model.EmbedEnabled; | ||||
| Features = model.Features; | |||||
| Features = model.Features.ToImmutableArray(); | |||||
| _iconId = model.Icon; | _iconId = model.Icon; | ||||
| Name = model.Name; | Name = model.Name; | ||||
| OwnerId = model.OwnerId; | OwnerId = model.OwnerId; | ||||
| @@ -84,10 +67,10 @@ namespace Discord.Rest | |||||
| var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | ||||
| for (int i = 0; i < model.Emojis.Length; i++) | for (int i = 0; i < model.Emojis.Length; i++) | ||||
| emojis.Add(new Emoji(model.Emojis[i])); | emojis.Add(new Emoji(model.Emojis[i])); | ||||
| Emojis = emojis.ToArray(); | |||||
| Emojis = emojis.ToImmutableArray(); | |||||
| } | } | ||||
| else | else | ||||
| Emojis = Array.Empty<Emoji>(); | |||||
| Emojis = ImmutableArray.Create<Emoji>(); | |||||
| var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0); | var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0); | ||||
| if (model.Roles != null) | if (model.Roles != null) | ||||
| @@ -97,28 +80,32 @@ namespace Discord.Rest | |||||
| } | } | ||||
| _roles = roles; | _roles = roles; | ||||
| } | } | ||||
| private void Update(EmbedModel model) | |||||
| public void Update(EmbedModel model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| IsEmbeddable = model.Enabled; | IsEmbeddable = model.Enabled; | ||||
| EmbedChannelId = model.ChannelId; | EmbedChannelId = model.ChannelId; | ||||
| } | } | ||||
| private void Update(IEnumerable<RoleModel> models) | |||||
| public void Update(IEnumerable<RoleModel> models, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Role role; | Role role; | ||||
| foreach (var model in models) | foreach (var model in models) | ||||
| { | { | ||||
| if (_roles.TryGetValue(model.Id, out role)) | if (_roles.TryGetValue(model.Id, out role)) | ||||
| role.Update(model); | |||||
| role.Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Update() | public async Task Update() | ||||
| { | { | ||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); | var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); | ||||
| Update(response); | |||||
| Update(response, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Modify(Action<ModifyGuildParams> func) | public async Task Modify(Action<ModifyGuildParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -126,9 +113,8 @@ namespace Discord.Rest | |||||
| var args = new ModifyGuildParams(); | var args = new ModifyGuildParams(); | ||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func) | public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -136,68 +122,57 @@ namespace Discord.Rest | |||||
| var args = new ModifyGuildEmbedParams(); | var args = new ModifyGuildEmbedParams(); | ||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args) | public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args) | ||||
| { | { | ||||
| //TODO: Update channels | |||||
| await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); | await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args) | public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args) | ||||
| { | { | ||||
| var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); | var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); | ||||
| Update(models); | |||||
| Update(models, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Leave() | public async Task Leave() | ||||
| { | { | ||||
| await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Delete() | public async Task Delete() | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task<IEnumerable<User>> GetBans() | |||||
| public async Task<IReadOnlyCollection<IUser>> GetBans() | |||||
| { | { | ||||
| var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); | var models = await Discord.ApiClient.GetGuildBans(Id).ConfigureAwait(false); | ||||
| return models.Select(x => new PublicUser(Discord, x)); | |||||
| return models.Select(x => new User(Discord, x)).ToImmutableArray(); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); | public Task AddBan(IUser user, int pruneDays = 0) => AddBan(user, pruneDays); | ||||
| /// <inheritdoc /> | |||||
| public async Task AddBan(ulong userId, int pruneDays = 0) | public async Task AddBan(ulong userId, int pruneDays = 0) | ||||
| { | { | ||||
| var args = new CreateGuildBanParams() { PruneDays = pruneDays }; | var args = new CreateGuildBanParams() { PruneDays = pruneDays }; | ||||
| await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); | await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public Task RemoveBan(IUser user) => RemoveBan(user.Id); | public Task RemoveBan(IUser user) => RemoveBan(user.Id); | ||||
| /// <inheritdoc /> | |||||
| public async Task RemoveBan(ulong userId) | public async Task RemoveBan(ulong userId) | ||||
| { | { | ||||
| await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); | await Discord.ApiClient.RemoveGuildBan(Id, userId).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary> | |||||
| public async Task<GuildChannel> GetChannel(ulong id) | |||||
| public virtual async Task<IGuildChannel> GetChannel(ulong id) | |||||
| { | { | ||||
| var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); | var model = await Discord.ApiClient.GetChannel(Id, id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return ToChannel(model); | return ToChannel(model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <summary> Gets a collection of all channels in this guild. </summary> | |||||
| public async Task<IEnumerable<GuildChannel>> GetChannels() | |||||
| public virtual async Task<IReadOnlyCollection<IGuildChannel>> GetChannels() | |||||
| { | { | ||||
| var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); | var models = await Discord.ApiClient.GetGuildChannels(Id).ConfigureAwait(false); | ||||
| return models.Select(x => ToChannel(x)); | |||||
| return models.Select(x => ToChannel(x)).ToImmutableArray(); | |||||
| } | } | ||||
| /// <summary> Creates a new text channel. </summary> | |||||
| public async Task<TextChannel> CreateTextChannel(string name) | |||||
| public async Task<ITextChannel> CreateTextChannel(string name) | |||||
| { | { | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | if (name == null) throw new ArgumentNullException(nameof(name)); | ||||
| @@ -205,8 +180,7 @@ namespace Discord.Rest | |||||
| var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | ||||
| return new TextChannel(this, model); | return new TextChannel(this, model); | ||||
| } | } | ||||
| /// <summary> Creates a new voice channel. </summary> | |||||
| public async Task<VoiceChannel> CreateVoiceChannel(string name) | |||||
| public async Task<IVoiceChannel> CreateVoiceChannel(string name) | |||||
| { | { | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | if (name == null) throw new ArgumentNullException(nameof(name)); | ||||
| @@ -214,29 +188,25 @@ namespace Discord.Rest | |||||
| var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | ||||
| return new VoiceChannel(this, model); | return new VoiceChannel(this, model); | ||||
| } | } | ||||
| /// <summary> Gets a collection of all integrations attached to this guild. </summary> | |||||
| public async Task<IEnumerable<GuildIntegration>> GetIntegrations() | |||||
| public async Task<IReadOnlyCollection<IGuildIntegration>> GetIntegrations() | |||||
| { | { | ||||
| var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); | var models = await Discord.ApiClient.GetGuildIntegrations(Id).ConfigureAwait(false); | ||||
| return models.Select(x => new GuildIntegration(this, x)); | |||||
| return models.Select(x => new GuildIntegration(this, x)).ToImmutableArray(); | |||||
| } | } | ||||
| /// <summary> Creates a new integration for this guild. </summary> | |||||
| public async Task<GuildIntegration> CreateIntegration(ulong id, string type) | |||||
| public async Task<IGuildIntegration> CreateIntegration(ulong id, string type) | |||||
| { | { | ||||
| var args = new CreateGuildIntegrationParams { Id = id, Type = type }; | var args = new CreateGuildIntegrationParams { Id = id, Type = type }; | ||||
| var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); | ||||
| return new GuildIntegration(this, model); | return new GuildIntegration(this, model); | ||||
| } | } | ||||
| /// <summary> Gets a collection of all invites to this guild. </summary> | |||||
| public async Task<IEnumerable<InviteMetadata>> GetInvites() | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvites() | |||||
| { | { | ||||
| var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); | var models = await Discord.ApiClient.GetGuildInvites(Id).ConfigureAwait(false); | ||||
| return models.Select(x => new InviteMetadata(Discord, x)); | |||||
| return models.Select(x => new InviteMetadata(Discord, x)).ToImmutableArray(); | |||||
| } | } | ||||
| /// <summary> Creates a new invite to this guild. </summary> | |||||
| public async Task<InviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) | |||||
| public async Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool isTemporary = false, bool withXkcd = false) | |||||
| { | { | ||||
| if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); | if (maxAge <= 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); | ||||
| if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | if (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | ||||
| @@ -251,18 +221,15 @@ namespace Discord.Rest | |||||
| var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); | var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); | ||||
| return new InviteMetadata(Discord, model); | return new InviteMetadata(Discord, model); | ||||
| } | } | ||||
| /// <summary> Gets the role in this guild with the provided id, or null if not found. </summary> | |||||
| public Role GetRole(ulong id) | public Role GetRole(ulong id) | ||||
| { | { | ||||
| Role result = null; | Role result = null; | ||||
| if (_roles?.TryGetValue(id, out result) == true) | if (_roles?.TryGetValue(id, out result) == true) | ||||
| return result; | return result; | ||||
| return null; | return null; | ||||
| } | |||||
| /// <summary> Creates a new role. </summary> | |||||
| public async Task<Role> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) | |||||
| } | |||||
| public async Task<IRole> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false) | |||||
| { | { | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | if (name == null) throw new ArgumentNullException(nameof(name)); | ||||
| @@ -280,34 +247,30 @@ namespace Discord.Rest | |||||
| return role; | return role; | ||||
| } | } | ||||
| /// <summary> Gets a collection of all users in this guild. </summary> | |||||
| public async Task<IEnumerable<GuildUser>> GetUsers() | |||||
| { | |||||
| var args = new GetGuildMembersParams(); | |||||
| var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new GuildUser(this, x)); | |||||
| } | |||||
| /// <summary> Gets a paged collection of all users in this guild. </summary> | |||||
| public async Task<IEnumerable<GuildUser>> GetUsers(int limit, int offset) | |||||
| { | |||||
| var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; | |||||
| var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new GuildUser(this, x)); | |||||
| } | |||||
| /// <summary> Gets the user in this guild with the provided id, or null if not found. </summary> | |||||
| public async Task<GuildUser> GetUser(ulong id) | |||||
| public virtual async Task<IGuildUser> GetUser(ulong id) | |||||
| { | { | ||||
| var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); | var model = await Discord.ApiClient.GetGuildMember(Id, id).ConfigureAwait(false); | ||||
| if (model != null) | if (model != null) | ||||
| return new GuildUser(this, model); | |||||
| return new GuildUser(this, new User(Discord, model.User), model); | |||||
| return null; | return null; | ||||
| } | } | ||||
| /// <summary> Gets a the current user. </summary> | |||||
| public async Task<GuildUser> GetCurrentUser() | |||||
| public virtual async Task<IGuildUser> GetCurrentUser() | |||||
| { | { | ||||
| var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | var currentUser = await Discord.GetCurrentUser().ConfigureAwait(false); | ||||
| return await GetUser(currentUser.Id).ConfigureAwait(false); | return await GetUser(currentUser.Id).ConfigureAwait(false); | ||||
| } | } | ||||
| public virtual async Task<IReadOnlyCollection<IGuildUser>> GetUsers() | |||||
| { | |||||
| var args = new GetGuildMembersParams(); | |||||
| var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); | |||||
| } | |||||
| public virtual async Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| { | |||||
| var args = new GetGuildMembersParams { Limit = limit, Offset = offset }; | |||||
| var models = await Discord.ApiClient.GetGuildMembers(Id, args).ConfigureAwait(false); | |||||
| return models.Select(x => new GuildUser(this, new User(Discord, x.User), x)).ToImmutableArray(); | |||||
| } | |||||
| public async Task<int> PruneUsers(int days = 30, bool simulate = false) | public async Task<int> PruneUsers(int days = 30, bool simulate = false) | ||||
| { | { | ||||
| var args = new GuildPruneParams() { Days = days }; | var args = new GuildPruneParams() { Days = days }; | ||||
| @@ -324,45 +287,22 @@ namespace Discord.Rest | |||||
| switch (model.Type) | switch (model.Type) | ||||
| { | { | ||||
| case ChannelType.Text: | case ChannelType.Text: | ||||
| default: | |||||
| return new TextChannel(this, model); | return new TextChannel(this, model); | ||||
| case ChannelType.Voice: | case ChannelType.Voice: | ||||
| return new VoiceChannel(this, model); | return new VoiceChannel(this, model); | ||||
| default: | |||||
| throw new InvalidOperationException($"Unknown channel type: {model.Type}"); | |||||
| } | } | ||||
| } | } | ||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| IEnumerable<Emoji> IGuild.Emojis => Emojis; | |||||
| ulong IGuild.EveryoneRoleId => EveryoneRole.Id; | |||||
| IEnumerable<string> IGuild.Features => Features; | |||||
| IRole IGuild.EveryoneRole => EveryoneRole; | |||||
| IReadOnlyCollection<Emoji> IGuild.Emojis => Emojis; | |||||
| IReadOnlyCollection<string> IGuild.Features => Features; | |||||
| async Task<IEnumerable<IUser>> IGuild.GetBans() | |||||
| => await GetBans().ConfigureAwait(false); | |||||
| async Task<IGuildChannel> IGuild.GetChannel(ulong id) | |||||
| => await GetChannel(id).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IGuildChannel>> IGuild.GetChannels() | |||||
| => await GetChannels().ConfigureAwait(false); | |||||
| async Task<IInviteMetadata> IGuild.CreateInvite(int? maxAge, int? maxUses, bool isTemporary, bool withXkcd) | |||||
| => await CreateInvite(maxAge, maxUses, isTemporary, withXkcd).ConfigureAwait(false); | |||||
| async Task<IRole> IGuild.CreateRole(string name, GuildPermissions? permissions, Color? color, bool isHoisted) | |||||
| => await CreateRole(name, permissions, color, isHoisted).ConfigureAwait(false); | |||||
| async Task<ITextChannel> IGuild.CreateTextChannel(string name) | |||||
| => await CreateTextChannel(name).ConfigureAwait(false); | |||||
| async Task<IVoiceChannel> IGuild.CreateVoiceChannel(string name) | |||||
| => await CreateVoiceChannel(name).ConfigureAwait(false); | |||||
| async Task<IEnumerable<IInviteMetadata>> IGuild.GetInvites() | |||||
| => await GetInvites().ConfigureAwait(false); | |||||
| Task<IRole> IGuild.GetRole(ulong id) | |||||
| => Task.FromResult<IRole>(GetRole(id)); | |||||
| Task<IEnumerable<IRole>> IGuild.GetRoles() | |||||
| => Task.FromResult<IEnumerable<IRole>>(Roles); | |||||
| async Task<IGuildUser> IGuild.GetUser(ulong id) | |||||
| => await GetUser(id).ConfigureAwait(false); | |||||
| async Task<IGuildUser> IGuild.GetCurrentUser() | |||||
| => await GetCurrentUser().ConfigureAwait(false); | |||||
| async Task<IEnumerable<IGuildUser>> IGuild.GetUsers() | |||||
| => await GetUsers().ConfigureAwait(false); | |||||
| IRole IGuild.GetRole(ulong id) => GetRole(id); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,18 @@ | |||||
| using Model = Discord.API.GuildEmbed; | |||||
| namespace Discord | |||||
| { | |||||
| public struct GuildEmbed | |||||
| { | |||||
| public bool IsEnabled { get; private set; } | |||||
| public ulong? ChannelId { get; private set; } | |||||
| public GuildEmbed(bool isEnabled, ulong? channelId) | |||||
| { | |||||
| ChannelId = channelId; | |||||
| IsEnabled = isEnabled; | |||||
| } | |||||
| internal GuildEmbed(Model model) | |||||
| : this(model.Enabled, model.ChannelId) { } | |||||
| } | |||||
| } | |||||
| @@ -4,47 +4,37 @@ using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Integration; | using Model = Discord.API.Integration; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class GuildIntegration : IGuildIntegration | |||||
| internal class GuildIntegration : Entity<ulong>, IGuildIntegration | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string Type { get; private set; } | public string Type { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsEnabled { get; private set; } | public bool IsEnabled { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsSyncing { get; private set; } | public bool IsSyncing { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public ulong ExpireBehavior { get; private set; } | public ulong ExpireBehavior { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public ulong ExpireGracePeriod { get; private set; } | public ulong ExpireGracePeriod { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime SyncedAt { get; private set; } | public DateTime SyncedAt { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public Guild Guild { get; private set; } | public Guild Guild { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public Role Role { get; private set; } | public Role Role { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public User User { get; private set; } | public User User { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public IntegrationAccount Account { get; private set; } | public IntegrationAccount Account { get; private set; } | ||||
| internal DiscordClient Discord => Guild.Discord; | |||||
| internal GuildIntegration(Guild guild, Model model) | |||||
| public override DiscordClient Discord => Guild.Discord; | |||||
| public GuildIntegration(Guild guild, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Guild = guild; | Guild = guild; | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| private void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Name = model.Name; | Name = model.Name; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| IsEnabled = model.Enabled; | IsEnabled = model.Enabled; | ||||
| @@ -53,16 +43,14 @@ namespace Discord.Rest | |||||
| ExpireGracePeriod = model.ExpireGracePeriod; | ExpireGracePeriod = model.ExpireGracePeriod; | ||||
| SyncedAt = model.SyncedAt; | SyncedAt = model.SyncedAt; | ||||
| Role = Guild.GetRole(model.RoleId); | |||||
| User = new PublicUser(Discord, model.User); | |||||
| Role = Guild.GetRole(model.RoleId) as Role; | |||||
| User = new User(Discord, model.User); | |||||
| } | } | ||||
| /// <summary> </summary> | |||||
| public async Task Delete() | public async Task Delete() | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <summary> </summary> | |||||
| public async Task Modify(Action<ModifyGuildIntegrationParams> func) | public async Task Modify(Action<ModifyGuildIntegrationParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -71,9 +59,8 @@ namespace Discord.Rest | |||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <summary> </summary> | |||||
| public async Task Sync() | public async Task Sync() | ||||
| { | { | ||||
| await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | ||||
| @@ -83,8 +70,7 @@ namespace Discord.Rest | |||||
| private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; | private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; | ||||
| IGuild IGuildIntegration.Guild => Guild; | IGuild IGuildIntegration.Guild => Guild; | ||||
| IRole IGuildIntegration.Role => Role; | |||||
| IUser IGuildIntegration.User => User; | IUser IGuildIntegration.User => User; | ||||
| IntegrationAccount IGuildIntegration.Account => Account; | |||||
| IRole IGuildIntegration.Role => Role; | |||||
| } | } | ||||
| } | } | ||||
| @@ -7,13 +7,17 @@ namespace Discord | |||||
| { | { | ||||
| public interface IGuild : IDeletable, ISnowflakeEntity, IUpdateable | public interface IGuild : IDeletable, ISnowflakeEntity, IUpdateable | ||||
| { | { | ||||
| /// <summary> Gets the name of this guild. </summary> | |||||
| string Name { get; } | |||||
| /// <summary> Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. </summary> | /// <summary> Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. </summary> | ||||
| int AFKTimeout { get; } | int AFKTimeout { get; } | ||||
| /// <summary> Returns true if this guild is embeddable (e.g. widget) </summary> | /// <summary> Returns true if this guild is embeddable (e.g. widget) </summary> | ||||
| bool IsEmbeddable { get; } | bool IsEmbeddable { get; } | ||||
| /// <summary> Gets the name of this guild. </summary> | |||||
| string Name { get; } | |||||
| int VerificationLevel { get; } | int VerificationLevel { get; } | ||||
| /// <summary> Returns the url to this guild's icon, or null if one is not set. </summary> | |||||
| string IconUrl { get; } | |||||
| /// <summary> Returns the url to this guild's splash image, or null if one is not set. </summary> | |||||
| string SplashUrl { get; } | |||||
| /// <summary> Gets the id of the AFK voice channel for this guild if set, or null if not. </summary> | /// <summary> Gets the id of the AFK voice channel for this guild if set, or null if not. </summary> | ||||
| ulong? AFKChannelId { get; } | ulong? AFKChannelId { get; } | ||||
| @@ -21,22 +25,19 @@ namespace Discord | |||||
| ulong DefaultChannelId { get; } | ulong DefaultChannelId { get; } | ||||
| /// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary> | /// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary> | ||||
| ulong? EmbedChannelId { get; } | ulong? EmbedChannelId { get; } | ||||
| /// <summary> Gets the id of the role containing all users in this guild. </summary> | |||||
| ulong EveryoneRoleId { get; } | |||||
| /// <summary> Gets the id of the user that created this guild. </summary> | /// <summary> Gets the id of the user that created this guild. </summary> | ||||
| ulong OwnerId { get; } | ulong OwnerId { get; } | ||||
| /// <summary> Gets the id of the server region hosting this guild's voice channels. </summary> | |||||
| /// <summary> Gets the id of the region hosting this guild's voice channels. </summary> | |||||
| string VoiceRegionId { get; } | string VoiceRegionId { get; } | ||||
| /// <summary> Returns the url to this server's icon, or null if one is not set. </summary> | |||||
| string IconUrl { get; } | |||||
| /// <summary> Returns the url to this server's splash image, or null if one is not set. </summary> | |||||
| string SplashUrl { get; } | |||||
| /// <summary> Gets the built-in role containing all users in this guild. </summary> | |||||
| IRole EveryoneRole { get; } | |||||
| /// <summary> Gets a collection of all custom emojis for this guild. </summary> | /// <summary> Gets a collection of all custom emojis for this guild. </summary> | ||||
| IEnumerable<Emoji> Emojis { get; } | |||||
| IReadOnlyCollection<Emoji> Emojis { get; } | |||||
| /// <summary> Gets a collection of all extra features added to this guild. </summary> | /// <summary> Gets a collection of all extra features added to this guild. </summary> | ||||
| IEnumerable<string> Features { get; } | |||||
| IReadOnlyCollection<string> Features { get; } | |||||
| /// <summary> Gets a collection of all roles in this guild. </summary> | |||||
| IReadOnlyCollection<IRole> Roles { get; } | |||||
| /// <summary> Modifies this guild. </summary> | /// <summary> Modifies this guild. </summary> | ||||
| Task Modify(Action<ModifyGuildParams> func); | Task Modify(Action<ModifyGuildParams> func); | ||||
| @@ -50,7 +51,7 @@ namespace Discord | |||||
| Task Leave(); | Task Leave(); | ||||
| /// <summary> Gets a collection of all users banned on this guild. </summary> | /// <summary> Gets a collection of all users banned on this guild. </summary> | ||||
| Task<IEnumerable<IUser>> GetBans(); | |||||
| Task<IReadOnlyCollection<IUser>> GetBans(); | |||||
| /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> | /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> | ||||
| Task AddBan(IUser user, int pruneDays = 0); | Task AddBan(IUser user, int pruneDays = 0); | ||||
| /// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary> | /// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary> | ||||
| @@ -61,7 +62,7 @@ namespace Discord | |||||
| Task RemoveBan(ulong userId); | Task RemoveBan(ulong userId); | ||||
| /// <summary> Gets a collection of all channels in this guild. </summary> | /// <summary> Gets a collection of all channels in this guild. </summary> | ||||
| Task<IEnumerable<IGuildChannel>> GetChannels(); | |||||
| Task<IReadOnlyCollection<IGuildChannel>> GetChannels(); | |||||
| /// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary> | /// <summary> Gets the channel in this guild with the provided id, or null if not found. </summary> | ||||
| Task<IGuildChannel> GetChannel(ulong id); | Task<IGuildChannel> GetChannel(ulong id); | ||||
| /// <summary> Creates a new text channel. </summary> | /// <summary> Creates a new text channel. </summary> | ||||
| @@ -70,7 +71,7 @@ namespace Discord | |||||
| Task<IVoiceChannel> CreateVoiceChannel(string name); | Task<IVoiceChannel> CreateVoiceChannel(string name); | ||||
| /// <summary> Gets a collection of all invites to this guild. </summary> | /// <summary> Gets a collection of all invites to this guild. </summary> | ||||
| Task<IEnumerable<IInviteMetadata>> GetInvites(); | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> GetInvites(); | |||||
| /// <summary> Creates a new invite to this guild. </summary> | /// <summary> Creates a new invite to this guild. </summary> | ||||
| /// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param> | /// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param> | ||||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> | /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> | ||||
| @@ -78,15 +79,13 @@ namespace Discord | |||||
| /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | ||||
| Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); | Task<IInviteMetadata> CreateInvite(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool withXkcd = false); | ||||
| /// <summary> Gets a collection of all roles in this guild. </summary> | |||||
| Task<IEnumerable<IRole>> GetRoles(); | |||||
| /// <summary> Gets the role in this guild with the provided id, or null if not found. </summary> | /// <summary> Gets the role in this guild with the provided id, or null if not found. </summary> | ||||
| Task<IRole> GetRole(ulong id); | |||||
| IRole GetRole(ulong id); | |||||
| /// <summary> Creates a new role. </summary> | /// <summary> Creates a new role. </summary> | ||||
| Task<IRole> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); | Task<IRole> CreateRole(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false); | ||||
| /// <summary> Gets a collection of all users in this guild. </summary> | /// <summary> Gets a collection of all users in this guild. </summary> | ||||
| Task<IEnumerable<IGuildUser>> GetUsers(); | |||||
| Task<IReadOnlyCollection<IGuildUser>> GetUsers(); | |||||
| /// <summary> Gets the user in this guild with the provided id, or null if not found. </summary> | /// <summary> Gets the user in this guild with the provided id, or null if not found. </summary> | ||||
| Task<IGuildUser> GetUser(ulong id); | Task<IGuildUser> GetUser(ulong id); | ||||
| /// <summary> Gets the current user for this guild. </summary> | /// <summary> Gets the current user for this guild. </summary> | ||||
| @@ -1,8 +0,0 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public interface IGuildEmbed : ISnowflakeEntity | |||||
| { | |||||
| bool IsEnabled { get; } | |||||
| ulong? ChannelId { get; } | |||||
| } | |||||
| } | |||||
| @@ -4,7 +4,7 @@ | |||||
| { | { | ||||
| /// <summary> Gets the name of this guild. </summary> | /// <summary> Gets the name of this guild. </summary> | ||||
| string Name { get; } | string Name { get; } | ||||
| /// <summary> Returns the url to this server's icon, or null if one is not set. </summary> | |||||
| /// <summary> Returns the url to this guild's icon, or null if one is not set. </summary> | |||||
| string IconUrl { get; } | string IconUrl { get; } | ||||
| /// <summary> Returns true if the current user owns this guild. </summary> | /// <summary> Returns true if the current user owns this guild. </summary> | ||||
| bool IsOwner { get; } | bool IsOwner { get; } | ||||
| @@ -1,7 +1,9 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IVoiceRegion : IEntity<string> | |||||
| public interface IVoiceRegion | |||||
| { | { | ||||
| /// <summary> Gets the unique identifier for this voice region. </summary> | |||||
| string Id { get; } | |||||
| /// <summary> Gets the name of this voice region. </summary> | /// <summary> Gets the name of this voice region. </summary> | ||||
| string Name { get; } | string Name { get; } | ||||
| /// <summary> Returns true if this voice region is exclusive to VIP accounts. </summary> | /// <summary> Returns true if this voice region is exclusive to VIP accounts. </summary> | ||||
| @@ -5,10 +5,7 @@ namespace Discord | |||||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||||
| public struct IntegrationAccount | public struct IntegrationAccount | ||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public string Id { get; } | public string Id { get; } | ||||
| /// <inheritdoc /> | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| @@ -1,50 +1,42 @@ | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.UserGuild; | using Model = Discord.API.UserGuild; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class UserGuild : IUserGuild | |||||
| internal class UserGuild : SnowflakeEntity, IUserGuild | |||||
| { | { | ||||
| private string _iconId; | private string _iconId; | ||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| internal IDiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| public bool IsOwner { get; private set; } | public bool IsOwner { get; private set; } | ||||
| public GuildPermissions Permissions { get; private set; } | public GuildPermissions Permissions { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc /> | |||||
| public override DiscordClient Discord { get; } | |||||
| public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | public string IconUrl => API.CDN.GetGuildIconUrl(Id, _iconId); | ||||
| internal UserGuild(IDiscordClient discord, Model model) | |||||
| public UserGuild(DiscordClient discord, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Discord = discord; | Discord = discord; | ||||
| Id = model.Id; | |||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| private void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| _iconId = model.Icon; | _iconId = model.Icon; | ||||
| IsOwner = model.Owner; | IsOwner = model.Owner; | ||||
| Name = model.Name; | Name = model.Name; | ||||
| Permissions = new GuildPermissions(model.Permissions); | Permissions = new GuildPermissions(model.Permissions); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Leave() | public async Task Leave() | ||||
| { | { | ||||
| await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Delete() | public async Task Delete() | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); | await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); | ||||
| @@ -4,22 +4,16 @@ using Model = Discord.API.VoiceRegion; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||||
| public class VoiceRegion : IVoiceRegion | |||||
| internal class VoiceRegion : IVoiceRegion | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public string Id { get; } | public string Id { get; } | ||||
| /// <inheritdoc /> | |||||
| public string Name { get; } | public string Name { get; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsVip { get; } | public bool IsVip { get; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsOptimal { get; } | public bool IsOptimal { get; } | ||||
| /// <inheritdoc /> | |||||
| public string SampleHostname { get; } | public string SampleHostname { get; } | ||||
| /// <inheritdoc /> | |||||
| public int SamplePort { get; } | public int SamplePort { get; } | ||||
| internal VoiceRegion(Model model) | |||||
| public VoiceRegion(Model model) | |||||
| { | { | ||||
| Id = model.Id; | Id = model.Id; | ||||
| Name = model.Name; | Name = model.Name; | ||||
| @@ -4,5 +4,9 @@ namespace Discord | |||||
| { | { | ||||
| /// <summary> Gets the unique identifier for this object. </summary> | /// <summary> Gets the unique identifier for this object. </summary> | ||||
| TId Id { get; } | TId Id { get; } | ||||
| //TODO: What do we do when an object is destroyed due to reconnect? This summary isn't correct. | |||||
| /// <summary> Returns true if this object is getting live updates from the DiscordClient. </summary> | |||||
| bool IsAttached { get;} | |||||
| } | } | ||||
| } | } | ||||
| @@ -4,7 +4,7 @@ namespace Discord | |||||
| { | { | ||||
| public interface IUpdateable | public interface IUpdateable | ||||
| { | { | ||||
| /// <summary> Ensures this objects's cached properties reflect its current state on the Discord server. </summary> | |||||
| /// <summary> Updates this object's properties with its current state. </summary> | |||||
| Task Update(); | Task Update(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -18,7 +18,7 @@ namespace Discord | |||||
| /// <summary> Gets the id of the guild this invite is linked to. </summary> | /// <summary> Gets the id of the guild this invite is linked to. </summary> | ||||
| ulong GuildId { get; } | ulong GuildId { get; } | ||||
| /// <summary> Accepts this invite and joins the target server. This will fail on bot accounts. </summary> | |||||
| /// <summary> Accepts this invite and joins the target guild. This will fail on bot accounts. </summary> | |||||
| Task Accept(); | Task Accept(); | ||||
| } | } | ||||
| } | } | ||||
| @@ -5,38 +5,31 @@ using Model = Discord.API.Invite; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Invite : IInvite | |||||
| internal class Invite : Entity<string>, IInvite | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public string Code { get; } | |||||
| internal IDiscordClient Discord { get; } | |||||
| public string ChannelName { get; private set; } | |||||
| public string GuildName { get; private set; } | |||||
| public string XkcdCode { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public ulong GuildId { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public ulong ChannelId { get; private set; } | public ulong ChannelId { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string XkcdCode { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public string GuildName { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public string ChannelName { get; private set; } | |||||
| public ulong GuildId { get; private set; } | |||||
| public override DiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | |||||
| public string Code => Id; | |||||
| public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; | public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; | ||||
| /// <inheritdoc /> | |||||
| public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; | public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; | ||||
| internal Invite(IDiscordClient discord, Model model) | |||||
| public Invite(DiscordClient discord, Model model) | |||||
| : base(model.Code) | |||||
| { | { | ||||
| Discord = discord; | Discord = discord; | ||||
| Code = model.Code; | |||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| protected virtual void Update(Model model) | |||||
| protected void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| XkcdCode = model.XkcdPass; | XkcdCode = model.XkcdPass; | ||||
| GuildId = model.Guild.Id; | GuildId = model.Guild.Id; | ||||
| ChannelId = model.Channel.Id; | ChannelId = model.Channel.Id; | ||||
| @@ -44,22 +37,16 @@ namespace Discord | |||||
| ChannelName = model.Channel.Name; | ChannelName = model.Channel.Name; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Accept() | public async Task Accept() | ||||
| { | { | ||||
| await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); | await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Delete() | public async Task Delete() | ||||
| { | { | ||||
| await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); | await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override string ToString() => XkcdUrl ?? Url; | public override string ToString() => XkcdUrl ?? Url; | ||||
| private string DebuggerDisplay => $"{XkcdUrl ?? Url} ({GuildName} / {ChannelName})"; | private string DebuggerDisplay => $"{XkcdUrl ?? Url} ({GuildName} / {ChannelName})"; | ||||
| string IEntity<string>.Id => Code; | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,26 +2,23 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public class InviteMetadata : Invite, IInviteMetadata | |||||
| internal class InviteMetadata : Invite, IInviteMetadata | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public bool IsRevoked { get; private set; } | public bool IsRevoked { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsTemporary { get; private set; } | public bool IsTemporary { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int? MaxAge { get; private set; } | public int? MaxAge { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int? MaxUses { get; private set; } | public int? MaxUses { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int Uses { get; private set; } | public int Uses { get; private set; } | ||||
| internal InviteMetadata(IDiscordClient client, Model model) | |||||
| public InviteMetadata(DiscordClient client, Model model) | |||||
| : base(client, model) | : base(client, model) | ||||
| { | { | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| private void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| IsRevoked = model.Revoked; | IsRevoked = model.Revoked; | ||||
| IsTemporary = model.Temporary; | IsTemporary = model.Temporary; | ||||
| MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; | MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; | ||||
| @@ -2,16 +2,16 @@ | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public struct Embed | |||||
| internal class Embed : IEmbed | |||||
| { | { | ||||
| public string Description { get; } | |||||
| public string Url { get; } | public string Url { get; } | ||||
| public string Type { get; } | |||||
| public string Title { get; } | public string Title { get; } | ||||
| public string Description { get; } | |||||
| public string Type { get; } | |||||
| public EmbedProvider Provider { get; } | public EmbedProvider Provider { get; } | ||||
| public EmbedThumbnail Thumbnail { get; } | public EmbedThumbnail Thumbnail { get; } | ||||
| internal Embed(Model model) | |||||
| public Embed(Model model) | |||||
| { | { | ||||
| Url = model.Url; | Url = model.Url; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| @@ -7,10 +7,12 @@ namespace Discord | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public string Url { get; } | public string Url { get; } | ||||
| internal EmbedProvider(Model model) | |||||
| public EmbedProvider(string name, string url) | |||||
| { | { | ||||
| Name = model.Name; | |||||
| Url = model.Url; | |||||
| Name = name; | |||||
| Url = url; | |||||
| } | } | ||||
| internal EmbedProvider(Model model) | |||||
| : this(model.Name, model.Url) { } | |||||
| } | } | ||||
| } | } | ||||
| @@ -9,12 +9,15 @@ namespace Discord | |||||
| public int? Height { get; } | public int? Height { get; } | ||||
| public int? Width { get; } | public int? Width { get; } | ||||
| internal EmbedThumbnail(Model model) | |||||
| public EmbedThumbnail(string url, string proxyUrl, int? height, int? width) | |||||
| { | { | ||||
| Url = model.Url; | |||||
| ProxyUrl = model.ProxyUrl; | |||||
| Height = model.Height; | |||||
| Width = model.Width; | |||||
| Url = url; | |||||
| ProxyUrl = proxyUrl; | |||||
| Height = height; | |||||
| Width = width; | |||||
| } | } | ||||
| internal EmbedThumbnail(Model model) | |||||
| : this(model.Url, model.ProxyUrl, model.Height, model.Width) { } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,12 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public interface IEmbed | |||||
| { | |||||
| string Url { get; } | |||||
| string Type { get; } | |||||
| string Title { get; } | |||||
| string Description { get; } | |||||
| EmbedProvider Provider { get; } | |||||
| EmbedThumbnail Thumbnail { get; } | |||||
| } | |||||
| } | |||||
| @@ -5,7 +5,7 @@ using System.Collections.Generic; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IMessage : IDeletable, ISnowflakeEntity | |||||
| public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable | |||||
| { | { | ||||
| /// <summary> Gets the time of this message's last edit, if any. </summary> | /// <summary> Gets the time of this message's last edit, if any. </summary> | ||||
| DateTime? EditedTimestamp { get; } | DateTime? EditedTimestamp { get; } | ||||
| @@ -16,23 +16,22 @@ namespace Discord | |||||
| /// <summary> Returns the text for this message after mention processing. </summary> | /// <summary> Returns the text for this message after mention processing. </summary> | ||||
| string Text { get; } | string Text { get; } | ||||
| /// <summary> Gets the time this message was sent. </summary> | /// <summary> Gets the time this message was sent. </summary> | ||||
| DateTime Timestamp { get; } //TODO: Is this different from IHasSnowflake.CreatedAt? | |||||
| DateTime Timestamp { get; } | |||||
| /// <summary> Gets the channel this message was sent to. </summary> | /// <summary> Gets the channel this message was sent to. </summary> | ||||
| IMessageChannel Channel { get; } | IMessageChannel Channel { get; } | ||||
| /// <summary> Gets the author of this message. </summary> | /// <summary> Gets the author of this message. </summary> | ||||
| IUser Author { get; } | IUser Author { get; } | ||||
| /// <summary> Returns a collection of all attachments included in this message. </summary> | /// <summary> Returns a collection of all attachments included in this message. </summary> | ||||
| IReadOnlyList<Attachment> Attachments { get; } | |||||
| IReadOnlyCollection<Attachment> Attachments { get; } | |||||
| /// <summary> Returns a collection of all embeds included in this message. </summary> | /// <summary> Returns a collection of all embeds included in this message. </summary> | ||||
| IReadOnlyList<Embed> Embeds { get; } | |||||
| IReadOnlyCollection<IEmbed> Embeds { get; } | |||||
| /// <summary> Returns a collection of channel ids mentioned in this message. </summary> | /// <summary> Returns a collection of channel ids mentioned in this message. </summary> | ||||
| IReadOnlyList<ulong> MentionedChannelIds { get; } | |||||
| IReadOnlyCollection<ulong> MentionedChannelIds { get; } | |||||
| /// <summary> Returns a collection of role ids mentioned in this message. </summary> | /// <summary> Returns a collection of role ids mentioned in this message. </summary> | ||||
| IReadOnlyList<ulong> MentionedRoleIds { get; } | |||||
| IReadOnlyCollection<ulong> MentionedRoleIds { get; } | |||||
| /// <summary> Returns a collection of user ids mentioned in this message. </summary> | /// <summary> Returns a collection of user ids mentioned in this message. </summary> | ||||
| IReadOnlyList<IUser> MentionedUsers { get; } | |||||
| IReadOnlyCollection<IUser> MentionedUsers { get; } | |||||
| /// <summary> Modifies this message. </summary> | /// <summary> Modifies this message. </summary> | ||||
| Task Modify(Action<ModifyMessageParams> func); | Task Modify(Action<ModifyMessageParams> func); | ||||
| @@ -6,55 +6,40 @@ using System.Diagnostics; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Message; | using Model = Discord.API.Message; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Message : IMessage | |||||
| { | |||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| /// <inheritdoc /> | |||||
| internal class Message : SnowflakeEntity, IMessage | |||||
| { | |||||
| public DateTime? EditedTimestamp { get; private set; } | public DateTime? EditedTimestamp { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsTTS { get; private set; } | public bool IsTTS { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string RawText { get; private set; } | public string RawText { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string Text { get; private set; } | public string Text { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime Timestamp { get; private set; } | public DateTime Timestamp { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public IMessageChannel Channel { get; } | public IMessageChannel Channel { get; } | ||||
| /// <inheritdoc /> | |||||
| public IUser Author { get; } | public IUser Author { get; } | ||||
| public ImmutableArray<Attachment> Attachments { get; private set; } | |||||
| public ImmutableArray<Embed> Embeds { get; private set; } | |||||
| public ImmutableArray<ulong> MentionedChannelIds { get; private set; } | |||||
| public ImmutableArray<ulong> MentionedRoleIds { get; private set; } | |||||
| public ImmutableArray<User> MentionedUsers { get; private set; } | |||||
| public override DiscordClient Discord => (Channel as Entity<ulong>).Discord; | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<Attachment> Attachments { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<Embed> Embeds { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<IUser> MentionedUsers { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<ulong> MentionedChannelIds { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<ulong> MentionedRoleIds { get; private set; } | |||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| internal DiscordClient Discord => (Channel as TextChannel)?.Discord ?? (Channel as DMChannel).Discord; | |||||
| internal Message(IMessageChannel channel, Model model) | |||||
| public Message(IMessageChannel channel, IUser author, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| Channel = channel; | Channel = channel; | ||||
| Author = new PublicUser(Discord, model.Author); | |||||
| Author = author; | |||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| private void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| var guildChannel = Channel as GuildChannel; | var guildChannel = Channel as GuildChannel; | ||||
| var guild = guildChannel?.Guild; | var guild = guildChannel?.Guild; | ||||
| var discord = Discord; | var discord = Discord; | ||||
| @@ -72,7 +57,7 @@ namespace Discord.Rest | |||||
| Attachments = ImmutableArray.Create(attachments); | Attachments = ImmutableArray.Create(attachments); | ||||
| } | } | ||||
| else | else | ||||
| Attachments = Array.Empty<Attachment>(); | |||||
| Attachments = ImmutableArray.Create<Attachment>(); | |||||
| if (model.Embeds.Length > 0) | if (model.Embeds.Length > 0) | ||||
| { | { | ||||
| @@ -82,17 +67,17 @@ namespace Discord.Rest | |||||
| Embeds = ImmutableArray.Create(embeds); | Embeds = ImmutableArray.Create(embeds); | ||||
| } | } | ||||
| else | else | ||||
| Embeds = Array.Empty<Embed>(); | |||||
| Embeds = ImmutableArray.Create<Embed>(); | |||||
| if (guildChannel != null && model.Mentions.Length > 0) | if (guildChannel != null && model.Mentions.Length > 0) | ||||
| { | { | ||||
| var mentions = new PublicUser[model.Mentions.Length]; | |||||
| var mentions = new User[model.Mentions.Length]; | |||||
| for (int i = 0; i < model.Mentions.Length; i++) | for (int i = 0; i < model.Mentions.Length; i++) | ||||
| mentions[i] = new PublicUser(discord, model.Mentions[i]); | |||||
| mentions[i] = new User(discord, model.Mentions[i]); | |||||
| MentionedUsers = ImmutableArray.Create(mentions); | MentionedUsers = ImmutableArray.Create(mentions); | ||||
| } | } | ||||
| else | else | ||||
| MentionedUsers = Array.Empty<PublicUser>(); | |||||
| MentionedUsers = ImmutableArray.Create<User>(); | |||||
| if (guildChannel != null) | if (guildChannel != null) | ||||
| { | { | ||||
| @@ -105,14 +90,20 @@ namespace Discord.Rest | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| MentionedChannelIds = Array.Empty<ulong>(); | |||||
| MentionedRoleIds = Array.Empty<ulong>(); | |||||
| MentionedChannelIds = ImmutableArray.Create<ulong>(); | |||||
| MentionedRoleIds = ImmutableArray.Create<ulong>(); | |||||
| } | } | ||||
| Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); | Text = MentionUtils.CleanUserMentions(model.Content, model.Mentions); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Update() | |||||
| { | |||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| var model = await Discord.ApiClient.GetChannelMessage(Channel.Id, Id).ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Modify(Action<ModifyMessageParams> func) | public async Task Modify(Action<ModifyMessageParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -126,10 +117,8 @@ namespace Discord.Rest | |||||
| model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); | model = await Discord.ApiClient.ModifyMessage(guildChannel.Guild.Id, Channel.Id, Id, args).ConfigureAwait(false); | ||||
| else | else | ||||
| model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); | model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| } | |||||
| /// <inheritdoc /> | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Delete() | public async Task Delete() | ||||
| { | { | ||||
| var guildChannel = Channel as GuildChannel; | var guildChannel = Channel as GuildChannel; | ||||
| @@ -140,9 +129,12 @@ namespace Discord.Rest | |||||
| } | } | ||||
| public override string ToString() => Text; | public override string ToString() => Text; | ||||
| private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Count > 0 ? $" [{Attachments.Count} Attachments]" : "")}"; | |||||
| private string DebuggerDisplay => $"{Author}: {Text}{(Attachments.Length > 0 ? $" [{Attachments.Length} Attachments]" : "")}"; | |||||
| IUser IMessage.Author => Author; | |||||
| IReadOnlyList<IUser> IMessage.MentionedUsers => MentionedUsers; | |||||
| IReadOnlyCollection<Attachment> IMessage.Attachments => Attachments; | |||||
| IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds; | |||||
| IReadOnlyCollection<ulong> IMessage.MentionedChannelIds => MentionedChannelIds; | |||||
| IReadOnlyCollection<ulong> IMessage.MentionedRoleIds => MentionedRoleIds; | |||||
| IReadOnlyCollection<IUser> IMessage.MentionedUsers => MentionedUsers; | |||||
| } | } | ||||
| } | } | ||||
| @@ -144,7 +144,7 @@ namespace Discord | |||||
| } | } | ||||
| return perms; | return perms; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override string ToString() => RawValue.ToString(); | public override string ToString() => RawValue.ToString(); | ||||
| private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | ||||
| } | } | ||||
| @@ -144,7 +144,7 @@ namespace Discord | |||||
| } | } | ||||
| return perms; | return perms; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override string ToString() => RawValue.ToString(); | public override string ToString() => RawValue.ToString(); | ||||
| private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | ||||
| } | } | ||||
| @@ -12,11 +12,14 @@ namespace Discord | |||||
| public OverwritePermissions Permissions { get; } | public OverwritePermissions Permissions { get; } | ||||
| /// <summary> Creates a new Overwrite with provided target information and modified permissions. </summary> | /// <summary> Creates a new Overwrite with provided target information and modified permissions. </summary> | ||||
| internal Overwrite(Model model) | |||||
| public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions) | |||||
| { | { | ||||
| TargetId = model.TargetId; | |||||
| TargetType = model.TargetType; | |||||
| Permissions = new OverwritePermissions(model.Allow, model.Deny); | |||||
| TargetId = targetId; | |||||
| TargetType = targetType; | |||||
| Permissions = permissions; | |||||
| } | } | ||||
| internal Overwrite(Model model) | |||||
| : this(model.TargetId, model.TargetType, new OverwritePermissions(model.Allow, model.Deny)) { } | |||||
| } | } | ||||
| } | } | ||||
| @@ -135,7 +135,7 @@ namespace Discord | |||||
| } | } | ||||
| return perms; | return perms; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; | public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; | ||||
| private string DebuggerDisplay => | private string DebuggerDisplay => | ||||
| $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + | $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + | ||||
| @@ -90,8 +90,8 @@ namespace Discord | |||||
| { | { | ||||
| var roles = user.Roles; | var roles = user.Roles; | ||||
| ulong newPermissions = 0; | ulong newPermissions = 0; | ||||
| for (int i = 0; i < roles.Count; i++) | |||||
| newPermissions |= roles[i].Permissions.RawValue; | |||||
| foreach (var role in roles) | |||||
| newPermissions |= role.Permissions.RawValue; | |||||
| return newPermissions; | return newPermissions; | ||||
| } | } | ||||
| @@ -110,25 +110,26 @@ namespace Discord | |||||
| { | { | ||||
| //Start with this user's guild permissions | //Start with this user's guild permissions | ||||
| resolvedPermissions = guildPermissions; | resolvedPermissions = guildPermissions; | ||||
| var overwrites = channel.PermissionOverwrites; | |||||
| Overwrite entry; | |||||
| OverwritePermissions? perms; | |||||
| var roles = user.Roles; | var roles = user.Roles; | ||||
| if (roles.Count > 0) | if (roles.Count > 0) | ||||
| { | { | ||||
| for (int i = 0; i < roles.Count; i++) | |||||
| ulong deniedPermissions = 0UL, allowedPermissions = 0UL; | |||||
| foreach (var role in roles) | |||||
| { | { | ||||
| if (overwrites.TryGetValue(roles[i].Id, out entry)) | |||||
| resolvedPermissions &= ~entry.Permissions.DenyValue; | |||||
| } | |||||
| for (int i = 0; i < roles.Count; i++) | |||||
| { | |||||
| if (overwrites.TryGetValue(roles[i].Id, out entry)) | |||||
| resolvedPermissions |= entry.Permissions.AllowValue; | |||||
| perms = channel.GetPermissionOverwrite(role); | |||||
| if (perms != null) | |||||
| { | |||||
| deniedPermissions |= perms.Value.DenyValue; | |||||
| allowedPermissions |= perms.Value.AllowValue; | |||||
| } | |||||
| } | } | ||||
| resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; | |||||
| } | } | ||||
| if (overwrites.TryGetValue(user.Id, out entry)) | |||||
| resolvedPermissions = (resolvedPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; | |||||
| perms = channel.GetPermissionOverwrite(user); | |||||
| if (perms != null) | |||||
| resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; | |||||
| #if CSHARP7 | #if CSHARP7 | ||||
| switch (channel) | switch (channel) | ||||
| @@ -11,7 +11,7 @@ namespace Discord | |||||
| Color Color { get; } | Color Color { get; } | ||||
| /// <summary> Returns true if users of this role are separated in the user list. </summary> | /// <summary> Returns true if users of this role are separated in the user list. </summary> | ||||
| bool IsHoisted { get; } | bool IsHoisted { get; } | ||||
| /// <summary> Returns true if this role is automatically managed by the Discord server. </summary> | |||||
| /// <summary> Returns true if this role is automatically managed by Discord. </summary> | |||||
| bool IsManaged { get; } | bool IsManaged { get; } | ||||
| /// <summary> Gets the name of this role. </summary> | /// <summary> Gets the name of this role. </summary> | ||||
| string Name { get; } | string Name { get; } | ||||
| @@ -25,8 +25,5 @@ namespace Discord | |||||
| /// <summary> Modifies this role. </summary> | /// <summary> Modifies this role. </summary> | ||||
| Task Modify(Action<ModifyGuildRoleParams> func); | Task Modify(Action<ModifyGuildRoleParams> func); | ||||
| /// <summary> Returns a collection of all users that have been assigned this role. </summary> | |||||
| Task<IEnumerable<IGuildUser>> GetUsers(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,51 +1,41 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.Role; | using Model = Discord.API.Role; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Role : IRole, IMentionable | |||||
| internal class Role : SnowflakeEntity, IRole, IMentionable | |||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| /// <summary> Returns the guild this role belongs to. </summary> | |||||
| public Guild Guild { get; } | public Guild Guild { get; } | ||||
| /// <inheritdoc /> | |||||
| public Color Color { get; private set; } | public Color Color { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsHoisted { get; private set; } | public bool IsHoisted { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsManaged { get; private set; } | public bool IsManaged { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public GuildPermissions Permissions { get; private set; } | public GuildPermissions Permissions { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public int Position { get; private set; } | public int Position { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc /> | |||||
| public bool IsEveryone => Id == Guild.Id; | public bool IsEveryone => Id == Guild.Id; | ||||
| /// <inheritdoc /> | |||||
| public string Mention => MentionUtils.Mention(this); | public string Mention => MentionUtils.Mention(this); | ||||
| internal DiscordClient Discord => Guild.Discord; | |||||
| public override DiscordClient Discord => Guild.Discord; | |||||
| internal Role(Guild guild, Model model) | |||||
| public Role(Guild guild, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| Guild = guild; | Guild = guild; | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| internal void Update(Model model) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Name = model.Name; | Name = model.Name; | ||||
| IsHoisted = model.Hoist.Value; | IsHoisted = model.Hoist.Value; | ||||
| IsManaged = model.Managed.Value; | IsManaged = model.Managed.Value; | ||||
| @@ -53,7 +43,7 @@ namespace Discord.Rest | |||||
| Color = new Color(model.Color.Value); | Color = new Color(model.Color.Value); | ||||
| Permissions = new GuildPermissions(model.Permissions.Value); | Permissions = new GuildPermissions(model.Permissions.Value); | ||||
| } | } | ||||
| /// <summary> Modifies the properties of this role. </summary> | |||||
| public async Task Modify(Action<ModifyGuildRoleParams> func) | public async Task Modify(Action<ModifyGuildRoleParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -61,23 +51,16 @@ namespace Discord.Rest | |||||
| var args = new ModifyGuildRoleParams(); | var args = new ModifyGuildRoleParams(); | ||||
| func(args); | func(args); | ||||
| var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); | var response = await Discord.ApiClient.ModifyGuildRole(Guild.Id, Id, args).ConfigureAwait(false); | ||||
| Update(response); | |||||
| Update(response, UpdateSource.Rest); | |||||
| } | } | ||||
| /// <summary> Deletes this message. </summary> | |||||
| public async Task Delete() | public async Task Delete() | ||||
| => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | |||||
| { | |||||
| await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); | |||||
| } | |||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| ulong IRole.GuildId => Guild.Id; | ulong IRole.GuildId => Guild.Id; | ||||
| async Task<IEnumerable<IGuildUser>> IRole.GetUsers() | |||||
| { | |||||
| //TODO: Rethink this, it isn't paginated or anything... | |||||
| var models = await Discord.ApiClient.GetGuildMembers(Guild.Id, new GetGuildMembersParams()).ConfigureAwait(false); | |||||
| return models.Where(x => x.Roles.Contains(Id)).Select(x => new GuildUser(Guild, x)); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,15 @@ | |||||
| using System; | |||||
| namespace Discord | |||||
| { | |||||
| internal abstract class SnowflakeEntity : Entity<ulong>, ISnowflakeEntity | |||||
| { | |||||
| //TODO: Candidate for Extension Property. Lets us remove this class. | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| public SnowflakeEntity(ulong id) | |||||
| : base(id) | |||||
| { | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| namespace Discord | |||||
| { | |||||
| internal enum UpdateSource | |||||
| { | |||||
| Creation, | |||||
| Rest, | |||||
| WebSocket | |||||
| } | |||||
| } | |||||
| @@ -5,19 +5,18 @@ using Model = Discord.API.Connection; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class Connection : IConnection | |||||
| internal class Connection : IConnection | |||||
| { | { | ||||
| public string Id { get; } | public string Id { get; } | ||||
| public string Type { get; } | public string Type { get; } | ||||
| public string Name { get; } | public string Name { get; } | ||||
| public bool IsRevoked { get; } | public bool IsRevoked { get; } | ||||
| public IEnumerable<ulong> IntegrationIds { get; } | |||||
| public IReadOnlyCollection<ulong> IntegrationIds { get; } | |||||
| public Connection(Model model) | public Connection(Model model) | ||||
| { | { | ||||
| Id = model.Id; | Id = model.Id; | ||||
| Type = model.Type; | Type = model.Type; | ||||
| Name = model.Name; | Name = model.Name; | ||||
| IsRevoked = model.Revoked; | IsRevoked = model.Revoked; | ||||
| @@ -1,4 +1,6 @@ | |||||
| namespace Discord | |||||
| using Model = Discord.API.Game; | |||||
| namespace Discord | |||||
| { | { | ||||
| public struct Game | public struct Game | ||||
| { | { | ||||
| @@ -6,17 +8,15 @@ | |||||
| public string StreamUrl { get; } | public string StreamUrl { get; } | ||||
| public StreamType StreamType { get; } | public StreamType StreamType { get; } | ||||
| public Game(string name) | |||||
| { | |||||
| Name = name; | |||||
| StreamUrl = null; | |||||
| StreamType = StreamType.NotStreaming; | |||||
| } | |||||
| public Game(string name, string streamUrl, StreamType type) | public Game(string name, string streamUrl, StreamType type) | ||||
| { | { | ||||
| Name = name; | Name = name; | ||||
| StreamUrl = streamUrl; | StreamUrl = streamUrl; | ||||
| StreamType = type; | StreamType = type; | ||||
| } | } | ||||
| public Game(string name) | |||||
| : this(name, null, StreamType.NotStreaming) { } | |||||
| internal Game(Model model) | |||||
| : this(model.Name, model.StreamUrl, model.StreamType ?? StreamType.NotStreaming) { } | |||||
| } | } | ||||
| } | } | ||||
| @@ -6,70 +6,64 @@ using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.GuildMember; | using Model = Discord.API.GuildMember; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public class GuildUser : User, IGuildUser | |||||
| internal class GuildUser : IGuildUser, ISnowflakeEntity | |||||
| { | { | ||||
| private ImmutableArray<Role> _roles; | |||||
| public Guild Guild { get; } | |||||
| /// <inheritdoc /> | |||||
| public bool IsDeaf { get; private set; } | public bool IsDeaf { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsMute { get; private set; } | public bool IsMute { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public DateTime JoinedAt { get; private set; } | public DateTime JoinedAt { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string Nickname { get; private set; } | public string Nickname { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public GuildPermissions GuildPermissions { get; private set; } | public GuildPermissions GuildPermissions { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public IReadOnlyList<Role> Roles => _roles; | |||||
| internal override DiscordClient Discord => Guild.Discord; | |||||
| public Guild Guild { get; private set; } | |||||
| public User User { get; private set; } | |||||
| public ImmutableArray<Role> Roles { get; private set; } | |||||
| public ulong Id => User.Id; | |||||
| public string AvatarUrl => User.AvatarUrl; | |||||
| public DateTime CreatedAt => User.CreatedAt; | |||||
| public ushort Discriminator => User.Discriminator; | |||||
| public Game? Game => User.Game; | |||||
| public bool IsAttached => User.IsAttached; | |||||
| public bool IsBot => User.IsBot; | |||||
| public string Mention => User.Mention; | |||||
| public UserStatus Status => User.Status; | |||||
| public string Username => User.Username; | |||||
| internal GuildUser(Guild guild, Model model) | |||||
| : base(model.User) | |||||
| public DiscordClient Discord => Guild.Discord; | |||||
| public GuildUser(Guild guild, User user, Model model) | |||||
| { | { | ||||
| Guild = guild; | Guild = guild; | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| internal void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| IsDeaf = model.Deaf; | IsDeaf = model.Deaf; | ||||
| IsMute = model.Mute; | IsMute = model.Mute; | ||||
| JoinedAt = model.JoinedAt.Value; | JoinedAt = model.JoinedAt.Value; | ||||
| Nickname = model.Nick; | Nickname = model.Nick; | ||||
| var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1); | var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1); | ||||
| roles.Add(Guild.EveryoneRole); | |||||
| roles.Add(Guild.EveryoneRole as Role); | |||||
| for (int i = 0; i < model.Roles.Length; i++) | for (int i = 0; i < model.Roles.Length; i++) | ||||
| roles.Add(Guild.GetRole(model.Roles[i])); | |||||
| _roles = roles.ToImmutable(); | |||||
| roles.Add(Guild.GetRole(model.Roles[i]) as Role); | |||||
| Roles = roles.ToImmutable(); | |||||
| GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); | GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); | ||||
| } | } | ||||
| public async Task Update() | public async Task Update() | ||||
| { | { | ||||
| var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); | |||||
| Update(model); | |||||
| } | |||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| public async Task Kick() | |||||
| { | |||||
| await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); | |||||
| } | |||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) | |||||
| { | |||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||||
| return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); | |||||
| var model = await Discord.ApiClient.GetGuildMember(Guild.Id, Id).ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| public async Task Modify(Action<ModifyGuildMemberParams> func) | public async Task Modify(Action<ModifyGuildMemberParams> func) | ||||
| { | { | ||||
| if (func == null) throw new NullReferenceException(nameof(func)); | if (func == null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -82,7 +76,7 @@ namespace Discord.Rest | |||||
| { | { | ||||
| var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" }; | var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" }; | ||||
| await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); | await Discord.ApiClient.ModifyCurrentUserNick(Guild.Id, nickArgs).ConfigureAwait(false); | ||||
| args.Nickname = new API.Optional<string>(); //Remove | |||||
| args.Nickname = new Optional<string>(); //Remove | |||||
| } | } | ||||
| if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) | if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.Roles.IsSpecified) | ||||
| @@ -95,18 +89,24 @@ namespace Discord.Rest | |||||
| if (args.Nickname.IsSpecified) | if (args.Nickname.IsSpecified) | ||||
| Nickname = args.Nickname.Value ?? ""; | Nickname = args.Nickname.Value ?? ""; | ||||
| if (args.Roles.IsSpecified) | if (args.Roles.IsSpecified) | ||||
| _roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); | |||||
| Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray(); | |||||
| } | } | ||||
| } | } | ||||
| public async Task Kick() | |||||
| { | |||||
| await Discord.ApiClient.RemoveGuildMember(Guild.Id, Id).ConfigureAwait(false); | |||||
| } | |||||
| public ChannelPermissions GetPermissions(IGuildChannel channel) | |||||
| { | |||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | |||||
| return new ChannelPermissions(Permissions.ResolveChannel(this, channel, GuildPermissions.RawValue)); | |||||
| } | |||||
| public Task<IDMChannel> CreateDMChannel() => User.CreateDMChannel(); | |||||
| IGuild IGuildUser.Guild => Guild; | IGuild IGuildUser.Guild => Guild; | ||||
| IReadOnlyList<IRole> IGuildUser.Roles => Roles; | |||||
| IReadOnlyCollection<IRole> IGuildUser.Roles => Roles; | |||||
| IVoiceChannel IGuildUser.VoiceChannel => null; | IVoiceChannel IGuildUser.VoiceChannel => null; | ||||
| GuildPermissions IGuildUser.GetGuildPermissions() | |||||
| => GuildPermissions; | |||||
| ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) | |||||
| => GetPermissions(channel); | |||||
| } | } | ||||
| } | } | ||||
| @@ -9,6 +9,6 @@ namespace Discord | |||||
| string Name { get; } | string Name { get; } | ||||
| bool IsRevoked { get; } | bool IsRevoked { get; } | ||||
| IEnumerable<ulong> IntegrationIds { get; } | |||||
| IReadOnlyCollection<ulong> IntegrationIds { get; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -16,16 +16,16 @@ namespace Discord | |||||
| DateTime JoinedAt { get; } | DateTime JoinedAt { get; } | ||||
| /// <summary> Gets the nickname for this user. </summary> | /// <summary> Gets the nickname for this user. </summary> | ||||
| string Nickname { get; } | string Nickname { get; } | ||||
| /// <summary> Gets the guild-level permissions granted to this user by their roles. </summary> | |||||
| GuildPermissions GuildPermissions { get; } | |||||
| /// <summary> Gets the guild for this guild-user pair. </summary> | /// <summary> Gets the guild for this guild-user pair. </summary> | ||||
| IGuild Guild { get; } | IGuild Guild { get; } | ||||
| /// <summary> Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. </summary> | /// <summary> Returns a collection of the roles this user is a member of in this guild, including the guild's @everyone role. </summary> | ||||
| IReadOnlyList<IRole> Roles { get; } | |||||
| IReadOnlyCollection<IRole> Roles { get; } | |||||
| /// <summary> Gets the voice channel this user is currently in, if any. </summary> | /// <summary> Gets the voice channel this user is currently in, if any. </summary> | ||||
| IVoiceChannel VoiceChannel { get; } | IVoiceChannel VoiceChannel { get; } | ||||
| /// <summary> Gets the guild-level permissions granted to this user by their roles. </summary> | |||||
| GuildPermissions GetGuildPermissions(); | |||||
| /// <summary> Gets the channel-level permissions granted to this user for a given channel. </summary> | /// <summary> Gets the channel-level permissions granted to this user for a given channel. </summary> | ||||
| ChannelPermissions GetPermissions(IGuildChannel channel); | ChannelPermissions GetPermissions(IGuildChannel channel); | ||||
| @@ -34,4 +34,4 @@ namespace Discord | |||||
| /// <summary> Modifies this user's properties in this guild. </summary> | /// <summary> Modifies this user's properties in this guild. </summary> | ||||
| Task Modify(Action<ModifyGuildMemberParams> func); | Task Modify(Action<ModifyGuildMemberParams> func); | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public interface IPresence | |||||
| { | |||||
| /// <summary> Gets the game this user is currently playing, if any. </summary> | |||||
| Game? Game { get; } | |||||
| /// <summary> Gets the current status of this user. </summary> | |||||
| UserStatus Status { get; } | |||||
| } | |||||
| } | |||||
| @@ -2,18 +2,14 @@ using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public interface IUser : ISnowflakeEntity, IMentionable | |||||
| public interface IUser : ISnowflakeEntity, IMentionable, IPresence | |||||
| { | { | ||||
| /// <summary> Gets the url to this user's avatar. </summary> | /// <summary> Gets the url to this user's avatar. </summary> | ||||
| string AvatarUrl { get; } | string AvatarUrl { get; } | ||||
| /// <summary> Gets the game this user is currently playing, if any. </summary> | |||||
| Game? CurrentGame { get; } | |||||
| /// <summary> Gets the per-username unique id for this user. </summary> | /// <summary> Gets the per-username unique id for this user. </summary> | ||||
| ushort Discriminator { get; } | ushort Discriminator { get; } | ||||
| /// <summary> Returns true if this user is a bot account. </summary> | /// <summary> Returns true if this user is a bot account. </summary> | ||||
| bool IsBot { get; } | bool IsBot { get; } | ||||
| /// <summary> Gets the current status of this user. </summary> | |||||
| UserStatus Status { get; } | |||||
| /// <summary> Gets the username for this user. </summary> | /// <summary> Gets the username for this user. </summary> | ||||
| string Username { get; } | string Username { get; } | ||||
| @@ -3,38 +3,34 @@ using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| public class SelfUser : User, ISelfUser | |||||
| { | |||||
| internal override DiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | |||||
| internal class SelfUser : User, ISelfUser | |||||
| { | |||||
| public string Email { get; private set; } | public string Email { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsVerified { get; private set; } | public bool IsVerified { get; private set; } | ||||
| internal SelfUser(DiscordClient discord, Model model) | |||||
| : base(model) | |||||
| public SelfUser(DiscordClient discord, Model model) | |||||
| : base(discord, model) | |||||
| { | { | ||||
| Discord = discord; | |||||
| } | } | ||||
| internal override void Update(Model model) | |||||
| public override void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| base.Update(model); | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| base.Update(model, source); | |||||
| Email = model.Email; | Email = model.Email; | ||||
| IsVerified = model.IsVerified; | IsVerified = model.IsVerified; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Update() | public async Task Update() | ||||
| { | { | ||||
| var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); | |||||
| Update(model); | |||||
| } | |||||
| if (IsAttached) throw new NotSupportedException(); | |||||
| /// <inheritdoc /> | |||||
| var model = await Discord.ApiClient.GetCurrentUser().ConfigureAwait(false); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | |||||
| public async Task Modify(Action<ModifyCurrentUserParams> func) | public async Task Modify(Action<ModifyCurrentUserParams> func) | ||||
| { | { | ||||
| if (func != null) throw new NullReferenceException(nameof(func)); | if (func != null) throw new NullReferenceException(nameof(func)); | ||||
| @@ -42,7 +38,7 @@ namespace Discord.Rest | |||||
| var args = new ModifyCurrentUserParams(); | var args = new ModifyCurrentUserParams(); | ||||
| func(args); | func(args); | ||||
| var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); | var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); | ||||
| Update(model); | |||||
| Update(model, UpdateSource.Rest); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,68 +1,52 @@ | |||||
| using Discord.API.Rest; | using Discord.API.Rest; | ||||
| using System; | |||||
| using System.Diagnostics; | using System.Diagnostics; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using Model = Discord.API.User; | using Model = Discord.API.User; | ||||
| namespace Discord.Rest | |||||
| namespace Discord | |||||
| { | { | ||||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||||
| public abstract class User : IUser | |||||
| internal class User : SnowflakeEntity, IUser | |||||
| { | { | ||||
| private string _avatarId; | private string _avatarId; | ||||
| /// <inheritdoc /> | |||||
| public ulong Id { get; } | |||||
| internal abstract DiscordClient Discord { get; } | |||||
| /// <inheritdoc /> | |||||
| public ushort Discriminator { get; private set; } | public ushort Discriminator { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public bool IsBot { get; private set; } | public bool IsBot { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public string Username { get; private set; } | public string Username { get; private set; } | ||||
| /// <inheritdoc /> | |||||
| public override DiscordClient Discord { get; } | |||||
| public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); | public string AvatarUrl => API.CDN.GetUserAvatarUrl(Id, _avatarId); | ||||
| /// <inheritdoc /> | |||||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||||
| /// <inheritdoc /> | |||||
| public virtual Game? Game => null; | |||||
| public string Mention => MentionUtils.Mention(this, false); | public string Mention => MentionUtils.Mention(this, false); | ||||
| /// <inheritdoc /> | |||||
| public string NicknameMention => MentionUtils.Mention(this, true); | public string NicknameMention => MentionUtils.Mention(this, true); | ||||
| public virtual UserStatus Status => UserStatus.Unknown; | |||||
| internal User(Model model) | |||||
| public User(DiscordClient discord, Model model) | |||||
| : base(model.Id) | |||||
| { | { | ||||
| Id = model.Id; | |||||
| Update(model); | |||||
| Discord = discord; | |||||
| Update(model, UpdateSource.Creation); | |||||
| } | } | ||||
| internal virtual void Update(Model model) | |||||
| public virtual void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| _avatarId = model.Avatar; | _avatarId = model.Avatar; | ||||
| Discriminator = model.Discriminator; | Discriminator = model.Discriminator; | ||||
| IsBot = model.Bot; | IsBot = model.Bot; | ||||
| Username = model.Username; | Username = model.Username; | ||||
| } | } | ||||
| protected virtual async Task<DMChannel> CreateDMChannelInternal() | |||||
| public async Task<IDMChannel> CreateDMChannel() | |||||
| { | { | ||||
| var args = new CreateDMChannelParams { RecipientId = Id }; | var args = new CreateDMChannelParams { RecipientId = Id }; | ||||
| var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); | var model = await Discord.ApiClient.CreateDMChannel(args).ConfigureAwait(false); | ||||
| return new DMChannel(Discord, model); | |||||
| return new DMChannel(Discord, this, model); | |||||
| } | } | ||||
| public override string ToString() => $"{Username}#{Discriminator}"; | public override string ToString() => $"{Username}#{Discriminator}"; | ||||
| private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id})"; | ||||
| /// <inheritdoc /> | |||||
| Game? IUser.CurrentGame => null; | |||||
| /// <inheritdoc /> | |||||
| UserStatus IUser.Status => UserStatus.Unknown; | |||||
| /// <inheritdoc /> | |||||
| async Task<IDMChannel> IUser.CreateDMChannel() | |||||
| => await CreateDMChannelInternal().ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,70 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using MessageModel = Discord.API.Message; | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel | |||||
| { | |||||
| private readonly MessageCache _messages; | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; | |||||
| public IReadOnlyCollection<IUser> Members => ImmutableArray.Create<IUser>(Discord.CurrentUser, Recipient); | |||||
| public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) | |||||
| : base(discord, recipient, model) | |||||
| { | |||||
| _messages = new MessageCache(Discord, this); | |||||
| } | |||||
| public override Task<IUser> GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); | |||||
| public override Task<IReadOnlyCollection<IUser>> GetUsers() => Task.FromResult(Members); | |||||
| public override Task<IReadOnlyCollection<IUser>> GetUsers(int limit, int offset) | |||||
| => Task.FromResult<IReadOnlyCollection<IUser>>(Members.Skip(offset).Take(limit).ToImmutableArray()); | |||||
| public IUser GetCachedUser(ulong id) | |||||
| { | |||||
| var currentUser = Discord.CurrentUser; | |||||
| if (id == Recipient.Id) | |||||
| return Recipient; | |||||
| else if (id == currentUser.Id) | |||||
| return currentUser; | |||||
| else | |||||
| return null; | |||||
| } | |||||
| public override async Task<IMessage> GetMessage(ulong id) | |||||
| { | |||||
| return await _messages.Download(id).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit) | |||||
| { | |||||
| return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit) | |||||
| { | |||||
| return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); | |||||
| } | |||||
| public CachedMessage AddCachedMessage(IUser author, MessageModel model) | |||||
| { | |||||
| var msg = new CachedMessage(this, author, model); | |||||
| _messages.Add(msg); | |||||
| return msg; | |||||
| } | |||||
| public CachedMessage GetCachedMessage(ulong id) | |||||
| { | |||||
| return _messages.Get(id); | |||||
| } | |||||
| public CachedMessage RemoveCachedMessage(ulong id) | |||||
| { | |||||
| return _messages.Remove(id); | |||||
| } | |||||
| public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,171 @@ | |||||
| using Discord.Data; | |||||
| using Discord.Extensions; | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using ChannelModel = Discord.API.Channel; | |||||
| using ExtendedModel = Discord.API.Gateway.ExtendedGuild; | |||||
| using MemberModel = Discord.API.GuildMember; | |||||
| using Model = Discord.API.Guild; | |||||
| using PresenceModel = Discord.API.Presence; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedGuild : Guild, ICachedEntity<ulong> | |||||
| { | |||||
| private ConcurrentHashSet<ulong> _channels; | |||||
| private ConcurrentDictionary<ulong, CachedGuildUser> _members; | |||||
| private ConcurrentDictionary<ulong, Presence> _presences; | |||||
| private int _userCount; | |||||
| public bool Available { get; private set; } //TODO: Add to IGuild | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public CachedGuildUser CurrentUser => GetCachedUser(Discord.CurrentUser.Id); | |||||
| public IReadOnlyCollection<ICachedGuildChannel> Channels => _channels.Select(x => GetCachedChannel(x)).ToReadOnlyCollection(_channels); | |||||
| public IReadOnlyCollection<CachedGuildUser> Members => _members.ToReadOnlyCollection(); | |||||
| public CachedGuild(DiscordSocketClient discord, Model model) : base(discord, model) | |||||
| { | |||||
| } | |||||
| public void Update(ExtendedModel model, UpdateSource source, DataStore dataStore) | |||||
| { | |||||
| if (source == UpdateSource.Rest && IsAttached) return; | |||||
| Available = !(model.Unavailable ?? false); | |||||
| if (!Available) | |||||
| { | |||||
| if (_channels == null) | |||||
| _channels = new ConcurrentHashSet<ulong>(); | |||||
| if (_members == null) | |||||
| _members = new ConcurrentDictionary<ulong, CachedGuildUser>(); | |||||
| if (_presences == null) | |||||
| _presences = new ConcurrentDictionary<ulong, Presence>(); | |||||
| if (_roles == null) | |||||
| _roles = new ConcurrentDictionary<ulong, Role>(); | |||||
| if (Emojis == null) | |||||
| Emojis = ImmutableArray.Create<Emoji>(); | |||||
| if (Features == null) | |||||
| Features = ImmutableArray.Create<string>(); | |||||
| return; | |||||
| } | |||||
| base.Update(model as Model, source); | |||||
| _userCount = model.MemberCount; | |||||
| var channels = new ConcurrentHashSet<ulong>(); | |||||
| if (model.Channels != null) | |||||
| { | |||||
| for (int i = 0; i < model.Channels.Length; i++) | |||||
| AddCachedChannel(model.Channels[i], channels, dataStore); | |||||
| } | |||||
| _channels = channels; | |||||
| var presences = new ConcurrentDictionary<ulong, Presence>(); | |||||
| if (model.Presences != null) | |||||
| { | |||||
| for (int i = 0; i < model.Presences.Length; i++) | |||||
| AddCachedPresence(model.Presences[i], presences); | |||||
| } | |||||
| _presences = presences; | |||||
| var members = new ConcurrentDictionary<ulong, CachedGuildUser>(); | |||||
| if (model.Members != null) | |||||
| { | |||||
| for (int i = 0; i < model.Members.Length; i++) | |||||
| AddCachedUser(model.Members[i], members, dataStore); | |||||
| } | |||||
| _members = members; | |||||
| } | |||||
| public override Task<IGuildChannel> GetChannel(ulong id) => Task.FromResult<IGuildChannel>(GetCachedChannel(id)); | |||||
| public override Task<IReadOnlyCollection<IGuildChannel>> GetChannels() => Task.FromResult<IReadOnlyCollection<IGuildChannel>>(Channels); | |||||
| public ICachedGuildChannel AddCachedChannel(ChannelModel model, ConcurrentHashSet<ulong> channels = null, DataStore dataStore = null) | |||||
| { | |||||
| var channel = ToChannel(model); | |||||
| (dataStore ?? Discord.DataStore).AddChannel(channel); | |||||
| (channels ?? _channels).TryAdd(model.Id); | |||||
| return channel; | |||||
| } | |||||
| public ICachedGuildChannel GetCachedChannel(ulong id) | |||||
| { | |||||
| return Discord.DataStore.GetChannel(id) as ICachedGuildChannel; | |||||
| } | |||||
| public ICachedGuildChannel RemoveCachedChannel(ulong id, ConcurrentHashSet<ulong> channels = null, DataStore dataStore = null) | |||||
| { | |||||
| (channels ?? _channels).TryRemove(id); | |||||
| return (dataStore ?? Discord.DataStore).RemoveChannel(id) as ICachedGuildChannel; | |||||
| } | |||||
| public Presence AddCachedPresence(PresenceModel model, ConcurrentDictionary<ulong, Presence> presences = null) | |||||
| { | |||||
| var game = model.Game != null ? new Game(model.Game) : (Game?)null; | |||||
| var presence = new Presence(model.Status, game); | |||||
| (presences ?? _presences)[model.User.Id] = presence; | |||||
| return presence; | |||||
| } | |||||
| public Presence? GetCachedPresence(ulong id) | |||||
| { | |||||
| Presence presence; | |||||
| if (_presences.TryGetValue(id, out presence)) | |||||
| return presence; | |||||
| return null; | |||||
| } | |||||
| public Presence? RemoveCachedPresence(ulong id) | |||||
| { | |||||
| Presence presence; | |||||
| if (_presences.TryRemove(id, out presence)) | |||||
| return presence; | |||||
| return null; | |||||
| } | |||||
| public override Task<IGuildUser> GetUser(ulong id) => Task.FromResult<IGuildUser>(GetCachedUser(id)); | |||||
| public override Task<IGuildUser> GetCurrentUser() | |||||
| => Task.FromResult<IGuildUser>(CurrentUser); | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers() | |||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members); | |||||
| //TODO: Is there a better way of exposing pagination? | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); | |||||
| public CachedGuildUser AddCachedUser(MemberModel model, ConcurrentDictionary<ulong, CachedGuildUser> members = null, DataStore dataStore = null) | |||||
| { | |||||
| var user = Discord.AddCachedUser(model.User); | |||||
| var member = new CachedGuildUser(this, user, model); | |||||
| (members ?? _members)[user.Id] = member; | |||||
| user.AddRef(); | |||||
| return member; | |||||
| } | |||||
| public CachedGuildUser GetCachedUser(ulong id) | |||||
| { | |||||
| CachedGuildUser member; | |||||
| if (_members.TryGetValue(id, out member)) | |||||
| return member; | |||||
| return null; | |||||
| } | |||||
| public CachedGuildUser RemoveCachedUser(ulong id) | |||||
| { | |||||
| CachedGuildUser member; | |||||
| if (_members.TryRemove(id, out member)) | |||||
| return member; | |||||
| return null; | |||||
| } | |||||
| new internal ICachedGuildChannel ToChannel(ChannelModel model) | |||||
| { | |||||
| switch (model.Type) | |||||
| { | |||||
| case ChannelType.Text: | |||||
| return new CachedTextChannel(this, model); | |||||
| case ChannelType.Voice: | |||||
| return new CachedVoiceChannel(this, model); | |||||
| default: | |||||
| throw new InvalidOperationException($"Unknown channel type: {model.Type}"); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using Model = Discord.API.GuildMember; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedGuildUser : GuildUser, ICachedEntity<ulong> | |||||
| { | |||||
| public VoiceChannel VoiceChannel { get; private set; } | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) | |||||
| : base(guild, user, model) | |||||
| { | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| using Model = Discord.API.Message; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedMessage : Message, ICachedEntity<ulong> | |||||
| { | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public new ICachedMessageChannel Channel => base.Channel as ICachedMessageChannel; | |||||
| public CachedMessage(ICachedMessageChannel channel, IUser author, Model model) | |||||
| : base(channel, author, model) | |||||
| { | |||||
| } | |||||
| public CachedMessage Clone() => MemberwiseClone() as CachedMessage; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,58 @@ | |||||
| using ChannelModel = Discord.API.Channel; | |||||
| using Model = Discord.API.User; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedPublicUser : User, ICachedEntity<ulong> | |||||
| { | |||||
| private int _references; | |||||
| public CachedDMChannel DMChannel { get; private set; } | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public CachedPublicUser(DiscordSocketClient discord, Model model) | |||||
| : base(discord, model) | |||||
| { | |||||
| } | |||||
| public CachedDMChannel SetDMChannel(ChannelModel model) | |||||
| { | |||||
| lock (this) | |||||
| { | |||||
| var channel = new CachedDMChannel(Discord, this, model); | |||||
| DMChannel = channel; | |||||
| return channel; | |||||
| } | |||||
| } | |||||
| public CachedDMChannel RemoveDMChannel(ulong id) | |||||
| { | |||||
| lock (this) | |||||
| { | |||||
| var channel = DMChannel; | |||||
| if (channel.Id == id) | |||||
| { | |||||
| DMChannel = null; | |||||
| return channel; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| } | |||||
| public void AddRef() | |||||
| { | |||||
| lock (this) | |||||
| _references++; | |||||
| } | |||||
| public void RemoveRef() | |||||
| { | |||||
| lock (this) | |||||
| { | |||||
| if (--_references == 0 && DMChannel == null) | |||||
| Discord.RemoveCachedUser(Id); | |||||
| } | |||||
| } | |||||
| public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using Model = Discord.API.User; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedSelfUser : SelfUser, ICachedEntity<ulong> | |||||
| { | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public CachedSelfUser(DiscordSocketClient discord, Model model) | |||||
| : base(discord, model) | |||||
| { | |||||
| } | |||||
| public CachedSelfUser Clone() => MemberwiseClone() as CachedSelfUser; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,73 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using MessageModel = Discord.API.Message; | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel | |||||
| { | |||||
| private readonly MessageCache _messages; | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public new CachedGuild Guild => base.Guild as CachedGuild; | |||||
| public IReadOnlyCollection<IGuildUser> Members | |||||
| => Guild.Members.Where(x => Permissions.GetValue(Permissions.ResolveChannel(x, this, x.GuildPermissions.RawValue), ChannelPermission.ReadMessages)).ToImmutableArray(); | |||||
| public CachedTextChannel(CachedGuild guild, Model model) | |||||
| : base(guild, model) | |||||
| { | |||||
| _messages = new MessageCache(Discord, this); | |||||
| } | |||||
| public override Task<IGuildUser> GetUser(ulong id) => Task.FromResult(GetCachedUser(id)); | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers() => Task.FromResult(Members); | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.Skip(offset).Take(limit).ToImmutableArray()); | |||||
| public IGuildUser GetCachedUser(ulong id) | |||||
| { | |||||
| var user = Guild.GetCachedUser(id); | |||||
| if (user != null && Permissions.GetValue(Permissions.ResolveChannel(user, this, user.GuildPermissions.RawValue), ChannelPermission.ReadMessages)) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| public override async Task<IMessage> GetMessage(ulong id) | |||||
| { | |||||
| return await _messages.Download(id).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessages(int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| { | |||||
| return await _messages.Download(null, Direction.Before, limit).ConfigureAwait(false); | |||||
| } | |||||
| public override async Task<IReadOnlyCollection<IMessage>> GetMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) | |||||
| { | |||||
| return await _messages.Download(fromMessageId, dir, limit).ConfigureAwait(false); | |||||
| } | |||||
| public CachedMessage AddCachedMessage(IUser author, MessageModel model) | |||||
| { | |||||
| var msg = new CachedMessage(this, author, model); | |||||
| _messages.Add(msg); | |||||
| return msg; | |||||
| } | |||||
| public CachedMessage GetCachedMessage(ulong id) | |||||
| { | |||||
| return _messages.Get(id); | |||||
| } | |||||
| public CachedMessage RemoveCachedMessage(ulong id) | |||||
| { | |||||
| return _messages.Remove(id); | |||||
| } | |||||
| public CachedTextChannel Clone() => MemberwiseClone() as CachedTextChannel; | |||||
| IReadOnlyCollection<IUser> ICachedMessageChannel.Members => Members; | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | |||||
| IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,38 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | |||||
| internal class CachedVoiceChannel : VoiceChannel, ICachedGuildChannel | |||||
| { | |||||
| public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; | |||||
| public new CachedGuild Guild => base.Guild as CachedGuild; | |||||
| public IReadOnlyCollection<IGuildUser> Members | |||||
| => Guild.Members.Where(x => x.VoiceChannel.Id == Id).ToImmutableArray(); | |||||
| public CachedVoiceChannel(CachedGuild guild, Model model) | |||||
| : base(guild, model) | |||||
| { | |||||
| } | |||||
| public override Task<IGuildUser> GetUser(ulong id) | |||||
| => Task.FromResult(GetCachedUser(id)); | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers() | |||||
| => Task.FromResult(Members); | |||||
| public override Task<IReadOnlyCollection<IGuildUser>> GetUsers(int limit, int offset) | |||||
| => Task.FromResult<IReadOnlyCollection<IGuildUser>>(Members.OrderBy(x => x.Id).Skip(offset).Take(limit).ToImmutableArray()); | |||||
| public IGuildUser GetCachedUser(ulong id) | |||||
| { | |||||
| var user = Guild.GetCachedUser(id); | |||||
| if (user != null && user.VoiceChannel.Id == Id) | |||||
| return user; | |||||
| return null; | |||||
| } | |||||
| public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| namespace Discord | |||||
| { | |||||
| internal interface ICachedChannel : IChannel, ICachedEntity<ulong> | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| namespace Discord | |||||
| { | |||||
| interface ICachedEntity<T> : IEntity<T> | |||||
| { | |||||
| DiscordSocketClient Discord { get; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| namespace Discord | |||||
| { | |||||
| internal interface ICachedGuildChannel : ICachedChannel, IGuildChannel | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System.Collections.Generic; | |||||
| using MessageModel = Discord.API.Message; | |||||
| namespace Discord | |||||
| { | |||||
| internal interface ICachedMessageChannel : ICachedChannel, IMessageChannel | |||||
| { | |||||
| IReadOnlyCollection<IUser> Members { get; } | |||||
| CachedMessage AddCachedMessage(IUser author, MessageModel model); | |||||
| new CachedMessage GetCachedMessage(ulong id); | |||||
| CachedMessage RemoveCachedMessage(ulong id); | |||||
| IUser GetCachedUser(ulong id); | |||||
| } | |||||
| } | |||||
| @@ -3,7 +3,7 @@ using Model = Discord.API.MemberVoiceState; | |||||
| namespace Discord.WebSocket | namespace Discord.WebSocket | ||||
| { | { | ||||
| public class VoiceState | |||||
| internal class VoiceState : IVoiceState | |||||
| { | { | ||||
| [Flags] | [Flags] | ||||
| private enum VoiceStates : byte | private enum VoiceStates : byte | ||||
| @@ -22,7 +22,7 @@ namespace Discord.WebSocket | |||||
| public ulong UserId { get; } | public ulong UserId { get; } | ||||
| /// <summary> Gets this user's current voice channel. </summary> | /// <summary> Gets this user's current voice channel. </summary> | ||||
| public VoiceChannel VoiceChannel { get; internal set; } | |||||
| public VoiceChannel VoiceChannel { get; set; } | |||||
| /// <summary> Returns true if this user has marked themselves as muted. </summary> | /// <summary> Returns true if this user has marked themselves as muted. </summary> | ||||
| public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; | public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; | ||||
| @@ -35,13 +35,13 @@ namespace Discord.WebSocket | |||||
| /// <summary> Returns true if the guild is temporarily blocking audio to/from this user. </summary> | /// <summary> Returns true if the guild is temporarily blocking audio to/from this user. </summary> | ||||
| public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; | public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; | ||||
| internal VoiceState(ulong userId, Guild guild) | |||||
| public VoiceState(ulong userId, Guild guild) | |||||
| { | { | ||||
| UserId = userId; | UserId = userId; | ||||
| Guild = guild; | Guild = guild; | ||||
| } | } | ||||
| internal void Update(Model model) | |||||
| private void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (model.IsMuted == true) | if (model.IsMuted == true) | ||||
| _voiceStates |= VoiceStates.Muted; | _voiceStates |= VoiceStates.Muted; | ||||
| @@ -0,0 +1,14 @@ | |||||
| namespace Discord | |||||
| { | |||||
| internal struct Presence : IPresence | |||||
| { | |||||
| public UserStatus Status { get; } | |||||
| public Game? Game { get; } | |||||
| public Presence(UserStatus status, Game? game) | |||||
| { | |||||
| Status = status; | |||||
| Game = game; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| using System.Collections; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| namespace Discord.Extensions | |||||
| { | |||||
| internal static class CollectionExtensions | |||||
| { | |||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source) | |||||
| => new ConcurrentDictionaryWrapper<TValue, KeyValuePair<TKey, TValue>>(source, source.Select(x => x.Value)); | |||||
| public static IReadOnlyCollection<TValue> ToReadOnlyCollection<TValue, TSource>(this IEnumerable<TValue> query, IReadOnlyCollection<TSource> source) | |||||
| => new ConcurrentDictionaryWrapper<TValue, TSource>(source, query); | |||||
| } | |||||
| internal struct ConcurrentDictionaryWrapper<TValue, TSource> : IReadOnlyCollection<TValue> | |||||
| { | |||||
| private readonly IReadOnlyCollection<TSource> _source; | |||||
| private readonly IEnumerable<TValue> _query; | |||||
| public int Count => _source.Count; | |||||
| public ConcurrentDictionaryWrapper(IReadOnlyCollection<TSource> source, IEnumerable<TValue> query) | |||||
| { | |||||
| _source = source; | |||||
| _query = query; | |||||
| } | |||||
| public IEnumerator<TValue> GetEnumerator() => _query.GetEnumerator(); | |||||
| IEnumerator IEnumerable.GetEnumerator() => _query.GetEnumerator(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Extensions | |||||
| { | |||||
| public static class DiscordClientExtensions | |||||
| { | |||||
| public static async Task<IVoiceRegion> GetOptimalVoiceRegion(this DiscordClient discord) | |||||
| { | |||||
| var regions = await discord.GetVoiceRegions().ConfigureAwait(false); | |||||
| return regions.FirstOrDefault(x => x.IsOptimal); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,7 +1,7 @@ | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | |||||
| namespace Discord.Extensions | |||||
| { | { | ||||
| internal static class EventExtensions | internal static class EventExtensions | ||||
| { | { | ||||
| @@ -0,0 +1,12 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Extensions | |||||
| { | |||||
| public static class GuildExtensions | |||||
| { | |||||
| public static async Task<ITextChannel> GetTextChannel(this IGuild guild, ulong id) | |||||
| => await guild.GetChannel(id).ConfigureAwait(false) as ITextChannel; | |||||
| public static async Task<IVoiceChannel> GetVoiceChannel(this IGuild guild, ulong id) | |||||
| => await guild.GetChannel(id).ConfigureAwait(false) as IVoiceChannel; | |||||
| } | |||||
| } | |||||
| @@ -1,6 +1,4 @@ | |||||
| using Discord.API; | using Discord.API; | ||||
| using Discord.Net.Queue; | |||||
| using Discord.WebSocket.Data; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | using System.IO; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -14,10 +12,7 @@ namespace Discord | |||||
| ConnectionState ConnectionState { get; } | ConnectionState ConnectionState { get; } | ||||
| DiscordApiClient ApiClient { get; } | DiscordApiClient ApiClient { get; } | ||||
| IRequestQueue RequestQueue { get; } | |||||
| IDataStore DataStore { get; } | |||||
| Task Login(string email, string password); | |||||
| Task Login(TokenType tokenType, string token, bool validateToken = true); | Task Login(TokenType tokenType, string token, bool validateToken = true); | ||||
| Task Logout(); | Task Logout(); | ||||
| @@ -25,12 +20,12 @@ namespace Discord | |||||
| Task Disconnect(); | Task Disconnect(); | ||||
| Task<IChannel> GetChannel(ulong id); | Task<IChannel> GetChannel(ulong id); | ||||
| Task<IEnumerable<IDMChannel>> GetDMChannels(); | |||||
| Task<IReadOnlyCollection<IDMChannel>> GetDMChannels(); | |||||
| Task<IEnumerable<IConnection>> GetConnections(); | |||||
| Task<IReadOnlyCollection<IConnection>> GetConnections(); | |||||
| Task<IGuild> GetGuild(ulong id); | Task<IGuild> GetGuild(ulong id); | ||||
| Task<IEnumerable<IUserGuild>> GetGuilds(); | |||||
| Task<IReadOnlyCollection<IUserGuild>> GetGuilds(); | |||||
| Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); | Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); | ||||
| Task<IInvite> GetInvite(string inviteIdOrXkcd); | Task<IInvite> GetInvite(string inviteIdOrXkcd); | ||||
| @@ -38,9 +33,9 @@ namespace Discord | |||||
| Task<IUser> GetUser(ulong id); | Task<IUser> GetUser(ulong id); | ||||
| Task<IUser> GetUser(string username, ushort discriminator); | Task<IUser> GetUser(string username, ushort discriminator); | ||||
| Task<ISelfUser> GetCurrentUser(); | Task<ISelfUser> GetCurrentUser(); | ||||
| Task<IEnumerable<IUser>> QueryUsers(string query, int limit); | |||||
| Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit); | |||||
| Task<IEnumerable<IVoiceRegion>> GetVoiceRegions(); | |||||
| Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions(); | |||||
| Task<IVoiceRegion> GetVoiceRegion(string id); | Task<IVoiceRegion> GetVoiceRegion(string id); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | |||||
| using Discord.Extensions; | |||||
| using System; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Logging | namespace Discord.Logging | ||||
| @@ -9,7 +10,7 @@ namespace Discord.Logging | |||||
| public event Func<LogMessage, Task> Message; | public event Func<LogMessage, Task> Message; | ||||
| internal LogManager(LogSeverity minSeverity) | |||||
| public LogManager(LogSeverity minSeverity) | |||||
| { | { | ||||
| Level = minSeverity; | Level = minSeverity; | ||||
| } | } | ||||
| @@ -110,6 +111,6 @@ namespace Discord.Logging | |||||
| Task ILogger.Debug(Exception ex) | Task ILogger.Debug(Exception ex) | ||||
| => Log(LogSeverity.Debug, "Discord", ex); | => Log(LogSeverity.Debug, "Discord", ex); | ||||
| internal Logger CreateLogger(string name) => new Logger(this, name); | |||||
| public Logger CreateLogger(string name) => new Logger(this, name); | |||||
| } | } | ||||
| } | } | ||||
| @@ -10,7 +10,7 @@ namespace Discord.Logging | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public LogSeverity Level => _manager.Level; | public LogSeverity Level => _manager.Level; | ||||
| internal Logger(LogManager manager, string name) | |||||
| public Logger(LogManager manager, string name) | |||||
| { | { | ||||
| _manager = manager; | _manager = manager; | ||||
| Name = name; | Name = name; | ||||
| @@ -11,7 +11,8 @@ namespace Discord.Net.Converters | |||||
| public class DiscordContractResolver : DefaultContractResolver | public class DiscordContractResolver : DefaultContractResolver | ||||
| { | { | ||||
| private static readonly TypeInfo _ienumerable = typeof(IEnumerable<ulong[]>).GetTypeInfo(); | private static readonly TypeInfo _ienumerable = typeof(IEnumerable<ulong[]>).GetTypeInfo(); | ||||
| private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize"); | |||||
| protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | ||||
| { | { | ||||
| var property = base.CreateProperty(member, memberSerialization); | var property = base.CreateProperty(member, memberSerialization); | ||||
| @@ -54,12 +55,15 @@ namespace Discord.Net.Converters | |||||
| converter = ImageConverter.Instance; | converter = ImageConverter.Instance; | ||||
| else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) | else if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>)) | ||||
| { | { | ||||
| var lambda = (Func<object, bool>)propInfo.GetMethod.CreateDelegate(typeof(Func<object, bool>)); | |||||
| /*var parentArg = Expression.Parameter(typeof(object)); | |||||
| var optional = Expression.Property(Expression.Convert(parentArg, property.DeclaringType), member as PropertyInfo); | |||||
| var isSpecified = Expression.Property(optional, OptionalConverter.IsSpecifiedProperty); | |||||
| var lambda = Expression.Lambda<Func<object, bool>>(isSpecified, parentArg).Compile();*/ | |||||
| property.ShouldSerialize = x => lambda(x); | |||||
| var typeInput = propInfo.DeclaringType; | |||||
| var typeOutput = propInfo.PropertyType; | |||||
| var getter = typeof(Func<,>).MakeGenericType(typeInput, typeOutput); | |||||
| var getterDelegate = propInfo.GetMethod.CreateDelegate(getter); | |||||
| var shouldSerialize = _shouldSerialize.MakeGenericMethod(typeInput, typeOutput); | |||||
| var shouldSerializeDelegate = (Func<object, Delegate, bool>)shouldSerialize.CreateDelegate(typeof(Func<object, Delegate, bool>)); | |||||
| property.ShouldSerialize = x => shouldSerializeDelegate(x, getterDelegate); | |||||
| converter = OptionalConverter.Instance; | converter = OptionalConverter.Instance; | ||||
| } | } | ||||
| } | } | ||||
| @@ -73,5 +77,11 @@ namespace Discord.Net.Converters | |||||
| return property; | return property; | ||||
| } | } | ||||
| private static bool ShouldSerialize<TOwner, TValue>(object owner, Delegate getter) | |||||
| where TValue : IOptional | |||||
| { | |||||
| return (getter as Func<TOwner, TValue>)((TOwner)owner).IsSpecified; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,14 +1,11 @@ | |||||
| using Discord.API; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json; | |||||
| using System; | using System; | ||||
| using System.Reflection; | |||||
| namespace Discord.Net.Converters | namespace Discord.Net.Converters | ||||
| { | { | ||||
| public class OptionalConverter : JsonConverter | public class OptionalConverter : JsonConverter | ||||
| { | { | ||||
| public static readonly OptionalConverter Instance = new OptionalConverter(); | public static readonly OptionalConverter Instance = new OptionalConverter(); | ||||
| internal static readonly PropertyInfo IsSpecifiedProperty = typeof(IOptional).GetTypeInfo().GetDeclaredProperty(nameof(IOptional.IsSpecified)); | |||||
| public override bool CanConvert(Type objectType) => true; | public override bool CanConvert(Type objectType) => true; | ||||
| public override bool CanRead => false; | public override bool CanRead => false; | ||||
| @@ -6,11 +6,13 @@ namespace Discord.Net | |||||
| public class HttpException : Exception | public class HttpException : Exception | ||||
| { | { | ||||
| public HttpStatusCode StatusCode { get; } | public HttpStatusCode StatusCode { get; } | ||||
| public string Reason { get; } | |||||
| public HttpException(HttpStatusCode statusCode) | |||||
| : base($"The server responded with error {(int)statusCode} ({statusCode})") | |||||
| public HttpException(HttpStatusCode statusCode, string reason = null) | |||||
| : base($"The server responded with error {(int)statusCode} ({statusCode}){(reason != null ? $": \"{reason}\"" : "")}") | |||||
| { | { | ||||
| StatusCode = statusCode; | StatusCode = statusCode; | ||||
| Reason = reason; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,16 @@ | |||||
| namespace Discord.Net.Queue | |||||
| { | |||||
| internal struct BucketDefinition | |||||
| { | |||||
| public int WindowCount { get; } | |||||
| public int WindowSeconds { get; } | |||||
| public GlobalBucket? Parent { get; } | |||||
| public BucketDefinition(int windowCount, int windowSeconds, GlobalBucket? parent = null) | |||||
| { | |||||
| WindowCount = windowCount; | |||||
| WindowSeconds = windowSeconds; | |||||
| Parent = parent; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,6 +1,6 @@ | |||||
| namespace Discord.Net.Queue | namespace Discord.Net.Queue | ||||
| { | { | ||||
| internal enum BucketGroup | |||||
| public enum BucketGroup | |||||
| { | { | ||||
| Global, | Global, | ||||
| Guild | Guild | ||||
| @@ -2,11 +2,10 @@ | |||||
| { | { | ||||
| public enum GlobalBucket | public enum GlobalBucket | ||||
| { | { | ||||
| General, | |||||
| Login, | |||||
| GeneralRest, | |||||
| DirectMessage, | DirectMessage, | ||||
| SendEditMessage, | SendEditMessage, | ||||
| Gateway, | |||||
| GeneralGateway, | |||||
| UpdateStatus | UpdateStatus | ||||
| } | } | ||||
| } | } | ||||