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