Skip to content

Commit

Permalink
feat: repeated events support
Browse files Browse the repository at this point in the history
  • Loading branch information
Крылов Александр committed Jul 29, 2024
1 parent 698a89b commit 3e566a3
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 18 deletions.
25 changes: 24 additions & 1 deletion caldav-fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def print_calendars_demo(calendars):
for calendar in calendars:
for event in calendar.events():
for component in event.icalendar_instance.walk():
if component.name != "VEVENT":
if component.name != "VEVENT" or component.get('STATUS') == 'CANCELLED':
continue
events.append(fill_event(component, calendar))
print(json.dumps(events, indent=2, ensure_ascii=False))
Expand All @@ -45,8 +45,31 @@ def fill_event(component, calendar) -> dict[str, str]:
if endDate and endDate.dt:
cur["end"] = endDate.dt.strftime("%m/%d/%Y %H:%M")
cur["datestamp"] = component.get("dtstamp").dt.strftime("%m/%d/%Y %H:%M")
duration = component.get("duration")
if duration:
cur["duration"] = str(duration.dt)
rrule = component.get('rrule')
if rrule:
cur["rrule"] = fill_rrule(rrule)
return cur


def fill_rrule(rrule) -> dict[str, str]:
res = dict()
res['freq'] = rrule.get('freq')
res['interval'] = rrule.get('interval')

until = rrule.get("until")
if until is not None:
res['until'] = until[0].strftime("%m/%d/%Y %H:%M")

days = rrule.get('byday')
if days:
res['byday'] = []
for d in days:
res['byday'].append(d)
return res


if __name__ == "__main__":
fetch_and_print()
169 changes: 152 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ func main() {
go planEvents(events)

icon := os.Getenv(notifyEnvIcon)
needSound := os.Getenv(notifyEnvWithSound) != ""
for e := range events {
if sound := os.Getenv(notifyEnvWithSound); sound != "" {
if needSound {
notify.Alert("", e.Summary, e.Description, icon)
} else {
notify.Notify("", e.Summary, e.Description, icon)
Expand All @@ -47,9 +48,9 @@ func planEvents(ch chan event) {
offset := getOffsetByServer()
period := getRefreshPeriod()
timeBefore := getTimeBeforeNotify()

for {
events := getEvents(offset)
checkAndRememberEvents(ch, events, planned, timeBefore)
checkAndRememberEvents(ch, getEvents(offset), planned, timeBefore)
time.Sleep(period)
}
}
Expand All @@ -58,9 +59,6 @@ func checkAndRememberEvents(ch chan event, events []event, planned map[string]st
for _, e := range events {
key := e.Summary + e.Start.String()
if _, ok := planned[key]; ok || e.isPast() {
if e.isToday() {
fmt.Printf("skipping event from past: %s at: %s\n %s", e.Summary, e.Start.String(), messageAboutOffsets)
}
continue
}

Expand Down Expand Up @@ -99,7 +97,7 @@ func getEvents(offset time.Duration) []event {
fmt.Printf("failed to parse event: %+v", e)
continue
}
result[i] = e
result[i] = repeatedEventFromEvent(e)
}

return result
Expand All @@ -112,6 +110,8 @@ type eventRaw struct {
Datestamp string
Summary string
Description string
Duration string
Rrule *rruleRaw
}

type event struct {
Expand All @@ -121,14 +121,22 @@ type event struct {
Start time.Time
End time.Time
CreatedAt time.Time
Rrule *rrule
}

func (e event) isPast() bool {
return e.Start.Before(time.Now()) && e.End.Before(time.Now())
type rruleRaw struct {
Freq []string
Until *string
ByDay []string
Interval []int
}

func (e event) isToday() bool {
return e.Start.Day() == time.Now().Day()
type rrule struct {
Freq string
Until *time.Time
ByDay map[time.Weekday]struct{}
Interval int
Duration time.Duration
}

func eventFromEventRaw(raw eventRaw, offset time.Duration) (event, error) {
Expand All @@ -143,24 +151,147 @@ func eventFromEventRaw(raw eventRaw, offset time.Duration) (event, error) {
// The time comes according to the server time zone, but is read as UTC, so we subtract offset
// Let's assume that the server is in the same zone as the client
// Otherwise, offset must be set in the corresponding environment variable
_, offset := time.Now().Zone()

// change time by timezone of current location
start = start.Add(-time.Duration(offset) * time.Second)
end = end.Add(-time.Duration(offset) * time.Second)
datestamp = datestamp.Add(-time.Duration(offset) * time.Second)
_, offsetI := time.Now().Zone()
offset = time.Duration(offsetI) * time.Second
}

// change time by timezone of current location
start = start.Add(-offset)
end = end.Add(-offset)
datestamp = datestamp.Add(-offset)

return event{
Name: raw.Name,
Summary: raw.Summary,
Description: raw.Description,
Start: start,
End: end,
CreatedAt: datestamp,
Rrule: readRrule(raw.Rrule, offset, raw.Duration),
}, nil
}

func repeatedEventFromEvent(e event) event {
if e.Rrule == nil || (e.Rrule.Until != nil && e.Rrule.Until.Before(time.Now())) {
return e
}

if !e.Rrule.checkByFreq(e.Start) {
return e
}

today0 := time.Now().UTC().Truncate(24 * time.Hour)
e.Start = today0.Add(offsetFromTime(e.Start))
e.End = e.Start.Add(e.Rrule.Duration)

return e
}

func (r rrule) checkByFreq(start time.Time) bool {
switch r.Freq {
case "WEEKLY":
if r.ByDay == nil {
return false
}

if _, ok := r.ByDay[time.Now().Weekday()]; !ok {
return false
}

_, w := time.Now().ISOWeek()
_, w2 := start.ISOWeek()

return (w2-w)%r.Interval == 0
case "DAILY":
return (start.Day()-time.Now().Day())%r.Interval == 0
case "MONTHLY":
return (int(start.Month())-int(time.Now().Month()))%r.Interval == 0
case "YEARLY":
return (start.Year()-time.Now().Year())%r.Interval == 0
default:
return false
}
}

func offsetFromTime(t time.Time) time.Duration {
h, m, s := t.Clock()
return time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second
}

func readRrule(raw *rruleRaw, offset time.Duration, dur string) *rrule {
if raw == nil {
return nil
}

res := &rrule{
Freq: getFirst(raw.Freq),
Interval: getFirst(raw.Interval),
Until: parseTime(raw.Until, offset),
Duration: durationFromString(dur),
}

if len(raw.ByDay) == 0 {
return res
}

res.ByDay = make(map[time.Weekday]struct{})
for _, day := range raw.ByDay {
res.ByDay[dayFromString(day)] = struct{}{}
}

return res
}

func getFirst[T any](arr []T) T {
if len(arr) == 0 {
return *new(T)
}
return arr[0]
}

func parseTime(raw *string, offset time.Duration) *time.Time {
if raw == nil {
return nil
}
res, _ := time.Parse(timeFormat, *raw)
res = res.Add(-offset)
return &res
}

func durationFromString(dur string) time.Duration {
tmp := strings.Split(dur, ":")
if len(tmp) < 3 {
return 0
}
dur = tmp[0] + "h" + tmp[1] + "m" + tmp[2] + "s"
res, err := time.ParseDuration(dur)
if err != nil {
return 0
}
return res
}

func dayFromString(day string) time.Weekday {
switch day {
case "MO":
return time.Monday
case "TU":
return time.Tuesday
case "WE":
return time.Wednesday
case "TH":
return time.Thursday
case "FR":
return time.Friday
case "SA":
return time.Saturday
case "SU":
return time.Sunday
default:
return time.Sunday
}
}

func getRefreshPeriod() time.Duration {
period := os.Getenv(notifyEnvRefreshPeriod)
if period == "" {
Expand Down Expand Up @@ -211,3 +342,7 @@ func processExitError(err error) {
fmt.Println("Unauthorized. Please check your credentials in python script.")
os.Exit(1)
}

func (e event) isPast() bool {
return e.Start.Before(time.Now()) && e.End.Before(time.Now())
}

0 comments on commit 3e566a3

Please sign in to comment.