diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 6bb44368b..4266f893a 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -7,12 +7,12 @@ namespace Discord { /// Gets the type of this system message. MessageType Type { get; } + /// Gets the source of this message. + MessageSource Source { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns true if this message was added to its channel's pinned messages. bool IsPinned { get; } - /// Returns true if this message was created using a webhook. - bool IsWebhook { get; } /// Returns the content for this message. string Content { get; } /// Gets the time this message was sent. @@ -24,8 +24,6 @@ namespace Discord IMessageChannel Channel { get; } /// Gets the author of this message. IUser Author { get; } - /// Gets the id of the webhook used to created this message, if any. - ulong? WebhookId { get; } /// Returns all attachments included in this message. IReadOnlyCollection Attachments { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs new file mode 100644 index 000000000..1cb2f8b94 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum MessageSource + { + System, + User, + Bot, + Webhook + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 2824a1426..054b80119 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,22 +7,24 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - //TODO: C#7 Candidate for binary literals - private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001011100110000000000", 2)); - private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000010001", 2)); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000001111111110001010001", 2)); - private static ChannelPermissions _allGroup { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001111110110000000000", 2)); - /// Gets a blank ChannelPermissions that grants no permissions. - public static ChannelPermissions None { get; } = new ChannelPermissions(); + public static readonly ChannelPermissions None = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for text channels. + public static readonly ChannelPermissions Text = new ChannelPermissions(0b00100_0000000_1111111110001_010001); + /// Gets a ChannelPermissions that grants all permissions for voice channels. + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); + /// Gets a ChannelPermissions that grants all permissions for direct message channels. + public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); + /// Gets a ChannelPermissions that grants all permissions for group channels. + public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); /// Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { //TODO: C#7 Candidate for typeswitch - if (channel is ITextChannel) return _allText; - if (channel is IVoiceChannel) return _allVoice; - if (channel is IDMChannel) return _allDM; - if (channel is IGroupChannel) return _allGroup; + if (channel is ITextChannel) return Text; + if (channel is IVoiceChannel) return Voice; + if (channel is IDMChannel) return DM; + if (channel is IGroupChannel) return Group; throw new ArgumentException("Unknown channel type", nameof(channel)); } @@ -77,7 +79,7 @@ namespace Discord /// Creates a new ChannelPermissions with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } - private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, + private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, @@ -111,25 +113,26 @@ namespace Discord } /// Creates a new ChannelPermissions with the provided permissions. - public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, + public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) { } + : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) + { } /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks); public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index e7461915c..c5f1efab0 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -9,9 +8,10 @@ namespace Discord { /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); - /// Gets a GuildPermissions that grants all permissions. - //TODO: C#7 Candidate for binary literals - public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110001111111", 2)); + /// Gets a GuildPermissions that grants all guild permissions for webhook users. + public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); + /// Gets a GuildPermissions that grants all guild permissions. + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_0111111110001_111111); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 62060da22..45d8862f1 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -12,8 +12,10 @@ namespace Discord string Discriminator { get; } /// Gets the per-username unique id for this user. ushort DiscriminatorValue { get; } - /// Returns true if this user is a bot account. + /// Returns true if this user is a bot user. bool IsBot { get; } + /// Returns true if this user is a webhook user. + bool IsWebhook { get; } /// Gets the username for this user. string Username { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs new file mode 100644 index 000000000..8f4d42187 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + //TODO: Add webhook endpoints + public interface IWebhookUser : IGuildUser + { + ulong WebhookId { get; } + } +} diff --git a/src/Discord.Net.Core/Utils/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs index a7fdeddf2..12a1fba9c 100644 --- a/src/Discord.Net.Core/Utils/AsyncEvent.cs +++ b/src/Discord.Net.Core/Utils/AsyncEvent.cs @@ -37,11 +37,8 @@ namespace Discord public static async Task InvokeAsync(this AsyncEvent> eventHandler) { var subscribers = eventHandler.Subscriptions; - if (subscribers.Count > 0) - { - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke().ConfigureAwait(false); - } + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) { diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index e2d55cf7f..df927b7ea 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -51,5 +51,9 @@ namespace Discord { public static Optional Create() => Optional.Unspecified; public static Optional Create(T value) => new Optional(value); + + public static T? ToNullable(this Optional val) + where T : struct + => val.IsSpecified ? val.Value : (T?)null; } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a99c64094..b69b103e1 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -86,12 +86,16 @@ namespace Discord [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); + public static ChannelPermissions ToChannelPerms(IGuildChannel channel, ulong guildPermissions) + => new ChannelPermissions(guildPermissions & ChannelPermissions.All(channel).RawValue); public static ulong ResolveGuild(IGuild guild, IGuildUser user) { ulong resolvedPermissions = 0; if (user.Id == guild.OwnerId) resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions + else if (user.IsWebhook) + resolvedPermissions = GuildPermissions.Webhook.RawValue; else { foreach (var roleId in user.RoleIds) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 07bdfe0eb..efcadac0d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -91,7 +91,7 @@ namespace Discord.Rest var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, @@ -119,7 +119,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -147,7 +147,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -264,13 +264,13 @@ namespace Discord.Rest => new TypingNotifier(client, channel, options); //Helpers - private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model) + private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { IUser author = null; if (guild != null) author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; if (author == null) - author = RestUser.Create(client, model); + author = RestUser.Create(client, guild, model, webhookId); return author; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 2c6f67477..f347563ad 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -67,7 +67,7 @@ namespace Discord.Rest await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ImmutableArray userMentions) + public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection userMentions) { var tags = ImmutableArray.CreateBuilder(); @@ -156,5 +156,16 @@ namespace Discord.Rest .Where(x => x != null) .ToImmutableArray(); } + + public static MessageSource GetSource(Model msg) + { + if (msg.Type != MessageType.Default) + return MessageSource.System; + else if (msg.WebhookId.IsSpecified) + return MessageSource.Webhook; + else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) + return MessageSource.Bot; + return MessageSource.User; + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index b50edf03b..bdd4800c1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -13,6 +13,7 @@ namespace Discord.Rest public IMessageChannel Channel { get; } public IUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } @@ -26,16 +27,15 @@ namespace Discord.Rest public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index a5ced8c8f..b9dda08ae 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.Rest public MessageType Type { get; private set; } internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index a9197188e..00ab0c299 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -13,7 +13,6 @@ namespace Discord.Rest { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -21,7 +20,6 @@ namespace Discord.Rest public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -31,13 +29,13 @@ namespace Discord.Rest public override IReadOnlyCollection Tags => _tags; public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); - internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - var entity = new RestUserMessage(discord, model.Id, channel, author); + var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); return entity; } @@ -54,8 +52,6 @@ namespace Discord.Rest _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 0acfe3ddf..e9b3b39ea 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,21 +13,26 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Offline; + public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) : base(discord, id) { } internal static RestUser Create(BaseDiscordClient discord, Model model) + => Create(discord, null, model, null); + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? webhookId) { - var entity = new RestUser(discord, model.Id); + RestUser entity; + if (webhookId.HasValue) + entity = new RestWebhookUser(discord, guild, model.Id, webhookId.Value); + else + entity = new RestUser(discord, model.Id); entity.Update(model); return entity; } @@ -52,6 +57,9 @@ namespace Discord.Rest public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs new file mode 100644 index 000000000..ae794becc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhookUser : RestUser, IWebhookUser + { + public ulong WebhookId { get; } + internal IGuild Guild { get; } + + public override bool IsWebhook => true; + public ulong GuildId => Guild.Id; + + internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) + : base(discord, id) + { + Guild = guild; + WebhookId = webhookId; + } + internal static RestWebhookUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong webhookId) + { + var entity = new RestWebhookUser(discord, guild, model.Id, webhookId); + entity.Update(model); + return entity; + } + + //IGuildUser + IGuild IGuildUser.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs index b85071f2a..c77c06288 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -13,6 +13,7 @@ namespace Discord.Rpc public IMessageChannel Channel { get; } public RpcUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } public Color AuthorColor { get; private set; } @@ -33,11 +34,12 @@ namespace Discord.Rpc public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) + internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RpcMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs index e8c918bc6..39c6026a7 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -10,14 +10,14 @@ namespace Discord.Rpc public MessageType Type { get; private set; } internal RpcSystemMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RpcSystemMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcSystemMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable())); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 23d277dd9..cdcff4a07 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -31,15 +31,16 @@ namespace Discord.Rpc public override IReadOnlyCollection Tags => _tags; public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); - internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) + : base(discord, id, channel, author, source) { } internal new static RpcUserMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcUserMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable()), + MessageHelper.GetSource(model)); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index daf299e2a..cf21928bb 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,11 +14,10 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); + public virtual bool IsWebhook => false; public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Offline; @@ -27,8 +26,14 @@ namespace Discord.Rpc { } internal static RpcUser Create(DiscordRpcClient discord, Model model) + => Create(discord, model, null); + internal static RpcUser Create(DiscordRpcClient discord, Model model, ulong? webhookId) { - var entity = new RpcUser(discord, model.Id); + RpcUser entity; + if (webhookId.HasValue) + entity = new RpcWebhookUser(discord, model.Id, webhookId.Value); + else + entity = new RpcUser(discord, model.Id); entity.Update(model); return entity; } @@ -47,6 +52,9 @@ namespace Discord.Rpc public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs new file mode 100644 index 000000000..9ea4312c2 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcWebhookUser : RpcUser + { + public ulong WebhookId { get; } + + public override bool IsWebhook => true; + + internal RpcWebhookUser(DiscordRpcClient discord, ulong id, ulong webhookId) + : base(discord, id) + { + WebhookId = webhookId; + } + internal static RpcWebhookUser Create(DiscordRpcClient discord, Model model, ulong webhookId) + { + var entity = new RpcWebhookUser(discord, model.Id, webhookId); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 9fb172612..093339028 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1045,8 +1045,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); return; } - - await _userBannedEvent.InvokeAsync(SocketSimpleUser.Create(this, State, data.User), guild).ConfigureAwait(false); + + SocketUser user = guild.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await _userBannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else { @@ -1071,7 +1074,7 @@ namespace Discord.WebSocket SocketUser user = State.GetUser(data.User.Id); if (user == null) - user = SocketSimpleUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, State, data.User); await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else @@ -1098,8 +1101,16 @@ namespace Discord.WebSocket return; } - var author = (guild != null ? guild.GetUser(data.Author.Value.Id) : (channel as SocketChannel).GetUser(data.Author.Value.Id)) ?? - SocketSimpleUser.Create(this, State, data.Author.Value); + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); if (author != null) { @@ -1153,7 +1164,7 @@ namespace Discord.WebSocket else author = (channel as SocketChannel).GetUser(data.Author.Value.Id); if (author == null) - author = SocketSimpleUser.Create(this, State, data.Author.Value); + author = SocketUnknownUser.Create(this, State, data.Author.Value); after = SocketMessage.Create(this, State, author, channel, data); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 0b09d2d22..2d63665de 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -14,6 +14,7 @@ namespace Discord.WebSocket public SocketUser Author { get; } public ISocketMessageChannel Channel { get; } + public MessageSource Source { get; } public string Content { get; private set; } @@ -27,16 +28,15 @@ namespace Discord.WebSocket public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index 50cdb964b..e6c67159f 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.WebSocket public MessageType Type { get; private set; } internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 93085234e..8b9acf118 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -14,7 +14,6 @@ namespace Discord.WebSocket { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -22,7 +21,6 @@ namespace Discord.WebSocket public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -32,13 +30,13 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); - internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { - var entity = new SocketUserMessage(discord, model.Id, channel, author); + var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); return entity; } @@ -55,8 +53,6 @@ namespace Discord.WebSocket _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { @@ -86,18 +82,18 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - ImmutableArray mentions = ImmutableArray.Create(); + IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection if (model.UserMentions.IsSpecified) { var value = model.UserMentions.Value; if (value.Length > 0) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); + var newMentions = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) { var val = value[i]; if (val.Object != null) - newMentions.Add(SocketSimpleUser.Create(Discord, Discord.State, val.Object)); + newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); } mentions = newMentions.ToImmutable(); } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 4870937a1..496ca7073 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -11,9 +11,10 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } public SocketDMChannel DMChannel { get; internal set; } + internal override SocketPresence Presence { get; set; } + public override bool IsWebhook => false; internal override SocketGlobalUser GlobalUser => this; - internal override SocketPresence Presence { get; set; } private readonly object _lockObj = new object(); private ushort _references; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 694d0ccb9..8d1b360e3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -15,6 +15,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) : base(channel.Discord, globalUser.Id) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 387a96f0a..d20fd0d33 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -28,6 +28,7 @@ namespace Discord.WebSocket public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 0f6d4e4f1..c0e483a56 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -20,6 +20,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) : base(discord, globalUser.Id) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs similarity index 70% rename from src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs rename to src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 0d2198888..57ff81433 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -6,23 +6,25 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketSimpleUser : SocketUser + public class SocketUnknownUser : SocketUser { - public override bool IsBot { get; internal set; } public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + public override bool IsBot { get; internal set; } - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + public override bool IsWebhook => false; + + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } - internal SocketSimpleUser(DiscordSocketClient discord, ulong id) + internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketSimpleUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketSimpleUser(discord, model.Id); + var entity = new SocketUnknownUser(discord, model.Id); entity.Update(state, model); return entity; } @@ -39,6 +41,6 @@ namespace Discord.WebSocket Username = model.User.Username.Value; } - internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; + internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 5c73e3b6a..6e97d4b31 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -12,11 +12,10 @@ namespace Discord.WebSocket public abstract string Username { get; internal set; } public abstract ushort DiscriminatorValue { get; internal set; } public abstract string AvatarId { get; internal set; } + public abstract bool IsWebhook { get; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -47,6 +46,9 @@ namespace Discord.WebSocket public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs new file mode 100644 index 000000000..1193eca8f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketWebhookUser : SocketUser, IWebhookUser + { + public SocketGuild Guild { get; } + public ulong WebhookId { get; } + + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + public override bool IsBot { get; internal set; } + + public override bool IsWebhook => true; + + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + + internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) + : base(guild.Discord, id) + { + WebhookId = webhookId; + } + internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) + { + var entity = new SocketWebhookUser(guild, model.Id, webhookId); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, PresenceModel model) + { + if (model.User.Avatar.IsSpecified) + AvatarId = model.User.Avatar.Value; + if (model.User.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.User.Discriminator.Value); + if (model.User.Bot.IsSpecified) + IsBot = model.User.Bot.Value; + if (model.User.Username.IsSpecified) + Username = model.User.Username.Value; + } + + internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; + + + //IGuildUser + IGuild IGuildUser.Guild => Guild; + ulong IGuildUser.GuildId => Guild.Id; + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +}