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