| @@ -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<DateTimeOffset> RawTimestamp { get; set; } | |||||
| [JsonProperty("guild_id")] | |||||
| public ulong? GuildID { get; set; } | |||||
| [JsonProperty("inviter")] | |||||
| public Optional<User> 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; } | |||||
| } | |||||
| } | |||||
| @@ -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<ulong> GuildID { get; set; } | |||||
| [JsonProperty("code")] | |||||
| public string Code { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -315,6 +315,22 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| internal readonly AsyncEvent<Func<SocketGuild, SocketGuild, Task>> _guildUpdatedEvent = new AsyncEvent<Func<SocketGuild, SocketGuild, Task>>(); | internal readonly AsyncEvent<Func<SocketGuild, SocketGuild, Task>> _guildUpdatedEvent = new AsyncEvent<Func<SocketGuild, SocketGuild, Task>>(); | ||||
| //Invites | |||||
| internal readonly AsyncEvent<Func<SocketGuildInvite, Task>> _inviteCreatedEvent = new AsyncEvent<Func<SocketGuildInvite, Task>>(); | |||||
| /// <summary> Fired when a invite is created. </summary> | |||||
| public event Func<SocketGuildInvite, Task> InviteCreated | |||||
| { | |||||
| add { _inviteCreatedEvent.Add(value); } | |||||
| remove { _inviteCreatedEvent.Remove(value); } | |||||
| } | |||||
| internal readonly AsyncEvent<Func<Cacheable<SocketGuildInvite, string>, Task>> _inviteDeletedEvent = new AsyncEvent<Func<Cacheable<SocketGuildInvite, string>, Task>>(); | |||||
| /// <summary> Fired when a invite is deleted. </summary> | |||||
| public event Func<Cacheable<SocketGuildInvite, string>, Task> InviteDeleted | |||||
| { | |||||
| add { _inviteDeletedEvent.Add(value); } | |||||
| remove { _inviteDeletedEvent.Remove(value); } | |||||
| } | |||||
| //Users | //Users | ||||
| /// <summary> Fired when a user joins a guild. </summary> | /// <summary> Fired when a user joins a guild. </summary> | ||||
| public event Func<SocketGuildUser, Task> UserJoined { | public event Func<SocketGuildUser, Task> UserJoined { | ||||
| @@ -275,7 +275,8 @@ namespace Discord.WebSocket | |||||
| await heartbeatTask.ConfigureAwait(false); | await heartbeatTask.ConfigureAwait(false); | ||||
| _heartbeatTask = null; | _heartbeatTask = null; | ||||
| while (_heartbeatTimes.TryDequeue(out _)) { } | |||||
| while (_heartbeatTimes.TryDequeue(out _)) | |||||
| { } | |||||
| _lastMessageTime = 0; | _lastMessageTime = 0; | ||||
| await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); | ||||
| @@ -286,7 +287,8 @@ namespace Discord.WebSocket | |||||
| //Clear large guild queue | //Clear large guild queue | ||||
| await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); | ||||
| while (_largeGuilds.TryDequeue(out _)) { } | |||||
| while (_largeGuilds.TryDequeue(out _)) | |||||
| { } | |||||
| //Raise virtual GUILD_UNAVAILABLEs | //Raise virtual GUILD_UNAVAILABLEs | ||||
| await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); | ||||
| @@ -578,7 +580,7 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| else if (_connection.CancelToken.IsCancellationRequested) | else if (_connection.CancelToken.IsCancellationRequested) | ||||
| return; | return; | ||||
| if (BaseConfig.AlwaysDownloadUsers) | if (BaseConfig.AlwaysDownloadUsers) | ||||
| _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); | _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); | ||||
| @@ -1680,6 +1682,70 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| break; | break; | ||||
| case "INVITE_CREATE": | |||||
| { | |||||
| await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); | |||||
| var data = (payload as JToken).ToObject<InviteCreatedEvent>(_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<InviteDeletedEvent>(_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<SocketGuildInvite, string>(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) | //Ignored (User only) | ||||
| case "CHANNEL_PINS_ACK": | case "CHANNEL_PINS_ACK": | ||||
| @@ -39,6 +39,7 @@ namespace Discord.WebSocket | |||||
| private ImmutableArray<GuildEmote> _emotes; | private ImmutableArray<GuildEmote> _emotes; | ||||
| private ImmutableArray<string> _features; | private ImmutableArray<string> _features; | ||||
| private AudioClient _audioClient; | private AudioClient _audioClient; | ||||
| private InviteCache _invites; | |||||
| #pragma warning restore IDISP002, IDISP006 | #pragma warning restore IDISP002, IDISP006 | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -280,6 +281,7 @@ namespace Discord.WebSocket | |||||
| _audioLock = new SemaphoreSlim(1, 1); | _audioLock = new SemaphoreSlim(1, 1); | ||||
| _emotes = ImmutableArray.Create<GuildEmote>(); | _emotes = ImmutableArray.Create<GuildEmote>(); | ||||
| _features = ImmutableArray.Create<string>(); | _features = ImmutableArray.Create<string>(); | ||||
| _invites = new InviteCache(client); | |||||
| } | } | ||||
| internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) | 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) | public Task RemoveBanAsync(ulong userId, RequestOptions options = null) | ||||
| => GuildHelper.RemoveBanAsync(this, Discord, userId, options); | => 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<SocketGuildInvite> 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 | //Channels | ||||
| /// <summary> | /// <summary> | ||||
| /// Gets a channel in this guild. | /// Gets a channel in this guild. | ||||
| @@ -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 | |||||
| { | |||||
| /// <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 using <see cref="Code"/>. | |||||
| /// </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; } | |||||
| } | |||||
| } | |||||
| @@ -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<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. | |||||
| _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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <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); | |||||
| } | |||||
| } | |||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| } | |||||