@@ -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 | |||
} | |||
} |