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