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