From 81dfdb1680fe520617f04f71318382bb6a74a9e6 Mon Sep 17 00:00:00 2001 From: Felix Winterleitner Date: Wed, 15 Feb 2023 15:10:02 +0100 Subject: [PATCH] add support for new Kontomanager UI remove WebSMS --- .gitignore | 1 + KontomanagerClient/CustomCarrierClient.cs | 4 +- KontomanagerClient/KontomanagerClient.cs | 812 ++++++++----------- KontomanagerClient/KontomanagerClient.csproj | 12 +- KontomanagerClient/PhoneNumber.cs | 4 + KontomanagerClient/XOXOClient.cs | 8 +- KontomanagerClient/YesssClient.cs | 7 +- KontomanagerClientNet.sln | 6 + README.md | 54 +- 9 files changed, 366 insertions(+), 542 deletions(-) diff --git a/.gitignore b/.gitignore index a674051..eb5b0bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /KontomanagerClient/bin/ /KontomanagerClient/obj/ /KontomanagerClientDemo/ +/TestProject/ /KontomanagerClientTests/ /.idea/ diff --git a/KontomanagerClient/CustomCarrierClient.cs b/KontomanagerClient/CustomCarrierClient.cs index c6f5466..f8cec5c 100644 --- a/KontomanagerClient/CustomCarrierClient.cs +++ b/KontomanagerClient/CustomCarrierClient.cs @@ -8,8 +8,8 @@ namespace KontomanagerClient /// public class CustomCarrierClient : KontomanagerClient { - public CustomCarrierClient(string username, string password, string baseUri, string loginUri, string sendUri) - : base(username, password, new Uri(baseUri), new Uri(loginUri), new Uri(sendUri)) + public CustomCarrierClient(string username, string password, string baseUri) + : base(username, password, new Uri(baseUri)) { } diff --git a/KontomanagerClient/KontomanagerClient.cs b/KontomanagerClient/KontomanagerClient.cs index 4f5b034..3f2b40c 100644 --- a/KontomanagerClient/KontomanagerClient.cs +++ b/KontomanagerClient/KontomanagerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; @@ -25,14 +25,13 @@ public abstract class KontomanagerClient : IDisposable #region Parameters private int _sessionTimeoutSeconds = 10 * 60; - private bool _autoReconnect = true; private bool _useQueue = false; private bool _enableDebugLogging = false; - private bool _exceptionOnInvalidNumberFormat = false; protected HashSet _excludedSections = new HashSet() { - "Ukraine Freieinheiten", "Ihre Kostenkontrolle" + "Ukraine Freieinheiten", "Ihre Kostenkontrolle", "TUR SYR Einheiten", "Verknüpfte Rufnummern", + "Aktuelle Kosten", "Oft benutzt" }; #endregion @@ -46,25 +45,22 @@ public abstract class KontomanagerClient : IDisposable #endregion + private CookieContainer _cookieContainer = new CookieContainer(); + private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClient _httpClient; + protected readonly Uri BaseUri; - protected readonly Uri LoginUri; - protected readonly Uri SendUri; - protected readonly Uri SettingsUri; + public string LoginPath { get; set; } = "index.php"; + public string SettingsPath { get; set; } = "einstellungen_profil.php"; + public string AccountUsagePath { get; set; } = "kundendaten.php"; private DateTime _lastConnected = DateTime.MinValue; - private CookieContainer _cookieContainer = new CookieContainer(); private readonly string _user; private readonly string _password; - #region Send Queue - - private BlockingCollection Messages = new BlockingCollection(); - public MessageCounter _counter = new MessageCounter(60 * 60, 50); - - #endregion - + private readonly Dictionary _numberToSubscriberId = new Dictionary(); public bool Connected => DateTime.Now - _lastConnected < TimeSpan.FromSeconds(_sessionTimeoutSeconds); @@ -74,18 +70,18 @@ public abstract class KontomanagerClient : IDisposable /// /// /// - protected KontomanagerClient(string user, string password, Uri baseUri, Uri loginUri, Uri sendUri) + protected KontomanagerClient(string user, string password, Uri baseUri) { _user = user; _password = password; BaseUri = baseUri; - LoginUri = loginUri; - SendUri = sendUri; - /* - * More uris can be changed in subclasses via protected fields - */ - SettingsUri = new Uri(Path.Combine(BaseUri.AbsoluteUri, "einstellungen.php")); + _cookieContainer.Add(new Cookie("CookieSettings", + "%7B%22categories%22%3A%5B%22necessary%22%2C%22improve_offers%22%5D%7D", "/", "yesss.at")); + _httpClientHandler = new HttpClientHandler(); + _httpClientHandler.CookieContainer = _cookieContainer; + _httpClient = new HttpClient(_httpClientHandler); + _httpClient.BaseAddress = baseUri; } /// @@ -99,131 +95,120 @@ public KontomanagerClient SetSessionTimeoutSeconds(int seconds) return this; } - public KontomanagerClient UseAutoReconnect(bool value) - { - _autoReconnect = value; - return this; - } - /// - /// Configures the Client to use a FIFO queue to ensure all messages are sent (provided that the app does not shut down). + /// Returns the phone number for which the client is currently active. + /// This is relevant when multiple phone numbers are grouped in one account. /// - /// /// - public KontomanagerClient UseQueue() + public async Task GetSelectedPhoneNumber() { - _useQueue = true; - Task.Run(StartQueueConsumer); - return this; + if (!Connected) + await Reconnect(); + HttpResponseMessage response = await _httpClient.GetAsync(SettingsPath); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException("Could not determine the selected phone number"); + var responseHtml = await response.Content.ReadAsStringAsync(); + string number = ExtractSelectedPhoneNumberFromSettingsPage(responseHtml); + + if (number is null) throw new Exception("Phone number could not be found"); + return number; } - private async void StartQueueConsumer() + private string ExtractSelectedPhoneNumberFromSettingsPage(string settingsPageHtml) { - while (!Messages.IsCompleted) - { - if (!_counter.CanSend()) - { - var delay = _counter.TimeUntilNewElementPossible(); - Log($"Waiting {delay.TotalSeconds} seconds for next message. {Messages.Count} messages waiting."); - await Task.Delay(_counter.TimeUntilNewElementPossible()); - } - - var m = Messages.Take(); - var res = await SendMessageWithReconnect(m); - while (res != MessageSendResult.Ok) - { - var delay = _counter.TimeUntilNewElementPossible(); - Log( - $"Waiting {delay.TotalSeconds} seconds for resending message. Result was {res}. {Messages.Count} messages waiting in queue."); - await Task.Delay(_counter.TimeUntilNewElementPossible()); - } + var doc = new HtmlDocument(); + doc.LoadHtml(settingsPageHtml); - await Task.Delay(1000); + foreach (var row in doc.DocumentNode.SelectNodes("//li[@class='list-group-item']")) + { + if (row.HasClass("list-group-header")) continue; + var d = row.SelectSingleNode("./div"); + var sides = d.SelectNodes("./div"); + if (sides.Count < 2 || + !sides[0].InnerText.StartsWith("Rufnummer")) continue; + return sides.Last().InnerText; } - } - /// - /// Specifies if the SendMessage method should throw an exception when the number format specified is invalid. - /// - /// - /// - public KontomanagerClient ThrowExceptionOnInvalidNumberFormat(bool value) - { - _exceptionOnInvalidNumberFormat = value; - return this; + return null; } - - /// - /// Returns the phone number for which the client is currently active. - /// This is relevant when multiple phone numbers are grouped in one account. - /// - /// - public async Task GetSelectedPhoneNumber() + + private PhoneNumber ExtractPhoneNumberFromDropdown(HtmlNode liNode) { - using (HttpClientHandler handler = new HttpClientHandler()) + if (liNode is null || liNode.InnerHtml.Contains("index.php?dologout=2") || (liNode.FirstChild != null && liNode.FirstChild.HasClass("dropdown-divider"))) return null; + var name = liNode.SelectSingleNode(".//span").InnerText; + + var pattern = @"\d+\/\d*"; + var match = Regex.Match(liNode.InnerHtml, pattern); + var number = $"43{match.Value.Replace("/", "").Substring(1)}"; + + var subscriberId = liNode.SelectSingleNode("./a").Attributes["href"].Value == "#" ? + null : + HttpUtility.UrlDecode(Regex.Match(liNode.SelectSingleNode("./a").Attributes["href"].Value, @"subscriber=([^&]*)").Groups[1].Value); + if (string.IsNullOrEmpty(subscriberId)) { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) - { - HttpResponseMessage response = await client.GetAsync(SettingsUri); - if (!response.IsSuccessStatusCode) - throw new HttpRequestException("Could not determine the selected phone number"); - var responseHtml = await response.Content.ReadAsStringAsync(); - string number = null; - if (response.RequestMessage.RequestUri.AbsoluteUri.EndsWith("kundendaten.php")) - { - number = ExtractSelectedPhoneNumberFromHeaderElement(responseHtml); - var nums = ExtractSelectablePhoneNumbersFromHomePage(responseHtml).ToList(); - number = !nums.Any() ? ExtractSelectedPhoneNumberFromHeaderElement(responseHtml) : nums.FirstOrDefault(n => n.Selected)?.Number; - } - else number = ExtractSelectedPhoneNumberFromSettingsPage(responseHtml); - if (number is null) throw new Exception("Phone number could not be found"); - return number; - } + if (_numberToSubscriberId.ContainsKey(number)) + subscriberId = _numberToSubscriberId[number]; + } + else + { + _numberToSubscriberId[number] = subscriberId; } + var isSelected = GetPreviousActualSibling(liNode)?.InnerText.ToLower().Contains("aktuell gewählte rufnummer")??false; + + return new PhoneNumber() + { + Name = name, SubscriberId = subscriberId, Number = number, + Selected = isSelected + }; } - private string ExtractSelectedPhoneNumberFromHeaderElement(string pageHtml) + private HtmlNode GetPreviousActualSibling(HtmlNode n) { - var doc = new HtmlDocument(); - doc.LoadHtml(pageHtml); + HtmlNode res = n; + while (res != null && (res == n || res.NodeType != n.NodeType)) + { + if (res.PreviousSibling == null) return null; + res = res.PreviousSibling; + } - var nodes = doc.DocumentNode.SelectNodes("//div[@class='loggedin']"); - var selectedNumberNode = nodes.LastOrDefault(); - if (selectedNumberNode is null) return null; - var pattern = @"\d+\/\d*"; - var match = Regex.Match(selectedNumberNode.InnerHtml, pattern); - return $"43{match.Value.Replace("/", "").Substring(1)}"; + return res; } - - private string ExtractSelectedPhoneNumberFromSettingsPage(string settingsPageHtml) + private HtmlNode GetNextActualSibling(HtmlNode n) { - var doc = new HtmlDocument(); - doc.LoadHtml(settingsPageHtml); - - foreach (var row in doc.DocumentNode.SelectNodes("//tr")) + HtmlNode res = n; + while (res != null && (res == n || res.NodeType != n.NodeType)) { - var childTds = row.SelectNodes("td"); - if (childTds is null || childTds.Count == 0 || !childTds.First().InnerText.StartsWith("Ihre Rufnummer")) continue; - return childTds.Last().InnerText; + if (res.NextSibling == null) return null; + res = res.NextSibling; } - return null; + return res; } - - private IEnumerable ExtractSelectablePhoneNumbersFromHomePage(string homePageHtml) + private IEnumerable ExtractSelectablePhoneNumbersFromDropdown(string homePageHtml) { + var res = new List(); var doc = new HtmlDocument(); doc.LoadHtml(homePageHtml); - var form = doc.GetElementbyId("subscriber_dropdown_form"); - if (form is null) - return new List(); - return form.SelectNodes("//select/option").Select(o => new PhoneNumber() + + var dd = doc.DocumentNode.SelectSingleNode("//ul[@aria-labelledby='user-dropdown']"); + if (dd is null) return res; + + // Selected Number + var sn = dd.ChildNodes.FirstOrDefault(n => n.InnerText.ToLower().Contains("aktuell gewählte rufnummer")); + if (sn != null && GetNextActualSibling(sn) != null) { - Number = o.InnerText.Split('-').First().Trim(), - SubscriberId = o.GetAttributeValue("value", null), - Selected = o.GetAttributeValue("selected", "") == "selected" - }); + res.Add(ExtractPhoneNumberFromDropdown(GetNextActualSibling(sn))); + } + //other numbers + var an = dd.ChildNodes.FirstOrDefault(n => n.InnerText.ToLower().Contains("rufnummer wechseln")); + if (an is null) return null; + while (GetNextActualSibling(an) != null) + { + an = GetNextActualSibling(an); + res.Add(ExtractPhoneNumberFromDropdown(an)); + } + + return res.Where(n => n != null); } /// @@ -233,18 +218,13 @@ private IEnumerable ExtractSelectablePhoneNumbersFromHomePage(strin /// public async Task> GetSelectablePhoneNumbers() { - using (HttpClientHandler handler = new HttpClientHandler()) - { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) - { - HttpResponseMessage response = await client.GetAsync(SettingsUri); - if (!response.IsSuccessStatusCode) - throw new HttpRequestException("Could not determine the selectable phone numbers"); - var responseHtml = await response.Content.ReadAsStringAsync(); - return ExtractSelectablePhoneNumbersFromHomePage(responseHtml); - } - } + if (!Connected) + await Reconnect(); + HttpResponseMessage response = await _httpClient.GetAsync(SettingsPath); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException("Could not determine the selectable phone numbers"); + var responseHtml = await response.Content.ReadAsStringAsync(); + return ExtractSelectablePhoneNumbersFromDropdown(responseHtml); } /// @@ -252,357 +232,240 @@ public async Task> GetSelectablePhoneNumbers() /// public async Task SelectPhoneNumber(PhoneNumber number) { + if (number == null || number.SubscriberId == null) + throw new Exception("No subscriber id provided!"); + var content = new FormUrlEncodedContent(new[] { new KeyValuePair("groupaction", "change_subscriber"), new KeyValuePair("subscriber", number.SubscriberId), }); - using (HttpClientHandler handler = new HttpClientHandler()) + + if (!Connected) + await Reconnect(); + + HttpResponseMessage response = await _httpClient.PostAsync(AccountUsagePath, content); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException("Could not change the selected phone number"); + var responseHtml = await response.Content.ReadAsStringAsync(); + // If the corresponding sim card has been deactivated, settings.php will automatically redirect to kundendaten.php + if (response.RequestMessage.RequestUri.AbsoluteUri.EndsWith("kundendaten.php")) { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) + if (ExtractSelectablePhoneNumbersFromDropdown(responseHtml) + .FirstOrDefault(n => n.SubscriberId == number.SubscriberId) is null) { - HttpResponseMessage response = await client.PostAsync(SettingsUri, content); - if (!response.IsSuccessStatusCode) - throw new HttpRequestException("Could not change the selected phone number"); - var responseHtml = await response.Content.ReadAsStringAsync(); - // If the corresponding sim card has been deactivated, settings.php will automatically redirect to kundendaten.php - if (response.RequestMessage.RequestUri.AbsoluteUri.EndsWith("kundendaten.php")) - { - if (ExtractSelectablePhoneNumbersFromHomePage(responseHtml) - .FirstOrDefault(n => n.SubscriberId == number.SubscriberId) is null) - { - throw new Exception("Could not change the selected phone number"); - } - } - else if (ExtractSelectedPhoneNumberFromSettingsPage(responseHtml) != number.Number) - throw new Exception("Could not change the selected phone number"); + throw new Exception("Could not change the selected phone number"); } } + else if (ExtractSelectedPhoneNumberFromSettingsPage(responseHtml) != number.Number) + throw new Exception("Could not change the selected phone number"); } + /// + /// Loads the account usage for the selected phone number. + /// + /// + /// public async Task GetAccountUsage() { - using (HttpClientHandler handler = new HttpClientHandler()) + if (!Connected) + await Reconnect(); + + HttpResponseMessage response = await _httpClient.GetAsync(AccountUsagePath); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException("Could not get account usage"); + var responseHtml = await response.Content.ReadAsStringAsync(); + var doc = new HtmlDocument(); + doc.LoadHtml(responseHtml); + + var result = new AccountUsage(); + + IEnumerable ParseBasePageSections() { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) + var res = new List(); + var contentAreas = doc.DocumentNode.SelectNodes("//div[@class='card']"); + foreach (var section in contentAreas) { - HttpResponseMessage response = await client.GetAsync(BaseUri); - if (!response.IsSuccessStatusCode) - throw new HttpRequestException("Could not get account usage"); - var responseHtml = await response.Content.ReadAsStringAsync(); - var doc = new HtmlDocument(); - doc.LoadHtml(responseHtml); - - var result = new AccountUsage(); + var pu = new PackageUsage(); + var heading = section.SelectSingleNode(".//h1"); + if (heading != null) + { + pu.PackageName = HttpUtility.HtmlDecode(heading.InnerText.TrimEnd(':')); + } - IEnumerable ParseBasePageSections() + var progressItems = section.SelectNodes(".//div[@class='progress-item']"); + if (progressItems != null) { - var res = new List(); - var contentAreas = doc.DocumentNode.SelectNodes("//div[@class='progress-list']"); - foreach (var section in contentAreas) + foreach (var progressItem in progressItems) { - var pu = new PackageUsage(); - var heading = section.SelectSingleNode("h3"); - if (heading != null) + var progressHeading = progressItem.SelectSingleNode(".//div[@class='progress-heading']"); + var available = progressItem.SelectSingleNode(".//div[@class='bar-label-left']"); + var used = progressItem.SelectSingleNode(".//div[@class='bar-label-right']"); + + if (progressHeading == null) continue; + var headingLowered = progressHeading.InnerText.ToLower(); + if (headingLowered.Contains("daten")) { - pu.PackageName = HttpUtility.HtmlDecode(heading.InnerText.TrimEnd(':')); + UnitQuota q = headingLowered.Contains("eu") ? pu.DataEu : pu.Data; + if (used != null) + { + var match = Regex.Match(used.InnerText, @"Verbraucht: (\d*) \(von (\S*)"); + var totText = match.Groups[2].Value; + q.Total = totText.ToLower() == "unlimited" + ? int.MaxValue + : int.Parse(totText); + q.Used = int.Parse(match.Groups[1].Value); + } } + else if (headingLowered.Contains("minuten") && !headingLowered.Contains("eu")) + { + if (headingLowered.Contains("sms")) + pu.MinutesAndSmsQuotasShared = true; + if (used != null) + { + var match = Regex.Match(used.InnerText, @"Verbraucht: (\d*) \(von (\S*)"); + var totText = match.Groups[2].Value; + pu.Minutes.Total = totText.ToLower() == "unlimited" + ? 10000 + : int.Parse(totText); + pu.Minutes.Used = int.Parse(match.Groups[1].Value); + } - var progressItems = section.SelectNodes("div[@class='progress-item']"); - if (progressItems != null) + if (pu.MinutesAndSmsQuotasShared) + { + pu.Sms = pu.Minutes; + } + } + else if (headingLowered.Contains("sms") && !headingLowered.Contains("minuten") && + !headingLowered.Contains("kostenwarnung")) { - foreach (var progressItem in progressItems) + if (used != null) { - var ul = progressItem.SelectSingleNode("div[@class='progress-heading left']"); - var ur = progressItem.SelectSingleNode("div[@class='progress-heading right']"); - var bl = progressItem.SelectSingleNode("div[@class='bar-label']"); - var br = progressItem.SelectSingleNode("div[@class='bar-label-right']"); - - if (ul == null) continue; - var ulTextLowered = ul.InnerText.ToLower(); - if (ulTextLowered.Contains("daten")) - { - UnitQuota q = ulTextLowered.Contains("eu") ? pu.DataEu : pu.Data; - if (ur != null) - { - var totText = ur.InnerText.Split(' ').First(); - q.Total = totText.ToLower() == "unlimited" - ? 999999 - : int.Parse(ur.InnerText.Split(' ').First()); - } - - if (bl != null) - { - q.Used = int.Parse(bl.ChildNodes[0].InnerText.Trim().Split(' ').Last()); - } - } - else if (ulTextLowered.Contains("minuten") || - (ur != null && ur.InnerText.ToLower().Contains("minuten"))) - { - if (ur != null) - { - if (ur.InnerText.ToLower().Contains("sms")) - pu.MinutesAndSmsQuotasShared = true; - if (ur.InnerText.ToLower().Split(' ').First() == "unlimited") - pu.Minutes.Total = 10000; - else pu.Minutes.Total = int.Parse(ulTextLowered.Split(' ').First()); - } - - if (bl != null) - { - pu.Minutes.Used = int.Parse(bl.InnerText.Split(' ').Last()); - } - - if (pu.MinutesAndSmsQuotasShared) - { - pu.Sms = pu.Minutes; - } - } - else if (ulTextLowered.Contains("sms") && - !ulTextLowered.Contains("kostenwarnung") || - (ur != null && ur.InnerText.ToLower().Contains("sms"))) - { - if (ur != null) - { - if (ur.InnerText.ToLower().Split(' ').First() == "unlimited") - pu.Sms.Total = 10000; - else pu.Sms.Total = int.Parse(ur.InnerText.ToLower().Split(' ').First()); - } - - if (bl != null) - { - pu.Sms.Used = int.Parse(bl.InnerText.Split(' ').Last()); - } - } - else if (ulTextLowered.Contains("ö") && ulTextLowered.Contains("eu") && - ulTextLowered.Contains("minuten")) - { - if (ur != null) - { - if (ulTextLowered.Split(' ').First() == "unlimited") - pu.AustriaToEuMinutes.Total = 10000; - else - pu.AustriaToEuMinutes.Total = - int.Parse(ulTextLowered.Split(' ').First()); - } - - if (bl != null) - { - pu.AustriaToEuMinutes.Used = int.Parse(bl.InnerText.Split(' ').First()); - } - } + var match = Regex.Match(used.InnerText, @"Verbraucht: (\d*) \(von (\S*)"); + var totText = match.Groups[2].Value; + pu.Sms.Total = totText.ToLower() == "unlimited" + ? 10000 + : int.Parse(totText); + pu.Sms.Used = int.Parse(match.Groups[1].Value); } } - - var infoTable = section.SelectSingleNode(".//table[@class='info-list']"); - if (infoTable != null) + else if (headingLowered.Contains("ö") && headingLowered.Contains("eu") && + headingLowered.Contains("minuten")) { - var infoItems = section.SelectNodes(".//td[@class='info-item']"); - if (infoItems != null) + if (used != null) { - foreach (var item in infoItems) - { - if (item.ChildNodes.Count == 2) - { - var infoTitle = HttpUtility.HtmlDecode(item.ChildNodes[0].InnerText.TrimEnd(':', ' ')); - var lowerTitle = infoTitle.ToLower(); - if (lowerTitle.Contains("datenvolumen") && lowerTitle.Contains("eu")) - { - try - { - pu.DataEu.Total = - int.Parse(item.ChildNodes[1].InnerText.Split(' ')[3]); - pu.DataEu.CorrectRemainingFree( - int.Parse(item.ChildNodes[1].InnerText.Split(' ')[0])); - } - catch - { - } - } - else if (lowerTitle.Contains("gültigkeit") && lowerTitle.Contains("sim")) - { - result.Prepaid = true; - result.SimCardValidUntil = DateTime.ParseExact(item.ChildNodes[1].InnerText, "dd.MM.yyyy", null); - } - else if (lowerTitle.Contains("letzte aufladung")) - { - result.LastRecharge = DateTime.ParseExact(item.ChildNodes[1].InnerText, "dd.MM.yyyy", null); - } - else if (lowerTitle.Contains("guthaben")) - { - result.Credit = - decimal.Parse(item.ChildNodes[1].ChildNodes[0].InnerText.Split(' ')[1].Replace(',', '.')); - } - else if (lowerTitle.Contains("gültig von") || lowerTitle.Contains("aktivierung des paket")) - { - pu.UnitsValidFrom = DateTime.ParseExact(item.ChildNodes[1].InnerText, - "dd.MM.yyyy HH:mm", null); - } - else if (lowerTitle.Contains("gültig bis") || lowerTitle.Contains("gültigkeit des paket")) - { - pu.UnitsValidUntil = DateTime.ParseExact(item.ChildNodes[1].InnerText, - "dd.MM.yyyy HH:mm", null); - } - else - { - pu.AdditionalInformation[infoTitle] = HttpUtility.HtmlDecode(item.ChildNodes[1].InnerText); - } - } - } + var match = Regex.Match(used.InnerText, @"Verbraucht: (\d*) \(von (\S*)"); + var totText = match.Groups[2].Value; + pu.AustriaToEuMinutes.Total = totText.ToLower() == "unlimited" + ? 10000 + : int.Parse(totText); + pu.AustriaToEuMinutes.Used = int.Parse(match.Groups[1].Value); } } - - if (!_excludedSections.Contains(pu.PackageName)) - res.Add(pu); } + } - var dataItemListTable = doc.DocumentNode.SelectSingleNode("//table[@class='data-item-list']"); - if (dataItemListTable != null) + var infoTable = section.SelectSingleNode(".//ul[@class='list-group list-group-flush']"); + if (infoTable != null) + { + var infoItems = section.SelectNodes(".//li[@class='list-group-item']"); + if (infoItems != null) { - var trs = dataItemListTable.SelectNodes(".//tr"); - if (trs != null) + foreach (var item in infoItems.Where(i => i.InnerText.Contains(":"))) { - foreach (var tr in trs) + var infoTitle = + HttpUtility.HtmlDecode(item.InnerText.Split(':').First()); + var lowerTitle = infoTitle.ToLower(); + var infoValue = HttpUtility.HtmlDecode(item.InnerText.Split(new []{':'}, 2)[1].Trim()); + if (lowerTitle.Contains("datenvolumen") && lowerTitle.Contains("eu")) { - var tds = tr.SelectNodes(".//td"); - if (tds.Count != 2) continue; - var lowerTitle = tds[0].InnerText.ToLower(); - if (lowerTitle.Contains("rechnungsdatum")) - { - result.InvoiceDate = DateTime.ParseExact(tds[1].InnerText, "dd.MM.yyyy", null); - } - else if (lowerTitle.Contains("vorläufige kosten")) + try { - result.Cost = decimal.Parse(tds[1].InnerText.Split(' ')[1].Replace(',', '.')); + var match = Regex.Match(infoValue, @"(\d*) MB von (\d*) MB"); + pu.DataEu.Total = + int.Parse(match.Groups[2].Value); + pu.DataEu.CorrectRemainingFree(int.Parse(match.Groups[1].Value)); } + catch { } + } + else if (lowerTitle.Contains("gültigkeit") && lowerTitle.Contains("sim")) + { + result.Prepaid = true; + result.SimCardValidUntil = + DateTime.ParseExact(infoValue, "dd.MM.yyyy", + null); + } + else if (lowerTitle.Contains("letzte aufladung")) + { + result.LastRecharge = DateTime.ParseExact(infoValue, + "dd.MM.yyyy", null); + } + else if (lowerTitle.Contains("guthaben")) + { + result.Credit = + decimal.Parse( + item.ChildNodes[1].ChildNodes[0].InnerText.Split(' ')[1] + .Replace(',', '.')); + } + else if (lowerTitle.Contains("gültig von") || + lowerTitle.Contains("aktivierung des paket")) + { + pu.UnitsValidFrom = DateTime.ParseExact(infoValue, + "dd.MM.yyyy HH:mm", null); + } + else if (lowerTitle.Contains("gültig bis") || + lowerTitle.Contains("gültigkeit des paket")) + { + pu.UnitsValidUntil = DateTime.ParseExact(infoValue, + "dd.MM.yyyy HH:mm", null); + } + else + { + pu.AdditionalInformation[infoTitle] = + HttpUtility.HtmlDecode(infoValue); } } } - - - return res; } - var sections = ParseBasePageSections(); - result.Number = await GetSelectedPhoneNumber(); - result.PackageUsages = sections.ToList(); - return result; - // TODO: parse base page + if (!_excludedSections.Contains(pu.PackageName)) + res.Add(pu); } - } - } - /// - /// Depending on configuration enqueues or directly sends the message m. - /// If the queue is used, MessageSendResult.Enqueued is returned. The actual result is then obtained by subscribing to the message's SendingAttempted event. - /// - /// - /// - public async Task SendMessage(Message m) - { - if (_useQueue) - { - Messages.Add(m); - return MessageSendResult.MessageEnqueued; - } - else - { - if (_autoReconnect) - return await SendMessageWithReconnect(m); - else return await CallMessageSendingEndpoint(m); - } - } - - public async Task SendMessage(string recipient, string message) - { - return await SendMessage(new Message(recipient, message)); - } - - /// - /// Calls the Message Sending endpoint and tries to send the message. Returns the result. - /// - /// - /// - private async Task CallMessageSendingEndpoint(Message m) - { - var content = new FormUrlEncodedContent(new[] - { - new KeyValuePair("telefonbuch", "-"), - new KeyValuePair("to_netz", "a"), - new KeyValuePair("to_nummer", m.RecipientNumber), - new KeyValuePair("nachricht", m.Body), - new KeyValuePair("token", await GetToken()) - }); - try - { - using (HttpClientHandler handler = new HttpClientHandler()) + // TODO: Aktuelle Kosten section auf neues UI updaten + var dataItemListTable = doc.DocumentNode.SelectSingleNode("//table[@class='data-item-list']"); + if (dataItemListTable != null) { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) + var trs = dataItemListTable.SelectNodes(".//tr"); + if (trs != null) { - HttpResponseMessage response = await client.PostAsync(SendUri, content); - var responseHTML = await response.Content.ReadAsStringAsync(); - MessageSendResult res; - if (responseHTML.Contains("erfolgreich")) - res = MessageSendResult.Ok; - else if (responseHTML.Contains("Pro Rufnummer sind maximal")) - res = MessageSendResult.LimitReached; - else if (responseHTML.Contains( - "Eine oder mehrere SMS konnte(n) nicht versendet werden, da die angegebene Empfängernummer ungültig war.") - ) + foreach (var tr in trs) { - if (_exceptionOnInvalidNumberFormat) - throw new FormatException( - $"The format of the recipient number {m.RecipientNumber} does not match the expected format 00[country][number_without_leading_0]"); - else return MessageSendResult.InvalidNumberFormat; + var tds = tr.SelectNodes(".//td"); + if (tds.Count != 2) continue; + var lowerTitle = tds[0].InnerText.ToLower(); + if (lowerTitle.Contains("rechnungsdatum")) + { + result.InvoiceDate = DateTime.ParseExact(tds[1].InnerText, "dd.MM.yyyy", null); + } + else if (lowerTitle.Contains("vorläufige kosten")) + { + result.Cost = decimal.Parse(tds[1].InnerText.Split(' ')[1].Replace(',', '.')); + } } - - else res = MessageSendResult.SessionExpired; - - m.NotifySendingAttempt(res); - if (res == MessageSendResult.Ok) _counter.Success(); - else _counter.Fail(); - return res; } } - } - catch (Exception e) - { - if (e is InvalidOperationException || e is HttpRequestException || e is TaskCanceledException) - { - Log("Error: " + e.Message); - m.NotifySendingAttempt(MessageSendResult.OtherError); - return MessageSendResult.OtherError; - } - throw; - } - } - /// - /// Sends messages with automatic reconnection enabled. - /// If message sending fails once due to expired session, the sending process is retried once. - /// - /// - /// - private async Task SendMessageWithReconnect(Message m) - { - if (!Connected) await Reconnect(); - Log($"Sending SMS to {m.RecipientNumber}"); - var res = await CallMessageSendingEndpoint(m); - if (res == MessageSendResult.SessionExpired) - { - Log("Kontomanager connection expired."); - await Reconnect(); - Log("Resending..."); - return await CallMessageSendingEndpoint(m); + return res; } - // else return true if message sent. - return res; + var sections = ParseBasePageSections(); + result.Number = await GetSelectedPhoneNumber(); + result.PackageUsages = sections.ToList(); + return result; + // TODO: parse base page } /// @@ -616,44 +479,29 @@ public KontomanagerClient EnableDebugLogging(bool value) return this; } - - /// - /// Obtains the hidden input "token" from the HTML form needed to send a message. - /// - /// - private async Task GetToken() - { - try - { - using (HttpClientHandler handler = new HttpClientHandler()) - { - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) - { - HttpResponseMessage response = await client.GetAsync(SendUri); - var responseHTML = await response.Content.ReadAsStringAsync(); - var regex = new Regex(".*"); - var match = regex.Match(responseHTML); - return match.Groups[1].Value; - } - } - } - catch (Exception e) - { - Log("Error: " + e.Message); - return string.Empty; - } - } - /// /// Initializes a session by calling the login endpoint. /// /// public async Task CreateConnection() { + await AcceptCookies(); return await CreateConnection(_user, _password); } + private async Task AcceptCookies() + { + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("dosave", "1"), + new KeyValuePair("accept-all", "1") + }); + + HttpResponseMessage response = + await _httpClient.PostAsync("einstellungen_datenschutz_web.php", content); + Console.WriteLine(response.ToString()); + } + /// /// Initializes a Kontomanager.at session. /// @@ -662,33 +510,29 @@ public async Task CreateConnection() /// private async Task CreateConnection(string user, string password) { - HttpClientHandler handler = new HttpClientHandler(); - _cookieContainer = new CookieContainer(); - handler.CookieContainer = _cookieContainer; - using (var client = new HttpClient(handler)) + var content = new FormUrlEncodedContent(new[] { - var content = new FormUrlEncodedContent(new[] - { - new KeyValuePair("login_rufnummer", user), - new KeyValuePair("login_passwort", password) - }); - - HttpResponseMessage response = await client.PostAsync(LoginUri, content); - IEnumerable responseCookies = _cookieContainer.GetCookies(LoginUri).Cast(); - foreach (Cookie cookie in responseCookies) - Log(cookie.Name + ": " + cookie.Value); - string responseHTML = await response.Content.ReadAsStringAsync(); - var doc = new HtmlDocument(); - doc.LoadHtml(responseHTML); - var success = doc.DocumentNode.SelectSingleNode("//form[@name='loginform']") == null; - if (success) - { - _lastConnected = DateTime.Now; - ConnectionEstablished?.Invoke(this, EventArgs.Empty); - return true; - } - return false; + new KeyValuePair("login_rufnummer", user), + new KeyValuePair("login_passwort", password) + }); + + HttpResponseMessage response = await _httpClient.PostAsync(LoginPath, content); + IEnumerable responseCookies = + _cookieContainer.GetCookies(new Uri(BaseUri, LoginPath)).Cast(); + foreach (Cookie cookie in responseCookies) + Log(cookie.Name + ": " + cookie.Value); + string responseHTML = await response.Content.ReadAsStringAsync(); + var doc = new HtmlDocument(); + doc.LoadHtml(responseHTML); + var success = doc.DocumentNode.SelectSingleNode("//form[@name='loginform']") == null; + if (success) + { + _lastConnected = DateTime.Now; + ConnectionEstablished?.Invoke(this, EventArgs.Empty); + return true; } + + return false; } /// @@ -698,6 +542,7 @@ private async Task CreateConnection(string user, string password) private async Task Reconnect() { Log("Reconnecting..."); + await AcceptCookies(); return await CreateConnection(_user, _password); } @@ -713,7 +558,8 @@ private void Log(string message) public void Dispose() { - Messages?.Dispose(); + _httpClient.Dispose(); + _httpClientHandler.Dispose(); } } } \ No newline at end of file diff --git a/KontomanagerClient/KontomanagerClient.csproj b/KontomanagerClient/KontomanagerClient.csproj index cb7a95b..bed226d 100644 --- a/KontomanagerClient/KontomanagerClient.csproj +++ b/KontomanagerClient/KontomanagerClient.csproj @@ -2,19 +2,19 @@ true - Kontomanager Web SMS Client + Kontomanager .NET Client Felix Winterleitner - A client library that wraps the Kontomanager Web Interface used by some Austrian cellular service providers. Enables the usage of the provided Web SMS functionality and querying of usage stats in .NET - Copyright (c) Felix Winterleitner 2022 + A client library that wraps the Kontomanager Web Interface used by some Austrian cellular service providers. Enables querying of usage stats in .NET + Copyright (c) Felix Winterleitner 2023 https://github.com/winterleitner/kontomanager-websms-client git https://licenses.nuget.org/MIT - net5.0;net6.0;netcoreapp3.1;netstandard2.0 - 1.2.7 + net5.0;net6.0;net7.0;netcoreapp3.1;netstandard2.0 + 2.0.0 - + diff --git a/KontomanagerClient/PhoneNumber.cs b/KontomanagerClient/PhoneNumber.cs index 3058d59..62183a4 100644 --- a/KontomanagerClient/PhoneNumber.cs +++ b/KontomanagerClient/PhoneNumber.cs @@ -3,6 +3,10 @@ namespace KontomanagerClient public class PhoneNumber { + /// + /// User chosen name for the number. + /// + public string Name { get; set; } public string Number { get; set; } public string SubscriberId { get; set; } diff --git a/KontomanagerClient/XOXOClient.cs b/KontomanagerClient/XOXOClient.cs index 3f1b2e1..33c5811 100644 --- a/KontomanagerClient/XOXOClient.cs +++ b/KontomanagerClient/XOXOClient.cs @@ -4,13 +4,11 @@ namespace KontomanagerClient { public class XOXOClient : KontomanagerClient { - public XOXOClient(string username, string password) - : base(username, password, new Uri("https://xoxo.kontomanager.at"), - new Uri("https://xoxo.kontomanager.at/index.php"), - new Uri("https://xoxo.kontomanager.at/websms_send.php")) + public XOXOClient(string user, string password) : + base(user, password, new Uri("https://xoxo.kontomanager.at/app/")) { - } } + } \ No newline at end of file diff --git a/KontomanagerClient/YesssClient.cs b/KontomanagerClient/YesssClient.cs index 8b00b5f..d815f14 100644 --- a/KontomanagerClient/YesssClient.cs +++ b/KontomanagerClient/YesssClient.cs @@ -4,12 +4,9 @@ namespace KontomanagerClient { public class YesssClient : KontomanagerClient { - public YesssClient(string username, string password) : - base(username, password, new Uri("https://www.yesss.at/kontomanager.at/"), - new Uri("https://www.yesss.at/kontomanager.at/index.php"), - new Uri("https://www.yesss.at/kontomanager.at/websms_send.php")) + public YesssClient(string user, string password) : + base(user, password, new Uri("https://www.yesss.at/kontomanager.at/app/")) { - } } } \ No newline at end of file diff --git a/KontomanagerClientNet.sln b/KontomanagerClientNet.sln index 1c47d8e..098bae2 100644 --- a/KontomanagerClientNet.sln +++ b/KontomanagerClientNet.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KontomanagerClientDemo", "K EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KontomanagerClientTests", "KontomanagerClientTests\KontomanagerClientTests.csproj", "{F5EC4F31-27C9-4E70-9C13-BC8EA2BC7B37}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject", "TestProject\TestProject.csproj", "{2979450B-5EC0-4F19-8CD1-791D1A3C8751}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {F5EC4F31-27C9-4E70-9C13-BC8EA2BC7B37}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5EC4F31-27C9-4E70-9C13-BC8EA2BC7B37}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5EC4F31-27C9-4E70-9C13-BC8EA2BC7B37}.Release|Any CPU.Build.0 = Release|Any CPU + {2979450B-5EC0-4F19-8CD1-791D1A3C8751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2979450B-5EC0-4F19-8CD1-791D1A3C8751}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2979450B-5EC0-4F19-8CD1-791D1A3C8751}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2979450B-5EC0-4F19-8CD1-791D1A3C8751}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e292229..dfe8d33 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,10 @@ .NET Client library that wraps the WebSMS functionality provided by the kontomanager.at web management interface used by a number of mobile carriers in Austria (MVNOs in the A1 network). # UPDATE 02/2023 -As of right now, a new version of the Kontomanager Website prevents this library from functioning in the current version. -This will be fixed in a future update. +The Kontomanager Web interface has had a major design overhaul. Unfortunately, the WebSMS functionality was removed in the process. +v1.x is no longer working for at least XOXO and YESSS as of 15.02.2023. + +v2.0.0 provides basic account usage reading functionality comparable to what was present before. Reading of current monthly cost is not implemented yet. # Installation @@ -23,37 +25,18 @@ Other carriers that use Kontomanager but were not tested include: And possibly more. Feel free to add carriers to that list. -# Limitations -The tested carriers each limit the maximum number of messages that may be sent per hour and phone number to **50**. -Unfortunatly, it is not possible to read the remaining time until a new message can be sent, so best the client can do is guess the time, unless all messages were sent from the currently running instance of THIS client. - # Usage ### Basic Example ```c# var client = new XOXOClient("", "") .EnableDebugLogging(true) // Enables Console Log outputs - .UseAutoReconnect(true) // Enables automatic re-login after a connection timeout - .UseQueue() // Enables a queue that reattempts to send messages when the SendLimit is reached - .ThrowExceptionOnInvalidNumberFormat(true); // Configures the client to throw an exception if a phone number format was rejected by Kontomanager + .UseAutoReconnect(true); // Enables automatic re-login after a connection timeout await client.CreateConnection(); -var r = await client.SendMessage("", ""); -``` - -When UseQueue() is called, the response from SendMessage is always **MessageEnqueued**. -To get the actual sending results, the **SendingAttempted** event of the Message class can be used like this. - -```c# -Message m = new Message("", ""); -m.SendingAttempted += (sender, result) => -{ - // result is the sending result enum - // keep in mind that this event can be called multiple times in case Sending fails - // MessageSendResult.Ok is only returned once the message has been successfully sent. -}; -var r = await client.SendMessage(m); +var usage = await client.GetAccountUsage(); +usage.PrintToConsole(); ``` ### 1.2.0 Additions @@ -61,9 +44,7 @@ var r = await client.SendMessage(m); ```c# var client = new XOXOClient("", "") .EnableDebugLogging(true) // Enables Console Log outputs - .UseAutoReconnect(true) // Enables automatic re-login after a connection timeout - .UseQueue() // Enables a queue that reattempts to send messages when the SendLimit is reached - .ThrowExceptionOnInvalidNumberFormat(true); // Configures the client to throw an exception if a phone number format was rejected by Kontomanager + .UseAutoReconnect(true); // Enables automatic re-login after a connection timeout await client.CreateConnection(); @@ -79,20 +60,6 @@ var otherNumberUsage = await client.GetAccountUsage(); // get account usage for otherNumberUsage.PrintToConsole(); ``` - -### Supported Phone Number Formats - -Kontomanager requires phone numbers to either specify an austrian carrier specific number prefix, or specify a number including a country prefix starting in 00. -This client uses the latter case exclusively. Numbers can either be specified as **00** or **+**. - -Valid examples are (in this example: +43 = Austrian Country Code, 0664: Provider Prefix for A1 (0 is omitted if country prefix is used): -- +436641234567 -- 00436641234567 - -Invalid: -- 436641234567 -- 06641234567 - # Similar projects The following projects seem to do the same thing as this client in other languages. However, I did not test any of them. @@ -103,6 +70,11 @@ The following projects seem to do the same thing as this client in other languag # Changelog +### 15.02.2023 2.0.0 +This is a breaking change. Some methods were removed and the constructor was refactored to only require one URL. +- remove no longer supported WebSMS functionality +- add support for new Kontomanager UI + ### 07.06.2022 1.2.7 - added a function to extract the selected phone number from the header