-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Крылов Александр
committed
Jul 12, 2024
0 parents
commit 163f170
Showing
8 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
env.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
https://github.com/Truenya |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Caldav notification daemon | ||
### prepare | ||
``` | ||
pip install -r requirements.txt | ||
sudo ln -s $PWD/caldav-fetch.py /usr/bin/caldav-fetch.py | ||
go mod tidy | ||
``` | ||
|
||
#### run | ||
``` | ||
go run main.go & disown | ||
``` | ||
|
||
#### or build and run | ||
``` | ||
go build -o caldav_daemon . | ||
./caldav_daemon & disown | ||
``` | ||
|
||
#### to stop | ||
``` | ||
killall caldav_daemon | ||
``` | ||
|
||
###### or | ||
``` | ||
killall go | ||
``` | ||
|
||
## environment variables to configure | ||
### must be set | ||
- CALDAV_USERNAME | ||
- CALDAV_PASSWORD | ||
- CALDAV_URL | ||
|
||
### optional | ||
- CALDAV_NOTIFY_ICON | ||
- CALDAV_SERVER_OFFSET_HOURS default - same as server | ||
- CALDAV_REFRESH_PERIOD_MINUTES default 10 | ||
- CALDAV_NOTIFY_BEFORE_MINUTES default 5 | ||
|
||
## for better experience | ||
it is useful to create env file with environment variables and source it |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
#!/usr/bin/env python3 | ||
from icalendar import Calendar | ||
import caldav | ||
import json | ||
from os import environ as env | ||
|
||
username = env["CALDAV_USERNAME"] | ||
password = env["CALDAV_PASSWORD"] | ||
url = env["CALDAV_URL"] | ||
caldav_url = f"https://{url}/{username}/" | ||
headers = {} | ||
|
||
|
||
def fetch_and_print(): | ||
with caldav.DAVClient( | ||
url=caldav_url, | ||
username=username, | ||
password=password, | ||
headers=headers, # Optional parameter to set HTTP headers on each request if needed | ||
) as client: | ||
print_calendar_events_json(client.principal().calendars()) | ||
|
||
|
||
def print_calendar_events_json(calendars): | ||
if not calendars: | ||
return | ||
events = [] | ||
for calendar in calendars: | ||
for eventraw in calendar.events(): | ||
for component in Calendar.from_ical(eventraw._data).walk(): | ||
if component.name != "VEVENT": | ||
continue | ||
cur = {} | ||
cur['calendar'] = f'{calendar}' | ||
cur['summary'] = component.get('summary') | ||
cur['description'] = component.get('description') | ||
cur['start'] = component.get('dtstart').dt.strftime('%m/%d/%Y %H:%M') | ||
endDate = component.get('dtend') | ||
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') | ||
events.append(cur) | ||
print(json.dumps(events, indent=2, ensure_ascii=False)) | ||
|
||
|
||
if __name__ == "__main__": | ||
fetch_and_print() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
module caldav_daemon | ||
|
||
go 1.22.5 | ||
|
||
require github.com/martinlindhe/notify v0.0.0-20181008203735-20632c9a275a | ||
|
||
require ( | ||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb // indirect | ||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect | ||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb h1:6S+TKObz6+Io2c8IOkcbK4Sz7nj6RpEVU7TkvmsZZcw= | ||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb/go.mod h1:wf3nKtOnQqCp7kp9xB7hHnNlZ6m3NoiOxjrB9hFRq4Y= | ||
github.com/martinlindhe/notify v0.0.0-20181008203735-20632c9a275a h1:nQcAxLK581HrmqF0TVy2GC3iFjB8X+aWGtxQ/t2uyGE= | ||
github.com/martinlindhe/notify v0.0.0-20181008203735-20632c9a275a/go.mod h1:zL1p4SieQ27ZZ4V4KdVYdEcSkVl1OwNoi8xI1r5hJkc= | ||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= | ||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= | ||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo= | ||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NKt867Xe18emOmtsO3eAKbDaon0o= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/martinlindhe/notify" | ||
) | ||
|
||
const ( | ||
defaultTimeBeforeNotify = 5 * time.Minute | ||
defaultRefreshPeriod = 10 * time.Minute | ||
timeFormat = "1/02/2006 15:04" | ||
tzFile = "/etc/timezone" | ||
messageAboutOffsets = "if event is in future, probably your timezone is not same as on your server, than please set offset in the CALDAV_SERVER_OFFSET_HOURS env var\n" | ||
notifyEnvIcon = "CALDAV_NOTIFY_ICON" | ||
notifyEnvOffset = "CALDAV_SERVER_OFFSET_HOURS" | ||
notifyEnvRefreshPeriod = "CALDAV_REFRESH_PERIOD_MINUTES" | ||
notifyEnvTimeBefore = "CALDAV_NOTIFY_BEFORE_MINUTES" | ||
) | ||
|
||
func main() { | ||
notify.Notify("", "https://github.com/Truenya", "notification daemon started", "") | ||
events := make(chan event) | ||
go planEvents(events) | ||
|
||
icon := os.Getenv(notifyEnvIcon) | ||
for e := range events { | ||
notify.Notify("", e.Summary, e.Description, icon) | ||
} | ||
} | ||
|
||
func planEvents(ch chan event) { | ||
planned := map[string]struct{}{} | ||
offset := getOffsetByServer() | ||
period := getRefreshPeriod() | ||
timeBefore := getTimeBeforeNotify() | ||
for { | ||
events := getEvents(offset) | ||
checkAndRememberEvents(ch, events, planned, timeBefore) | ||
time.Sleep(period) | ||
} | ||
} | ||
|
||
func checkAndRememberEvents(ch chan event, events []event, planned map[string]struct{}, timeBefore time.Duration) { | ||
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 | ||
} | ||
|
||
go e.notify(ch, timeBefore) | ||
planned[key] = struct{}{} | ||
|
||
fmt.Println("planned event:", e.Summary, "at:", e.Start.String()) | ||
} | ||
} | ||
|
||
func (e event) notify(ch chan event, timeBefore time.Duration) { | ||
dur := time.Until(e.Start) - timeBefore | ||
if dur > 0 { | ||
fmt.Println("sleeping for:", dur, "for event:", e.Summary) | ||
time.Sleep(dur) | ||
} | ||
ch <- e | ||
fmt.Printf("notified for event: %+v", e) | ||
} | ||
|
||
func getEvents(offset time.Duration) []event { | ||
out, err := exec.Command("caldav-fetch.py").Output() | ||
if err != nil { | ||
processExitError(err) | ||
} | ||
|
||
data := []eventRaw{} | ||
if err := json.Unmarshal(out, &data); err != nil { | ||
panic(err) | ||
} | ||
|
||
result := make([]event, len(data)) | ||
for i, e := range data { | ||
e, err := eventFromEventRaw(e, offset) | ||
if err != nil { | ||
fmt.Printf("failed to parse event: %+v", e) | ||
continue | ||
} | ||
result[i] = e | ||
} | ||
|
||
return result | ||
} | ||
|
||
type eventRaw struct { | ||
Name string | ||
Start string | ||
End string | ||
Datestamp string | ||
Summary string | ||
Description string | ||
} | ||
|
||
type event struct { | ||
Name string | ||
Summary string | ||
Description string | ||
Start time.Time | ||
End time.Time | ||
CreatedAt time.Time | ||
} | ||
|
||
func (e event) isPast() bool { | ||
return e.Start.Before(time.Now()) && e.End.Before(time.Now()) | ||
} | ||
|
||
func (e event) isToday() bool { | ||
return e.Start.Day() == time.Now().Day() | ||
} | ||
|
||
func eventFromEventRaw(raw eventRaw, offset time.Duration) (event, error) { | ||
start, err := time.Parse(timeFormat, raw.Start) | ||
if err != nil { | ||
return event{}, err | ||
} | ||
end, _ := time.Parse(timeFormat, raw.End) | ||
datestamp, _ := time.Parse(timeFormat, raw.Datestamp) | ||
|
||
if offset == 0 { | ||
// 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) | ||
} | ||
|
||
return event{ | ||
Name: raw.Name, | ||
Summary: raw.Summary, | ||
Description: raw.Description, | ||
Start: start, | ||
End: end, | ||
CreatedAt: datestamp, | ||
}, nil | ||
} | ||
|
||
func getRefreshPeriod() time.Duration { | ||
period := os.Getenv(notifyEnvRefreshPeriod) | ||
if period == "" { | ||
return defaultRefreshPeriod | ||
} | ||
d, err := strconv.Atoi(period) | ||
if err != nil { | ||
fmt.Println("error parsing refresh period:", err) | ||
return defaultRefreshPeriod | ||
} | ||
return time.Duration(d) * time.Minute | ||
} | ||
|
||
func getTimeBeforeNotify() time.Duration { | ||
period := os.Getenv(notifyEnvTimeBefore) | ||
if period == "" { | ||
return defaultTimeBeforeNotify | ||
} | ||
d, err := strconv.Atoi(period) | ||
if err != nil { | ||
fmt.Println("error parsing time before notify:", err) | ||
return defaultTimeBeforeNotify | ||
} | ||
return time.Duration(d) * time.Minute | ||
} | ||
|
||
func getOffsetByServer() time.Duration { | ||
offsetHours := os.Getenv(notifyEnvOffset) | ||
if offsetHours == "" { | ||
return 0 | ||
} | ||
|
||
intOffset, err := strconv.Atoi(offsetHours) | ||
if err != nil { | ||
return 0 | ||
} | ||
|
||
return time.Duration(intOffset) * time.Hour | ||
} | ||
|
||
func processExitError(err error) { | ||
ee := &exec.ExitError{} | ||
if !errors.As(err, &ee) || !strings.Contains(string(ee.Stderr), "Unauthorized") { | ||
fmt.Printf("failed to run caldav-fetch.py: \n%s", err) | ||
os.Exit(1) | ||
} | ||
|
||
fmt.Println("Unauthorized. Please check your credentials in python script.") | ||
os.Exit(1) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
icalendar | ||
caldav | ||
json |