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<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 JsonSerializer _serializer;
@@ -66,14 +66,14 @@ namespace Discord.API
using (var reader = new StreamReader(decompressed))
{
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 =>
{
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);
}
public async Task SendHeartbeat(int lastSeq, RequestOptions options = null)
{
await SendGateway(GatewayOpCode.Heartbeat, lastSeq, options: options).ConfigureAwait(false);
}

//Channels
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)]
public string Type { get; set; }
[JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)]
public uint? Sequence { get; set; }
public int? Sequence { get; set; }
[JsonProperty("d")]
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 API.DiscordApiClient ApiClient { get; private set; }

/// <summary> Creates a new discord client using only the REST API. </summary>
public DiscordClient()
: this(new DiscordConfig()) { }
/// <summary> Creates a new discord client using only the REST API. </summary>
public DiscordClient(DiscordConfig config)
{
_log = new LogManager(config.LogLevel);
@@ -40,10 +42,12 @@ namespace Discord
_connectionLock = new SemaphoreSlim(1, 1);
_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.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)
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
@@ -89,6 +93,7 @@ namespace Discord
}
protected virtual Task OnLogin() => Task.CompletedTask;

/// <inheritdoc />
public async Task Logout()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
@@ -115,12 +120,14 @@ namespace Discord
}
protected virtual Task OnLogout() => Task.CompletedTask;

/// <inheritdoc />
public async Task<IReadOnlyCollection<IConnection>> GetConnections()
{
var models = await ApiClient.GetCurrentUserConnections().ConfigureAwait(false);
return models.Select(x => new Connection(x)).ToImmutableArray();
}

/// <inheritdoc />
public virtual async Task<IChannel> GetChannel(ulong id)
{
var model = await ApiClient.GetChannel(id).ConfigureAwait(false);
@@ -140,12 +147,14 @@ namespace Discord
}
return null;
}
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IDMChannel>> GetDMChannels()
{
var models = await ApiClient.GetCurrentUserDMs().ConfigureAwait(false);
return models.Select(x => new DMChannel(this, new User(this, x.Recipient), x)).ToImmutableArray();
}

/// <inheritdoc />
public virtual async Task<IInvite> GetInvite(string inviteIdOrXkcd)
{
var model = await ApiClient.GetInvite(inviteIdOrXkcd).ConfigureAwait(false);
@@ -154,6 +163,7 @@ namespace Discord
return null;
}

/// <inheritdoc />
public virtual async Task<IGuild> GetGuild(ulong id)
{
var model = await ApiClient.GetGuild(id).ConfigureAwait(false);
@@ -161,6 +171,7 @@ namespace Discord
return new Guild(this, model);
return null;
}
/// <inheritdoc />
public virtual async Task<GuildEmbed?> GetGuildEmbed(ulong id)
{
var model = await ApiClient.GetGuildEmbed(id).ConfigureAwait(false);
@@ -168,12 +179,14 @@ namespace Discord
return new GuildEmbed(model);
return null;
}
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IUserGuild>> GetGuilds()
{
var models = await ApiClient.GetCurrentUserGuilds().ConfigureAwait(false);
return models.Select(x => new UserGuild(this, x)).ToImmutableArray();

}
/// <inheritdoc />
public virtual async Task<IGuild> CreateGuild(string name, IVoiceRegion region, Stream jpegIcon = null)
{
var args = new CreateGuildParams();
@@ -181,6 +194,7 @@ namespace Discord
return new Guild(this, model);
}

/// <inheritdoc />
public virtual async Task<IUser> GetUser(ulong id)
{
var model = await ApiClient.GetUser(id).ConfigureAwait(false);
@@ -188,6 +202,7 @@ namespace Discord
return new User(this, model);
return null;
}
/// <inheritdoc />
public virtual async Task<IUser> GetUser(string username, string discriminator)
{
var model = await ApiClient.GetUser(username, discriminator).ConfigureAwait(false);
@@ -195,6 +210,7 @@ namespace Discord
return new User(this, model);
return null;
}
/// <inheritdoc />
public virtual async Task<ISelfUser> GetCurrentUser()
{
var user = _currentUser;
@@ -206,17 +222,20 @@ namespace Discord
}
return user;
}
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IUser>> QueryUsers(string query, int limit)
{
var models = await ApiClient.QueryUsers(query, limit).ConfigureAwait(false);
return models.Select(x => new User(this, x)).ToImmutableArray();
}

/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IVoiceRegion>> GetVoiceRegions()
{
var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false);
return models.Select(x => new VoiceRegion(x)).ToImmutableArray();
}
/// <inheritdoc />
public virtual async Task<IVoiceRegion> GetVoiceRegion(string id)
{
var models = await ApiClient.GetVoiceRegions().ConfigureAwait(false);
@@ -228,6 +247,7 @@ namespace Discord
if (!_isDisposed)
_isDisposed = true;
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
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.Extensions;
using Discord.Logging;
@@ -11,19 +10,23 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Discord
{
//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 event Func<Task> Connected, Disconnected;
public event Func<Task> Ready;
//public event Func<Channel> VoiceConnected, VoiceDisconnected;
/*public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed;
public event Func<IChannel, Task> ChannelCreated, ChannelDestroyed;
public event Func<IChannel, IChannel, Task> ChannelUpdated;
public event Func<IMessage, Task> MessageReceived, MessageDeleted;
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, IUser, Task> UserUpdated;
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 Logger _gatewayLogger;
@@ -44,13 +48,21 @@ namespace Discord
private readonly bool _enablePreUpdateEvents;
private readonly int _largeThreshold;
private readonly int _totalShards;
private ImmutableDictionary<string, VoiceRegion> _voiceRegions;
private string _sessionId;
private int _lastSeq;
private ImmutableDictionary<string, VoiceRegion> _voiceRegions;
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; }
/// <summary> Gets the current connection state of this client. </summary>
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 bool UsePermissionCache { get; private set; }
internal DataStore DataStore { get; private set; }
@@ -61,7 +73,7 @@ namespace Discord
get
{
var guilds = DataStore.Guilds;
return guilds.Select(x => x as CachedGuild).ToReadOnlyCollection(guilds);
return guilds.ToReadOnlyCollection(guilds);
}
}
internal IReadOnlyCollection<CachedDMChannel> DMChannels
@@ -69,13 +81,15 @@ namespace Discord
get
{
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();

/// <summary> Creates a new discord client using the REST and WebSocket APIs. </summary>
public DiscordSocketClient()
: this(new DiscordSocketConfig()) { }
/// <summary> Creates a new discord client using the REST and WebSocket APIs. </summary>
public DiscordSocketClient(DiscordSocketConfig config)
: base(config)
{
@@ -117,6 +131,7 @@ namespace Discord
_voiceRegions = ImmutableDictionary.Create<string, VoiceRegion>();
}

/// <inheritdoc />
public async Task Connect()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
@@ -135,6 +150,7 @@ namespace Discord
try
{
_connectTask = new TaskCompletionSource<bool>();
_heartbeatCancelToken = new CancellationTokenSource();
await ApiClient.Connect().ConfigureAwait(false);

await _connectTask.Task.ConfigureAwait(false);
@@ -148,6 +164,7 @@ namespace Discord

await Connected.Raise().ConfigureAwait(false);
}
/// <inheritdoc />
public async Task Disconnect()
{
await _connectionLock.WaitAsync().ConfigureAwait(false);
@@ -165,13 +182,15 @@ namespace Discord
ConnectionState = ConnectionState.Disconnecting;

await ApiClient.Disconnect().ConfigureAwait(false);
await _heartbeatTask.ConfigureAwait(false);
while (_largeGuilds.TryDequeue(out guildId)) { }

ConnectionState = ConnectionState.Disconnected;

await Disconnected.Raise().ConfigureAwait(false);
}

/// <inheritdoc />
public override Task<IVoiceRegion> GetVoiceRegion(string id)
{
VoiceRegion region;
@@ -180,6 +199,7 @@ namespace Discord
return Task.FromResult<IVoiceRegion>(null);
}

/// <inheritdoc />
public override Task<IGuild> GetGuild(ulong id)
{
return Task.FromResult<IGuild>(DataStore.GetGuild(id));
@@ -192,7 +212,7 @@ namespace Discord
if (model.Unavailable != true)
{
for (int i = 0; i < model.Channels.Length; i++)
AddCachedChannel(model.Channels[i], dataStore);
AddCachedChannel(guild, model.Channels[i], dataStore);
}
dataStore.AddGuild(guild);
if (model.Large)
@@ -203,7 +223,7 @@ namespace Discord
{
dataStore = dataStore ?? DataStore;

var guild = dataStore.RemoveGuild(id) as CachedGuild;
var guild = dataStore.RemoveGuild(id);
foreach (var channel in guild.Channels)
guild.RemoveCachedChannel(channel.Id);
foreach (var user in guild.Members)
@@ -211,25 +231,25 @@ namespace Discord
return guild;
}

/// <inheritdoc />
public override Task<IChannel> GetChannel(ulong 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;

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);
return channel;
}
@@ -237,8 +257,8 @@ namespace Discord
{
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;
if (guildChannel != null)
@@ -258,10 +278,12 @@ namespace Discord
return null;
}

/// <inheritdoc />
public override Task<IUser> GetUser(ulong id)
{
return Task.FromResult<IUser>(DataStore.GetUser(id));
}
/// <inheritdoc />
public override Task<IUser> GetUser(string username, string discriminator)
{
return Task.FromResult<IUser>(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault());
@@ -270,7 +292,7 @@ namespace Discord
{
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();
return user;
}
@@ -278,22 +300,34 @@ namespace Discord
{
dataStore = dataStore ?? DataStore;

var user = dataStore.GetUser(id) as CachedPublicUser;
var user = dataStore.GetUser(id);
user.RemoveRef();
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
{
switch (opCode)
{
case GatewayOpCode.Hello:
{
var data = payload.ToObject<HelloEvent>(_serializer);
var data = (payload as JToken).ToObject<HelloEvent>(_serializer);

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;
case GatewayOpCode.Dispatch:
@@ -303,15 +337,15 @@ namespace Discord
case "READY":
{
//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);

_currentUser = new CachedSelfUser(this,data.User);
_currentUser = new CachedSelfUser(this, data.User);

for (int i = 0; i < data.Guilds.Length; i++)
AddCachedGuild(data.Guilds[i], dataStore);
for (int i = 0; i < data.PrivateChannels.Length; i++)
AddCachedChannel(data.PrivateChannels[i], dataStore);
AddCachedDMChannel(data.PrivateChannels[i], dataStore);

_sessionId = data.SessionId;
DataStore = dataStore;
@@ -323,9 +357,9 @@ namespace Discord
break;

//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);
DataStore.AddGuild(guild);

@@ -342,12 +376,12 @@ namespace Discord
break;
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);
if (guild != null)
{
var before = _enablePreUpdateEvents ? guild.Clone() : null;
guild.Update(data);
guild.Update(data, UpdateSource.WebSocket);
await GuildUpdated.Raise(before, guild);
}
else
@@ -356,7 +390,7 @@ namespace Discord
break;
case "GUILD_DELETE":
{
var data = payload.ToObject<ExtendedGuild>(_serializer);
var data = (payload as JToken).ToObject<ExtendedGuild>(_serializer);
var guild = DataStore.RemoveGuild(data.Id);
if (guild != null)
{
@@ -375,34 +409,34 @@ namespace Discord
//Channels
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)
{
var guild = GetCachedGuild(data.GuildId.Value);
var guild = DataStore.GetGuild(data.GuildId.Value);
if (guild != null)
channel = guild.AddCachedChannel(data.Id, true);
{
channel = guild.AddCachedChannel(data);
DataStore.AddChannel(channel);
}
else
await _gatewayLogger.Warning("CHANNEL_CREATE referenced an unknown guild.");
}
else
channel = AddCachedPrivateChannel(data.Id, data.Recipient.Id);
channel = AddCachedDMChannel(data);
if (channel != null)
{
channel.Update(data);
await ChannelCreated.Raise(channel);
}
}
break;
case "CHANNEL_UPDATE":
{
var data = payload.ToObject<API.Channel>(_serializer);
var channel = DataStore.GetChannel(data.Id) as Channel;
var data = (payload as JToken).ToObject<API.Channel>(_serializer);
var channel = DataStore.GetChannel(data.Id);
if (channel != null)
{
var before = _enablePreUpdateEvents ? channel.Clone() : null;
channel.Update(data);
channel.Update(data, UpdateSource.WebSocket);
await ChannelUpdated.Raise(before, channel);
}
else
@@ -411,7 +445,7 @@ namespace Discord
break;
case "CHANNEL_DELETE":
{
var data = payload.ToObject<API.Channel>(_serializer);
var data = (payload as JToken).ToObject<API.Channel>(_serializer);
var channel = RemoveCachedChannel(data.Id);
if (channel != null)
await ChannelDestroyed.Raise(channel);
@@ -421,9 +455,9 @@ namespace Discord
break;

//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);
if (guild != null)
{
@@ -438,7 +472,7 @@ namespace Discord
break;
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);
if (guild != null)
{
@@ -458,7 +492,7 @@ namespace Discord
break;
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);
if (guild != null)
{
@@ -479,7 +513,7 @@ namespace Discord
break;
case "GUILD_MEMBERS_CHUNK":
{
var data = payload.ToObject<GuildMembersChunkEvent>(_serializer);
var data = (payload as JToken).ToObject<GuildMembersChunkEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
{
@@ -498,9 +532,9 @@ namespace Discord
break;

//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);
if (guild != null)
{
@@ -514,7 +548,7 @@ namespace Discord
break;
case "GUILD_ROLE_UPDATE":
{
var data = payload.ToObject<GuildRoleUpdateEvent>(_serializer);
var data = (payload as JToken).ToObject<GuildRoleUpdateEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
{
@@ -534,8 +568,8 @@ namespace Discord
break;
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)
{
var role = guild.RemoveRole(data.RoleId);
@@ -552,7 +586,7 @@ namespace Discord
//Bans
case "GUILD_BAN_ADD":
{
var data = payload.ToObject<GuildBanEvent>(_serializer);
var data = (payload as JToken).ToObject<GuildBanEvent>(_serializer);
var guild = GetCachedGuild(data.GuildId);
if (guild != null)
await UserBanned.Raise(new User(this, data));
@@ -574,8 +608,7 @@ namespace Discord
//Messages
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);
if (channel != null)
{
@@ -599,7 +632,7 @@ namespace Discord
break;
case "MESSAGE_UPDATE":
{
var data = payload.ToObject<API.Message>(_serializer);
var data = (payload as JToken).ToObject<API.Message>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
@@ -614,7 +647,7 @@ namespace Discord
break;
case "MESSAGE_DELETE":
{
var data = payload.ToObject<API.Message>(_serializer);
var data = (payload as JToken).ToObject<API.Message>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
@@ -629,7 +662,7 @@ namespace Discord
//Statuses
case "PRESENCE_UPDATE":
{
var data = payload.ToObject<API.Presence>(_serializer);
var data = (payload as JToken).ToObject<API.Presence>(_serializer);
User user;
Guild guild;
if (data.GuildId == null)
@@ -664,7 +697,7 @@ namespace Discord
break;
case "TYPING_START":
{
var data = payload.ToObject<TypingStartEvent>(_serializer);
var data = (payload as JToken).ToObject<TypingStartEvent>(_serializer);
var channel = GetCachedChannel(data.ChannelId);
if (channel != null)
{
@@ -683,7 +716,7 @@ namespace Discord
//Voice
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);
if (guild != null)
{
@@ -708,7 +741,7 @@ namespace Discord
//Settings
case "USER_UPDATE":
{
var data = payload.ToObject<SelfUser>(_serializer);
var data = (payload as JToken).ToObject<SelfUser>(_serializer);
if (data.Id == CurrentUser.Id)
{
var before = _enablePreUpdateEvents ? CurrentUser.Clone() : null;
@@ -746,5 +779,17 @@ namespace Discord
}
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);
}
protected void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
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);
}
protected virtual void Update(Model model, UpdateSource source)
public virtual void Update(Model model, UpdateSource source)
{
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)
{
}
protected override void Update(Model model, UpdateSource source)
public override void Update(Model model, UpdateSource source)
{
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)
{
}
protected override void Update(Model model, UpdateSource source)
public override void Update(Model model, UpdateSource source)
{
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);
}

private void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

@@ -43,7 +43,7 @@ namespace Discord
ExpireGracePeriod = model.ExpireGracePeriod;
SyncedAt = model.SyncedAt;

Role = Guild.GetRole(model.RoleId) as Role;
Role = Guild.GetRole(model.RoleId);
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;
Update(model, UpdateSource.Creation);
}
private void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
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);
}
protected void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
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);
}
private void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
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);
}
private void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
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);
if (perms != null)
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 voiceChannel = channel as IVoiceChannel;
if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages))
resolvedPermissions = 0; //No read permission on a text channel removes all other permissions
else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect))
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)
}



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

@@ -39,7 +39,7 @@ namespace Discord

Update(model, UpdateSource.Creation);
}
private void Update(Model model, UpdateSource source)
public void Update(Model model, UpdateSource source)
{
if (source == UpdateSource.Rest && IsAttached) return;

@@ -49,9 +49,9 @@ namespace Discord
Nickname = model.Nick;

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++)
roles.Add(Guild.GetRole(model.Roles[i]) as Role);
roles.Add(Guild.GetRole(model.Roles[i]));
Roles = roles.ToImmutable();

GuildPermissions = new GuildPermissions(Permissions.ResolveGuild(this));
@@ -89,7 +89,7 @@ namespace Discord
if (args.Nickname.IsSpecified)
Nickname = args.Nickname.Value ?? "";
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()


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

@@ -65,6 +65,7 @@ namespace Discord

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

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

new internal ICachedGuildChannel ToChannel(ChannelModel model)
{
switch (model.Type)


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

@@ -12,5 +12,7 @@ namespace Discord
: 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)
{


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

@@ -69,5 +69,6 @@ namespace Discord

IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(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;

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>
{
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
{
//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)
{
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);
}
}
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)
{
#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;
if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; }
var byteArrayValue = p.Value as byte[];
@@ -125,7 +107,6 @@ namespace Discord.Net.Rest
}

throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"");
#endif
}
}
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;
private readonly ClientWebSocket _client;
private readonly SemaphoreSlim _sendLock;
private Task _task;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken;
@@ -30,6 +31,7 @@ namespace Discord.Net.WebSockets
_client.Options.Proxy = null;
_client.Options.KeepAliveInterval = TimeSpan.Zero;

_sendLock = new SemaphoreSlim(1, 1);
_cancelTokenSource = new CancellationTokenSource();
_cancelToken = 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)
{
//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


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

@@ -74,7 +74,7 @@ namespace Discord
{
CachedMessage msg;
if (_messages.TryGetValue(x, out msg))
return msg as CachedMessage;
return msg;
return null;
})
.Where(x => x != null)


Loading…
Cancel
Save