From 1ad5cb3164071bc3f9e79e6085c22482eaac9df9 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Fri, 1 May 2026 18:20:09 +0200 Subject: [PATCH] Add privacy filter with channel whitelist (GDPR Art. 25) 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. --- .env.example | 19 +++++ .gitignore | 8 ++ ChatTwo/Configuration.cs | 23 +++++- ChatTwo/MessageStore.cs | 9 +++ ChatTwo/Plugin.cs | 18 +++++ ChatTwo/Privacy/PrivacyDefaults.cs | 46 +++++++++++ ChatTwo/Ui/Settings.cs | 1 + ChatTwo/Ui/SettingsTabs/Privacy.cs | 119 +++++++++++++++++++++++++++++ 8 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 ChatTwo/Privacy/PrivacyDefaults.cs create mode 100644 ChatTwo/Ui/SettingsTabs/Privacy.cs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9f37f8e --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Local development environment template +# +# Copy this file to `.env` and adjust paths to your setup, +# or run: bash scripts/setup-dev-env.sh +# +# `.env` is gitignored — never commit your local paths. +# +# Activate in shell: +# set -a; source .env; set +a +# +# Or use direnv (recommended): +# echo 'dotenv .env' > .envrc && direnv allow + +# Path to Dalamud development DLLs (Dalamud.dll, FFXIVClientStructs.dll, +# Lumina.dll, Lumina.Excel.dll). Required for building ChatTwo.Tests project. +# +# XIVLauncher Core (Linux): ~/.xlcore/dalamud/Hooks/dev +# XIVLauncher (Windows): %AppData%\XIVLauncher\addon\Hooks\dev +DALAMUD_HOME=/path/to/dalamud/dev/dlls diff --git a/.gitignore b/.gitignore index b9fb6f1..c1c3f33 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,14 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Local development environment (HellionChat fork) +.env +.env.bak* +.envrc +!.env.example +.vscode/ +scripts/ + # Packaging pack/ diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 50c4202..e1f7d59 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -33,10 +33,27 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 6; + private const int LatestVersion = 7; public int Version { get; set; } = LatestVersion; + // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default). + // Master-switch defaults to true; set false to restore upstream behavior. + public bool PrivacyFilterEnabled = true; + // Empty set means the migration has not run yet — see Plugin.cs v6→v7. + public HashSet PrivacyPersistChannels = []; + // Failsafe for ChatTypes added by future FFXIV patches we don't know about. + public bool PrivacyPersistUnknownChannels; + + public bool IsAllowedForStorage(ChatType type) + { + if (!PrivacyFilterEnabled) + return true; + if (PrivacyPersistChannels.Contains(type)) + return true; + return PrivacyPersistUnknownChannels; + } + public bool HideChat = true; public bool HideDuringCutscenes = true; public bool HideWhenNotLoggedIn = true; @@ -195,6 +212,10 @@ public class Configuration : IPluginConfiguration WebinterfacePassword = other.WebinterfacePassword; WebinterfacePort = other.WebinterfacePort; WebinterfaceMaxLinesToSend = other.WebinterfaceMaxLinesToSend; + + PrivacyFilterEnabled = other.PrivacyFilterEnabled; + PrivacyPersistChannels = [..other.PrivacyPersistChannels]; + PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels; } } diff --git a/ChatTwo/MessageStore.cs b/ChatTwo/MessageStore.cs index a3d911b..0400cc2 100644 --- a/ChatTwo/MessageStore.cs +++ b/ChatTwo/MessageStore.cs @@ -332,6 +332,15 @@ internal class MessageStore : IDisposable internal void UpsertMessage(Message message) { + // Hellion Chat privacy filter — drop disallowed ChatTypes before + // they reach the storage layer (single source of truth, also + // covers any future write paths e.g. webinterface backfill). + if (!Plugin.Config.IsAllowedForStorage(message.Code.Type)) + { + Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}"); + return; + } + using var cmd = Connection.CreateCommand(); cmd.CommandText = @" INSERT INTO messages ( diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 1844d09..742376f 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -113,6 +113,24 @@ public sealed class Plugin : IDalamudPlugin } #pragma warning restore CS0618 // Type or member is obsolete + // Hellion Chat v6→v7: seed Privacy-First defaults. + if (Config.Version <= 6) + { + Config.PrivacyFilterEnabled = true; + Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist]; + Config.PrivacyPersistUnknownChannels = false; + Config.Version = 7; + SaveConfig(); + + Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification + { + Title = "Hellion Chat", + Content = "Privacy filter activated by default. Settings → Privacy to adjust.", + Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, + InitialDuration = TimeSpan.FromSeconds(15), + }); + } + if (Config.Tabs.Count == 0) Config.Tabs.Add(TabsUtil.VanillaGeneral); diff --git a/ChatTwo/Privacy/PrivacyDefaults.cs b/ChatTwo/Privacy/PrivacyDefaults.cs new file mode 100644 index 0000000..28202f8 --- /dev/null +++ b/ChatTwo/Privacy/PrivacyDefaults.cs @@ -0,0 +1,46 @@ +using ChatTwo.Code; + +namespace ChatTwo.Privacy; + +internal static class PrivacyDefaults +{ + // Privacy-First default whitelist (DSGVO Art. 25 - Privacy by Default). + // Only the player's own conversations are persisted out-of-the-box. + // Public chat (Say/Shout/Yell), Novice Network, NPC dialogue, system + // logs and battle messages are NOT persisted unless the user opts in. + internal static readonly IReadOnlySet PrivacyFirstWhitelist = new HashSet + { + ChatType.TellIncoming, + ChatType.TellOutgoing, + ChatType.Party, + ChatType.CrossParty, + ChatType.Alliance, + ChatType.FreeCompany, + ChatType.FreeCompanyAnnouncement, + ChatType.FreeCompanyLoginLogout, + ChatType.Linkshell1, + ChatType.Linkshell2, + ChatType.Linkshell3, + ChatType.Linkshell4, + ChatType.Linkshell5, + ChatType.Linkshell6, + ChatType.Linkshell7, + ChatType.Linkshell8, + ChatType.CrossLinkshell1, + ChatType.CrossLinkshell2, + ChatType.CrossLinkshell3, + ChatType.CrossLinkshell4, + ChatType.CrossLinkshell5, + ChatType.CrossLinkshell6, + ChatType.CrossLinkshell7, + ChatType.CrossLinkshell8, + ChatType.ExtraChatLinkshell1, + ChatType.ExtraChatLinkshell2, + ChatType.ExtraChatLinkshell3, + ChatType.ExtraChatLinkshell4, + ChatType.ExtraChatLinkshell5, + ChatType.ExtraChatLinkshell6, + ChatType.ExtraChatLinkshell7, + ChatType.ExtraChatLinkshell8, + }; +} diff --git a/ChatTwo/Ui/Settings.cs b/ChatTwo/Ui/Settings.cs index 138d438..96c3ff7 100755 --- a/ChatTwo/Ui/Settings.cs +++ b/ChatTwo/Ui/Settings.cs @@ -40,6 +40,7 @@ public sealed class SettingsWindow : Window new Fonts(Mutable), new ChatColours(Plugin, Mutable), new Tabs(Plugin, Mutable), + new SettingsTabs.Privacy(Mutable), new Database(Plugin, Mutable), new Webinterface(Plugin, Mutable), new Miscellaneous(Mutable), diff --git a/ChatTwo/Ui/SettingsTabs/Privacy.cs b/ChatTwo/Ui/SettingsTabs/Privacy.cs new file mode 100644 index 0000000..9f9db75 --- /dev/null +++ b/ChatTwo/Ui/SettingsTabs/Privacy.cs @@ -0,0 +1,119 @@ +using ChatTwo.Code; +using ChatTwo.Privacy; +using ChatTwo.Util; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Bindings.ImGui; + +namespace ChatTwo.Ui.SettingsTabs; + +internal sealed class Privacy : ISettingsTab +{ + private Configuration Mutable { get; } + + public string Name => "Privacy###tabs-privacy"; + + internal Privacy(Configuration mutable) + { + Mutable = mutable; + } + + // Channels grouped for the UI. Order = display order. + private static readonly (string Heading, ChatType[] Types)[] Groups = + [ + ("Direct Messages", [ChatType.TellIncoming, ChatType.TellOutgoing]), + ("Party & Alliance", [ChatType.Party, ChatType.CrossParty, ChatType.Alliance, ChatType.PvpTeam]), + ("Free Company", [ChatType.FreeCompany, ChatType.FreeCompanyAnnouncement, ChatType.FreeCompanyLoginLogout]), + ("Linkshells", [ + ChatType.Linkshell1, ChatType.Linkshell2, ChatType.Linkshell3, ChatType.Linkshell4, + ChatType.Linkshell5, ChatType.Linkshell6, ChatType.Linkshell7, ChatType.Linkshell8, + ]), + ("Cross-World Linkshells", [ + ChatType.CrossLinkshell1, ChatType.CrossLinkshell2, ChatType.CrossLinkshell3, ChatType.CrossLinkshell4, + ChatType.CrossLinkshell5, ChatType.CrossLinkshell6, ChatType.CrossLinkshell7, ChatType.CrossLinkshell8, + ]), + ("ExtraChat (Encrypted)", [ + ChatType.ExtraChatLinkshell1, ChatType.ExtraChatLinkshell2, ChatType.ExtraChatLinkshell3, ChatType.ExtraChatLinkshell4, + ChatType.ExtraChatLinkshell5, ChatType.ExtraChatLinkshell6, ChatType.ExtraChatLinkshell7, ChatType.ExtraChatLinkshell8, + ]), + ("Public Chat (third-party data)", [ChatType.Say, ChatType.Shout, ChatType.Yell, ChatType.NoviceNetwork, ChatType.CustomEmote, ChatType.StandardEmote]), + ("System & Game Logs", [ + ChatType.System, ChatType.Notice, ChatType.Urgent, ChatType.Echo, + ChatType.NpcDialogue, ChatType.NpcAnnouncement, + ChatType.LootNotice, ChatType.LootRoll, ChatType.RetainerSale, + ChatType.Crafting, ChatType.Gathering, ChatType.Sign, ChatType.RandomNumber, + ]), + ]; + + public void Draw(bool changed) + { + ImGuiUtil.OptionCheckbox( + ref Mutable.PrivacyFilterEnabled, + "Enable privacy filter", + "When enabled, only messages from whitelisted channels are persisted to the database. " + + "Disabling restores upstream ChatTwo behavior (everything except battle messages is stored)."); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + using (ImRaii.Disabled(!Mutable.PrivacyFilterEnabled)) + { + ImGuiUtil.HelpText( + "Pick which channels are stored in the local database. " + + "Privacy-First default: only your own conversations. " + + "Use the buttons below to apply a preset."); + + ImGui.Spacing(); + + if (ImGui.Button("Privacy-First (recommended)")) + Mutable.PrivacyPersistChannels = [..PrivacyDefaults.PrivacyFirstWhitelist]; + + ImGui.SameLine(); + if (ImGui.Button("Clear all")) + Mutable.PrivacyPersistChannels.Clear(); + + ImGui.SameLine(); + if (ImGui.Button("Select all")) + foreach (var group in Groups) + foreach (var t in group.Types) + Mutable.PrivacyPersistChannels.Add(t); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + foreach (var (heading, types) in Groups) + { + using var tree = ImRaii.TreeNode(heading); + if (!tree.Success) + continue; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + foreach (var type in types) + { + var enabled = Mutable.PrivacyPersistChannels.Contains(type); + var label = type.ToString(); + if (ImGui.Checkbox($"{label}##privacy-{(int)type}", ref enabled)) + { + if (enabled) + Mutable.PrivacyPersistChannels.Add(type); + else + Mutable.PrivacyPersistChannels.Remove(type); + } + } + } + } + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + ImGuiUtil.OptionCheckbox( + ref Mutable.PrivacyPersistUnknownChannels, + "Persist unknown channel types", + "Failsafe for ChatTypes added by future FFXIV patches that this plugin does not yet know about. " + + "Default OFF (Privacy-First). Turn ON if you want a complete log including future channels."); + } + } +}