diff --git a/infomentor/__init__.py b/infomentor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infomentor/__main__.py b/infomentor/__main__.py new file mode 100644 index 0000000..d94a463 --- /dev/null +++ b/infomentor/__main__.py @@ -0,0 +1,153 @@ +import infomentor.flock as flock +import logging +import argparse +import datetime +import sys +import os +from infomentor import db, model, connector, informer + + +logformat='{asctime} - {name:25s} - {levelname:8s} - {message}' +def logtofile(): + logging.basicConfig( + level=logging.INFO, + format=logformat, + filename='log.txt', + filemode='a+', + style='{' + ) +def logtoconsole(): + logging.basicConfig( + level=logging.DEBUG, + format=logformat, + style='{' + ) + +def parse_args(arglist): + parser = argparse.ArgumentParser(description='Infomentor Grabber and Notifier') + 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('--addpushover', type=str, help='add pushover') + parser.add_argument('--addmail', type=str, help='add mail') + args = parser.parse_args(arglist) + return args + +def add_user(username): + session = db.get_db() + existing_user = session.query(model.User.name == username).one_or_none() + if existing_user is not None: + print('user exists, change pw') + else: + print(f'Adding user: {username}') + + import getpass + password = getpass.getpass(prompt='Password: ') + if existing_user is not None: + existing_user.password = password + else: + user = model.User(name=username, password=password) + session.add(user) + session.commit() + + +def add_pushover(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 PUSHOVER for user: {username}') + id = input('PUSHOVER ID: ') + user.notification = model.Notification(ntype=model.Notification.Types.PUSHOVER, info=id) + session.commit() + +def add_mail(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 user: {username}') + address = input('MAIL ADDRESS: ') + user.notification = model.Notification(ntype=model.Notification.Types.EMAIL, info=address) + session.commit() + +def notify_users(): + logger = logging.getLogger(__name__) + session = db.get_db() + for user in session.query(model.User): + logger.info('==== USER: %s =====', user.name) + if user.password == '': + logger.warning('User %s not enabled', user.name) + continue + now = datetime.datetime.now() + im = connector.Infomentor(user.name) + im.login(user.password) + logger.info('User loggedin') + statusinfo = {'datetime': now, 'ok': False, 'info': '', 'degraded_count':0} + if user.apistatus is None: + user.apistatus = model.ApiStatus(**statusinfo) + logger.info('Former API status: %s', user.apistatus) + try: + i = informer.Informer(user, im, logger=logger) + i.update_news() + i.update_homework() + statusinfo['ok'] = True + statusinfo['degraded'] = False + except Exception as e: + inforstr = 'Exception occured:\n{}:{}\n'.format(type(e).__name__, e) + statusinfo['ok'] = False + statusinfo['info'] = inforstr + logger.exception("Something went wrong") + finally: + if user.apistatus.ok == True and statusinfo['ok'] == False: + logger.error('Switching to degraded state %s', user.name) + statusinfo['degraded_count'] = 1 + if user.apistatus.ok == False and statusinfo['ok'] == False: + if user.apistatus.degraded_count == 1 and user.wantstatus: + send_status_update(user, statusinfo['info']) + try: + statusinfo['degraded_count'] = user.apistatus['degraded_count'] + 1 + except Exception as e: + statusinfo['degraded_count'] = 1 + if user.apistatus.ok == False and statusinfo['ok'] == True: + statusinfo['info'] = 'Works as expected, failed {} times'.format(user.apistatus.degraded_count) + statusinfo['degraded_count'] = 0 + if user.wantstatus: + send_status_update(user, statusinfo['info']) + user.apistatus.updateobj(statusinfo) + logger.info('New API status: %s', user.apistatus) + session.commit() + +def send_status_update(user, text): + pass + +def main(): + args = parse_args(sys.argv[1:]) + if args.nolog: + logtoconsole() + else: + 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() + if args.adduser: + add_user(args.adduser) + elif args.addpushover: + add_pushover(args.addpushover) + else: + notify_users() + 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/connector.py b/infomentor/connector.py new file mode 100644 index 0000000..8bd2184 --- /dev/null +++ b/infomentor/connector.py @@ -0,0 +1,401 @@ +import requests +import re +import json +import os +import http.cookiejar +import time +import math +import datetime +import contextlib +import logging +import urllib.parse +import uuid +from infomentor import model + +class InfomentorFile(object): + def __init__(self, directory, filename): + if directory is None: + raise Exception('directory is required') + self.filename = filename + self.randomid = str(uuid.uuid4()) + self.directory = directory + + @property + def targetfile(self): + return os.path.join(self.directory, self.fullfilename) + + @property + def targetdir(self): + return os.path.join(self.directory, self.randomid) + + @property + def fullfilename(self): + if self.filename is None: + raise Exception('no filename set') + return os.path.join(self.randomid, self.filename) + + def save_file(self, content): + os.makedirs(self.targetdir, exist_ok=True) + with open(self.targetfile, 'wb+') as f: + f.write(content) + + +class Infomentor(object): + '''Basic object for handling infomentor site login and fetching of data''' + + BASE_IM1 = 'https://im1.infomentor.de/Germany/Germany/Production' + BASE_MIM = 'https://mein.infomentor.de' + + def __init__(self, user, logger=None): + '''Create informentor object for username''' + self.logger = logger or logging.getLogger(__name__) + self._last_result = None + self.user = user + self._create_session() + + def _create_session(self): + '''Create the session for handling all further requests''' + self.session = requests.Session() + self.session.headers.update({'User-Agent': 'Mozilla/5.0'}) + self._load_cookies() + + def _load_cookies(self): + '''Setup the cookie requests''' + os.makedirs('cookiejars', exist_ok=True) + self.session.cookies = http.cookiejar.MozillaCookieJar( + filename='cookiejars/{}.cookies'.format(self.user) + ) + with contextlib.suppress(FileNotFoundError): + self.session.cookies.load(ignore_discard=True, ignore_expires=True) + + def login(self, password): + '''Login using the given password''' + if self.logged_in(self.user): + return True + self._do_login(self.user, password) + return self.logged_in(self.user) + + def logged_in(self, username): + '''Check if user is logged in (with cookies)''' + ts = math.floor(time.time()) + auth_check_url = 'authentication/authentication/' + \ + 'isauthenticated/?_={}000'.format(ts) + url = self._mim_url(auth_check_url) + r = self._do_post(url) + self.logger.info('%s loggedin: %s', username, r.text) + return r.text.lower() == 'true' + + def _do_login(self, user, password): + self._do_request_initial_token() + self._perform_login(password) + self._finalize_login() + + def _do_request_initial_token(self): + '''Request initial oauth_token''' + # Get the initial oauth token + self._do_get(self._mim_url()) + self._oauth_token = self._get_auth_token() + # This request is performed by the browser, the reason is unclear + login_url = self._mim_url( + 'Authentication/Authentication/Login?ReturnUrl=%2F') + self._do_get(login_url) + + def _get_auth_token(self): + '''Reading oauth_token from response text''' + token_re = r'name="oauth_token" value="([^"]*)"' + tokens = re.findall(token_re, self._last_result.text) + if len(tokens) != 1: + self.logger.error('OAUTH_TOKEN not found') + raise Exception('Invalid Count of tokens') + return tokens[0] + + def _perform_login(self, password): + self._do_post( + self._im1_url('mentor/'), + data={'oauth_token': self._oauth_token} + ) + # Extract the hidden fields content + payload = self._get_hidden_fields() + # update with the missing and the login parameters + payload.update({ + 'login_ascx$txtNotandanafn': self.user, + 'login_ascx$txtLykilord': password, + '__EVENTTARGET': 'login_ascx$btnLogin', + '__EVENTARGUMENT': '' + }) + + # perform the login + self._do_post( + self._im1_url('mentor/'), + data=payload, + headers={ + 'Referer': self._im1_url('mentor/'), + 'Content-Type': 'application/x-www-form-urlencoded' + } + ) + + def _get_hidden_fields(self): + hiddenfields = self._extract_hidden_fields() + field_values = {} + for f in hiddenfields: + names = re.findall('name="([^"]*)"', f) + if len(names) != 1: + self.logger.error('Could not parse hidden field (fieldname)') + continue + values = re.findall('value="([^"]*)"', f) + if len(values) != 1: + self.logger.error('Could not parse hidden field (value)') + continue + field_values[names[0]] = values[0] + return field_values + + def _extract_hidden_fields(self): + hidden_re = '' + hiddenfields = re.findall(hidden_re, self._last_result.text) + return hiddenfields + + def _finalize_login(self): + # Read the oauth token which is the final token for the login + oauth_token = self._get_auth_token() + # authenticate + self._do_post( + self._im1_url('mentor/'), + data={'oauth_token': oauth_token} + ) + self._do_get(self._mim_url()) + + def _do_post(self, url, **kwargs): + '''Post request for session''' + self.logger.info('post to: %s', url) + if 'data' in kwargs: + self.logger.info('data: %s', json.dumps(kwargs['data'], indent=2)) + self._last_result = self.session.post(url, **kwargs) + self.logger.info('result: %d', self._last_result.status_code) + self._save_cookies() + return self._last_result + + def _do_get(self, url, **kwargs): + '''get request for session''' + self.logger.info('get: %s', url) + self._last_result = self.session.get(url, **kwargs) + self.logger.info('result: %d', self._last_result.status_code) + self._save_cookies() + if self._last_result.status_code != 200: + raise Exception('Got response with code {}'.format( + self._last_result.status_code + )) + return self._last_result + + def _save_cookies(self): + '''Save cookies''' + self.session.cookies.save(ignore_discard=True, ignore_expires=True) + + def download_file(self, url, filename=None, directory=None): + '''download a file with given name or provided filename''' + self.logger.info('fetching download: %s', url) + if filename is not None or directory is not None: + return self._download_file(url, directory, filename) + else: + self.logger.error('fetching download requires filename or folder') + raise Exception('Download Failed') + + def _get_filename_from_cd(self): + '''determine filename from headers or random uuid''' + cd = self._last_result.headers.get('content-disposition') + if cd: + filename_re = r''' + .* # Anything + (?: + filename=(?P.+) # normal filename + | + filename\*=(?P.+) # extended filename + ) # The filename + (?:$|;.*) # End or more + ''' + fname = re.match(filename_re, cd, flags=re.VERBOSE) + filename = fname.group('native') + if filename is not None and len(filename) != 0: + return filename + filename = fname.group('extended') + if filename is not None and len(filename) != 0: + encoding, string = filename.split("''") + return urllib.parse.unquote(string, encoding) + filename = str(uuid.uuid4()) + self.logger.warning( + 'no filename detected in %s: using random filename %s', + cd, filename) + return filename + + def _download_file(self, url, directory, filename=None): + '''download a file with provided filename''' + file = InfomentorFile(directory, filename) + self.logger.info('to (randomized) directory %s', file.targetdir) + url = self._mim_url(url) + self._do_get(url) + if filename is None: + self.logger.info('determine filename from headers') + filename = self._get_filename_from_cd() + self.logger.info('determined filename: %s', filename) + file.filename = filename + self.logger.info('full filename: %s', file.fullfilename) + file.save_file(self._last_result.content) + return file.fullfilename + + def _build_url(self, path='', base=BASE_IM1): + return '{}/{}'.format(base, path) + + def _mim_url(self, path=''): + return self._build_url(path, base=self.BASE_MIM) + + def _im1_url(self, path=''): + return self._build_url(path, base=self.BASE_IM1) + + def get_news_list(self): + self.logger.info('fetching news') + self._do_post(self._mim_url('News/news/GetArticleList')) + news_json = self.get_json_return() + return [str(i['id']) for i in news_json['items']] + + def parse_news(self, news_json): + idlist = [str(i['id']) for i in im_news['items']] + self.logger.info('Parsing %d news (%s)', im_news['totalItems'], ', '.join(idlist)) + for news_item in reversed(im_news['items']): + newsdata = self.im.get_article(news_item['id']) + + def get_news_article(self, id): + article_json = self.get_article(id) + storenewsdata = { + k: article_json[k] for k in ('title', 'content', 'date') + } + storenewsdata['news_id'] = article_json['id'] + storenewsdata['raw'] = json.dumps(article_json) + storenewsdata['attachments'] = [] + for attachment in article_json['attachments']: + self.logger.info('found attachment %s', attachment['title']) + att_id = re.findall('Download/([0-9]+)?', attachment['url'])[0] + f = self.download_file(attachment['url'], directory='files') + try: + storenewsdata['attachments'].append(model.Attachment(attachment_id=att_id, url=attachment['url'], localpath=f, title=attachment['title'])) + except Exception as e: + self.logger.exception('failed to store attachment') + news = model.News(**storenewsdata) + with contextlib.suppress(Exception): + news.imagefile = self.get_newsimage(id) + return news + + def get_article(self, id): + self.logger.info('fetching article: %s', id) + self._do_post( + self._mim_url('News/news/GetArticle'), + data={'id': id} + ) + return self.get_json_return() + + def get_newsimage(self, id): + self.logger.info('fetching article image: %s', id) + filename = '{}.image'.format(id) + url = self._mim_url('News/NewsImage/GetImage?id={}'.format(id)) + return self.download_file(url, directory='images', filename=filename) + + def get_calendar(self, offset=0, weeks=1): + self.logger.info('fetching calendar') + data = self._get_week_dates(offset=offset, weeks=weeks) + self._do_post( + self._mim_url('Calendar/Calendar/getEntries'), + data=data + ) + return self.get_json_return() + + def get_homework(self, offset=0): + self.logger.info('fetching homework') + startofweek = self._get_start_of_week(offset) + timestamp = startofweek.strftime('%Y-%m-%dT00:00:00.000Z') + data = { + 'date': timestamp, + 'isWeek': True, + } + self._do_post( + self._mim_url('Homework/homework/GetHomework'), + data=data + ) + return self.get_json_return() + + def get_homework_list(self): + self._homework = {} + homeworklist = [] + homework = [] + homework.extend(self.get_homework()) + homework.extend(self.get_homework(1)) + for dategroup in homework: + for hw in dategroup['items']: + if hw['id'] == 0: + continue + else: + self._homework[hw['id']] = hw + homeworklist.append(hw['id']) + return homeworklist + + def get_homework_info(self, id): + hw = self._homework[id] + storehw = { + k: hw[k] for k in ('subject', 'courseElement') + } + storehw['homework_id'] = hw['id'] + storehw['text'] = hw['homeworkText'] + storehw['attachments'] = [] + for attachment in hw['attachments']: + self.logger.info('found attachment %s', attachment['title']) + att_id = re.findall('Download/([0-9]+)?', attachment['url'])[0] + f = self.download_file(attachment['url'], directory='files') + try: + storehw['attachments'].append(model.Attachment(attachment_id=att_id, url=attachment['url'], localpath=f, title=attachment['title'])) + except Exception as e: + self.logger.exception('failed to store attachment') + hw = model.Homework(**storehw) + return hw + + def get_timetable(self, offset=0): + self.logger.info('fetching timetable') + data = self._get_week_dates(offset) + self._do_post( + self._mim_url('timetable/timetable/gettimetablelist'), + data=data + ) + return self.get_json_return() + + def get_json_return(self): + try: + return self._last_result.json() + except json.JSONDecodeError as jse: + self.logger.exception('JSON coudl not be decoded') + self.logger.info('status code: %d', self._last_result.status_code) + self.logger.info('response was: %s', self._last_result.text) + raise + + def _get_week_dates(self, offset=0, weeks=1): + weekoffset = datetime.timedelta(days=7*offset) + + startofweek = self._get_start_of_weekdays() + endofweek = startofweek + datetime.timedelta(days=5+7*(weeks-1)) + + startofweek += weekoffset + endofweek += weekoffset + + now = datetime.datetime.now() + utctime = datetime.datetime.utcnow() + utcoffset = (now.tm_hour - utctime.tm_hour)*60 + + data = { + 'UTCOffset': utcoffset, + 'start': startofweek.strftime('%Y-%m-%d'), + 'end': endofweek.strftime('%Y-%m-%d'), + } + return data + + def _get_start_of_week(self, offset=0): + now = datetime.datetime.now() + dayofweek = now.weekday() + startofweek = now - datetime.timedelta(days=dayofweek) + startofweek -= datetime.timedelta(days=offset*7) + return startofweek + diff --git a/infomentor/db.py b/infomentor/db.py new file mode 100755 index 0000000..a2799ab --- /dev/null +++ b/infomentor/db.py @@ -0,0 +1,16 @@ +from infomentor import model +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +_session = None + +def get_db(filename='infomentor.db'): + global _session + if _session is None: + engine = create_engine(f'sqlite:///{filename}') + model.ModelBase.metadata.create_all(engine) + model.ModelBase.metadata.bind = engine + DBSession = sessionmaker(bind=engine) + _session = DBSession() + return _session + diff --git a/infomentor/flock.py b/infomentor/flock.py new file mode 100644 index 0000000..af6ca3c --- /dev/null +++ b/infomentor/flock.py @@ -0,0 +1,48 @@ +import os + +class flock(object): + filename = '.im.lock' + + def __init__(self): + self.pid = os.getpid() + + def aquire(self): + if self.is_locked(): + return False + with open(self.filename, 'w+') as f: + f.write('{}'.format(self.pid)) + return True + + def release(self): + if self.own_lock(): + os.unlink(self.filename) + + def __del__(self): + self.release() + + def own_lock(self): + lockinfo = self._get_lockinfo() + return lockinfo == self.pid + + def is_locked(self): + lockinfo = self._get_lockinfo() + if not lockinfo: + return False + return self._is_process_active(lockinfo) + + def _is_process_active(self, pid): + try: + os.kill(pid, 0) + return pid != self.pid + except Exception as e: + return False + + def _get_lockinfo(self): + try: + lock = {} + with open(self.filename, 'r') as f: + pid = int(f.read().strip()) + return pid + except Exception as e: + return False + diff --git a/infomentor/informer.py b/infomentor/informer.py new file mode 100755 index 0000000..5147cb4 --- /dev/null +++ b/infomentor/informer.py @@ -0,0 +1,188 @@ +from infomentor import model, db +import logging +import uuid +import os +import re +import dateparser +import datetime +import math +import pushover +pushover.init('***REMOVED***') + +class Informer(object): + def __init__(self, user, im, logger): + self.logger = logger or logging.getLogger(__name__) + self.user = user + self.im = im + + def update_news(self): + session = db.get_db() + newslist = self.im.get_news_list() + for newsid in newslist: + news = session.query(model.News).filter(model.News.news_id == newsid).with_parent(self.user, 'news').one_or_none() + if news is None: + news = self.im.get_news_article(newsid) + self._notify_news(news) + self.user.news.append(news) + session.commit() + + def _notify_news(self, news): + if self.user.notification.ntype == model.Notification.Types.PUSHOVER: + self._notify_news_pushover(news) + elif self.user.notification.ntype == model.Notification.Types.EMAIL: + self._notify_news_mail(news) + else: + raise Exception('invalid notification') + pass + + def _notify_news_pushover(self, news): + text = news.content + for attachment in news.attachments: + fid, fname = attachment.localpath.split('/') + text += '''
Attachment {0}: https://files.hyttioaoa.de/{1}
'''.format(fname, attachment.localpath) + parsed_date = dateparser.parse(news.date) + now = datetime.datetime.now() + parsed_date += datetime.timedelta(hours=now.hour, minutes=now.minute) + timestamp = math.floor(parsed_date.timestamp()) + if len(text) > 900: + url = self._make_site(text) + shorttext = text[:900] + text = '{}...\n\nfulltext saved at: {}'.format(shorttext, url) + text = text.replace('
', '\n') + try: + self.logger.info(text) + self.logger.info(news.title) + if news.imagefile is not None: + image = open(os.path.join('images', news.imagefile), 'rb') + else: + image = None + pushover.Client(self.user.notification.info).send_message( + text, + title=news.title, + attachment=image, + html=True, + timestamp=timestamp + ) + except pushover.RequestError as e: + self.logger.error('Sending notification failed', exc_info=e) + finally: + if image is not None: + image.close() + + def _make_site(self, text): + filename = str(uuid.uuid4()) + fpath = os.path.join('files', filename+'.html') + urlfinder = re.compile("(https?://[^ \n\t]*)") + text = urlfinder.sub(r'\1', text) + text = ' {}'.format(text) + with open(fpath, 'w+') as f: + f.write(text) + return 'https://files.hyttioaoa.de/{}.html'.format(filename) + + def _notify_news_mail(self, news): + # Import the email modules we'll need + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email.mime.base import MIMEBase + from email import encoders + import mimetypes + import smtplib + + outer = MIMEMultipart() + text = news.content.replace('
', '\n') + outer.attach(MIMEText(text + '\n\n')) + outer['Subject'] = f'INFOMENTOR News: {news.title}' + outer['From'] = 'infomentor@09a.de' + outer['To'] = self.user.notification.info + for attachment in news.attachments: + fid, fname = attachment.localpath.split('/') + filename = os.path.join('files', attachment.localpath) + ctype, encoding = mimetypes.guess_type(filename) + if ctype is None or encoding is not None: + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/', 1) + with open(filename, 'rb') as fp: + msg = MIMEBase(maintype, subtype) + msg.set_payload(fp.read()) + encoders.encode_base64(msg) + msg.add_header('Content-Disposition', 'attachment', filename=fname) + outer.attach(msg) + s = smtplib.SMTP_SSL('09a.de') + s.login('infomentor@09a.de', '***REMOVED***') + s.send_message(outer) + s.quit() + + def update_homework(self): + session = db.get_db() + homeworklist = self.im.get_homework_list() + for homeworkid in homeworklist: + homework = session.query(model.Homework).filter(model.Homework.homework_id == homeworkid).with_parent(self.user, 'homeworks').one_or_none() + if homework is None: + homework = self.im.get_homework_info(homeworkid) + self._notify_hw(homework) + self.user.homeworks.append(homework) + session.commit() + + def _notify_hw(self, hw): + if self.user.notification.ntype == model.Notification.Types.PUSHOVER: + self._notify_hw_pushover(hw) + elif self.user.notification.ntype == model.Notification.Types.EMAIL: + self._notify_hw_mail(hw) + else: + raise Exception('invalid notification') + pass + + def _notify_hw_pushover(self, hw): + text = hw.text + for attachment in hw.attachments: + fid, fname = attachment.localpath.split('/') + text += '''
Attachment {0}: https://files.hyttioaoa.de/{1}
'''.format(fname, attachment.localpath) + parsed_date = dateparser.parse(hw.date) + if len(text) > 900: + url = self._make_site(text) + shorttext = text[:900] + text = '{}...\n\nfulltext saved at: {}'.format(shorttext, url) + text = text.replace('
', '\n') + try: + self.logger.info(text) + self.logger.info(hw.subject) + pushover.Client(self.user.notification.info).send_message( + text, + title=hw.title, + html=True, + ) + except pushover.RequestError as e: + self.logger.error('Sending notification failed', exc_info=e) + + def _notify_hw_mail(self, hw): + # Import the email modules we'll need + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email.mime.base import MIMEBase + from email import encoders + import mimetypes + import smtplib + + outer = MIMEMultipart() + text = hw.text.replace('
', '\n') + outer.attach(MIMEText(text + '\n\n')) + outer['Subject'] = f'INFOMENTOR Homework: {hw.subject}' + outer['From'] = 'infomentor@09a.de' + outer['To'] = self.user.notification.info + for attachment in hw.attachments: + fid, fname = attachment.localpath.split('/') + filename = os.path.join('files', attachment.localpath) + ctype, encoding = mimetypes.guess_type(filename) + if ctype is None or encoding is not None: + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/', 1) + with open(filename, 'rb') as fp: + msg = MIMEBase(maintype, subtype) + msg.set_payload(fp.read()) + encoders.encode_base64(msg) + msg.add_header('Content-Disposition', 'attachment', filename=fname) + outer.attach(msg) + s = smtplib.SMTP_SSL('09a.de') + s.login('infomentor@09a.de', '***REMOVED***') + s.send_message(outer) + s.quit() diff --git a/infomentor/model.py b/infomentor/model.py new file mode 100755 index 0000000..4990df4 --- /dev/null +++ b/infomentor/model.py @@ -0,0 +1,144 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean +from sqlalchemy.orm import relationship +from Crypto.Cipher import AES +import base64 +import enum +import hashlib + +ModelBase = declarative_base() + +_PASSWORD_SECRET_KEY = '***REMOVED***' +BS = 16 +def pad(s): + diff = BS - len(s) % BS + return (s + (diff) * chr(diff)).encode('utf8') +def unpad(s): + return s[0:-s[-1]].decode('utf8') + +class User(ModelBase): + __tablename__ = 'users' + + id = Column(Integer, primary_key=True) + name = Column(String) + enc_password = Column(String) + notification = relationship("Notification", back_populates="user", uselist=False) + apistatus = relationship("ApiStatus", back_populates="user", uselist=False) + wantstatus = Column(Boolean) + homeworks = relationship("Homework", back_populates="user") + news = relationship("News",back_populates="user") + + def __init__(self, *args, **kwargs): + self._setup_cipher() + super().__init__(*args, **kwargs) + + def _setup_cipher(self): + if not hasattr(self, 'cipher'): + aeskey = hashlib.sha256(_PASSWORD_SECRET_KEY.encode()).digest() + self.cipher = AES.new(aeskey,AES.MODE_ECB) + + @property + def password(self): + self._setup_cipher() + decoded = self.cipher.decrypt(base64.b64decode(self.enc_password)) + return unpad(decoded) + + @password.setter + def password(self, value): + self._setup_cipher() + encoded = base64.b64encode(self.cipher.encrypt(pad(value))) + self.enc_password = encoded + + def __repr__(self): + return "" % ( + self.name, '*' * len(self.password)) + + +class Notification(ModelBase): + __tablename__ = 'notifications' + + class Types(enum.Enum): + PUSHOVER = 1 + EMAIL = 2 + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + ntype = Column(Enum(Types)) + info = Column(String) + user = relationship("User", back_populates="notification") + + def __repr__(self): + return "".format( + self.ntype, self.info) + + +class Attachment(ModelBase): + __tablename__ = 'attachments' + + id = Column(Integer, primary_key=True) + attachment_id = Column(Integer) + filetype = Column(String) + url = Column(String) + title = Column(String) + localpath = Column(String) + news_id = Column(Integer, ForeignKey('news.id')) + homework_id = Column(Integer, ForeignKey('homework.id')) + + news = relationship("News", back_populates="attachments") + homework = relationship("Homework", back_populates="attachments") + + +class News(ModelBase): + __tablename__ = 'news' + + id = Column(Integer, primary_key=True) + news_id = Column(Integer) + user_id = Column(Integer, ForeignKey('users.id')) + title = Column(String) + content = Column(String) + category = Column(String) + date = Column(String) + imageUrl = Column(String) + imagefile = Column(String) + notified = Column(Boolean, default=False) + raw = Column(String) + attachments = relationship("Attachment", order_by=Attachment.id, back_populates="news", uselist=True) + user = relationship("User", back_populates="news") + + def __repr__(self): + return "" % ( + self.id, self.title) + +class Homework(ModelBase): + __tablename__ = 'homework' + + id = Column(Integer, primary_key=True) + homework_id = Column(Integer) + user_id = Column(Integer, ForeignKey('users.id')) + subject = Column(String) + courseElement = Column(String) + text = Column(String) + date = Column(String) + imageUrl = Column(String) + attachments = relationship("Attachment", order_by=Attachment.id, back_populates="homework") + user = relationship("User", back_populates="homeworks") + +class ApiStatus(ModelBase): + __tablename__ = 'api_status' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + degraded_count = Column(Integer) + datetime = Column(DateTime) + info = Column(String) + ok = Column(Boolean) + user = relationship("User", back_populates="apistatus", uselist=False) + + def updateobj(self, data): + for key, value in data.items(): + setattr(self, key, value) + + def __repr__(self): + return "" % ( + self.ok, self.degraded_count, self.info) + diff --git a/requirements.txt b/requirements.txt index 4f28466..96ea5f7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests -dataset +sqlalchemy dateparser python-pushover +pycrypto diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cd646aa --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +setup( + name = 'infomentor', + version = '1.0.0', + url = 'https://github.com/mypackage.git', + author = 'Matthias Bilger', + author_email = 'matthias@bilger.info', + description = 'grab infomentor news and push them', + packages = find_packages(), + install_requires = ['pycrypt', 'requests'], +)