| @@ -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). | |||
| ##### 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 | |||
| You can download Discord.Net and its extensions from NuGet: | |||
| - [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/) | |||
| ### 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) | |||
| #### CLI | |||
| - [.Net Core SDK](https://www.microsoft.com/net/core#windows) | |||
| @@ -15,6 +15,6 @@ namespace Discord.API | |||
| public bool Revoked { get; set; } | |||
| [JsonProperty("integrations")] | |||
| public IEnumerable<ulong> Integrations { get; set; } | |||
| public IReadOnlyCollection<ulong> Integrations { get; set; } | |||
| } | |||
| } | |||
| @@ -9,6 +9,6 @@ namespace Discord.API | |||
| [JsonProperty("url")] | |||
| public string StreamUrl { get; set; } | |||
| [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 | |||
| { | |||
| [JsonProperty("guild_id")] | |||
| public ulong? GuildId { get; set; } | |||
| [JsonProperty("channel_id")] | |||
| public ulong ChannelId { get; set; } | |||
| [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, | |||
| /// <summary> C→S - Used to join a particular voice channel. </summary> | |||
| 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, | |||
| /// <summary> C→S - Used to resume a connection after a redirect occurs. </summary> | |||
| Resume = 6, | |||
| /// <summary> C←S - Used to notify a client that they must reconnect to another gateway. </summary> | |||
| Reconnect = 7, | |||
| /// <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")] | |||
| public ReadState[] ReadStates { get; set; } | |||
| [JsonProperty("guilds")] | |||
| public Guild[] Guilds { get; set; } | |||
| public ExtendedGuild[] Guilds { get; set; } | |||
| [JsonProperty("private_channels")] | |||
| public Channel[] PrivateChannels { get; set; } | |||
| [JsonProperty("heartbeat_interval")] | |||
| public int HeartbeatInterval { get; set; } | |||
| [JsonProperty("relationships")] | |||
| public Relationship[] Relationships { get; set; } | |||
| //Ignored | |||
| [JsonProperty("user_settings")] | |||
| @@ -3,7 +3,7 @@ using System.Collections.Generic; | |||
| namespace Discord.API.Rest | |||
| { | |||
| public class DeleteMessagesParam | |||
| public class DeleteMessagesParams | |||
| { | |||
| [JsonProperty("messages")] | |||
| 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; | |||
| namespace Discord.API.Rest | |||
| @@ -8,12 +7,6 @@ namespace Discord.API.Rest | |||
| { | |||
| [JsonProperty("username")] | |||
| 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] | |||
| 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.Extensions; | |||
| using Discord.Logging; | |||
| using Discord.Net; | |||
| using Discord.Net.Queue; | |||
| using Discord.Net.Rest; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.IO; | |||
| using System.Linq; | |||
| using System.Threading; | |||
| 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<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 API.DiscordApiClient ApiClient { get; private set; } | |||
| public IRequestQueue RequestQueue => _requestQueue; | |||
| public DiscordClient(DiscordConfig config = null) | |||
| { | |||
| if (config == null) | |||
| config = new DiscordConfig(); | |||
| _log = new LogManager(config.LogLevel); | |||
| _log.Message += async msg => await Log.Raise(msg).ConfigureAwait(false); | |||
| _discordLogger = _log.CreateLogger("Discord"); | |||
| @@ -49,26 +44,17 @@ namespace Discord.Rest | |||
| ApiClient = new API.DiscordApiClient(config.RestClientProvider, requestQueue: _requestQueue); | |||
| 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) | |||
| { | |||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| await LoginInternal(tokenType, token, null, null, false, validateToken).ConfigureAwait(false); | |||
| await LoginInternal(tokenType, token, validateToken).ConfigureAwait(false); | |||
| } | |||
| 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) | |||
| await LogoutInternal().ConfigureAwait(false); | |||
| @@ -76,13 +62,7 @@ namespace Discord.Rest | |||
| 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) | |||
| { | |||
| @@ -96,6 +76,8 @@ namespace Discord.Rest | |||
| } | |||
| } | |||
| await OnLogin().ConfigureAwait(false); | |||
| LoginState = LoginState.LoggedIn; | |||
| } | |||
| catch (Exception) | |||
| @@ -106,6 +88,7 @@ namespace Discord.Rest | |||
| await LoggedIn.Raise().ConfigureAwait(false); | |||
| } | |||
| protected virtual Task OnLogin() => Task.CompletedTask; | |||
| public async Task Logout() | |||
| { | |||
| @@ -122,6 +105,8 @@ namespace Discord.Rest | |||
| LoginState = LoginState.LoggingOut; | |||
| await ApiClient.Logout().ConfigureAwait(false); | |||
| await OnLogout().ConfigureAwait(false); | |||
| _currentUser = null; | |||
| @@ -129,14 +114,15 @@ namespace Discord.Rest | |||
| 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); | |||
| 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); | |||
| if (model != null) | |||
| @@ -151,17 +137,17 @@ namespace Discord.Rest | |||
| } | |||
| } | |||
| else | |||
| return new DMChannel(this, model); | |||
| return new DMChannel(this, new User(this, model.Recipient), model); | |||
| } | |||
| return null; | |||
| } | |||
| public async Task<IEnumerable<DMChannel>> GetDMChannels() | |||
| public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels() | |||
| { | |||
| 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); | |||
| if (model != null) | |||
| @@ -169,48 +155,48 @@ namespace Discord.Rest | |||
| 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); | |||
| if (model != null) | |||
| return new Guild(this, model); | |||
| 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); | |||
| if (model != null) | |||
| return new GuildEmbed(model); | |||
| return null; | |||
| } | |||
| public async Task<IEnumerable<UserGuild>> GetGuilds() | |||
| public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds() | |||
| { | |||
| 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 model = await ApiClient.CreateGuild(args).ConfigureAwait(false); | |||
| 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); | |||
| if (model != null) | |||
| return new PublicUser(this, model); | |||
| return new User(this, model); | |||
| 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); | |||
| if (model != null) | |||
| return new PublicUser(this, model); | |||
| return new User(this, model); | |||
| return null; | |||
| } | |||
| public async Task<SelfUser> GetCurrentUser() | |||
| public virtual async Task<ISelfUser> GetCurrentUser() | |||
| { | |||
| var user = _currentUser; | |||
| if (user == null) | |||
| @@ -221,60 +207,32 @@ namespace Discord.Rest | |||
| } | |||
| 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); | |||
| 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); | |||
| 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); | |||
| return models.Select(x => new VoiceRegion(x)).Where(x => x.Id == id).FirstOrDefault(); | |||
| } | |||
| void Dispose(bool disposing) | |||
| internal void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| _isDisposed = true; | |||
| } | |||
| public void Dispose() => Dispose(true); | |||
| 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 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 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 | |||
| { | |||
| @@ -15,15 +15,15 @@ namespace Discord.WebSocket | |||
| /// <summary> Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. </summary> | |||
| public int ReconnectDelay { get; set; } = 1000; | |||
| /// <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> | |||
| public int MessageCacheSize { get; set; } = 100; | |||
| /// <summary> | |||
| /*/// <summary> | |||
| /// 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> | |||
| 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> | |||
| public bool EnablePreUpdateEvents { get; set; } = true; | |||
| /// <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.Extensions; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| 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; | |||
| /// <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; } | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| Position = model.Position; | |||
| @@ -49,6 +46,13 @@ namespace Discord.Rest | |||
| _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) | |||
| { | |||
| if (func != null) throw new NullReferenceException(nameof(func)); | |||
| @@ -56,10 +60,35 @@ namespace Discord.Rest | |||
| var args = new ModifyGuildChannelParams(); | |||
| func(args); | |||
| 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) | |||
| { | |||
| Overwrite value; | |||
| @@ -67,7 +96,6 @@ namespace Discord.Rest | |||
| return value.Permissions; | |||
| return null; | |||
| } | |||
| /// <inheritdoc /> | |||
| public OverwritePermissions? GetPermissionOverwrite(IRole role) | |||
| { | |||
| Overwrite value; | |||
| @@ -75,28 +103,19 @@ namespace Discord.Rest | |||
| return value.Permissions; | |||
| 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) | |||
| { | |||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | |||
| 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 }); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task AddPermissionOverwrite(IRole role, OverwritePermissions perms) | |||
| { | |||
| var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; | |||
| 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 }); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task RemovePermissionOverwrite(IUser user) | |||
| { | |||
| await Discord.ApiClient.DeleteChannelPermission(Id, user.Id).ConfigureAwait(false); | |||
| @@ -104,7 +123,6 @@ namespace Discord.Rest | |||
| Overwrite value; | |||
| _overwrites.TryRemove(user.Id, out value); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task RemovePermissionOverwrite(IRole role) | |||
| { | |||
| await Discord.ApiClient.DeleteChannelPermission(Id, role.Id).ConfigureAwait(false); | |||
| @@ -112,58 +130,15 @@ namespace Discord.Rest | |||
| Overwrite 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; | |||
| 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; | |||
| 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 | |||
| { | |||
| /// <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> | |||
| 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> | |||
| 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> | |||
| 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> | |||
| Task<IEnumerable<IInviteMetadata>> GetInvites(); | |||
| Task<IReadOnlyCollection<IInviteMetadata>> GetInvites(); | |||
| /// <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> | |||
| Task Modify(Action<ModifyGuildChannelParams> func); | |||
| @@ -44,7 +44,7 @@ namespace Discord | |||
| Task AddPermissionOverwrite(IUser user, OverwritePermissions permissions); | |||
| /// <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> | |||
| new Task<IGuildUser> GetUser(ulong id); | |||
| } | |||
| @@ -7,25 +7,25 @@ namespace Discord | |||
| public interface IMessageChannel : IChannel | |||
| { | |||
| /// <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); | |||
| /// <summary> Sends a file to this text channel, with an optional caption. </summary> | |||
| Task<IMessage> SendFile(string filePath, string text = null, bool isTTS = false); | |||
| /// <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); | |||
| /// <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> | |||
| Task DeleteMessages(IEnumerable<IMessage> messages); | |||
| /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.</summary> | |||
| 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 Model = Discord.API.Channel; | |||
| namespace Discord.Rest | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class VoiceChannel : GuildChannel, IVoiceChannel | |||
| internal class VoiceChannel : GuildChannel, IVoiceChannel | |||
| { | |||
| /// <inheritdoc /> | |||
| public int Bitrate { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int UserLimit { get; private set; } | |||
| internal VoiceChannel(Guild guild, Model model) | |||
| public VoiceChannel(Guild guild, Model 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; | |||
| UserLimit = model.UserLimit; | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Modify(Action<ModifyVoiceChannelParams> func) | |||
| { | |||
| if (func != null) throw new NullReferenceException(nameof(func)); | |||
| @@ -34,12 +33,21 @@ namespace Discord.Rest | |||
| var args = new ModifyVoiceChannelParams(); | |||
| func(args); | |||
| 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)"; | |||
| } | |||
| @@ -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 IImmutableList<ulong> RoleIds { get; } | |||
| internal Emoji(Model model) | |||
| public Emoji(Model model) | |||
| { | |||
| Id = model.Id; | |||
| Name = model.Name; | |||
| @@ -1,77 +1,60 @@ | |||
| using Discord.API.Rest; | |||
| using Discord.Extensions; | |||
| using System; | |||
| using System.Collections.Concurrent; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Guild; | |||
| using EmbedModel = Discord.API.GuildEmbed; | |||
| using Model = Discord.API.Guild; | |||
| 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}")] | |||
| 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; } | |||
| /// <inheritdoc /> | |||
| public int AFKTimeout { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsEmbeddable { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int VerificationLevel { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? AFKChannelId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong? EmbedChannelId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong OwnerId { get; private set; } | |||
| /// <inheritdoc /> | |||
| 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); | |||
| /// <inheritdoc /> | |||
| public string SplashUrl => API.CDN.GetGuildSplashUrl(Id, _splashId); | |||
| /// <inheritdoc /> | |||
| public ulong DefaultChannelId => Id; | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| AFKTimeout = model.AFKTimeout; | |||
| EmbedChannelId = model.EmbedChannelId; | |||
| AFKTimeout = model.AFKTimeout; | |||
| IsEmbeddable = model.EmbedEnabled; | |||
| Features = model.Features; | |||
| Features = model.Features.ToImmutableArray(); | |||
| _iconId = model.Icon; | |||
| Name = model.Name; | |||
| OwnerId = model.OwnerId; | |||
| @@ -84,10 +67,10 @@ namespace Discord.Rest | |||
| var emojis = ImmutableArray.CreateBuilder<Emoji>(model.Emojis.Length); | |||
| for (int i = 0; i < model.Emojis.Length; i++) | |||
| emojis.Add(new Emoji(model.Emojis[i])); | |||
| Emojis = emojis.ToArray(); | |||
| Emojis = emojis.ToImmutableArray(); | |||
| } | |||
| else | |||
| Emojis = Array.Empty<Emoji>(); | |||
| Emojis = ImmutableArray.Create<Emoji>(); | |||
| var roles = new ConcurrentDictionary<ulong, Role>(1, model.Roles?.Length ?? 0); | |||
| if (model.Roles != null) | |||
| @@ -97,28 +80,32 @@ namespace Discord.Rest | |||
| } | |||
| _roles = roles; | |||
| } | |||
| private void Update(EmbedModel model) | |||
| public void Update(EmbedModel model, UpdateSource source) | |||
| { | |||
| if (source == UpdateSource.Rest && IsAttached) return; | |||
| IsEmbeddable = model.Enabled; | |||
| 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; | |||
| foreach (var model in models) | |||
| { | |||
| if (_roles.TryGetValue(model.Id, out role)) | |||
| role.Update(model); | |||
| role.Update(model, UpdateSource.Rest); | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Update() | |||
| { | |||
| if (IsAttached) throw new NotSupportedException(); | |||
| var response = await Discord.ApiClient.GetGuild(Id).ConfigureAwait(false); | |||
| Update(response); | |||
| Update(response, UpdateSource.Rest); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Modify(Action<ModifyGuildParams> func) | |||
| { | |||
| if (func == null) throw new NullReferenceException(nameof(func)); | |||
| @@ -126,9 +113,8 @@ namespace Discord.Rest | |||
| var args = new ModifyGuildParams(); | |||
| func(args); | |||
| var model = await Discord.ApiClient.ModifyGuild(Id, args).ConfigureAwait(false); | |||
| Update(model); | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task ModifyEmbed(Action<ModifyGuildEmbedParams> func) | |||
| { | |||
| if (func == null) throw new NullReferenceException(nameof(func)); | |||
| @@ -136,68 +122,57 @@ namespace Discord.Rest | |||
| var args = new ModifyGuildEmbedParams(); | |||
| func(args); | |||
| var model = await Discord.ApiClient.ModifyGuildEmbed(Id, args).ConfigureAwait(false); | |||
| Update(model); | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task ModifyChannels(IEnumerable<ModifyGuildChannelsParams> args) | |||
| { | |||
| //TODO: Update channels | |||
| await Discord.ApiClient.ModifyGuildChannels(Id, args).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task ModifyRoles(IEnumerable<ModifyGuildRolesParams> args) | |||
| { | |||
| var models = await Discord.ApiClient.ModifyGuildRoles(Id, args).ConfigureAwait(false); | |||
| Update(models); | |||
| Update(models, UpdateSource.Rest); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Leave() | |||
| { | |||
| await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Delete() | |||
| { | |||
| 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); | |||
| 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); | |||
| /// <inheritdoc /> | |||
| public async Task AddBan(ulong userId, int pruneDays = 0) | |||
| { | |||
| var args = new CreateGuildBanParams() { PruneDays = pruneDays }; | |||
| await Discord.ApiClient.CreateGuildBan(Id, userId, args).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public Task RemoveBan(IUser user) => RemoveBan(user.Id); | |||
| /// <inheritdoc /> | |||
| public async Task RemoveBan(ulong userId) | |||
| { | |||
| 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); | |||
| if (model != null) | |||
| return ToChannel(model); | |||
| 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); | |||
| 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)); | |||
| @@ -205,8 +180,7 @@ namespace Discord.Rest | |||
| var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | |||
| 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)); | |||
| @@ -214,29 +188,25 @@ namespace Discord.Rest | |||
| var model = await Discord.ApiClient.CreateGuildChannel(Id, args).ConfigureAwait(false); | |||
| 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); | |||
| 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 model = await Discord.ApiClient.CreateGuildIntegration(Id, args).ConfigureAwait(false); | |||
| 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); | |||
| 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 (maxUses <= 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | |||
| @@ -251,18 +221,15 @@ namespace Discord.Rest | |||
| var model = await Discord.ApiClient.CreateChannelInvite(DefaultChannelId, args).ConfigureAwait(false); | |||
| 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) | |||
| { | |||
| Role result = null; | |||
| if (_roles?.TryGetValue(id, out result) == true) | |||
| return result; | |||
| 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)); | |||
| @@ -280,34 +247,30 @@ namespace Discord.Rest | |||
| 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); | |||
| if (model != null) | |||
| return new GuildUser(this, model); | |||
| return new GuildUser(this, new User(Discord, model.User), model); | |||
| 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); | |||
| 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) | |||
| { | |||
| var args = new GuildPruneParams() { Days = days }; | |||
| @@ -324,45 +287,22 @@ namespace Discord.Rest | |||
| switch (model.Type) | |||
| { | |||
| case ChannelType.Text: | |||
| default: | |||
| return new TextChannel(this, model); | |||
| case ChannelType.Voice: | |||
| return new VoiceChannel(this, model); | |||
| default: | |||
| throw new InvalidOperationException($"Unknown channel type: {model.Type}"); | |||
| } | |||
| } | |||
| public override string ToString() => Name; | |||
| 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 Model = Discord.API.Integration; | |||
| namespace Discord.Rest | |||
| namespace Discord | |||
| { | |||
| [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; } | |||
| /// <inheritdoc /> | |||
| public string Type { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsEnabled { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsSyncing { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong ExpireBehavior { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong ExpireGracePeriod { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTime SyncedAt { get; private set; } | |||
| /// <inheritdoc /> | |||
| public Guild Guild { get; private set; } | |||
| /// <inheritdoc /> | |||
| public Role Role { get; private set; } | |||
| /// <inheritdoc /> | |||
| public User User { get; private set; } | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| Type = model.Type; | |||
| IsEnabled = model.Enabled; | |||
| @@ -53,16 +43,14 @@ namespace Discord.Rest | |||
| ExpireGracePeriod = model.ExpireGracePeriod; | |||
| 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() | |||
| { | |||
| await Discord.ApiClient.DeleteGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | |||
| } | |||
| /// <summary> </summary> | |||
| public async Task Modify(Action<ModifyGuildIntegrationParams> func) | |||
| { | |||
| if (func == null) throw new NullReferenceException(nameof(func)); | |||
| @@ -71,9 +59,8 @@ namespace Discord.Rest | |||
| func(args); | |||
| var model = await Discord.ApiClient.ModifyGuildIntegration(Guild.Id, Id, args).ConfigureAwait(false); | |||
| Update(model); | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| /// <summary> </summary> | |||
| public async Task Sync() | |||
| { | |||
| await Discord.ApiClient.SyncGuildIntegration(Guild.Id, Id).ConfigureAwait(false); | |||
| @@ -83,8 +70,7 @@ namespace Discord.Rest | |||
| private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; | |||
| IGuild IGuildIntegration.Guild => Guild; | |||
| IRole IGuildIntegration.Role => Role; | |||
| IUser IGuildIntegration.User => User; | |||
| IntegrationAccount IGuildIntegration.Account => Account; | |||
| IRole IGuildIntegration.Role => Role; | |||
| } | |||
| } | |||
| @@ -7,13 +7,17 @@ namespace Discord | |||
| { | |||
| 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> | |||
| int AFKTimeout { get; } | |||
| /// <summary> Returns true if this guild is embeddable (e.g. widget) </summary> | |||
| bool IsEmbeddable { get; } | |||
| /// <summary> Gets the name of this guild. </summary> | |||
| string Name { 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> | |||
| ulong? AFKChannelId { get; } | |||
| @@ -21,22 +25,19 @@ namespace Discord | |||
| ulong DefaultChannelId { get; } | |||
| /// <summary> Gets the id of the embed channel for this guild if set, or null if not. </summary> | |||
| 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> | |||
| 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; } | |||
| /// <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> | |||
| IEnumerable<Emoji> Emojis { get; } | |||
| IReadOnlyCollection<Emoji> Emojis { get; } | |||
| /// <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> | |||
| Task Modify(Action<ModifyGuildParams> func); | |||
| @@ -50,7 +51,7 @@ namespace Discord | |||
| Task Leave(); | |||
| /// <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> | |||
| Task AddBan(IUser user, int pruneDays = 0); | |||
| /// <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); | |||
| /// <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> | |||
| Task<IGuildChannel> GetChannel(ulong id); | |||
| /// <summary> Creates a new text channel. </summary> | |||
| @@ -70,7 +71,7 @@ namespace Discord | |||
| Task<IVoiceChannel> CreateVoiceChannel(string name); | |||
| /// <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> | |||
| /// <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> | |||
| @@ -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> | |||
| 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> | |||
| Task<IRole> GetRole(ulong id); | |||
| IRole GetRole(ulong id); | |||
| /// <summary> Creates a new role. </summary> | |||
| 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> | |||
| 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> | |||
| Task<IGuildUser> GetUser(ulong id); | |||
| /// <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> | |||
| 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; } | |||
| /// <summary> Returns true if the current user owns this guild. </summary> | |||
| bool IsOwner { get; } | |||
| @@ -1,7 +1,9 @@ | |||
| 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> | |||
| string Name { get; } | |||
| /// <summary> Returns true if this voice region is exclusive to VIP accounts. </summary> | |||
| @@ -5,10 +5,7 @@ namespace Discord | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| public struct IntegrationAccount | |||
| { | |||
| /// <inheritdoc /> | |||
| public string Id { get; } | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| public override string ToString() => Name; | |||
| @@ -1,50 +1,42 @@ | |||
| using System; | |||
| using System.Diagnostics; | |||
| using System.Diagnostics; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.UserGuild; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class UserGuild : IUserGuild | |||
| internal class UserGuild : SnowflakeEntity, IUserGuild | |||
| { | |||
| private string _iconId; | |||
| /// <inheritdoc /> | |||
| public ulong Id { get; } | |||
| internal IDiscordClient Discord { get; } | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| public bool IsOwner { 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); | |||
| internal UserGuild(IDiscordClient discord, Model model) | |||
| public UserGuild(DiscordClient discord, Model model) | |||
| : base(model.Id) | |||
| { | |||
| 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; | |||
| IsOwner = model.Owner; | |||
| Name = model.Name; | |||
| Permissions = new GuildPermissions(model.Permissions); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Leave() | |||
| { | |||
| await Discord.ApiClient.LeaveGuild(Id).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Delete() | |||
| { | |||
| await Discord.ApiClient.DeleteGuild(Id).ConfigureAwait(false); | |||
| @@ -4,22 +4,16 @@ using Model = Discord.API.VoiceRegion; | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| public class VoiceRegion : IVoiceRegion | |||
| internal class VoiceRegion : IVoiceRegion | |||
| { | |||
| /// <inheritdoc /> | |||
| public string Id { get; } | |||
| /// <inheritdoc /> | |||
| public string Name { get; } | |||
| /// <inheritdoc /> | |||
| public bool IsVip { get; } | |||
| /// <inheritdoc /> | |||
| public bool IsOptimal { get; } | |||
| /// <inheritdoc /> | |||
| public string SampleHostname { get; } | |||
| /// <inheritdoc /> | |||
| public int SamplePort { get; } | |||
| internal VoiceRegion(Model model) | |||
| public VoiceRegion(Model model) | |||
| { | |||
| Id = model.Id; | |||
| Name = model.Name; | |||
| @@ -4,5 +4,9 @@ namespace Discord | |||
| { | |||
| /// <summary> Gets the unique identifier for this object. </summary> | |||
| 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 | |||
| { | |||
| /// <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(); | |||
| } | |||
| } | |||
| @@ -18,7 +18,7 @@ namespace Discord | |||
| /// <summary> Gets the id of the guild this invite is linked to. </summary> | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -5,38 +5,31 @@ using Model = Discord.API.Invite; | |||
| namespace Discord | |||
| { | |||
| [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; } | |||
| /// <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}"; | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| GuildId = model.Guild.Id; | |||
| ChannelId = model.Channel.Id; | |||
| @@ -44,22 +37,16 @@ namespace Discord | |||
| ChannelName = model.Channel.Name; | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Accept() | |||
| { | |||
| await Discord.ApiClient.AcceptInvite(Code).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task Delete() | |||
| { | |||
| await Discord.ApiClient.DeleteInvite(Code).ConfigureAwait(false); | |||
| } | |||
| /// <inheritdoc /> | |||
| public override string ToString() => XkcdUrl ?? Url; | |||
| private string DebuggerDisplay => $"{XkcdUrl ?? Url} ({GuildName} / {ChannelName})"; | |||
| string IEntity<string>.Id => Code; | |||
| } | |||
| } | |||
| @@ -2,26 +2,23 @@ | |||
| namespace Discord | |||
| { | |||
| public class InviteMetadata : Invite, IInviteMetadata | |||
| internal class InviteMetadata : Invite, IInviteMetadata | |||
| { | |||
| /// <inheritdoc /> | |||
| public bool IsRevoked { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsTemporary { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int? MaxAge { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int? MaxUses { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int Uses { get; private set; } | |||
| internal InviteMetadata(IDiscordClient client, Model model) | |||
| public InviteMetadata(DiscordClient client, Model 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; | |||
| IsTemporary = model.Temporary; | |||
| MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; | |||
| @@ -2,16 +2,16 @@ | |||
| namespace Discord | |||
| { | |||
| public struct Embed | |||
| internal class Embed : IEmbed | |||
| { | |||
| public string Description { get; } | |||
| public string Url { get; } | |||
| public string Type { get; } | |||
| public string Title { get; } | |||
| public string Description { get; } | |||
| public string Type { get; } | |||
| public EmbedProvider Provider { get; } | |||
| public EmbedThumbnail Thumbnail { get; } | |||
| internal Embed(Model model) | |||
| public Embed(Model model) | |||
| { | |||
| Url = model.Url; | |||
| Type = model.Type; | |||
| @@ -7,10 +7,12 @@ namespace Discord | |||
| public string Name { 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? 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 | |||
| { | |||
| public interface IMessage : IDeletable, ISnowflakeEntity | |||
| public interface IMessage : IDeletable, ISnowflakeEntity, IUpdateable | |||
| { | |||
| /// <summary> Gets the time of this message's last edit, if any. </summary> | |||
| DateTime? EditedTimestamp { get; } | |||
| @@ -16,23 +16,22 @@ namespace Discord | |||
| /// <summary> Returns the text for this message after mention processing. </summary> | |||
| string Text { get; } | |||
| /// <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> | |||
| IMessageChannel Channel { get; } | |||
| /// <summary> Gets the author of this message. </summary> | |||
| IUser Author { get; } | |||
| /// <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> | |||
| IReadOnlyList<Embed> Embeds { get; } | |||
| IReadOnlyCollection<IEmbed> Embeds { get; } | |||
| /// <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> | |||
| IReadOnlyList<ulong> MentionedRoleIds { get; } | |||
| IReadOnlyCollection<ulong> MentionedRoleIds { get; } | |||
| /// <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> | |||
| Task Modify(Action<ModifyMessageParams> func); | |||
| @@ -6,55 +6,40 @@ using System.Diagnostics; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Message; | |||
| namespace Discord.Rest | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class Message : IMessage | |||
| { | |||
| /// <inheritdoc /> | |||
| public ulong Id { get; } | |||
| /// <inheritdoc /> | |||
| internal class Message : SnowflakeEntity, IMessage | |||
| { | |||
| public DateTime? EditedTimestamp { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsTTS { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string RawText { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Text { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTime Timestamp { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IMessageChannel Channel { get; } | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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 guild = guildChannel?.Guild; | |||
| var discord = Discord; | |||
| @@ -72,7 +57,7 @@ namespace Discord.Rest | |||
| Attachments = ImmutableArray.Create(attachments); | |||
| } | |||
| else | |||
| Attachments = Array.Empty<Attachment>(); | |||
| Attachments = ImmutableArray.Create<Attachment>(); | |||
| if (model.Embeds.Length > 0) | |||
| { | |||
| @@ -82,17 +67,17 @@ namespace Discord.Rest | |||
| Embeds = ImmutableArray.Create(embeds); | |||
| } | |||
| else | |||
| Embeds = Array.Empty<Embed>(); | |||
| Embeds = ImmutableArray.Create<Embed>(); | |||
| 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++) | |||
| mentions[i] = new PublicUser(discord, model.Mentions[i]); | |||
| mentions[i] = new User(discord, model.Mentions[i]); | |||
| MentionedUsers = ImmutableArray.Create(mentions); | |||
| } | |||
| else | |||
| MentionedUsers = Array.Empty<PublicUser>(); | |||
| MentionedUsers = ImmutableArray.Create<User>(); | |||
| if (guildChannel != null) | |||
| { | |||
| @@ -105,14 +90,20 @@ namespace Discord.Rest | |||
| } | |||
| else | |||
| { | |||
| MentionedChannelIds = Array.Empty<ulong>(); | |||
| MentionedRoleIds = Array.Empty<ulong>(); | |||
| MentionedChannelIds = ImmutableArray.Create<ulong>(); | |||
| MentionedRoleIds = ImmutableArray.Create<ulong>(); | |||
| } | |||
| 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) | |||
| { | |||
| 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); | |||
| else | |||
| model = await Discord.ApiClient.ModifyDMMessage(Channel.Id, Id, args).ConfigureAwait(false); | |||
| Update(model); | |||
| } | |||
| /// <inheritdoc /> | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| public async Task Delete() | |||
| { | |||
| var guildChannel = Channel as GuildChannel; | |||
| @@ -140,9 +129,12 @@ namespace Discord.Rest | |||
| } | |||
| 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; | |||
| } | |||
| /// <inheritdoc /> | |||
| public override string ToString() => RawValue.ToString(); | |||
| private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | |||
| } | |||
| @@ -144,7 +144,7 @@ namespace Discord | |||
| } | |||
| return perms; | |||
| } | |||
| /// <inheritdoc /> | |||
| public override string ToString() => RawValue.ToString(); | |||
| private string DebuggerDisplay => $"{RawValue} ({string.Join(", ", ToList())})"; | |||
| } | |||
| @@ -12,11 +12,14 @@ namespace Discord | |||
| public OverwritePermissions Permissions { get; } | |||
| /// <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; | |||
| } | |||
| /// <inheritdoc /> | |||
| public override string ToString() => $"Allow {AllowValue}, Deny {DenyValue}"; | |||
| private string DebuggerDisplay => | |||
| $"Allow {AllowValue} ({string.Join(", ", ToAllowList())})\n" + | |||
| @@ -90,8 +90,8 @@ namespace Discord | |||
| { | |||
| var roles = user.Roles; | |||
| 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; | |||
| } | |||
| @@ -110,25 +110,26 @@ namespace Discord | |||
| { | |||
| //Start with this user's guild permissions | |||
| resolvedPermissions = guildPermissions; | |||
| var overwrites = channel.PermissionOverwrites; | |||
| Overwrite entry; | |||
| OverwritePermissions? perms; | |||
| var roles = user.Roles; | |||
| 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 | |||
| switch (channel) | |||
| @@ -11,7 +11,7 @@ namespace Discord | |||
| Color Color { get; } | |||
| /// <summary> Returns true if users of this role are separated in the user list. </summary> | |||
| 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; } | |||
| /// <summary> Gets the name of this role. </summary> | |||
| string Name { get; } | |||
| @@ -25,8 +25,5 @@ namespace Discord | |||
| /// <summary> Modifies this role. </summary> | |||
| 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 System; | |||
| using System.Collections.Generic; | |||
| using System.Collections.Immutable; | |||
| using System.Diagnostics; | |||
| using System.Linq; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.Role; | |||
| namespace Discord.Rest | |||
| namespace Discord | |||
| { | |||
| [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; } | |||
| /// <inheritdoc /> | |||
| public Color Color { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsHoisted { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsManaged { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| /// <inheritdoc /> | |||
| public GuildPermissions Permissions { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int Position { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTime CreatedAt => DateTimeUtils.FromSnowflake(Id); | |||
| /// <inheritdoc /> | |||
| public bool IsEveryone => Id == Guild.Id; | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| IsHoisted = model.Hoist.Value; | |||
| IsManaged = model.Managed.Value; | |||
| @@ -53,7 +43,7 @@ namespace Discord.Rest | |||
| Color = new Color(model.Color.Value); | |||
| Permissions = new GuildPermissions(model.Permissions.Value); | |||
| } | |||
| /// <summary> Modifies the properties of this role. </summary> | |||
| public async Task Modify(Action<ModifyGuildRoleParams> func) | |||
| { | |||
| if (func == null) throw new NullReferenceException(nameof(func)); | |||
| @@ -61,23 +51,16 @@ namespace Discord.Rest | |||
| var args = new ModifyGuildRoleParams(); | |||
| func(args); | |||
| 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() | |||
| => await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| { | |||
| await Discord.ApiClient.DeleteGuildRole(Guild.Id, Id).ConfigureAwait(false); | |||
| } | |||
| public override string ToString() => Name; | |||
| private string DebuggerDisplay => $"{Name} ({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 | |||
| { | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class Connection : IConnection | |||
| internal class Connection : IConnection | |||
| { | |||
| public string Id { get; } | |||
| public string Type { get; } | |||
| public string Name { get; } | |||
| public bool IsRevoked { get; } | |||
| public IEnumerable<ulong> IntegrationIds { get; } | |||
| public IReadOnlyCollection<ulong> IntegrationIds { get; } | |||
| public Connection(Model model) | |||
| { | |||
| Id = model.Id; | |||
| Type = model.Type; | |||
| Name = model.Name; | |||
| IsRevoked = model.Revoked; | |||
| @@ -1,4 +1,6 @@ | |||
| namespace Discord | |||
| using Model = Discord.API.Game; | |||
| namespace Discord | |||
| { | |||
| public struct Game | |||
| { | |||
| @@ -6,17 +8,15 @@ | |||
| public string StreamUrl { get; } | |||
| public StreamType StreamType { get; } | |||
| public Game(string name) | |||
| { | |||
| Name = name; | |||
| StreamUrl = null; | |||
| StreamType = StreamType.NotStreaming; | |||
| } | |||
| public Game(string name, string streamUrl, StreamType type) | |||
| { | |||
| Name = name; | |||
| StreamUrl = streamUrl; | |||
| 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 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; } | |||
| /// <inheritdoc /> | |||
| public bool IsMute { get; private set; } | |||
| /// <inheritdoc /> | |||
| public DateTime JoinedAt { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Nickname { get; private set; } | |||
| /// <inheritdoc /> | |||
| 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; | |||
| 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; | |||
| IsMute = model.Mute; | |||
| JoinedAt = model.JoinedAt.Value; | |||
| Nickname = model.Nick; | |||
| 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++) | |||
| 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)); | |||
| } | |||
| 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) | |||
| { | |||
| if (func == null) throw new NullReferenceException(nameof(func)); | |||
| @@ -82,7 +76,7 @@ namespace Discord.Rest | |||
| { | |||
| var nickArgs = new ModifyCurrentUserNickParams { Nickname = args.Nickname.Value ?? "" }; | |||
| 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) | |||
| @@ -95,18 +89,24 @@ namespace Discord.Rest | |||
| if (args.Nickname.IsSpecified) | |||
| Nickname = args.Nickname.Value ?? ""; | |||
| 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; | |||
| IReadOnlyList<IRole> IGuildUser.Roles => Roles; | |||
| IReadOnlyCollection<IRole> IGuildUser.Roles => Roles; | |||
| IVoiceChannel IGuildUser.VoiceChannel => null; | |||
| GuildPermissions IGuildUser.GetGuildPermissions() | |||
| => GuildPermissions; | |||
| ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) | |||
| => GetPermissions(channel); | |||
| } | |||
| } | |||
| @@ -9,6 +9,6 @@ namespace Discord | |||
| string Name { get; } | |||
| bool IsRevoked { get; } | |||
| IEnumerable<ulong> IntegrationIds { get; } | |||
| IReadOnlyCollection<ulong> IntegrationIds { get; } | |||
| } | |||
| } | |||
| @@ -16,16 +16,16 @@ namespace Discord | |||
| DateTime JoinedAt { get; } | |||
| /// <summary> Gets the nickname for this user. </summary> | |||
| 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> | |||
| 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> | |||
| IReadOnlyList<IRole> Roles { get; } | |||
| IReadOnlyCollection<IRole> Roles { get; } | |||
| /// <summary> Gets the voice channel this user is currently in, if any. </summary> | |||
| 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> | |||
| ChannelPermissions GetPermissions(IGuildChannel channel); | |||
| @@ -34,4 +34,4 @@ namespace Discord | |||
| /// <summary> Modifies this user's properties in this guild. </summary> | |||
| 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 | |||
| { | |||
| public interface IUser : ISnowflakeEntity, IMentionable | |||
| public interface IUser : ISnowflakeEntity, IMentionable, IPresence | |||
| { | |||
| /// <summary> Gets the url to this user's avatar. </summary> | |||
| 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> | |||
| ushort Discriminator { get; } | |||
| /// <summary> Returns true if this user is a bot account. </summary> | |||
| bool IsBot { get; } | |||
| /// <summary> Gets the current status of this user. </summary> | |||
| UserStatus Status { get; } | |||
| /// <summary> Gets the username for this user. </summary> | |||
| string Username { get; } | |||
| @@ -3,38 +3,34 @@ using System; | |||
| using System.Threading.Tasks; | |||
| 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; } | |||
| /// <inheritdoc /> | |||
| 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; | |||
| IsVerified = model.IsVerified; | |||
| } | |||
| /// <inheritdoc /> | |||
| 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) | |||
| { | |||
| if (func != null) throw new NullReferenceException(nameof(func)); | |||
| @@ -42,7 +38,7 @@ namespace Discord.Rest | |||
| var args = new ModifyCurrentUserParams(); | |||
| func(args); | |||
| var model = await Discord.ApiClient.ModifyCurrentUser(args).ConfigureAwait(false); | |||
| Update(model); | |||
| Update(model, UpdateSource.Rest); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,68 +1,52 @@ | |||
| using Discord.API.Rest; | |||
| using System; | |||
| using System.Diagnostics; | |||
| using System.Threading.Tasks; | |||
| using Model = Discord.API.User; | |||
| namespace Discord.Rest | |||
| namespace Discord | |||
| { | |||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | |||
| public abstract class User : IUser | |||
| internal class User : SnowflakeEntity, IUser | |||
| { | |||
| private string _avatarId; | |||
| /// <inheritdoc /> | |||
| public ulong Id { get; } | |||
| internal abstract DiscordClient Discord { get; } | |||
| /// <inheritdoc /> | |||
| public ushort Discriminator { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool IsBot { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Username { get; private set; } | |||
| /// <inheritdoc /> | |||
| public override DiscordClient Discord { get; } | |||
| 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); | |||
| /// <inheritdoc /> | |||
| 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; | |||
| Discriminator = model.Discriminator; | |||
| IsBot = model.Bot; | |||
| Username = model.Username; | |||
| } | |||
| protected virtual async Task<DMChannel> CreateDMChannelInternal() | |||
| public async Task<IDMChannel> CreateDMChannel() | |||
| { | |||
| var args = new CreateDMChannelParams { RecipientId = Id }; | |||
| 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}"; | |||
| 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 | |||
| { | |||
| public class VoiceState | |||
| internal class VoiceState : IVoiceState | |||
| { | |||
| [Flags] | |||
| private enum VoiceStates : byte | |||
| @@ -22,7 +22,7 @@ namespace Discord.WebSocket | |||
| public ulong UserId { get; } | |||
| /// <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> | |||
| 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> | |||
| public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; | |||
| internal VoiceState(ulong userId, Guild guild) | |||
| public VoiceState(ulong userId, Guild guild) | |||
| { | |||
| UserId = userId; | |||
| Guild = guild; | |||
| } | |||
| internal void Update(Model model) | |||
| private void Update(Model model, UpdateSource source) | |||
| { | |||
| if (model.IsMuted == true) | |||
| _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.Threading.Tasks; | |||
| namespace Discord | |||
| namespace Discord.Extensions | |||
| { | |||
| 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.Net.Queue; | |||
| using Discord.WebSocket.Data; | |||
| using System.Collections.Generic; | |||
| using System.IO; | |||
| using System.Threading.Tasks; | |||
| @@ -14,10 +12,7 @@ namespace Discord | |||
| ConnectionState ConnectionState { 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 Logout(); | |||
| @@ -25,12 +20,12 @@ namespace Discord | |||
| Task Disconnect(); | |||
| 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<IEnumerable<IUserGuild>> GetGuilds(); | |||
| Task<IReadOnlyCollection<IUserGuild>> GetGuilds(); | |||
| Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null); | |||
| Task<IInvite> GetInvite(string inviteIdOrXkcd); | |||
| @@ -38,9 +33,9 @@ namespace Discord | |||
| Task<IUser> GetUser(ulong id); | |||
| Task<IUser> GetUser(string username, ushort discriminator); | |||
| 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); | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using Discord.Extensions; | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.Logging | |||
| @@ -9,7 +10,7 @@ namespace Discord.Logging | |||
| public event Func<LogMessage, Task> Message; | |||
| internal LogManager(LogSeverity minSeverity) | |||
| public LogManager(LogSeverity minSeverity) | |||
| { | |||
| Level = minSeverity; | |||
| } | |||
| @@ -110,6 +111,6 @@ namespace Discord.Logging | |||
| Task ILogger.Debug(Exception 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 LogSeverity Level => _manager.Level; | |||
| internal Logger(LogManager manager, string name) | |||
| public Logger(LogManager manager, string name) | |||
| { | |||
| _manager = manager; | |||
| Name = name; | |||
| @@ -11,7 +11,8 @@ namespace Discord.Net.Converters | |||
| public class DiscordContractResolver : DefaultContractResolver | |||
| { | |||
| 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) | |||
| { | |||
| var property = base.CreateProperty(member, memberSerialization); | |||
| @@ -54,12 +55,15 @@ namespace Discord.Net.Converters | |||
| converter = ImageConverter.Instance; | |||
| 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; | |||
| } | |||
| } | |||
| @@ -73,5 +77,11 @@ namespace Discord.Net.Converters | |||
| 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.Reflection; | |||
| namespace Discord.Net.Converters | |||
| { | |||
| public class OptionalConverter : JsonConverter | |||
| { | |||
| 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 CanRead => false; | |||
| @@ -6,11 +6,13 @@ namespace Discord.Net | |||
| public class HttpException : Exception | |||
| { | |||
| 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; | |||
| 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 | |||
| { | |||
| internal enum BucketGroup | |||
| public enum BucketGroup | |||
| { | |||
| Global, | |||
| Guild | |||
| @@ -2,11 +2,10 @@ | |||
| { | |||
| public enum GlobalBucket | |||
| { | |||
| General, | |||
| Login, | |||
| GeneralRest, | |||
| DirectMessage, | |||
| SendEditMessage, | |||
| Gateway, | |||
| GeneralGateway, | |||
| UpdateStatus | |||
| } | |||
| } | |||