| @@ -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>>(); | |||
| //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 | |||
| /// <summary> Fired when a user joins a guild. </summary> | |||
| public event Func<SocketGuildUser, Task> UserJoined { | |||
| @@ -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<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) | |||
| case "CHANNEL_PINS_ACK": | |||
| @@ -39,6 +39,7 @@ namespace Discord.WebSocket | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private ImmutableArray<string> _features; | |||
| private AudioClient _audioClient; | |||
| private InviteCache _invites; | |||
| #pragma warning restore IDISP002, IDISP006 | |||
| /// <inheritdoc /> | |||
| @@ -280,6 +281,7 @@ namespace Discord.WebSocket | |||
| _audioLock = new SemaphoreSlim(1, 1); | |||
| _emotes = ImmutableArray.Create<GuildEmote>(); | |||
| _features = ImmutableArray.Create<string>(); | |||
| _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<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 | |||
| /// <summary> | |||
| /// 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); | |||
| } | |||
| } | |||
| } | |||