~aleteoryx/muditaos

7dee85f011b50783da4aff0e96b33893c1600e3a — Tomas Rogala 5 years ago e63cb65
[EGD-4711] Change calendar events endpoints

-Create providers object
-Change endpoints data message structure to json object
-Extend ical format validation
-Extend service desktop tests for calendar endpoints
M module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp => module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.cpp +187 -51
@@ 34,29 34,74 @@ namespace parserFSM
            const inline auto two_days_before        = Duration(0, 2, 0, 0);
            const inline auto one_week_before        = Duration(1, 0, 0, 0);
            const inline auto event_time             = Duration(0, 0, 0, 0);
            const inline auto never                  = Duration(0, 0, 0, 0xFFFF);
        } // namespace duration
    }     // namespace ical

    namespace json::calendar::events
    namespace json::calendar
    {
        constexpr inline auto UID         = "UID";
        constexpr inline auto data        = "data";
        constexpr inline auto events = "calendar_events";
        namespace event
        {
            constexpr inline auto vevent = "VEVENT";

            constexpr inline auto uid     = "UID";
            constexpr inline auto summary = "SUMMARY";
            constexpr inline auto dtstart = "DTSTART";
            constexpr inline auto dtend   = "DTEND";

            namespace recurrence_rule
            {
                constexpr inline auto rrule = "RRULE";

                constexpr inline auto frequency = "FREQ";
                constexpr inline auto count     = "COUNT";
                constexpr inline auto interval  = "INTERVAL";
            } // namespace recurrence_rule

            namespace alarm
            {
                constexpr inline auto valarm = "VALARM";

                constexpr inline auto trigger = "TRIGGER";
                constexpr inline auto action  = "ACTION";
            } // namespace alarm

            namespace provider
            {
                constexpr inline auto provider = "provider";

                constexpr inline auto type    = "type";
                constexpr inline auto id      = "id";
                constexpr inline auto iCalUid = "iCalUid";
            } // namespace provider
        }     // namespace event
        constexpr inline auto offset      = "offset";
        constexpr inline auto limit       = "limit";
        constexpr inline auto count       = "count";
        constexpr inline auto total_count = "total_count";

        constexpr inline auto providers = "providers";
        namespace provider
        {
            constexpr inline auto type    = "provider_type";
            constexpr inline auto id      = "provider_id";
            constexpr inline auto iCalUid = "provider_iCalUid";
        } // namespace provider
    }     // namespace json::calendar::events
    } // namespace json::calendar
} // namespace parserFSM
using namespace parserFSM;

auto CalendarEventsHelper::isICalEventValid(ICalEvent icalEvent) const -> bool
{
    if (!icalEvent.event.isValid) {
        LOG_ERROR("Ical event invalid!");
        return false;
    }
    if (!icalEvent.alarm.isValid) {
        LOG_ERROR("Ical alarm invalid!");
        return false;
    }
    if (!icalEvent.rrule.isValid) {
        LOG_ERROR("Ical recurrence rule invalid!");
        return false;
    }
    return true;
}

auto CalendarEventsHelper::frequencyFromCustomRepeat(Repeat repeat) const -> Frequency
{
    return Frequency(static_cast<uint32_t>(repeat));


@@ 101,43 146,44 @@ auto CalendarEventsHelper::alarmFrom(Reminder reminder) const -> Alarm
{
    switch (reminder) {
    case Reminder::never: {
        return Alarm();
        auto beforeEvent = ical::duration::never;
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::five_min_before: {
        auto beforeEvent = ical::duration::five_minutes_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::fifteen_min_before: {
        auto beforeEvent = ical::duration::fifteen_minutes_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::thirty_min_before: {
        auto beforeEvent = ical::duration::thirty_minutes_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::one_hour_before: {
        auto beforeEvent = ical::duration::one_hour_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::two_hour_before: {
        auto beforeEvent = ical::duration::two_hours_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::one_day_before: {
        auto beforeEvent = ical::duration::one_day_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::two_days_before: {
        auto beforeEvent = ical::duration::two_days_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::one_week_before: {
        auto beforeEvent = ical::duration::one_week_before;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    case Reminder::event_time: {
        auto beforeEvent = ical::duration::event_time;
        return Alarm(beforeEvent, Action::display);
        return Alarm(beforeEvent, Action::none);
    }
    }
    return Alarm();


@@ 152,11 198,43 @@ auto CalendarEventsHelper::icalEventFrom(const EventsRecord &record) const -> IC
    return ICalEvent{event, alarm, rrule};
}

auto CalendarEventsHelper::eventJsonObjectFrom(EventsRecord record) const -> json11::Json
{
    auto icalEvent = icalEventFrom(record);
    if (!isICalEventValid(icalEvent)) {
        LOG_ERROR("Bad event record formatting  (Event UID: %s)", icalEvent.event.getUID().c_str());
    }

    auto rruleObj = json11::Json::object{
        {json::calendar::event::recurrence_rule::frequency, icalEvent.rrule.getFrequencyString().c_str()},
        {json::calendar::event::recurrence_rule::count, icalEvent.rrule.getCountString().c_str()},
        {json::calendar::event::recurrence_rule::interval, icalEvent.rrule.getIntervalString().c_str()}};

    auto alarmObj =
        json11::Json::object{{json::calendar::event::alarm::trigger, icalEvent.alarm.getTriggerString().c_str()},
                             {json::calendar::event::alarm::action, icalEvent.alarm.getActionString().c_str()}};

    auto providerObj =
        json11::Json::object{{json::calendar::event::provider::type, record.provider_type.c_str()},
                             {json::calendar::event::provider::id, record.provider_id.c_str()},
                             {json::calendar::event::provider::iCalUid, record.provider_iCalUid.c_str()}};

    auto eventObj = json11::Json::object{{json::calendar::event::dtend, icalEvent.event.getDTEndString().c_str()},
                                         {json::calendar::event::dtstart, icalEvent.event.getDTStartString().c_str()},
                                         {json::calendar::event::summary, icalEvent.event.getSummary().c_str()},
                                         {json::calendar::event::uid, icalEvent.event.getUID().c_str()},
                                         {json::calendar::event::recurrence_rule::rrule, rruleObj},
                                         {json::calendar::event::alarm::valarm, alarmObj},
                                         {json::calendar::event::provider::provider, providerObj}};

    return eventObj;
}

auto CalendarEventsHelper::requestDataFromDB(Context &context) -> sys::ReturnCodes
{
    auto obj        = context.getBody();
    uint32_t offset = obj[json::calendar::events::offset].int_value();
    uint32_t limit  = obj[json::calendar::events::limit].int_value();
    const auto obj  = context.getBody();
    uint32_t offset = obj[json::calendar::offset].int_value();
    uint32_t limit  = obj[json::calendar::limit].int_value();
    auto query      = std::make_unique<db::query::events::GetAllLimited>(offset, limit);

    auto listener = std::make_unique<db::EndpointListener>(


@@ 166,17 244,22 @@ auto CalendarEventsHelper::requestDataFromDB(Context &context) -> sys::ReturnCod
                uint32_t totalCount = EventsResult->getCountResult();
                auto parser         = std::make_unique<ParserICS>();
                std::vector<ICalEvent> icalEvents;

                auto eventsArray = json11::Json::array();

                for (auto rec : records) {
                    icalEvents.push_back(icalEventFrom(rec));
                    auto eventObject = eventJsonObjectFrom(rec);

                    eventsArray.emplace_back(eventObject);
                }
                parser->importEvents(icalEvents);
                auto jsonObj =
                    json11::Json::object({{json::calendar::events::data, parser->getIcsData()},
                                          {json::calendar::events::count, std::to_string(records.size())},
                                          {json::calendar::events::total_count, std::to_string(totalCount)}});

                auto jsonObj = json11::Json::object({{json::calendar::events, eventsArray},
                                                     {json::calendar::count, std::to_string(records.size())},
                                                     {json::calendar::total_count, std::to_string(totalCount)}});

                context.setResponseBody(jsonObj);
                MessageHandler::putToSendQueue(context.createSimpleResponse());

                return true;
            }
            return false;


@@ 224,8 307,7 @@ auto CalendarEventsHelper::repeatFrom(RecurrenceRule &rrule) const -> Repeat
    case Frequency::yearly: {
        return Repeat::yearly;
    }
    case Frequency::invalid: {
        LOG_ERROR("Frequency invalid");
    case Frequency::never: {
        return Repeat::never;
    }
    }


@@ 249,26 331,67 @@ auto CalendarEventsHelper::eventsRecordFrom(ICalEvent &icalEvent) const -> Event
    return record;
}

auto CalendarEventsHelper::ICalEventFromJson(json11::Json eventObj) const -> ICalEvent
{
    ICalEvent icalEvent;
    icalEvent.event.setUID(eventObj[json::calendar::event::uid].string_value());
    icalEvent.event.setSummary(eventObj[json::calendar::event::summary].string_value());
    icalEvent.event.setDTStart(eventObj[json::calendar::event::dtstart].string_value());
    icalEvent.event.setDTEnd(eventObj[json::calendar::event::dtend].string_value());

    icalEvent.rrule.setFrequency(
        eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::frequency]
            .string_value());
    icalEvent.rrule.setCount(
        eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::count]
            .string_value());
    icalEvent.rrule.setInterval(
        eventObj[json::calendar::event::recurrence_rule::rrule][json::calendar::event::recurrence_rule::interval]
            .string_value());

    icalEvent.alarm.setTrigger(
        eventObj[json::calendar::event::alarm::valarm][json::calendar::event::alarm::trigger].string_value());
    icalEvent.alarm.setAction(
        eventObj[json::calendar::event::alarm::valarm][json::calendar::event::alarm::action].string_value());

    auto record = eventsRecordFrom(icalEvent);

    return icalEvent;
}

auto CalendarEventsHelper::createDBEntry(Context &context) -> sys::ReturnCodes
{
    auto parser = std::make_shared<ParserICS>();
    parser->loadData(context.getBody()[json::calendar::events::data].string_value());
    auto icalEvents = parser->exportEvents();
    auto eventsJsonObj   = context.getBody();
    auto eventsJsonArray = eventsJsonObj[json::calendar::events].array_items();
    bool ret             = true;
    for (auto event : eventsJsonArray) {

    bool ret = true;
    for (auto event : icalEvents) {
        auto UID    = event.event.getUID();
        auto record = eventsRecordFrom(event);
        auto query  = std::make_unique<db::query::events::Add>(record);
        auto icalEvent = ICalEventFromJson(event);

        if (!isICalEventValid(icalEvent)) {
            context.setResponseStatus(http::Code::BadRequest);
            MessageHandler::putToSendQueue(context.createSimpleResponse());
            return sys::ReturnCodes::Failure;
        }

        auto record = eventsRecordFrom(icalEvent);
        if (record.UID.empty()) {
            record.UID   = createUID();
            auto jsonObj = json11::Json::object({{json::calendar::event::uid, record.UID}});
            context.setResponseBody(jsonObj);
        }
        else {
            LOG_ERROR("UID should not be recieved in put event endpoint. Recieved UID: %s", record.UID.c_str());
            context.setResponseStatus(http::Code::BadRequest);
            MessageHandler::putToSendQueue(context.createSimpleResponse());
            return sys::ReturnCodes::Failure;
        }

        auto query    = std::make_unique<db::query::events::Add>(record);
        auto listener = std::make_unique<db::EndpointListener>(
            [=](db::QueryResult *result, Context context) {
            [&](db::QueryResult *result, Context context) {
                if (auto EventResult = dynamic_cast<db::query::events::AddResult *>(result)) {

                    auto jsonObj = json11::Json::object(
                        {{json::calendar::events::data, parser->getIcsData()}, {json::calendar::events::UID, UID}});

                    context.setResponseBody(jsonObj);
                    context.setResponseStatus(EventResult->getResult() ? http::Code::OK
                                                                       : http::Code::InternalServerError);



@@ 295,14 418,19 @@ auto CalendarEventsHelper::createDBEntry(Context &context) -> sys::ReturnCodes

auto CalendarEventsHelper::updateDBEntry(Context &context) -> sys::ReturnCodes
{
    auto parser = std::make_unique<ParserICS>();
    parser->loadData(context.getBody()[json::calendar::events::data].string_value());
    auto icalEvents = parser->exportEvents();
    auto eventsJsonObj = context.getBody();

    bool ret = true;
    for (auto event : icalEvents) {
    for (auto event : eventsJsonObj[json::calendar::events].array_items()) {

        auto record   = eventsRecordFrom(event);
        auto icalEvent = ICalEventFromJson(event);
        if (!isICalEventValid(icalEvent) || icalEvent.event.getUID().empty()) {
            context.setResponseStatus(http::Code::BadRequest);
            MessageHandler::putToSendQueue(context.createSimpleResponse());
            return sys::ReturnCodes::Failure;
        }

        auto record   = eventsRecordFrom(icalEvent);
        auto query    = std::make_unique<db::query::events::EditICS>(record);
        auto listener = std::make_unique<db::EndpointListener>(
            [](db::QueryResult *result, Context context) {


@@ 331,7 459,15 @@ auto CalendarEventsHelper::updateDBEntry(Context &context) -> sys::ReturnCodes

auto CalendarEventsHelper::deleteDBEntry(Context &context) -> sys::ReturnCodes
{
    auto UID      = context.getBody()[json::calendar::events::UID].string_value();
    auto UID      = context.getBody()[json::calendar::event::uid].string_value();
    auto checkUID = Event();
    checkUID.setUID(UID);
    if (!checkUID.isValid) {
        LOG_ERROR("Wrong UID format. Provided UID: %s", UID.c_str());
        context.setResponseStatus(http::Code::BadRequest);
        MessageHandler::putToSendQueue(context.createSimpleResponse());
        return sys::ReturnCodes::Failure;
    }
    auto query    = std::make_unique<db::query::events::RemoveICS>(UID);
    auto listener = std::make_unique<db::EndpointListener>(
        [=](db::QueryResult *result, Context context) {

M module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp => module-services/service-desktop/endpoints/calendarEvents/CalendarEventsHelper.hpp +4 -0
@@ 29,6 29,10 @@ namespace parserFSM
        [[nodiscard]] auto repeatFrom(RecurrenceRule &rrule) const -> Repeat;
        [[nodiscard]] auto eventsRecordFrom(ICalEvent &icalEvent) const -> EventsRecord;

        [[nodiscard]] auto eventJsonObjectFrom(EventsRecord record) const -> json11::Json;
        [[nodiscard]] auto ICalEventFromJson(json11::Json eventObj) const -> ICalEvent;
        [[nodiscard]] auto isICalEventValid(ICalEvent event) const -> bool;

      public:
        CalendarEventsHelper(sys::Service *_ownerServicePtr) : DBHelper(_ownerServicePtr)
        {}

M module-utils/ical/ParserICS.cpp => module-utils/ical/ParserICS.cpp +211 -38
@@ 2,7 2,8 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ParserICS.hpp"
//#include <exception>
#include <cmath>
#include <module-utils/time/time_date_validation.hpp>

namespace ical
{


@@ 72,6 73,8 @@ namespace ical
        } // namespace frequency
        constexpr inline auto count    = "COUNT=";
        constexpr inline auto interval = "INTERVAL=";

        constexpr inline auto max_interval = 2;
    } // namespace rrule

    namespace duration


@@ 79,22 82,44 @@ namespace ical
        constexpr inline auto minutesInHour = 60;
        constexpr inline auto minutesInDay  = 24 * minutesInHour;
        constexpr inline auto minutesInWeek = 7 * minutesInDay;

        namespace minutes
        {
            constexpr inline auto never_happens_value = 0xFFFF;
        }
    } // namespace duration
    constexpr inline auto date_char_length = yearDigitsNumb + monthDigitsNumb + dayDigitsNumb;
    constexpr inline auto time_char_length = HourDigitsNumb + MinDigitsNumb + SecDigitsNumb;
    constexpr inline auto dt_char_length   = date_char_length + 1 + time_char_length;
} // namespace ical

Duration::Duration(uint32_t week, uint32_t day, uint32_t hour, uint32_t minute)
{
    if (minute == ical::duration::minutes::never_happens_value) {
        this->minute = ical::duration::minutes::never_happens_value;
        return;
    }
    this->week   = week;
    this->day    = day;
    this->hour   = hour;
    this->minute = minute;

    if (!utils::time::validateTime(hour, minute, false)) {
        isValid = false;
    }
}

Duration::Duration(const std::string &property)
{
    if (property.empty()) {
        LOG_DEBUG("Duration is empty. Event with no Alarm!");
        this->minute = ical::duration::minutes::never_happens_value;
        return;
    }
    uint32_t i = 0;
    if (property.empty() || (property[0] != 'P' && property[1] != 'P')) {
    if ((property[0] != 'P' && property[1] != 'P')) {
        LOG_ERROR("Duration constructor: Invalid format provided: %s", property.c_str());
        isValid = false;
        return;
    }
    while (property[i] != '\0' && i < property.length()) {


@@ 118,14 143,24 @@ Duration::Duration(const std::string &property)
                case 'M':
                    this->minute = stoi(value);
                    break;
                default:
                    LOG_ERROR("Wrong duration unit value format");
                    isValid = false;
                    break;
                }
            }
            catch (std::exception &e) {
                LOG_DEBUG("Duration conversion from string to int failed with exception:%s", e.what());
                isValid = false;
            }
        }
        ++i;
    }

    if (!utils::time::validateTime(hour, minute, false)) {
        LOG_ERROR("Duration time value is invalid");
        isValid = false;
    }
}

auto Duration::getDurationInMinutes() const -> uint32_t


@@ 140,20 175,23 @@ auto Duration::getDurationInMinutes() const -> uint32_t
auto Duration::getDurationString() const -> std::string
{
    std::string durationString;
    if (minute == ical::duration::minutes::never_happens_value) {
        return durationString;
    }
    if (week) {
        durationString += std::to_string(week) + 'W';
        durationString += utils::to_string(week) + 'W';
    }
    if (day) {
        durationString += std::to_string(day) + 'D';
        durationString += utils::to_string(day) + 'D';
    }
    if (hour || minute) {
        durationString += 'T';
    }
    if (hour) {
        durationString += std::to_string(hour) + 'H';
        durationString += utils::to_string(hour) + 'H';
    }
    if (minute) {
        durationString += std::to_string(minute) + 'M';
        durationString += utils::to_string(minute) + 'M';
    }
    if (week == 0 && day == 0 && hour == 0 && minute == 0) {
        durationString += "T0M";


@@ 161,13 199,14 @@ auto Duration::getDurationString() const -> std::string
    return durationString;
}

Alarm::Alarm(Duration &beforeEvent, Action action)
Alarm::Alarm(Duration &timeBeforeEvent, Action action)
{
    if (beforeEvent.getDurationString().empty()) {
        LOG_ERROR("Got empty duration value");
    if (!timeBeforeEvent.isValid) {
        LOG_ERROR("Duration format is invalid");
        isValid = false;
        return;
    }
    this->trigger = beforeEvent;
    this->trigger = timeBeforeEvent;
    this->action  = action;
}



@@ 182,14 221,25 @@ void Alarm::setAction(const std::string &action)
    else if (action == ical::alarm::value::action::audio) {
        this->action = Action::audio;
    }
    else if (action.empty()) {
        LOG_DEBUG("Alarm with no action");
        this->action = Action::none;
    }
    else {
        this->action = Action::invalid;
        LOG_ERROR("Alarm action invalid");
        isValid = false;
    }
}

void Alarm::setTrigger(const std::string &duration)
{
    this->trigger = Duration(duration);
    auto timeBeforeEvent = Duration(duration);
    if (!timeBeforeEvent.isValid) {
        LOG_ERROR("Duration format is invalid");
        isValid = false;
        return;
    }
    this->trigger = timeBeforeEvent;
}

auto Alarm::getTriggerValue() const -> Duration


@@ 204,7 254,11 @@ auto Alarm::getActionValue() const -> Action

auto Alarm::getTriggerString() const -> std::string
{
    return ical::alarm::value::before + trigger.getDurationString();
    auto durationString = trigger.getDurationString();
    if (durationString.empty()) {
        return durationString;
    }
    return ical::alarm::value::before + durationString;
}

auto Alarm::getActionString() const -> std::string


@@ 220,8 274,8 @@ auto Alarm::getActionString() const -> std::string
    case Action::procedure: {
        return ical::alarm::value::action::procedure;
    }
    case Action::invalid: {
        LOG_ERROR("Alarm with no action");
    case Action::none: {
        LOG_DEBUG("Alarm with no action");
        return "";
    }
    }


@@ 230,11 284,15 @@ auto Alarm::getActionString() const -> std::string

RecurrenceRule::RecurrenceRule(Frequency freq, uint32_t count, uint32_t interval)
{
    if (count == 0 || interval == 0) {
        LOG_ERROR("Invalid count or interval value!");
    if ((count == 0 || interval == 0) && freq != Frequency::never) {
        LOG_ERROR("Invalid rrule format!");
        isValid = false;
        return;
    }

    if (interval > ical::rrule::max_interval) {
        LOG_ERROR("Invalid interval value!");
        isValid = false;
    }
    this->frequency = freq;
    this->count     = count;
    this->interval  = interval;


@@ 254,9 312,12 @@ void RecurrenceRule::setFrequency(const std::string &property)
    else if (property == ical::rrule::frequency::yearly) {
        this->frequency = Frequency::yearly;
    }
    else if (property.empty()) {
        this->frequency = Frequency::never;
    }
    else {
        LOG_ERROR("Invalid frequency!");
        this->frequency = Frequency::invalid;
        isValid = false;
    }
}
void RecurrenceRule::setCount(const std::string &property)


@@ 265,7 326,8 @@ void RecurrenceRule::setCount(const std::string &property)
        this->count = stoi(property);
    }
    catch (...) {
        LOG_ERROR("Count value not conversionable to int!");
        LOG_ERROR("Count value is not an integer!");
        isValid = false;
    }
}



@@ 276,6 338,7 @@ void RecurrenceRule::setInterval(const std::string &property)
    }
    catch (...) {
        LOG_ERROR("Interval value not conversionable to int!");
        isValid = false;
    }
}



@@ 314,8 377,8 @@ auto RecurrenceRule::getFrequencyString() const -> std::string
        frequencyStr = ical::rrule::frequency::yearly;
        break;
    }
    case Frequency::invalid: {
        LOG_ERROR("Frequency invalid");
    case Frequency::never: {
        LOG_DEBUG("Frequency never");
        return "";
    }
    }


@@ 324,21 387,12 @@ auto RecurrenceRule::getFrequencyString() const -> std::string

auto RecurrenceRule::getCountString() const -> std::string
{
    if (this->frequency == Frequency::invalid) {
        LOG_ERROR("Frequency value is invalid!");
        return "";
    }
    return std::to_string(this->count);
    return utils::to_string(this->count);
}

auto RecurrenceRule::getIntervalString() const -> std::string
{
    if (this->frequency == Frequency::invalid) {
        LOG_ERROR("Frequency value is invalid!");
        return "";
    }

    return std::to_string(this->interval);
    return utils::to_string(this->interval);
}

auto Event::getDateFromIcalFormat(const std::string &icalDateTime) const -> std::string


@@ 346,21 400,50 @@ auto Event::getDateFromIcalFormat(const std::string &icalDateTime) const -> std:
    return icalDateTime.substr(0, icalDateTime.find_first_of('T'));
}

auto Event::getYearFromIcalDate(const std::string &icalDate) const -> std::string
{
    return icalDate.substr(0, yearDigitsNumb);
}

auto Event::getMonthFromIcalDate(const std::string &icalDate) const -> std::string
{
    return icalDate.substr(yearDigitsNumb, monthDigitsNumb);
}

auto Event::getDayFromIcalDate(const std::string &icalDate) const -> std::string
{
    return icalDate.substr(yearDigitsNumb + monthDigitsNumb, dayDigitsNumb);
}

auto Event::getTimeFromIcalFormat(const std::string &icalDateTime) const -> std::string
{
    return icalDateTime.substr(icalDateTime.find_first_of('T') + 1);
}

auto Event::getHourFromIcalTime(const std::string &icalTime) const -> std::string
{
    return icalTime.substr(0, HourDigitsNumb);
}

auto Event::getMinutesFromIcalTime(const std::string &icalTime) const -> std::string
{
    return icalTime.substr(HourDigitsNumb, MinDigitsNumb);
}

auto Event::getSecondsFromIcalTime(const std::string &icalTime) const -> std::string
{
    return icalTime.substr(HourDigitsNumb + MinDigitsNumb, SecDigitsNumb);
}

auto Event::dateStringFrom(const std::string &icalDate) const -> std::string
{
    return icalDate.substr(0, yearDigitsNumb) + "-" + icalDate.substr(yearDigitsNumb, monthDigitsNumb) + "-" +
           icalDate.substr(yearDigitsNumb + monthDigitsNumb, dayDigitsNumb);
    return getYearFromIcalDate(icalDate) + "-" + getMonthFromIcalDate(icalDate) + "-" + getDayFromIcalDate(icalDate);
}

auto Event::timeStringFrom(const std::string &icalTime) const -> std::string
{
    return icalTime.substr(0, HourDigitsNumb) + ":" + icalTime.substr(HourDigitsNumb, MinDigitsNumb) + ":" +
           icalTime.substr(HourDigitsNumb + MinDigitsNumb, SecDigitsNumb);
    return getHourFromIcalTime(icalTime) + ":" + getMinutesFromIcalTime(icalTime) + ":" +
           getSecondsFromIcalTime(icalTime);
}

auto Event::TimePointFromIcalDate(const std::string &icalDateTime) const -> TimePoint


@@ 387,28 470,118 @@ auto Event::TimePointToIcalDate(const TimePoint &tp) const -> std::string
    return IcalDate;
}

auto Event::isDate(const std::string &date) -> bool
{
    return utils::time::validateDate(getDayFromIcalDate(date), getMonthFromIcalDate(date), getYearFromIcalDate(date));
}

auto Event::isTime(const std::string &time) -> bool
{
    [[maybe_unused]] uint32_t seconds;
    try {
        seconds = stoi(getSecondsFromIcalTime(time));
    }
    catch (...) {
        LOG_ERROR("Seconds value is not an integer!");
        return false;
    }
    return utils::time::validateTime(getHourFromIcalTime(time), getMinutesFromIcalTime(time), false);
}

auto Event::validateDT(const std::string &dt) -> bool
{
    uint32_t dateTimeSize;
    if (dt.find_first_of('Z') == ical::date_char_length + ical::time_char_length) {
        dateTimeSize = ical::dt_char_length + 1;
    }
    else {
        dateTimeSize = ical::dt_char_length;
    }
    if (dt.size() != dateTimeSize) {
        LOG_ERROR("Date time length is invalid");
        return false;
    }

    auto separatorIndex = dt.find_first_of('T');
    LOG_DEBUG("Separator index = %d", (int)separatorIndex);
    if (separatorIndex != (yearDigitsNumb + monthDigitsNumb + dayDigitsNumb)) {
        LOG_ERROR("Date time separator is invalid");
        return false;
    }

    auto date = getDateFromIcalFormat(dt);
    auto time = getTimeFromIcalFormat(dt);

    if (!isDate(date)) {
        LOG_ERROR("Date is invalid");
        return false;
    }

    if (!isTime(time)) {
        LOG_ERROR("Time is invalid");
        return false;
    }

    return true;
}

auto Event::validateUID(const std::string &UID) -> bool
{
    auto DTimestamp = UID.substr(0, UID.find_first_of('-'));
    auto id         = UID.substr(UID.find_first_of('-') + 1);

    try {
        stoi(id);
    }
    catch (...) {
        LOG_ERROR("UID value is not an integer!");
        return false;
    }

    return validateDT(DTimestamp);
}

Event::Event(const std::string &summary, const TimePoint from, TimePoint till, const std::string &uid)
{
    if (summary.empty()) {
        isValid = false;
    }
    if (!validateUID(uid) && !uid.empty()) {
        isValid = false;
    }
    this->uid     = uid;
    this->summary = summary;
    this->dtstart = from;
    this->dtend   = till;
    this->uid     = uid;
}

void Event::setUID(const std::string &property)
{
    if (!validateUID(property) && !property.empty()) {
        LOG_ERROR("UID invalid format");
        isValid = false;
    }
    this->uid = property;
}
void Event::setSummary(const std::string &property)
{
    if (property.empty()) {
        isValid = false;
    }
    this->summary = property;
}
void Event::setDTStart(const std::string &property)
{
    if (!validateDT(property)) {
        isValid = false;
    }
    this->dtstart = TimePointFromIcalDate(property);
}
void Event::setDTEnd(const std::string &property)
{
    if (!validateDT(property)) {
        isValid = false;
    }
    this->dtend = TimePointFromIcalDate(property);
}


M module-utils/ical/ParserICS.hpp => module-utils/ical/ParserICS.hpp +24 -7
@@ 17,6 17,7 @@ class Duration
    uint32_t week = 0, day = 0, hour = 0, minute = 0;

  public:
    bool isValid = true;
    Duration(uint32_t week = 0, uint32_t day = 0, uint32_t hour = 0, uint32_t minute = 0);
    explicit Duration(const std::string &property);



@@ 26,7 27,7 @@ class Duration

enum class Action
{
    invalid,
    none,
    audio,
    display,
    procedure


@@ 35,11 36,12 @@ enum class Action
class Alarm
{
    Duration trigger;
    Action action;
    Action action = Action::none;

  public:
    Alarm() = default;
    explicit Alarm(Duration &beforeEvent, Action action = Action::invalid);
    bool isValid = true;
    Alarm()      = default;
    explicit Alarm(Duration &beforeEvent, Action action = Action::none);

    void setAction(const std::string &action);
    void setTrigger(const std::string &duration);


@@ 53,7 55,7 @@ class Alarm

enum class Frequency
{
    invalid,
    never,
    daily,
    weekly,
    monthly,


@@ 63,11 65,12 @@ enum class Frequency

class RecurrenceRule
{
    Frequency frequency = Frequency::invalid;
    Frequency frequency = Frequency::never;
    uint32_t count      = 0;
    uint32_t interval   = 0;

  public:
    bool isValid     = true;
    RecurrenceRule() = default;
    RecurrenceRule(Frequency freq, uint32_t count, uint32_t interval);



@@ 91,8 94,21 @@ class Event
    TimePoint dtstart;
    TimePoint dtend;

    auto isDate(const std::string &dt) -> bool;
    auto isTime(const std::string &dt) -> bool;
    auto validateDT(const std::string &dt) -> bool;
    auto validateUID(const std::string &uid) -> bool;

    [[nodiscard]] auto getDateFromIcalFormat(const std::string &icalDateTime) const -> std::string;
    [[nodiscard]] auto getYearFromIcalDate(const std::string &icalDate) const -> std::string;
    [[nodiscard]] auto getMonthFromIcalDate(const std::string &icalDate) const -> std::string;
    [[nodiscard]] auto getDayFromIcalDate(const std::string &icalDate) const -> std::string;

    [[nodiscard]] auto getTimeFromIcalFormat(const std::string &icalDateTime) const -> std::string;
    [[nodiscard]] auto getHourFromIcalTime(const std::string &icalTime) const -> std::string;
    [[nodiscard]] auto getMinutesFromIcalTime(const std::string &icalTime) const -> std::string;
    [[nodiscard]] auto getSecondsFromIcalTime(const std::string &icalTime) const -> std::string;

    [[nodiscard]] auto dateStringFrom(const std::string &icalDate) const -> std::string;
    [[nodiscard]] auto timeStringFrom(const std::string &icalTime) const -> std::string;



@@ 100,7 116,8 @@ class Event
    [[nodiscard]] auto TimePointToIcalDate(const TimePoint &tp) const -> std::string;

  public:
    Event() = default;
    bool isValid = true;
    Event()      = default;
    Event(const std::string &summary, TimePoint from, TimePoint till, const std::string &uid);

    void setUID(const std::string &property);

M test/pytest/service-desktop/test_calendar.py => test/pytest/service-desktop/test_calendar.py +243 -31
@@ 2,41 2,103 @@
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
import pytest
from harness.interface.defs import status
import copy


@pytest.mark.skip("ICS format bug")
@pytest.mark.service_desktop_test
def test_calendar(harness):
    # add event
    event = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nSUMMARY:Testowy\nDTSTART:20200929T123611\nDTEND:20200929T124611\nBEGIN" \
            ":VALARM\nTRIGGER:-P5M\nEND:VALARM\nEND:VEVENT\nEND:VCALENDAR\n "
    # add events
    add_body = {
        "calendar_events":[
            {
                "UID":"",
                "SUMMARY":"Testowy",
                "DTSTART":"20200129T123600",
                "DTEND":"20200129T124600",
                "RRULE":{
                    "COUNT":"0",
                    "FREQ":"",
                    "INTERVAL":"0"
                },
                "VALARM":{
                    "ACTION":"",
                    "TRIGGER":""
                },
                "provider":{
                    "iCalUid":"",
                    "id":"",
                    "type":""
                }
            }
        ]
    }

    ret = harness.endpoint_request("events", "put", add_body)
    assert ret["status"] == status["OK"]

    body = {"data": event}
    ret = harness.endpoint_request("events", "put", body)
    #add second event
    second_event = copy.deepcopy(add_body)
    second_event["calendar_events"][0]["SUMMARY"] = "Testowy2"
    ret = harness.endpoint_request("events", "put", second_event)
    assert ret["status"] == status["OK"]

    # get all limited events
    body = {"offset": 0, "limit": 1}
    ret = harness.endpoint_request("events", "get", body)
    get_body = {"offset": 0, "limit": 1}
    ret = harness.endpoint_request("events", "get", get_body)

    assert ret["status"] == status["OK"]
    assert ret["body"]["data"] == event
    assert ret["body"]["count"] == 1
    assert ret["body"]["count"] == '1'
    assert ret["body"]["total_count"] == '2'


    uid_index = ret["body"]["data"].index('UID:')
    summary_index = ret["body"]["data"].index('SUMMARY:')
    UID = ret["body"]["data"][uid_index + 4:summary_index]
    for event in ret["body"]["calendar_events"]:
        assert event["SUMMARY"] == add_body["calendar_events"][0]["SUMMARY"]
        assert event["RRULE"]["COUNT"] == add_body["calendar_events"][0]["RRULE"]["COUNT"]
        assert event["RRULE"]["FREQ"] == add_body["calendar_events"][0]["RRULE"]["FREQ"]
        assert event["RRULE"]["INTERVAL"] == add_body["calendar_events"][0]["RRULE"]["INTERVAL"]
        assert event["VALARM"]["ACTION"] == add_body["calendar_events"][0]["VALARM"]["ACTION"]
        assert event["VALARM"]["TRIGGER"] == add_body["calendar_events"][0]["VALARM"]["TRIGGER"]
        assert event["provider"]["iCalUid"] == add_body["calendar_events"][0]["provider"]["iCalUid"]
        assert event["provider"]["id"] == add_body["calendar_events"][0]["provider"]["id"]
        assert event["provider"]["type"] == add_body["calendar_events"][0]["provider"]["type"]

    assert "SUMMARY:Testowy" in ret["body"]["data"]
    assert "DTSTART:20200929T123611" in ret["body"]["data"]
    assert "DTEND:20200929T124611" in ret["body"]["data"]
    # remove event
    del_body = {"UID": ret["body"]["calendar_events"][0]["UID"]}
    ret = harness.endpoint_request("events", "del", del_body)
    assert ret["status"] == status["OK"]

    # check events after remove
    body = {"offset": 0, "limit": 1}
    ret = harness.endpoint_request("events", "get", body)
    assert ret["status"] == status["OK"]
    assert ret["body"]["count"] == "1"

    # update event
    event_update = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:" + UID + "\n" + "SUMMARY:Update\nDTSTART:20200928T123611" \
                                                                        "\nDTEND:20200928T124611\nEND:VEVENT\nEND" \
                                                                        ":VCALENDAR\n "
    body = {"data": event_update}
    ret = harness.endpoint_request("events", "post", body)
    update_body = {
        "calendar_events":[
            {
                "UID":ret["body"]["calendar_events"][0]["UID"],
                "SUMMARY":"TestowyUpdate",
                "DTSTART":"20200929T123600",
                "DTEND":"20200929T124600",
                "RRULE":{
                    "COUNT":"0",
                    "FREQ":"",
                    "INTERVAL":"0"
                },
                "VALARM":{
                    "ACTION":"",
                    "TRIGGER":"-PT5M"
                },
                "provider":{
                    "iCalUid":"",
                    "id":"",
                    "type":""
                }
            }
        ]
    }

    ret = harness.endpoint_request("events", "post", update_body)
    assert ret["status"] == status["OK"]

    # get updated event


@@ 44,20 106,170 @@ def test_calendar(harness):
    ret = harness.endpoint_request("events", "get", body)
    assert ret["status"] == status["OK"]

    assert ret["body"]["data"] == event_update
    assert ret["body"]["count"] == 1
    for event in ret["body"]["calendar_events"]:
        assert event["SUMMARY"] == update_body["calendar_events"][0]["SUMMARY"]
        assert event["RRULE"]["COUNT"] == update_body["calendar_events"][0]["RRULE"]["COUNT"]
        assert event["RRULE"]["FREQ"] == update_body["calendar_events"][0]["RRULE"]["FREQ"]
        assert event["RRULE"]["INTERVAL"] == update_body["calendar_events"][0]["RRULE"]["INTERVAL"]
        assert event["VALARM"]["ACTION"] == update_body["calendar_events"][0]["VALARM"]["ACTION"]
        assert event["VALARM"]["TRIGGER"] == update_body["calendar_events"][0]["VALARM"]["TRIGGER"]
        assert event["provider"]["iCalUid"] == update_body["calendar_events"][0]["provider"]["iCalUid"]
        assert event["provider"]["id"] == update_body["calendar_events"][0]["provider"]["id"]
        assert event["provider"]["type"] == update_body["calendar_events"][0]["provider"]["type"]

    assert "SUMMARY:Update" in ret["body"]["data"]
    assert "DTSTART:20200928T123611" in ret["body"]["data"]
    assert "DTEND:20200928T124611" in ret["body"]["data"]
    assert ret["body"]["count"] == "1"

    # remove event
    body = {"UID": UID}
    ret = harness.endpoint_request("events", "del", body)


    # remove second event
    body = {"offset": 0, "limit": 1}
    ret = harness.endpoint_request("events", "get", body)
    assert ret["status"] == status["OK"]

    del_body = {"UID": ret["body"]["calendar_events"][0]["UID"]}
    ret = harness.endpoint_request("events", "del", del_body)
    assert ret["status"] == status["OK"]

    # check events after remove
    body = {"data": "", "count": "1"}
    body = {"offset": 0, "limit": 1}
    ret = harness.endpoint_request("events", "get", body)
    assert ret["status"] == status["OK"]
    assert ret["body"]["count"] == "0"



    # INVALID FORMAT
    add_body_invalid = copy.deepcopy(add_body)
    update_body_invalid = copy.deepcopy(update_body)

    # add events invalid date and time formats
    # wrong month
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201329T123600"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong day
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201132T123600"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong year
    add_body_invalid["calendar_events"][0]["DTEND"] = "50201120T123600"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong hour
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201120T253600"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong minute
    add_body_invalid["calendar_events"][0]["DTEND"] = "20201122T126300"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong second
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122T1200OO"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    #wrong separator
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122R120059"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # wrong length
    add_body_invalid["calendar_events"][0]["DTSTART"] = "20201122T12550000"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    add_body_invalid["calendar_events"][0]["DTEND"] = "201122T125500"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # Summary invalid
    add_body_invalid["calendar_events"][0]["SUMMARY"] = ""
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]

    # Alarm invalid
    #trigger
    add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PTM"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PT4T"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    add_body_invalid["calendar_events"][0]["VALARM"]["TRIGGER"] = "-PT63M"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # Recurrency Rule invalid
    # Freq
    add_body_invalid["calendar_events"][0]["RRULE"]["FREQ"] = "DAIL"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # Count
    add_body_invalid["calendar_events"][0]["RRULE"]["COUNT"] = ""
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # interval
    add_body_invalid["calendar_events"][0]["RRULE"]["COUNT"] = ""
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)

    # UID invalid (UID should be empty in add event request)
    add_body_invalid["calendar_events"][0]["UID"] = "20201122T125500-63"
    ret = harness.endpoint_request("events", "put", add_body_invalid)
    assert ret["status"] == status["BadRequest"]
    add_body_invalid = copy.deepcopy(add_body)


    # UPDATE INVALID
    # UID invalid
    update_body_invalid["calendar_events"][0]["UID"] = ""
    ret = harness.endpoint_request("events", "post", update_body_invalid)
    assert ret["status"] == status["BadRequest"]
    update_body_invalid = copy.deepcopy(update_body_invalid)

    update_body_invalid["calendar_events"][0]["UID"] = "43526gsdgsa322"
    ret = harness.endpoint_request("events", "post", update_body_invalid)
    assert ret["status"] == status["BadRequest"]
    update_body_invalid = copy.deepcopy(update_body_invalid)

    # REMOVE INVALID
    # It is a valid UID, but not exist in any events in DB
    # (handle it in db)
    # del_body = {"UID": "20151122T125500-63"}
    #
    # ret = harness.endpoint_request("events", "post", del_body)
    # assert ret["status"] == status["InternalServerError"]

    # UID invalid
    del_body = {"UID": "20201122T125500-dadsadsada"}
    ret = harness.endpoint_request("events", "del", del_body)
    assert ret["status"] == status["BadRequest"]

    del_body = {"UID": "201122T125500-63"}
    ret = harness.endpoint_request("events", "del", del_body)
    assert ret["status"] == status["BadRequest"]


M test/pytest/service-desktop/test_templates.py => test/pytest/service-desktop/test_templates.py +1 -1
@@ 59,4 59,4 @@ def test_messages(harness):
    body = {"template": True, "count": True}
    ret = harness.endpoint_request("messages", "get", body)
    assert ret["status"] == status["OK"]
    assert ret["body"]["count"] == count
\ No newline at end of file
    assert ret["body"]["count"] == count