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
+17
View File
@@ -113,4 +113,21 @@ internal class HellionStrings
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
internal static string Export_Heading => Get(nameof(Export_Heading));
internal static string Export_Help => Get(nameof(Export_Help));
internal static string Export_Range_Label => Get(nameof(Export_Range_Label));
internal static string Export_Sender_Label => Get(nameof(Export_Sender_Label));
internal static string Export_Channels_Heading => Get(nameof(Export_Channels_Heading));
internal static string Export_Channels_AllOff => Get(nameof(Export_Channels_AllOff));
internal static string Export_Format_Label => Get(nameof(Export_Format_Label));
internal static string Export_Format_Markdown => Get(nameof(Export_Format_Markdown));
internal static string Export_Format_Json => Get(nameof(Export_Format_Json));
internal static string Export_Format_Csv => Get(nameof(Export_Format_Csv));
internal static string Export_Button => Get(nameof(Export_Button));
internal static string Export_Dialog_Title => Get(nameof(Export_Dialog_Title));
internal static string Export_Running => Get(nameof(Export_Running));
internal static string Export_Success => Get(nameof(Export_Success));
internal static string Export_Empty => Get(nameof(Export_Empty));
internal static string Export_Error => Get(nameof(Export_Error));
}
+48
View File
@@ -216,4 +216,52 @@
<data name="Wizard_Reopen_Button" xml:space="preserve">
<value>Wizard erneut zeigen</value>
</data>
<data name="Export_Heading" xml:space="preserve">
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
</data>
<data name="Export_Help" xml:space="preserve">
<value>Gespeicherte Nachrichten als Markdown, JSON oder CSV exportieren. Damit kannst du einer Auskunftsanfrage einer Person nachkommen, deren Nachrichten du gespeichert hast, oder deine eigene Historie mitnehmen.</value>
</data>
<data name="Export_Range_Label" xml:space="preserve">
<value>Letzte X Tage (0 = ohne Zeitlimit)</value>
</data>
<data name="Export_Sender_Label" xml:space="preserve">
<value>Sender enthält (optional, Groß-/Kleinschreibung egal)</value>
</data>
<data name="Export_Channels_Heading" xml:space="preserve">
<value>Auf Kanäle einschränken</value>
</data>
<data name="Export_Channels_AllOff" xml:space="preserve">
<value>(nichts ausgewählt = alle gespeicherten Kanäle)</value>
</data>
<data name="Export_Format_Label" xml:space="preserve">
<value>Format</value>
</data>
<data name="Export_Format_Markdown" xml:space="preserve">
<value>Markdown</value>
</data>
<data name="Export_Format_Json" xml:space="preserve">
<value>JSON</value>
</data>
<data name="Export_Format_Csv" xml:space="preserve">
<value>CSV</value>
</data>
<data name="Export_Button" xml:space="preserve">
<value>In Datei exportieren…</value>
</data>
<data name="Export_Dialog_Title" xml:space="preserve">
<value>Export speichern</value>
</data>
<data name="Export_Running" xml:space="preserve">
<value>Export läuft im Hintergrund…</value>
</data>
<data name="Export_Success" xml:space="preserve">
<value>Export abgeschlossen, {0:N0} Nachrichten in {1} geschrieben</value>
</data>
<data name="Export_Empty" xml:space="preserve">
<value>Export abgeschlossen, keine Nachricht passte zum Filter.</value>
</data>
<data name="Export_Error" xml:space="preserve">
<value>Export fehlgeschlagen, siehe /xllog</value>
</data>
</root>
+48
View File
@@ -216,4 +216,52 @@
<data name="Wizard_Reopen_Button" xml:space="preserve">
<value>Show wizard again</value>
</data>
<data name="Export_Heading" xml:space="preserve">
<value>Export (GDPR Art. 15 — right of access)</value>
</data>
<data name="Export_Help" xml:space="preserve">
<value>Export stored messages to Markdown, JSON or CSV. Use this to fulfil a request for access from someone whose messages you have, or to take your own history with you.</value>
</data>
<data name="Export_Range_Label" xml:space="preserve">
<value>Last X days (0 = all time)</value>
</data>
<data name="Export_Sender_Label" xml:space="preserve">
<value>Sender contains (optional, case-insensitive)</value>
</data>
<data name="Export_Channels_Heading" xml:space="preserve">
<value>Limit to channels</value>
</data>
<data name="Export_Channels_AllOff" xml:space="preserve">
<value>(none selected = all stored channels)</value>
</data>
<data name="Export_Format_Label" xml:space="preserve">
<value>Format</value>
</data>
<data name="Export_Format_Markdown" xml:space="preserve">
<value>Markdown</value>
</data>
<data name="Export_Format_Json" xml:space="preserve">
<value>JSON</value>
</data>
<data name="Export_Format_Csv" xml:space="preserve">
<value>CSV</value>
</data>
<data name="Export_Button" xml:space="preserve">
<value>Export to file…</value>
</data>
<data name="Export_Dialog_Title" xml:space="preserve">
<value>Save export</value>
</data>
<data name="Export_Running" xml:space="preserve">
<value>Export running in background…</value>
</data>
<data name="Export_Success" xml:space="preserve">
<value>Export complete: {0:N0} messages written to {1}</value>
</data>
<data name="Export_Empty" xml:space="preserve">
<value>Export complete: no messages matched the filter.</value>
</data>
<data name="Export_Error" xml:space="preserve">
<value>Export failed, see /xllog</value>
</data>
</root>