Don't consider it an error if there is nothing to export (#1349)
Some checks are pending
docker / pack (push) Waiting to run
docker / deploy (push) Waiting to run
main / format (push) Waiting to run
main / test (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-musl-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x86) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-musl-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, linux-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, osx-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, osx-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-arm64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-x64) (push) Waiting to run
main / pack (DiscordChatExporter.Gui, DiscordChatExporter, win-x86) (push) Waiting to run
main / release (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-musl-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, linux-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, osx-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Cli, DiscordChatExporter.Cli, win-x86) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-musl-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, linux-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, osx-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, osx-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-arm64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-x64) (push) Blocked by required conditions
main / deploy (DiscordChatExporter.Gui, DiscordChatExporter, win-x86) (push) Blocked by required conditions
main / notify (push) Blocked by required conditions

This commit is contained in:
Leonardo Mosquera 2025-04-01 18:14:35 -03:00 committed by GitHub
parent cf7580014c
commit 7add81a472
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 110 additions and 49 deletions

View file

@ -146,4 +146,33 @@ public class DateRangeSpecs
.WhenTypeIs<DateTimeOffset>()
);
}
[Fact]
public async Task Export_file_is_created_even_when_nothing_to_export()
{
var long_in_the_past = new DateTimeOffset(1921, 08, 01, 0, 0, 0, TimeSpan.Zero);
// Arrange
var before = long_in_the_past;
using var file = TempFile.Create();
// Act
await new ExportChannelsCommand
{
Token = Secrets.DiscordToken,
ChannelIds = [ChannelIds.DateRangeTestCases],
ExportFormat = ExportFormat.Json,
OutputPath = file.Path,
Before = Snowflake.FromDate(before),
}.ExecuteAsync(new FakeConsole());
// Assert
var timestamps = Json.Parse(await File.ReadAllTextAsync(file.Path))
.GetProperty("messages")
.EnumerateArray()
.Select(j => j.GetProperty("timestamp").GetDateTimeOffset())
.ToArray();
timestamps.Should().BeEmpty();
}
}

View file

@ -179,6 +179,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
// Export
var cancellationToken = console.RegisterCancellationHandler();
var errorsByChannel = new ConcurrentDictionary<Channel, string>();
var warningsByChannel = new ConcurrentDictionary<Channel, string>();
await console.Output.WriteLineAsync($"Exporting {channels.Count} channel(s)...");
await console
@ -236,6 +237,10 @@ public abstract class ExportCommandBase : DiscordCommandBase
}
);
}
catch (ChannelEmptyException ex)
{
warningsByChannel[channel] = ex.Message;
}
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{
errorsByChannel[channel] = ex.Message;
@ -252,6 +257,28 @@ public abstract class ExportCommandBase : DiscordCommandBase
);
}
// Print warnings
if (warningsByChannel.Any())
{
await console.Output.WriteLineAsync();
using (console.WithForegroundColor(ConsoleColor.Yellow))
{
await console.Error.WriteLineAsync(
$"Warnings reported for the following channel(s):"
);
}
foreach (var (channel, message) in warningsByChannel)
{
await console.Error.WriteAsync($"{channel.GetHierarchicalName()}: ");
using (console.WithForegroundColor(ConsoleColor.Yellow))
await console.Error.WriteLineAsync(message);
}
await console.Error.WriteLineAsync();
}
// Print errors
if (errorsByChannel.Any())
{
@ -259,16 +286,14 @@ public abstract class ExportCommandBase : DiscordCommandBase
using (console.WithForegroundColor(ConsoleColor.Red))
{
await console.Error.WriteLineAsync(
$"Failed to export {errorsByChannel.Count} the following channel(s):"
);
await console.Error.WriteLineAsync($"Failed to export the following channel(s):");
}
foreach (var (channel, error) in errorsByChannel)
foreach (var (channel, message) in errorsByChannel)
{
await console.Error.WriteAsync($"{channel.GetHierarchicalName()}: ");
using (console.WithForegroundColor(ConsoleColor.Red))
await console.Error.WriteLineAsync(error);
await console.Error.WriteLineAsync(message);
}
await console.Error.WriteLineAsync();

View file

@ -0,0 +1,8 @@
using System;
namespace DiscordChatExporter.Core.Exceptions;
// Thrown when there is circumstancially no message to export with given parameters,
// though it should not be treated as a runtime error; simply warn instead
public class ChannelEmptyException(string message)
: DiscordChatExporterException(message, false, null) { }

View file

@ -27,45 +27,42 @@ public class ChannelExporter(DiscordClient discord)
);
}
// Check if the channel is empty
if (request.Channel.IsEmpty)
{
throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages."
);
}
// Check if the 'after' boundary is valid
if (request.After is not null && !request.Channel.MayHaveMessagesAfter(request.After.Value))
{
throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages within the specified period."
);
}
// Check if the 'before' boundary is valid
if (
request.Before is not null
&& !request.Channel.MayHaveMessagesBefore(request.Before.Value)
)
{
throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages within the specified period."
);
}
// Build context
var context = new ExportContext(discord, request);
await context.PopulateChannelsAndRolesAsync(cancellationToken);
// Export messages
await using var messageExporter = new MessageExporter(context);
// Check if the channel is empty
if (request.Channel.IsEmpty)
{
throw new ChannelEmptyException(
$"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages; an empty file will be created."
);
}
// Check if the 'before' and 'after' boundaries are valid
if (
(
request.Before is not null
&& !request.Channel.MayHaveMessagesBefore(request.Before.Value)
)
|| (
request.After is not null
&& !request.Channel.MayHaveMessagesAfter(request.After.Value)
)
)
{
throw new ChannelEmptyException(
$"Channel '{request.Channel.Name}' "
+ $"of guild '{request.Guild.Name}' "
+ $"does not contain any messages within the specified period; an empty file will be created."
);
}
await foreach (
var message in discord.GetMessagesAsync(
request.Channel.Id,
@ -98,15 +95,5 @@ public class ChannelExporter(DiscordClient discord)
);
}
}
// Throw if no messages were exported
if (messageExporter.MessagesExported <= 0)
{
throw new DiscordChatExporterException(
$"Channel '{request.Channel.Name}' (#{request.Channel.Id}) "
+ $"of guild '{request.Guild.Name}' (#{request.Guild.Id}) "
+ $"does not contain any matching messages within the specified period."
);
}
}
}

View file

@ -70,7 +70,12 @@ internal partial class MessageExporter(ExportContext context) : IAsyncDisposable
MessagesExported++;
}
public async ValueTask DisposeAsync() => await ResetWriterAsync();
public async ValueTask DisposeAsync()
{
// causes the file to be created whether there were messages written or not
await GetWriterAsync();
await ResetWriterAsync();
}
}
internal partial class MessageExporter

View file

@ -283,6 +283,13 @@ public partial class DashboardViewModel : ViewModelBase
Interlocked.Increment(ref successfulExportCount);
}
catch (ChannelEmptyException ex)
{
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
// FIXME: not exactly successful, but not a failure either. Not ideal to duplicate the line
Interlocked.Increment(ref successfulExportCount);
}
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
{
_snackbarManager.Notify(ex.Message.TrimEnd('.'));