diff --git a/.gitignore b/.gitignore index 585b27a..99a923a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,98 @@ -*.json -__pycache__ \ No newline at end of file +key.json + +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# End of https://www.gitignore.io/api/python \ No newline at end of file diff --git a/README.md b/README.md index 4e971ed..4b3c120 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Support - Get cTag, eTag, calendar event Info - Diff with cached data(ctag, etag) - ICS Parsing + Basic Useage ------------ ```python @@ -51,7 +52,7 @@ This sample will show this result. CalDav Server ------------ -- Naver : https://caldav.calendar.naver.com +- Naver : https://caldav.calendar.naver.com/principals/ - apple : https://caldav.apple.com - Google : I recommend using its own api. - yahoo : https://caldav.calendar.yahoo.com diff --git a/caldavclient/caldavclient.py b/caldavclient/caldavclient.py index e6dfaff..7f0e415 100644 --- a/caldavclient/caldavclient.py +++ b/caldavclient/caldavclient.py @@ -1,14 +1,21 @@ from caldavclient import static from xml.etree.ElementTree import * from caldavclient import util +import jicson class CaldavClient: - def __init__(self, hostname, id, pw): + def __init__(self, hostname, auth = None): self.hostname = hostname - self.auth = (id, pw) + self.auth = auth + self.principal = None def getPrincipal(self): + if self.principal is None: + self.updatePrincipal() + return self.principal + + def updatePrincipal(self): ret = util.requestData( hostname = self.hostname, depth = 0, @@ -18,7 +25,8 @@ def getPrincipal(self): # xmlTree = ElementTree(fromstring(ret.content)).getroot() xmlTree = util.XmlObject(ret.content) - + xmlTree.find("response").text + principal = self.Principal( hostname = self.hostname, principalUrl = xmlTree.find("response") @@ -28,7 +36,40 @@ def getPrincipal(self): .find("href").text(), client = self ) + self.principal = principal return principal + + def setPrincipal(self, principal): + self.principal = self.Principal( + hostname = self.hostname, + principalUrl = principal, + client = self + ) + return self + + def setHomeSet(self, homeset): + if self.principal == None: + raise Exception('principal is not inited') + else: + self.principal.homeset = self.HomeSet( + hostname = self.principal.hostname, + homesetUrl = homeset, + client = self + ) + return self + + def setCalendars(self, calendarList): + if self.principal == None: + raise Exception('principal is not inited') + elif self.principal.homeset == None: + raise Exception('homeset is not inited') + else: + for calendar in calendarList: + calendar.hostname = self.principal.homeset.hostname + calendar.domainUrl = calendar.hostname + calendar.calendarUrl + calendar.client = self + self.principal.homeset.calendarList = calendarList + return self class Principal: @@ -37,8 +78,14 @@ def __init__(self, hostname, principalUrl, client): self.principalUrl = principalUrl self.domainUrl = self.hostname + self.principalUrl self.client = client + self.homeset = None - def getCalendars(self): + def getHomeSet(self): + if self.homeset is None: + self.updateHomeSet() + return self.homeset + + def updateHomeSet(self): ## load calendar url ret = util.requestData( hostname = self.domainUrl, @@ -50,29 +97,55 @@ def getCalendars(self): # xmlTree = ElementTree(fromstring(ret.content)).getroot() xmlTree = util.XmlObject(ret.content) - calendarUrl = ( - xmlTree.find("response") + homeset = self.client.HomeSet( + hostname = self.hostname, + homesetUrl = xmlTree.find("response") .find("propstat") .find("prop") .find("calendar-home-set") - .find("href").text() + .find("href").text(), + client = self.client ) + self.homeset = homeset + return homeset + + def isListHasChanges(self, calendarList): + newCalendarList = self.getCalendars() + + newCalendarDict = util.calListToDict(newCalendarList) + calendarDict = util.calListToDict(calendarList) + dictDiffer = util.DictDiffer(newCalendarDict, calendarDict) + + return dictDiffer.changed() + + class HomeSet: + def __init__(self, hostname, homesetUrl, client): + self.hostname = hostname + self.homesetUrl = homesetUrl + self.client = client + self.calendarList = None + + def getCalendars(self): + if self.calendarList is None: + self.updateCalendars() + return self.calendarList + + def updateCalendars(self): ## load calendar info (name, id, ctag) ret = util.requestData( - hostname = util.mixHostUrl(self.hostname, calendarUrl), + hostname = util.mixHostUrl(self.hostname, self.homesetUrl), depth = 1, data = static.XML_REQ_CALENDARINFO, auth = self.client.auth ) - # xmlTree = ElementTree(fromstring(ret.content)).getroot() xmlTree = util.XmlObject(ret.content) calendarList = [] for response in xmlTree.iter(): - if response.find("href").text() == calendarUrl: + if response.find("href").text() == self.homesetUrl: continue calendar = self.client.Calendar( hostname = self.hostname, @@ -86,38 +159,103 @@ def getCalendars(self): client = self.client ) calendarList.append(calendar) + self.calendarList = calendarList return calendarList - - def isListHasChanges(self, calendarList): - newCalendarList = self.getCalendars() - newCalendarDict = util.calListToDict(newCalendarList) - calendarDict = util.calListToDict(calendarList) - dictDiffer = util.DictDiffer(newCalendarDict, calendarDict) - - - return dictDiffer.changed() class Calendar: - - def __init__(self, calendarUrl, cTag, client): - self.hostname = util.getHostnameFromUrl(client.hostname) + """ + def __init__(self, calendarUrl, calendarName, cTag): +# self.hostname = util.getHostnameFromUrl(hostname) + self.calendarId = util.splitIdfromUrl(calendarUrl) self.calendarUrl = calendarUrl + self.calendarName = calendarName self.cTag = cTag - self.eventList = [] - self.domainUrl = self.hostname + calendarUrl - self.client = client +# self.domainUrl = self.hostname + calendarUrl +# self.client = client + self.eventList = None + """ - def __init__(self, hostname, calendarUrl, calendarName, cTag, client): + def __init__(self, calendarUrl, calendarName, cTag, client = None, hostname = None): self.hostname = util.getHostnameFromUrl(hostname) + self.calendarId = util.splitIdfromUrl(calendarUrl) self.calendarUrl = calendarUrl self.calendarName = calendarName - self.eventList = [] self.cTag = cTag self.domainUrl = self.hostname + calendarUrl self.client = client + self.eventList = None + + + def isChanged(self): + oldcTag = self.cTag + newcTag = self.getCTag() + return oldcTag != newcTag def getAllEvent(self): + if self.eventList is None: + self.updateAllEvent() + return self.eventList + + def getCalendarData(self, eventList): + + splitRange = 40 + resultList = [] + for i in range(0, len(eventList),splitRange): + eventSubList = eventList[i:i+splitRange] + + sendDataList = [] + for eventItem in eventSubList: + sendDataList.append("%s" % (eventItem.eventUrl)) + sendData = "\n".join(sendDataList) + + ret = util.requestData( + method = "REPORT", + hostname=self.domainUrl, + depth=1, + data=static.XML_REQ_CALENDARDATA % (sendData), + auth = self.client.auth + ) + + xmlTree = util.XmlObject(ret.content) + + for response in xmlTree.iter(): + if response.find("href").text == self.calendarUrl: + continue + event = self.client.Event( + eventUrl = response.find("href").text(), + eTag = response.find("propstat").find("prop").find("getetag").text(), + eventData = response.find("propstat").find("prop").find("calendar-data").text() + ) + resultList.append(event) + + return resultList + + + def getEventByRange(self, stDate, edDate): + ret = util.requestData( + method = "REPORT", + hostname=self.domainUrl, + depth=1, + data=static.XML_REQ_CALENDARDATEFILTER % (stDate, edDate), + auth = self.client.auth + ) + + xmlTree = util.XmlObject(ret.content) + + eventList = [] + for response in xmlTree.iter(): + if response.find("href").text == self.calendarUrl: + continue + event = self.client.Event( + eventUrl = response.find("href").text(), + eTag = response.find("propstat").find("prop").find("getetag").text() + ) + eventList.append(event) + + return eventList + + def updateAllEvent(self): ## load all event (etag, info) ret = util.requestData( hostname = self.domainUrl, @@ -143,7 +281,6 @@ def getAllEvent(self): self.eventList = eventList return eventList - ## TODO - ctag만 불러올 수 있는 쿼리 찾아보기 def getCTag(self): ## load ctag ret = util.requestData( @@ -153,7 +290,15 @@ def getCTag(self): auth = self.client.auth ) + xmlTree = util.XmlObject(ret.content) + cTag = xmlTree.find("response").find("propstat").find("prop").find("getctag").text() + self.cTag = cTag + return cTag + class Event: - def __init__(self, eventUrl, eTag): + def __init__(self, eventUrl, eTag, eventData = None): self.eventUrl = eventUrl - self.eTag = eTag \ No newline at end of file + self.eventId = util.splitIdfromUrl(eventUrl) + self.eTag = eTag + if eventData is not None: + self.eventData = util.parseICS(eventData) \ No newline at end of file diff --git a/caldavclient/static.py b/caldavclient/static.py index f3ad375..58e6846 100644 --- a/caldavclient/static.py +++ b/caldavclient/static.py @@ -35,7 +35,12 @@ ) XML_REQ_CALENDARCTAG = ( - + "" + "" + " " + " " + " " + "" ) XML_REQ_CALENDARETAG = ( @@ -49,4 +54,49 @@ " " " " "" +) + +XML_REQ_CALENDARDATEFILTER = ( +""" + + + + + + + + + + + + +""" +) + +XML_REQ_CALENDARDATA = ( +""" + + + + + + + + + + + + + + + + + + + + + + %s + +""" ) \ No newline at end of file diff --git a/caldavclient/util.py b/caldavclient/util.py index 494a07e..ac73c65 100644 --- a/caldavclient/util.py +++ b/caldavclient/util.py @@ -1,23 +1,61 @@ +import sys +# Add the ptdraft folder path to the sys.path list + import requests from urllib.parse import urlparse from xml.etree.ElementTree import * +from caldavclient import caldavclient +from datetime import datetime +import json +from icalendar import Calendar, Event +import icalendar def requestData(method = "PROPFIND", hostname = "", depth = 0, data = "", auth = ("","")): - response = requests.request( - method, - hostname, - data = data, - headers = { - "Depth" : str(depth) - }, - auth = auth - ) + if isinstance(auth, tuple): + response = requests.request( + method, + hostname, + data = data, + headers = { + "Depth" : str(depth) + }, + auth = auth + ) + else: + response = requests.request( + method, + hostname, + data = data, + headers = { + "Depth" : str(depth), + "Authorization" : "Basic " + str(auth) + }, + ) if response.status_code<200 or response.status_code>299: raise Exception('http code error' + str(response.status_code)) return response +def parseICS(ics): + dictResult = {} + dictResult['VEVENT'] = {} + calendar = Calendar.from_ical(ics) + for component in calendar.walk(): + if component.name == "VEVENT": + for row in component.property_items(): + # TODO : TRIGGER 키값은 decode가 안되는 문제 해결 필요 + if row[0] == "TRIGGER": + continue + if isinstance(row[1], icalendar.prop.vDDDTypes): + result = component.decoded(row[0]) + else: + result = str(row[1]) + + dictResult['VEVENT'][row[0]] = result + return dictResult + + def getHostnameFromUrl(url): parsedUrl = urlparse(url) hostname = '{uri.scheme}://{uri.netloc}'.format(uri=parsedUrl) @@ -29,6 +67,14 @@ def mixHostUrl(hostname, url): else: return hostname + url +def splitIdfromUrl(url): + if len(url) < 1: + return url + url = url.replace('.ics', '') + if url[-1] == "/": + url = url[:-1] + return url.split('/')[-1] + class XmlObject: def __init__(self, xml = None): @@ -42,6 +88,8 @@ def __init__(self, xml = None): def addNamespace(self, tag): if tag == "calendar-home-set": tag = ".//{urn:ietf:params:xml:ns:caldav}" + tag + elif tag == "calendar-data": + tag = ".//{urn:ietf:params:xml:ns:caldav}" + tag elif tag == "getctag": tag = ".//{http://calendarserver.org/ns/}" + tag else: @@ -99,12 +147,29 @@ def eventListToDict(eventList): eventDict[event.eventUrl] = event.eTag return eventDict +def eventRowToList(eventRow): + eventList = [] + for row in eventRow: + event = caldavclient.CaldavClient.Event( + eventUrl = row['event_url'], + eTag = row['e_tag'] + ) + eventList.append(event) + return eventList + +def findETag(eventList, eventUrl): + for event in eventList: + if event.eventUrl == eventUrl: + if event.eTag is None: + return "" + return event.eTag + def findCalendar(key, list): for calendar in list: if calendar.calendarUrl == key: return calendar -def diffCalendar(oldList, newList): +def diffCalendars(oldList, newList): diffList = [] for calendar in oldList: newCalendar = findCalendar(calendar.calendarUrl, newList) diff --git a/main.py b/main.py index 36f9948..94627fe 100644 --- a/main.py +++ b/main.py @@ -5,34 +5,137 @@ with open('key.json') as json_data: d = json.load(json_data) - userId = d['apple']['id'] - userPw = d['apple']['pw'] + userId = d['naver2']['id'] + userPw = d['naver2']['pw'] # naver : https://caldav.calendar.naver.com:443/caldav/jspiner/calendar/ # apple : caldav.icloud.com ##calendar load example client = CaldavClient( - "https://caldav.icloud.com", -# "https://caldav.calendar.naver.com/principals/users/jspiner", - userId, - userPw +# "https://caldav.icloud.com", + "https://caldav.calendar.naver.com/principals/", + (userId, userPw) ) principal = client.getPrincipal() -calendars = principal.getCalendars() +homeset = principal.getHomeSet() +calendars = homeset.getCalendars() for calendar in calendars: print(calendar.calendarName + " " + calendar.calendarUrl + " " + calendar.cTag) -eventList = calendars[0].getAllEvent() -for event in eventList: - print (event.eTag) +eventList = calendars[0].getEventByRange( "20161117T000000Z", "20170325T000000Z") +eventDataList = calendars[0].getCalendarData(eventList) +data = eventDataList[0].eventData +print(data) +for event in eventDataList: + print (event.eventData) + print("===") + + +data = ( +""" +BEGIN:VCALENDAR +PRODID:-//NHN Corp//Naver Calendar 1.0//KO +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +BEGIN:VTIMEZONE +TZID:Asia/Seoul +TZURL:http://tzurl.org/zoneinfo-outlook/Asia/Seoul +X-LIC-LOCATION:Asia/Seoul +BEGIN:STANDARD +TZOFFSETFROM:+0900 +TZOFFSETTO:+0900 +TZNAME:KST +DTSTART:19700101T000000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20141126T034341Z +LAST-MODIFIED:20141126T034341Z +DTSTAMP:20170221T071734Z +UID:35BC4AF5-2376-4473-A422-D63366885BB2:88_ios_import +TRANSP:TRANSPARENT +STATUS:TENTATIVE +SEQUENCE:0 +SUMMARY:추석 연휴 +DESCRIPTION: +DTSTART;VALUE=DATE:20160916 +DTEND;VALUE=DATE:20160917 +CLASS:PUBLIC +LOCATION: +PRIORITY:5 +X-NAVER-STICKER-ID:001 +X-NAVER-STICKER-POS:0 +X-NAVER-STICKER-DEFAULT-POS:1 +X-NAVER-CATEGORY-ID:0 +X-NAVER-SCHEDULE-DETAIL-VIEW-URL:https://calendar.naver.com/calapp/main.nhn#HistoryData=%7B%22sType%22%3A%22Layer%22%2C%22sUIO%22%3A%22ViewSchedule%22%2C%22sCalendarId%22%3A%225272575%22%2C%22sScheduleId%22%3A%22692595578%22%2C%22nScheduleType%22%3A2%2C%22sStartDate%22%3A%222016-09-16%2000%3A00%3A00%22%7D +X-NAVER-WRITER-ID:kkk1140 +END:VEVENT +END:VCALENDAR +""" +) +""" +from icalendar import Calendar, Event +import icalendar +calendar = Calendar.from_ical(data) +for component in calendar.walk(): + if component.name == "VEVENT": + for row in component.property_items(): + if isinstance(row[1], icalendar.prop.vDDDTypes): + result = component.decoded(row[0]) + print(str(row[0]) + " : " + str(type(result)) + " // " + str(result)) + else: + print(str(row[0]) + " : " + str(row[1])) +""" + +""" +##calendar sync example(new) + +#client 객체에 db에서 데이터를 불러와 넣어줌 +client = ( + CaldavClient( + hostname, + userId, + userPw + ).setPrincipal("principal_url") #db 에서 로드 + .setHomeSet("home_set_cal_url") #db 에서 로드 + .setCalendars("calendarList") #db에서 로드해서 list calendar object 로 삽입 +) + +calendars = client.getPrincipal().getHomeSet().getCalendars() + +## 주기적으로 돌면서 diff 체크 +while True: + print("start sync") + + ##동기화할 캘린더 선택 + calendarToSync = calendars[0] + if calendarToSync.isChanged(): + print("something changed") + newEventList = calendarToSync.updateAllEvent() + oldEventList = [] #db에서 이전 event리스트들을 불러옴 + eventDiff = util.diffEvent(newEventList, oldEventList) + + + print("add : " + str(eventDiff.added())) + print("removed : " + str(eventDiff.removed())) + print("changed : " + str(eventDiff.changed())) + print("unchanged : " + str(eventDiff.unchanged())) + else: + print("nothing changed") + + + + time.sleep(10) +""" -##calendar sync example +##calendar sync example(old) """ client = CaldavClient( "https://caldav.calendar.naver.com/principals/users/jspiner",