Both PRAGMA call sites take values that SQLite does not accept as
bound parameters. ColumnExists takes a hardcoded table name, the
migration call takes a compile-time int from the version sequence.
Comments now state both facts so future readers don't try to wedge a
defensive whitelist into a path that cannot be reached from anywhere
user-controlled.
Per-channel WHERE tuples and the catch-all default-clause now bind
ChatType and cutoff via named parameters instead of being inlined as
literals. Combines BindIntList for the explicit-types exclusion with
explicit AddWithValue for each (type, cutoff) tuple. Behavioural diff
against v0.5.0: none — same retention windows, same cutoff math, just
parameterised.
Migrate CleanupRetainOnly, StreamForExport, CountDateRange, GetDateRange
and GetPagedDateRange from interpolated IN lists onto BindIntList.
Eliminates the string-interpolation pattern for SQL value lists in the
IN-clause sites. Behavioural diff against v0.5.0: none — same enum/byte
values, just bound under named parameters instead of inlined.
Centralised builder for dynamic IN-clauses that binds each value as a
named parameter and returns the comma-joined placeholder string. Used
by the upcoming MessageStore migrations away from string-interpolated
SQL.
Audit findings M-1 and M-2. Two small consistency issues in the
upstream DbViewer paging path that we now own as a fork:
- RowPerPage is a row count and should be an int. The upstream
declaration was 1000.0f, which forced an implicit float divide
in Math.Ceiling and an implicit float-to-integer conversion when
SQLite bound the LIMIT parameter. Switching the constant to int
and casting Count to double right at the division keeps the
ceiling math intact while making the type story honest.
- GetPagedDateRange's SQL uses the placeholder $OffsetCount, but
the matching AddWithValue call passed the unprefixed name
"OffsetCount". Microsoft.Data.Sqlite tolerates this today, so
paging still worked; another provider or a stricter future
version would not. Re-aligned the parameter name with the SQL.
No behavioural change for users — paging continues to return 1000
rows per page. The fixes are kept on the fork rather than offered
upstream because the project's recent triage history makes a
non-trivial PR turnaround unlikely.
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.
Privacy filter trimmed history "by what" — this adds the time axis.
Each ChatType gets its own retention window in days; channels
without an explicit override fall back to a configurable global
default. The master switch defaults to OFF: the plugin never
deletes history without explicit user consent.
MessageStore.DeleteByRetentionPolicy builds an OR'd WHERE clause
over (ChatType = X AND Date < cutoff_X) plus a NOT IN catch-all
for the global default, hard-deletes matches, and only runs VACUUM
when something was actually removed.
Plugin.RunRetentionSweepIfDue runs at most once per 24 hours on a
background thread (off the load path) and persists the timestamp
so subsequent restarts skip the sweep until enough time has
passed. The Privacy tab gains a retention section with the master
switch, default-days input, per-channel override tree, reset
buttons, and a Ctrl+Shift "apply now" action that mirrors the
auto-sweep but on demand.
Spec defaults: Tells 365 days, own-conversation channels (Party,
Cross-Party, Alliance, PvP Team, FC, Linkshells 1-8, Cross-World
Linkshells 1-8, ExtraChat 1-8) 90 days, fallback 30 days.
The privacy filter only catches new messages. Two new MessageStore
methods support a one-shot retroactive sweep: GetMessageCountsByChatType
returns a (ChatType, count) snapshot so the UI can preview the impact,
and CleanupRetainOnly hard-deletes everything outside the supplied
allowlist and runs VACUUM to reclaim disk space.
The Privacy tab gains a new section with a refresh-preview button, a
keep/delete summary, a per-channel breakdown tree, and a Ctrl+Shift
confirm. The cleanup runs on a background thread so a 800+ MB VACUUM
does not block the settings UI; tabs are rebuilt via the framework
thread once the delete finishes. The cleanup deliberately uses the
saved Plugin.Config whitelist (not unsaved Mutable edits) so it stays
consistent with the prospective filter.
Introduce an opt-out channel whitelist so the database only persists
messages from channels the user explicitly wants to keep. Default
profile follows GDPR data minimization: own conversations only
(Tells, Party, FC, Linkshells, Cross-World Linkshells, Alliance,
ExtraChat). Public chat (Say/Shout/Yell), Novice Network, NPC
dialogue and system logs are dropped by default.
The filter sits inside MessageStore.UpsertMessage so any current or
future write path is covered uniformly. Configuration provides an
IsAllowedForStorage(ChatType) helper plus a "persist unknown
channels" failsafe (default off) for ChatTypes added by future
patches.
A new Privacy settings tab exposes the whitelist as grouped
checkboxes with three preset buttons (Privacy-First, Clear all,
Select all). Configuration version bumps from 6 to 7; existing
users are migrated to the Privacy-First defaults on first load
and notified once via the Dalamud notification manager.
Also includes a small .env.example and gitignore hygiene for local
development setup.
If Migrate3 has already applied its schema changes but failed to
bump user_version (e.g. process crashed between ALTER and
SetMigrationVersion), the next run currently hits a duplicate
column error because ALTER TABLE ADD COLUMN is not idempotent in
SQLite.
Detect the recovery case by checking for the presence of the v3
target columns and the absence of the dropped Code column, and
just record the migration version when found.
- /chat2Viewer is now sorted by date
- Echo everything that hasn't any channel set
- Fixed channel inputs are now enforced
- Added a warning for ECL channel mismatch
- Replace LiteDB database engine with Sqlite
Note: old databases will not be deleted
- Message duplication detection improvements
- Tolerate parse errors in release builds, log them