Browse Source

Added heartbeats, latency, guild events and channel events

tags/1.0-rc
RogueException 9 years ago
parent
commit
a831ae9484
26 changed files with 214 additions and 143 deletions
  1. +7
    -3
      src/Discord.Net/API/DiscordAPIClient.cs
  2. +1
    -1
      src/Discord.Net/API/WebSocketMessage.cs
  3. +20
    -0
      src/Discord.Net/DiscordClient.cs
  4. +114
    -69
      src/Discord.Net/DiscordSocketClient.cs
  5. +1
    -1
      src/Discord.Net/Entities/Channels/DMChannel.cs
  6. +1
    -1
      src/Discord.Net/Entities/Channels/GuildChannel.cs
  7. +1
    -1
      src/Discord.Net/Entities/Channels/TextChannel.cs
  8. +1
    -1
      src/Discord.Net/Entities/Channels/VoiceChannel.cs
  9. +2
    -2
      src/Discord.Net/Entities/Guilds/GuildIntegration.cs
  10. +1
    -1
      src/Discord.Net/Entities/Guilds/UserGuild.cs
  11. +1
    -1
      src/Discord.Net/Entities/Invites/Invite.cs
  12. +1
    -1
      src/Discord.Net/Entities/Invites/InviteMetadata.cs
  13. +1
    -1
      src/Discord.Net/Entities/Messages/Message.cs
  14. +2
    -15
      src/Discord.Net/Entities/Permissions/Permissions.cs
  15. +4
    -4
      src/Discord.Net/Entities/Users/GuildUser.cs
  16. +2
    -1
      src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs
  17. +2
    -0
      src/Discord.Net/Entities/WebSocket/CachedGuild.cs
  18. +2
    -0
      src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs
  19. +1
    -1
      src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs
  20. +1
    -0
      src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs
  21. +2
    -0
      src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs
  22. +6
    -1
      src/Discord.Net/Entities/WebSocket/ICachedChannel.cs
  23. +10
    -0
      src/Discord.Net/Extensions/EventExtensions.cs
  24. +1
    -20
      src/Discord.Net/Net/Rest/DefaultRestClient.cs
  25. +28
    -17
      src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs
  26. +1
    -1
      src/Discord.Net/Utilities/MessageCache.cs

+ 7
- 3
src/Discord.Net/API/DiscordAPIClient.cs View File

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


+ 1
- 1
src/Discord.Net/API/WebSocketMessage.cs View File

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


+ 20
- 0
src/Discord.Net/DiscordClient.cs View File

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


+ 114
- 69
src/Discord.Net/DiscordSocketClient.cs View File

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

+ 1
- 1
src/Discord.Net/Entities/Channels/DMChannel.cs View File

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


+ 1
- 1
src/Discord.Net/Entities/Channels/GuildChannel.cs View File

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




+ 1
- 1
src/Discord.Net/Entities/Channels/TextChannel.cs View File

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




+ 1
- 1
src/Discord.Net/Entities/Channels/VoiceChannel.cs View File

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




+ 2
- 2
src/Discord.Net/Entities/Guilds/GuildIntegration.cs View File

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


+ 1
- 1
src/Discord.Net/Entities/Guilds/UserGuild.cs View File

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




+ 1
- 1
src/Discord.Net/Entities/Invites/Invite.cs View File

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




+ 1
- 1
src/Discord.Net/Entities/Invites/InviteMetadata.cs View File

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




+ 1
- 1
src/Discord.Net/Entities/Messages/Message.cs View File

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




+ 2
- 15
src/Discord.Net/Entities/Permissions/Permissions.cs View File

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




+ 4
- 4
src/Discord.Net/Entities/Users/GuildUser.cs View File

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


+ 2
- 1
src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs View File

@@ -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();
} }
} }

+ 2
- 0
src/Discord.Net/Entities/WebSocket/CachedGuild.cs View File

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


+ 2
- 0
src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs View File

@@ -12,5 +12,7 @@ namespace Discord
: base(guild, user, model) : base(guild, user, model)
{ {
} }

public CachedGuildUser Clone() => MemberwiseClone() as CachedGuildUser;
} }
} }

+ 1
- 1
src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs View File

@@ -16,7 +16,7 @@ namespace Discord
{ {
} }


public CachedDMChannel SetDMChannel(ChannelModel model)
public CachedDMChannel AddDMChannel(ChannelModel model)
{ {
lock (this) lock (this)
{ {


+ 1
- 0
src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs View File

@@ -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();
} }
} }

+ 2
- 0
src/Discord.Net/Entities/WebSocket/CachedVoiceChannel.cs View File

@@ -34,5 +34,7 @@ namespace Discord
} }


public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel; public CachedVoiceChannel Clone() => MemberwiseClone() as CachedVoiceChannel;

ICachedChannel ICachedChannel.Clone() => Clone();
} }
} }

+ 6
- 1
src/Discord.Net/Entities/WebSocket/ICachedChannel.cs View File

@@ -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();
} }
} }

+ 10
- 0
src/Discord.Net/Extensions/EventExtensions.cs View File

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

+ 1
- 20
src/Discord.Net/Net/Rest/DefaultRestClient.cs View File

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


+ 28
- 17
src/Discord.Net/Net/WebSockets/DefaultWebsocketClient.cs View File

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


+ 1
- 1
src/Discord.Net/Utilities/MessageCache.cs View File

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


Loading…
Cancel
Save