diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8a2e1f9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,139 @@ +infomentor.db + +# 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 +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +### Python Patch ### +.venv/ + +### Python.VirtualEnv Stack ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + + +# End of https://www.gitignore.io/api/python + +infomentor.ini +infomentor.db +log.* + diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..86662d6 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,16 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: deploy + image: plugins/docker + settings: + registry: registry.d1v3.de + repo: registry.d1v3.de/infomentor + username: + from_secret: docker_username + password: + from_secret: docker_password + tags: ["commit_${DRONE_COMMIT}","build_${DRONE_BUILD_NUMBER}", "latest"] + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d86fc01 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7.3-stretch + +COPY requirements.txt /tmp/ + +RUN pip install -r /tmp/requirements.txt +COPY . /tmp/infomentor +RUN pip install /tmp/infomentor + +RUN useradd --create-home appuser +WORKDIR /home/appuser +USER appuser +VOLUME ["/home/appuser"] + +CMD [ "python", "-m", "infomentor" ] diff --git a/README.md b/README.md index 2a7281c..bac5772 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This tool is designed to check the infomentor portal and send notifications usin python3 -m venv venv source venv/bin/activate python setup.py install -python -m infomentor +infomentor ``` After the first run a `infomentor.ini` file is available which has a few values to be entered. @@ -21,19 +21,19 @@ After the first run a `infomentor.ini` file is available which has a few values Provide the username and password for infomentor. ``` source venv/bin/activate -python -m infomentor --adduser +adduser --username ``` ### Step 2 add notification mechanism ``` source venv/bin/activate -python -m infomentor --addmail +addmail --username ``` or ``` source venv/bin/activate -python -m infomentor --addpushover +addpushover --username ``` ### Step 3 (optional) Add iCloud calendar @@ -42,7 +42,7 @@ It is capable of syncing all the infomentor calendar elements to icloud calendar ``` source venv/bin/activate -python -m infomentor --addcalendar +addcalendar --username ``` ## NB @@ -51,3 +51,21 @@ The login process is a bit scary and mostly hacked. It happens often on the firs The script shall be run every 10 minutes, that will keep the session alive and minimize errors. + +## Docker + +This could be run within docker. You it has a volume `/home/appuser` where all the data is stored. In favour of accessing it from a webserver you should bindmount it. +There also the infomentor.ini should be placed. + +Build the container by `docker build -t infomentor:latest .` and run it like this: + +``` +docker run -v '/var/docker/infomentor/:/home/appuser' infomentor:latest +``` + +for adding an user or all the commands run it adding -it to it, like: + +``` +docker run -it -v '/var/docker/infomentor/:/home/appuser' infomentor:latest adduser +``` + diff --git a/infomentor.ini.sample b/infomentor.ini.sample index 3bc0601..7cb33ba 100644 --- a/infomentor.ini.sample +++ b/infomentor.ini.sample @@ -12,3 +12,5 @@ server = example.org username = infomentor@example.org password = secret1234 +[healthcheck] +url = https://health.d1v3.de/ping/123123123123123 diff --git a/infomentor/__init__.py b/infomentor/__init__.py index e69de29..38c8aa7 100644 --- a/infomentor/__init__.py +++ b/infomentor/__init__.py @@ -0,0 +1 @@ +#from infomentor.__main__ import * diff --git a/infomentor/__main__.py b/infomentor/__main__.py index 1d8a017..2ad8f57 100644 --- a/infomentor/__main__.py +++ b/infomentor/__main__.py @@ -4,16 +4,17 @@ import argparse import datetime import sys import os -from infomentor import db, model, connector, informer +import requests +from infomentor import db, model, connector, informer, config -logformat = "{asctime} - {name:25s} - {levelname:8s} - {message}" +logformat = "{asctime} - {name:25s}[{filename:20s}:{lineno:3d}] - {levelname:8s} - {message}" def logtofile(): from logging.handlers import RotatingFileHandler - handler = RotatingFileHandler("log.txt", maxBytes=51200, backupCount=5) + handler = RotatingFileHandler("log.txt", maxBytes=1024*1024, backupCount=10) logging.basicConfig( level=logging.INFO, format=logformat, handlers=[handler], style="{" ) @@ -28,12 +29,14 @@ def parse_args(arglist): parser.add_argument( "--nolog", action="store_true", help="print log instead of logging to file" ) - parser.add_argument("--adduser", type=str, help="add user") - parser.add_argument("--addfake", type=str, help="add fake") - parser.add_argument("--addpushover", type=str, help="add pushover") - parser.add_argument("--addmail", type=str, help="add mail") - parser.add_argument("--addcalendar", type=str, help="add icloud calendar") + parser.add_argument("--adduser", action='store_true', help="add user") + parser.add_argument("--addfake", action='store_true', help="add fake") + parser.add_argument("--addpushover", action='store_true', help="add pushover") + parser.add_argument("--addmail", action='store_true', help="add mail") + parser.add_argument("--addcalendar", action='store_true', help="add icloud calendar") + parser.add_argument("--addinvitation", action='store_true', help="add calendar invitation") parser.add_argument("--test", action="store_true", help="test") + parser.add_argument("--username", type=str, nargs='?', help="username") args = parser.parse_args(arglist) return args @@ -104,6 +107,18 @@ def add_calendar(username): ) session.commit() +def add_invitation(username): + session = db.get_db() + user = session.query(model.User).filter(model.User.name == username).one_or_none() + if user is None: + print("user does not exist") + return + else: + print(f"Adding Mail for calendar invitation for user: {username}") + mail = input("Mail: ") + user.invitation = model.Invitation(email=mail) + session.commit() + def add_mail(username): session = db.get_db() @@ -123,6 +138,10 @@ def add_mail(username): def notify_users(): logger = logging.getLogger(__name__) session = db.get_db() + cfg = config.load() + if cfg["healthchecks"]["url"] != "": + requests.get(cfg["healthchecks"]["url"]) + for user in session.query(model.User): logger.info("==== USER: %s =====", user.name) if user.password == "": @@ -187,15 +206,17 @@ def main(): logger.info("EXITING - PREVIOUS IS RUNNING") raise Exception() if args.addfake: - add_fake(args.addfake) + add_fake(args.username) elif args.adduser: - add_user(args.adduser) + add_user(args.username) elif args.addpushover: - add_pushover(args.addpushover) + add_pushover(args.username) elif args.addmail: - add_mail(args.addmail) + add_mail(args.username) elif args.addcalendar: - add_calendar(args.addcalendar) + add_calendar(args.username) + elif args.addinvitation: + add_invitation(args.username) else: notify_users() except Exception as e: @@ -204,6 +225,64 @@ def main(): finally: logger.info("EXITING--------------------- %s", os.getpid()) +def run_notify(): + run_without_args(notify_users) + +def run_adduser(): + run_with_args(add_user) + +def run_addfake(): + run_with_args(add_fake) + +def run_addpushover(): + run_with_args(add_pushover) + +def run_addmail(): + run_with_args(add_mail) + +def run_addcalendar(): + run_with_args(add_calendar) + +def run_addinvitation(): + run_with_args(add_invitation) + + +def run_with_args(fct): + args = parse_args(sys.argv[1:]) + logtofile() + logger = logging.getLogger("Infomentor Notifier") + logger.info("STARTING-------------------- %s", os.getpid()) + lock = flock.flock() + try: + if not lock.aquire(): + logger.info("EXITING - PREVIOUS IS RUNNING") + raise Exception() + if args.username is None: + print('Provide Username using --username') + raise Exception('No username provided') + fct(args.username) + except Exception as e: + logger.info("Exceptional exit") + logger.exception("Info") + finally: + logger.info("EXITING--------------------- %s", os.getpid()) + +def run_without_args(fct): + args = parse_args(sys.argv[1:]) + logtofile() + logger = logging.getLogger("Infomentor Notifier") + logger.info("STARTING-------------------- %s", os.getpid()) + try: + lock = flock.flock() + if not lock.aquire(): + logger.info("EXITING - PREVIOUS IS RUNNING") + raise Exception() + fct() + except Exception as e: + logger.info("Exceptional exit") + logger.exception("Info") + finally: + logger.info("EXITING--------------------- %s", os.getpid()) if __name__ == "__main__": main() diff --git a/infomentor/config.py b/infomentor/config.py index 2fc21e8..052495a 100644 --- a/infomentor/config.py +++ b/infomentor/config.py @@ -3,27 +3,36 @@ import os _config = None +_defaults = { + "pushover": { + "apikey": "", + }, + "general": { + "secretkey": "", + "baseurl": "", + "adminmail": "", + "im1url": "https://im1.infomentor.de/Germany/Germany/Production", + "mimrul": "https://mein.infomentor.de", + }, + "smtp": { + "server": "", + "username": "", + "password": "", + }, + "healthchecks": { + "url": "", + }, +} def _set_defaults(config): - config.add_section("pushover") - config.add_section("general") - config.add_section("smtp") - config["pushover"]["apikey"] = "" - config["general"]["secretkey"] = "" - config["general"]["baseurl"] = "" - config["general"]["adminmail"] = "" - config["general"]["im1url"] = "https://im1.infomentor.de/Germany/Germany/Production" - config["general"]["mimurl"] = "https://mein.infomentor.de" - config["smtp"]["server"] = "" - config["smtp"]["username"] = "" - config["smtp"]["password"] = "" + config = _defaults def load(cfg_file="infomentor.ini"): """Load the config from the file""" global _config if _config is None: - _config = configparser.ConfigParser() + _config = configparser.ConfigParser(_defaults) if not os.path.isfile(cfg_file): _set_defaults(_config) save(cfg_file) diff --git a/infomentor/connector.py b/infomentor/connector.py index 10a0988..310dbdc 100644 --- a/infomentor/connector.py +++ b/infomentor/connector.py @@ -10,17 +10,19 @@ import contextlib import logging import urllib.parse import uuid +import glob +import hashlib from infomentor import model, config class InfomentorFile(object): """Represent a file which is downloaded""" - def __init__(self, directory, filename): + def __init__(self, directory, filename, seed=''): if directory is None: raise Exception("directory is required") self.filename = filename - self.randomid = str(uuid.uuid4()) + self.randomid = hashlib.sha1('{}{}'.format(filename, seed).encode('utf-8')).hexdigest() self.directory = directory @property @@ -232,7 +234,7 @@ class Infomentor(object): def _download_file(self, url, directory, filename=None): """download a file with provided filename""" - file = InfomentorFile(directory, filename) + file = InfomentorFile(directory, filename, seed=url) self.logger.info("to (randomized) directory %s", file.targetdir) url = self._mim_url(url) self._do_get(url) @@ -291,7 +293,7 @@ class Infomentor(object): self.logger.exception("failed to store attachment") news = model.News(**storenewsdata) with contextlib.suppress(Exception): - news.imagefile = self.get_newsimage(id) + news.imagefile = self.get_newsimage(article_json["id"]) return news def get_article(self, id): diff --git a/infomentor/icloudcalendar.py b/infomentor/icloudcalendar.py index dc4f55f..b746be3 100644 --- a/infomentor/icloudcalendar.py +++ b/infomentor/icloudcalendar.py @@ -2,11 +2,15 @@ from datetime import datetime import sys from bs4 import BeautifulSoup +import time import caldav from caldav.elements import dav, cdav from lxml import etree import requests from requests.auth import HTTPBasicAuth +import logging + +_logger = logging.getLogger(__name__) class iCloudConnector(object): @@ -14,15 +18,9 @@ class iCloudConnector(object): icloud_url = "https://caldav.icloud.com" username = None password = None - propfind_principal = ( - u"""""" - u"""""" - ) - propfind_calendar_home_set = ( - u"""""" - u"""""" - ) + propfind_principal = '' + propfind_calendar_home_set = " " + def __init__(self, username, password, **kwargs): self.username = username @@ -32,6 +30,7 @@ class iCloudConnector(object): self.discover() self.get_calendars() + # discover: connect to icloud using the provided credentials and discover # # 1. The principal URL @@ -45,40 +44,53 @@ class iCloudConnector(object): # given credentials headers = {"Depth": "1"} auth = HTTPBasicAuth(self.username, self.password) - principal_response = requests.request( + principal_response = self.repeated_request( "PROPFIND", self.icloud_url, auth=auth, - headers=headers, - data=self.propfind_principal.encode("utf-8"), + data=self.propfind_principal ) - if principal_response.status_code != 207: - print("Failed to retrieve Principal: ", principal_response.status_code) - exit(-1) # Parse the resulting XML response soup = BeautifulSoup(principal_response.content, "lxml") self.principal_path = ( soup.find("current-user-principal").find("href").get_text() ) discovery_url = self.icloud_url + self.principal_path + _logger.debug("Discovery url {}".format(discovery_url)) # Next use the discovery URL to get more detailed properties - such as # the calendar-home-set - home_set_response = requests.request( + home_set_response = self.repeated_request( "PROPFIND", discovery_url, auth=auth, - headers=headers, - data=self.propfind_calendar_home_set.encode("utf-8"), + data=self.propfind_calendar_home_set, ) + _logger.debug("Result code: {}".format(home_set_response.status_code)) if home_set_response.status_code != 207: - print("Failed to retrieve calendar-home-set", home_set_response.status_code) - exit(-1) + _logger.error("Failed to retrieve calendar-home-set {}".format(home_set_response.status_code)) + raise Exception("failed to retrieve calender home set {}".format(home_set_response.content)) # And then extract the calendar-home-set URL soup = BeautifulSoup(home_set_response.content, "lxml") self.calendar_home_set_url = soup.find( "href", attrs={"xmlns": "DAV:"} ).get_text() + def repeated_request(self, *args, **kwargs): + for _ in range(0, 5): + response = requests.request( + *args, **kwargs + ) + _logger.debug("Request result code: {}".format(response.status_code)) + if response.status_code != 207: + _logger.error("Failed to retrieve response: {}".format(response.status_code)) + _logger.error("Retry") + time.sleep(0.25) + if response.status_code == 207: + break + else: + raise Exception("failed to retrieve {} {}".format(response.content, response.headers)) + return response + # get_calendars # Having discovered the calendar-home-set url # we can create a local object to control calendars (thin wrapper around @@ -123,3 +135,4 @@ class iCloudConnector(object): ): # to do pass + diff --git a/infomentor/informer.py b/infomentor/informer.py index 5de2fa4..dd769c0 100755 --- a/infomentor/informer.py +++ b/infomentor/informer.py @@ -23,13 +23,14 @@ pushover.init(cfg["pushover"]["apikey"]) class Informer(object): """The Logic part of the infomentor notifier. - + This class offers the methods required to notify a user of new News and Homework items posted on infomentor.""" def __init__(self, user, im, logger): self.logger = logger or logging.getLogger(__name__) self.user = user self.im = im + self.cal = None def send_status_update(self, text): """In case something unexpected happends and the user has activated the feature to get notified about it, this will send out the information""" @@ -56,11 +57,12 @@ class Informer(object): news = ( session.query(model.News) .filter(model.News.news_id == news_entry['id']) + .filter(model.News.date == news_entry['publishedDate']) .with_parent(self.user, "news") .one_or_none() ) if news is not None: - self.logger.debug('Skipping news') + self.logger.debug('Skipping news %s', news_entry['id']) continue news = self.im.get_news_article(news_entry) self._notify_news(news) @@ -68,6 +70,9 @@ class Informer(object): session.commit() def _notify_news(self, news): + if self.user.notification is None: + self.logger.debug('Warn: no notification for user') + return if self.user.notification.ntype == model.Notification.Types.PUSHOVER: self._notify_news_pushover(news) elif self.user.notification.ntype == model.Notification.Types.EMAIL: @@ -233,77 +238,128 @@ class Informer(object): s.send_message(mail) s.quit() + def _send_invitation(self, calobj, to, fr="infomentor@09a.de"): + event = calobj.subcomponents[0] + eml_body = event['description'] + eml_body_bin = event['description'] + msg = MIMEMultipart('mixed') + msg['Reply-To']=fr + msg['Subject'] = event['summary'] + msg['From'] = fr + msg['To'] = to + + part_email = MIMEText(eml_body,"html") + part_cal = MIMEText(calobj.to_ical().decode('utf-8'),'calendar;method=REQUEST') + + msgAlternative = MIMEMultipart('alternative') + msg.attach(msgAlternative) + + ical_atch = MIMEBase('application/ics',' ;name="%s"'%("invite.ics")) + ical_atch.set_payload(calobj.to_ical().decode('utf-8')) + encoders.encode_base64(ical_atch) + ical_atch.add_header('Content-Disposition', 'attachment; filename="%s"'%("invite.ics")) + + eml_atch = MIMEBase('text/plain','') + eml_atch.set_payload('') + encoders.encode_base64(eml_atch) + eml_atch.add_header('Content-Transfer-Encoding', "") + + msgAlternative.attach(part_email) + msgAlternative.attach(part_cal) + self._send_mail(msg) + + + def _setup_icloudconnector(self): + if self.cal is None: + try: + icx = icloudcalendar.iCloudConnector( + self.user.icalendar.icloud_user, self.user.icalendar.password + ) + cname = self.user.icalendar.calendarname + self.cal = icx.get_named_calendar(cname) + if not self.cal: + self.cal = icx.create_calendar(cname) + self.logger.warn("using icloud") + except Exception as e: + self.logger.exception("using icloud dummy connector") + class Dummy(object): + def add_event(self, *args, **kwargs): + pass + self.cal = Dummy() + + def _write_icalendar(self, calend): + try: + self._setup_icloudconnector() + self.cal.add_event(calend.to_ical()) + except Exception as e: + self.logger.exception('Calendar failed') + def update_calendar(self): session = db.get_db() - if self.user.icalendar is None: + if self.user.icalendar is None and self.user.invitation is None: return - icx = icloudcalendar.iCloudConnector( - self.user.icalendar.icloud_user, self.user.icalendar.password - ) - cname = self.user.icalendar.calendarname - cal = icx.get_named_calendar(cname) - if not cal: - cal = icx.create_calendar(cname) - - calentries = self.im.get_calendar() - for entry in calentries: - self.logger.debug(entry) - uid = "infomentor_{}".format(entry["id"]) - event_details = self.im.get_event(entry["id"]) - self.logger.debug(event_details) - calend = Calendar() - event = Event() - event.add("uid", "infomentor_{}".format(entry["id"])) - event.add("summary", entry["title"]) - if not event_details["allDayEvent"]: - event.add("dtstart", dateparser.parse(entry["start"])) - event.add("dtend", dateparser.parse(entry["end"])) - else: - event.add("dtstart", dateparser.parse(entry["start"]).date()) - event.add("dtend", dateparser.parse(entry["end"]).date()) - - description = event_details["notes"] - self.logger.debug(event_details['info']) - self.logger.debug(type(event_details['info'])) - eventinfo = event_details['info'] - self.logger.debug(eventinfo) - self.logger.debug(type(eventinfo)) - for res in eventinfo['resources']: - f = self.im.download_file(res["url"], directory="files") - description += """\nAttachment {0}: {2}/{1}""".format( - res['title'], urllib.parse.quote(f), cfg["general"]["baseurl"] - ) - event.add("description", description) - - calend.add_component(event) - new_cal_entry = calend.to_ical().replace(b"\r", b"") - new_cal_hash = hashlib.sha1(new_cal_entry).hexdigest() - session = db.get_db() - storedata = { - "calendar_id": uid, - "ical": new_cal_entry, - "hash": new_cal_hash, - } - calendarentry = ( - session.query(model.CalendarEntry) - .filter(model.CalendarEntry.calendar_id == uid) - .with_parent(self.user, "calendarentries") - .one_or_none() - ) - if calendarentry is not None: - if calendarentry.hash == new_cal_hash: - self.logger.info("no change for calendar entry {}".format(uid)) - continue + try: + calentries = self.im.get_calendar() + for entry in calentries: + self.logger.debug(entry) + uid = str(uuid.uuid5(uuid.NAMESPACE_URL, "infomentor_{}".format(entry["id"]))) + event_details = self.im.get_event(entry["id"]) + calend = Calendar() + event = Event() + event.add("uid", uid) + event.add("summary", entry["title"]) + event.add("dtstamp", datetime.datetime.now()) + if not event_details["allDayEvent"]: + event.add("dtstart", dateparser.parse(entry["start"])) + event.add("dtend", dateparser.parse(entry["end"])) else: - self.logger.info("update calendar entry {}".format(uid)) - for key, value in storedata.items(): - setattr(calendarentry, key, value) + event.add("dtstart", dateparser.parse(entry["start"]).date()) + event.add("dtend", dateparser.parse(entry["end"]).date()) - else: - self.logger.info("new calendar entry {}".format(uid)) - calendarentry = model.CalendarEntry(**storedata) + description = event_details["notes"] + eventinfo = event_details['info'] + new_cal_entry = calend.to_ical().replace(b"\r", b"") + new_cal_hash = hashlib.sha1(new_cal_entry).hexdigest() + for res in eventinfo['resources']: + f = self.im.download_file(res["url"], directory="files") + description += """\nAttachment {0}: {2}/{1}""".format( + res['title'], urllib.parse.quote(f), cfg["general"]["baseurl"] + ) + event.add("description", description) - self.user.calendarentries.append(calendarentry) - session.commit() - self.logger.debug(new_cal_entry.decode("utf-8")) - cal.add_event(calend.to_ical()) + calend.add_component(event) + new_cal_entry = calend.to_ical().replace(b"\r", b"") + session = db.get_db() + storedata = { + "calendar_id": uid, + "ical": new_cal_entry, + "hash": new_cal_hash, + } + calendarentry = ( + session.query(model.CalendarEntry) + .filter(model.CalendarEntry.calendar_id == uid) + .with_parent(self.user, "calendarentries") + .one_or_none() + ) + if calendarentry is not None: + if calendarentry.hash == new_cal_hash: + self.logger.info("calendar entry UNCHANGED {}".format(uid)) + continue + else: + self.logger.info("calendar entry UPDATED {}".format(uid)) + for key, value in storedata.items(): + setattr(calendarentry, key, value) + + else: + self.logger.info("calendar entry NEW {}".format(uid)) + calendarentry = model.CalendarEntry(**storedata) + + self.logger.debug(new_cal_entry.decode("utf-8")) + if self.user.icalendar is not None: + self._write_icalendar(calend) + if self.user.invitation is not None: + self._send_invitation(calend, self.user.invitation.email) + self.user.calendarentries.append(calendarentry) + session.commit() + except Exception as e: + self.logger.exception('Calendar failed') diff --git a/infomentor/model.py b/infomentor/model.py index eba0db1..98c150d 100755 --- a/infomentor/model.py +++ b/infomentor/model.py @@ -35,6 +35,7 @@ class User(ModelBase): notification = relationship("Notification", back_populates="user", uselist=False) apistatus = relationship("ApiStatus", back_populates="user", uselist=False) icalendar = relationship("ICloudCalendar", back_populates="user", uselist=False) + invitation = relationship("Invitation", back_populates="user", uselist=False) wantstatus = Column(Boolean) homeworks = relationship("Homework", back_populates="user") news = relationship("News", back_populates="user") @@ -236,3 +237,23 @@ class ICloudCalendar(ModelBase): self.icloud_user, self.calendarname, ) + + +class Invitation(ModelBase): + """An icloud account with a calendar name""" + + __tablename__ = "invitation_mails" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + email = Column(String) + user = relationship("User", back_populates="invitation", uselist=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + + def __repr__(self): + return "" % ( + self.email, + ) diff --git a/requirements.txt b/requirements.txt index 67ebd0d..1fb768c 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,29 @@ -requests -sqlalchemy -dateparser -python-pushover -pycrypto -bs4 -flask -flask-bootstrap -caldav -icalendar +Click==7.0 +Flask==1.1.1 +Flask-Bootstrap==3.3.7.1 +Jinja2==2.10.1 +SQLAlchemy==1.3.6 +Werkzeug==0.15.5 +beautifulsoup4==4.8.0 +bs4==0.0.1 +caldav==0.6.1 +certifi==2019.6.16 +chardet==3.0.4 +dateparser==0.7.1 +dominate==2.3.5 +icalendar==4.0.3 +idna==2.8 +itsdangerous==1.1.0 +lxml==4.3.4 +pycrypto==2.6.1 +python-dateutil==2.8.0 +python-pushover==0.4 +pytz==2019.1 +regex==2019.06.08 +requests==2.22.0 +six==1.12.0 +tzlocal==1.5.1 +urllib3==1.25.3 +visitor==0.1.3 +vobject==0.9.6.1 + diff --git a/setup.py b/setup.py index a939076..91ef732 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,14 @@ setup( author_email="matthias@bilger.info", description="grab infomentor news and push or mail them", packages=find_packages(), + entry_points = { + 'console_scripts': [ + 'infomentor=infomentor:main', + 'adduser=infomentor:run_adduser', + 'addmail=infomentor:run_addmail', + 'addpushover=infomentor:run_addpushover', + ], + }, install_requires=[ "pycrypto", "request",