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."); + } + } +}