diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b39a49776..cdf0118c4 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -683,6 +683,9 @@ namespace Discord /// /// Downloads all users for this guild if the current list is incomplete. /// + /// + /// This method downloads all users found within this guild throught the Gateway and caches them. + /// /// /// A task that represents the asynchronous download operation. /// diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index bc52dd01c..e2fb25aae 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -57,6 +57,21 @@ namespace Discord /// Task UnpinAsync(RequestOptions options = null); + /// + /// Publishes (crossposts) this message. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for publishing this message. + /// + /// + /// + /// This call will throw an if attempted in a non-news channel. + /// + /// This method will publish (crosspost) the message. Please note, publishing (crossposting), is only available in news channels. + /// + Task CrosspostAsync(RequestOptions options = null); + /// /// Transforms this message's text into a human-readable form by resolving its tags. /// diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a726ef75d..732cb5f17 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -695,6 +695,15 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); } + public async Task CrosspostAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/crosspost", ids, options: options).ConfigureAwait(false); + } //Channel Permissions public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index b29eca62e..57f8b2509 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -44,8 +44,10 @@ namespace Discord.Rest }; return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); } + public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) => DeleteAsync(msg.Channel.Id, msg.Id, client, options); + public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client, RequestOptions options) { @@ -115,6 +117,7 @@ namespace Discord.Rest { await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { @@ -240,6 +243,7 @@ namespace Discord.Rest return tags.ToImmutable(); } + private static int? FindIndex(IReadOnlyList tags, int index) { int i = 0; @@ -253,6 +257,7 @@ namespace Discord.Rest return null; //Overlaps tag before this return i; } + public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags @@ -260,6 +265,7 @@ namespace Discord.Rest .Select(x => x.Key) .ToImmutableArray(); } + public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArray tags) { return tags @@ -279,5 +285,14 @@ namespace Discord.Rest return MessageSource.Bot; return MessageSource.User; } + + public static Task CrosspostAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + => CrosspostAsync(msg.Channel.Id, msg.Id, client, options); + + public static async Task CrosspostAsync(ulong channelId, ulong msgId, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.CrosspostAsync(channelId, msgId, options).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index f457f4f7a..b4a33c76c 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -165,7 +165,7 @@ namespace Discord.Rest IReadOnlyCollection IMessage.Embeds => Embeds; /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); - + /// public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 7d652687a..ad2a65615 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -148,6 +148,18 @@ namespace Discord.Rest TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is RestNewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index be7432bc3..b56498061 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1771,17 +1771,7 @@ namespace Discord.WebSocket return guild; } internal SocketGuild RemoveGuild(ulong id) - { - var guild = State.RemoveGuild(id); - if (guild != null) - { - foreach (var _ in guild.Channels) - State.RemoveChannel(id); - foreach (var user in guild.Users) - user.GlobalUser.RemoveRef(this); - } - return guild; - } + => State.RemoveGuild(id); /// Unexpected channel type is created. internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b26dfe5fb..e1f0f74dc 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -123,7 +123,7 @@ namespace Discord.WebSocket model.Content = text; } } - + /// /// Only the author of a message may modify the message. /// Message content is too long, length must be less or equal to . @@ -147,7 +147,19 @@ namespace Discord.WebSocket public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - + + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is SocketNewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 09c4165f4..b830ce79c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -44,8 +44,11 @@ namespace Discord.WebSocket /// /// Gets mutual guilds shared with this user. /// + /// + /// This property will only include guilds in the same . + /// public IReadOnlyCollection MutualGuilds - => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); + => Discord.Guilds.Where(g => g.GetUser(Id) != null).ToImmutableArray(); internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 9c90df565..353345ded 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -33,7 +33,7 @@ namespace Discord.Webhook : this(webhookUrl, new DiscordRestConfig()) { } // regex pattern to match webhook urls - private static Regex WebhookUrlRegex = new Regex(@"^.*discordapp\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) @@ -132,13 +132,13 @@ namespace Discord.Webhook if (match != null) { // ensure that the first group is a ulong, set the _webhookId - // 0th group is always the entire match, so start at index 1 - if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) + // 0th group is always the entire match, and 1 is the domain; so start at index 2 + if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) throw ex("The webhook Id could not be parsed."); - if (!match.Groups[2].Success) + if (!match.Groups[3].Success) throw ex("The webhook token could not be parsed."); - webhookToken = match.Groups[2].Value; + webhookToken = match.Groups[3].Value; } else throw ex();