| @@ -39,4 +39,3 @@ jobs: | |||||
| steps: | steps: | ||||
| - template: azure/build.yml | - template: azure/build.yml | ||||
| - template: azure/deploy.yml | - template: azure/deploy.yml | ||||
| - template: azure/docs.yml | |||||
| @@ -10,7 +10,7 @@ namespace Discord | |||||
| public interface IMessage : ISnowflakeEntity, IDeletable | public interface IMessage : ISnowflakeEntity, IDeletable | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets the type of this system message. | |||||
| /// Gets the type of this message. | |||||
| /// </summary> | /// </summary> | ||||
| MessageType Type { get; } | MessageType Type { get; } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -16,7 +16,7 @@ namespace Discord.Rest | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the logged-in user. | /// Gets the logged-in user. | ||||
| /// </summary> | /// </summary> | ||||
| public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; | |||||
| public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public DiscordRestClient() : this(new DiscordRestConfig()) { } | public DiscordRestClient() : this(new DiscordRestConfig()) { } | ||||
| @@ -72,6 +72,8 @@ namespace Discord.Rest | |||||
| public MessageReference Reference { get; private set; } | public MessageReference Reference { get; private set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public MessageFlags? Flags { get; private set; } | public MessageFlags? Flags { get; private set; } | ||||
| /// <inheritdoc/> | |||||
| public MessageType Type { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public IReadOnlyCollection<ActionRowComponent> Components { get; private set; } | public IReadOnlyCollection<ActionRowComponent> Components { get; private set; } | ||||
| @@ -92,6 +94,8 @@ namespace Discord.Rest | |||||
| } | } | ||||
| internal virtual void Update(Model model) | internal virtual void Update(Model model) | ||||
| { | { | ||||
| Type = model.Type; | |||||
| if (model.Timestamp.IsSpecified) | if (model.Timestamp.IsSpecified) | ||||
| _timestampTicks = model.Timestamp.Value.UtcTicks; | _timestampTicks = model.Timestamp.Value.UtcTicks; | ||||
| @@ -219,8 +223,6 @@ namespace Discord.Rest | |||||
| /// </returns> | /// </returns> | ||||
| public override string ToString() => Content; | public override string ToString() => Content; | ||||
| /// <inheritdoc /> | |||||
| MessageType IMessage.Type => MessageType.Default; | |||||
| IUser IMessage.Author => Author; | IUser IMessage.Author => Author; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments; | IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments; | ||||
| @@ -9,9 +9,6 @@ namespace Discord.Rest | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class RestSystemMessage : RestMessage, ISystemMessage | public class RestSystemMessage : RestMessage, ISystemMessage | ||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public MessageType Type { get; private set; } | |||||
| internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) | internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) | ||||
| : base(discord, id, channel, author, MessageSource.System) | : base(discord, id, channel, author, MessageSource.System) | ||||
| { | { | ||||
| @@ -25,8 +22,6 @@ namespace Discord.Rest | |||||
| internal override void Update(Model model) | internal override void Update(Model model) | ||||
| { | { | ||||
| base.Update(model); | base.Update(model); | ||||
| Type = model.Type; | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; | private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; | ||||
| @@ -75,11 +75,6 @@ namespace Discord | |||||
| nextReconnectDelay = 1000; //Reset delay | nextReconnectDelay = 1000; //Reset delay | ||||
| await _connectionPromise.Task.ConfigureAwait(false); | await _connectionPromise.Task.ConfigureAwait(false); | ||||
| } | } | ||||
| catch (OperationCanceledException ex) | |||||
| { | |||||
| Cancel(); //In case this exception didn't come from another Error call | |||||
| await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); | |||||
| } | |||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| Error(ex); //In case this exception didn't come from another Error call | Error(ex); //In case this exception didn't come from another Error call | ||||
| @@ -143,16 +138,7 @@ namespace Discord | |||||
| catch (OperationCanceledException) { } | catch (OperationCanceledException) { } | ||||
| }); | }); | ||||
| try | |||||
| { | |||||
| await _onConnecting().ConfigureAwait(false); | |||||
| } | |||||
| catch (TaskCanceledException ex) | |||||
| { | |||||
| Exception innerEx = ex.InnerException ?? new OperationCanceledException("Failed to connect."); | |||||
| Error(innerEx); | |||||
| throw innerEx; | |||||
| } | |||||
| await _onConnecting().ConfigureAwait(false); | |||||
| await _logger.InfoAsync("Connected").ConfigureAwait(false); | await _logger.InfoAsync("Connected").ConfigureAwait(false); | ||||
| State = ConnectionState.Connected; | State = ConnectionState.Connected; | ||||
| @@ -188,9 +188,9 @@ namespace Discord.API | |||||
| catch { } | catch { } | ||||
| if (ex is GatewayReconnectException) | if (ex is GatewayReconnectException) | ||||
| await WebSocketClient.DisconnectAsync(4000); | |||||
| await WebSocketClient.DisconnectAsync(4000).ConfigureAwait(false); | |||||
| else | else | ||||
| await WebSocketClient.DisconnectAsync().ConfigureAwait(false); | |||||
| await WebSocketClient.DisconnectAsync().ConfigureAwait(false); | |||||
| ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
| } | } | ||||
| @@ -621,6 +621,7 @@ namespace Discord.WebSocket | |||||
| var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; | ||||
| currentUser.Presence = new SocketPresence(Status, null, activities); | currentUser.Presence = new SocketPresence(Status, null, activities); | ||||
| ApiClient.CurrentUserId = currentUser.Id; | ApiClient.CurrentUserId = currentUser.Id; | ||||
| Rest.CurrentUser = RestSelfUser.Create(this, data.User); | |||||
| int unavailableGuilds = 0; | int unavailableGuilds = 0; | ||||
| for (int i = 0; i < data.Guilds.Length; i++) | for (int i = 0; i < data.Guilds.Length; i++) | ||||
| { | { | ||||
| @@ -1,42 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| public interface ISocketInvite | |||||
| { | |||||
| /// <summary> | |||||
| /// Gets the unique identifier for this invite. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A string containing the invite code (e.g. <c>FTqNnyS</c>). | |||||
| /// </returns> | |||||
| string Code { get; } | |||||
| /// <summary> | |||||
| /// Gets the URL used to accept this invite | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A string containing the full invite URL (e.g. <c>https://discord.gg/FTqNnyS</c>). | |||||
| /// </returns> | |||||
| string Url { get; } | |||||
| /// <summary> | |||||
| /// Gets the channel this invite is linked to. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A generic channel that the invite points to. | |||||
| /// </returns> | |||||
| SocketGuildChannel Channel { get; } | |||||
| /// <summary> | |||||
| /// Gets the guild this invite is linked to. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// A guild object representing the guild that the invite points to. | |||||
| /// </returns> | |||||
| SocketGuild Guild { get; } | |||||
| } | |||||
| } | |||||
| @@ -1,47 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal class InviteCache | |||||
| { | |||||
| private readonly ConcurrentDictionary<string, SocketGuildInvite> _invites; | |||||
| private readonly ConcurrentQueue<string> _queue; | |||||
| private static int _size; | |||||
| public InviteCache(DiscordSocketClient client) | |||||
| { | |||||
| //NOTE: | |||||
| //This should be an option in the client config. default for now is 20 invites per guild | |||||
| _size = client.Guilds.Count * 20; | |||||
| _invites = new ConcurrentDictionary<string, SocketGuildInvite>(); | |||||
| _queue = new ConcurrentQueue<string>(); | |||||
| } | |||||
| public void Add(SocketGuildInvite invite) | |||||
| { | |||||
| if(_invites.TryAdd(invite.Code, invite)) | |||||
| { | |||||
| _queue.Enqueue(invite.Code); | |||||
| while (_queue.Count > _size && _queue.TryDequeue(out string invCode)) | |||||
| _invites.TryRemove(invCode, out _); | |||||
| } | |||||
| } | |||||
| public SocketGuildInvite Remove(string inviteCode) | |||||
| { | |||||
| _invites.TryRemove(inviteCode, out SocketGuildInvite inv); | |||||
| return inv; | |||||
| } | |||||
| public SocketGuildInvite Get(string inviteCode) | |||||
| { | |||||
| if(_invites.TryGetValue(inviteCode, out SocketGuildInvite inv)) | |||||
| return inv; | |||||
| return null; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,112 +0,0 @@ | |||||
| using Discord.Rest; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Runtime.Serialization.Formatters; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| using InviteUpdate = Discord.API.Gateway.InviteCreatedEvent; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a guild invite | |||||
| /// </summary> | |||||
| public class SocketGuildInvite : SocketEntity<string>, ISocketInvite | |||||
| { | |||||
| public string Code { get; private set; } | |||||
| public string Url => $"{DiscordConfig.InviteUrl}{Code}"; | |||||
| public SocketGuildChannel Channel { get; private set; } | |||||
| public SocketGuild Guild { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the unique invite code | |||||
| /// <returns> | |||||
| /// Returns the unique invite code | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public string Id => Code; | |||||
| /// <summary> | |||||
| /// Gets the user who created the invite | |||||
| /// <returns> | |||||
| /// Returns the user who created the invite | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public SocketGuildUser Inviter { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the maximum number of times the invite can be used, if there is no limit then the value will be 0 | |||||
| /// <returns> | |||||
| /// Returns the maximum number of times the invite can be used, if there is no limit then the value will be 0 | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public int? MaxUses { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) | |||||
| /// <returns> | |||||
| /// Returns whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public bool Temporary { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets the time at which the invite was created | |||||
| /// <returns> | |||||
| /// Returns the time at which the invite was created | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public DateTimeOffset? CreatedAt { get; private set; } | |||||
| /// <summary> | |||||
| /// Gets how long the invite is valid for | |||||
| /// <returns> | |||||
| /// Returns how long the invite is valid for (in seconds) | |||||
| /// </returns> | |||||
| /// </summary> | |||||
| public TimeSpan? MaxAge { get; private set; } | |||||
| internal SocketGuildInvite(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, RestInviteMetadata rest) : base(_client, inviteCode) | |||||
| { | |||||
| Code = inviteCode; | |||||
| Guild = guild; | |||||
| Channel = channel; | |||||
| CreatedAt = rest.CreatedAt; | |||||
| Temporary = rest.IsTemporary; | |||||
| MaxUses = rest.MaxUses; | |||||
| Inviter = guild.GetUser(rest.Inviter.Id); | |||||
| if (rest.MaxAge.HasValue) | |||||
| MaxAge = TimeSpan.FromSeconds(rest.MaxAge.Value); | |||||
| } | |||||
| internal SocketGuildInvite(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, InviteUpdate Update) : base(_client, inviteCode) | |||||
| { | |||||
| Code = inviteCode; | |||||
| Guild = guild; | |||||
| Channel = channel; | |||||
| if (Update.RawTimestamp.IsSpecified) | |||||
| CreatedAt = Update.RawTimestamp.Value; | |||||
| else | |||||
| CreatedAt = DateTimeOffset.Now; | |||||
| if (Update.inviter.IsSpecified) | |||||
| Inviter = guild.GetUser(Update.inviter.Value.Id); | |||||
| Temporary = Update.TempInvite; | |||||
| MaxUses = Update.MaxUsers; | |||||
| MaxAge = TimeSpan.FromSeconds(Update.RawAge); | |||||
| } | |||||
| internal static SocketGuildInvite Create(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, InviteUpdate Update) | |||||
| { | |||||
| var invite = new SocketGuildInvite(_client, guild, channel, inviteCode, Update); | |||||
| return invite; | |||||
| } | |||||
| internal static SocketGuildInvite CreateFromRest(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, RestInviteMetadata rest) | |||||
| { | |||||
| var invite = new SocketGuildInvite(_client, guild, channel, inviteCode, rest); | |||||
| return invite; | |||||
| } | |||||
| /// <summary> | |||||
| /// Deletes the invite | |||||
| /// </summary> | |||||
| /// <param name="options"></param> | |||||
| /// <returns></returns> | |||||
| public Task DeleteAsync(RequestOptions options = null) | |||||
| => SocketInviteHelper.DeleteAsync(this, Discord, options); | |||||
| } | |||||
| } | |||||
| @@ -1,17 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| internal class SocketInviteHelper | |||||
| { | |||||
| public static async Task DeleteAsync(ISocketInvite invite, BaseSocketClient client, | |||||
| RequestOptions options) | |||||
| { | |||||
| await client.ApiClient.DeleteInviteAsync(invite.Code, options).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -65,6 +65,9 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public MessageFlags? Flags { get; private set; } | public MessageFlags? Flags { get; private set; } | ||||
| /// <inheritdoc/> | |||||
| public MessageType Type { get; private set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// Returns all attachments included in this message. | /// Returns all attachments included in this message. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -126,6 +129,8 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| internal virtual void Update(ClientState state, Model model) | internal virtual void Update(ClientState state, Model model) | ||||
| { | { | ||||
| Type = model.Type; | |||||
| if (model.Timestamp.IsSpecified) | if (model.Timestamp.IsSpecified) | ||||
| _timestampTicks = model.Timestamp.Value.UtcTicks; | _timestampTicks = model.Timestamp.Value.UtcTicks; | ||||
| @@ -238,8 +243,6 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IMessageChannel IMessage.Channel => Channel; | IMessageChannel IMessage.Channel => Channel; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| MessageType IMessage.Type => MessageType.Default; | |||||
| /// <inheritdoc /> | |||||
| IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments; | IReadOnlyCollection<IAttachment> IMessage.Attachments => Attachments; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds; | IReadOnlyCollection<IEmbed> IMessage.Embeds => Embeds; | ||||
| @@ -9,9 +9,6 @@ namespace Discord.WebSocket | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketSystemMessage : SocketMessage, ISystemMessage | public class SocketSystemMessage : SocketMessage, ISystemMessage | ||||
| { | { | ||||
| /// <inheritdoc /> | |||||
| public MessageType Type { get; private set; } | |||||
| internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) | internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) | ||||
| : base(discord, id, channel, author, MessageSource.System) | : base(discord, id, channel, author, MessageSource.System) | ||||
| { | { | ||||
| @@ -25,8 +22,6 @@ namespace Discord.WebSocket | |||||
| internal override void Update(ClientState state, Model model) | internal override void Update(ClientState state, Model model) | ||||
| { | { | ||||
| base.Update(state, model); | base.Update(state, model); | ||||
| Type = model.Type; | |||||
| } | } | ||||
| private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; | private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; | ||||
| @@ -125,12 +125,16 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | ||||
| entity.Update(state, model); | entity.Update(state, model); | ||||
| if (!model.Roles.IsSpecified) | |||||
| entity.UpdateRoles(new ulong[0]); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) | internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) | ||||
| { | { | ||||
| var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); | ||||
| entity.Update(state, model, false); | entity.Update(state, model, false); | ||||
| if (!model.Roles.IsSpecified) | |||||
| entity.UpdateRoles(new ulong[0]); | |||||
| return entity; | return entity; | ||||
| } | } | ||||
| internal void Update(ClientState state, MemberModel model) | internal void Update(ClientState state, MemberModel model) | ||||
| @@ -108,11 +108,11 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) | private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) | ||||
| { | { | ||||
| _isDisconnecting = true; | |||||
| try { _disconnectTokenSource.Cancel(false); } | try { _disconnectTokenSource.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| _isDisconnecting = true; | |||||
| if (_client != null) | if (_client != null) | ||||
| { | { | ||||
| if (!isDisposing) | if (!isDisposing) | ||||
| @@ -166,7 +166,14 @@ namespace Discord.Net.WebSockets | |||||
| public async Task SendAsync(byte[] data, int index, int count, bool isText) | public async Task SendAsync(byte[] data, int index, int count, bool isText) | ||||
| { | { | ||||
| await _lock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await _lock.WaitAsync(_cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| catch (TaskCanceledException) | |||||
| { | |||||
| return; | |||||
| } | |||||
| try | try | ||||
| { | { | ||||
| if (_client == null) return; | if (_client == null) return; | ||||
| @@ -201,7 +208,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); | |||||
| WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); | |||||
| byte[] result; | byte[] result; | ||||
| int resultCount; | int resultCount; | ||||