diff --git a/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs b/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs new file mode 100644 index 000000000..74263b888 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Invites/TargetUserType.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public enum TargetUserType + { + /// + /// The invite whose target user type is not defined. + /// + Undefined = 0, + /// + /// The invite is for a Go Live stream. + /// + Stream = 1 + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs new file mode 100644 index 000000000..e2ddd8816 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteCreateEvent.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.Gateway +{ + internal class InviteCreateEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("inviter")] + public Optional Inviter { get; set; } + [JsonProperty("max_age")] + public int MaxAge { get; set; } + [JsonProperty("max_uses")] + public int MaxUses { get; set; } + [JsonProperty("target_user")] + public Optional TargetUser { get; set; } + [JsonProperty("target_user_type")] + public Optional TargetUserType { get; set; } + [JsonProperty("temporary")] + public bool Temporary { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs new file mode 100644 index 000000000..54bc75595 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteDeleteEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class InviteDeleteEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 2cd62b3e8..966aec7fa 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -389,5 +389,47 @@ namespace Discord.WebSocket remove { _recipientRemovedEvent.Remove(value); } } internal readonly AsyncEvent> _recipientRemovedEvent = new AsyncEvent>(); + + //Invites + /// + /// Fired when an invite is created. + /// + /// + /// + /// This event is fired when an invite is created. The event handler must return a + /// and accept a as its parameter. + /// + /// + /// The invite created will be passed into the parameter. + /// + /// + public event Func InviteCreated + { + add { _inviteCreatedEvent.Add(value); } + remove { _inviteCreatedEvent.Remove(value); } + } + internal readonly AsyncEvent> _inviteCreatedEvent = new AsyncEvent>(); + /// + /// Fired when an invite is deleted. + /// + /// + /// + /// This event is fired when an invite is deleted. The event handler must return + /// a and accept a and + /// as its parameter. + /// + /// + /// The channel where this invite was created will be passed into the parameter. + /// + /// + /// The code of the deleted invite will be passed into the parameter. + /// + /// + public event Func InviteDeleted + { + add { _inviteDeletedEvent.Add(value); } + remove { _inviteDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent> _inviteDeletedEvent = new AsyncEvent>(); } } diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 930ea1585..a8780a7b0 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -338,6 +338,9 @@ namespace Discord.WebSocket client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user); client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user); + + client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite); + client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite); } //IDiscordClient diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 24ed8d5ff..dfdad99fc 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1688,6 +1688,64 @@ namespace Discord.WebSocket } break; + //Invites + case "INVITE_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + SocketGuildUser inviter = data.Inviter.IsSpecified + ? (guild.GetUser(data.Inviter.Value.Id) ?? guild.AddOrUpdateUser(data.Inviter.Value)) + : null; + + SocketUser target = data.TargetUser.IsSpecified + ? (guild.GetUser(data.TargetUser.Value.Id) ?? (SocketUser)SocketUnknownUser.Create(this, State, data.TargetUser.Value)) + : null; + + var invite = SocketInvite.Create(this, guild, channel, inviter, target, data); + + await TimedInvokeAsync(_inviteCreatedEvent, nameof(InviteCreated), invite).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + case "INVITE_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel) + { + var guild = channel.Guild; + if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_inviteDeletedEvent, nameof(InviteDeleted), channel, data.Code).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; + //Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs new file mode 100644 index 000000000..fa8c56599 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs @@ -0,0 +1,143 @@ +using Discord.Rest; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.Gateway.InviteCreateEvent; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketInvite : SocketEntity, IInviteMetadata + { + private long _createdAtTicks; + + /// + public ulong ChannelId { get; private set; } + /// + /// Gets the channel where this invite was created. + /// + public SocketGuildChannel Channel { get; private set; } + /// + public ulong? GuildId { get; private set; } + /// + /// Gets the guild where this invite was created. + /// + public SocketGuild Guild { get; private set; } + /// + ChannelType IInvite.ChannelType + { + get + { + switch (Channel) + { + case IVoiceChannel voiceChannel: return ChannelType.Voice; + case ICategoryChannel categoryChannel: return ChannelType.Category; + case IDMChannel dmChannel: return ChannelType.DM; + case IGroupChannel groupChannel: return ChannelType.Group; + case SocketNewsChannel socketNewsChannel: return ChannelType.News; + case ITextChannel textChannel: return ChannelType.Text; + default: throw new InvalidOperationException("Invalid channel type."); + } + } + } + /// + string IInvite.ChannelName => Channel.Name; + /// + string IInvite.GuildName => Guild.Name; + /// + int? IInvite.PresenceCount => throw new NotImplementedException(); + /// + int? IInvite.MemberCount => throw new NotImplementedException(); + /// + bool IInviteMetadata.IsRevoked => throw new NotImplementedException(); + /// + public bool IsTemporary { get; private set; } + /// + int? IInviteMetadata.MaxAge { get => MaxAge; } + /// + int? IInviteMetadata.MaxUses { get => MaxUses; } + /// + int? IInviteMetadata.Uses { get => Uses; } + /// + /// Gets the time (in seconds) until the invite expires. + /// + public int MaxAge { get; private set; } + /// + /// Gets the max number of uses this invite may have. + /// + public int MaxUses { get; private set; } + /// + /// Gets the number of times this invite has been used. + /// + public int Uses { get; private set; } + /// + /// Gets the user that created this invite if available. + /// + public SocketGuildUser Inviter { get; private set; } + /// + DateTimeOffset? IInviteMetadata.CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + /// + /// Gets when this invite was created. + /// + public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); + /// + /// Gets the user targeted by this invite if available. + /// + public SocketUser TargetUser { get; private set; } + /// + /// Gets the type of the user targeted by this invite. + /// + public TargetUserType TargetUserType { get; private set; } + + /// + public string Code => Id; + /// + public string Url => $"{DiscordConfig.InviteUrl}{Code}"; + + internal SocketInvite(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, string id) + : base(discord, id) + { + Guild = guild; + Channel = channel; + Inviter = inviter; + TargetUser = target; + } + internal static SocketInvite Create(DiscordSocketClient discord, SocketGuild guild, SocketGuildChannel channel, SocketGuildUser inviter, SocketUser target, Model model) + { + var entity = new SocketInvite(discord, guild, channel, inviter, target, model.Code); + entity.Update(model); + return entity; + } + internal void Update(Model model) + { + ChannelId = model.ChannelId; + GuildId = model.GuildId.IsSpecified ? model.GuildId.Value : Guild.Id; + IsTemporary = model.Temporary; + MaxAge = model.MaxAge; + MaxUses = model.MaxUses; + Uses = model.Uses; + _createdAtTicks = model.CreatedAt.UtcTicks; + TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined; + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => InviteHelper.DeleteAsync(this, Discord, options); + + /// + /// Gets the URL of the invite. + /// + /// + /// A string that resolves to the Url of the invite. + /// + public override string ToString() => Url; + private string DebuggerDisplay => $"{Url} ({Guild?.Name} / {Channel.Name})"; + + /// + IGuild IInvite.Guild => Guild; + /// + IChannel IInvite.Channel => Channel; + /// + IUser IInviteMetadata.Inviter => Inviter; + } +}