From 9abb5d1bb53b35277aa7d7a5ca88a5e1d282fe1d Mon Sep 17 00:00:00 2001
From: Martin Rotter <rotter.martinos@gmail.com>
Date: Wed, 13 Mar 2024 09:31:09 +0100
Subject: [PATCH] Ical: add some kind of better parsing when date/time of event
 is in specified timezone, it sucks before the iCal SPEC is huge and i just
 cannot implement it all correctly for purpose in rssguard

---
 .../standard/gui/formdiscoverfeeds.cpp        |   6 +-
 .../services/standard/parsers/icalparser.cpp  | 127 +++++++++++++++---
 .../services/standard/parsers/icalparser.h    |  15 ++-
 3 files changed, 121 insertions(+), 27 deletions(-)

diff --git a/src/librssguard/services/standard/gui/formdiscoverfeeds.cpp b/src/librssguard/services/standard/gui/formdiscoverfeeds.cpp
index 8d1ec0676..4f9124f1b 100644
--- a/src/librssguard/services/standard/gui/formdiscoverfeeds.cpp
+++ b/src/librssguard/services/standard/gui/formdiscoverfeeds.cpp
@@ -265,13 +265,15 @@ void FormDiscoverFeeds::loadDiscoveredFeeds(const QList<StandardFeed*>& feeds) {
   RootItem* root = new RootItem();
 
   for (Feed* feed : feeds) {
+    if (feed->title().isEmpty()) {
+      feed->setTitle(tr("No title"));
+    }
+
     root->appendChild(feed);
   }
 
   m_ui.m_pbDiscovery->setVisible(false);
   m_discoveredModel->setRootItem(root);
-
-  qDebugNN << "finish";
 }
 
 DiscoveredFeedsModel::DiscoveredFeedsModel(QObject* parent) : AccountCheckModel(parent) {}
diff --git a/src/librssguard/services/standard/parsers/icalparser.cpp b/src/librssguard/services/standard/parsers/icalparser.cpp
index 8d72f1623..2056c8844 100644
--- a/src/librssguard/services/standard/parsers/icalparser.cpp
+++ b/src/librssguard/services/standard/parsers/icalparser.cpp
@@ -109,7 +109,19 @@ QString IcalParser::objMessageDescription(const QVariant& msg_element) const {
   const IcalendarComponent& comp_base = msg_element.value<IcalendarComponent>();
   const EventComponent& comp = static_cast<const EventComponent&>(comp_base);
 
-  return comp.description();
+  QString body = QSL("Start date/time: %2<br/>"
+                     "End date/time: %3<br/>"
+                     "Location: %4<br/>"
+                     "UID: %5<br/>"
+                     "<br/>"
+                     "%1")
+                   .arg(comp.description(),
+                        QLocale().toString(comp.startsOn(m_iCalendar.m_tzs)),
+                        QLocale().toString(comp.endsOn(m_iCalendar.m_tzs)),
+                        comp.location(),
+                        comp.uid());
+
+  return body;
 }
 
 QString IcalParser::objMessageAuthor(const QVariant& msg_element) const {
@@ -123,7 +135,9 @@ QDateTime IcalParser::objMessageDateCreated(const QVariant& msg_element) const {
   const IcalendarComponent& comp_base = msg_element.value<IcalendarComponent>();
   const EventComponent& comp = static_cast<const EventComponent&>(comp_base);
 
-  return comp.startsOn();
+  QDateTime dat = comp.startsOn(m_iCalendar.m_tzs);
+
+  return dat;
 }
 
 QString IcalParser::objMessageId(const QVariant& msg_element) const {
@@ -162,9 +176,9 @@ void Icalendar::setTitle(const QString& title) {
 }
 
 void Icalendar::processLines(const QString& data) {
-  QRegularExpression regex("^BEGIN:(\\w+)\\r$(.+?)(?=^BEGIN|^END)",
-                           QRegularExpression::PatternOption::MultilineOption |
-                             QRegularExpression::PatternOption::DotMatchesEverythingOption);
+  static QRegularExpression regex("^BEGIN:(\\w+)\\r$(.+?)(?=^BEGIN|^END)",
+                                  QRegularExpression::PatternOption::MultilineOption |
+                                    QRegularExpression::PatternOption::DotMatchesEverythingOption);
 
   auto all_matches = regex.globalMatch(data);
 
@@ -173,10 +187,16 @@ void Icalendar::processLines(const QString& data) {
     QString component = match.captured(1);
     QString body = match.captured(2);
 
+    // Root calendar component.
     if (component == QSL("VCALENDAR")) {
       processComponentCalendar(body);
     }
 
+    if (component == QSL("VTIMEZONE")) {
+      processComponentTimezone(body);
+    }
+
+    // Event component.
     if (component == QSL("VEVENT")) {
       processComponentEvent(body);
     }
@@ -199,8 +219,20 @@ void Icalendar::processComponentEvent(const QString& body) {
   m_components.append(event);
 }
 
+void Icalendar::processComponentTimezone(const QString& body) {
+  auto tokenized = tokenizeBody(body);
+
+  QString tz_id = tokenized.value(QSL("TZID")).toString();
+
+  if (!tz_id.isEmpty()) {
+    m_tzs.insert(tz_id, QTimeZone(tz_id.toLocal8Bit()));
+  }
+}
+
 QVariantMap Icalendar::tokenizeBody(const QString& body) const {
-  QRegularExpression regex("^(?=[A-Z-]+(?:;[A-Z]+=[A-Z]+)?:)", QRegularExpression::PatternOption::MultilineOption);
+  static QRegularExpression regex("^(?=[A-Z-]+(?:;[A-Z-]+=[A-Z-\\/]+)?:)",
+                                  QRegularExpression::PatternOption::MultilineOption |
+                                    QRegularExpression::PatternOption::CaseInsensitiveOption);
   auto all_matches = body.split(regex);
   QVariantMap res;
 
@@ -215,7 +247,9 @@ QVariantMap Icalendar::tokenizeBody(const QString& body) const {
     QString value = match.mid(sep + 1);
 
     value = value.replace(QRegularExpression("\\r\\n\\s?"), QString());
-    value = value.replace(QRegularExpression("\\\\n"), QSL("<br/>"));
+    value = value.replace(QSL("\\n"), QSL("<br/>"));
+    value = value.replace(QSL("\\,"), QSL(","));
+    value = value.replace(QSL("\\;"), QSL(";"));
 
     res.insert(property, value);
   }
@@ -236,6 +270,12 @@ void IcalendarComponent::setProperties(const QVariantMap& properties) {
 }
 
 QVariant IcalendarComponent::getPropertyValue(const QString& property_name) const {
+  QString modifier;
+
+  return getPropertyValue(property_name, modifier);
+}
+
+QVariant IcalendarComponent::getPropertyValue(const QString& property_name, QString& property_modifier) const {
   if (m_properties.contains(property_name)) {
     return m_properties.value(property_name);
   }
@@ -244,45 +284,90 @@ QVariant IcalendarComponent::getPropertyValue(const QString& property_name) cons
   auto linq = boolinq::from(keys.begin(), keys.end());
   QString found_key = linq.firstOrDefault([&](const QString& ky) {
     int index_sep = ky.indexOf(';');
+    bool res = ky.startsWith(property_name) && index_sep == property_name.size();
 
-    return ky.startsWith(property_name) && index_sep == property_name.size();
+    if (res) {
+      property_modifier = ky.mid(index_sep + 1);
+    }
+
+    return res;
   });
 
   return m_properties.value(found_key);
 }
 
-QDateTime EventComponent::startsOn() const {
-  return TextFactory::parseDateTime(getPropertyValue(QSL("DTSTART")).toString());
+QDateTime IcalendarComponent::fixupDate(QDateTime dat,
+                                        const QMap<QString, QTimeZone>& time_zones,
+                                        const QString& modifiers) const {
+  // dat.setTimeSpec(Qt::TimeSpec::LocalTime);
+
+  // auto xx = dat.toUTC().toString();
+
+  QStringList spl = modifiers.split('=');
+
+  if ((dat.time().hour() > 0 || dat.time().minute() > 0 || dat.time().second() > 0) && spl.size() == 2 &&
+      time_zones.contains(spl.at(1))) {
+    QTimeZone tz = time_zones.value(spl.at(1));
+
+    dat.setTimeSpec(Qt::TimeSpec::TimeZone);
+    dat.setTimeZone(tz);
+
+    /*
+    auto yy = dat.toString();
+    auto aa = dat.timeZone().id();
+    auto zz = dat.toUTC().toString();
+*/
+    return dat.toUTC();
+  }
+  else {
+    return dat;
+  }
 }
 
-QDateTime EventComponent::endsOn() const {
-  return TextFactory::parseDateTime(m_properties.value(QSL("DTEND")).toString());
+QDateTime EventComponent::startsOn(const QMap<QString, QTimeZone>& time_zones) const {
+  QString modifiers;
+  QDateTime dat = TextFactory::parseDateTime(getPropertyValue(QSL("DTSTART"), modifiers).toString());
+
+  return fixupDate(dat, time_zones, modifiers);
+}
+
+QDateTime EventComponent::endsOn(const QMap<QString, QTimeZone>& time_zones) const {
+  QString modifiers;
+  QDateTime dat = TextFactory::parseDateTime(getPropertyValue(QSL("DTEND"), modifiers).toString());
+
+  return fixupDate(dat, time_zones, modifiers);
 }
 
 QString EventComponent::title() const {
-  return m_properties.value(QSL("SUMMARY")).toString();
+  return getPropertyValue(QSL("SUMMARY")).toString();
 }
 
 QString EventComponent::url() const {
-  return m_properties.value(QSL("URL")).toString();
+  return getPropertyValue(QSL("URL")).toString();
 }
 
 QString EventComponent::organizer() const {
-  return m_properties.value(QSL("ORGANIZER")).toString();
+  return getPropertyValue(QSL("ORGANIZER")).toString();
 }
 
 QString EventComponent::location() const {
-  return m_properties.value(QSL("LOCATION")).toString();
+  return getPropertyValue(QSL("LOCATION")).toString();
 }
 
 QString EventComponent::description() const {
-  return m_properties.value(QSL("DESCRIPTION")).toString();
+  return getPropertyValue(QSL("DESCRIPTION")).toString();
 }
 
-QDateTime EventComponent::created() const {
-  return TextFactory::parseDateTime(m_properties.value(QSL("CREATED")).toString());
+QDateTime EventComponent::created(const QMap<QString, QTimeZone>& time_zones) const {
+  QString modifiers;
+  QDateTime dat = TextFactory::parseDateTime(getPropertyValue(QSL("CREATED"), modifiers).toString());
+
+  return fixupDate(dat, time_zones, modifiers);
 }
 
-QDateTime EventComponent::lastModified() const {
-  return TextFactory::parseDateTime(m_properties.value(QSL("LAST-MODIFIED")).toString());
+QDateTime EventComponent::lastModified(const QMap<QString, QTimeZone>& time_zones) const {
+  QString modifiers;
+  QDateTime dat = TextFactory::parseDateTime(getPropertyValue(QSL("LAST-MODIFIED"), modifiers).toString());
+
+  return fixupDate(dat, time_zones, modifiers);
 }
diff --git a/src/librssguard/services/standard/parsers/icalparser.h b/src/librssguard/services/standard/parsers/icalparser.h
index 08122f305..91577b51b 100644
--- a/src/librssguard/services/standard/parsers/icalparser.h
+++ b/src/librssguard/services/standard/parsers/icalparser.h
@@ -5,6 +5,8 @@
 
 #include "services/standard/parsers/feedparser.h"
 
+#include <QTimeZone>
+
 class IcalendarComponent {
   public:
     QString uid() const;
@@ -14,6 +16,9 @@ class IcalendarComponent {
 
   protected:
     QVariant getPropertyValue(const QString& property_name) const;
+    QVariant getPropertyValue(const QString& property_name, QString& property_modifier) const;
+
+    QDateTime fixupDate(QDateTime dat, const QMap<QString, QTimeZone>& time_zones, const QString& modifiers) const;
 
     QVariantMap m_properties;
 };
@@ -22,15 +27,15 @@ Q_DECLARE_METATYPE(IcalendarComponent)
 
 class EventComponent : public IcalendarComponent {
   public:
-    QDateTime startsOn() const;
-    QDateTime endsOn() const;
+    QDateTime startsOn(const QMap<QString, QTimeZone>& time_zones = {}) const;
+    QDateTime endsOn(const QMap<QString, QTimeZone>& time_zones = {}) const;
     QString title() const;
     QString url() const;
     QString organizer() const;
     QString location() const;
     QString description() const;
-    QDateTime created() const;
-    QDateTime lastModified() const;
+    QDateTime created(const QMap<QString, QTimeZone>& time_zones = {}) const;
+    QDateTime lastModified(const QMap<QString, QTimeZone>& time_zones = {}) const;
 };
 
 Q_DECLARE_METATYPE(EventComponent)
@@ -48,12 +53,14 @@ class Icalendar : public FeedParser {
     void processLines(const QString& data);
     void processComponentCalendar(const QString& body);
     void processComponentEvent(const QString& body);
+    void processComponentTimezone(const QString& body);
 
     QDateTime parseDateTime(const QString& date_time) const;
     QVariantMap tokenizeBody(const QString& body) const;
 
   private:
     QString m_title;
+    QMap<QString, QTimeZone> m_tzs;
     QList<IcalendarComponent> m_components;
 };