Few more fixes to ics export (#5004)
<!-- PR description--> --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [x] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> * https://github.com/alcionai/corso/issues/3890 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
da8466ae0b
commit
985220562c
@ -19,20 +19,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// This package is used to convert json response from graph to ics
|
// This package is used to convert json response from graph to ics
|
||||||
// Ref: https://icalendar.org/
|
// https://icalendar.org/
|
||||||
// Ref: https://www.rfc-editor.org/rfc/rfc5545
|
// https://www.rfc-editor.org/rfc/rfc5545
|
||||||
// Ref: https://learn.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-1.0
|
// https://www.rfc-editor.org/rfc/rfc2445.txt
|
||||||
|
// https://learn.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-1.0
|
||||||
|
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/a685a040-5b69-4c84-b084-795113fb4012
|
||||||
|
|
||||||
// TODO: Items not handled
|
// TODO locations: https://github.com/alcionai/corso/issues/5003
|
||||||
// locations (different from location)
|
|
||||||
// exceptions and modifications
|
|
||||||
|
|
||||||
// Field in the backed up data that we cannot handle
|
|
||||||
// allowNewTimeProposals, hideAttendees, importance, isOnlineMeeting,
|
|
||||||
// isOrganizer, isReminderOn, onlineMeeting, onlineMeetingProvider,
|
|
||||||
// onlineMeetingUrl, originalEndTimeZone, originalStart,
|
|
||||||
// originalStartTimeZone, reminderMinutesBeforeStart, responseRequested,
|
|
||||||
// responseStatus, sensitivity
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
iCalDateTimeFormat = "20060102T150405Z"
|
iCalDateTimeFormat = "20060102T150405Z"
|
||||||
@ -54,7 +47,6 @@ func getLocationString(location models.Locationable) string {
|
|||||||
dn := ptr.Val(location.GetDisplayName())
|
dn := ptr.Val(location.GetDisplayName())
|
||||||
segments := []string{dn}
|
segments := []string{dn}
|
||||||
|
|
||||||
// TODO: Handle different location types
|
|
||||||
addr := location.GetAddress()
|
addr := location.GetAddress()
|
||||||
if addr != nil {
|
if addr != nil {
|
||||||
street := ptr.Val(addr.GetStreet())
|
street := ptr.Val(addr.GetStreet())
|
||||||
@ -260,18 +252,20 @@ func FromJSON(ctx context.Context, body []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateEventProperties(ctx context.Context, event models.Eventable, iCalEvent *ics.VEvent) error {
|
func updateEventProperties(ctx context.Context, event models.Eventable, iCalEvent *ics.VEvent) error {
|
||||||
|
// CREATED - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1
|
||||||
created := event.GetCreatedDateTime()
|
created := event.GetCreatedDateTime()
|
||||||
if created != nil {
|
if created != nil {
|
||||||
iCalEvent.SetCreatedTime(ptr.Val(created))
|
iCalEvent.SetCreatedTime(ptr.Val(created))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LAST-MODIFIED - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.3
|
||||||
modified := event.GetLastModifiedDateTime()
|
modified := event.GetLastModifiedDateTime()
|
||||||
if modified != nil {
|
if modified != nil {
|
||||||
iCalEvent.SetModifiedAt(ptr.Val(modified))
|
iCalEvent.SetModifiedAt(ptr.Val(modified))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DTSTART - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4
|
||||||
allDay := ptr.Val(event.GetIsAllDay())
|
allDay := ptr.Val(event.GetIsAllDay())
|
||||||
|
|
||||||
startString := event.GetStart().GetDateTime()
|
startString := event.GetStart().GetDateTime()
|
||||||
startTimezone := event.GetStart().GetTimeZone()
|
startTimezone := event.GetStart().GetTimeZone()
|
||||||
|
|
||||||
@ -288,6 +282,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DTEND - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2
|
||||||
endString := event.GetEnd().GetDateTime()
|
endString := event.GetEnd().GetDateTime()
|
||||||
endTimezone := event.GetEnd().GetTimeZone()
|
endTimezone := event.GetEnd().GetTimeZone()
|
||||||
|
|
||||||
@ -314,22 +309,21 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
iCalEvent.AddRrule(pattern)
|
iCalEvent.AddRrule(pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// STATUS - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.11
|
||||||
cancelled := event.GetIsCancelled()
|
cancelled := event.GetIsCancelled()
|
||||||
if cancelled != nil {
|
if cancelled != nil {
|
||||||
iCalEvent.SetStatus(ics.ObjectStatusCancelled)
|
iCalEvent.SetStatus(ics.ObjectStatusCancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
draft := event.GetIsDraft()
|
// SUMMARY - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.12
|
||||||
if draft != nil {
|
|
||||||
iCalEvent.SetStatus(ics.ObjectStatusDraft)
|
|
||||||
}
|
|
||||||
|
|
||||||
summary := event.GetSubject()
|
summary := event.GetSubject()
|
||||||
if summary != nil {
|
if summary != nil {
|
||||||
iCalEvent.SetSummary(ptr.Val(summary))
|
iCalEvent.SetSummary(ptr.Val(summary))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DESCRIPTION - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.5
|
||||||
// TODO: Emojies currently don't seem to be read properly by Outlook
|
// TODO: Emojies currently don't seem to be read properly by Outlook
|
||||||
|
// When outlook exports them(in .eml), it exports them in text as it strips down html
|
||||||
bodyPreview := ptr.Val(event.GetBodyPreview())
|
bodyPreview := ptr.Val(event.GetBodyPreview())
|
||||||
|
|
||||||
if event.GetBody() != nil {
|
if event.GetBody() != nil {
|
||||||
@ -350,29 +344,28 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRANSP - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.7
|
||||||
showAs := ptr.Val(event.GetShowAs()).String()
|
showAs := ptr.Val(event.GetShowAs()).String()
|
||||||
if len(showAs) > 0 && showAs != "unknown" {
|
if len(showAs) > 0 {
|
||||||
var status ics.FreeBusyTimeType
|
var transp ics.TimeTransparency
|
||||||
|
|
||||||
switch showAs {
|
switch showAs {
|
||||||
case "free":
|
case "free", "unknown":
|
||||||
status = ics.FreeBusyTimeTypeFree
|
transp = ics.TransparencyTransparent
|
||||||
case "tentative":
|
default:
|
||||||
status = ics.FreeBusyTimeTypeBusyTentative
|
transp = ics.TransparencyOpaque
|
||||||
case "busy":
|
|
||||||
status = ics.FreeBusyTimeTypeBusy
|
|
||||||
case "oof", "workingElsewhere": // this is just best effort conversion
|
|
||||||
status = ics.FreeBusyTimeTypeBusyUnavailable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iCalEvent.AddProperty(ics.ComponentPropertyFreebusy, string(status))
|
iCalEvent.AddProperty(ics.ComponentPropertyTransp, string(transp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CATEGORIES - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.2
|
||||||
categories := event.GetCategories()
|
categories := event.GetCategories()
|
||||||
for _, category := range categories {
|
for _, category := range categories {
|
||||||
iCalEvent.AddProperty(ics.ComponentPropertyCategories, category)
|
iCalEvent.AddProperty(ics.ComponentPropertyCategories, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URL - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.6
|
||||||
// According to the RFC, this property may be used in a calendar
|
// According to the RFC, this property may be used in a calendar
|
||||||
// component to convey a location where a more dynamic rendition of
|
// component to convey a location where a more dynamic rendition of
|
||||||
// the calendar information associated with the calendar component
|
// the calendar information associated with the calendar component
|
||||||
@ -382,12 +375,13 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
iCalEvent.SetURL(url)
|
iCalEvent.SetURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ORGANIZER - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.3
|
||||||
organizer := event.GetOrganizer()
|
organizer := event.GetOrganizer()
|
||||||
if organizer != nil {
|
if organizer != nil {
|
||||||
name := ptr.Val(organizer.GetEmailAddress().GetName())
|
name := ptr.Val(organizer.GetEmailAddress().GetName())
|
||||||
addr := ptr.Val(organizer.GetEmailAddress().GetAddress())
|
addr := ptr.Val(organizer.GetEmailAddress().GetAddress())
|
||||||
|
|
||||||
// TODO: What to do if we only have a name?
|
// It does not look like we can get just a name without an address
|
||||||
if len(name) > 0 && len(addr) > 0 {
|
if len(name) > 0 && len(addr) > 0 {
|
||||||
iCalEvent.SetOrganizer(addr, ics.WithCN(name))
|
iCalEvent.SetOrganizer(addr, ics.WithCN(name))
|
||||||
} else if len(addr) > 0 {
|
} else if len(addr) > 0 {
|
||||||
@ -395,6 +389,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ATTENDEE - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.1
|
||||||
attendees := event.GetAttendees()
|
attendees := event.GetAttendees()
|
||||||
for _, attendee := range attendees {
|
for _, attendee := range attendees {
|
||||||
props := []ics.PropertyParameter{}
|
props := []ics.PropertyParameter{}
|
||||||
@ -445,12 +440,50 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
iCalEvent.AddAttendee(addr, props...)
|
iCalEvent.AddAttendee(addr, props...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOCATION - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7
|
||||||
location := getLocationString(event.GetLocation())
|
location := getLocationString(event.GetLocation())
|
||||||
if len(location) > 0 {
|
if len(location) > 0 {
|
||||||
iCalEvent.SetLocation(location)
|
iCalEvent.SetLocation(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Handle different attachment type (file, item and reference)
|
// X-MICROSOFT-LOCATIONDISPLAYNAME (Outlook seems to use this)
|
||||||
|
loc := event.GetLocation()
|
||||||
|
if loc != nil {
|
||||||
|
locationDisplayName := ptr.Val(event.GetLocation().GetDisplayName())
|
||||||
|
if len(locationDisplayName) > 0 {
|
||||||
|
iCalEvent.AddProperty("X-MICROSOFT-LOCATIONDISPLAYNAME", locationDisplayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLASS - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.3
|
||||||
|
// Graph also has the value "personal" which is not supported by the spec
|
||||||
|
// Default value is "public" (works for "normal")
|
||||||
|
sensitivity := ptr.Val(event.GetSensitivity()).String()
|
||||||
|
if sensitivity == "private" {
|
||||||
|
iCalEvent.AddProperty(ics.ComponentPropertyClass, "PRIVATE")
|
||||||
|
} else if sensitivity == "confidential" {
|
||||||
|
iCalEvent.AddProperty(ics.ComponentPropertyClass, "CONFIDENTIAL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.9
|
||||||
|
imp := ptr.Val(event.GetImportance()).String()
|
||||||
|
switch imp {
|
||||||
|
case "high":
|
||||||
|
iCalEvent.AddProperty(ics.ComponentPropertyPriority, "1")
|
||||||
|
case "low":
|
||||||
|
iCalEvent.AddProperty(ics.ComponentPropertyPriority, "9")
|
||||||
|
}
|
||||||
|
|
||||||
|
meeting := event.GetOnlineMeeting()
|
||||||
|
if meeting != nil {
|
||||||
|
url := ptr.Val(meeting.GetJoinUrl())
|
||||||
|
if len(url) > 0 {
|
||||||
|
iCalEvent.AddProperty("X-MICROSOFT-SKYPETEAMSMEETINGURL", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ATTACH - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.1
|
||||||
|
// TODO Handle different attachment types (file, item and reference)
|
||||||
attachments := event.GetAttachments()
|
attachments := event.GetAttachments()
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
props := []ics.PropertyParameter{}
|
props := []ics.PropertyParameter{}
|
||||||
@ -482,6 +515,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Inline attachments don't show up in Outlook
|
// TODO: Inline attachments don't show up in Outlook
|
||||||
|
// Inline attachments is not something supported by the spec
|
||||||
inline := ptr.Val(attachment.GetIsInline())
|
inline := ptr.Val(attachment.GetIsInline())
|
||||||
if inline {
|
if inline {
|
||||||
cidv, err := attachment.GetBackingStore().Get("contentId")
|
cidv, err := attachment.GetBackingStore().Get("contentId")
|
||||||
@ -500,6 +534,7 @@ func updateEventProperties(ctx context.Context, event models.Eventable, iCalEven
|
|||||||
iCalEvent.AddAttachment(base64.StdEncoding.EncodeToString(content), props...)
|
iCalEvent.AddAttachment(base64.StdEncoding.EncodeToString(content), props...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXDATE - https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.1
|
||||||
cancelledDates, err := getCancelledDates(ctx, event)
|
cancelledDates, err := getCancelledDates(ctx, event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return clues.Wrap(err, "getting cancelled dates")
|
return clues.Wrap(err, "getting cancelled dates")
|
||||||
|
|||||||
@ -569,19 +569,6 @@ func (suite *ICSUnitSuite) TestEventConversion() {
|
|||||||
assert.Contains(t, out, "STATUS:CANCELLED", "cancelled status")
|
assert.Contains(t, out, "STATUS:CANCELLED", "cancelled status")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "draft event",
|
|
||||||
event: func() *models.Event {
|
|
||||||
e := baseEvent()
|
|
||||||
|
|
||||||
e.SetIsDraft(ptr.To(true))
|
|
||||||
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
check: func(out string) {
|
|
||||||
assert.Contains(t, out, "STATUS:DRAFT", "draft status")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "text body",
|
name: "text body",
|
||||||
event: func() *models.Event {
|
event: func() *models.Event {
|
||||||
@ -625,7 +612,7 @@ func (suite *ICSUnitSuite) TestEventConversion() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "free busy free",
|
name: "showas free",
|
||||||
event: func() *models.Event {
|
event: func() *models.Event {
|
||||||
e := baseEvent()
|
e := baseEvent()
|
||||||
|
|
||||||
@ -637,27 +624,11 @@ func (suite *ICSUnitSuite) TestEventConversion() {
|
|||||||
return e
|
return e
|
||||||
},
|
},
|
||||||
check: func(out string) {
|
check: func(out string) {
|
||||||
assert.Contains(t, out, "FREEBUSY:FREE", "free busy status")
|
assert.Contains(t, out, "TRANSP:TRANSPARENT", "free busy status")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "free busy unknown",
|
name: "showas oof",
|
||||||
event: func() *models.Event {
|
|
||||||
e := baseEvent()
|
|
||||||
|
|
||||||
fbs, err := models.ParseFreeBusyStatus("unknown")
|
|
||||||
require.NoError(t, err, "parse free busy status")
|
|
||||||
|
|
||||||
e.SetShowAs(fbs.(*models.FreeBusyStatus))
|
|
||||||
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
check: func(out string) {
|
|
||||||
assert.NotContains(t, out, "FREEBUSY", "free busy status")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "free busy oof",
|
|
||||||
event: func() *models.Event {
|
event: func() *models.Event {
|
||||||
e := baseEvent()
|
e := baseEvent()
|
||||||
|
|
||||||
@ -669,7 +640,7 @@ func (suite *ICSUnitSuite) TestEventConversion() {
|
|||||||
return e
|
return e
|
||||||
},
|
},
|
||||||
check: func(out string) {
|
check: func(out string) {
|
||||||
assert.Contains(t, out, "FREEBUSY:BUSY-UNAVAILABLE", "free busy status")
|
assert.Contains(t, out, "TRANSP:OPAQUE", "free busy status")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -756,6 +727,66 @@ func (suite *ICSUnitSuite) TestEventConversion() {
|
|||||||
assert.Contains(t, out, "LOCATION:DisplayName", "location")
|
assert.Contains(t, out, "LOCATION:DisplayName", "location")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "teams url",
|
||||||
|
event: func() *models.Event {
|
||||||
|
e := baseEvent()
|
||||||
|
|
||||||
|
mi := models.NewOnlineMeetingInfo()
|
||||||
|
mi.SetJoinUrl(ptr.To("https://team.microsoft.com/meeting-url"))
|
||||||
|
|
||||||
|
e.SetOnlineMeeting(mi)
|
||||||
|
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
check: func(out string) {
|
||||||
|
assert.Contains(t, out, "X-MICROSOFT-SKYPETEAMSMEETINGURL:https://team.microsoft.com/meeting-url", "teams url")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-MICROSOFT-LOCATIONDISPLAYNAME",
|
||||||
|
event: func() *models.Event {
|
||||||
|
e := baseEvent()
|
||||||
|
|
||||||
|
loc := models.NewLocation()
|
||||||
|
loc.SetDisplayName(ptr.To("DisplayName"))
|
||||||
|
|
||||||
|
e.SetLocation(loc)
|
||||||
|
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
check: func(out string) {
|
||||||
|
assert.Contains(t, out, "X-MICROSOFT-LOCATIONDISPLAYNAME:DisplayName", "location display name")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "class",
|
||||||
|
event: func() *models.Event {
|
||||||
|
e := baseEvent()
|
||||||
|
|
||||||
|
sen := models.CONFIDENTIAL_SENSITIVITY
|
||||||
|
e.SetSensitivity(&sen)
|
||||||
|
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
check: func(out string) {
|
||||||
|
assert.Contains(t, out, "CLASS:CONFIDENTIAL", "class")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority",
|
||||||
|
event: func() *models.Event {
|
||||||
|
e := baseEvent()
|
||||||
|
|
||||||
|
pri := models.HIGH_IMPORTANCE
|
||||||
|
e.SetImportance(&pri)
|
||||||
|
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
check: func(out string) {
|
||||||
|
assert.Contains(t, out, "PRIORITY:1", "priority")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user