calendarpush.py 4.96 KB
Newer Older
Robin Sonnabend's avatar
Robin Sonnabend committed
1
2
3
4
5
from datetime import datetime, timedelta
import random
import quopri

from caldav import DAVClient, Principal, Calendar, Event
6
from caldav.lib.error import PropfindError
Robin Sonnabend's avatar
Robin Sonnabend committed
7
8
9
10
11
12
13
14
15
16
17
from vobject.base import ContentLine

import config

class CalendarException(Exception):
    pass

class Client:
    def __init__(self, calendar=None, url=None):
        self.url = url if url is not None else config.CALENDAR_URL
        self.client = DAVClient(self.url)
18
19
20
21
22
23
24
25
26
        self.principal = None
        for _ in range(config.CALENDAR_MAX_REQUESTS):
            try:
                self.principal = self.client.principal()
                break
            except PropfindError as exc:
                print(exc)
        if self.principal is None:
            raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS))
Robin Sonnabend's avatar
Robin Sonnabend committed
27
28
29
30
31
32
        if calendar is not None:
            self.calendar = self.get_calendar(calendar)
        else:
            self.calendar = calendar

    def get_calendars(self):
33
34
35
36
37
38
39
40
41
42
43
44
        if not config.CALENDAR_ACTIVE:
            return
        for _ in range(config.CALENDAR_MAX_REQUESTS):
            try:
                return [
                    calendar.name
                    for calendar in self.principal.calendars()
                ]
            except PropfindError as exc:
                print(exc)
        raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS))

Robin Sonnabend's avatar
Robin Sonnabend committed
45
46
47
48
49
50
51
52
53

    def get_calendar(self, calendar_name):
        candidates = self.principal.calendars()
        for calendar in candidates:
            if calendar.name == calendar_name:
                return calendar
        raise CalendarException("No calendar named {}.".format(calendar_name))

    def set_event_at(self, begin, name, description):
54
55
        if not config.CALENDAR_ACTIVE:
            return
Robin Sonnabend's avatar
Robin Sonnabend committed
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
        candidates = [
            Event.from_raw_event(raw_event)
            for raw_event in self.calendar.date_search(begin)
        ]
        candidates = [event for event in candidates if event.name == name]
        event = None
        if len(candidates) == 0:
            event = Event(None, name, description, begin,
                begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION))
            vevent = self.calendar.add_event(event.to_vcal())
            event.vevent = vevent
        else:
            event = candidates[0]
        event.set_description(description)
        event.vevent.save()


NAME_KEY = "summary"
DESCRIPTION_KEY = "description"
BEGIN_KEY = "dtstart"
END_KEY = "dtend"
def _get_item(content, key):
    if key in content:
        return content[key][0].value
    return None
        
class Event:
    def __init__(self, vevent, name, description, begin, end):
        self.vevent = vevent
        self.name = name
        self.description = description
        self.begin = begin
        self.end = end

    @staticmethod
    def from_raw_event(vevent):
        raw_event = vevent.instance.contents["vevent"][0]
        content = raw_event.contents
        name = _get_item(content, NAME_KEY)
        description = _get_item(content, DESCRIPTION_KEY)
        begin = _get_item(content, BEGIN_KEY)
        end = _get_item(content, END_KEY)
        return Event(vevent=vevent, name=name, description=description,
            begin=begin, end=end)

    def set_description(self, description):
        raw_event = self.vevent.instance.contents["vevent"][0]
        self.description = description
        encoded = encode_quopri(description)
        if DESCRIPTION_KEY not in raw_event.contents:
            raw_event.contents[DESCRIPTION_KEY] = [ContentLine(DESCRIPTION_KEY, {"ENCODING": ["QUOTED-PRINTABLE"]}, encoded)]
        else:
            content_line = raw_event.contents[DESCRIPTION_KEY][0]
            content_line.value = encoded
            content_line.params["ENCODING"] = ["QUOTED-PRINTABLE"]

    def __repr__(self):
        return "<Event(name='{}', description='{}', begin={}, end={})>".format(
            self.name, self.description, self.begin, self.end)

    def to_vcal(self):
        offset = get_timezone_offset()
        return """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//FSMPI Protokollsystem//CalDAV Client//EN
BEGIN:VEVENT
UID:{uid}
DTSTAMP:{now}
DTSTART:{begin}
DTEND:{end}
SUMMARY:{summary}
DESCRIPTION;ENCODING=QUOTED-PRINTABLE:{description}
END:VEVENT
END:VCALENDAR""".format(
            uid=create_uid(), now=date_format(datetime.now()-offset),
            begin=date_format(self.begin-offset), end=date_format(self.end-offset),
            summary=self.name,
            description=encode_quopri(self.description))

def create_uid():
    return str(random.randint(0, 1e10)).rjust(10, "0")

def date_format(dt):
    return dt.strftime("%Y%m%dT%H%M%SZ")

def get_timezone_offset():
    difference = datetime.now() - datetime.utcnow()
    return timedelta(hours=round(difference.seconds / 3600 + difference.days * 24))

def encode_quopri(text):
    return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0D=0A").decode("utf-8")