@@ -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) | |||
@@ -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; } | |||
} | |||
@@ -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; | |||
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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); | |||
} | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -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) | |||
} | |||
@@ -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() | |||
@@ -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(); | |||
} | |||
} |
@@ -153,6 +153,8 @@ namespace Discord | |||
return null; | |||
} | |||
public CachedGuild Clone() => MemberwiseClone() as CachedGuild; | |||
new internal ICachedGuildChannel ToChannel(ChannelModel model) | |||
{ | |||
switch (model.Type) | |||
@@ -12,5 +12,7 @@ namespace Discord | |||
: 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) | |||
{ | |||
@@ -69,5 +69,6 @@ namespace Discord | |||
IMessage IMessageChannel.GetCachedMessage(ulong id) => GetCachedMessage(id); | |||
IUser ICachedMessageChannel.GetCachedUser(ulong id) => GetCachedUser(id); | |||
ICachedChannel ICachedChannel.Clone() => Clone(); | |||
} | |||
} |
@@ -34,5 +34,7 @@ namespace Discord | |||
} | |||
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> | |||
{ | |||
void Update(Model model, UpdateSource source); | |||
ICachedChannel Clone(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
@@ -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 | |||
@@ -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) | |||