Add message export for GDPR Art. 15 right of access

The privacy story is incomplete without a way to actually hand the
data over. New Export section in the Privacy tab streams matching
messages to a Markdown, JSON or CSV file using Dalamud's file
dialog and a background thread, so the settings UI stays
responsive even when the export crawls a 150k-message archive.

MessageStore.StreamForExport returns a MessageEnumerator over
non-deleted rows filtered by ChatType list and date range, sorted
ascending. MessageExporter.ExportToFile takes that enumerator,
optionally narrows by SenderSource.TextValue substring (case-
insensitive), and writes one of three formats:

  Markdown — human-readable, day headers, [HH:mm] ChatType Sender:
  prefix per line, trailing total.

  JSON — single object with metadata (filter snapshot, exported_at,
  plugin name) and a messages array carrying id, ISO-8601 date,
  numeric and named ChatType, source/target kinds, receiver,
  content_id, sender plaintext, content plaintext.

  CSV — header line plus quoted-when-needed rows for spreadsheet
  ingestion.

Sender plaintext, channel filter, date range and format are
exposed as form fields above the Export button. Empty channel
selection means "all stored channels", a 0-day range means "no
time limit". Result count and target path are reported via
WrapperUtil notifications.
This commit is contained in:
2026-05-01 20:41:58 +02:00
parent 33cfc7effa
commit 68a6910c53
6 changed files with 534 additions and 0 deletions
+48
View File
@@ -500,6 +500,54 @@ internal class MessageStore : IDisposable
cmd.ExecuteNonQuery();
}
/// <summary>
/// Streams messages for export. Optional filters:
/// - <paramref name="chatTypes"/>: limit to these ChatTypes
/// - <paramref name="from"/> / <paramref name="to"/>: inclusive date range
/// Result is sorted ascending by Date and excludes soft-deleted rows.
/// Caller is responsible for disposing the enumerator.
/// </summary>
internal MessageEnumerator StreamForExport(
IReadOnlyCollection<int>? chatTypes,
DateTimeOffset? from,
DateTimeOffset? to)
{
var clauses = new List<string> { "deleted = false" };
if (chatTypes is { Count: > 0 })
clauses.Add($"ChatType IN ({string.Join(",", chatTypes)})");
if (from is not null)
clauses.Add("Date >= $From");
if (to is not null)
clauses.Add("Date <= $To");
var cmd = Connection.CreateCommand();
cmd.CommandText = @"
SELECT
Id,
Receiver,
ContentId,
Date,
ChatType,
SourceKind,
TargetKind,
Sender,
Content,
SenderSource,
ContentSource,
ExtraChatChannel
FROM messages
WHERE " + string.Join(" AND ", clauses) + @"
ORDER BY Date ASC;";
cmd.CommandTimeout = 600;
if (from is not null)
cmd.Parameters.AddWithValue("$From", from.Value.ToUnixTimeMilliseconds());
if (to is not null)
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
return new MessageEnumerator(cmd.ExecuteReader());
}
/// <summary>
/// Get the most recent messages.
/// </summary>