diff --git a/PolyTraderSharp.sln b/PolyTraderSharp.sln index 73be81a..746a137 100644 --- a/PolyTraderSharp.sln +++ b/PolyTraderSharp.sln @@ -5,12 +5,6 @@ VisualStudioVersion = 17.14.36915.13 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.App", "PolyTrader.App.csproj", "{8B91AC5C-8A7C-47BF-B651-6CF3B60FEC84}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libs", "libs", "{6C4D298B-81FB-2B6B-5AB6-46205EDDC72C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Threema-MsgApi-Net-Core", "Threema-MsgApi-Net-Core", "{35072DD7-6FE3-A86A-51D9-DD886E81202C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IcgSoftware.Threema.CoreMsgApi", "libs\Threema-MsgApi-Net-Core\IcgSoftware.Threema.CoreMsgApi\IcgSoftware.Threema.CoreMsgApi.csproj", "{EFAE2853-E52F-4163-8266-3859C7748B85}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Core", "src\PolyTrader.Core\PolyTrader.Core.csproj", "{3502A702-FA60-456E-BF23-05EAD02465E6}" @@ -49,18 +43,6 @@ Global {8B91AC5C-8A7C-47BF-B651-6CF3B60FEC84}.Release|x64.Build.0 = Release|Any CPU {8B91AC5C-8A7C-47BF-B651-6CF3B60FEC84}.Release|x86.ActiveCfg = Release|Any CPU {8B91AC5C-8A7C-47BF-B651-6CF3B60FEC84}.Release|x86.Build.0 = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|x64.ActiveCfg = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|x64.Build.0 = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|x86.ActiveCfg = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Debug|x86.Build.0 = Debug|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|Any CPU.Build.0 = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|x64.ActiveCfg = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|x64.Build.0 = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|x86.ActiveCfg = Release|Any CPU - {EFAE2853-E52F-4163-8266-3859C7748B85}.Release|x86.Build.0 = Release|Any CPU {3502A702-FA60-456E-BF23-05EAD02465E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3502A702-FA60-456E-BF23-05EAD02465E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {3502A702-FA60-456E-BF23-05EAD02465E6}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -138,8 +120,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {35072DD7-6FE3-A86A-51D9-DD886E81202C} = {6C4D298B-81FB-2B6B-5AB6-46205EDDC72C} - {EFAE2853-E52F-4163-8266-3859C7748B85} = {35072DD7-6FE3-A86A-51D9-DD886E81202C} {3502A702-FA60-456E-BF23-05EAD02465E6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {FA3FC57B-EA9D-4703-B643-2874C8C0461E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {E361C601-CC50-409F-8298-FD753DB4F6FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223} diff --git a/Program.cs b/Program.cs index 3b3c833..9939bc1 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Net.Http; @@ -146,9 +146,13 @@ internal static class Program // Master-Key-/Lizenz-Meldungen von oben im Terminal-Fenster auftauchen. services.AddSingleton(bootLog); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); + // Benachrichtigungen laufen über eine neutrale Senke. Threema ist entfallen; bis RocketChat + // und Telegram angebunden sind, landen Meldungen im Terminal-Log (LogNotificationSink). + services.AddSingleton( + sp => new PolyTrader.Core.Notifications.LogNotificationSink(sp.GetRequiredService())); + // Watchdog-Heartbeat (Dead-Man's-Switch). Als Singleton registriert, damit das // Settings-Fenster denselben Dienst für ReloadSettings/Test-Heartbeat nutzt. services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService())); @@ -166,7 +170,6 @@ internal static class Program services.AddHostedService(); services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); - services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddHostedService((IServiceProvider sp) => sp.GetRequiredService()); services.AddSingleton(); services.AddTransient(); @@ -229,7 +232,6 @@ internal static class Program { var view = new PolyTraderSharp.Ui.Views.SettingsView(); view.Initialize( - viewServices.GetRequiredService(), viewServices.GetRequiredService(), viewServices.GetRequiredService(), viewServices.GetRequiredService(), diff --git a/Ui/Views/SettingsView.cs b/Ui/Views/SettingsView.cs index ed5ce02..8f37e53 100644 --- a/Ui/Views/SettingsView.cs +++ b/Ui/Views/SettingsView.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.IO; using System.Linq; @@ -23,7 +23,6 @@ namespace PolyTraderSharp.Ui.Views private const string SettingsPath = "server_settings.xml"; private ServerSettings _settings = new(); - private ThreemaService? _threema; private MullvadVpnService? _vpn; private WatchdogHeartbeatService? _watchdog; private TerminalLogger? _logger; @@ -57,10 +56,9 @@ namespace PolyTraderSharp.Ui.Views }; } - public void Initialize(ThreemaService threema, MullvadVpnService vpn, TerminalLogger logger, + public void Initialize(MullvadVpnService vpn, TerminalLogger logger, IAccountRepository accountRepo, TradingState state, WatchdogHeartbeatService? watchdog = null) { - _threema = threema; _vpn = vpn; _logger = logger; _accountRepo = accountRepo; @@ -84,7 +82,6 @@ namespace PolyTraderSharp.Ui.Views try { _settings.Save(SettingsPath); - _threema?.ReloadSettings(); _vpn?.ReloadSettings(); _watchdog?.ReloadSettings(); _logger?.Info("Server-Einstellungen gespeichert und Services neu geladen."); diff --git a/docs/ANALYSE-Linux-Portierung.md b/docs/ANALYSE-Linux-Portierung.md new file mode 100644 index 0000000..47d7585 --- /dev/null +++ b/docs/ANALYSE-Linux-Portierung.md @@ -0,0 +1,543 @@ +# Analyse & Umsetzungsrahmen: Linux-Fähigkeit von PolyTrader + +**Stand:** 06.08.2026 · **Revision 3** (Entscheidungen Richard eingearbeitet, P3b umgesetzt) +**Basis:** 7 Projekte, ~24.250 LOC Produktivcode (ohne Tests/EF-Migrationen), 448 Tests + +--- + +## Entscheidungen (06.08.2026) + +| # | Thema | Entscheidung | Auswirkung | +|---|---|---|---| +| 1 | Kultur-Bug | **Sofort gefixt** ✅ | Erledigt, siehe [Abschnitt 4.1](#41-erledigt--der-aktive-bug-ist-behoben) | +| 2 | Designer-Regel | **Neue Formulierung übernommen:** Layout deklarativ in `.axaml`, nicht zur Laufzeit im Code | ersetzt die WinForms-Designer-Regel | +| 3 | Windows-UI | **Variante 2**: auf heutigem Stand eingefroren; soll später **komplett aus dem Build verschwinden** | Strategie siehe [5.5](#55-windows-ui-als-fallback-variante-2) | +| 4 | `PropertyGrid` | wird durch **normale Steuerelemente** ersetzt | +5 PT ggü. Fremdbibliothek, dafür bessere UX | +| 5 | Lizenz/Watchdog | laufen jetzt über das **Deploymentcenter**, nicht mehr über Einzeldienste | **Linux-Blocker entfällt**, siehe [5.2](#52-lizenz--watchdog--jetzt-über-das-deploymentcenter) | +| 6 | Zeitzone | **einmalig bei der Installation** festgelegt, nicht im laufenden Betrieb gewechselt | vereinfacht die Umsetzung, siehe [3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6) | +| 7 | Threema | **entfernt** ✅; Nachfolger: **RocketChat + Telegram** | umgesetzt am 06.08.2026, siehe [5.3](#53-threema--entfernt-) | +| 8 | Charts | **LiveCharts2** bestätigt (nach Klarstellung, dass die Integration nicht komplex ist) | +1 PT ggü. ScottPlot.Avalonia | + +**Netto-Effekt auf den Gesamtaufwand:** leicht gesunken auf **41–61 PT**. PropertyGrid (+5 PT) und LiveCharts2 (+1 PT) stehen gegen den bereits **erledigten** Threema-Ausbau (−2 PT), die vereinfachte Zeitzonen-Anforderung (−1 PT) und den entfallenden Lizenz-Umbau (−2 PT). + +--- + +## 0. Kurzfassung + +| Frage | Antwort | +|---|---| +| Ist es machbar? | Ja, ohne architektonische Sackgassen. | +| Was ist der Löwenanteil? | Die UI. ~8.300 LOC (34 % des Produktivcodes) hängen an WinForms. | +| Was ist überraschend gut? | Die **gesamte Trading-Kernlogik ist bereits portabel**: kein einziger `DllImport`, keine Registry, keine WMI, keine DPAPI, keine `SpecialFolder`, saubere `Path.Combine`-Nutzung. Nethereum, Pomelo/EF, `HttpClient`, `ClientWebSocket`, AES-GCM laufen unverändert. | +| Was ist der teuerste Einzelpunkt? | Der **`PropertyGrid`-Ersatz** durch handgebaute Steuerelemente (6 Instanzen in 4 Fenstern). | +| Gesamtaufwand | **41–61 Personentage** (davon 2 PT bereits erledigt) | +| Wichtigste Erkenntnis | **Für ~15 PT (≈ 25 %) bekommst du 80 % des Nutzens**: Der Trading-Kern läuft headless auf Linux, die WinForms-UI bleibt als eingefrorener Fallback. Siehe [Abschnitt 7](#7-fahrplan). | + +--- + +## 1. Bestandsaufnahme + +### 1.1 Projektstruktur und Zielframeworks + +| Projekt | TargetFramework | `UseWindowsForms` | Warum WinForms? | +|---|---|---|---| +| `PolyTrader.App` | `net8.0-windows7.0` (`WinExe`) | ja | Shell, Launcher, 4 Core-Views | +| `PolyTrader.Core` | `net8.0-windows` | ja | **nur** UI-Contract (`ModuleUi.cs`, `WindowMenu.cs`) | +| `Modules.CopyTrading` | `net8.0-windows` | ja | 5 Views + `TradeRowColoring` (`System.Drawing.Color`) | +| `Modules.ResolutionFarming` | `net8.0-windows` | ja | 1 Fenster | +| `Modules.Supervisor` | `net8.0-windows` | ja | 1 Fenster | +| `Modules.Accounting` | `net8.0-windows` | ja | 1 Fenster | +| `PolyTrader.Tests` | `net8.0-windows` | ja | nur weil die Referenzen `-windows` sind | +| ~~`IcgSoftware.Threema.CoreMsgApi`~~ (lib) | — | — | **entfernt am 06.08.2026** (Entscheidung 7) | + +Alle verbleibenden Projekte sind `-windows` – **aber die Bindung ist dünn**. Core und Module brauchen WinForms ausschließlich für den UI-Contract; die Services darunter sind plattformneutral. + +### 1.2 Wo sitzt der Windows-Code? + +``` +WinForms/System.Drawing berührt 41 Dateien – davon: + ├─ 33 reine UI-Dateien (Ui/-Ordner) → müssen ohnehin neu + └─ 5 „Ausreißer" außerhalb der Ui-Ordner → das ist die eigentliche Kopplung + ├─ src/PolyTrader.Core/Modularity/ModuleUi.cs (Func
, Image) + ├─ src/PolyTrader.Core/Modularity/WindowMenu.cs (MenuStrip, Font) + ├─ src/…CopyTrading/Logic/TradeRowColoring.cs (Color) + ├─ Models/DashboardRow.cs (using System.Drawing) + └─ Program.cs (ApplicationConfiguration.Initialize) +``` + +**Nur 5 Dateien** stehen zwischen „Core/Module sind plattformneutral" und dem Ist-Zustand. + +### 1.3 Code-Verteilung + +| Kategorie | LOC | Anteil | +|---|---:|---:| +| UI (Designer + Code-Behind), App + Module | ~8.300 | 34 % | +| davon Designer-generiert (entfällt ersatzlos) | ~4.500 | 19 % | +| Services / Logik / Persistenz / Modelle | ~15.950 | 66 % | +| Tests (448 Testfälle, 48 Dateien) | ~5.300 | — | +| EF-Migrationen (unverändert übernehmbar) | 6.891 | — | + +**Nur 1 von 48 Testdateien** ist UI-abhängig (`WindowMenuTests.cs`). + +--- + +## 2. Die UI-Portierung + +### 2.1 Control-Inventar und die neue Layout-Regel + +16 Fenster/UserControls, ~8.300 LOC. Aus den Designer-Dateien: + +| WinForms-Control | Anzahl | Avalonia-Ersatz | Bewertung | +|---|---:|---|---| +| `DataGridViewTextBoxColumn` | 69 | `DataGridTextColumn` | mechanisch, viel Fleißarbeit | +| `Label` | 49 | `TextBlock` | trivial | +| `ToolStripButton` | 37 | `Button` in `StackPanel` | Layout neu | +| `TabPage` / `TabControl` | 22 / 7 | `TabItem` / `TabControl` | trivial | +| `DataGridView` | 20 | `DataGrid` | siehe unten | +| `ToolStrip` | 16 | `StackPanel`/`DockPanel` | Layout neu | +| `Panel` / `Button` | 16 / 16 | direkt | trivial | +| `TextBox` / `ComboBox` | 9 / 9 | direkt | trivial | +| **`PropertyGrid`** | **6** | **handgebaute Formulare** (Entscheidung 4) | **teuerster Punkt, siehe 2.2** | +| `DateTimePicker` | 5 | `CalendarDatePicker` + `TimePicker` (getrennt!) | kleine Nacharbeit | +| `GroupBox` | 4 | `HeaderedContentControl`/`Border` | trivial | +| **`RichTextBox`** | **3** | kein Äquivalent, siehe 2.3 | mittel | +| `PictureBox` | 3 | `CartesianChart` (LiveCharts2) | siehe 2.4 | +| `SplitContainer` | 2 | `GridSplitter` | Layout neu | +| `MenuStrip` / `StatusStrip` | 1 / 1 | `Menu` / `DockPanel` | `ToolStripItemAlignment.Right` gibt es nicht → `DockPanel` | + +**Entwarnung bei den DataGrids:** Die Nutzung ist erfreulich schlicht – 25× `DataSource`-Binding, 35× `DefaultCellStyle`, 8× `AutoGenerateColumns`, nur 4 `SelectionChanged`, 3 `CellContentClick`, 1 `CellDoubleClick`. **Kein Virtual Mode, kein Custom Painting, keine `CellFormatting`.** Direkt auf `ItemsSource` + `Binding StringFormat` abbildbar. + +Die Zeilenfärbung (`ClosedTradesView.cs`, `OpenTradesView.cs` über `DataBindingComplete` → `row.DefaultCellStyle.BackColor`) wird zu einem `IValueConverter` auf `DataGrid.LoadingRow` – sauberer als heute. + +> ### 📐 Die neue Layout-Regel (Entscheidung 2) +> +> Die bisherige Vorgabe „alle WinForms-Controls designerfähig (partial + `.Designer.cs`), nie code-only" wird ersetzt durch: +> +> **„Jede View besteht aus `View.axaml` (vollständiges Layout, deklarativ) und `View.axaml.cs` (nur Verdrahtung und Datenlogik). Steuerelemente und Layout werden NICHT zur Laufzeit im Code erzeugt."** +> +> Nutzen bleibt identisch: Layout ist vollständig inspizierbar, diffbar und an einer Stelle. Ein WYSIWYG-Designer existiert in Avalonia nicht – stattdessen der **XAML-Previewer** (VS-/Rider-Extension) mit Live-Vorschau beim Tippen. +> +> **Praktische Konsequenz für die Portierung:** `WindowMenu.Populate()` baut die Menüleiste heute komplett zur Laufzeit auf (`menu.Items.Clear()` + dynamisch erzeugte `ToolStripMenuItem`). Das verstößt gegen die neue Regel und wird zu einem deklarativen `Menu` mit `ItemsSource`-Bindung an eine `ObservableCollection` – Layout im XAML, nur die Daten kommen aus dem Code. Gleiches gilt für die dynamisch angehängte Menüleiste in `ShellUiHost.AttachWindowMenu()`. + +### 2.2 `PropertyGrid` → normale Steuerelemente (Entscheidung 4) + +Betroffen sind **4 Fenster mit 6 Grid-Instanzen**: + +| Datei | Instanz | Gebundenes Objekt | +|---|---|---| +| `Ui/Views/SettingsView.Designer.cs:21,33` | `propertyGrid`, `pgAccount` | `ServerSettings`, Account-Daten | +| `…CopyTrading/Ui/AccountSettingsView.Designer.cs:25` | `pgSettings` | `CopyTradingAccountSettings` | +| `…CopyTrading/Ui/MasterTradersView.Designer.cs:41` | `pgDetail` | `TrackedTrader` | +| `…ResolutionFarming/Ui/ResolutionFarmingMainForm.Designer.cs:38` | `pgSettings` | Farming-Settings | + +**Vorarbeit, die sich jetzt auszahlt:** Die Modelle tragen bereits `[Description]`-Attribute als Inline-Erklärungen (`CopyTradingAccountSettings.cs:11`, `TrackedTrader.cs:10`) und `[Browsable(false)]` zum Ausblenden von Geheimnissen (`ServerSettings.cs:107,146` – Watchdog-Token und Lizenzschlüssel). **Diese Attribute sind die Spezifikation der neuen Formulare** – Beschriftung, Tooltip und Sichtbarkeit jedes Feldes stehen schon im Code. Das reduziert die Denkarbeit auf Layout und Validierung. + +**Was die handgebauten Formulare besser können als das `PropertyGrid`:** +- **Eingabevalidierung vor dem Speichern** statt erst beim Commit der Zelle (bei Risiko-Limits und Preisgrenzen relevant) +- **Einheiten direkt am Feld** (USDC, %, bps, Minuten) statt im Beschreibungstext +- **Fachliche Gruppierung** statt alphabetischer Reflection-Reihenfolge +- **Abhängige Felder ausgrauen** (z. B. Ladder-Parameter nur bei aktivierter Sell-Ladder) + +**Aufwand:** ~8–10 PT statt ~2 PT mit Fremdbibliothek. Der Aufschlag ist bewusst und kauft dauerhafte Wartbarkeit ohne Fremdabhängigkeit. + +### 2.3 `RichTextBox` → Terminal-Fenster + +`TerminalView` (378 + 195 LOC) und `SupervisorMainForm` (Chat) nutzen `RichTextBox` mit **Farbausgabe pro Log-Zeile**. Avalonia hat kein `RichTextBox`. + +**Empfehlung:** `ItemsControl` über eine `ObservableCollection` mit Farb-Converter – virtualisiert, deklarativ (passt zur neuen Layout-Regel), und schneller als heute. `TerminalView.cs:186` implementiert bereits ein Auto-Clear bei RAM-Limit; der Performance-Druck ist bekannt, ein virtualisiertes `ItemsControl` löst ihn an der Wurzel. + +### 2.4 Charts: ScottPlot → LiveCharts2 (Entscheidung 8) + +Nur **3 Charts**, alle in `DashboardView.RenderCharts()` (Equity-Kurve, PnL je Modul, PnL je Tag). Die Auswertungslogik liegt bereits pur in `TradeAnalytics` und bleibt **komplett unverändert**. + +Der heutige Weg (`plot.GetImage(w,h).GetImageBytes()` → `System.Drawing.Bitmap` → `PictureBox`, `DashboardView.cs:207-227`) entfällt und wird zu drei deklarativen `CartesianChart`-Elementen im XAML – was der neuen Layout-Regel ohnehin besser entspricht als das heutige Rendern im Code. + +> **Zur Einordnung deiner Sorge:** Der Unterschied ist kleiner als befürchtet. `ScottPlot.Avalonia` wäre ~1 PT günstiger gewesen, nicht mehr – bei **drei** Charts fällt der Integrationsvorteil kaum ins Gewicht. LiveCharts2 bringt dafür Animationen, Tooltips und Zoom out of the box, die du bei ScottPlot nachbauen müsstest. **Die Entscheidung für LiveCharts2 ist bei dieser Chart-Anzahl gut begründet** – du zahlst dafür keinen nennenswerten Aufpreis. + +### 2.5 `System.Drawing.Common` muss raus + +Seit **.NET 7 ist `System.Drawing.Common` Windows-only** und wirft auf Linux `PlatformNotSupportedException`. Betroffen: + +- `ModuleUi.cs:32` – `System.Drawing.Image? Icon` → `Avalonia.Media.IImage` +- `TradeRowColoring.cs:11-13` – `Color.FromArgb(...)` → eigenes Farbtupel oder `Avalonia.Media.Color` +- `WindowMenu.cs` – `Font`, `FontStyle.Bold` +- `Models/DashboardRow.cs` – `using System.Drawing` (vermutlich verwaist, prüfen) +- `DashboardView.cs:222-225` – entfällt mit LiveCharts2 + +`TradeRowColoringTests.cs` testet die Farben direkt → zieht mit (klein). + +### 2.6 PDF-Export: `PDFsharp-MigraDoc-GDI` → Core-Build + +`Modules.Accounting` referenziert `PDFsharp-MigraDoc-GDI` 6.2.4 – der **GDI+-Build ist Windows-only**. Ersatz: `PDFsharp-MigraDoc` (Core-Build), API-kompatibel. + +**Aber:** Der Core-Build findet auf Linux keine Systemschriften. `PdfExporter.cs:34` setzt `style.Font.Name = "Segoe UI"` – existiert auf Linux nicht. Nötig ist ein eigener **`IFontResolver`** plus eine als `EmbeddedResource` mitgelieferte Schrift (DejaVu Sans oder Open Sans, beide frei lizenziert). Einziger nennenswerter Umbau am Accounting-Modul. + +`AccountingReportTests.cs:131` rendert PDFs headless im Test → zieht mit. + +--- + +## 3. Zeitzonen ⏰ + +### 3.1 Der harte Punkt + +`Ui/Views/TerminalView.cs:150`: +```csharp +TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time"); +``` + +Eine **Windows-Zeitzonen-ID**. Seit .NET 6 mappt `FindSystemTimeZoneById` Windows-IDs auf Linux über ICU – funktioniert also *meistens*. Es bricht, sobald `InvariantGlobalization=true` gesetzt ist oder die Distro kein vollständiges ICU mitbringt (Alpine ohne `icu-libs`). Dann fliegt beim Öffnen des Terminal-Fensters eine `TimeZoneNotFoundException`. + +Mit Entscheidung 6 verschwindet diese Stelle ohnehin – die Zeitzone kommt künftig aus der Konfiguration, nicht mehr hartcodiert. + +### 3.2 Die Now/UtcNow-Mischung + +**122× `DateTime.UtcNow` gegen 23× `DateTime.Now`.** Konsistent, solange der Rechner auf `Europe/Berlin` steht. **Linux-Server laufen praktisch immer auf UTC.** Betroffene Stellen: + +| Stelle | Auswirkung bei Server-TZ = UTC | +|---|---| +| `TerminalLogger.cs:23` – `Timestamp = DateTime.Now` | Log-Zeitstempel 1–2 h versetzt | +| `TerminalLogger.cs:122`, `LauncherWidgetsPanel.cs:112` – Logdatei `{DateTime.Now:yyyy-MM-dd}.jsonl` | **Tagesgrenze der Logdateien verschiebt sich** | +| `DossierService.cs:81`, `SupervisorTools.cs:144` – suchen Logdateien per Datum | müssen dieselbe Konvention nutzen, sonst leere Dossiers | +| `DailyReportService.cs:61` | Tagesbericht schneidet an anderer Stelle | +| `AccountingMainForm.cs:57` – Periodenvorbelegung | **steuerlich relevant** – Monats-/Quartalsgrenzen | +| `PdfExporter.cs:55` – „Erstellt: …" | Belegdatum im Steuer-PDF | +| `MasterTraderAnalyticsJob.cs` | mischt `Now` (Anzeige) und `UtcNow` (Cutoffs) – funktioniert, ist aber verwirrend | + +### 3.3 Anforderung: konfigurierbare Zeitzone (Entscheidung 6) + +> **Ziel (präzisiert):** Die Zeitzone wird **einmalig bei der Installation** festgelegt — wie in einem Setup — und im laufenden Betrieb normalerweise nicht mehr gewechselt. + +**Vorgeschlagene Umsetzung:** + +1. **Neue Einstellung** `ServerSettings.ApplicationTimeZoneId` (Default `"Europe/Berlin"`). IANA-IDs verwenden – .NET akzeptiert sie seit Version 6 auf **beiden** Plattformen, Windows-IDs dagegen nur eingeschränkt auf Linux. + +2. **Zentraler Zugriffspunkt** statt verstreuter `DateTime.Now`-Aufrufe: + ``` + IAppClock + ├─ DateTime UtcNow → für alle Domänen-/Persistenzwerte + ├─ DateTime Local → für Anzeige und Logdatei-Namen + └─ TimeZoneInfo TimeZone → aufgelöst aus ApplicationTimeZoneId, mit Fallback auf UTC + Warnung + ``` + Damit ist die Zeitzone an **einer** Stelle wirksam statt an 23. + +3. **Festlegung bei der Erstinstallation**, danach schreibgeschützt-mit-Bestätigung: + - Beim **ersten Start ohne gesetzte Zeitzone** wird sie abgefragt (UI-Dialog bzw. CLI-Schalter `--set-timezone` für den headless Betrieb) und in `server_settings.xml` festgeschrieben. + - Eine **spätere Änderung** ist möglich, aber bewusst unbequem: Bestätigungsdialog mit Klartext-Folgen („Logdatei-Tagesgrenzen verschieben sich um X Stunden. Bereits geschriebene Logs und Buchungen bleiben unverändert und sind danach in der neuen Zeitzone zu lesen.") und **Wirksamkeit erst nach Neustart**. + - **Protokolleintrag** mit alter und neuer Zeitzone, damit die Verschiebung später in den Logs nachvollziehbar ist. + + Weil der Regelfall die einmalige Festlegung ist, entfällt jede Komplexität für einen Wechsel *zur Laufzeit* — kein Neu-Verdrahten laufender Jobs, kein Umschalten offener Fenster. Das ist der eigentliche Gewinn dieser Entscheidung. + +4. **Zusätzlich** `TZ`-Umgebungsvariable in der systemd-Unit setzen, damit auch die Ausgaben *außerhalb* der Anwendung (Logrotate, Cron, Systemlogs) zusammenpassen. + +**Offene fachliche Frage:** Soll die Zeitzone im Accounting **mitwandern** oder dort fix auf der steuerlich maßgeblichen Zeitzone stehen bleiben? Ein Wechsel des Betriebsstandorts ändert nicht zwingend den steuerlichen Bezugsrahmen. Empfehlung: **zwei getrennte Einstellungen** – `ApplicationTimeZoneId` (Anzeige, Logs, Jobs) und `AccountingTimeZoneId` (Buchungsperioden, Steuerbelege), Default identisch. Kostet fast nichts, verhindert aber, dass ein Standortwechsel rückwirkend die Buchungsperioden verschiebt. + +**Aufwand:** ~2 PT (Einstellung, `IAppClock`, 23 Aufrufstellen umziehen, Erstinstallations-Abfrage, Tests). Durch die Beschränkung auf die Installationszeit ~1 PT günstiger als in Revision 2 angesetzt. + +### 3.4 MySQL + +Pomelo speichert `DateTime` als `DATETIME` ohne Zeitzoneninformation. Bestehende Daten ändern sich beim Plattformwechsel nicht – **aber die Interpretation neuer Werte** ändert sich. Prüfen: `SELECT @@global.time_zone, @@session.time_zone;` auf dem Ziel-Server. Bei gemischt geschriebenen Daten entstehen im Accounting stumme Lücken/Überlappungen an der Tagesgrenze. Der `IAppClock` aus 3.3 löst das, indem er die Persistenz konsequent auf UTC festlegt. + +--- + +## 4. String- und Zahlenformatierung + +### 4.1 ERLEDIGT – der aktive Bug ist behoben ✅ + +`TraderMonitorService.cs` parste API-Preise kulturabhängig. **Empirisch verifiziert:** + +``` +de-DE "0.53" -> 53 "1234.5" -> 12345 +Invariant "0.53" -> 0,53 "1234.5" -> 1234,5 +``` + +Ein **Faktor-100-Fehler** im Einstandspreis der Master-Trader-Auswertung. + +**Behoben am 06.08.2026:** +- `TraderMonitorService.cs:1276-1280` – nutzt jetzt den bereits vorhandenen, korrekten Helper `ParseDecimal` statt duplizierter Inline-Logik. Der Helper ist `internal static` und dokumentiert. +- `PolymarketClobClient.cs:770, 784, 796, 820, 822, 827` – alle `TryParse` auf `InvariantCulture` (Regex-Gruppen sind zwar ziffernrein und waren damit faktisch sicher, aber die Klasse Fehler soll nirgends mehr im Code stehen). +- `MasterTraderAnalyticsJob.cs:140` – Unix-Timestamp-String invariant. +- **Neuer Regressionstest** `tests/PolyTrader.Tests/ApiNumberParsingTests.cs` – 10 Fälle, die unter erzwungener **de-DE-Kultur** laufen und fehlschlagen, sobald jemand die Invariant-Angabe entfernt. + +**Verifikation:** Build fehlerfrei, **448 Tests grün** (438 vorher + 10 neue). + +### 4.2 Ausgabeformatierung (offen) + +35× `DefaultCellStyle.Format`, 19× `ToString("…")`, dazu zahlreiche `:N2`/`:F2`/`:F3` in Log- und UI-Strings – alle nach `CultureInfo.CurrentCulture`. + +Auf Windows ist das `de-DE`. Auf Linux ist es **`LANG`/`LC_ALL`** – unter systemd oft **gar nicht gesetzt** → .NET fällt auf **Invariant** zurück → aus `1.234,56 USDC` wird `1,234.56 USDC`. Kein Datenfehler, aber sofort sichtbar. + +**Empfehlung:** +1. **Anzeige-Kultur beim Start explizit pinnen**: `CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("de-DE")` in `Program.Main`. Damit ist die Darstellung unabhängig von `LANG`. (Sinnvollerweise ebenfalls konfigurierbar, analog zur Zeitzone – dieselbe Begründung: Betrieb im Ausland.) +2. **Globalisierungs-Analyzer aktivieren** (`CA1304`, `CA1305`, `CA1307`, `CA1310`). Sie hätten den Bug aus 4.1 gefunden. + +### 4.3 Weitere Kulturthemen + +- **ICU vs. NLS:** .NET nutzt seit Version 5 auf beiden Plattformen ICU – Sortier- und Vergleichssemantik bleibt gleich. Voraussetzung: **kein `InvariantGlobalization=true`** im Publish-Profil, `libicu` auf dem Zielsystem (Debian/Ubuntu Standard, Alpine nachinstallieren). +- **String-Vergleiche:** durchgängig `StringComparison.OrdinalIgnoreCase` – vorbildlich, keine Nacharbeit. + +--- + +## 5. Weitere plattformspezifische Punkte + +### 5.1 Dateisystem – erfreulich sauber + +| Prüfpunkt | Befund | +|---|---| +| `Path.Combine` | **17 von 17** korrekt, durchgängig über `AppContext.BaseDirectory` | +| `Environment.SpecialFolder` | **0 Treffer** | +| Harte Windows-Pfade | **genau 1**: `ServerSettings.cs:73` – `MullvadCliPath` (konfigurierbar, nur der Default ist Windows) | +| `DllImport` / `LibraryImport` | **0 Treffer** | +| Registry / WMI / DPAPI / EventLog | **0 Treffer** im eigenen Code | + +**Groß-/Kleinschreibung:** Linux ist case-sensitive. Kritisch: `server_settings.xml`, `appsettings.Local.json`, `master.key`, `openrouter.key`, `Logs/`, `favicon.ico`, die 24 PNGs in `Resources/`. Im Code konsistent – trotzdem beim ersten Linux-Build gezielt prüfen, ebenso die `\`-Pfade in den `.csproj` (MSBuild normalisiert sie, aber es ist eine bekannte Stolperstelle). + +**Dateirechte:** `master.key` und `openrouter.key` liegen als Klartext neben der Anwendung (`SettingsView.cs:301,356`, `Program.cs:393`, `OpenRouterClient.cs:46`). Auf Linux erzeugt `File.WriteAllText` mit Standard-umask **`644` – world-readable**. Nach jedem Schreiben `File.SetUnixFileMode(path, UserRead | UserWrite)` (.NET 7+). Ergänzt Befund F1 des Sicherheitskonzepts. + +> Das Deploymentcenter macht es beim Lizenz-Cache bereits richtig (`chmod 600 state.dat`, siehe LICENSE_INTEGRATION_GUIDE Abschnitt 3) – dieselbe Konvention für die eigenen Schlüsseldateien übernehmen. + +**Zeilenenden:** `Environment.NewLine` wechselt von `\r\n` auf `\n`. `CsvExporter.cs` nutzt bereits explizites `'\n'` – korrekt. + +### 5.2 Lizenz & Watchdog – jetzt über das Deploymentcenter (Entscheidung 5) + +**Das war in Revision 1 ein Blocker (R4) – er ist gelöst.** Aus `J:\Softwareprojekte\Deploymentcenter\docs`: + +| Thema | Alt (`LicenseLabrador.Client` 1.0.0) | Neu (Deploymentcenter, HWID v2) | +|---|---|---| +| Hardware-ID | Windows: Registry `MachineGuid`; Linux: `/etc/machine-id` → **plattformabhängig verschieden** | `2::` mit dokumentierter Quellen-Priorisierung je Plattform | +| Container | jede Instanz eine neue ID → Lizenz invalidiert bei jedem Neustart | **`DEPLOYMENTCENTER_HWID`-Override** gewinnt plattformweit | +| Rechnername im Hash | — | **bewusst nicht enthalten**, Umbenennen kostet keinen Aktivierungsplatz | +| Migration | — | Client sendet `hardware_id` (v2) **und** `legacy_hardware_id` (v1); Server zieht die Aktivierung lautlos um, **ohne zusätzlichen Platz** | +| Headless | modaler Dialog blockiert den Start | CLI-Schalter `--license-status`, `--license-set-key`, `--license-deactivate` **plus** `ILicensePrompt`/`ConsoleLicensePrompt` als UI-Abstraktion | +| Cache | — | `LLS2`-Envelope, AES-256-GCM, HKDF aus HWID+Slug, Linux `0600`, kein Klartext-Rückfall | +| SDK | `netstandard2.0` | `Deploymentcenter.Client`, Multi-Targeting `netstandard2.0;net8.0` | + +**Damit entfallen zwei Risiken vollständig:** die Neubindung der Lizenz beim Plattformwechsel (Migration v1→v2 ist serverseitig gelöst) und der Container-Neustart-Effekt. + +**Verbleibende Arbeit** – ein Umbau, kein Blocker: + +1. **Lizenz:** `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` von `LicenseLabrador.Client` auf `Deploymentcenter.Client` umstellen. Die API unterscheidet sich: `LicenseResult`/`LicenseState`/`GetHardwareId()` → `LicenseValidationResult` (`IsValid`, `Status`, `IsCached`, `Message`) / `HardwareId.GetHardwareId(slug)` / `ValidateAsync(slug, key, serverUrl)`. **`ILicensePrompt` implementieren** statt den Dialog direkt aufzurufen – das ist genau die Naht, die den headless-Betrieb und später die Avalonia-UI trägt. Das lokale NuGet-Paket in `lib/nuget/` wird ersetzt. +2. **Watchdog:** `WatchdogHeartbeatService` sendet bereits `source`, `instance`, `interval` – **das Payload-Schema passt schon**. Zu ändern sind nur Header (`X-Watchdog-Key` → `X-Agent-Token`) und Ziel-URL (`watchdog.mhdf.de` → `dc.mhdf.de/api/watchdog/v1/ping`). Ergänzen: `os`- und `group`-Feld, sowie der im Guide empfohlene Abschluss-Ping beim geordneten Herunterfahren. + +**Aufwand:** ~2–3 PT. **Empfehlung: in Stufe L1 mitnehmen**, nicht später – der headless-Betrieb (L2) hängt an `ILicensePrompt`. + +> **Sicherheitshinweis am Rande:** `server_settings.xml` enthält den Watchdog-Token und den Lizenzschlüssel im Klartext (`WatchdogToken`, `LicenseKey`). Beim Umbau auf das Deploymentcenter ist der passende Moment, beide über die `SecretProtection` laufen zu lassen oder auf Umgebungsvariablen umzustellen – passend zur bereits offenen Auflage, die Watchdog-Secrets zu rotieren. + +### 5.3 Threema – entfernt ✅ (Entscheidung 7) + +**Am 06.08.2026 umgesetzt.** Nachfolgekanäle werden **RocketChat und Telegram**. + +**Was entfernt wurde:** + +| Entfernt | Umfang | +|---|---| +| `src/PolyTrader.Core/Services/ThreemaService.cs` | 1 Datei (`BackgroundService` + Webhook-Listener) | +| `libs/Threema-MsgApi-Net-Core` (vendorte Fremdbibliothek) | 35 Dateien, aus Solution und Git entfernt (History bleibt) | +| Projektreferenz in `PolyTrader.Core.csproj` | 1 | +| Threema-Block in `ServerSettings` (7 Properties, `[Category("Threema Notifications")]`) | 36 Zeilen | +| DI-Registrierungen in `Program.cs` (Singleton + HostedService + View-Injektion) | 3 | +| `ThreemaService`-Verdrahtung in `SettingsView` | 3 Stellen | + +**Was an die Stelle getreten ist — `INotificationSink`:** + +``` +PolyTrader.Core/Notifications/ + ├─ INotificationSink.cs Schnittstelle + NotificationSeverity (Info/Warning/Critical) + └─ LogNotificationSink.cs Übergangs-Implementierung: schreibt ins Terminal-Log +``` + +Die drei Fachdienste kennen jetzt nur noch die Schnittstelle: + +| Dienst | Meldung | Severity | +|---|---|---| +| `MasterTraderAnalyticsJob` | Auto-Pause eines Master-Traders | `Warning` | +| `SellLadderService` | SELL-Floor ohne Fill erreicht, Position wird gehalten | `Critical` | +| `DailyReportService` | täglicher Supervisor-Kurzbericht | `Info` | + +**Zwei Entwurfsentscheidungen, die sich später auszahlen:** + +1. **No-Throw-Vertrag.** `SendAsync` wirft laut Schnittstellen-Doku **niemals** — Kanalfehler werden intern abgefangen und protokolliert. Das ist genau die Best-Practice aus dem Watchdog-Guide („Fange Netzwerkfehler stets stumm ab, damit der Ausfall des Monitoring-Servers niemals den Hauptanwendungsfluss unterbricht"). Die Aufrufer verzichten deshalb bewusst auf eigene `try/catch`-Blöcke — der Code wurde dabei kürzer, nicht länger. **Bindend für die kommenden RocketChat- und Telegram-Implementierungen.** +2. **`NotificationSeverity`** ist von Anfang an dabei, obwohl die Log-Implementierung sie kaum nutzt. Bei zwei Kanälen (RocketChat für alles, Telegram als Fallback für Kritisches — oder umgekehrt) ist die Einstufung die natürliche Routing-Information. Sie nachträglich einzuziehen hätte alle Aufrufstellen erneut angefasst. + +**Nebeneffekte — vier Linux-Themen sind mit weggefallen:** + +| Entfallen | War | +|---|---| +| `libsodium` 1.0.16 (Baujahr 2018) | Risiko R7: `.so` gegen alte glibc gelinkt, Ladefehler auf Ubuntu 24.04 möglich | +| `Microsoft.Win32.Registry` (`E2EHelper.cs`, `#if CoreWinOnly`) | **die einzige Registry-Nutzung im gesamten Build** | +| `HttpListener` auf `http://*:{port}/` (Webhook) | Port-Bindung + `netsh`-Fehlermeldung mit Windows-Semantik | +| `System.Web.HttpUtility` | — | + +**Verifikation:** Clean-Build fehlerfrei, **448 Tests grün**, Compiler-Warnungen von **17 auf 9** gesunken (die 8 Warnungen der Threema-Bibliothek — 4× `SYSLIB0014`, 3× `CA1416`, 1× `CS0472` — sind weg). + +**Testverbesserung nebenbei:** Die drei Tests, die bisher `new ThreemaService(logger, new JobManager())` als Wegwerf-Instanz injizierten, nutzen jetzt `FakeNotificationSink` — damit ist **prüfbar, ob und mit welcher Severity gemeldet wurde**. Vorher war das nicht beobachtbar. + +**Offen für später:** `RocketChatNotificationSink` und `TelegramNotificationSink` implementieren und in `Program.cs` registrieren — eine Klasse je Kanal, keine Änderung an der Fachlogik. Bei mehreren aktiven Kanälen bietet sich ein `CompositeNotificationSink` an (leitet an alle weiter, jeder Kanal fängt seine Fehler selbst ab). + +### 5.4 Verbleibende Netzwerkdienste + +- **`HttpListener`** – nach dem Threema-Ausbau nur noch der MCP-Light-Server (`127.0.0.1:{port}/mcp`). Läuft auf Linux über die managed Implementierung, kein `netsh` nötig. Unkritisch. +- **`MullvadVpnService`**: Der CLI-Aufruf ist portabel (`UseShellExecute = false`, sauber). Auf Linux heißt die CLI `mullvad` (typisch `/usr/bin/mullvad`). **Achtung:** `HealthCheckAsync()` prüft `status.Contains("Connected")` – der Wortlaut der Linux-CLI weicht ab. **Muss gegen die echte CLI verifiziert werden**, sonst reconnectet der Dienst dauerhaft ins Leere und meldet nach 3 Fehlversuchen „unrecoverable". +- **Nethereum 6.1.0, Pomelo/EF Core 8, `HttpClient`, `ClientWebSocket`, `System.Text.Json`, AES-GCM** (OpenSSL): plattformneutral, **keine Anpassung nötig**. Das ist der gesamte Trading-Pfad. + +### 5.5 Windows-UI als Fallback: Variante 2 (Entscheidung 3) + +Heute laufen Generic Host und `Application.Run` im **selben Prozess**; darunter **19 `BackgroundService`-Instanzen**. Auf Linux gibt es kein „WinExe" – eine Avalonia-App braucht X11/Wayland, ein Trading-Server hat typischerweise kein Display. + +**Entschieden: Variante 2 – eingefrorener Stand.** Die WinForms-UI bleibt auf dem heutigen Stand und wird nicht weiterentwickelt; später verschwindet sie **vollständig aus dem Build**, damit keine Altlast mitgeschleppt wird. + +**Was das konkret heißt:** + +| | | +|---|---| +| **Einfrieren** | Vor Beginn von L3 einen Git-Tag setzen (Vorschlag: `winforms-final`) und im Repo dokumentieren. Ab da keine Feature-Arbeit mehr an `Ui/` und den `Ui/`-Ordnern der Module. | +| **Fallback im Ernstfall** | Tag auschecken, bauen, starten. Greift auf dieselbe MySQL zu — Anzeige und Einstellungen bleiben voll brauchbar. | +| **Weiterentwicklung** | Läuft unverändert weiter — nur eben im **Trading-Kern**, der ab L2 headless auf Linux läuft. Eingefroren ist die *Oberfläche*, nicht die Funktion. | +| **Endgültiger Ausbau** | Nach Abschluss von L4: `Ui/`-Ordner, `PolyTrader.App` als `WinExe`, alle `.Designer.cs`/`.resx`, `UseWindowsForms`, `favicon.ico`-Einbindung und `ApplicationConfiguration.Initialize()` entfernen. **~4.500 LOC generierter Designer-Code fallen ersatzlos weg.** | + +**Wichtig für die Reihenfolge:** Damit der eingefrorene Stand überhaupt lauffähig bleibt, darf **P1 die WinForms-UI nicht kaputtmachen**. Beim Entkoppeln von Core und Modulen (Zielframework `net8.0`, `System.Drawing` raus) muss der WinForms-Zweig bis zum Tag baubar bleiben — praktisch heisst das: **erst taggen, dann P1 durchziehen**. Sonst ist der Fallback genau in dem Moment weg, in dem man ihn braucht. + +> **Konkreter Vorschlag zur Absicherung:** Vor P1 einmal `dotnet publish -c Release` des heutigen Stands erzeugen und das Ergebnis zusammen mit dem Tag archivieren. Ein fertig gebautes Verzeichnis ist im Ernstfall schneller einsatzbereit als ein Checkout, der erst wieder kompiliert werden muss — und beweist nebenbei, dass der eingefrorene Stand tatsächlich läuft. + +**Unabhängig davon nötig:** headless-fähiger Einstiegspunkt für den Linux-Betrieb, systemd-Unit mit `Restart=always` und `Environment=TZ=…`, Master-Key über `EnvironmentFile` mit `chmod 600`, Logrotate für `Logs/*.jsonl`. + +### 5.6 Tests und CI + +- Testprojekt auf `net8.0` (fällt mit den Referenzen). +- `WindowMenuTests.cs` – einziger UI-Test, neu schreiben (~0,5 PT). +- `TradeRowColoringTests.cs` – zieht mit dem Farbtyp-Wechsel mit (~0,2 PT). +- `AccountingReportTests.cs:131` – braucht den neuen FontResolver. +- **`--smoke-ui`** (`Program.cs:500-570`): konstruiert alle Views headless und hat laut Kommentar bereits Designer-Regressionen gefangen. **Unbedingt erhalten** – Avalonia bietet `Avalonia.Headless`, das Muster überträgt sich gut. +- **Es gibt keine CI.** Die Portierung ist der richtige Anlass, `dotnet build` + `dotnet test` auf einem Linux-Runner einzurichten – sonst schleicht sich die Windows-Abhängigkeit unbemerkt zurück. + +--- + +## 6. Aufwandsschätzung (Revision 2) + +| # | Paket | Änderung ggü. Rev. 1 | Aufwand | +|---|---|---|---:| +| ~~L0~~ | ~~Kultur-Bug~~ | **erledigt** ✅ | ~~0,5 PT~~ | +| **P0** | Zielbild, Avalonia-Spike an einer echten View, Layout-Konventionen festzurren | — | **2–3 PT** | +| **P1** | Core/Module von WinForms entkoppeln, `TargetFramework` auf `net8.0`, `System.Drawing` raus | — | **2–3 PT** | +| **P2** | Kultur & Zeit: `IAppClock`, Zeitzone bei Installation festlegen, Kultur pinnen, Analyzer | **−1 PT** (Entscheidung 6 vereinfacht) | **2–3 PT** | +| **P3** | Plattform-Kleinkram: Mullvad-Pfad + Status-Parsing, Unix-Dateirechte | **−0,5 PT** (Threema entfällt) | **1 PT** | +| ~~**P3b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | **erledigt 06.08.2026** ✅ | ~~2 PT~~ | +| **P3c** | **Lizenz/Watchdog auf Deploymentcenter umstellen** | **neu** (Entscheidung 5) | **2–3 PT** | +| **P4** | PDF-Export: PDFsharp Core + `IFontResolver` + eingebettete Schrift | — | **1–2 PT** | +| **P5** | Charts auf LiveCharts2 | **+1 PT** (Entscheidung 8) | **2 PT** | +| **P6** | UI-Shell: Launcher, Fenstermenü (deklarativ!), `ShellUiHost`, Shutdown-, Lizenz-Dialog, Widgets | — | **5–7 PT** | +| **P7** | Core-Views: Dashboard, Settings, Jobs, Terminal (inkl. **2 PropertyGrid-Ersätze** + `RichTextBox`) | **+3 PT** (Entscheidung 4) | **11–15 PT** | +| **P8** | Modul-Fenster: CopyTrading (5), ResolutionFarming, Supervisor, Accounting (inkl. **4 PropertyGrid-Ersätze**, 20 DataGrids) | **+2 PT** (Entscheidung 4) | **14–20 PT** | +| **P9** | Tests, `--smoke-ui` auf `Avalonia.Headless`, Linux-CI | — | **3–4 PT** | +| **P10** | Deployment: systemd, TZ, Logrotate, Feldtest im Zielland | **−0,5 PT** (Lizenz-Neubindung entfällt) | **3–4 PT** | +| **P11** | **WinForms endgültig ausbauen** (nach L4): `Ui/`, Designer-Dateien, `UseWindowsForms`, `WinExe` | **neu** (Entscheidung 3) | **1 PT** | +| | **Summe (offen)** | | **41–61 PT** | + +*Personentage entwickelnder Arbeit. Mit Agentenunterstützung sinkt die Kalenderzeit deutlich; der Review- und Feldtest-Anteil (P9, P10) bleibt weitgehend gleich – bei einer Anwendung, die echtes Geld bewegt, ist das die richtige Reihenfolge.* + +--- + +## 7. Fahrplan + +### Die zentrale Erkenntnis bleibt + +**P1–P4 brauchen kein Avalonia.** Sie sind auf Windows entwickelbar und verifizierbar. Zusammen mit einem headless-Einstiegspunkt hast du für **~15 PT von rund 50** den eigentlichen Nutzen: + +> Der **Bot läuft auf Linux**. Die **WinForms-UI bleibt als eingefrorener Fallback** (Entscheidung 3) und greift über dieselbe MySQL auf dieselben Daten zu. + +| Stufe | Inhalt | Aufwand | Ergebnis | +|---|---|---:|---| +| ~~**L0**~~ | ~~Kultur-Bug~~ | — | ✅ **erledigt** – Faktor-100-Preisfehler behoben, 10 Regressionstests | +| ~~**L0b**~~ | ~~Threema-Ausbau + `INotificationSink`~~ | — | ✅ **erledigt** – 36 Dateien raus, 448 Tests grün, 8 Warnungen weniger | +| **L1** | Tag `winforms-final` setzen → dann P0 + P1 + P2 + P3 + P3c + P4 | 11–15 PT | Core/Module sind `net8.0` und plattformneutral; Zeitzone bei Installation festlegbar; Deploymentcenter angebunden; Tests laufen auf Linux | +| **L2** | Headless-Einstiegspunkt + systemd + Feldtest im Zielland | 3–5 PT | **Trading läuft headless auf Linux.** ⭐ Meilenstein | +| **L3** | P5 + P6 + P7 (Charts, Shell, Core-Views) | 18–24 PT | Avalonia-Grundgerüst steht; Dashboard, Settings, Terminal nutzbar | +| **L4** | P8 + P9 (Modul-Fenster, Tests, CI) | 17–24 PT | WinForms funktional abgelöst | +| **L5** | P11 – WinForms endgültig aus dem Build entfernen | 1 PT | Keine Altlast mehr; ~4.500 LOC Designer-Code fallen weg | + +### Reihenfolge-Begründung + +- **Der Tag `winforms-final` kommt VOR P1.** P1 entkoppelt Core und Module von WinForms — danach ist der alte Stand nicht mehr aus dem Hauptzweig baubar. Erst taggen (und einmal `dotnet publish` archivieren), dann umbauen. Sonst ist der Fallback genau dann weg, wenn man ihn braucht. +- **P3c (Deploymentcenter) gehört nach L1, nicht später** – der headless-Betrieb in L2 hängt an der `ILicensePrompt`-Abstraktion. Ohne sie blockiert der Lizenzdialog den Start auf einem Server ohne Display. +- **P2 vor L2** – die Zeitzone muss feststehen, bevor der erste Linux-Server Logs und Buchungen schreibt. Nachträglich umzustellen bedeutet gemischte Daten an der Tagesgrenze. + +### Noch offene Punkte + +1. **Accounting-Zeitzone getrennt von der Anwendungszeitzone?** ([3.3](#33-anforderung-konfigurierbare-zeitzone-entscheidung-6)) – Empfehlung ja, verhindert rückwirkend verschobene Buchungsperioden bei einem Standortwechsel. +2. **Anzeige-Kultur ebenfalls bei der Installation festlegbar?** ([4.2](#42-ausgabeformatierung-offen)) – naheliegend, da dieselbe Begründung wie bei der Zeitzone (Auslandsbetrieb). +3. **RocketChat und Telegram:** Wann sollen die echten `INotificationSink`-Implementierungen kommen? Bis dahin landen Auto-Pause- und Eskalationsmeldungen nur im Terminal-Log. Für den unbeaufsichtigten Linux-Betrieb (L2) wäre mindestens **ein** echter Kanal wichtig — sonst bemerkst du eine Auto-Pause erst beim nächsten Blick ins Log. + +--- + +## 8. Risikoliste (Revision 3) + +| # | Risiko | Status | Wirkung / Gegenmaßnahme | +|---|---|---|---| +| ~~R1~~ | Kulturabhängiger Parse, Faktor-100-Preisfehler | ✅ **behoben** | gefixt + 10 Regressionstests unter de-DE | +| ~~R4~~ | Lizenz-Hardware-ID ändert sich, in Containern instabil | ✅ **gelöst** | Deploymentcenter HWID v2 mit `DEPLOYMENTCENTER_HWID`-Override und v1→v2-Migration ohne Platzverlust | +| ~~R7~~ | libsodium 1.0.16 gegen moderne glibc | ✅ **entfallen** | mit dem Threema-Ausbau am 06.08.2026 | +| **R2** | Kein WYSIWYG-Designer; Layout-Regel ist neu | offen | Im Spike (P0) an einer echten View erproben, bevor 16 Views folgen | +| **R3** | `PropertyGrid`-Ersatz durch handgebaute Formulare | offen | Teuerster Posten (~10 PT). `[Description]`-Attribute liefern die Spezifikation | +| **R5** | Zeitzonen-Semantikwechsel trifft Logs, Tagesberichte, **Accounting-Perioden** | offen | P2 **vor** L2; `IAppClock`; Wirksamkeit erst nach Neustart | +| **R6** | Headless-Server ohne Display | offen | `ILicensePrompt`/`ConsoleLicensePrompt` aus dem Deploymentcenter-SDK; headless-Einstiegspunkt in L2 | +| **R8** | Anzeigeformate kippen auf Invariant (`LANG` nicht gesetzt) | offen | Kultur in `Program.Main` pinnen | +| **R9** | Keine CI – Windows-Abhängigkeiten schleichen zurück | offen | Linux-Build in Gitea Actions ab L1 | +| **R10** | Mullvad-CLI meldet auf Linux anderen Statustext | offen | Gegen die echte CLI im Zielland verifizieren (5.4) | +| ~~R11~~ | Doppelpflege beider UIs | ✅ **entfällt** | Variante 2 gewählt (eingefrorener Stand) | +| ~~R12~~ | Benachrichtigungen fallen still | ✅ **entschärft** | `INotificationSink` eingezogen; Meldungen landen im Terminal-Log | +| **R13** | **Kein echter Benachrichtigungskanal** bis RocketChat/Telegram da sind | offen | Für den unbeaufsichtigten Betrieb (L2) mindestens einen Kanal implementieren — sonst bleibt eine Auto-Pause bis zum nächsten Log-Blick unbemerkt | +| **R14** | **Fallback-Stand nicht mehr baubar**, weil P1 vor dem Tag lief | offen | Tag `winforms-final` + archiviertes `dotnet publish` **vor** P1 (5.5) | + +--- + +## 9. Was **nicht** angepasst werden muss + +- **Der gesamte Trading-Pfad:** `PolymarketClobClient` (EIP-712 über Nethereum), `PolymarketApiService`, `PolymarketWssClient`, `AlchemyWssClient`, `CopyTradingEngine`, `SellLadderService`, `TraderMonitorService` +- **Persistenz:** EF Core 8 + Pomelo/MySQL, alle 15 Migrationen, `EncryptedStringConverter` +- **Sicherheit:** `SecretProtection` (AES-256-GCM läuft auf Linux über OpenSSL), Master-Key-Ableitung +- **Alle Analytics:** `TradeAnalytics`, `DossierBuilder`, Supervisor-Agent, OpenRouter-Client, MCP-Light-Server +- **Accounting-Logik:** Ingest, Klassifikation, BWA, CSV-Export (nur der PDF-Export braucht den FontResolver) +- **Watchdog-Payload-Schema** (`source`/`instance`/`interval` passen bereits zur Deploymentcenter-API), Modularitäts-Infrastruktur +- **438 von 448 Tests** + +Das ist der Grund, warum die Schätzung trotz 24.000 LOC bei 41–61 PT landet und nicht bei 150: **Zwei Drittel des Codes sind bereits plattformneutral, und die Trennung ist sauber.** + +--- + +## 10. Anhang: verifizierte Fundstellen + +### Behoben am 06.08.2026 + +| Thema | Datei:Zeile | +|---|---| +| Kulturabhängiger Preis-Parse (Faktor 100) | `…CopyTrading/Services/TraderMonitorService.cs:1276-1280` → nutzt jetzt `ParseDecimal` | +| `ParseDecimal` testbar + dokumentiert | `…CopyTrading/Services/TraderMonitorService.cs:1223-1234` | +| Kulturabhängige Parses (Regex-Gruppen) | `…Core/Services/PolymarketClobClient.cs:770, 784, 796, 820, 822, 827` | +| Kulturabhängiger Unix-Timestamp | `…CopyTrading/Services/MasterTraderAnalyticsJob.cs:140` | +| Neuer Regressionstest (10 Fälle, de-DE) | `tests/PolyTrader.Tests/ApiNumberParsingTests.cs` | +| Threema vollständig entfernt | `ThreemaService.cs`, `libs/Threema-MsgApi-Net-Core/` (35 Dateien), `ServerSettings` (7 Properties), `Program.cs`, `SettingsView.cs`, `PolyTrader.Core.csproj`, `PolyTraderSharp.sln` | +| Neutraler Benachrichtigungsausgang | `src/PolyTrader.Core/Notifications/INotificationSink.cs`, `LogNotificationSink.cs` | +| Fachdienste auf `INotificationSink` umgestellt | `MasterTraderAnalyticsJob.cs`, `SellLadderService.cs`, `DailyReportService.cs` | +| Testdouble mit Assertion-Möglichkeit | `tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs` | + +### Offen + +| Thema | Datei:Zeile | +|---|---| +| Windows-Zeitzonen-ID hartcodiert | `Ui/Views/TerminalView.cs:150` | +| Harter Windows-Pfad (Mullvad) | `src/PolyTrader.Core/Models/ServerSettings.cs:73` | +| Mullvad-Statusprüfung (Wortlaut) | `src/PolyTrader.Core/Services/MullvadVpnService.cs:HealthCheckAsync` | +| UI-Contract mit WinForms-Typen | `src/PolyTrader.Core/Modularity/ModuleUi.cs:32, 36` | +| Menü wird zur Laufzeit gebaut (neue Layout-Regel) | `src/PolyTrader.Core/Modularity/WindowMenu.cs:Populate`, `Ui/ShellUiHost.cs:AttachWindowMenu` | +| `System.Drawing.Color` in Modul-Logik | `…CopyTrading/Logic/TradeRowColoring.cs:11-13` | +| ScottPlot → `System.Drawing.Bitmap` | `Ui/Views/DashboardView.cs:207-227` | +| Windows-only Schrift im PDF | `…Accounting/Logic/PdfExporter.cs:34` | +| Schlüsseldateien ohne Rechteschutz | `Ui/Views/SettingsView.cs:301, 356`; `Program.cs:393` | +| Headless-Smoke-Test (erhaltenswert) | `Program.cs:500-570` | +| Lizenz-Gate mit modalem Dialog | `Licensing/LicenseGate.cs`, `Program.cs:98` | +| Secrets im Klartext | `server_settings.xml` (`WatchdogToken`, `LicenseKey`) | + +### Referenzen + +- `J:\Softwareprojekte\Deploymentcenter\docs\LICENSE_INTEGRATION_GUIDE.md` – Hardware-ID v2, `LLS2`-Cache, CLI-Schalter, v1→v2-Migration +- `J:\Softwareprojekte\Deploymentcenter\docs\WATCHDOG_INTEGRATION_GUIDE.md` – Ping-API, `X-Agent-Token`, Payload-Schema +- `J:\Softwareprojekte\Deploymentcenter\client-dotnet\Deploymentcenter.Client` – SDK-Quellen (`LicenseClient`, `HardwareId`, `ILicensePrompt`, `StateStore`) diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/APIConnector.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/APIConnector.cs deleted file mode 100644 index 7d6d4c7..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/APIConnector.cs +++ /dev/null @@ -1,402 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; -using IcgSoftware.Threema.CoreMsgApi.Results; -using Microsoft.Extensions.Configuration; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - /// - /// Facilitates HTTPS communication with the Threema Message API. - /// - public class APIConnector - { - public const string DEFAULTAPIURL = "https://msgapi.threema.ch/"; - - private readonly string apiUrl; - private readonly PublicKeyStore publicKeyStore; - private readonly string apiIdentity; - private readonly string secret; - - public APIConnector(string apiIdentity, string secret, PublicKeyStore publicKeyStore) : - this(apiIdentity, secret, APIConnector.DEFAULTAPIURL, publicKeyStore) - { - } - public APIConnector(string apiIdentity, string secret, string apiUrl, PublicKeyStore publicKeyStore) - { - this.apiIdentity = apiIdentity; - this.secret = secret; - this.apiUrl = apiUrl; - - if (publicKeyStore != null) - { - this.publicKeyStore = publicKeyStore; - } - else - { - this.publicKeyStore = this.GetSQLiteDbProvider(); - } - } - - /// - /// Lookup credits for an ID. - /// - /// credits or null - public int? LookupCredits() - { - string res = DoGet(new Uri(this.apiUrl + "credits"), - MakeRequestParams()); - if(res != null) - { - int credits; - int.TryParse(res, out credits); - return credits; - } - return null; - } - - /// - /// Lookup an ID by email address. The email address will be hashed before - /// being sent to the server. - /// - /// the email address - /// the ID, or null if not found - public string LookupEmail(string email) - { - try - { - Dictionary getParams = MakeRequestParams(); - - byte[] emailHash = CryptTool.HashEmail(email); - - return DoGet(new Uri(this.apiUrl + "lookup/email_hash/" + DataUtils.ByteArrayToHexString(emailHash)), getParams); - } - catch (FileNotFoundException) - { - return null; - } - } - - /// - /// Lookup a public key by ID. - /// - /// The ID whose public key is desired - /// The corresponding public key, or null if not found - public byte[] LookupKey(string id) - { - byte[] key = this.publicKeyStore.GetPublicKey(id); - if (key == null) - { - try - { - Dictionary getParams = MakeRequestParams(); - string pubkeyHex = DoGet(new Uri(this.apiUrl + "pubkeys/" + id), getParams); - key = DataUtils.HexStringToByteArray(pubkeyHex); - - this.publicKeyStore.SetPublicKey(id, key); - } - catch (FileNotFoundException) - { - return null; - } - } - return key; - } - - /// - /// Lookup the capabilities of a ID - /// - /// The ID whose capabilities should be checked - /// The capabilities, or null if not found - public CapabilityResult LookupKeyCapability(string threemaId) - { - string res = DoGet(new Uri(this.apiUrl + "capabilities/" + threemaId), - MakeRequestParams()); - if (res != null) - { - return new CapabilityResult(threemaId, res.Split(',')); - } - return null; - } - - /// - /// Lookup an ID by phone number. The phone number will be hashed before - /// being sent to the server. - /// - /// the phone number in E.164 format - /// the ID, or null if not found - public string LookupPhone(string phoneNumber) - { - try - { - Dictionary getParams = MakeRequestParams(); - - byte[] phoneHash = CryptTool.HashPhoneNo(phoneNumber); - - return DoGet(new Uri(this.apiUrl + "lookup/phone_hash/" + DataUtils.ByteArrayToHexString(phoneHash)), getParams); - } - catch (FileNotFoundException) - { - return null; - } - } - - /// - /// Download a file given its blob ID. - /// - /// The blob ID of the file - /// Encrypted file data - public byte[] DownloadFile(byte[] blobId) - { - return this.DownloadFile(blobId, null); - } - - /// - /// Download a file given its blob ID. - /// - /// The blob ID of the file - /// An object that will receive progress information, or null - /// Encrypted file data - public byte[] DownloadFile(byte[] blobId, IProgressListener progressListener) - { - string queryString = MakeUrlEncoded(MakeRequestParams()); - Uri blobUrl = new Uri(string.Format(this.apiUrl + "blobs/{0}?{1}", - DataUtils.ByteArrayToHexString(blobId), queryString)); - - byte[] blob; - - WebRequest request = WebRequest.CreateHttp(blobUrl); - request.Method = "GET"; - request.Timeout = 20 * 1000; - - using (WebResponse response = request.GetResponse()) - using (Stream stream = response.GetResponseStream()) - { - blob = DataUtils.StreamToBytes(stream, progressListener); - - stream.Close(); - response.Close(); - } - - return blob; - } - - /// - /// Upload a file. - /// - /// The result of the file encryption (i.e. encrypted file data) - /// the result of the upload - public UploadResult UploadFile(EncryptResult fileEncryptionResult) - { - string attachmentName = "blob"; - string attachmentFileName = "blob.file"; - string crlf = "\r\n"; - string twoHyphens = "--"; - - char[] chars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray(); - string boundary = string.Empty; - Random rand = new Random(); - int count = rand.Next(11) + 30; - for (int i = 0; i < count; i++) - { - boundary += chars[rand.Next(chars.Length)]; - } - - byte[] header = Encoding.UTF8.GetBytes(twoHyphens + boundary + crlf + - "Content-Disposition: form-data; name=\"" + attachmentName + "\";filename=\"" + attachmentFileName + "\"" + crlf + crlf - ); - byte[] footer = Encoding.UTF8.GetBytes(crlf + twoHyphens + boundary + twoHyphens + crlf); - byte[] postData = new byte[header.Length + fileEncryptionResult.Result.Length + footer.Length]; - - header.CopyTo(postData, 0); - fileEncryptionResult.Result.CopyTo(postData, header.Length); - footer.CopyTo(postData, header.Length + fileEncryptionResult.Result.Length); - - string queryString = MakeUrlEncoded(MakeRequestParams()); - Uri url = new Uri(this.apiUrl + "upload_blob?" + queryString); - - WebRequest request = WebRequest.CreateHttp(url); - request.Method = "POST"; - - if (!WebHeaderCollection.IsRestricted("Connection")) - { - request.Headers.Set(HttpRequestHeader.Connection, "Keep-Alive"); - } - if (!WebHeaderCollection.IsRestricted("CacheControl")) - { - request.Headers.Add(HttpRequestHeader.CacheControl, "no-cache"); - } - - request.ContentType = "multipart/form-data;boundary=" + boundary; - request.ContentLength = postData.Length; - - using (Stream stream = request.GetRequestStream()) - { - stream.Write(postData, 0, postData.Length); - stream.Close(); - } - - string responseData; - HttpStatusCode responseCode = GetResponse(request, out responseData); - - return new UploadResult((int)responseCode, responseData != null ? DataUtils.HexStringToByteArray(responseData) : null); - } - - /// - /// Send an end-to-end encrypted message. - /// - /// recipient ID - /// nonce used for encryption (24 bytes) - /// encrypted message data (max. 4000 bytes) - /// message ID - public string SendE2EMessage(string to, byte[] nonce, byte[] box) - { - Dictionary postParams = MakeRequestParams(); - postParams.Add("to", to); - postParams.Add("nonce", DataUtils.ByteArrayToHexString(nonce)); - postParams.Add("box", DataUtils.ByteArrayToHexString(box)); - - return DoPost(new Uri(this.apiUrl + "send_e2e"), postParams); - } - - /// - /// Send a text message with server-side encryption. - /// - /// recipient ID - /// message text (max. 3500 bytes) - /// message ID - public string SendTextMessageSimple(string to, string text) - { - Dictionary postParams = MakeRequestParams(); - postParams.Add("to", to); - postParams.Add("text", text); - - return DoPost(new Uri(this.apiUrl + "send_simple"), postParams); - } - - private string DoGet(Uri url, Dictionary getParams) - { - if (getParams != null) - { - string queryString = MakeUrlEncoded(getParams); - - url = new Uri(url.ToString() + "?" + queryString); - } - - WebRequest request = WebRequest.CreateHttp(url); - request.Method = "GET"; - - string responseData; - HttpStatusCode responseCode = GetResponse(request, out responseData); - if (responseCode != HttpStatusCode.OK) - { - throw new HttpListenerException((int)responseCode); - } - - return responseData; - } - - private string DoPost(Uri url, Dictionary postParams) - { - ASCIIEncoding encoding = new ASCIIEncoding(); - byte[] postData = encoding.GetBytes(MakeUrlEncoded(postParams)); - - WebRequest request = WebRequest.CreateHttp(url); - request.Method = "POST"; - request.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8"); - request.ContentType = "application/x-www-form-urlencoded"; - request.ContentLength = postData.Length; - - using (Stream stream = request.GetRequestStream()) - { - stream.Write(postData, 0, postData.Length); - stream.Close(); - } - - string responseData; - HttpStatusCode responseCode = GetResponse(request, out responseData); - if (responseCode != HttpStatusCode.OK) - { - throw new HttpListenerException((int)responseCode); - } - - return responseData; - } - - private HttpStatusCode GetResponse(WebRequest request, out string responseData) - { - responseData = null; - - WebResponse response = request.GetResponse(); - HttpStatusCode status = ((HttpWebResponse)response).StatusCode; - if (status == HttpStatusCode.OK) - { - using (Stream stream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(stream)) - { - responseData = reader.ReadToEnd(); - - reader.Close(); - stream.Close(); - } - } - response.Close(); - - return status; - } - - private Dictionary MakeRequestParams() - { - Dictionary postParams = new Dictionary(); - postParams.Add("from", apiIdentity); - postParams.Add("secret", secret); - return postParams; - } - - private String MakeUrlEncoded(Dictionary parameters) - { - StringBuilder s = new StringBuilder(); - - foreach (KeyValuePair param in parameters) - { - if (s.Length > 0) - { - s.Append('&'); - } - - s.Append(param.Key); - s.Append('='); - s.Append(DataUtils.Utf8Endcode(WebUtility.UrlEncode(param.Value))); - } - - return s.ToString(); - } - - private PublicKeyStore GetSQLiteDbProvider() - { - PublicKeyStore store = null; - - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); - IConfigurationRoot configuration = builder.Build(); - string connectionString = configuration.GetConnectionString("SQLiteConnectionString"); - if (!String.IsNullOrWhiteSpace(connectionString)) - { - store = new PublicKeyStoreDb(connectionString); - } - else - { - store = new PublicKeyStoreNone(); - } - - return store; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/CryptToolWrapper.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/CryptToolWrapper.cs deleted file mode 100644 index 05ea4bd..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/CryptToolWrapper.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Messages; -using IcgSoftware.Threema.CoreMsgApi.Results; - -namespace IcgSoftware.Threema.CoreMsgApi.Com -{ - [ComVisible(true)] - [ProgId("Threema.MsgApi.Com.CryptTool")] - [Guid("0B98D653-CD23-4827-9C5B-AEC0DCFBD142")] - public class CryptToolWrapper : ICryptToolWrapper - { - /// - /// Wrapper to encrypt text - /// - /// Text to encrypt - /// Sender private key as hex-string - /// Recipient public key as hex-string - /// Array with encrypted text, nonce and size - public ArrayList EncryptTextMessage(string text, string senderPrivateKey, string recipientPublicKey) - { - byte[] privateKey = GetKey(senderPrivateKey, Key.KeyType.PRIVATE); - byte[] publicKey = GetKey(recipientPublicKey, Key.KeyType.PUBLIC); - - string textEncoded = DataUtils.Utf8Endcode(text); - - EncryptResult encryptResult = CryptTool.EncryptTextMessage(textEncoded, privateKey, publicKey); - - var result = new ArrayList(); - result.Add(DataUtils.ByteArrayToHexString(encryptResult.Result)); - result.Add(DataUtils.ByteArrayToHexString(encryptResult.Nonce)); - result.Add(encryptResult.Size.ToString()); - return result; - } - - /// - /// Wrapper to decrypt box - /// - /// Encrypted box as hex-straing - /// Recipient private key as hex-string - /// Sender public key as hex-string - /// Nonce as hex-string - /// Array with type and decrypted message - public ArrayList DecryptMessage(string box, string recipientPrivateKey, string senderPublicKey, string nonce) - { - byte[] privateKey = GetKey(recipientPrivateKey, Key.KeyType.PRIVATE); - byte[] publicKey = GetKey(senderPublicKey, Key.KeyType.PUBLIC); - byte[] nonceBytes = DataUtils.HexStringToByteArray(nonce); - byte[] boxBytes = DataUtils.HexStringToByteArray(box); - - ThreemaMessage message = CryptTool.DecryptMessage(boxBytes, privateKey, publicKey, nonceBytes); - - var result = new ArrayList(); - result.Add(message.GetTypeCode().ToString()); - result.Add(message.ToString()); - return result; - } - - /// - /// Wrapper to hash email - /// - /// Email adress - /// Hash of email adress as hex-string - public string HashEmail(string email) - { - byte[] emailHash = CryptTool.HashEmail(email); - return DataUtils.ByteArrayToHexString(emailHash); - } - - /// - /// Wrapper to hash email - /// - /// Phone number - /// Hash of phone number as hex-string - public string HashPhoneNo(string phoneNo) - { - byte[] phoneHash = CryptTool.HashPhoneNo(phoneNo); - return DataUtils.ByteArrayToHexString(phoneHash); - } - - /// - /// Wrapper to generate key pair - /// - /// Full path name of private key file - /// Full path name of public key file - public void GenerateKeyPair(string privateKeyPath, string publicKeyPath) - { - byte[] privateKey = new byte[32]; //NaCl.SECRETKEYBYTES - byte[] publicKey = new byte[32]; //NaCl.PUBLICKEYBYTES - - CryptTool.GenerateKeyPair(ref privateKey, ref publicKey); - - // Write both keys to file - DataUtils.WriteKeyFile(privateKeyPath, new Key(Key.KeyType.PRIVATE, privateKey)); - DataUtils.WriteKeyFile(publicKeyPath, new Key(Key.KeyType.PUBLIC, publicKey)); - } - - /// - /// Wrapper to derive public key - /// - /// private key as file path or hex-string - /// Public key as hex-string - public string DerivePublicKey(string privateKey) - { - byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE); - byte[] publicKey = CryptTool.DerivePublicKey(privateKeyBytes); - - return new Key(Key.KeyType.PUBLIC, publicKey).Encode(); - } - - private byte[] GetKey(string argument, string expectedKeyType) - { - Key key; - - if (File.Exists(argument)) - { - key = DataUtils.ReadKeyFile(argument, expectedKeyType); - } - else - { - key = IcgSoftware.Threema.CoreMsgApi.Key.DecodeKey(argument, expectedKeyType); - } - - return key.key; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/ICryptToolWrapper.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/ICryptToolWrapper.cs deleted file mode 100644 index 49662da..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/ICryptToolWrapper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Results; - -namespace IcgSoftware.Threema.CoreMsgApi.Com -{ - [ComVisible(true)] - [Guid("4F376DEB-6F78-460A-836F-B38371712EFF")] - public interface ICryptToolWrapper - { - ArrayList EncryptTextMessage(string text, string senderPrivateKey, string recipientPublicKey); - ArrayList DecryptMessage(string box, string recipientPrivateKey, string senderPublicKey, string nonce); - string HashEmail(string email); - string HashPhoneNo(string phoneNo); - void GenerateKeyPair(string privateKeyPath, string publicKeyPath); - string DerivePublicKey(string privateKey); - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/IMessageToolWrapper.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/IMessageToolWrapper.cs deleted file mode 100644 index e97d20e..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/IMessageToolWrapper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Com -{ - [ComVisible(true)] - [Guid("E9E32936-CCAB-4297-BAB6-7F3B5939F3D4")] - public interface IMessageToolWrapper - { - string SendTextMessageSimple(string to, string from, string secret, string text, string apiUrl = APIConnector.DEFAULTAPIURL); - string SendTextMessage(string to, string from, string secret, string privateKey, string text, string apiUrl = APIConnector.DEFAULTAPIURL); - string SendImageMessage(string to, string from, string secret, string privateKey, string imageFilePath, string apiUrl = APIConnector.DEFAULTAPIURL); - string SendFileMessage(string to, string from, string secret, string privateKey, string file, string thumbnail = null, string apiUrl = APIConnector.DEFAULTAPIURL); - string LookupEmail(string email, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL); - string LookupPhone(string phoneNo, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL); - string LookupKey(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL); - ArrayList LookupKeyCapability(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL); - int? LookupCredits(string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL); - ArrayList ReceiveMessage(string id, string from, string secret, string privateKey, string messageId, string nonce, string box, string outputFolder = null, string apiUrl = APIConnector.DEFAULTAPIURL); - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/MessageToolWrapper.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/MessageToolWrapper.cs deleted file mode 100644 index 9aad030..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Com/MessageToolWrapper.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Helpers; -using IcgSoftware.Threema.CoreMsgApi.Results; - -namespace IcgSoftware.Threema.CoreMsgApi.Com -{ - [ComVisible(true)] - [ProgId("Threema.MsgApi.Com.MessageTool")] - [Guid("097F1D26-B38E-4501-845D-6DE7DBFB5EAA")] - public class MessageToolWrapper : IMessageToolWrapper - { - - /// - /// Wrapper to send simple message - /// - /// Recipient id - /// Sender id - /// Sender sercret - /// Text message - /// Optional api url - /// Message id - public string SendTextMessageSimple(string to, string from, string secret, string text, string apiUrl = APIConnector.DEFAULTAPIURL) - { - APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl); - return apiConnector.SendTextMessageSimple(to, DataUtils.Utf8Endcode(text)); - } - - /// - /// Wrapper to send text message E2E - /// - /// Recipient id - /// Sender id - /// Sender sercret - /// Sender private key - /// Text message - /// Optional api url - /// Message id - public string SendTextMessage(string to, string from, string secret, string privateKey, string text, string apiUrl = APIConnector.DEFAULTAPIURL) - { - byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE); - - E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes); - return e2EHelper.SendTextMessage(to, DataUtils.Utf8Endcode(text)); - } - - /// - /// Wrapper to send image message E2E - /// - /// Recipient id - /// Sender id - /// Sender sercret - /// Sender private key - /// File path to image - /// Optional api url - /// Message id - public string SendImageMessage(string to, string from, string secret, string privateKey, string imageFilePath, string apiUrl = APIConnector.DEFAULTAPIURL) - { - byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE); - - E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes); - return e2EHelper.SendImageMessage(to, imageFilePath); - } - - /// - /// Wrapper to send file message E2E - /// - /// Recipient id - /// Sender id - /// Sender sercret - /// Sender private key - /// File path to file - /// File path to thumbnail - /// Optional api url - /// Message id - public string SendFileMessage(string to, string from, string secret, string privateKey, string file, string thumbnail = null, string apiUrl = APIConnector.DEFAULTAPIURL) - { - byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE); - FileInfo fileInfo = file != null ? new FileInfo(file) : null; - FileInfo thumbnailInfo = thumbnail != null ? new FileInfo(thumbnail) : null; - - E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes); - return e2EHelper.SendFileMessage(to, fileInfo, thumbnailInfo); - } - - /// - /// Wrapper to id lookup via email - /// - /// Email for lookup - /// Sender id - /// Sender secret - /// Optional api url - /// id - public string LookupEmail(string email, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL) - { - APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl); - return apiConnector.LookupEmail(email); - } - - /// - /// Wrapper to id lookup via phone number - /// - /// Phone number for lookup - /// Sender id - /// Sender secret - /// Optional api url - /// id - public string LookupPhone(string phoneNo, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL) - { - APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl); - return apiConnector.LookupPhone(phoneNo); - } - - /// - /// Wrapper to lookup/fetch public key - /// - /// Id for lookup - /// Sender id - /// Sender secret - /// Optional api url - /// public key has hex-string - public string LookupKey(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL) - { - APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl); - byte[] publicKey = apiConnector.LookupKey(threemaId); - if (publicKey != null) - { - return new Key(Key.KeyType.PUBLIC, publicKey).Encode(); - } - return null; - } - - /// - /// Wrapper to lookup capabilities - /// - /// Id for lookup - /// Sender id - /// Sender secret - /// Optional api url - /// Array with capatilities - public System.Collections.ArrayList LookupKeyCapability(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL) - { - CapabilityResult capabilities = this.CreateConnector(from, secret, apiUrl) - .LookupKeyCapability(threemaId); - - var result = new ArrayList(); - capabilities.Capabilities.ToList().ForEach(c => result.Add(c)); - return result; - } - - /// - /// Wrapper to lookup credits - /// - /// From id - /// From secret - /// Optional api url - /// credits or null - public int? LookupCredits(string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL) - { - return this.CreateConnector(from, secret, apiUrl).LookupCredits(); - } - - /// - /// Wrapper to receive message and download files - /// - /// Sender id - /// From id - /// From secret - /// From private key - /// Message id - /// Nonce as hex-string - /// Box message as hex-string - /// Optional path to output folder - /// Optional api url - /// Array with message-type, message-id and message - public ArrayList ReceiveMessage(string id, string from, string secret, string privateKey, string messageId, string nonce, string box, string outputFolder = null, string apiUrl = APIConnector.DEFAULTAPIURL) - { - byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE); - byte[] nonceBytes = DataUtils.HexStringToByteArray(nonce); - - E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes); - - byte[] boxBytes = DataUtils.HexStringToByteArray(box); - - ReceiveMessageResult res = e2EHelper.ReceiveMessage(id, messageId, boxBytes, nonceBytes, outputFolder); - - ArrayList result = new ArrayList(); - result.Add(res.Message.GetTypeCode().ToString()); - result.Add(res.MessageId); - result.Add(res.Message.ToString()); - return result; - } - - private APIConnector CreateConnector(string gatewayId, string secret, string apiUrl) - { - return new APIConnector(gatewayId, secret, apiUrl, new PublicKeyStoreNone()); - } - - private byte[] GetKey(string argument, string expectedKeyType) - { - Key key; - - if (File.Exists(argument)) - { - key = DataUtils.ReadKeyFile(argument, expectedKeyType); - } - else - { - key = IcgSoftware.Threema.CoreMsgApi.Key.DecodeKey(argument, expectedKeyType); - } - - return key.key; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/CryptTool.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/CryptTool.cs deleted file mode 100644 index e43f648..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/CryptTool.cs +++ /dev/null @@ -1,380 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; -using IcgSoftware.Threema.CoreMsgApi.Messages; -using IcgSoftware.Threema.CoreMsgApi.Results; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - /// - /// Contains static methods to do various Threema cryptography related tasks. - /// - public class CryptTool - { - // HMAC-SHA256 keys for email/mobile phone hashing - private static readonly byte[] EMAIL_HMAC_KEY = new byte[] {(byte)0x30,(byte)0xa5,(byte)0x50,(byte)0x0f,(byte)0xed,(byte)0x97,(byte)0x01,(byte)0xfa,(byte)0x6d,(byte)0xef,(byte)0xdb,(byte)0x61,(byte)0x08,(byte)0x41,(byte)0x90,(byte)0x0f,(byte)0xeb,(byte)0xb8,(byte)0xe4,(byte)0x30,(byte)0x88,(byte)0x1f,(byte)0x7a,(byte)0xd8,(byte)0x16,(byte)0x82,(byte)0x62,(byte)0x64,(byte)0xec,(byte)0x09,(byte)0xba,(byte)0xd7}; - private static readonly byte[] PHONENO_HMAC_KEY = new byte[] {(byte)0x85,(byte)0xad,(byte)0xf8,(byte)0x22,(byte)0x69,(byte)0x53,(byte)0xf3,(byte)0xd9,(byte)0x6c,(byte)0xfd,(byte)0x5d,(byte)0x09,(byte)0xbf,(byte)0x29,(byte)0x55,(byte)0x5e,(byte)0xb9,(byte)0x55,(byte)0xfc,(byte)0xd8,(byte)0xaa,(byte)0x5e,(byte)0xc4,(byte)0xf9,(byte)0xfc,(byte)0xd8,(byte)0x69,(byte)0xe2,(byte)0x58,(byte)0x37,(byte)0x07,(byte)0x23}; - - private static readonly byte[] FILE_NONCE = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}; - private static readonly byte[] FILE_THUMBNAIL_NONCE = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02}; - - private const int SYMMKEYBYTES = 32; - - /// - /// Encrypt a text message. - /// - /// the text to be encrypted (max. 3500 bytes) - /// the private key of the sending ID - /// the public key of the receiving ID - /// - public static EncryptResult EncryptTextMessage(String text, byte[] senderPrivateKey, byte[] recipientPublicKey) - { - return EncryptMessage(new TextMessage(text), senderPrivateKey, recipientPublicKey); - } - - /// - /// Encrypt an image message. - /// - /// result of the image encryption - /// result of the upload - /// the private key of the sending ID - /// the public key of the receiving ID - /// encrypted result - public static EncryptResult EncryptImageMessage(EncryptResult encryptResult, UploadResult uploadResult, byte[] senderPrivateKey, byte[] recipientPublicKey) - { - return EncryptMessage( - new ImageMessage(uploadResult.BlobId, - encryptResult.Size, - encryptResult.Nonce), - senderPrivateKey, - recipientPublicKey); - } - - /// - /// Encrypt a file message. - /// - /// result of the file data encryption - /// result of the upload - /// MIME type of the file - /// File name - /// Size of the file, in bytes - /// result of thumbnail upload - /// Private key of sender - /// Public key of recipient - /// Result of the file message encryption (not the same as the file data encryption!) - public static EncryptResult EncryptFileMessage(EncryptResult encryptResult, - UploadResult uploadResult, - String mimeType, - String fileName, - int fileSize, - UploadResult uploadResultThumbnail, - byte[] senderPrivateKey, byte[] recipientPublicKey) - { - return EncryptMessage( - new FileMessage(uploadResult.BlobId, - encryptResult.Secret, - mimeType, - fileName, - fileSize, - uploadResultThumbnail != null ? uploadResultThumbnail.BlobId : null), - senderPrivateKey, - recipientPublicKey); - } - - private static EncryptResult EncryptMessage(ThreemaMessage threemaMessage, byte[] privateKey, byte[] publicKey) - { - // determine random amount of PKCS7 padding - int padbytes = new Random().Next(254) + 1; - - byte[] messageBytes; - try - { - messageBytes = threemaMessage.GetData(); - } - catch - { - return null; - } - - // prepend type byte (0x02) to message data - byte[] data = new byte[1 + messageBytes.Length + padbytes]; - data[0] = (byte)threemaMessage.GetTypeCode(); - - messageBytes.CopyTo(data, 1); - - // append padding - for (int i = 0; i < padbytes; i++) - { - data[i + 1 + messageBytes.Length] = (byte)padbytes; - } - - return Encrypt(data, privateKey, publicKey); - } - - /// - /// Decrypt an NaCl box using the recipient's private key and the sender's public key. - /// - /// The box to be decrypted - /// The private key of the recipient - /// The public key of the sender - /// The nonce that was used for encryption - /// The decrypted data, or null if decryption failed - public static byte[] Decrypt(byte[] box, byte[] privateKey, byte[] publicKey, byte[] nonce) - { - return Sodium.PublicKeyBox.Open(box, nonce, privateKey, publicKey); - } - - /// - /// Decrypt symmetrically encrypted file data. - /// - /// The encrypted file data - /// The symmetric key that was used for encryption - /// The decrypted file data, or null if decryption failed - public static byte[] DecryptFileData(byte[] fileData, byte[] secret) - { - byte[] box = new byte[fileData.Length + 16]; - fileData.CopyTo(box, 16); - return Sodium.SecretBox.Open(box, FILE_NONCE, secret); - } - - /// - /// Decrypt symmetrically encrypted file thumbnail data. - /// - /// The encrypted thumbnail data - /// The symmetric key that was used for encryption - /// The decrypted thumbnail data, or null if decryption failed - public static byte[] DecryptFileThumbnailData(byte[] fileData, byte[] secret) - { - byte[] box = new byte[fileData.Length + 16]; - fileData.CopyTo(box, 16); - return Sodium.SecretBox.Open(box, FILE_THUMBNAIL_NONCE, secret); - } - - /// - /// Decrypt a message. - /// - /// the box to be decrypted - /// the private key of the receiving ID - /// the public key of the sending ID - /// the nonce that was used for the encryption - /// decrypted message (text or delivery receipt) - public static ThreemaMessage DecryptMessage(byte[] box, byte[] recipientPrivateKey, byte[] senderPublicKey, byte[] nonce) - { - byte[] data = Decrypt(box, recipientPrivateKey, senderPublicKey, nonce); - if (data == null) - { - throw new DecryptionFailedException(); - } - - // remove padding - int padbytes = data[data.Length - 1] & 0xFF; - int realDataLength = data.Length - padbytes; - if (realDataLength < 1) - { - // Bad message padding - throw new BadMessageException(); - } - - // first byte of data is type - int type = data[0] & 0xFF; - - switch (type) - { - case TextMessage.TYPE_CODE: - // Text message - if (realDataLength < 2) - { - throw new BadMessageException(); - } - - return new TextMessage(Encoding.UTF8.GetString(data.Skip(1).Take(realDataLength - 1).ToArray())); - - case DeliveryReceipt.TYPE_CODE: - /* Delivery receipt */ - if (realDataLength < MessageId.MESSAGE_ID_LEN + 2 || ((realDataLength - 2) % MessageId.MESSAGE_ID_LEN) != 0) - { - throw new BadMessageException(); - } - - DeliveryReceipt.Type receiptType = (DeliveryReceipt.Type)Enum.Parse(typeof(DeliveryReceipt.Type), Convert.ToString((int)data[1] & 0xFF)); - if (receiptType == null) - { - throw new BadMessageException(); - } - - IEnumerable messageIds = new LinkedList(); - - int numMsgIds = ((realDataLength - 2) / MessageId.MESSAGE_ID_LEN); - for (int i = 0; i < numMsgIds; i++) - { - messageIds.ToList().Add(new MessageId(data, 2 + i*MessageId.MESSAGE_ID_LEN)); - } - - return new DeliveryReceipt(receiptType, messageIds.ToList()); - - case ImageMessage.TYPE_CODE: - if(realDataLength != (1 + ThreemaMessage.BLOB_ID_LEN + 4 + ThreemaMessage.NONCEBYTES)) - { - throw new BadMessageException(); - } - byte[] blobId = new byte[ThreemaMessage.BLOB_ID_LEN]; - data.Skip(1).Take(ThreemaMessage.BLOB_ID_LEN).ToArray().CopyTo(blobId, 0); - int size = ReadSwappedInteger(data, 1 + ThreemaMessage.BLOB_ID_LEN); - byte[] fileNonce = new byte[ThreemaMessage.NONCEBYTES]; - data.Skip(1 + ThreemaMessage.BLOB_ID_LEN + 4).Take(ThreemaMessage.NONCEBYTES).ToArray().CopyTo(fileNonce, 0); - - return new ImageMessage(blobId, size, fileNonce); - - case FileMessage.TYPE_CODE: - ASCIIEncoding encoding = new ASCIIEncoding(); - return FileMessage.FromString(encoding.GetString(data.Skip(1).Take(realDataLength - 1).ToArray())); - - default: - throw new UnsupportedMessageTypeException(); - } - } - - private static int ReadSwappedInteger(byte[] data, int offset) - { - return ((data[offset + 0] & 255) << 0) + ((data[offset + 1] & 255) << 8) + ((data[offset + 2] & 255) << 16) + ((data[offset + 3] & 255) << 24); - } - - - /// - /// Generate a new key pair. - /// - /// is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes) - /// is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes) - public static void GenerateKeyPair(ref byte[] privateKey, ref byte[] publicKey) - { - if (publicKey.Length != Sodium.SealedPublicKeyBox.RecipientPublicKeyBytes || privateKey.Length != Sodium.SealedPublicKeyBox.RecipientSecretKeyBytes) - { - throw new ArgumentException("Wrong key length"); - } - - Sodium.KeyPair keyPair = Sodium.PublicKeyBox.GenerateKeyPair(); - privateKey = keyPair.PrivateKey; - publicKey = keyPair.PublicKey; - } - - /// - /// Encrypt data using NaCl asymmetric ("box") encryption. - /// - /// the data to be encrypted - /// is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes) - /// is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes) - /// - public static EncryptResult Encrypt(byte[] data,byte[] privateKey, byte[] publicKey) - { - if (publicKey.Length != Sodium.SealedPublicKeyBox.RecipientPublicKeyBytes || privateKey.Length != Sodium.SealedPublicKeyBox.RecipientSecretKeyBytes) - { - throw new ArgumentException("Wrong key length"); - } - - byte[] nonce = RandomNonce(); - byte[] box = Sodium.PublicKeyBox.Create(data, nonce, privateKey, publicKey); - return new EncryptResult(box, null, nonce); - } - - /// - /// Encrypt file data using NaCl symmetric encryption with a random key. - /// - /// the file contents to be encrypted - /// the encryption result including the random key - public static EncryptResult EncryptFileData(byte[] data) - { - //create random key - Random rnd = new Random(); - byte[] encryptionKey = new byte[CryptTool.SYMMKEYBYTES]; - rnd.NextBytes(encryptionKey); - - //encrypt file data in-place - data = Sodium.SecretBox.Create(data, FILE_NONCE, encryptionKey); - - //skip first temp 16 bytes for encryption - return new EncryptResult(data.Skip(16).ToArray(), encryptionKey, FILE_NONCE); - } - - /// - /// Encrypt file thumbnail data using NaCl symmetric encryption with a random key. - /// - /// data the file contents to be encrypted - /// - /// the encryption result including the random key - public static EncryptResult encryptFileThumbnailData(byte[] data, byte[] encryptionKey) - { - // encrypt file data in-place - data = Sodium.SecretBox.Create(data, FILE_THUMBNAIL_NONCE, encryptionKey); - - return new EncryptResult(data, encryptionKey, FILE_THUMBNAIL_NONCE); - } - - /// - /// Hashes an email address for identity lookup. - /// - /// email the email address - /// the raw hash - public static byte[] HashEmail(string email) - { - try - { - ASCIIEncoding encoding = new ASCIIEncoding(); - var hmac = new HMACSHA256(EMAIL_HMAC_KEY); - return hmac.ComputeHash(encoding.GetBytes(email.Trim())).ToArray(); - } - catch (Exception ex) - { - Debug.WriteLine("Error in HashEmail(): {0}", ex.Message); - return null; - } - } - - /// - /// Hashes a phone number for identity lookup. - /// - /// phoneNo the phone number - /// the raw hash - public static byte[] HashPhoneNo(string phoneNo) - { - try - { - ASCIIEncoding encoding = new ASCIIEncoding(); - var hmac = new HMACSHA256(PHONENO_HMAC_KEY); - return hmac.ComputeHash(encoding.GetBytes(Regex.Replace(phoneNo, "[^0-9]", ""))); - } - catch (Exception ex) - { - Debug.WriteLine("Error in HashPhoneNo(): {0}", ex.Message); - return null; - } - } - - /// - /// Generate a random nonce. - /// - /// random nonce - public static byte[] RandomNonce() - { - byte[] nonce = new byte[ThreemaMessage.NONCEBYTES]; - new Random().NextBytes(nonce); - return nonce; - } - - /// - /// Return the public key that corresponds with a given private key. - /// - /// The private key whose public key should be derived - /// The corresponding public key. - public static byte[] DerivePublicKey(byte[] privateKey) - { - Sodium.KeyPair keyPair = Sodium.PublicKeyBox.GenerateKeyPair(privateKey); - return keyPair.PublicKey; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/DataUtils.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/DataUtils.cs deleted file mode 100644 index 91d6d97..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/DataUtils.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - public static class DataUtils - { - private const int BUFFER_SIZE = 16384; - - /// - /// Convert a byte array into a hexadecimal string (lowercase). - /// - /// the bytes to encode - /// hex encoded string - public static string ByteArrayToHexString(byte[] bytes) - { - var hex = BitConverter.ToString(bytes); - return hex.Replace("-", ""); - } - - /// - /// Convert a string in hexadecimal representation to a byte array. - /// - /// hex string - /// decoded byte array - public static byte[] HexStringToByteArray(string s) - { - string sc = s.Replace("[^0-9a-fA-F]", ""); - int len = sc.Length; - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) - { - System.Diagnostics.Debug.WriteLine(sc.Substring(i, 2)); - data[i / 2] = Convert.ToByte(sc.Substring(i, 2), 16); - } - return data; - } - - /// - /// UTF8 encoded string. - /// - /// string to encode - /// encoded string - public static string Utf8Endcode(string value) - { - return Encoding.UTF8.GetString(Encoding.Default.GetBytes(value)); - } - - /// - /// Read hexadecimal data from a file and return it as a byte array. - /// - /// input file - /// the decoded data - public static byte[] ReadHexFile(string file) - { - byte[] data = null; - - using (FileStream stream = File.OpenRead(file)) - using (StreamReader reader = new StreamReader(stream)) - { - data = HexStringToByteArray(reader.ReadLine().Trim()); - reader.Close(); - } - - return data; - } - - /// - /// Read an encoded key from a file and return it as a key instance. - /// - /// input file - /// the decoded key - public static Key ReadKeyFile(string file) - { - return Key.DecodeKey(ReadLineFromFile(file)); - } - - /// - /// Read an encoded key from a file and return it as a key instance. - /// - /// input file - /// validates the key type (private or public) - /// the decoded key - public static Key ReadKeyFile(string file, string expectedKeyType) - { - return Key.DecodeKey(ReadLineFromFile(file), expectedKeyType); - } - - /// - /// Wirte stream data to byte array. - /// - /// data write to byte array - /// progress - /// bytes from stream - public static byte[] StreamToBytes(Stream stream, IProgressListener progressListener) - { - if (stream == null) - { - throw new ArgumentNullException("stream must not be null."); - } - - byte[] bytes; - - // Content length known? - if (stream.CanSeek) - { - bytes = new byte[stream.Length]; - int bytesRead = 0; - int offset = 0; - - //reader.Read - while (offset < stream.Length && (bytesRead = stream.Read(bytes, offset, (int)(bytes.Length - offset))) > 0) - { - offset += bytesRead; - - if (progressListener != null) - { - progressListener.updateProgress((int)(100 * offset / bytes.Length)); - } - } - - if (offset != (int)bytes.Length) - { - throw new IOException("Unexpected read size. current: " + offset + ", excepted: " + bytes.Length); - } - } - else - { - // Content length is unknown - need to read until EOF - byte[] buffer = new byte[BUFFER_SIZE]; - - using (MemoryStream outputStream = new MemoryStream()) - { - try - { - //int offset = 0; - while (true) - { - int bytesRead = stream.Read(buffer, 0, buffer.Length); - if (bytesRead == 0) - { - break; - } - outputStream.Write(buffer, 0, bytesRead); - } - } - catch (ArgumentOutOfRangeException) - { - } - - outputStream.Position = 0; - bytes = new byte[outputStream.Length]; - outputStream.Read(bytes, 0, bytes.Length); - } - } - - return bytes; - } - - /// - /// Write a byte array into a file in hexadecimal format. - /// - /// output file - /// the data to be written - public static void WriteHexFile(string file, byte[] data) - { - using (FileStream stream = File.OpenWrite(file)) - using (StreamWriter writer = new StreamWriter(stream)) - { - writer.Write(ByteArrayToHexString(data)); - writer.Write('\n'); - writer.Close(); - } - } - - /// - /// Write an encoded key to a file - /// Encoded key format: type:hex_key. - /// - /// output file - /// a key that will be encoded and written to a file - public static void WriteKeyFile(string file, Key key) - { - using (FileStream stream = File.OpenWrite(file)) - using (StreamWriter writer = new StreamWriter(stream)) - { - writer.Write(key.Encode()); - writer.Write('\n'); - } - } - - private static string ReadLineFromFile(string file) - { - string data = null; - - using (FileStream stream = File.OpenRead(file)) - using (StreamReader reader = new StreamReader(stream)) - { - data = reader.ReadLine().Trim(); - reader.Close(); - } - - return data; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/BadMessageException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/BadMessageException.cs deleted file mode 100644 index d0e296a..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/BadMessageException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - public class BadMessageException : Exception - { - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/CoreMigrationException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/CoreMigrationException.cs deleted file mode 100644 index f7328a1..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/CoreMigrationException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - public class CoreMigrationException : Exception - { - public CoreMigrationException(string message) : - base(message) - { - } - - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/DecryptionFailedException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/DecryptionFailedException.cs deleted file mode 100644 index e56195c..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/DecryptionFailedException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - class DecryptionFailedException : Exception - { - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/InvalidKeyException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/InvalidKeyException.cs deleted file mode 100644 index 520d85a..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/InvalidKeyException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - public class InvalidKeyException : Exception - { - public InvalidKeyException(string message) : - base(message) - { - } - - public InvalidKeyException(string message, Exception innerException) : - base(message, innerException) - { - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/MessageParseException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/MessageParseException.cs deleted file mode 100644 index 2ae00a3..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/MessageParseException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - public class MessageParseException : Exception - { - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/NotAllowedException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/NotAllowedException.cs deleted file mode 100644 index 6062759..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/NotAllowedException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - public class NotAllowedException : Exception - { - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/UnsupportedMessageTypeException.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/UnsupportedMessageTypeException.cs deleted file mode 100644 index f1a98d6..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Exceptions/UnsupportedMessageTypeException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Exceptions -{ - class UnsupportedMessageTypeException : Exception - { - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Helpers/E2EHelper.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Helpers/E2EHelper.cs deleted file mode 100644 index 00552ec..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Helpers/E2EHelper.cs +++ /dev/null @@ -1,319 +0,0 @@ -#if CoreWinOnly -using Microsoft.Win32; -#endif -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; -using IcgSoftware.Threema.CoreMsgApi.Messages; -using IcgSoftware.Threema.CoreMsgApi.Results; - -namespace IcgSoftware.Threema.CoreMsgApi.Helpers -{ - /// - /// Helper to handle Threema end-to-end encryption. - /// - public class E2EHelper - { - private readonly APIConnector apiConnector; - private readonly byte[] privateKey; - - public E2EHelper(APIConnector apiConnector, byte[] privateKey) - { - this.apiConnector = apiConnector; - this.privateKey = privateKey; - } - - /// - /// Decrypt a Message and download the blobs of the Message (e.g. image or file) - /// - /// Threema ID of the sender - /// Message ID - /// Encrypted box data of the file/image message - /// Nonce that was used for message encryption - /// Output folder for storing decrypted images/files - /// Result of message reception - public ReceiveMessageResult ReceiveMessage(string threemaId, string messageId, byte[] box, byte[] nonce, string outputFolder) - { - //fetch public key - byte[] publicKey = this.apiConnector.LookupKey(threemaId); - - if(publicKey == null) - { - throw new InvalidKeyException("invalid threema id"); - } - - ThreemaMessage message = CryptTool.DecryptMessage(box, this.privateKey, publicKey, nonce); - if(message == null) - { - return null; - } - - ReceiveMessageResult result = new ReceiveMessageResult(messageId, message); - - if (message.GetType() == typeof(ImageMessage)) - { - //download image - ImageMessage imageMessage = (ImageMessage)message; - byte[] fileData = this.apiConnector.DownloadFile(imageMessage.BlobId); - - if(fileData == null) - { - throw new MessageParseException(); - } - - byte[] decryptedFileContent = CryptTool.Decrypt(fileData, privateKey, publicKey, imageMessage.Nonce); - FileInfo imageFile = new FileInfo(outputFolder + "/" + messageId + ".jpg"); - - using (FileStream stream = File.OpenWrite(imageFile.FullName)) - { - stream.Write(decryptedFileContent, 0, decryptedFileContent.Length); - stream.Close(); - } - - result.Files.Add(imageFile); - } - else if (message.GetType() == typeof(FileMessage)) - { - //download file - FileMessage fileMessage = (FileMessage)message; - byte[] fileData = this.apiConnector.DownloadFile(fileMessage.BlobId); - - byte[] decryptedFileData = CryptTool.DecryptFileData(fileData, fileMessage.EncryptionKey); - FileInfo file = new FileInfo(outputFolder + "/" + messageId + "-" + fileMessage.FileName); - - using (FileStream stream = File.OpenWrite(file.FullName)) - { - stream.Write(decryptedFileData, 0, decryptedFileData.Length); - stream.Close(); - } - - result.Files.Add(file); - - if(fileMessage.ThumbnailBlobId != null) - { - byte[] thumbnailData = this.apiConnector.DownloadFile(fileMessage.ThumbnailBlobId); - - byte[] decryptedThumbnailData = CryptTool.DecryptFileThumbnailData(thumbnailData, fileMessage.EncryptionKey); - FileInfo thumbnailFile = new FileInfo(outputFolder + "/" + messageId + "-thumbnail.jpg"); - using (FileStream stream = File.OpenWrite(thumbnailFile.FullName)) - { - stream.Write(decryptedThumbnailData, 0, decryptedThumbnailData.Length); - stream.Close(); - } - - result.Files.Add(thumbnailFile); - } - } - - return result; - } - - /// - /// Encrypt a file message and send it to the given recipient. - /// The thumbnailMessagePath can be null. - /// - /// target Threema ID - /// the file to be sent - /// file for thumbnail; if not set, no thumbnail will be sent - /// generated message ID - public string SendFileMessage(string threemaId, FileInfo fileMessageFile, FileInfo thumbnailMessageFile) - { - //fetch public key - byte[] publicKey = this.apiConnector.LookupKey(threemaId); - - if (publicKey == null) - { - throw new InvalidKeyException("invalid threema id"); - } - - //check capability of a key - CapabilityResult capabilityResult = this.apiConnector.LookupKeyCapability(threemaId); - if (capabilityResult == null || !capabilityResult.CanImage) - { - throw new NotAllowedException(); - } - - if (fileMessageFile == null) - { - throw new ArgumentException("fileMessageFile must not be null."); - } - - if (!fileMessageFile.Exists) - { - throw new FileNotFoundException(fileMessageFile.FullName); - } - - byte[] fileData; - using (Stream stream = File.OpenRead(fileMessageFile.FullName)) - { - fileData = new byte[stream.Length]; - stream.Read(fileData, 0, (int)stream.Length); - stream.Close(); - } - - if (fileData == null) - { - throw new IOException("invalid file"); - } - - //encrypt the image - EncryptResult encryptResult = CryptTool.EncryptFileData(fileData); - - //upload the image - UploadResult uploadResult = apiConnector.UploadFile(encryptResult); - - if(!uploadResult.IsSuccess) - { - throw new IOException("could not upload file (upload response " + uploadResult.ResponseCode + ")"); - } - - UploadResult uploadResultThumbnail = null; - - if (thumbnailMessageFile != null && thumbnailMessageFile.Exists) - { - byte[] thumbnailData; - using (Stream stream = File.OpenRead(thumbnailMessageFile.FullName)) - { - thumbnailData = new byte[stream.Length]; - stream.Read(thumbnailData, 0, (int)stream.Length); - stream.Close(); - } - - if (thumbnailData == null) - { - throw new IOException("invalid thumbnail file"); - } - - //encrypt the thumbnail - EncryptResult encryptResultThumbnail = CryptTool.encryptFileThumbnailData(fileData, encryptResult.Secret); - - //upload the thumbnail - uploadResultThumbnail = this.apiConnector.UploadFile(encryptResultThumbnail); - } - - //send it - EncryptResult fileMessage = CryptTool.EncryptFileMessage( - encryptResult, - uploadResult, - GetMIMEType(fileMessageFile), - fileMessageFile.Name, - (int) fileMessageFile.Length, - uploadResultThumbnail, - privateKey, publicKey); - - return this.apiConnector.SendE2EMessage( - threemaId, - fileMessage.Nonce, - fileMessage.Result); - } - - /// - /// Encrypt an image message and send it to the given recipient. - /// - /// threemaId target Threema ID - /// path to read image data from - /// generated message ID - public string SendImageMessage(string threemaId, string imageFilePath) - { - //fetch public key - byte[] publicKey = this.apiConnector.LookupKey(threemaId); - - if (publicKey == null) - { - throw new InvalidKeyException("invalid threema id"); - } - - //check capability of a key - CapabilityResult capabilityResult = this.apiConnector.LookupKeyCapability(threemaId); - if (capabilityResult == null || !capabilityResult.CanImage) - { - throw new NotAllowedException(); - } - - byte[] fileData = File.ReadAllBytes(imageFilePath); - if (fileData == null) - { - throw new IOException("invalid file"); - } - - //encrypt the image - EncryptResult encryptResult = CryptTool.Encrypt(fileData, this.privateKey, publicKey); - - //upload the image - UploadResult uploadResult = apiConnector.UploadFile(encryptResult); - - if (!uploadResult.IsSuccess) - { - throw new IOException("could not upload file (upload response " + uploadResult.ResponseCode + ")"); - } - - //send it - EncryptResult imageMessage = CryptTool.EncryptImageMessage(encryptResult, uploadResult, this.privateKey, publicKey); - - return apiConnector.SendE2EMessage( - threemaId, - imageMessage.Nonce, - imageMessage.Result); - } - - /// - /// Encrypt a text message and send it to the given recipient. - /// - /// target Threema ID - /// the text to send - /// generated message ID - public string SendTextMessage(string threemaId, string text) - { - //fetch public key - byte[] publicKey = this.apiConnector.LookupKey(threemaId); - - if (publicKey == null) - { - throw new InvalidKeyException("invalid threema id"); - } - EncryptResult res = CryptTool.EncryptTextMessage(text, this.privateKey, publicKey); - - return this.apiConnector.SendE2EMessage(threemaId, res.Nonce, res.Result); - } - - /// - /// Get mime type of the file extension via registry. - /// - /// mime type of this file - /// mime type - private string GetMIMEType(FileInfo file) - { -#if CoreWinOnly - if (file == null) - { - throw new ArgumentException("file must not be null."); - } - - string mimeType = "application/unknown"; - - //Using Microsoft.Win32.Registry. But you lose portability and can't run the application on Linux and MacOS anymore. Implement GetMIMEType in another way. - RegistryKey regKey = Registry.ClassesRoot.OpenSubKey( - file.Extension.ToLower() - ); - - if (regKey != null) - { - object contentType = regKey.GetValue("Content Type"); - - if (contentType != null) - { - mimeType = contentType.ToString(); - } - } - - return mimeType; -#else - throw new CoreMigrationException("Using Microsoft.Win32.Registry. But you lose portability and can't run the application on Linux and MacOS anymore. Implement GetMIMEType in another way."); -#endif - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IProgressListener.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IProgressListener.cs deleted file mode 100644 index 75a37dd..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IProgressListener.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - public interface IProgressListener - { - /// - /// Update the progress of an upload/download process. - /// - /// in percent (0..100) - void updateProgress(int progress); - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IcgSoftware.Threema.CoreMsgApi.csproj b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IcgSoftware.Threema.CoreMsgApi.csproj deleted file mode 100644 index 6197d33..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/IcgSoftware.Threema.CoreMsgApi.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net8.0 - false - - - - TRACE;DEBUG;NETCOREAPP;NETCOREAPP2_1;CoreWinOnly - - - - - - - - - - - - - - diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Key.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Key.cs deleted file mode 100644 index 1a9cdcc..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Key.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - /// - /// Encapsulates an asymmetric key, either public or private. - /// - public class Key - { - public static readonly char separator = ':'; - - public static class KeyType - { - public const string PRIVATE = "private"; - public const string PUBLIC = "public"; - } - - public byte[] key; - public string type; - - public Key(string type, byte[] key) - { - this.key = key; - this.type = type; - } - - - /// - /// Decodes and validates an encoded key. - /// Encoded key format: type:hex_key - /// - /// an encoded key - /// - public static Key DecodeKey(string encodedKey) - { - // Split key and check length - string[] keyArray = encodedKey.Split(Key.separator); - if (keyArray.Length != 2) - { - throw new InvalidKeyException("Does not contain a valid key format"); - } - - // Unpack key - string keyType = keyArray[0]; - string keyContent = keyArray[1]; - - // Is this a valid hex key? - if (!Regex.IsMatch(keyContent, "[0-9a-fA-F]{64}")) - { - throw new InvalidKeyException("Does not contain a valid key"); - } - - return new Key(keyType, DataUtils.HexStringToByteArray(keyContent)); - } - - /// - /// Decodes and validates an encoded key. - /// Encoded key format: type:hex_key - /// - /// an encoded key - /// the expected type of the key - /// - public static Key DecodeKey(String encodedKey, String expectedKeyType) - { - Key key = DecodeKey(encodedKey); - - // Check key type - if (!key.type.Equals(expectedKeyType)) - { - throw new InvalidKeyException("Expected key type: " + expectedKeyType + ", got: " + key.type); - } - - return key; - } - - /// - /// Encodes a key. - /// - /// an encoded key - public String Encode() - { - return this.type + Key.separator + DataUtils.ByteArrayToHexString(this.key); - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/MessageId.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/MessageId.cs deleted file mode 100644 index 1c5e10f..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/MessageId.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - class MessageId - { - public const int MESSAGE_ID_LEN = 8; - - private readonly byte[] messageId; - - public MessageId(byte[] messageId) - { - if (messageId.Length != MESSAGE_ID_LEN) - { - throw new ArgumentException("Bad message ID length"); - } - - this.messageId = messageId; - } - - public MessageId(byte[] data, int offset) - { - if ((offset + MESSAGE_ID_LEN) > data.Length) - { - throw new ArgumentException("Bad message ID buffer length"); - } - - this.messageId = new byte[MESSAGE_ID_LEN]; - //System.arraycopy(data, offset, this.messageId, 0, MESSAGE_ID_LEN); - data.Skip(offset).Take(MESSAGE_ID_LEN).ToArray().CopyTo(this.messageId, 0); - } - - public byte[] GetMessageId - { - get { return messageId; } - } - - public override string ToString() { - return DataUtils.ByteArrayToHexString(messageId); - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/DeliveryReceipt.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/DeliveryReceipt.cs deleted file mode 100644 index 450dd67..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/DeliveryReceipt.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Messages -{ - class DeliveryReceipt : ThreemaMessage - { - public const int TYPE_CODE = 0x80; - - public enum Type - { - RECEIVED = 1, - READ = 2, - USER_ACK = 3 - } - - private readonly Type receiptType; - private readonly List ackedMessageIds; - - public DeliveryReceipt(Type receiptType, List ackedMessageIds) { - this.receiptType = receiptType; - this.ackedMessageIds = ackedMessageIds; - } - - public Type ReceiptType - { - get { return receiptType; } - } - - public List AckedMessageIds - { - get { return ackedMessageIds; } - } - - public override int GetTypeCode() { - return TYPE_CODE; - } - - public override byte[] GetData() - { - //Not implemented yet - return new byte[0]; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder("Delivery receipt ("); - sb.Append(receiptType); - sb.Append("): "); - int i = 0; - ackedMessageIds.ForEach(messageId => - { - if (i != 0) - { - sb.Append(", "); - } - sb.Append(messageId); - i++; - }); - return sb.ToString(); - } - - /** - * A delivery receipt type. The following types are defined: - * - *
    - *
  • RECEIVED: the message has been received and decrypted on the recipient's device
  • - *
  • READ: the message has been shown to the user in the chat view - * (note that this status can be disabled)
  • - *
  • USER_ACK: the user has explicitly acknowledged the message (usually by - * long-pressing it and choosing the "acknowledge" option)
  • - *
- */ - /* - public enum Type { - RECEIVED(1), READ(2), USER_ACK(3); - - private final int code; - - Type(int code) { - this.code = code; - } - - public int getCode() { - return code; - } - - public static Type get(int code) { - for (Type t : values()) { - if (t.code == code) - return t; - } - return null; - } - } - */ - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/FileMessage.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/FileMessage.cs deleted file mode 100644 index d2cb93f..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/FileMessage.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; - -namespace IcgSoftware.Threema.CoreMsgApi.Messages -{ - class FileMessage : ThreemaMessage - { - public const int TYPE_CODE = 0x17; - - private const string KEY_BLOB_ID = "b"; - private const string KEY_THUMBNAIL_BLOB_ID = "t"; - private const string KEY_ENCRYPTION_KEY = "k"; - private const string KEY_MIME_TYPE = "m"; - private const string KEY_FILE_NAME = "n"; - private const string KEY_FILE_SIZE = "s"; - private const string KEY_TYPE = "i"; - - private readonly byte[] blobId; - private readonly byte[] encryptionKey; - private readonly string mimeType; - private readonly string fileName; - private readonly int fileSize; - private readonly byte[] thumbnailBlobId; - - public FileMessage(byte[] blobId, byte[] encryptionKey, String mimeType, String fileName, int fileSize, byte[] thumbnailBlobId) - { - this.blobId = blobId; - this.encryptionKey = encryptionKey; - this.mimeType = mimeType; - this.fileName = fileName; - this.fileSize = fileSize; - this.thumbnailBlobId = thumbnailBlobId; - } - - public byte[] BlobId - { - get { return this.blobId; } - } - - public byte[] EncryptionKey - { - get { return this.encryptionKey; } - } - - public string MimeType - { - get { return this.mimeType; } - } - - public string FileName - { - get { return this.fileName; } - } - - public int FileSize - { - get { return this.fileSize; } - } - - public byte[] ThumbnailBlobId - { - get { return this.thumbnailBlobId; } - } - - public override int GetTypeCode() - { - return TYPE_CODE; - } - - public override string ToString() - { - return string.Format("file message {0}", this.fileName); - } - - public override byte[] GetData() - { - JObject jo = new JObject(); - try - { - jo.Add(KEY_BLOB_ID, JToken.FromObject(DataUtils.ByteArrayToHexString(this.blobId))); - if (this.thumbnailBlobId != null) - { - jo.Add(KEY_THUMBNAIL_BLOB_ID, JToken.FromObject(DataUtils.ByteArrayToHexString(this.thumbnailBlobId))); - } - jo.Add(KEY_ENCRYPTION_KEY, JToken.FromObject(DataUtils.ByteArrayToHexString(this.encryptionKey))); - jo.Add(KEY_MIME_TYPE, JToken.FromObject(this.mimeType)); - jo.Add(KEY_FILE_NAME, JToken.FromObject(this.fileName)); - jo.Add(KEY_FILE_SIZE, JToken.FromObject(this.fileSize)); - jo.Add(KEY_TYPE, JToken.FromObject(0)); - } - catch (Exception) - { - throw new BadMessageException(); - } - - return Encoding.UTF8.GetBytes(jo.ToString()); - } - - public static FileMessage FromString(string json) - { - try - { - JObject jo = JObject.Parse(json); - byte[] encryptionKey = DataUtils.HexStringToByteArray(jo[KEY_ENCRYPTION_KEY].Value()); - string mimeType = jo[KEY_MIME_TYPE].Value(); - int fileSize = jo[KEY_FILE_SIZE].Value(); - byte[] blobId = DataUtils.HexStringToByteArray(jo[KEY_BLOB_ID].Value()); - - string fileName; - byte[] thumbnailBlobId = null; - - //optional field - if (jo.Children().Any(e => e.Path.EndsWith(KEY_THUMBNAIL_BLOB_ID))) - { - thumbnailBlobId = DataUtils.HexStringToByteArray(jo[KEY_THUMBNAIL_BLOB_ID].Value()); - } - - if (jo.Children().Any(e => e.Path.EndsWith(KEY_FILE_NAME))) - { - fileName = jo[KEY_FILE_NAME].Value(); - } - else - { - fileName = "unnamed"; - } - - return new FileMessage( - blobId, - encryptionKey, - mimeType, - fileName, - fileSize, - thumbnailBlobId - ); - } - catch - { - throw new BadMessageException(); - } - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ImageMessage.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ImageMessage.cs deleted file mode 100644 index cbfe748..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ImageMessage.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Messages -{ - class ImageMessage : ThreemaMessage - { - public const int TYPE_CODE = 0x02; - - private readonly byte[] blobId; - private readonly int size; - private readonly byte[] nonce; - - public ImageMessage(byte[] blobId, int size, byte[] nonce) - { - this.blobId = blobId; - this.size = size; - this.nonce = nonce; - } - - public byte[] BlobId - { - get { return this.blobId; } - } - - - public int Size - { - get { return this.size; } - } - - - public byte[] Nonce - { - get { return this.nonce; } - } - - public override int GetTypeCode() - { - return TYPE_CODE; - } - - public override byte[] GetData() - { - byte[] data = new byte[BLOB_ID_LEN + 4 + ThreemaMessage.NONCEBYTES]; - int pos = 0; - - //System.arraycopy(this.blobId, 0, data, pos, BLOB_ID_LEN); - this.blobId.CopyTo(data, 0); - pos += BLOB_ID_LEN; - - //EndianUtils.writeSwappedInteger(data, pos, this.size); - byte[] size = BitConverter.GetBytes(this.size); - size.CopyTo(data, pos); - pos += 4; - - //System.arraycopy(this.nonce, 0, data, pos, ThreemaMessage.NONCEBYTES); - this.nonce.CopyTo(data, pos); - - return data; - - } - - public override string ToString() - { - return string.Format("blob {0}", DataUtils.ByteArrayToHexString(this.blobId)); - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/TextMessage.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/TextMessage.cs deleted file mode 100644 index 1178044..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/TextMessage.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Messages -{ - /// - /// A text message that can be sent/received with end-to-end encryption via Threema. - /// - public class TextMessage : ThreemaMessage - { - public const int TYPE_CODE = 0x01; - - private readonly string text; - - public TextMessage(String text) - { - this.text = text; - } - - public string Text { get { return text; } } - - public override int GetTypeCode() { - return TYPE_CODE; - } - - public override byte[] GetData() - { - return Encoding.UTF8.GetBytes(text); - } - - public override string ToString() - { - return text; - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ThreemaMessage.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ThreemaMessage.cs deleted file mode 100644 index 937f40d..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Messages/ThreemaMessage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Messages -{ - /// - /// Abstract base class of messages that can be sent with end-to-end encryption via Threema. - /// - public abstract class ThreemaMessage - { - public const int NONCEBYTES = 24; - public const int BLOB_ID_LEN = 16; - - /// - /// Get message's raw content - /// - /// - public abstract byte[] GetData(); - - /// - /// Get message's type code - /// - /// - public abstract int GetTypeCode(); - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStore.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStore.cs deleted file mode 100644 index 0094bf2..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStore.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - /// - /// Stores and caches public keys for Threema users. Extend this class to provide your - /// own storage implementation, e.g. in a file or database. - /// - public abstract class PublicKeyStore - { - private readonly static object lockCache = new object(); - private readonly Dictionary cache = new Dictionary(); - - /// - /// Get the public key for a given Threema ID. The cache is checked first; if it - /// is not found in the cache, fetchPublicKey() is called. - /// - /// The Threema ID whose public key should be obtained - /// The public key, or null if not found - public byte[] GetPublicKey(string threemaId) - { - lock (lockCache) - { - byte[] pk = null; - - if (this.cache.Keys.Contains(threemaId)) - { - pk = this.cache[threemaId]; - } - else - { - pk = this.FetchPublicKey(threemaId); - if (pk != null) - { - this.cache.Add(threemaId, pk); - } - } - - return pk; - } - } - - /// - /// Store the public key for a given Threema ID in the cache, and the underlying store. - /// - /// The Threema ID whose public key should be stored - /// The corresponding public key - public void SetPublicKey(string threemaId, byte[] publicKey) - { - if(publicKey != null) - { - lock (lockCache) - { - this.cache.Add(threemaId, publicKey); - this.Save(threemaId, publicKey); - } - } - } - - /// - /// Fetch the public key for the given Threema ID from the store. Override to provide - /// your own implementation to read from the store. - /// - /// The Threema ID whose public key should be obtained - /// The public key, or null if not found - abstract protected byte[] FetchPublicKey(string threemaId); - - /// - /// Save the public key for a given Threema ID in the store. Override to provide - /// your own implementation to write to the store. - /// - /// The Threema ID whose public key should be stored - /// The corresponding public key - abstract protected void Save(string threemaId, byte[] publicKey); - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreDb.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreDb.cs deleted file mode 100644 index 0876692..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreDb.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Data.Common; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Configuration; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - class PublicKeyStoreDb : PublicKeyStore - { - private readonly string connectionString; - - public PublicKeyStoreDb(string connectionString) - { - if (string.IsNullOrEmpty(connectionString)) - { - throw new ArgumentException("connectionString must be not null or empty."); - } - - this.connectionString = connectionString; - - this.CreateDatabase(); - } - - /// - /// Fetch public key in store for particular threema id - /// - /// Threema id to fetch - /// Public key - protected override byte[] FetchPublicKey(string threemaId) - { - using (DbConnection connection = GetConnection(this.connectionString)) - { - connection.Open(); - - string sql = "SELECT threema_id, key FROM public_key WHERE threema_id = @threemaId"; - - var command = connection.CreateCommand(); - command.CommandType = CommandType.Text; - command.CommandText = sql; - - var paramThreemaId = command.CreateParameter(); - paramThreemaId.ParameterName = "threemaId"; - paramThreemaId.Value = threemaId; - command.Parameters.Add(paramThreemaId); - - byte[] publicKey = null; - - using (var reader = command.ExecuteReader(CommandBehavior.SingleRow)) - { - if (reader.HasRows) - { - if (reader.Read()) - { - publicKey = DataUtils.HexStringToByteArray(reader[1].ToString()); - } - } - reader.Close(); - } - - return publicKey; - } - } - - /// - /// Save threema id and public key into store - /// - /// Threema id - /// public key - protected override void Save(string threemaId, byte[] publicKey) - { - using (DbConnection connection = GetConnection(this.connectionString)) - { - connection.Open(); - - string sql = "INSERT INTO public_key (threema_id, key) VALUES (@threemaId, @key)"; - - var command = connection.CreateCommand(); - command.CommandType = CommandType.Text; - command.CommandText = sql; - - var paramThreemaId = command.CreateParameter(); - paramThreemaId.ParameterName = "threemaId"; - paramThreemaId.Value = threemaId; - - var paramKey = command.CreateParameter(); - paramKey.ParameterName = "key"; - paramKey.Value = DataUtils.ByteArrayToHexString(publicKey); - - command.Parameters.Add(paramThreemaId); - command.Parameters.Add(paramKey); - command.ExecuteNonQuery(); - - connection.Close(); - } - } - - /// - /// Create database and table for public key store - /// - private void CreateDatabase() - { - using (DbConnection connection = GetConnection(this.connectionString)) - { - connection.Open(); - - string sql = "CREATE TABLE IF NOT EXISTS public_key (id INTEGER PRIMARY KEY AUTOINCREMENT, threema_id VARCHAR(8) NOT NULL UNIQUE, key VARCHAR(64) NOT NULL)"; - - var command = connection.CreateCommand(); - command.CommandText = sql; - command.ExecuteNonQuery(); - - connection.Close(); - } - } - - /// - /// Get connetion is configured in App.config - /// - /// Connect String - /// Db Connection - private DbConnection GetConnection(string connectionString) - { - //throw new CoreMigrationException("SQLite is not supported"); - return new SqliteConnection(connectionString); - - //string providerName = "System.Data.SQLite"; - //var dbProvider = Microsoft.Data.Sqlite.SqliteFactory.Instance; -/* - string providerName = null; - - DbConnectionStringBuilder connectionStringBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; - - if (connectionStringBuilder.ContainsKey("provider")) - { - providerName = connectionStringBuilder["provider"].ToString(); - } - else - { - ConnectionStringSettings connectionStringSetting = ConfigurationManager - .ConnectionStrings - .Cast() - .FirstOrDefault(x => x.ConnectionString == connectionString); - if (connectionStringSetting != null) - { - providerName = connectionStringSetting.ProviderName; - } - } - - if (providerName != null) - { - bool providerExists = DbProviderFactories - .GetFactoryClasses() - .Rows.Cast() - .Any(r => r[2].Equals(providerName)); - if (providerExists) - { - DbProviderFactory factory = DbProviderFactories.GetFactory(providerName); - DbConnection dbConnection = factory.CreateConnection(); - - dbConnection.ConnectionString = connectionString; - return dbConnection; - } - } - - return null; -*/ - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreNone.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreNone.cs deleted file mode 100644 index 569929f..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/PublicKeyStoreNone.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi -{ - public class PublicKeyStoreNone : PublicKeyStore - { - protected override byte[] FetchPublicKey(string threemaId) - { - return null; - } - - protected override void Save(string threemaId, byte[] publicKey) - { - //do nothing - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/CapabilityResult.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/CapabilityResult.cs deleted file mode 100644 index e6b1160..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/CapabilityResult.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Results -{ - /// - /// Result of a capability lookup - /// - public class CapabilityResult - { - private readonly string key; - private readonly string[] capabilities; - - public CapabilityResult(string key, string[] capabilities) - { - this.key = key; - this.capabilities = capabilities; - } - - public string Key - { - get { return key; } - } - - /// - /// Get all capabilities as a string array. - /// - public string[] Capabilities - { - get { return capabilities; } - } - - /// - /// Check whether the Threema ID can receive text - /// - public bool CanText - { - get { return this.Can("text"); } - } - - /// - /// Check whether the Threema ID can receive images - /// - public bool CanImage - { - get { return this.Can("image"); } - } - - /// - /// Check whether the Threema ID can receive videos - /// - public bool CanVideo - { - get { return this.Can("video"); } - } - - /// - /// Check whether the Threema ID can receive audio - /// - public bool CanAudio - { - get { return this.Can("audio"); } - } - - /// - /// Check whether the Threema ID can receive files - /// - public bool CanFile - { - get { return this.Can("file"); } - } - - public override string ToString() - { - StringBuilder b = new StringBuilder(); - b.Append(this.key).Append(": "); - for (int n = 0; n < this.capabilities.Length; n++) { - if (n > 0) - { - b.Append(","); - } - b.Append(this.capabilities[n]); - } - return b.ToString(); - } - - private bool Can(string key) - { - return this.capabilities.Any(k => k.Equals(key)); - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/EncryptResult.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/EncryptResult.cs deleted file mode 100644 index 8e71f04..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/EncryptResult.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Results -{ - public class EncryptResult - { - private readonly byte[] result; - private readonly byte[] secret; - private readonly byte[] nonce; - - public EncryptResult(byte[] result, byte[] secret, byte[] nonce) - { - this.result = result; - this.secret = secret; - this.nonce = nonce; - } - - /// - /// the encrypted data - /// - public byte[] Result - { - get { return this.result; } - } - - /// - /// the size (in bytes) of the encrypted data - /// - public int Size - { - get { return this.result.Length; } - } - - /// - /// the nonce that was used for encryption - /// - public byte[] Nonce - { - get { return this.nonce; } - } - - /// - /// the secret that was used for encryption (only for symmetric encryption, e.g. files) - /// - public byte[] Secret - { - get { return secret; } - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/ReceiveMessageResult.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/ReceiveMessageResult.cs deleted file mode 100644 index bb2216a..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/ReceiveMessageResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using IcgSoftware.Threema.CoreMsgApi.Messages; - -namespace IcgSoftware.Threema.CoreMsgApi.Results -{ - public class ReceiveMessageResult - { - private readonly string messageId; - private readonly ThreemaMessage message; - protected List files = new List(); - protected List errors = new List(); - - public ReceiveMessageResult(string messageId, ThreemaMessage message) - { - this.messageId = messageId; - this.message = message; - } - - public List Files - { - get { return this.files; } - } - - public List Errors - { - get { return this.errors; } - } - - public ThreemaMessage Message - { - get { return this.message; } - } - - public string MessageId - { - get { return messageId; } - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/UploadResult.cs b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/UploadResult.cs deleted file mode 100644 index 55a51f0..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.CoreMsgApi/Results/UploadResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IcgSoftware.Threema.CoreMsgApi.Results -{ - public class UploadResult - { - private readonly int responseCode; - private readonly byte[] blobId; - - public UploadResult(int responseCode, byte[] blobId) - { - this.responseCode = responseCode; - this.blobId = blobId; - } - - /// - /// the blob ID that has been created - /// - public byte[] BlobId - { - get { return this.blobId; } - } - - /// - /// whether the upload succeeded - /// - public bool IsSuccess - { - get { return this.responseCode == 200; } - } - - /// - /// the response code of the upload - /// - public int ResponseCode - { - get { return this.responseCode; } - } - } -} diff --git a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.sln b/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.sln deleted file mode 100644 index f1c11e4..0000000 --- a/libs/Threema-MsgApi-Net-Core/IcgSoftware.Threema.sln +++ /dev/null @@ -1,79 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2043 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi", "IcgSoftware.Threema.CoreMsgApi\IcgSoftware.Threema.CoreMsgApi.csproj", "{B5799291-7E0A-4B42-AAC8-378535E1E579}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi.Test", "IcgSoftware.Threema.CoreMsgApi.Test\IcgSoftware.Threema.CoreMsgApi.Test.csproj", "{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi.Console", "IcgSoftware.Threema.CoreMsgApi.Console\IcgSoftware.Threema.CoreMsgApi.Console.csproj", "{909129FB-810C-4B5D-AD92-774525BD3D77}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreWebApp", "IcgSoftware.Threema.CoreWebApp\IcgSoftware.Threema.CoreWebApp.csproj", "{68345BDF-E4BA-4F41-8424-F02F613529FB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x64.ActiveCfg = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x64.Build.0 = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x86.ActiveCfg = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x86.Build.0 = Debug|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|Any CPU.Build.0 = Release|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x64.ActiveCfg = Release|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x64.Build.0 = Release|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x86.ActiveCfg = Release|Any CPU - {B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x86.Build.0 = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x64.ActiveCfg = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x64.Build.0 = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x86.ActiveCfg = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x86.Build.0 = Debug|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|Any CPU.Build.0 = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x64.ActiveCfg = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x64.Build.0 = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x86.ActiveCfg = Release|Any CPU - {05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x86.Build.0 = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|Any CPU.Build.0 = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x64.ActiveCfg = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x64.Build.0 = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x86.ActiveCfg = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x86.Build.0 = Debug|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|Any CPU.ActiveCfg = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|Any CPU.Build.0 = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x64.ActiveCfg = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x64.Build.0 = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x86.ActiveCfg = Release|Any CPU - {909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x86.Build.0 = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x64.ActiveCfg = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x64.Build.0 = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x86.ActiveCfg = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x86.Build.0 = Debug|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|Any CPU.Build.0 = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x64.ActiveCfg = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x64.Build.0 = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x86.ActiveCfg = Release|Any CPU - {68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DDF51293-E36E-4B16-A826-B74150F6F825} - EndGlobalSection -EndGlobal diff --git a/libs/Threema-MsgApi-Net-Core/LICENSE b/libs/Threema-MsgApi-Net-Core/LICENSE deleted file mode 100644 index 9c4f47e..0000000 --- a/libs/Threema-MsgApi-Net-Core/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 pt-icg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/libs/Threema-MsgApi-Net-Core/README.md b/libs/Threema-MsgApi-Net-Core/README.md deleted file mode 100644 index 008e12d..0000000 --- a/libs/Threema-MsgApi-Net-Core/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Threema MsgApi .NET Core - -This project is a .NET Core fork of the [Threema Message API SDK-NET](https://gateway.threema.ch/en/developer/sdk-net). - -The Threema Message API is an interface that can be used from within customer-specific software to send and receive messages via [Threema Gateway](https://gateway.threema.ch/en). - - - -### Configuration for Testing - - -Configure in application.json: - -- Threema - - PrivateKey - - ThreemaId - - Secret -- Remove entry "SQLiteConnectionString" or set entry for using public key store. - diff --git a/libs/Threema-MsgApi-Net-Core/VisualStudio.gitignore b/libs/Threema-MsgApi-Net-Core/VisualStudio.gitignore deleted file mode 100644 index 94b41b9..0000000 --- a/libs/Threema-MsgApi-Net-Core/VisualStudio.gitignore +++ /dev/null @@ -1,332 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ diff --git a/src/PolyTrader.Core/Models/ServerSettings.cs b/src/PolyTrader.Core/Models/ServerSettings.cs index e0ce3a9..e0076c7 100644 --- a/src/PolyTrader.Core/Models/ServerSettings.cs +++ b/src/PolyTrader.Core/Models/ServerSettings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Xml.Serialization; @@ -16,42 +16,6 @@ namespace PolyTraderSharp.Models [XmlArrayItem("Module")] public List DisabledModules { get; set; } = new(); - [Category("Threema Notifications")] - [DisplayName("Threema Enabled")] - [Description("Enable or disable Threema notifications.")] - public bool ThreemaEnabled { get; set; } = true; - - [Category("Threema Notifications")] - [DisplayName("Gateway ID")] - [Description("The Threema Gateway ID (e.g. *3MAGW01).")] - public string ThreemaGatewayId { get; set; } = "*3MAGW01"; - - [Category("Threema Notifications")] - [DisplayName("Gateway Secret")] - [Description("The secret for the Threema Gateway integration.")] - public string ThreemaSecret { get; set; } = ""; - - [Category("Threema Notifications")] - [DisplayName("Private Key")] - [Description("The Private Key (hex) for End-to-End encryption.")] - public string ThreemaPrivateKey { get; set; } = ""; - - [Category("Threema Notifications")] - [DisplayName("Group ID")] - [Description("The Threema Group ID to send messages to.")] - public string ThreemaGroupId { get; set; } = ""; - - [Category("Threema Notifications")] - [DisplayName("Webhook Port")] - [Description("The local port to listen on for incoming Threema messages (e.g. 8080).")] - public int ThreemaWebhookPort { get; set; } = 8080; - - [Category("Threema Notifications")] - [DisplayName("Report Interval (Hours)")] - [Description("Interval for the automatic summary report.")] - public int ThreemaReportIntervalHours { get; set; } = 6; - - [Category("Mullvad VPN")] [DisplayName("VPN Enabled")] [Description("Enable or disable automatic VPN rotation.")] diff --git a/src/PolyTrader.Core/Notifications/INotificationSink.cs b/src/PolyTrader.Core/Notifications/INotificationSink.cs new file mode 100644 index 0000000..2fc15fa --- /dev/null +++ b/src/PolyTrader.Core/Notifications/INotificationSink.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PolyTrader.Core.Notifications +{ + /// Einstufung einer Benachrichtigung – steuert später die Kanalwahl und das Alarmverhalten. + public enum NotificationSeverity + { + /// Reine Information (z.B. Tagesbericht). + Info, + + /// Auffälligkeit, die Beachtung verdient (z.B. Auto-Pause eines Master-Traders). + Warning, + + /// Kritisch – erfordert Eingreifen (z.B. Verlust-Eskalation der Sell-Ladder). + Critical + } + + /// + /// Neutraler Ausgang für Benachrichtigungen an den Betreiber. Ersetzt die frühere direkte + /// Abhängigkeit der Dienste auf den (verworfenen) Threema-Versand: die Fachlogik kennt nur noch + /// diese Schnittstelle, der tatsächliche Kanal wird per DI gesetzt. Geplante Implementierungen + /// sind RocketChat und Telegram; bis dahin schreibt ins Terminal. + /// + /// Vertrag – für JEDE Implementierung bindend: wirft NIEMALS. + /// Netzwerk- oder Kanalfehler werden intern abgefangen und protokolliert. Ein ausgefallener + /// Benachrichtigungskanal darf den Handelsablauf unter keinen Umständen unterbrechen – die + /// Aufrufer verzichten deshalb bewusst auf eigene try/catch-Blöcke. + /// + public interface INotificationSink + { + /// + /// Sendet eine Benachrichtigung. Wirft nie (siehe Vertrag in der Schnittstellen-Doku). + /// + Task SendAsync(string message, NotificationSeverity severity = NotificationSeverity.Info, + CancellationToken ct = default); + } +} diff --git a/src/PolyTrader.Core/Notifications/LogNotificationSink.cs b/src/PolyTrader.Core/Notifications/LogNotificationSink.cs new file mode 100644 index 0000000..e066d48 --- /dev/null +++ b/src/PolyTrader.Core/Notifications/LogNotificationSink.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using PolyTraderSharp.Services; + +namespace PolyTrader.Core.Notifications +{ + /// + /// Übergangs-Implementierung von : schreibt Benachrichtigungen ins + /// Terminal-Log (und damit in die Tages-JSONL-Datei), bis die echten Kanäle (RocketChat, Telegram) + /// angebunden sind. Damit gehen Auto-Pause- und Eskalationsmeldungen nach dem Threema-Ausbau nicht + /// verloren – sie sind im Terminal-Fenster und in den Logs auffindbar. + /// + /// Erfüllt den No-Throw-Vertrag der Schnittstelle. + /// + public sealed class LogNotificationSink : INotificationSink + { + private readonly TerminalLogger _logger; + + public LogNotificationSink(TerminalLogger logger) => _logger = logger; + + public Task SendAsync(string message, NotificationSeverity severity = NotificationSeverity.Info, + CancellationToken ct = default) + { + try + { + string text = $"🔔 [Benachrichtigung] {message}"; + switch (severity) + { + case NotificationSeverity.Critical: + case NotificationSeverity.Warning: + _logger.Warning(text); + break; + default: + _logger.Info(text); + break; + } + } + catch (Exception ex) + { + // No-Throw-Vertrag: ein defekter Benachrichtigungsweg darf den Handel nie unterbrechen. + try { Console.WriteLine($"[NotificationSink] Zustellung fehlgeschlagen (ignoriert): {ex.Message}"); } + catch { /* selbst Console kann in Sonderfällen fehlschlagen */ } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/PolyTrader.Core/PolyTrader.Core.csproj b/src/PolyTrader.Core/PolyTrader.Core.csproj index 5ddecc1..0bb23f2 100644 --- a/src/PolyTrader.Core/PolyTrader.Core.csproj +++ b/src/PolyTrader.Core/PolyTrader.Core.csproj @@ -21,10 +21,6 @@ - - - - diff --git a/src/PolyTrader.Core/Services/PolymarketClobClient.cs b/src/PolyTrader.Core/Services/PolymarketClobClient.cs index 2503e66..fb829ea 100644 --- a/src/PolyTrader.Core/Services/PolymarketClobClient.cs +++ b/src/PolyTrader.Core/Services/PolymarketClobClient.cs @@ -767,7 +767,10 @@ namespace PolyTraderSharp.Services var decMatch = System.Text.RegularExpressions.Regex.Match(responseContent, @"maker amount supports a max accuracy of (\d+) decimals, taker amount a max of (\d+) decimals"); if (decMatch.Success && overrideMakerDecimals == null) { - if (int.TryParse(decMatch.Groups[1].Value, out int newMaker) && int.TryParse(decMatch.Groups[2].Value, out int newTaker)) + if (int.TryParse(decMatch.Groups[1].Value, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out int newMaker) + && int.TryParse(decMatch.Groups[2].Value, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out int newTaker)) { _logger.Info($"🔄 Automatische Anpassung an Dezimalregeln (Maker: {newMaker}, Taker: {newTaker}). Order wird neu berechnet..."); return await PlaceOrderAsync(account, tokenId, sideStr, investAmountUsd, limitPrice, orderType, debugPayloadLog, isNegRisk, actualFeeBps, overrideTickSize, newMaker, newTaker); @@ -778,7 +781,8 @@ namespace PolyTraderSharp.Services var match = System.Text.RegularExpressions.Regex.Match(responseContent, @"invalid fee rate \(\d+\), current market's (?:taker|maker) fee: (\d+)"); if (match.Success && actualFeeBps == 0) // Only retry once { - if (int.TryParse(match.Groups[1].Value, out int newFeeBps)) + if (int.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out int newFeeBps)) { _logger.Info($"🔄 Automatische Anpassung an Fee Rate ({newFeeBps} bps). Order wird erneut platziert..."); return await PlaceOrderAsync(account, tokenId, sideStr, investAmountUsd, limitPrice, orderType, debugPayloadLog, isNegRisk, newFeeBps, overrideTickSize); @@ -789,7 +793,8 @@ namespace PolyTraderSharp.Services var sizeMatch = System.Text.RegularExpressions.Regex.Match(responseContent, @"Size \([\d\.]+\) lower than the minimum: (\d+)"); if (sizeMatch.Success) { - if (decimal.TryParse(sizeMatch.Groups[1].Value, out decimal minReq)) + if (decimal.TryParse(sizeMatch.Groups[1].Value, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out decimal minReq)) { if (orderType != "MARKET") { @@ -812,12 +817,15 @@ namespace PolyTraderSharp.Services decimal totalBal = 0m, activeOrders = 0m; if (balMatch1.Success) { - _ = decimal.TryParse(balMatch1.Groups[1].Value, out totalBal); - _ = decimal.TryParse(balMatch1.Groups[2].Value, out activeOrders); + _ = decimal.TryParse(balMatch1.Groups[1].Value, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out totalBal); + _ = decimal.TryParse(balMatch1.Groups[2].Value, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out activeOrders); } else if (balMatch2.Success) { - _ = decimal.TryParse(balMatch2.Groups[1].Value, out totalBal); + _ = decimal.TryParse(balMatch2.Groups[1].Value, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out totalBal); activeOrders = 0m; } diff --git a/src/PolyTrader.Core/Services/ThreemaService.cs b/src/PolyTrader.Core/Services/ThreemaService.cs deleted file mode 100644 index 15af02e..0000000 --- a/src/PolyTrader.Core/Services/ThreemaService.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using PolyTraderSharp.Models; -using IcgSoftware.Threema.CoreMsgApi; -using IcgSoftware.Threema.CoreMsgApi.Exceptions; - -namespace PolyTraderSharp.Services -{ - public class ThreemaService : BackgroundService - { - public event Action? OnCommandReceived; - - private readonly TerminalLogger _logger; - private ServerSettings _settings; - private readonly string _settingsPath = "server_settings.xml"; - - private readonly JobStatusRow _jobStatus; - private HttpListener? _httpListener; - private APIConnector? _apiConnector; - - public ThreemaService(TerminalLogger logger, JobManager jobManager) - { - _logger = logger; - _settings = ServerSettings.Load(_settingsPath); - - _jobStatus = new JobStatusRow - { - JobName = "Threema Webhook Listener", - Description = "Listens for incoming Threema Gateway Webhooks on the configured port.", - StatusText = "Pending Initial Delay..." - }; - - _jobStatus.ManualTriggerAction = async () => - { - _jobStatus.StatusText = "Manual trigger not supported for Webhook"; - await Task.Delay(2000); - }; - - jobManager.RegisterJob(_jobStatus); - InitConnector(); - } - - public void ReloadSettings() - { - _settings = ServerSettings.Load(_settingsPath); - InitConnector(); - } - - private void InitConnector() - { - if (_settings.ThreemaEnabled && !string.IsNullOrEmpty(_settings.ThreemaGatewayId) && !string.IsNullOrEmpty(_settings.ThreemaSecret)) - { - // Initialize the pt-icg SDK APIConnector - _apiConnector = new APIConnector(_settings.ThreemaGatewayId, _settings.ThreemaSecret, new PublicKeyStoreNone()); - } - } - - public async Task SendMessageAsync(string text, string parseMode = "") - { - if (!_settings.ThreemaEnabled || _apiConnector == null) - { - return false; - } - - // Using the GroupID field as the target (could be a Threema ID) - string targetId = _settings.ThreemaGroupId; - if (string.IsNullOrEmpty(targetId)) - { - _logger.Error("Threema send failed: Target ID (Group ID) is not configured."); - return false; - } - - try - { - return await Task.Run(() => - { - // If no private key is configured, fallback to Basic mode (SendTextMessageSimple) - // Note: Basic mode does not support actual Group messaging, so targetId must be a personal Threema ID. - // If a private key IS configured, we would use E2E mode, but without the official 2.0 SDK's - // SendGroupTextMessage, we just send a direct E2E message. - - if (string.IsNullOrEmpty(_settings.ThreemaPrivateKey)) - { - string msgId = _apiConnector.SendTextMessageSimple(targetId, text); - if (!string.IsNullOrEmpty(msgId)) - { - _logger.Info($"Threema basic message sent. ID: {msgId}"); - return true; - } - } - else - { - // E2E Mode Direct Message - byte[] privateKey = DataUtils.HexStringToByteArray(_settings.ThreemaPrivateKey); - byte[] publicKey = _apiConnector.LookupKey(targetId); - - if (publicKey == null) - { - _logger.Error($"Threema E2E failed: Could not lookup public key for {targetId}"); - return false; - } - - byte[] nonce = CryptTool.RandomNonce(); - var encryptResult = CryptTool.EncryptTextMessage(text, privateKey, publicKey); - - if (encryptResult != null && encryptResult.Result != null) - { - byte[] box = encryptResult.Result; - string msgId = _apiConnector.SendE2EMessage(targetId, encryptResult.Nonce, box); - if (!string.IsNullOrEmpty(msgId)) - { - _logger.Info($"Threema E2E message sent. ID: {msgId}"); - return true; - } - } - } - return false; - }); - } - catch (Exception ex) - { - _logger.Error($"Threema send failed: {ex.Message}"); - return false; - } - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _jobStatus.StatusText = "Idle"; - - while (!stoppingToken.IsCancellationRequested) - { - if (!_settings.ThreemaEnabled || !_jobStatus.IsEnabled) - { - _jobStatus.StatusText = "Paused / Disabled"; - if (_httpListener != null && _httpListener.IsListening) - { - _httpListener.Stop(); - } - await Task.Delay(5000, stoppingToken); - continue; - } - - try - { - if (_httpListener == null || !_httpListener.IsListening) - { - _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://*:{_settings.ThreemaWebhookPort}/"); - _httpListener.Start(); - _jobStatus.StatusText = $"Listening on port {_settings.ThreemaWebhookPort}..."; - _logger.Info($"[Threema] Webhook listener started on port {_settings.ThreemaWebhookPort}"); - } - - _jobStatus.LastRun = DateTime.Now; - - var getContextTask = _httpListener.GetContextAsync(); - var delayTask = Task.Delay(5000, stoppingToken); - - var completedTask = await Task.WhenAny(getContextTask, delayTask); - - if (completedTask == getContextTask) - { - var context = await getContextTask; - _ = Task.Run(() => HandleIncomingWebhook(context), stoppingToken); - } - } - catch (TaskCanceledException) { } - catch (Exception ex) - { - if (ex is HttpListenerException hle && hle.ErrorCode == 5) - { - _logger.Error($"[Threema] Access Denied starting Webhook. Try running as Administrator or run: netsh http add urlacl url=http://*:{_settings.ThreemaWebhookPort}/ user=Everyone"); - } - else - { - _logger.Error($"[Threema] Listener error: {ex.Message}"); - } - - _jobStatus.StatusText = "Error! Retrying in 5s..."; - if (_httpListener != null) - { - try { _httpListener.Close(); } catch { } - _httpListener = null; - } - await Task.Delay(5000, stoppingToken); - } - - _jobStatus.NextRun = DateTime.Now; - } - - if (_httpListener != null) - { - try { _httpListener.Close(); } catch { } - } - } - - private void HandleIncomingWebhook(HttpListenerContext context) - { - try - { - var request = context.Request; - var response = context.Response; - - if (request.HttpMethod == "POST") - { - using (var reader = new StreamReader(request.InputStream, request.ContentEncoding)) - { - string body = reader.ReadToEnd(); - var parsedParams = System.Web.HttpUtility.ParseQueryString(body); - - string? from = parsedParams["from"]; - string? to = parsedParams["to"]; - string? nonceStr = parsedParams["nonce"]; - string? boxStr = parsedParams["box"]; - string? macStr = parsedParams["mac"]; - - // Decrypt the message if E2E - if (!string.IsNullOrEmpty(from) && !string.IsNullOrEmpty(nonceStr) && !string.IsNullOrEmpty(boxStr) && !string.IsNullOrEmpty(_settings.ThreemaPrivateKey) && _apiConnector != null) - { - try - { - byte[] privateKey = DataUtils.HexStringToByteArray(_settings.ThreemaPrivateKey); - byte[] publicKey = _apiConnector.LookupKey(from); - byte[] nonce = DataUtils.HexStringToByteArray(nonceStr); - byte[] box = DataUtils.HexStringToByteArray(boxStr); - - if (publicKey != null) - { - var msg = CryptTool.DecryptMessage(box, privateKey, publicKey, nonce); - if (msg is IcgSoftware.Threema.CoreMsgApi.Messages.TextMessage textMsg) - { - string text = textMsg.Text; - string logText = text.Length > 200 ? text.Substring(0, 200) + "..." : text; - _logger.Info($"[Threema] Empfangen von {from}: {logText}"); - - if (text.StartsWith("/")) - { - OnCommandReceived?.Invoke(text); - } - } - } - } - catch (Exception dex) - { - _logger.Warning($"[Threema] Failed to decrypt incoming message: {dex.Message}"); - } - } - else - { - _logger.Info($"[Threema] Received webhook, but cannot process (Basic mode doesn't support incoming, or missing E2E keys)."); - } - } - } - - response.StatusCode = 200; - response.Close(); - } - catch (Exception ex) - { - _logger.Error($"[Threema] Webhook handling error: {ex.Message}"); - } - } - } -} diff --git a/src/PolyTrader.Modules.CopyTrading/Services/MasterTraderAnalyticsJob.cs b/src/PolyTrader.Modules.CopyTrading/Services/MasterTraderAnalyticsJob.cs index 28e7de4..309c547 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/MasterTraderAnalyticsJob.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/MasterTraderAnalyticsJob.cs @@ -1,4 +1,5 @@ using System; +using PolyTrader.Core.Notifications; using PolyTrader.Modules.CopyTrading.Logic; using PolyTrader.Modules.CopyTrading.Persistence; using System.Collections.Generic; @@ -19,11 +20,11 @@ namespace PolyTraderSharp.Services private readonly IMasterTraderHistoryRepository _historyRepo; private readonly ITrackedTraderRepository _traderRepo; private readonly ICopyTradeLogRepository _tradeLog; - private readonly ThreemaService _threema; + private readonly INotificationSink _notify; private readonly JobStatusRow _jobStatus; private readonly PolymarketApiService _api; - public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMasterTraderHistoryRepository historyRepo, ITrackedTraderRepository traderRepo, ICopyTradeLogRepository tradeLog, ThreemaService threema, JobManager jobManager, PolymarketApiService api) + public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMasterTraderHistoryRepository historyRepo, ITrackedTraderRepository traderRepo, ICopyTradeLogRepository tradeLog, INotificationSink notify, JobManager jobManager, PolymarketApiService api) { _state = state; _copyState = copyState; @@ -31,7 +32,7 @@ namespace PolyTraderSharp.Services _historyRepo = historyRepo; _traderRepo = traderRepo; _tradeLog = tradeLog; - _threema = threema; + _notify = notify; _api = api; _jobStatus = new JobStatusRow @@ -137,7 +138,9 @@ namespace PolyTraderSharp.Services if (tsRaw > 1000000000000) closedTs = DateTimeOffset.FromUnixTimeMilliseconds(tsRaw).UtcDateTime; else closedTs = DateTimeOffset.FromUnixTimeSeconds(tsRaw).UtcDateTime; } - else if (tsProp.ValueKind == JsonValueKind.String && long.TryParse(tsProp.GetString(), out long tsStrRaw)) + else if (tsProp.ValueKind == JsonValueKind.String + && long.TryParse(tsProp.GetString(), System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out long tsStrRaw)) { if (tsStrRaw > 1000000000000) closedTs = DateTimeOffset.FromUnixTimeMilliseconds(tsStrRaw).UtcDateTime; else closedTs = DateTimeOffset.FromUnixTimeSeconds(tsStrRaw).UtcDateTime; @@ -258,8 +261,12 @@ namespace PolyTraderSharp.Services trader.IsActive = false; trader.Reasoning = $"[Auto-Pause {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC] Copy-PnL {liveMetrics.CopyPnl:F2} USDC über {liveMetrics.TradeCount} Live-Trades (30T) unter Schwelle (-{_copyState.AutoPauseDrawdownUsd:F0}). Reaktivierung manuell."; _logger.Warning($"🛑 [AUTO-PAUSE] Master '{trader.DisplayName}' deaktiviert. Live-Copy-PnL {liveMetrics.CopyPnl:F2} / {liveMetrics.TradeCount} Trades. Reaktivierung nur manuell."); - try { await _threema.SendMessageAsync($"🛑 Auto-Pause: Master '{trader.DisplayName}' deaktiviert.\nLive-Copy-PnL 30T: {liveMetrics.CopyPnl:F2} USDC über {liveMetrics.TradeCount} Trades.\nReaktivierung manuell."); } - catch (Exception ex) { _logger.Error($"Threema Auto-Pause-Benachrichtigung fehlgeschlagen: {ex.Message}"); } + // Kein try/catch nötig: INotificationSink wirft laut Vertrag nie. + await _notify.SendAsync( + $"🛑 Auto-Pause: Master '{trader.DisplayName}' deaktiviert.\n" + + $"Live-Copy-PnL 30T: {liveMetrics.CopyPnl:F2} USDC über {liveMetrics.TradeCount} Trades.\n" + + "Reaktivierung manuell.", + NotificationSeverity.Warning); } _copyState.Traders[trader.Id] = trader; // Hot-Path-State synchron halten diff --git a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs index 2d78898..0f6c785 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Linq; using System.Threading; using PolyTrader.Core.Trading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; +using PolyTrader.Core.Notifications; using PolyTrader.Core.Persistence; using PolyTrader.Modules.CopyTrading.Logic; using PolyTraderSharp.Models; @@ -16,7 +17,7 @@ namespace PolyTraderSharp.Services /// Master-Exit und markiert die Position . Dieser Service /// senkt das Limit stufenweise (relative Schrittweite) bis zum Floor, wenn kein Fill kommt: /// canceln → eine Stufe tiefer neu platzieren. Am Floor ohne Fill: Position halten + - /// Threema-Benachrichtigung. Fills erkennt der bestehende Sync (TraderMonitorService), der die + /// Benachrichtigung über . Fills erkennt der bestehende Sync (TraderMonitorService), der die /// Position entfernt → die Leiter endet dann von selbst. /// /// Bewusst separat vom CLOB-Order-Code gehalten; die Preislogik ist in @@ -30,7 +31,7 @@ namespace PolyTraderSharp.Services private readonly TradingState _state; private readonly IClobClient _clob; private readonly TerminalLogger _logger; - private readonly ThreemaService _threema; + private readonly INotificationSink _notify; private readonly IPositionRepository _positionRepo; private readonly IOrderEventLog _orderEvents; @@ -39,7 +40,7 @@ namespace PolyTraderSharp.Services TradingState state, IClobClient clob, TerminalLogger logger, - ThreemaService threema, + INotificationSink notify, IPositionRepository positionRepo, IOrderEventLog orderEvents) { @@ -47,7 +48,7 @@ namespace PolyTraderSharp.Services _state = state; _clob = clob; _logger = logger; - _threema = threema; + _notify = notify; _positionRepo = positionRepo; _orderEvents = orderEvents; } @@ -269,7 +270,8 @@ namespace PolyTraderSharp.Services $"Konto: {account.Name}\nMarkt: {ladder.MarketQuestion}\n" + $"Floor: {ladder.Floor:F3} (Master-Exit war {ladder.ReferencePrice:F3})"; _logger.Warning($"🛑 [SELL-LEITER Floor] {account.Name} | {ladder.MarketQuestion} | Floor {ladder.Floor:F3} ohne Fill – halte Position."); - try { await _threema.SendMessageAsync(msg); } catch (Exception ex) { _logger.Error($"Threema-Benachrichtigung fehlgeschlagen: {ex.Message}"); } + // Kein try/catch nötig: INotificationSink wirft laut Vertrag nie. + await _notify.SendAsync(msg, NotificationSeverity.Critical); } ladder.LastActionAt = DateTime.UtcNow; // Re-Notify-Spam vermeiden return; diff --git a/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs b/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs index a3f324e..736cca6 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs @@ -1220,7 +1220,13 @@ namespace PolyTraderSharp.Services } } - private decimal ParseDecimal(JsonElement prop) + /// + /// Liest einen Zahlenwert, der von der API je nach Endpunkt als JSON-Zahl ODER als String + /// ("0.53") kommt. Das String-Parsing ist bewusst invariant: unter de-DE gilt "." als + /// Tausendertrennzeichen, "0.53" würde sonst als 53 gelesen (Faktor-100-Fehler im Preis). + /// internal static, damit die Kultur-Unabhängigkeit testbar bleibt. + /// + internal static decimal ParseDecimal(JsonElement prop) { if (prop.ValueKind == JsonValueKind.Number) return prop.GetDecimal(); if (prop.ValueKind == JsonValueKind.String && decimal.TryParse(prop.GetString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var parsed)) return parsed; @@ -1273,19 +1279,11 @@ namespace PolyTraderSharp.Services foreach (var act in elements) { - decimal price = 0m; - if (act.TryGetProperty("price", out var priceProp)) - { - if (priceProp.ValueKind == JsonValueKind.Number) price = priceProp.GetDecimal(); - else if (priceProp.ValueKind == JsonValueKind.String) decimal.TryParse(priceProp.GetString(), out price); - } - - decimal size = 0m; - if (act.TryGetProperty("size", out var sizeProp)) - { - if (sizeProp.ValueKind == JsonValueKind.Number) size = sizeProp.GetDecimal(); - else if (sizeProp.ValueKind == JsonValueKind.String) decimal.TryParse(sizeProp.GetString(), out size); - } + // Über ParseDecimal (invariant) statt inline: die API liefert Preis/Size je nach + // Endpunkt als Zahl ODER als String ("0.53"). Kulturabhängiges Parsen läse "." + // unter de-DE als Tausendertrennzeichen – aus 0,53 würde 53 (Faktor-100-Fehler). + decimal price = act.TryGetProperty("price", out var priceProp) ? ParseDecimal(priceProp) : 0m; + decimal size = act.TryGetProperty("size", out var sizeProp) ? ParseDecimal(sizeProp) : 0m; totalSize += size; weightedPriceSum += (price * size); diff --git a/src/PolyTrader.Modules.ResolutionFarming/Models/RfSettings.cs b/src/PolyTrader.Modules.ResolutionFarming/Models/RfSettings.cs index 9736723..351e675 100644 --- a/src/PolyTrader.Modules.ResolutionFarming/Models/RfSettings.cs +++ b/src/PolyTrader.Modules.ResolutionFarming/Models/RfSettings.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; namespace PolyTrader.Modules.ResolutionFarming.Models { @@ -72,7 +72,7 @@ namespace PolyTrader.Modules.ResolutionFarming.Models [Category("03. Risiko")] [DisplayName("Tagesverlust-Kill-Switch (USDC)")] - [Description("Überschreitet der realisierte Tagesverlust diesen Betrag, pausiert das Modul + Threema-Warnung. 0 = deaktiviert.")] + [Description("Überschreitet der realisierte Tagesverlust diesen Betrag, pausiert das Modul + Warnmeldung. 0 = deaktiviert.")] public decimal DailyLossKillSwitchUsd { get; set; } = 0m; // ----- Ausführung ----- diff --git a/src/PolyTrader.Modules.Supervisor/Services/DailyReportService.cs b/src/PolyTrader.Modules.Supervisor/Services/DailyReportService.cs index c7c54ef..5982a5b 100644 --- a/src/PolyTrader.Modules.Supervisor/Services/DailyReportService.cs +++ b/src/PolyTrader.Modules.Supervisor/Services/DailyReportService.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; +using PolyTrader.Core.Notifications; using PolyTrader.Modules.Supervisor.Agent; using PolyTrader.Modules.Supervisor.Persistence; using PolyTraderSharp.Services; @@ -10,7 +11,8 @@ namespace PolyTrader.Modules.Supervisor.Services { /// /// Täglicher Supervisor-Bericht (S-3): lässt den Agenten einmal am Tag eine Kurz-Analyse der - /// letzten 24 h erstellen (KPIs, Auffälligkeiten, Fehler) und sendet sie via Threema; zusätzlich + /// letzten 24 h erstellen (KPIs, Auffälligkeiten, Fehler) und sendet sie über den konfigurierten + /// Benachrichtigungskanal (); zusätzlich /// als sup_report gespeichert. OPT-IN: läuft nur, wenn POLYTRADER_SUPERVISOR_DAILY die Ziel-Stunde /// (0–23, lokale Zeit) enthält — und braucht den OpenRouter-Key (sonst still übersprungen). /// @@ -25,12 +27,12 @@ namespace PolyTrader.Modules.Supervisor.Services private readonly SupervisorAgent _agent; private readonly ISupervisorReportRepository _reports; - private readonly Func _sendAsync; // Threema-Versand (injizierbar für Tests) + private readonly Func _sendAsync; // Versand (injizierbar für Tests) private readonly TerminalLogger _logger; public DailyReportService(SupervisorAgent agent, ISupervisorReportRepository reports, - ThreemaService threema, TerminalLogger logger) - : this(agent, reports, async msg => await threema.SendMessageAsync(msg), logger) { } + INotificationSink notify, TerminalLogger logger) + : this(agent, reports, async msg => await notify.SendAsync(msg, NotificationSeverity.Info), logger) { } internal DailyReportService(SupervisorAgent agent, ISupervisorReportRepository reports, Func sendAsync, TerminalLogger logger) diff --git a/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs b/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs index 6e40f37..95d4d09 100644 --- a/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs +++ b/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -43,7 +43,7 @@ namespace PolyTrader.Modules.Supervisor services.AddSingleton(); services.AddSingleton(); - // S-3: Counterfactual-Job (abgelehnte BUYs vs. Marktausgang) + täglicher Threema-Bericht + // S-3: Counterfactual-Job (abgelehnte BUYs vs. Marktausgang) + täglicher Bericht // (OPT-IN via POLYTRADER_SUPERVISOR_DAILY = Stunde 0–23). services.AddSingleton(); services.AddHostedService(); diff --git a/tests/PolyTrader.Tests/ApiNumberParsingTests.cs b/tests/PolyTrader.Tests/ApiNumberParsingTests.cs new file mode 100644 index 0000000..e445328 --- /dev/null +++ b/tests/PolyTrader.Tests/ApiNumberParsingTests.cs @@ -0,0 +1,64 @@ +using System.Globalization; +using System.Text.Json; +using System.Threading; +using PolyTraderSharp.Services; +using Xunit; + +namespace PolyTrader.Tests +{ + /// + /// Regressionsschutz für den Kultur-Bug in der Aktivitäts-Auswertung: Polymarket liefert + /// price/size je nach Endpunkt als JSON-Zahl ODER als String ("0.53"). Wurde der + /// String kulturabhängig geparst, las de-DE den Punkt als Tausendertrennzeichen – aus 0,53 wurde + /// 53 (Faktor-100-Fehler im Einstandspreis der Master-Trader-Auswertung). + /// + /// Die Tests setzen die Thread-Kultur bewusst auf de-DE, damit sie auch auf einer invarianten + /// Umgebung (z.B. Linux-CI ohne LANG) fehlschlagen würden, sobald jemand die Invariant-Angabe + /// wieder entfernt. + /// + public class ApiNumberParsingTests + { + /// Führt die Prüfung unter erzwungener de-DE-Kultur aus und stellt sie danach wieder her. + private static void UnderGermanCulture(System.Action assertions) + { + var previous = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); + try { assertions(); } + finally { Thread.CurrentThread.CurrentCulture = previous; } + } + + private static JsonElement Prop(string json) => + JsonDocument.Parse(json).RootElement.GetProperty("v"); + + [Theory] + [InlineData("\"0.53\"", 0.53)] // der eigentliche Bug: wurde unter de-DE zu 53 + [InlineData("\"0.045\"", 0.045)] + [InlineData("\"1234.5\"", 1234.5)] // ohne Invariant unter de-DE: 12345 + [InlineData("\"0\"", 0)] + public void ParseDecimal_reads_string_values_invariantly(string jsonValue, double expected) + { + UnderGermanCulture(() => + Assert.Equal((decimal)expected, TraderMonitorService.ParseDecimal(Prop($"{{\"v\":{jsonValue}}}")))); + } + + [Theory] + [InlineData("0.53", 0.53)] + [InlineData("1234.5", 1234.5)] + public void ParseDecimal_reads_numeric_values(string jsonValue, double expected) + { + UnderGermanCulture(() => + Assert.Equal((decimal)expected, TraderMonitorService.ParseDecimal(Prop($"{{\"v\":{jsonValue}}}")))); + } + + [Theory] + [InlineData("null")] + [InlineData("\"\"")] + [InlineData("\"n/a\"")] + [InlineData("true")] + public void ParseDecimal_returns_zero_for_unusable_values(string jsonValue) + { + UnderGermanCulture(() => + Assert.Equal(0m, TraderMonitorService.ParseDecimal(Prop($"{{\"v\":{jsonValue}}}")))); + } + } +} diff --git a/tests/PolyTrader.Tests/CopyTradingEngineTests.cs b/tests/PolyTrader.Tests/CopyTradingEngineTests.cs index 9c5c79c..08ae30b 100644 --- a/tests/PolyTrader.Tests/CopyTradingEngineTests.cs +++ b/tests/PolyTrader.Tests/CopyTradingEngineTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading.Channels; using System.Threading.Tasks; @@ -40,10 +40,10 @@ namespace PolyTrader.Tests var posRepo = new FakePositionRepository(); var marketRepo = new FakeMarketRepository(); var accountRepo = new FakeAccountRepository(); - var threema = new ThreemaService(logger, new JobManager()); + var notify = new FakeNotificationSink(); var journal = new FakeDecisionJournal(); var orderEvents = new FakeOrderEventLog(); - var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo, orderEvents); + var ladder = new SellLadderService(copy, state, clob, logger, notify, posRepo, orderEvents); var signalCh = Channel.CreateUnbounded(); var closedCh = Channel.CreateUnbounded(); diff --git a/tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs b/tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs new file mode 100644 index 0000000..3b2389c --- /dev/null +++ b/tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PolyTrader.Core.Notifications; + +namespace PolyTrader.Tests.Fakes +{ + /// + /// In-Memory-Stub des Benachrichtigungsausgangs – Tests können asserten, WAS gemeldet wurde + /// (zuvor bekamen die Tests eine echte ThreemaService-Instanz, die nichts prüfbar machte). + /// + public sealed class FakeNotificationSink : INotificationSink + { + public List<(string Message, NotificationSeverity Severity)> Sent { get; } = new(); + + public Task SendAsync(string message, NotificationSeverity severity = NotificationSeverity.Info, + CancellationToken ct = default) + { + Sent.Add((message, severity)); + return Task.CompletedTask; + } + } +} diff --git a/tests/PolyTrader.Tests/SellLadderServiceTests.cs b/tests/PolyTrader.Tests/SellLadderServiceTests.cs index 878478f..e86d787 100644 --- a/tests/PolyTrader.Tests/SellLadderServiceTests.cs +++ b/tests/PolyTrader.Tests/SellLadderServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using PolyTrader.Tests.Fakes; @@ -25,9 +25,9 @@ namespace PolyTrader.Tests var state = new TradingState(); clob ??= new FakeClobClient(); var logger = new TerminalLogger(); - var threema = new ThreemaService(logger, new JobManager()); + var notify = new FakeNotificationSink(); var repo = new FakePositionRepository(); - var svc = new SellLadderService(copy, state, clob, logger, threema, repo, new FakeOrderEventLog()); + var svc = new SellLadderService(copy, state, clob, logger, notify, repo, new FakeOrderEventLog()); return (svc, copy, state, clob, repo); } @@ -169,7 +169,7 @@ namespace PolyTrader.Tests var (svc, copy, state, clob, _) = Build(); var (account, pos) = LiveAccountWithPosition(state); var ladder = FloorLadder(); - ladder.FloorNotified = true; // Threema-Notify überspringen + ladder.FloorNotified = true; // Floor-Benachrichtigung überspringen copy.ExitLadders["1_" + Tok] = ladder; clob.OpenOrdersByAsset[Tok] = new() { ("oid-1", "SELL", 0.40m) }; diff --git a/tests/PolyTrader.Tests/TraderMonitorServiceTests.cs b/tests/PolyTrader.Tests/TraderMonitorServiceTests.cs index 0479db3..8a651a8 100644 --- a/tests/PolyTrader.Tests/TraderMonitorServiceTests.cs +++ b/tests/PolyTrader.Tests/TraderMonitorServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading; using System.Threading.Channels; @@ -29,8 +29,8 @@ namespace PolyTrader.Tests var posRepo = new FakePositionRepository(); var marketRepo = new FakeMarketRepository(); var tradeLog = new FakeCopyTradeLogRepository(); - var threema = new ThreemaService(logger, new JobManager()); - var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo, new FakeOrderEventLog()); + var notify = new FakeNotificationSink(); + var ladder = new SellLadderService(copy, state, clob, logger, notify, posRepo, new FakeOrderEventLog()); var signalCh = Channel.CreateUnbounded(); var closedCh = Channel.CreateUnbounded();