From a8895663fe19408363cb2de4348f8a21b211f4d0 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:32:13 +0300 Subject: [PATCH] Add check for message content intent --- .../Discord/Data/Application.cs | 28 ++++++++++ .../Discord/Data/ApplicationFlags.cs | 20 +++++++ .../Discord/Data/Channel.cs | 2 +- .../Discord/Data/Message.cs | 9 ++-- .../Discord/Data/Sticker.cs | 2 +- .../Discord/DiscordClient.cs | 52 ++++++++++++++----- 6 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 DiscordChatExporter.Core/Discord/Data/Application.cs create mode 100644 DiscordChatExporter.Core/Discord/Data/ApplicationFlags.cs diff --git a/DiscordChatExporter.Core/Discord/Data/Application.cs b/DiscordChatExporter.Core/Discord/Data/Application.cs new file mode 100644 index 0000000..230f8e2 --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/Application.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using DiscordChatExporter.Core.Utils.Extensions; +using JsonExtensions.Reading; + +namespace DiscordChatExporter.Core.Discord.Data; + +// https://discord.com/developers/docs/resources/application#application-object +public partial record Application(Snowflake Id, string Name, ApplicationFlags Flags) +{ + public bool IsMessageContentIntentEnabled => + Flags.HasFlag(ApplicationFlags.GatewayMessageContent) + || Flags.HasFlag(ApplicationFlags.GatewayMessageContentLimited); +} + +public partial record Application +{ + public static Application Parse(JsonElement json) + { + var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); + var name = json.GetProperty("name").GetNonWhiteSpaceString(); + + var flags = + json.GetPropertyOrNull("flags")?.GetInt32OrNull()?.Pipe(x => (ApplicationFlags)x) + ?? ApplicationFlags.None; + + return new Application(id, name, flags); + } +} diff --git a/DiscordChatExporter.Core/Discord/Data/ApplicationFlags.cs b/DiscordChatExporter.Core/Discord/Data/ApplicationFlags.cs new file mode 100644 index 0000000..850ea6f --- /dev/null +++ b/DiscordChatExporter.Core/Discord/Data/ApplicationFlags.cs @@ -0,0 +1,20 @@ +using System; + +namespace DiscordChatExporter.Core.Discord.Data; + +// https://discord.com/developers/docs/resources/application#application-object-application-flags +[Flags] +public enum ApplicationFlags +{ + None = 0, + ApplicationAutoModerationRuleCreateBadge = 64, + GatewayPresence = 4096, + GatewayPresenceLimited = 8192, + GatewayGuildMembers = 16384, + GatewayGuildMembersLimited = 32768, + VerificationPendingGuildLimit = 65536, + Embedded = 131072, + GatewayMessageContent = 262144, + GatewayMessageContentLimited = 524288, + ApplicationCommandBadge = 8388608 +} diff --git a/DiscordChatExporter.Core/Discord/Data/Channel.cs b/DiscordChatExporter.Core/Discord/Data/Channel.cs index b11b6f4..70d8f17 100644 --- a/DiscordChatExporter.Core/Discord/Data/Channel.cs +++ b/DiscordChatExporter.Core/Discord/Data/Channel.cs @@ -62,7 +62,7 @@ public partial record Channel public static Channel Parse(JsonElement json, Channel? parent = null, int? positionHint = null) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var kind = (ChannelKind)json.GetProperty("type").GetInt32(); + var kind = json.GetProperty("type").GetInt32().Pipe(t => (ChannelKind)t); var guildId = json.GetPropertyOrNull("guild_id") diff --git a/DiscordChatExporter.Core/Discord/Data/Message.cs b/DiscordChatExporter.Core/Discord/Data/Message.cs index 5a97116..de127d4 100644 --- a/DiscordChatExporter.Core/Discord/Data/Message.cs +++ b/DiscordChatExporter.Core/Discord/Data/Message.cs @@ -118,14 +118,15 @@ public partial record Message public static Message Parse(JsonElement json) { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); - var kind = (MessageKind)json.GetProperty("type").GetInt32(); + var kind = json.GetProperty("type").GetInt32().Pipe(t => (MessageKind)t); + var flags = - (MessageFlags?)json.GetPropertyOrNull("flags")?.GetInt32OrNull() ?? MessageFlags.None; - var author = json.GetProperty("author").Pipe(User.Parse); + json.GetPropertyOrNull("flags")?.GetInt32OrNull()?.Pipe(f => (MessageFlags)f) + ?? MessageFlags.None; + var author = json.GetProperty("author").Pipe(User.Parse); var timestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffsetOrNull(); - var callEndedTimestamp = json.GetPropertyOrNull("call") ?.GetPropertyOrNull("ended_timestamp") ?.GetDateTimeOffsetOrNull(); diff --git a/DiscordChatExporter.Core/Discord/Data/Sticker.cs b/DiscordChatExporter.Core/Discord/Data/Sticker.cs index e048f6e..631312f 100644 --- a/DiscordChatExporter.Core/Discord/Data/Sticker.cs +++ b/DiscordChatExporter.Core/Discord/Data/Sticker.cs @@ -13,7 +13,7 @@ public record Sticker(Snowflake Id, string Name, StickerFormat Format, string So { var id = json.GetProperty("id").GetNonWhiteSpaceString().Pipe(Snowflake.Parse); var name = json.GetProperty("name").GetNonNullString(); - var format = (StickerFormat)json.GetProperty("format_type").GetInt32(); + var format = json.GetProperty("format_type").GetInt32().Pipe(t => (StickerFormat)t); var sourceUrl = ImageCdn.GetStickerUrl( id, diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 169261e..2ae79a9 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -86,10 +86,13 @@ public class DiscordClient ); } - private async ValueTask GetTokenKindAsync( + private async ValueTask ResolveTokenKindAsync( CancellationToken cancellationToken = default ) { + if (_resolvedTokenKind is not null) + return _resolvedTokenKind.Value; + // Try authenticating as a user using var userResponse = await GetResponseAsync( "users/@me", @@ -98,7 +101,7 @@ public class DiscordClient ); if (userResponse.StatusCode != HttpStatusCode.Unauthorized) - return TokenKind.User; + return (_resolvedTokenKind = TokenKind.User).Value; // Try authenticating as a bot using var botResponse = await GetResponseAsync( @@ -108,7 +111,7 @@ public class DiscordClient ); if (botResponse.StatusCode != HttpStatusCode.Unauthorized) - return TokenKind.Bot; + return (_resolvedTokenKind = TokenKind.Bot).Value; throw new DiscordChatExporterException("Authentication token is invalid.", true); } @@ -116,11 +119,12 @@ public class DiscordClient private async ValueTask GetResponseAsync( string url, CancellationToken cancellationToken = default - ) - { - var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken); - return await GetResponseAsync(url, tokenKind, cancellationToken); - } + ) => + await GetResponseAsync( + url, + await ResolveTokenKindAsync(cancellationToken), + cancellationToken + ); private async ValueTask GetJsonResponseAsync( string url, @@ -152,9 +156,9 @@ public class DiscordClient _ => throw new DiscordChatExporterException( $""" - Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}. - Response content: {await response.Content.ReadAsStringAsync(cancellationToken)} - """, + Request to '{url}' failed: {response.StatusCode.ToString().ToSpaceSeparatedWords().ToLowerInvariant()}. + Response content: {await response.Content.ReadAsStringAsync(cancellationToken)} + """, true ) }; @@ -174,6 +178,14 @@ public class DiscordClient : null; } + public async ValueTask GetApplicationAsync( + CancellationToken cancellationToken = default + ) + { + var response = await GetJsonResponseAsync("applications/@me", cancellationToken); + return Application.Parse(response); + } + public async ValueTask TryGetUserAsync( Snowflake userId, CancellationToken cancellationToken = default @@ -285,7 +297,7 @@ public class DiscordClient if (guildId == Guild.DirectMessages.Id) yield break; - var tokenKind = _resolvedTokenKind ??= await GetTokenKindAsync(cancellationToken); + var tokenKind = await ResolveTokenKindAsync(cancellationToken); var channels = (await GetGuildChannelsAsync(guildId, cancellationToken)) // Categories cannot have threads @@ -559,6 +571,22 @@ public class DiscordClient [EnumeratorCancellation] CancellationToken cancellationToken = default ) { + // If authenticating as a bot, ensure that we have the correct permissions to + // retrieve message content. + // https://github.com/Tyrrrz/DiscordChatExporter/issues/1106#issuecomment-1741548959 + var tokenKind = await ResolveTokenKindAsync(cancellationToken); + if (tokenKind == TokenKind.Bot) + { + var application = await GetApplicationAsync(cancellationToken); + if (!application.IsMessageContentIntentEnabled) + { + throw new DiscordChatExporterException( + "Bot account does not have the Message Content Intent enabled.", + true + ); + } + } + // Get the last message in the specified range, so we can later calculate the // progress based on the difference between message timestamps. // This also snapshots the boundaries, which means that messages posted after