Mercurial > hg > gcmultimerge
view multimerge.py @ 24:1bce0c6a673c
Comments.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Mon, 04 Jul 2016 14:30:22 +0300 |
parents | ff47f8088ef9 |
children | d32e4d4ef163 |
line wrap: on
line source
#!/usr/bin/python # coding=utf-8 ### ### Google Calendar MultiMerge v0.000001 ### (C) 2016 Matti 'ccr' Hamalainen <ccr@tnsp.org> ### ### Python 2.7 <= x < 3 required! Please refer to ### README.txt for information on other depencies. ### import os import sys import signal import re import time import datetime import smtplib from email.mime.text import MIMEText import httplib2 import ConfigParser import oauth2client from oauth2client import client from oauth2client import tools from oauth2client import file from googleapiclient import discovery ### ### Misc. helper functions ### ## Wrapper for print() that does not break when redirecting stdin/out ## because of piped output not having a defined encoding. We default ## to UTF-8 encoding in output here. def gcm_print(smsg): gcm_msgbuf.append(smsg.encode("UTF-8")) if sys.stdout.encoding != None: print(smsg.encode(sys.stdout.encoding)) else: print(smsg.encode("UTF-8")) ## Fatal errors def gcm_fatal(smsg): gcm_print(u"ERROR: "+ smsg) if cfg.email_ok and cfg.email: ## If e-mail is set, send e-mail msg = MIMEText(("\n".join(gcm_msgbuf)).encode("UTF-8"), "plain") msg.set_charset("UTF-8") msg["Subject"] = cfg.email_subject msg["From"] = cfg.email_sender msg["To"] = ",".join(cfg.email_to) try: smtpH = smtplib.SMTP('localhost') smtpH.sendmail(cfg.email_sender, cfg.email_to, msg.as_string()) smtpH.quit() except: gcm_print("FATAL: Oh crap, e-mail sending failed.") sys.exit(1) ## Debug messages def gcm_debug(smsg): if cfg.debug: gcm_print(u"DBG: "+ smsg) else: gcm_msgbuf.append(u"DBG: "+ smsg.encode("UTF-8")) ## Handler for SIGINT signals def gcm_signal_handler(signal, frame): gcm_print("\nQuitting due to SIGINT / Ctrl+C!") sys.exit(0) ## Function for handling Google API credentials def gcm_get_credentials(mcfg): store = oauth2client.file.Storage(mcfg.credential_file) credentials = store.get() if not credentials or credentials.invalid: flow = client.flow_from_clientsecrets(mcfg.secret_file, mcfg.scope) flow.user_agent = mcfg.app_name credentials = tools.run_flow(flow, store, mcfg) if not credentials or credentials.invalid: gcm_fatal("Failed to authenticate / invalid credentials.") return credentials def gcm_dump_events(events): for event in events: ev_start = event["start"].get("dateTime", event["start"].get("date")) ev_end = event["end"].get("dateTime", event["end"].get("date")) gcm_print(u"{0:25} - {1:25} : {2}".format(ev_start, ev_end, event["summary"])) ## ## Class for handling configuration / settings ## class GCMSettings(dict): def __init__(self): self.m_data = {} self.m_saveable = {} self.m_validate = {} self.m_translate = {} def __getattr__(self, name): if name in self.m_data: return self.m_data[name] else: gcm_fatal("GCMSettings.__getattr__(): No such attribute '"+ name +"'.") def mvalidate(self, name, value): if name in self.m_validate and self.m_validate[name]: if not self.m_validate[name](value): gcm_fatal("GCMSettings.mvalidate(): Invalid value for attribute '{0}': {1}".format(name, value)) def mtranslate(self, name, value): if name in self.m_translate and self.m_translate[name]: return self.m_translate[name](value) else: return value def mdef(self, name, saveable, validate, translate, value): self.mvalidate(name, value) self.m_saveable[name] = saveable self.m_validate[name] = validate self.m_translate[name] = translate self.m_data[name] = self.mtranslate(name, value) def mset(self, name, value): self.mvalidate(name, value) if name in self.m_data: self.m_data[name] = self.mtranslate(name, value) else: gcm_fatal("GCMSettings.mset(): No such attribute '"+ name +"'.") def mget(self, name): if name in self.m_data: return self.m_data[name] else: return None def mread(self, cfgparser, sect): for name in self.m_saveable: if cfgparser.has_option(sect, name): value = cfgparser.get(sect, name) self.mset(name, value) gcm_debug("{0} -> '{1}' == {2}".format(name, value, self.mget(name))) def is_str(self, mvalue): return isinstance(mvalue, basestring) def is_string(self, mvalue): return mvalue == None or self.is_str(mvalue) def is_log_level(self, mvalue): if not self.is_str(mvalue): return False else: return mvalue.upper() in ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] def trans_log_level(self, mvalue): return mvalue.upper() def is_filename(self, mvalue): if not self.is_str(mvalue): return False else: return re.match("^[a-z0-9][a-z0-9\.\_\-]+$", mvalue, flags=re.IGNORECASE) def trans_bool(self, mvalue): if self.is_str(mvalue): if re.match("^\s*(true|1|on|yes)\s*$", mvalue, re.IGNORECASE): mvalue = True elif re.match("^\s*(false|0|off|no)\s*$", mvalue, re.IGNORECASE): mvalue = False else: return None return mvalue def is_bool(self, mvalue): mval = self.trans_bool(mvalue) if not isinstance(mval, bool): gcm_fatal("GCMSettings.is_bool(): Invalid boolean value '{0}', should be true|false|1|0|on|off|yes|no.".format(mvalue)) else: return True def trans_list(self, mvalue): morig = mvalue if self.is_str(mvalue): mvalue = re.split("\s*,\s*", mvalue, flags=re.IGNORECASE) if not isinstance(mvalue, list): gcm_fatal("GCMSettings.trans_list(): Could not parse list '{0}'.".format(mvalue)) elif not isinstance(mvalue, list): gcm_fatal("GCMSettings.trans_list(): Invalid value '{0}'.".format(mvalue)) return mvalue def is_list(self, mvalue): return self.trans_list(mvalue) def is_email(self, mvalue): if not self.is_string(mvalue): return False else: return re.match("^.*?\s+<[a-z0-9]+[a-z0-9\.\+\-]*\@[a-z0-9]+[a-z0-9\.\-]+>\s*$|[a-z0-9]+[a-z0-9\.\+\-]*\@[a-z0-9]+[a-z0-9\.\-]+", mvalue, flags=re.IGNORECASE) def trans_email_list(self, mvalue): if mvalue == None: return mvalue else: return self.trans_list(mvalue.strip()) def is_email_list(self, mvalue): mvalue = self.trans_email_list(mvalue) if mvalue != None: for email in mvalue: if not self.is_email(email): gcm_fatal("Invalid e-mail address '{0}' in list {1}.".format(email, ", ".join(mvalue))) return True ### ### Main program starts ### gcm_msgbuf = [] signal.signal(signal.SIGINT, gcm_signal_handler) ## Settings cfg = GCMSettings() cfg.mdef("debug", True, cfg.is_bool, cfg.trans_bool, False) cfg.mdef("email_ok", False, None, None, False) cfg.mdef("email", True, cfg.is_bool, cfg.trans_bool, False) cfg.mdef("email_to", True, cfg.is_email_list, cfg.trans_email_list, None) cfg.mdef("email_sender", True, cfg.is_email, None, None) cfg.mdef("email_subject", True, cfg.is_string, None, "Google Calendar MultiMerge status") cfg.mdef("source_regex", True, cfg.is_string, None, "^R:\s*(.*?)\s*\(\s*(.+?)\s*\)\s*$") cfg.mdef("source_regmap", False, cfg.is_list, cfg.trans_list, [1, 2]) cfg.mdef("source_regmap_len", False, None, None, len(cfg.source_regmap)) cfg.mdef("dest_name", True, cfg.is_string, None, u"Raahen kansainvälisyystoiminta") cfg.mdef("dest_id", True, cfg.is_string, None, None) cfg.mdef("noauth_local_webserver", False, None, None, True) #cfg.mdef("auth_host_name", False, None, None, "localhost") #cfg.mdef("auth_host_port", False, None, None, [8080, 8090]) cfg.mdef("logging_level", True, cfg.is_log_level, cfg.trans_log_level, "ERROR") # No need to touch these cfg.mdef("app_name", False, None, None, "Google Calendar MultiMerge") cfg.mdef("scope", False, None, None, "https://www.googleapis.com/auth/calendar") #cfg.mdef("scope", False, None, None, "https://www.googleapis.com/auth/calendar.readonly") cfg.mdef("secret_file", True, cfg.is_filename, None, "client_secret.json") cfg.mdef("credential_file", True, cfg.is_filename, None, "client_credentials.json") ## Read, parse and validate configuration file if len(sys.argv) > 1: gcm_debug("Reading configuration from '{0}'.".format(sys.argv[1])) try: cfgparser = ConfigParser.RawConfigParser() cfgparser.read(sys.argv[1]) except Exception as e: gcm_fatal("Failed to read configuration file '{0}': {1}".format(sys.argv[1], str(e))) # Check that the required section exists section = "gcm" if not cfgparser.has_section(section): gcm_fatal("Invalid configuration, missing '{0}' section.".format(section)) # Debug setting is a special case, we need to get it # set before everything else, so do it here .. if cfgparser.has_option(section, "debug"): cfg.mset("debug", cfgparser.get(section, "debug")) # Parse the settings and validate cfg.mread(cfgparser, section) ## Validate settings if cfg.email: if cfg.email_subject == None or len(cfg.email_subject) == 0: gcm_fatal("E-mail enabled but email_subject not set.") elif cfg.email_sender == None: gcm_fatal("E-mail enabled but email_sender not set.") elif cfg.email_to == None: gcm_fatal("E-mail enabled but email_to not set.") else: cfg.mset("email_ok", True) if len(cfg.source_regmap) != cfg.source_regmap_len: gcm_fatal("Setting source_regmap list must be {0} items.".format(cfg.source_regmap_len)) else: # Force to integers try: cfg.source_regmap = map(lambda x: int(x), cfg.source_regmap) except Exception as e: gcm_fatal("Invalid source_regmap: {0}".format(str(e))) if not cfg.dest_name and not cfg.dest_id: gcm_fatal("Target calendar ID or name required, but not set.") if cfg.dest_name: cfg.mset("dest_name", cfg.mget("dest_name").strip()) ## Initialize and authorize API connection credentials = gcm_get_credentials(cfg) http = credentials.authorize(httplib2.Http()) service = discovery.build("calendar", "v3", http=http) ## Fetch complete calendar list gcm_debug("Fetching available calendars ..") calendars = [] cal_token = None while True: # We want everything except deleted and hidden calendars result = service.calendarList().list( showHidden=False, showDeleted=False, pageToken=cal_token ).execute() calendars.extend(result.get("items", [])) cal_token = result.get("nextPageToken") if not cal_token: break if len(calendars) == 0: gcm_fatal("No calendars found?") ## Filter desired SOURCE calendars based on specified regexp src_re = re.compile(cfg.source_regex) src_calendars = [] for calendar in calendars: if "summary" in calendar: if not cfg.dest_id and cfg.dest_name == calendar["summary"].strip(): cfg.mset("dest_id", calendar["id"]) mre = src_re.match(calendar["summary"]) if mre: calendar["gcm_title"] = mre.group(cfg.source_regmap[0]) calendar["gcm_id"] = mre.group(cfg.source_regmap[1]) src_calendars.append(calendar) ## Check if we have target ID if not cfg.dest_id: gcm_fatal(u"Could not find target/destination calendar ID for '"+ cfg.dest_name +"'.") ## Now, we fetch and collect events gcm_debug(u"Fetching calendar events .. ") src_events = [] for calendar in src_calendars: gcm_debug("- "+calendar["id"]) result = service.events().list( timeZone="EEST", calendarId=calendar["id"], singleEvents=True, showDeleted=False, # orderBy="startTime", ).execute() # Add events, if any, to main list events = result.get("items", []) if events: src_events.extend(events) if cfg.debug: gcm_dump_events(events) ## Get current events gcm_debug(u"Fetching current target calendar events {0}".format(cfg.dest_id)) result = service.events().list( calendarId=cfg.dest_id, singleEvents=True, showDeleted=True).execute() dst_events = result.get("items", []) if dst_events: gcm_debug(u"Found {0} event(s).".format(len(dst_events))) else: gcm_debug(u"No current events.")