| @@ -27,7 +27,7 @@ namespace Discord.API | |||||
| { | { | ||||
| public event Func<string, string, double, Task> SentRequest; | public event Func<string, string, double, Task> SentRequest; | ||||
| public event Func<int, Task> SentGatewayMessage; | public event Func<int, Task> SentGatewayMessage; | ||||
| public event Func<GatewayOpCode, string, JToken, Task> ReceivedGatewayEvent; | |||||
| public event Func<GatewayOpCode, int?, string, object, Task> ReceivedGatewayEvent; | |||||
| private readonly RequestQueue _requestQueue; | private readonly RequestQueue _requestQueue; | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| @@ -66,14 +66,14 @@ namespace Discord.API | |||||
| using (var reader = new StreamReader(decompressed)) | using (var reader = new StreamReader(decompressed)) | ||||
| { | { | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd()); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(reader.ReadToEnd()); | ||||
| await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); | |||||
| await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| _gatewayClient.TextMessage += async text => | _gatewayClient.TextMessage += async text => | ||||
| { | { | ||||
| var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | var msg = JsonConvert.DeserializeObject<WebSocketMessage>(text); | ||||
| await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Type, msg.Payload as JToken).ConfigureAwait(false); | |||||
| await ReceivedGatewayEvent.Raise((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false); | |||||
| }; | }; | ||||
| } | } | ||||
| @@ -363,6 +363,10 @@ namespace Discord.API | |||||
| }; | }; | ||||
| await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); | await SendGateway(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task SendHeartbeat(int lastSeq, RequestOptions options = null) | |||||
| { | |||||
| await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false); | |||||
| } | |||||
| //Channels | //Channels | ||||
| public async Task<Channel> GetChannel(ulong channelId, RequestOptions options = null) | public async Task<Channel> GetChannel(ulong channelId, RequestOptions options = null) | ||||
| @@ -9,7 +9,7 @@ namespace Discord.API | |||||
| [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] | ||||
| public string Type { get; set; } | public string Type { get; set; } | ||||
| [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] | ||||
| public uint? Sequence { get; set; } | |||||
| public int? Sequence { get; set; } | |||||
| [JsonProperty("d")] | [JsonProperty("d")] | ||||
| public object Payload { get; set; } | public object Payload { get; set; } | ||||
| } | } | ||||
| @@ -28,8 +28,10 @@ namespace Discord | |||||
| public LoginState LoginState { get; private set; } | public LoginState LoginState { get; private set; } | ||||
| public API.DiscordApiClient ApiClient { get; private set; } | public API.DiscordApiClient ApiClient { get; private set; } | ||||
| /// <summary> Creates a new discord client using only the REST API. </summary> | |||||
| public DiscordClient() | public DiscordClient() | ||||
| : this(new DiscordConfig()) { } | : this(new DiscordConfig()) { } | ||||
| /// <summary> Creates a new discord client using only the REST API. </summary> | |||||
| public DiscordClient(DiscordConfig config) | public DiscordClient(DiscordConfig config) | ||||
| { | { | ||||
| _log = new LogManager(config.LogLevel); | _log = new LogManager(config.LogLevel); | ||||
| @@ -40,10 +42,12 @@ namespace Discord | |||||
| _connectionLock = new SemaphoreSlim(1, 1); | _connectionLock = new SemaphoreSlim(1, 1); | ||||
| _requestQueue = new RequestQueue(); | _requestQueue = new RequestQueue(); | ||||
| //TODO: Is there any better way to do this WebSocketProvider access? | |||||
| ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); | ApiClient = new API.DiscordApiClient(config.RestClientProvider, (config as DiscordSocketConfig)?.WebSocketProvider, requestQueue: _requestQueue); | ||||
| ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | ApiClient.SentRequest += async (method, endpoint, millis) => await _log.Verbose("Rest", $"{method} {endpoint}: {millis} ms").ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Login(TokenType tokenType, string token, bool validateToken = true) | public async Task Login(TokenType tokenType, string token, bool validateToken = true) | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -89,6 +93,7 @@ namespace Discord | |||||
| } | } | ||||
| protected virtual Task OnLogin() => Task.CompletedTask; | protected virtual Task OnLogin() => Task.CompletedTask; | ||||
| /// <inheritdoc /> | |||||
| public async Task Logout() | public async Task Logout() | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -115,12 +120,14 @@ namespace Discord | |||||
| } | } | ||||
| protected virtual Task OnLogout() => Task.CompletedTask; | protected virtual Task OnLogout() => Task.CompletedTask; | ||||
| /// <inheritdoc /> | |||||
| public async Task<IReadOnlyCollection<IConnection>> GetConnections() | public async Task<IReadOnlyCollection<IConnection>> GetConnections() | ||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false); | ||||
| return models.Select(x => new Connection(x)).ToImmutableArray(); | return models.Select(x => new Connection(x)).ToImmutableArray(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IChannel> GetChannel(ulong id) | public virtual async Task<IChannel> GetChannel(ulong id) | ||||
| { | { | ||||
| var model = await ApiClient.GetChannel(id).ConfigureAwait(false); | var model = await ApiClient.GetChannel(id).ConfigureAwait(false); | ||||
| @@ -140,12 +147,14 @@ namespace Discord | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels() | public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels() | ||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false); | ||||
| return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); | return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IInvite> GetInvite(string inviteIdOrXkcd) | public virtual async Task<IInvite> GetInvite(string inviteIdOrXkcd) | ||||
| { | { | ||||
| var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); | var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false); | ||||
| @@ -154,6 +163,7 @@ namespace Discord | |||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IGuild> GetGuild(ulong id) | public virtual async Task<IGuild> GetGuild(ulong id) | ||||
| { | { | ||||
| var model = await ApiClient.GetGuild(id).ConfigureAwait(false); | var model = await ApiClient.GetGuild(id).ConfigureAwait(false); | ||||
| @@ -161,6 +171,7 @@ namespace Discord | |||||
| return new Guild(this, model); | return new Guild(this, model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<GuildEmbed?> GetGuildEmbed(ulong id) | public virtual async Task<GuildEmbed?> GetGuildEmbed(ulong id) | ||||
| { | { | ||||
| var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); | var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false); | ||||
| @@ -168,12 +179,14 @@ namespace Discord | |||||
| return new GuildEmbed(model); | return new GuildEmbed(model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds() | public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds() | ||||
| { | { | ||||
| var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); | var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false); | ||||
| return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); | return models.Select(x => new UserGuild(this, x)).ToImmutableArray(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) | public virtual async Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null) | ||||
| { | { | ||||
| var args = new CreateGuildParams(); | var args = new CreateGuildParams(); | ||||
| @@ -181,6 +194,7 @@ namespace Discord | |||||
| return new Guild(this, model); | return new Guild(this, model); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IUser> GetUser(ulong id) | public virtual async Task<IUser> GetUser(ulong id) | ||||
| { | { | ||||
| var model = await ApiClient.GetUser(id).ConfigureAwait(false); | var model = await ApiClient.GetUser(id).ConfigureAwait(false); | ||||
| @@ -188,6 +202,7 @@ namespace Discord | |||||
| return new User(this, model); | return new User(this, model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IUser> GetUser(string username, string discriminator) | public virtual async Task<IUser> GetUser(string username, string discriminator) | ||||
| { | { | ||||
| var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); | var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false); | ||||
| @@ -195,6 +210,7 @@ namespace Discord | |||||
| return new User(this, model); | return new User(this, model); | ||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<ISelfUser> GetCurrentUser() | public virtual async Task<ISelfUser> GetCurrentUser() | ||||
| { | { | ||||
| var user = _currentUser; | var user = _currentUser; | ||||
| @@ -206,17 +222,20 @@ namespace Discord | |||||
| } | } | ||||
| return user; | return user; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit) | public virtual async Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit) | ||||
| { | { | ||||
| var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); | var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false); | ||||
| return models.Select(x => new User(this, x)).ToImmutableArray(); | return models.Select(x => new User(this, x)).ToImmutableArray(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions() | public virtual async Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions() | ||||
| { | { | ||||
| var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | ||||
| return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); | return models.Select(x => new VoiceRegion(x)).ToImmutableArray(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public virtual async Task<IVoiceRegion> GetVoiceRegion(string id) | public virtual async Task<IVoiceRegion> GetVoiceRegion(string id) | ||||
| { | { | ||||
| var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false); | ||||
| @@ -228,6 +247,7 @@ namespace Discord | |||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public void Dispose() => Dispose(true); | public void Dispose() => Dispose(true); | ||||
| ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; | ||||
| @@ -1,5 +1,4 @@ | |||||
| using Discord.API; | |||||
| using Discord.API.Gateway; | |||||
| using Discord.API.Gateway; | |||||
| using Discord.Data; | using Discord.Data; | ||||
| using Discord.Extensions; | using Discord.Extensions; | ||||
| using Discord.Logging; | using Discord.Logging; | ||||
| @@ -11,19 +10,23 @@ using System; | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Diagnostics; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| //TODO: Remove unnecessary `as` casts | //TODO: Remove unnecessary `as` casts | ||||
| //TODO: Add docstrings | |||||
| //TODO: Add event docstrings | |||||
| //TODO: Add reconnect logic (+ensure the heartbeat task shuts down) | |||||
| //TODO: Add resume logic | |||||
| public class DiscordSocketClient : DiscordClient, IDiscordClient | public class DiscordSocketClient : DiscordClient, IDiscordClient | ||||
| { | { | ||||
| public event Func<Task> Connected, Disconnected; | public event Func<Task> Connected, Disconnected; | ||||
| public event Func<Task> Ready; | public event Func<Task> Ready; | ||||
| //public event Func<Channel> VoiceConnected, VoiceDisconnected; | //public event Func<Channel> VoiceConnected, VoiceDisconnected; | ||||
| /*public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed; | |||||
| public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed; | |||||
| public event Func<IChannel, IChannel, Task> ChannelUpdated; | public event Func<IChannel, IChannel, Task> ChannelUpdated; | ||||
| public event Func<IMessage, Task> MessageReceived, MessageDeleted; | public event Func<IMessage, Task> MessageReceived, MessageDeleted; | ||||
| public event Func<IMessage, IMessage, Task> MessageUpdated; | public event Func<IMessage, IMessage, Task> MessageUpdated; | ||||
| @@ -34,7 +37,8 @@ namespace Discord | |||||
| public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | public event Func<IUser, Task> UserJoined, UserLeft, UserBanned, UserUnbanned; | ||||
| public event Func<IUser, IUser, Task> UserUpdated; | public event Func<IUser, IUser, Task> UserUpdated; | ||||
| public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated; | public event Func<ISelfUser, ISelfUser, Task> CurrentUserUpdated; | ||||
| public event Func<IChannel, IUser, Task> UserIsTyping;*/ | |||||
| public event Func<IChannel, IUser, Task> UserIsTyping; | |||||
| public event Func<int, Task> LatencyUpdated; | |||||
| private readonly ConcurrentQueue<ulong> _largeGuilds; | private readonly ConcurrentQueue<ulong> _largeGuilds; | ||||
| private readonly Logger _gatewayLogger; | private readonly Logger _gatewayLogger; | ||||
| @@ -44,13 +48,21 @@ namespace Discord | |||||
| private readonly bool _enablePreUpdateEvents; | private readonly bool _enablePreUpdateEvents; | ||||
| private readonly int _largeThreshold; | private readonly int _largeThreshold; | ||||
| private readonly int _totalShards; | private readonly int _totalShards; | ||||
| private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | |||||
| private string _sessionId; | private string _sessionId; | ||||
| private int _lastSeq; | |||||
| private ImmutableDictionary<string, VoiceRegion> _voiceRegions; | |||||
| private TaskCompletionSource<bool> _connectTask; | private TaskCompletionSource<bool> _connectTask; | ||||
| private CancellationTokenSource _heartbeatCancelToken; | |||||
| private Task _heartbeatTask; | |||||
| private long _heartbeatTime; | |||||
| /// <summary> Gets the shard if of this client. </summary> | |||||
| public int ShardId { get; } | public int ShardId { get; } | ||||
| /// <summary> Gets the current connection state of this client. </summary> | |||||
| public ConnectionState ConnectionState { get; private set; } | public ConnectionState ConnectionState { get; private set; } | ||||
| public IWebSocketClient GatewaySocket { get; private set; } | |||||
| /// <summary> Gets the estimated round-trip latency to the gateway server. </summary> | |||||
| public int Latency { get; private set; } | |||||
| internal IWebSocketClient GatewaySocket { get; private set; } | |||||
| internal int MessageCacheSize { get; private set; } | internal int MessageCacheSize { get; private set; } | ||||
| //internal bool UsePermissionCache { get; private set; } | //internal bool UsePermissionCache { get; private set; } | ||||
| internal DataStore DataStore { get; private set; } | internal DataStore DataStore { get; private set; } | ||||
| @@ -61,7 +73,7 @@ namespace Discord | |||||
| get | get | ||||
| { | { | ||||
| var guilds = DataStore.Guilds; | var guilds = DataStore.Guilds; | ||||
| return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds); | |||||
| return guilds.ToReadOnlyCollection(guilds); | |||||
| } | } | ||||
| } | } | ||||
| internal IReadOnlyCollection<CachedDMChannel> DMChannels | internal IReadOnlyCollection<CachedDMChannel> DMChannels | ||||
| @@ -69,13 +81,15 @@ namespace Discord | |||||
| get | get | ||||
| { | { | ||||
| var users = DataStore.Users; | var users = DataStore.Users; | ||||
| return users.Select(x => (x as CachedPublicUser).DMChannel).Where(x => x != null).ToReadOnlyCollection(users); | |||||
| return users.Select(x => x.DMChannel).Where(x => x != null).ToReadOnlyCollection(users); | |||||
| } | } | ||||
| } | } | ||||
| internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | internal IReadOnlyCollection<VoiceRegion> VoiceRegions => _voiceRegions.ToReadOnlyCollection(); | ||||
| /// <summary> Creates a new discord client using the REST and WebSocket APIs. </summary> | |||||
| public DiscordSocketClient() | public DiscordSocketClient() | ||||
| : this(new DiscordSocketConfig()) { } | : this(new DiscordSocketConfig()) { } | ||||
| /// <summary> Creates a new discord client using the REST and WebSocket APIs. </summary> | |||||
| public DiscordSocketClient(DiscordSocketConfig config) | public DiscordSocketClient(DiscordSocketConfig config) | ||||
| : base(config) | : base(config) | ||||
| { | { | ||||
| @@ -117,6 +131,7 @@ namespace Discord | |||||
| _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | _voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>(); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Connect() | public async Task Connect() | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -135,6 +150,7 @@ namespace Discord | |||||
| try | try | ||||
| { | { | ||||
| _connectTask = new TaskCompletionSource<bool>(); | _connectTask = new TaskCompletionSource<bool>(); | ||||
| _heartbeatCancelToken = new CancellationTokenSource(); | |||||
| await ApiClient.Connect().ConfigureAwait(false); | await ApiClient.Connect().ConfigureAwait(false); | ||||
| await _connectTask.Task.ConfigureAwait(false); | await _connectTask.Task.ConfigureAwait(false); | ||||
| @@ -148,6 +164,7 @@ namespace Discord | |||||
| await Connected.Raise().ConfigureAwait(false); | await Connected.Raise().ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public async Task Disconnect() | public async Task Disconnect() | ||||
| { | { | ||||
| await _connectionLock.WaitAsync().ConfigureAwait(false); | await _connectionLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -165,13 +182,15 @@ namespace Discord | |||||
| ConnectionState = ConnectionState.Disconnecting; | ConnectionState = ConnectionState.Disconnecting; | ||||
| await ApiClient.Disconnect().ConfigureAwait(false); | await ApiClient.Disconnect().ConfigureAwait(false); | ||||
| await _heartbeatTask.ConfigureAwait(false); | |||||
| while (_largeGuilds.TryDequeue(out guildId)) { } | while (_largeGuilds.TryDequeue(out guildId)) { } | ||||
| ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
| await Disconnected.Raise().ConfigureAwait(false); | await Disconnected.Raise().ConfigureAwait(false); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<IVoiceRegion> GetVoiceRegion(string id) | public override Task<IVoiceRegion> GetVoiceRegion(string id) | ||||
| { | { | ||||
| VoiceRegion region; | VoiceRegion region; | ||||
| @@ -180,6 +199,7 @@ namespace Discord | |||||
| return Task.FromResult<IVoiceRegion>(null); | return Task.FromResult<IVoiceRegion>(null); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<IGuild> GetGuild(ulong id) | public override Task<IGuild> GetGuild(ulong id) | ||||
| { | { | ||||
| return Task.FromResult<IGuild>(DataStore.GetGuild(id)); | return Task.FromResult<IGuild>(DataStore.GetGuild(id)); | ||||
| @@ -192,7 +212,7 @@ namespace Discord | |||||
| if (model.Unavailable != true) | if (model.Unavailable != true) | ||||
| { | { | ||||
| for (int i = 0; i < model.Channels.Length; i++) | for (int i = 0; i < model.Channels.Length; i++) | ||||
| AddCachedChannel(model.Channels[i], dataStore); | |||||
| AddCachedChannel(guild, model.Channels[i], dataStore); | |||||
| } | } | ||||
| dataStore.AddGuild(guild); | dataStore.AddGuild(guild); | ||||
| if (model.Large) | if (model.Large) | ||||
| @@ -203,7 +223,7 @@ namespace Discord | |||||
| { | { | ||||
| dataStore = dataStore ?? DataStore; | dataStore = dataStore ?? DataStore; | ||||
| var guild = dataStore.RemoveGuild(id) as CachedGuild; | |||||
| var guild = dataStore.RemoveGuild(id); | |||||
| foreach (var channel in guild.Channels) | foreach (var channel in guild.Channels) | ||||
| guild.RemoveCachedChannel(channel.Id); | guild.RemoveCachedChannel(channel.Id); | ||||
| foreach (var user in guild.Members) | foreach (var user in guild.Members) | ||||
| @@ -211,25 +231,25 @@ namespace Discord | |||||
| return guild; | return guild; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<IChannel> GetChannel(ulong id) | public override Task<IChannel> GetChannel(ulong id) | ||||
| { | { | ||||
| return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | return Task.FromResult<IChannel>(DataStore.GetChannel(id)); | ||||
| } | } | ||||
| internal ICachedChannel AddCachedChannel(API.Channel model, DataStore dataStore = null) | |||||
| internal ICachedGuildChannel AddCachedChannel(CachedGuild guild, API.Channel model, DataStore dataStore = null) | |||||
| { | { | ||||
| dataStore = dataStore ?? DataStore; | dataStore = dataStore ?? DataStore; | ||||
| ICachedChannel channel; | |||||
| if (model.IsPrivate) | |||||
| { | |||||
| var recipient = AddCachedUser(model.Recipient, dataStore); | |||||
| channel = recipient.SetDMChannel(model); | |||||
| } | |||||
| else | |||||
| { | |||||
| var guild = dataStore.GetGuild(model.GuildId.Value); | |||||
| channel = guild.AddCachedChannel(model); | |||||
| } | |||||
| var channel = guild.AddCachedChannel(model); | |||||
| dataStore.AddChannel(channel); | |||||
| return channel; | |||||
| } | |||||
| internal CachedDMChannel AddCachedDMChannel(API.Channel model, DataStore dataStore = null) | |||||
| { | |||||
| dataStore = dataStore ?? DataStore; | |||||
| var recipient = AddCachedUser(model.Recipient, dataStore); | |||||
| var channel = recipient.AddDMChannel(model); | |||||
| dataStore.AddChannel(channel); | dataStore.AddChannel(channel); | ||||
| return channel; | return channel; | ||||
| } | } | ||||
| @@ -237,8 +257,8 @@ namespace Discord | |||||
| { | { | ||||
| dataStore = dataStore ?? DataStore; | dataStore = dataStore ?? DataStore; | ||||
| //TODO: C#7 | |||||
| var channel = DataStore.RemoveChannel(id) as ICachedChannel; | |||||
| //TODO: C#7 Typeswitch Candidate | |||||
| var channel = DataStore.RemoveChannel(id); | |||||
| var guildChannel = channel as ICachedGuildChannel; | var guildChannel = channel as ICachedGuildChannel; | ||||
| if (guildChannel != null) | if (guildChannel != null) | ||||
| @@ -258,10 +278,12 @@ namespace Discord | |||||
| return null; | return null; | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<IUser> GetUser(ulong id) | public override Task<IUser> GetUser(ulong id) | ||||
| { | { | ||||
| return Task.FromResult<IUser>(DataStore.GetUser(id)); | return Task.FromResult<IUser>(DataStore.GetUser(id)); | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| public override Task<IUser> GetUser(string username, string discriminator) | public override Task<IUser> GetUser(string username, string discriminator) | ||||
| { | { | ||||
| return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); | ||||
| @@ -270,7 +292,7 @@ namespace Discord | |||||
| { | { | ||||
| dataStore = dataStore ?? DataStore; | dataStore = dataStore ?? DataStore; | ||||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)) as CachedPublicUser; | |||||
| var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(this, model)); | |||||
| user.AddRef(); | user.AddRef(); | ||||
| return user; | return user; | ||||
| } | } | ||||
| @@ -278,22 +300,34 @@ namespace Discord | |||||
| { | { | ||||
| dataStore = dataStore ?? DataStore; | dataStore = dataStore ?? DataStore; | ||||
| var user = dataStore.GetUser(id) as CachedPublicUser; | |||||
| var user = dataStore.GetUser(id); | |||||
| user.RemoveRef(); | user.RemoveRef(); | ||||
| return user; | return user; | ||||
| } | } | ||||
| private async Task ProcessMessage(GatewayOpCode opCode, string type, JToken payload) | |||||
| private async Task ProcessMessage(GatewayOpCode opCode, int? seq, string type, object payload) | |||||
| { | { | ||||
| if (seq != null) | |||||
| _lastSeq = seq.Value; | |||||
| try | try | ||||
| { | { | ||||
| switch (opCode) | switch (opCode) | ||||
| { | { | ||||
| case GatewayOpCode.Hello: | case GatewayOpCode.Hello: | ||||
| { | { | ||||
| var data = payload.ToObject<HelloEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<HelloEvent>(_serializer); | |||||
| await ApiClient.SendIdentify().ConfigureAwait(false); | await ApiClient.SendIdentify().ConfigureAwait(false); | ||||
| _heartbeatTask = RunHeartbeat(data.HeartbeatInterval, _heartbeatCancelToken.Token); | |||||
| } | |||||
| break; | |||||
| case GatewayOpCode.HeartbeatAck: | |||||
| { | |||||
| var latency = (int)(Environment.TickCount - _heartbeatTime); | |||||
| await _gatewayLogger.Debug($"Latency: {latency} ms").ConfigureAwait(false); | |||||
| Latency = latency; | |||||
| await LatencyUpdated.Raise(latency).ConfigureAwait(false); | |||||
| } | } | ||||
| break; | break; | ||||
| case GatewayOpCode.Dispatch: | case GatewayOpCode.Dispatch: | ||||
| @@ -303,15 +337,15 @@ namespace Discord | |||||
| case "READY": | case "READY": | ||||
| { | { | ||||
| //TODO: Make downloading large guilds optional | //TODO: Make downloading large guilds optional | ||||
| var data = payload.ToObject<ReadyEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<ReadyEvent>(_serializer); | |||||
| var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); | var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); | ||||
| _currentUser = new CachedSelfUser(this,data.User); | |||||
| _currentUser = new CachedSelfUser(this, data.User); | |||||
| for (int i = 0; i < data.Guilds.Length; i++) | for (int i = 0; i < data.Guilds.Length; i++) | ||||
| AddCachedGuild(data.Guilds[i], dataStore); | AddCachedGuild(data.Guilds[i], dataStore); | ||||
| for (int i = 0; i < data.PrivateChannels.Length; i++) | for (int i = 0; i < data.PrivateChannels.Length; i++) | ||||
| AddCachedChannel(data.PrivateChannels[i], dataStore); | |||||
| AddCachedDMChannel(data.PrivateChannels[i], dataStore); | |||||
| _sessionId = data.SessionId; | _sessionId = data.SessionId; | ||||
| DataStore = dataStore; | DataStore = dataStore; | ||||
| @@ -323,9 +357,9 @@ namespace Discord | |||||
| break; | break; | ||||
| //Guilds | //Guilds | ||||
| /*case "GUILD_CREATE": | |||||
| case "GUILD_CREATE": | |||||
| { | { | ||||
| var data = payload.ToObject<ExtendedGuild>(_serializer); | |||||
| var data = (payload as JToken).ToObject<ExtendedGuild>(_serializer); | |||||
| var guild = new CachedGuild(this, data); | var guild = new CachedGuild(this, data); | ||||
| DataStore.AddGuild(guild); | DataStore.AddGuild(guild); | ||||
| @@ -342,12 +376,12 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_UPDATE": | case "GUILD_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Guild>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Guild>(_serializer); | |||||
| var guild = DataStore.GetGuild(data.Id); | var guild = DataStore.GetGuild(data.Id); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| var before = _enablePreUpdateEvents ? guild.Clone() : null; | var before = _enablePreUpdateEvents ? guild.Clone() : null; | ||||
| guild.Update(data); | |||||
| guild.Update(data, UpdateSource.WebSocket); | |||||
| await GuildUpdated.Raise(before, guild); | await GuildUpdated.Raise(before, guild); | ||||
| } | } | ||||
| else | else | ||||
| @@ -356,7 +390,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_DELETE": | case "GUILD_DELETE": | ||||
| { | { | ||||
| var data = payload.ToObject<ExtendedGuild>(_serializer); | |||||
| var data = (payload as JToken).ToObject<ExtendedGuild>(_serializer); | |||||
| var guild = DataStore.RemoveGuild(data.Id); | var guild = DataStore.RemoveGuild(data.Id); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -375,34 +409,34 @@ namespace Discord | |||||
| //Channels | //Channels | ||||
| case "CHANNEL_CREATE": | case "CHANNEL_CREATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||||
| IChannel channel = null; | |||||
| ICachedChannel channel = null; | |||||
| if (data.GuildId != null) | if (data.GuildId != null) | ||||
| { | { | ||||
| var guild = GetCachedGuild(data.GuildId.Value); | |||||
| var guild = DataStore.GetGuild(data.GuildId.Value); | |||||
| if (guild != null) | if (guild != null) | ||||
| channel = guild.AddCachedChannel(data.Id, true); | |||||
| { | |||||
| channel = guild.AddCachedChannel(data); | |||||
| DataStore.AddChannel(channel); | |||||
| } | |||||
| else | else | ||||
| await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); | await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild."); | ||||
| } | } | ||||
| else | else | ||||
| channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id); | |||||
| channel = AddCachedDMChannel(data); | |||||
| if (channel != null) | if (channel != null) | ||||
| { | |||||
| channel.Update(data); | |||||
| await ChannelCreated.Raise(channel); | await ChannelCreated.Raise(channel); | ||||
| } | |||||
| } | } | ||||
| break; | break; | ||||
| case "CHANNEL_UPDATE": | case "CHANNEL_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| var channel = DataStore.GetChannel(data.Id) as Channel; | |||||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||||
| var channel = DataStore.GetChannel(data.Id); | |||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| var before = _enablePreUpdateEvents ? channel.Clone() : null; | var before = _enablePreUpdateEvents ? channel.Clone() : null; | ||||
| channel.Update(data); | |||||
| channel.Update(data, UpdateSource.WebSocket); | |||||
| await ChannelUpdated.Raise(before, channel); | await ChannelUpdated.Raise(before, channel); | ||||
| } | } | ||||
| else | else | ||||
| @@ -411,7 +445,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "CHANNEL_DELETE": | case "CHANNEL_DELETE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Channel>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Channel>(_serializer); | |||||
| var channel = RemoveCachedChannel(data.Id); | var channel = RemoveCachedChannel(data.Id); | ||||
| if (channel != null) | if (channel != null) | ||||
| await ChannelDestroyed.Raise(channel); | await ChannelDestroyed.Raise(channel); | ||||
| @@ -421,9 +455,9 @@ namespace Discord | |||||
| break; | break; | ||||
| //Members | //Members | ||||
| case "GUILD_MEMBER_ADD": | |||||
| /*case "GUILD_MEMBER_ADD": | |||||
| { | { | ||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | var guild = GetGuild(data.GuildId.Value); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -438,7 +472,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_UPDATE": | case "GUILD_MEMBER_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | var guild = GetGuild(data.GuildId.Value); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -458,7 +492,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBER_REMOVE": | case "GUILD_MEMBER_REMOVE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.GuildMember>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.GuildMember>(_serializer); | |||||
| var guild = GetGuild(data.GuildId.Value); | var guild = GetGuild(data.GuildId.Value); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -479,7 +513,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_MEMBERS_CHUNK": | case "GUILD_MEMBERS_CHUNK": | ||||
| { | { | ||||
| var data = payload.ToObject<GuildMembersChunkEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<GuildMembersChunkEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | var guild = GetCachedGuild(data.GuildId); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -498,9 +532,9 @@ namespace Discord | |||||
| break; | break; | ||||
| //Roles | //Roles | ||||
| case "GUILD_ROLE_CREATE": | |||||
| /*case "GUILD_ROLE_CREATE": | |||||
| { | { | ||||
| var data = payload.ToObject<GuildRoleCreateEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<GuildRoleCreateEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | var guild = GetCachedGuild(data.GuildId); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -514,7 +548,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_UPDATE": | case "GUILD_ROLE_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<GuildRoleUpdateEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<GuildRoleUpdateEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | var guild = GetCachedGuild(data.GuildId); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -534,8 +568,8 @@ namespace Discord | |||||
| break; | break; | ||||
| case "GUILD_ROLE_DELETE": | case "GUILD_ROLE_DELETE": | ||||
| { | { | ||||
| var data = payload.ToObject<GuildRoleDeleteEvent>(_serializer); | |||||
| var guild = DataStore.GetGuild(data.GuildId) as CachedGuild; | |||||
| var data = (payload as JToken).ToObject<GuildRoleDeleteEvent>(_serializer); | |||||
| var guild = DataStore.GetGuild(data.GuildId); | |||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| var role = guild.RemoveRole(data.RoleId); | var role = guild.RemoveRole(data.RoleId); | ||||
| @@ -552,7 +586,7 @@ namespace Discord | |||||
| //Bans | //Bans | ||||
| case "GUILD_BAN_ADD": | case "GUILD_BAN_ADD": | ||||
| { | { | ||||
| var data = payload.ToObject<GuildBanEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer); | |||||
| var guild = GetCachedGuild(data.GuildId); | var guild = GetCachedGuild(data.GuildId); | ||||
| if (guild != null) | if (guild != null) | ||||
| await UserBanned.Raise(new User(this, data)); | await UserBanned.Raise(new User(this, data)); | ||||
| @@ -574,8 +608,7 @@ namespace Discord | |||||
| //Messages | //Messages | ||||
| case "MESSAGE_CREATE": | case "MESSAGE_CREATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Message>(_serializer); | |||||
| var channel = DataStore.GetChannel(data.ChannelId); | var channel = DataStore.GetChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -599,7 +632,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "MESSAGE_UPDATE": | case "MESSAGE_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Message>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | var channel = GetCachedChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -614,7 +647,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "MESSAGE_DELETE": | case "MESSAGE_DELETE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Message>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Message>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | var channel = GetCachedChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -629,7 +662,7 @@ namespace Discord | |||||
| //Statuses | //Statuses | ||||
| case "PRESENCE_UPDATE": | case "PRESENCE_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.Presence>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.Presence>(_serializer); | |||||
| User user; | User user; | ||||
| Guild guild; | Guild guild; | ||||
| if (data.GuildId == null) | if (data.GuildId == null) | ||||
| @@ -664,7 +697,7 @@ namespace Discord | |||||
| break; | break; | ||||
| case "TYPING_START": | case "TYPING_START": | ||||
| { | { | ||||
| var data = payload.ToObject<TypingStartEvent>(_serializer); | |||||
| var data = (payload as JToken).ToObject<TypingStartEvent>(_serializer); | |||||
| var channel = GetCachedChannel(data.ChannelId); | var channel = GetCachedChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -683,7 +716,7 @@ namespace Discord | |||||
| //Voice | //Voice | ||||
| case "VOICE_STATE_UPDATE": | case "VOICE_STATE_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<API.VoiceState>(_serializer); | |||||
| var data = (payload as JToken).ToObject<API.VoiceState>(_serializer); | |||||
| var guild = GetGuild(data.GuildId); | var guild = GetGuild(data.GuildId); | ||||
| if (guild != null) | if (guild != null) | ||||
| { | { | ||||
| @@ -708,7 +741,7 @@ namespace Discord | |||||
| //Settings | //Settings | ||||
| case "USER_UPDATE": | case "USER_UPDATE": | ||||
| { | { | ||||
| var data = payload.ToObject<SelfUser>(_serializer); | |||||
| var data = (payload as JToken).ToObject<SelfUser>(_serializer); | |||||
| if (data.Id == CurrentUser.Id) | if (data.Id == CurrentUser.Id) | ||||
| { | { | ||||
| var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; | var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null; | ||||
| @@ -746,5 +779,17 @@ namespace Discord | |||||
| } | } | ||||
| await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); | await _gatewayLogger.Debug($"Received {opCode}{(type != null ? $" ({type})" : "")}").ConfigureAwait(false); | ||||
| } | } | ||||
| private async Task RunHeartbeat(int intervalMillis, CancellationToken cancelToken) | |||||
| { | |||||
| var state = ConnectionState; | |||||
| while (state == ConnectionState.Connecting || state == ConnectionState.Connected) | |||||
| { | |||||
| //if (_heartbeatTime != 0) //TODO: Connection lost, reconnect | |||||
| _heartbeatTime = Environment.TickCount; | |||||
| await ApiClient.SendHeartbeat(_lastSeq).ConfigureAwait(false); | |||||
| await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -26,7 +26,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| protected void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -30,7 +30,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| protected virtual void Update(Model model, UpdateSource source) | |||||
| public virtual void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -22,7 +22,7 @@ namespace Discord | |||||
| : base(guild, model) | : base(guild, model) | ||||
| { | { | ||||
| } | } | ||||
| protected override void Update(Model model, UpdateSource source) | |||||
| public override void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -17,7 +17,7 @@ namespace Discord | |||||
| : base(guild, model) | : base(guild, model) | ||||
| { | { | ||||
| } | } | ||||
| protected override void Update(Model model, UpdateSource source) | |||||
| public override void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -31,7 +31,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| private void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -43,7 +43,7 @@ namespace Discord | |||||
| ExpireGracePeriod = model.ExpireGracePeriod; | ExpireGracePeriod = model.ExpireGracePeriod; | ||||
| SyncedAt = model.SyncedAt; | SyncedAt = model.SyncedAt; | ||||
| Role = Guild.GetRole(model.RoleId) as Role; | |||||
| Role = Guild.GetRole(model.RoleId); | |||||
| User = new User(Discord, model.User); | User = new User(Discord, model.User); | ||||
| } | } | ||||
| @@ -23,7 +23,7 @@ namespace Discord | |||||
| Discord = discord; | Discord = discord; | ||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| private void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -26,7 +26,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| protected void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -15,7 +15,7 @@ namespace Discord | |||||
| { | { | ||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| private void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -36,7 +36,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| private void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -130,27 +130,14 @@ namespace Discord | |||||
| perms = channel.GetPermissionOverwrite(user); | perms = channel.GetPermissionOverwrite(user); | ||||
| if (perms != null) | if (perms != null) | ||||
| resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; | resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; | ||||
| #if CSHARP7 | |||||
| switch (channel) | |||||
| { | |||||
| case ITextChannel _: | |||||
| if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) | |||||
| resolvedPermissions = 0; //No read permission on a text channel removes all other permissions | |||||
| break; | |||||
| case IVoiceChannel _: | |||||
| if (!GetValue(resolvedPermissions, ChannelPermission.Connect)) | |||||
| resolvedPermissions = 0; //No read permission on a text channel removes all other permissions | |||||
| break; | |||||
| } | |||||
| #else | |||||
| //TODO: C# Typeswitch candidate | |||||
| var textChannel = channel as ITextChannel; | var textChannel = channel as ITextChannel; | ||||
| var voiceChannel = channel as IVoiceChannel; | var voiceChannel = channel as IVoiceChannel; | ||||
| if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) | if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) | ||||
| resolvedPermissions = 0; //No read permission on a text channel removes all other permissions | resolvedPermissions = 0; //No read permission on a text channel removes all other permissions | ||||
| else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) | else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) | ||||
| resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions | resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions | ||||
| #endif | |||||
| resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) | resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) | ||||
| } | } | ||||
| @@ -39,7 +39,7 @@ namespace Discord | |||||
| Update(model, UpdateSource.Creation); | Update(model, UpdateSource.Creation); | ||||
| } | } | ||||
| private void Update(Model model, UpdateSource source) | |||||
| public void Update(Model model, UpdateSource source) | |||||
| { | { | ||||
| if (source == UpdateSource.Rest && IsAttached) return; | if (source == UpdateSource.Rest && IsAttached) return; | ||||
| @@ -49,9 +49,9 @@ namespace Discord | |||||
| Nickname = model.Nick; | Nickname = model.Nick; | ||||
| var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1); | var roles = ImmutableArray.CreateBuilder<Role>(model.Roles.Length + 1); | ||||
| roles.Add(Guild.EveryoneRole as Role); | |||||
| roles.Add(Guild.EveryoneRole); | |||||
| for (int i = 0; i < model.Roles.Length; i++) | for (int i = 0; i < model.Roles.Length; i++) | ||||
| roles.Add(Guild.GetRole(model.Roles[i]) as Role); | |||||
| roles.Add(Guild.GetRole(model.Roles[i])); | |||||
| Roles = roles.ToImmutable(); | Roles = roles.ToImmutable(); | ||||
| GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); | GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this)); | ||||
| @@ -89,7 +89,7 @@ namespace Discord | |||||
| if (args.Nickname.IsSpecified) | if (args.Nickname.IsSpecified) | ||||
| Nickname = args.Nickname.Value ?? ""; | Nickname = args.Nickname.Value ?? ""; | ||||
| if (args.Roles.IsSpecified) | if (args.Roles.IsSpecified) | ||||
| Roles = args.Roles.Value.Select(x => Guild.GetRole(x) as Role).Where(x => x != null).ToImmutableArray(); | |||||
| Roles = args.Roles.Value.Select(x => Guild.GetRole(x)).Where(x => x != null).ToImmutableArray(); | |||||
| } | } | ||||
| } | } | ||||
| public async Task Kick() | public async Task Kick() | ||||
| @@ -65,6 +65,7 @@ namespace Discord | |||||
| public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; | public CachedDMChannel Clone() => MemberwiseClone() as CachedDMChannel; | ||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | |||||
| ICachedChannel ICachedChannel.Clone() => Clone(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -153,6 +153,8 @@ namespace Discord | |||||
| return null; | return null; | ||||
| } | } | ||||
| public CachedGuild Clone() => MemberwiseClone() as CachedGuild; | |||||
| new internal ICachedGuildChannel ToChannel(ChannelModel model) | new internal ICachedGuildChannel ToChannel(ChannelModel model) | ||||
| { | { | ||||
| switch (model.Type) | switch (model.Type) | ||||
| @@ -12,5 +12,7 @@ namespace Discord | |||||
| : base(guild, user, model) | : base(guild, user, model) | ||||
| { | { | ||||
| } | } | ||||
| public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser; | |||||
| } | } | ||||
| } | } | ||||
| @@ -16,7 +16,7 @@ namespace Discord | |||||
| { | { | ||||
| } | } | ||||
| public CachedDMChannel SetDMChannel(ChannelModel model) | |||||
| public CachedDMChannel AddDMChannel(ChannelModel model) | |||||
| { | { | ||||
| lock (this) | lock (this) | ||||
| { | { | ||||
| @@ -69,5 +69,6 @@ namespace Discord | |||||
| IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | ||||
| IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); | IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); | ||||
| ICachedChannel ICachedChannel.Clone() => Clone(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -34,5 +34,7 @@ namespace Discord | |||||
| } | } | ||||
| public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; | public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; | ||||
| ICachedChannel ICachedChannel.Clone() => Clone(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,11 @@ | |||||
| namespace Discord | |||||
| using Model = Discord.API.Channel; | |||||
| namespace Discord | |||||
| { | { | ||||
| internal interface ICachedChannel : IChannel, ICachedEntity<ulong> | internal interface ICachedChannel : IChannel, ICachedEntity<ulong> | ||||
| { | { | ||||
| void Update(Model model, UpdateSource source); | |||||
| ICachedChannel Clone(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -6,6 +6,7 @@ namespace Discord.Extensions | |||||
| internal static class EventExtensions | internal static class EventExtensions | ||||
| { | { | ||||
| //TODO: Optimize these for if there is only 1 subscriber (can we do this?) | //TODO: Optimize these for if there is only 1 subscriber (can we do this?) | ||||
| //TODO: Could we maintain our own list instead of generating one on every invocation? | |||||
| public static async Task Raise(this Func<Task> eventHandler) | public static async Task Raise(this Func<Task> eventHandler) | ||||
| { | { | ||||
| var subscriptions = eventHandler?.GetInvocationList(); | var subscriptions = eventHandler?.GetInvocationList(); | ||||
| @@ -42,5 +43,14 @@ namespace Discord.Extensions | |||||
| await (subscriptions[i] as Func<T1, T2, T3, Task>).Invoke(arg1, arg2, arg3).ConfigureAwait(false); | await (subscriptions[i] as Func<T1, T2, T3, Task>).Invoke(arg1, arg2, arg3).ConfigureAwait(false); | ||||
| } | } | ||||
| } | } | ||||
| public static async Task Raise<T1, T2, T3, T4>(this Func<T1, T2, T3, T4, Task> eventHandler, T1 arg1, T2 arg2, T3 arg3, T4 arg4) | |||||
| { | |||||
| var subscriptions = eventHandler?.GetInvocationList(); | |||||
| if (subscriptions != null) | |||||
| { | |||||
| for (int i = 0; i < subscriptions.Length; i++) | |||||
| await (subscriptions[i] as Func<T1, T2, T3, T4, Task>).Invoke(arg1, arg2, arg3, arg4).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -92,25 +92,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| foreach (var p in multipartParams) | foreach (var p in multipartParams) | ||||
| { | { | ||||
| #if CSHARP7 | |||||
| switch (p.Value) | |||||
| { | |||||
| case string value: | |||||
| content.Add(new StringContent(value), p.Key); | |||||
| break; | |||||
| case byte[] value: | |||||
| content.Add(new ByteArrayContent(value), p.Key); | |||||
| break; | |||||
| case Stream value: | |||||
| content.Add(new StreamContent(value), p.Key); | |||||
| break; | |||||
| case MultipartFile value: | |||||
| content.Add(new StreamContent(value.Stream), value.Filename, p.Key); | |||||
| break; | |||||
| default: | |||||
| throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); | |||||
| } | |||||
| #else | |||||
| //TODO: C# Typeswitch candidate | |||||
| var stringValue = p.Value as string; | var stringValue = p.Value as string; | ||||
| if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } | if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } | ||||
| var byteArrayValue = p.Value as byte[]; | var byteArrayValue = p.Value as byte[]; | ||||
| @@ -125,7 +107,6 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); | throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); | ||||
| #endif | |||||
| } | } | ||||
| } | } | ||||
| restRequest.Content = content; | restRequest.Content = content; | ||||
| @@ -19,6 +19,7 @@ namespace Discord.Net.WebSockets | |||||
| public event Func<string, Task> TextMessage; | public event Func<string, Task> TextMessage; | ||||
| private readonly ClientWebSocket _client; | private readonly ClientWebSocket _client; | ||||
| private readonly SemaphoreSlim _sendLock; | |||||
| private Task _task; | private Task _task; | ||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| private CancellationToken _cancelToken, _parentToken; | private CancellationToken _cancelToken, _parentToken; | ||||
| @@ -30,6 +31,7 @@ namespace Discord.Net.WebSockets | |||||
| _client.Options.Proxy = null; | _client.Options.Proxy = null; | ||||
| _client.Options.KeepAliveInterval = TimeSpan.Zero; | _client.Options.KeepAliveInterval = TimeSpan.Zero; | ||||
| _sendLock = new SemaphoreSlim(1, 1); | |||||
| _cancelTokenSource = new CancellationTokenSource(); | _cancelTokenSource = new CancellationTokenSource(); | ||||
| _cancelToken = CancellationToken.None; | _cancelToken = CancellationToken.None; | ||||
| _parentToken = CancellationToken.None; | _parentToken = CancellationToken.None; | ||||
| @@ -82,28 +84,37 @@ namespace Discord.Net.WebSockets | |||||
| public async Task Send(byte[] data, int index, int count, bool isText) | public async Task Send(byte[] data, int index, int count, bool isText) | ||||
| { | { | ||||
| //TODO: If connection is temporarily down, retry? | |||||
| int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); | |||||
| for (int i = 0; i < frameCount; i++, index += SendChunkSize) | |||||
| await _sendLock.WaitAsync(_cancelToken); | |||||
| try | |||||
| { | { | ||||
| bool isLast = i == (frameCount - 1); | |||||
| //TODO: If connection is temporarily down, retry? | |||||
| int frameCount = (int)Math.Ceiling((double)count / SendChunkSize); | |||||
| int frameSize; | |||||
| if (isLast) | |||||
| frameSize = count - (i * SendChunkSize); | |||||
| else | |||||
| frameSize = SendChunkSize; | |||||
| try | |||||
| for (int i = 0; i < frameCount; i++, index += SendChunkSize) | |||||
| { | { | ||||
| await _client.SendAsync(new ArraySegment<byte>(data, index, count), isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary, isLast, _cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||||
| { | |||||
| return; | |||||
| bool isLast = i == (frameCount - 1); | |||||
| int frameSize; | |||||
| if (isLast) | |||||
| frameSize = count - (i * SendChunkSize); | |||||
| else | |||||
| frameSize = SendChunkSize; | |||||
| try | |||||
| { | |||||
| var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; | |||||
| await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||||
| { | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| finally | |||||
| { | |||||
| _sendLock.Release(); | |||||
| } | |||||
| } | } | ||||
| //TODO: Check this code | //TODO: Check this code | ||||
| @@ -74,7 +74,7 @@ namespace Discord | |||||
| { | { | ||||
| CachedMessage msg; | CachedMessage msg; | ||||
| if (_messages.TryGetValue(x, out msg)) | if (_messages.TryGetValue(x, out msg)) | ||||
| return msg as CachedMessage; | |||||
| return msg; | |||||
| return null; | return null; | ||||
| }) | }) | ||||
| .Where(x => x != null) | .Where(x => x != null) | ||||