Refactor ICS export

This commit is contained in:
Christophe Deschamps 2026-01-24 08:02:29 +01:00
parent ed9fe9c563
commit 909bc98622

View file

@ -660,7 +660,7 @@ bool ConferenceInfoCore::isAllDayConf() const {
} }
void ConferenceInfoCore::exportConferenceToICS() { void ConferenceInfoCore::exportConferenceToICS() {
// Collect participant addresses to look up display names // Collect participant addresses
QStringList participantAddresses; QStringList participantAddresses;
for (const auto &participant : mParticipants) { for (const auto &participant : mParticipants) {
auto map = participant.toMap(); auto map = participant.toMap();
@ -679,159 +679,96 @@ void ConferenceInfoCore::exportConferenceToICS() {
QDateTime dateTime = mDateTime; QDateTime dateTime = mDateTime;
QDateTime endDateTime = mEndDateTime; QDateTime endDateTime = mEndDateTime;
// Look up display names from friend list in model thread // Generate ICS on model thread (for display name lookup) then open file
App::postModelAsync( App::postModelAsync(
[participantAddresses, uri, organizerAddress, organizerName, subject, description, dateTime, endDateTime]() { [participantAddresses, uri, organizerAddress, organizerName, subject, description, dateTime, endDateTime]() {
// Helper to extract phone number from SIP address // Helper lambda to escape special characters in ICS text fields
auto extractPhoneNumber = [](const QString &address) -> QString { auto escapeIcsText = [](const QString &text) {
QString addr = address; QString escaped = text;
if (addr.startsWith("sip:", Qt::CaseInsensitive)) { escaped.replace("\\", "\\\\");
addr = addr.mid(4); escaped.replace(";", "\\;");
} else if (addr.startsWith("sips:", Qt::CaseInsensitive)) { escaped.replace(",", "\\,");
addr = addr.mid(5); escaped.replace("\n", "\\n");
} return escaped;
int atIndex = addr.indexOf('@');
if (atIndex > 0) {
return addr.left(atIndex);
}
return addr;
}; };
// Build map of address -> display name // Helper lambda to format datetime in ICS format (UTC)
QMap<QString, QString> displayNames; auto formatIcsDateTime = [](const QDateTime &dt) { return dt.toUTC().toString("yyyyMMdd'T'HHmmss'Z'"); };
auto appFriendList = ToolModel::getAppFriendList();
for (const QString &address : participantAddresses) { // Generate a unique UID based on URI or datetime + organizer
QString displayName; QString uid;
if (!uri.isEmpty()) {
// First try standard lookup by address uid = uri;
auto linFriend = ToolModel::findFriendByAddress(address); uid.replace("sip:", "").replace("@", "-at-");
if (linFriend) { } else {
displayName = Utils::coreStringToAppString(linFriend->getName()); uid = dateTime.toUTC().toString("yyyyMMddHHmmss") + "-" + organizerAddress;
} uid.replace("sip:", "").replace("@", "-at-");
// If not found, search by phone number in friend list
if (displayName.isEmpty() && appFriendList) {
QString phoneNumber = extractPhoneNumber(address);
if (!phoneNumber.isEmpty()) {
// Iterate through all friends and check their phone numbers
for (const auto &friendEntry : appFriendList->getFriends()) {
auto friendPhoneNumbers = friendEntry->getPhoneNumbers();
for (const auto &friendPhone : friendPhoneNumbers) {
QString friendPhoneStr = Utils::coreStringToAppString(friendPhone);
// Normalize both numbers for comparison (digits only)
QString normalizedFriendPhone = friendPhoneStr;
QString normalizedSearchPhone = phoneNumber;
normalizedFriendPhone.remove(QRegularExpression("[^0-9]"));
normalizedSearchPhone.remove(QRegularExpression("[^0-9]"));
// Check if phone numbers match
if (friendPhoneStr == phoneNumber || normalizedFriendPhone == normalizedSearchPhone) {
displayName = Utils::coreStringToAppString(friendEntry->getName());
break;
}
}
if (!displayName.isEmpty()) break;
}
}
}
// Fallback to phone number if no display name found
if (displayName.isEmpty()) {
displayName = extractPhoneNumber(address);
}
displayNames[address] = displayName;
} }
// Write ICS file in main thread // Build the ICS content
App::postCoreAsync([participantAddresses, displayNames, uri, organizerAddress, organizerName, subject, QString icsContent;
description, dateTime, endDateTime]() { QTextStream out(&icsContent);
QString filePath(Paths::getAppLocalDirPath() + "conference.ics");
QFile file(filePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
// Helper lambda to escape special characters in ICS text fields out << "BEGIN:VCALENDAR\r\n";
auto escapeIcsText = [](const QString &text) { out << "VERSION:2.0\r\n";
QString escaped = text; out << "PRODID:-//Titanium Comms//EN\r\n";
escaped.replace("\\", "\\\\"); out << "METHOD:REQUEST\r\n";
escaped.replace(";", "\\;"); out << "BEGIN:VEVENT\r\n";
escaped.replace(",", "\\,");
escaped.replace("\n", "\\n");
return escaped;
};
// Helper lambda to format datetime in ICS format (UTC) // UID and timestamps
auto formatIcsDateTime = [](const QDateTime &dt) { out << "UID:" << uid << "\r\n";
return dt.toUTC().toString("yyyyMMdd'T'HHmmss'Z'"); out << "DTSTAMP:" << formatIcsDateTime(QDateTime::currentDateTimeUtc()) << "\r\n";
}; out << "DTSTART:" << formatIcsDateTime(dateTime) << "\r\n";
out << "DTEND:" << formatIcsDateTime(endDateTime) << "\r\n";
// Generate a unique UID based on URI or datetime + organizer // Organizer
QString uid; if (!organizerAddress.isEmpty()) {
if (!uri.isEmpty()) { out << "ORGANIZER";
uid = uri; if (!organizerName.isEmpty()) {
uid.replace("sip:", "").replace("@", "-at-"); out << ";CN=" << escapeIcsText(organizerName);
} else {
uid = dateTime.toUTC().toString("yyyyMMddHHmmss") + "-" + organizerAddress;
uid.replace("sip:", "").replace("@", "-at-");
}
// Build the ICS content
out << "BEGIN:VCALENDAR\r\n";
out << "VERSION:2.0\r\n";
out << "PRODID:-//Titanium Comms//EN\r\n";
out << "METHOD:REQUEST\r\n";
out << "BEGIN:VEVENT\r\n";
// UID and timestamps
out << "UID:" << uid << "\r\n";
out << "DTSTAMP:" << formatIcsDateTime(QDateTime::currentDateTimeUtc()) << "\r\n";
out << "DTSTART:" << formatIcsDateTime(dateTime) << "\r\n";
out << "DTEND:" << formatIcsDateTime(endDateTime) << "\r\n";
// Organizer
if (!organizerAddress.isEmpty()) {
out << "ORGANIZER";
if (!organizerName.isEmpty()) {
out << ";CN=" << escapeIcsText(organizerName);
}
out << ":" << organizerAddress << "\r\n";
}
// Attendees/Participants
for (const QString &address : participantAddresses) {
QString displayName = displayNames.value(address);
out << "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
if (!displayName.isEmpty()) {
out << ";CN=" << escapeIcsText(displayName);
}
out << ":" << address << "\r\n";
}
// Subject/Summary
if (!subject.isEmpty()) {
out << "SUMMARY:" << escapeIcsText(subject) << "\r\n";
}
// Description
if (!description.isEmpty()) {
out << "DESCRIPTION:" << escapeIcsText(description) << "\r\n";
}
// Location (conference URI)
if (!uri.isEmpty()) {
out << "LOCATION:" << uri << "\r\n";
out << "URL:" << uri << "\r\n";
}
out << "STATUS:CONFIRMED\r\n";
out << "SEQUENCE:0\r\n";
out << "END:VEVENT\r\n";
out << "END:VCALENDAR\r\n";
file.close();
} }
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); out << ":" << organizerAddress << "\r\n";
}); }
// Attendees/Participants
for (const QString &address : participantAddresses) {
QString displayName = ToolModel::getDisplayName(address);
out << "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
if (!displayName.isEmpty()) {
out << ";CN=" << escapeIcsText(displayName);
}
out << ":" << address << "\r\n";
}
// Subject/Summary
if (!subject.isEmpty()) {
out << "SUMMARY:" << escapeIcsText(subject) << "\r\n";
}
// Description
if (!description.isEmpty()) {
out << "DESCRIPTION:" << escapeIcsText(description) << "\r\n";
}
// Location (conference URI)
if (!uri.isEmpty()) {
out << "LOCATION:" << uri << "\r\n";
out << "URL:" << uri << "\r\n";
}
out << "STATUS:CONFIRMED\r\n";
out << "SEQUENCE:0\r\n";
out << "END:VEVENT\r\n";
out << "END:VCALENDAR\r\n";
// Write the file and open it
QString filePath(Paths::getAppLocalDirPath() + "conference.ics");
QFile file(filePath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream fileOut(&file);
fileOut << icsContent;
file.close();
}
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
}); });
} }