From dde4ef9e197d46542fd2fe7539b82716885869b4 Mon Sep 17 00:00:00 2001 From: quinchs <49576606+quinchs@users.noreply.github.com> Date: Tue, 7 Jul 2020 04:59:50 -0300 Subject: [PATCH] Added SocketGuildInvites --- .../API/Gateway/InviteCreatedEvent.cs | 32 +++++ .../API/Gateway/InviteDeletedEvent.cs | 19 +++ .../BaseSocketClient.Events.cs | 16 +++ .../DiscordSocketClient.cs | 72 ++++++++++- .../Entities/Guilds/SocketGuild.cs | 18 +++ .../Entities/Invites/ISocketInvite.cs | 42 +++++++ .../Entities/Invites/InviteCache.cs | 47 ++++++++ .../Entities/Invites/SocketGuildInvite.cs | 112 ++++++++++++++++++ .../Entities/Invites/SocketInviteHelper.cs | 17 +++ 9 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs new file mode 100644 index 000000000..8f8002029 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs @@ -0,0 +1,32 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class InviteCreatedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("code")] + public string InviteCode { get; set; } + [JsonProperty("timestamp")] + public Optional RawTimestamp { get; set; } + [JsonProperty("guild_id")] + public ulong? GuildID { get; set; } + [JsonProperty("inviter")] + public Optional inviter { get; set; } + [JsonProperty("max_age")] + public int RawAge { get; set; } + [JsonProperty("max_uses")] + public int MaxUsers { get; set; } + [JsonProperty("temporary")] + public bool TempInvite { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs new file mode 100644 index 000000000..6bdd337f5 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class InviteDeletedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("guild_id")] + public Optional GuildID { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 2cd62b3e8..221067d22 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -315,6 +315,22 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + //Invites + internal readonly AsyncEvent> _inviteCreatedEvent = new AsyncEvent>(); + /// Fired when a invite is created. + public event Func InviteCreated + { + add { _inviteCreatedEvent.Add(value); } + remove { _inviteCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent, Task>> _inviteDeletedEvent = new AsyncEvent, Task>>(); + /// Fired when a invite is deleted. + public event Func, Task> InviteDeleted + { + add { _inviteDeletedEvent.Add(value); } + remove { _inviteDeletedEvent.Remove(value); } + } + //Users /// Fired when a user joins a guild. public event Func UserJoined { diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 1bfa467b6..f3cb054db 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -275,7 +275,8 @@ namespace Discord.WebSocket await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - while (_heartbeatTimes.TryDequeue(out _)) { } + while (_heartbeatTimes.TryDequeue(out _)) + { } _lastMessageTime = 0; await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); @@ -286,7 +287,8 @@ namespace Discord.WebSocket //Clear large guild queue await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); - while (_largeGuilds.TryDequeue(out _)) { } + while (_largeGuilds.TryDequeue(out _)) + { } //Raise virtual GUILD_UNAVAILABLEs await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); @@ -578,7 +580,7 @@ namespace Discord.WebSocket } else if (_connection.CancelToken.IsCancellationRequested) return; - + if (BaseConfig.AlwaysDownloadUsers) _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); @@ -1680,6 +1682,70 @@ namespace Discord.WebSocket } break; + case "INVITE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + if(data.GuildID.HasValue) + { + var guild = State.GetGuild(data.GuildID.Value); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var channel = guild.GetChannel(data.ChannelID); + + if (channel != null) + { + var invite = new SocketGuildInvite(this, guild, channel, data.InviteCode, data); + guild.AddSocketInvite(invite); + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); + } + } + else + { + //add else + } + } + + } + break; + case "INVITE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); + if(data.GuildID.IsSpecified) + { + var guild = State.GetGuild(data.GuildID.Value); + if (guild != null) + { + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + var channel = guild.GetChannel(data.ChannelID); + + if (channel != null) + { + var invite = guild.RemoveSocketInvite(data.Code); + var cache = new Cacheable(null, data.Code, invite != null, async () => await guild.GetSocketInviteAsync(data.Code)); + await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), cache).ConfigureAwait(false); + } + } + else + { + //add else + } + } + + } + break; //Ignored (User only) case "CHANNEL_PINS_ACK": diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index d2d759bb3..58a07ff33 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -39,6 +39,7 @@ namespace Discord.WebSocket private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; + private InviteCache _invites; #pragma warning restore IDISP002, IDISP006 /// @@ -280,6 +281,7 @@ namespace Discord.WebSocket _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); _features = ImmutableArray.Create(); + _invites = new InviteCache(client); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { @@ -515,6 +517,22 @@ namespace Discord.WebSocket public Task RemoveBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, userId, options); + //Invites + internal void AddSocketInvite(SocketGuildInvite invite) + => _invites.Add(invite); + internal SocketGuildInvite RemoveSocketInvite(string code) + => _invites.Remove(code); + internal async Task GetSocketInviteAsync(string code) + { + var invites = await this.GetInvitesAsync(); + RestInviteMetadata restInvite = invites.First(x => x.Code == code); + if (restInvite == null) + return null; + + var invite = new SocketGuildInvite(Discord, this, this.GetChannel(restInvite.ChannelId), code, restInvite); + return invite; + } + //Channels /// /// Gets a channel in this guild. diff --git a/src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs new file mode 100644 index 000000000..976fa348a --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ISocketInvite + { + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + string Code { get; } + /// + /// Gets the URL used to accept this invite using . + /// + /// + /// A string containing the full invite URL (e.g. https://discord.gg/FTqNnyS). + /// + string Url { get; } + + /// + /// Gets the channel this invite is linked to. + /// + /// + /// A generic channel that the invite points to. + /// + SocketGuildChannel Channel { get; } + + /// + /// Gets the guild this invite is linked to. + /// + /// + /// A guild object representing the guild that the invite points to. + /// + SocketGuild Guild { get; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs b/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs new file mode 100644 index 000000000..2346fb163 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs @@ -0,0 +1,47 @@ +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 _invites; + private readonly ConcurrentQueue _queue; + private static int _size; + + public InviteCache(DiscordSocketClient client) + { + //NOTE: + //This should be an option in the client config. + _size = client.Guilds.Count * 20; + + _invites = new ConcurrentDictionary(); + _queue = new ConcurrentQueue(); + } + 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; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs new file mode 100644 index 000000000..35fdf237c --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs @@ -0,0 +1,112 @@ +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 +{ + /// + /// Represents a guild invite + /// + public class SocketGuildInvite : SocketEntity, 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; } + /// + /// Gets the unique invite code + /// + /// Returns the unique invite code + /// + /// + public string Id => Code; + /// + /// Gets the user who created the invite + /// + /// Returns the user who created the invite + /// + /// + public SocketGuildUser Inviter { get; private set; } + /// + /// Gets the maximum number of times the invite can be used, if there is no limit then the value will be 0 + /// + /// Returns the maximum number of times the invite can be used, if there is no limit then the value will be 0 + /// + /// + public int? MaxUses { get; private set; } + /// + /// Gets whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) + /// + /// Returns whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) + /// + /// + public bool Temporary { get; private set; } + /// + /// Gets the time at which the invite was created + /// + /// Returns the time at which the invite was created + /// + /// + public DateTimeOffset? CreatedAt { get; private set; } + /// + /// Gets how long the invite is valid for + /// + /// Returns how long the invite is valid for (in seconds) + /// + /// + 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; + } + /// + /// Deletes the invite + /// + /// + /// + public Task DeleteAsync(RequestOptions options = null) + => SocketInviteHelper.DeleteAsync(this, Discord, options); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs new file mode 100644 index 000000000..3781739a9 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs @@ -0,0 +1,17 @@ +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); + } + } +}