Browse Source

Sourcecode zu remind

efbeff 4 weeks ago
parent
commit
42ba4de1d8
1 changed files with 465 additions and 0 deletions
  1. 465 0
      remind

+ 465 - 0
remind

@@ -0,0 +1,465 @@
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+efbeff 2024-04-16
+
+Dieses script läuft z.Zt. produktiv auf Duesseldorf blech02
+
+Freifunk Dortmund Terminerinnerungen aus dem Kalender erzeugen
+und über die Mailingliste(n) verschicken.
+
+Die Erinnerung für das Monatstreffen wird 5 Tage vorher verschickt,
+die restlichen (FF@HOME, ... 2 Tage vorher )
+Unterschieden wird es durch den ical tag SUMMARY.
+Aus der WEBseite lassen sich die Termine leider nur monatsweise lesen,
+deshalb werden 2 Monatskalender gelesen, falls das Prüfintervall über eine
+Monatsgrenze geht.
+
+Das alte script von FFdo als Basis erheblich überarbeitet
+Dies ist die zweite Überarbeitung mit erstellen von *.ics Anhängen
+Dies ist die dritte Überarbeitung mit Entfernen HTML Kommentar
+
+Es darf nur 1mal am Tag laufen, sonst gibt es mehrfache Erinnerungen
+
+Der Absender reminder@freifunk-dortmund.de ist keine echte mailadresse,
+sondern nur als Nichtmitglied aber sendeberechtigt zugelassen in der
+    DEFAULT_LISTE, VEREIN_LISTE und INFRA_LISTE
+Dafür in der Listenadministration jeweils unter
+  Abo-regeln und Adressfilter
+     Absender-filter
+        Anti-Spam-filter
+           reminder@freifunk-dortmund.de
+eintragen.
+
+Mails an reminder@freifunk-dortmund.de werden abgewiesen.
+
+"""
+# testmode 0 setzen für Produktion
+# testmode 1 nur aufbereiten und in log schreiben
+# testmode 2 über smarthost an einzelnen Empfänger schicken
+#           setzt eventuell username PW und port voraus
+#           ACHTUNG PW im Klartext
+testmode = 0
+
+# bei testmode 1 wird keine mail versandt, die Erinnerungen werden
+# nur aufbereitet und ins log geschrieben
+
+import logging
+import logging.handlers
+import sys
+import traceback
+import os
+import shutil
+
+import smtplib
+import requests
+from email.message import EmailMessage
+from datetime import datetime, timedelta, date
+########from icalendar import Calendar, Event
+import re
+
+ABSFILE=os.path.abspath(__file__)
+CALURL_TEMPLATE='http://www.freifunk-dortmund.de/termine/{}-{:02d}/?ical=1'
+
+# empfängerlisten
+DEFAULT_LISTE="freifunk-do@list.free.de"
+INFRA_LISTE="freifunk-do-infra@list.free.de"
+VEREIN_LISTE="freifunk-do-verein@list.free.de"
+
+# Absender
+MAIL_FROM="FF-DO Termine <reminder@freifunk-dortmund.de>" if testmode < 2 else\
+"fbeythien@gmx.de"
+
+MAIL_SUBJECT="Monatstreffen"  # wird angepasst
+if testmode < 2:
+    MAIL_HOST="list.free.de"
+    MAIL_STARTTLS=False
+    MAIL_USER=""
+    MAIL_PASSWORD=""
+    MAIL_PORT="25"
+else:
+    MAIL_HOST="mail.gmx.net"
+    MAIL_STARTTLS=True
+    MAIL_USER="xxxx"
+    MAIL_PASSWORD="xxxx"
+    MAIL_PORT="587"
+    DEFAULT_LISTE="efbe@prima.de"
+    INFRA_LISTE=DEFAULT_LISTE
+    VEREIN_LISTE=DEFAULT_LISTE
+
+
+# Tagesordnungspunkte Monatstreffen
+TOP_LINK="https://wiki.ffdo.de/Community/TOPs"
+TOP_LINK_PAGE= TOP_LINK +".page"
+
+WOTAGE = ("Montag","Dienstag","Mittwoch","Donnerstag","Freitag",
+        "Samstag","Sonntag")
+
+REM_TREFFEN_MONAT = 4   # Tage vorher erinnern Monatstreffen
+REM_TREFFEN_TOPS  = REM_TREFFEN_MONAT + 2 # Tage vorher erinnern TOP ergänzen
+REM_STANDARD = 2        # Tage vorher erinnern FF@home, ...
+REM_INTERVALL = 10      # heute + x Tage Termine prüfen
+
+# logging init
+logging.basicConfig(level=logging.DEBUG)
+mylog = logging.getLogger("remind")
+
+sysl = logging.handlers.SysLogHandler(address='/dev/log')
+#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
+sysl.setFormatter(formatter)
+mylog.addHandler(sysl)
+
+def print_exc_plus():
+    """
+    Print the usual traceback information, followed by a listing of all the
+    local variables in each frame.
+    """
+    tb = sys.exc_info()[2]
+    while 1:
+        if not tb.tb_next:
+            break
+        tb = tb.tb_next
+    stack = []
+    f = tb.tb_frame
+    while f:
+        stack.append(f)
+        f = f.f_back
+    stack.reverse()
+    traceback.print_exc()
+    print("Locals by frame, innermost last")
+    for frame in stack:
+        print()
+        print("Frame %s in %s at line %s" % (frame.f_code.co_name,
+                                             frame.f_code.co_filename,
+                                             frame.f_lineno))
+        for key, value in frame.f_locals.items():
+            print("\t%20s =" % key,end=" ")
+            #We have to be careful not to cause a new error in our error
+            #printer! Calling str() on an unknown object could cause an
+            #error we don't want.
+            try:
+                print(value)
+            except:
+                print( "<ERROR WHILE PRINTING VALUE>")
+
+
+#falls remind nicht erfolgreich beim cron.daily Lauf (z.B. temporäre Netzproblem)
+#dann versuche es in cron.hourly bis es klappt
+def falls_daily_versuche_hourly(progfile):
+    (pfad, datei) = os.path.split(progfile)
+    if pfad[-6:] == ".daily":
+        hpfad = pfad[:-5] + "hourly/"
+        shutil.copy2(progfile, hpfad, follow_symlinks=False)
+        mylog.info("remind nach cron.hourly kopiert.")
+
+# evtl remind aus hourly wieder entfernen nach erfolgreichem Lauf
+def falls_hourly_loe(progfile):
+    if "hourly" in progfile:
+        os.remove(progfile)
+        mylog.info("remind in cron.hourly gelöscht")
+
+#   alle gefundenen Erinnerungen versenden wenn kein testmode gesetzt
+def send(msgs, sender, server, username=None, password=None):
+    if testmode == 0:
+        s = smtplib.SMTP(server, port=MAIL_PORT)
+#       s.set_debuglevel(2)
+
+        for m in msgs:
+            s.send_message(m)
+            mylog.info(str(m))
+#           print('\n', str(m), '\n')
+        s.quit()
+    elif testmode == 1:
+        for m in msgs:
+            mylog.info(str(m))
+    #        print('\n', str(m), '\n')
+    elif  testmode == 2:
+        with smtplib.SMTP(MAIL_HOST, port=MAIL_PORT) as s:
+            s.set_debuglevel(2)
+            s.starttls()
+            s.login(MAIL_USER, MAIL_PASSWORD, initial_response_ok=True)
+            for m in msgs:
+                s.send_message(m)
+                mylog.info(str(m))
+#               print('\n', str(m), '\n')
+            s.close()
+    else:
+        mylog.info(" testmode nicht 0 - 2 ungültig.")
+
+
+# Erinnerung für allgemeinen Termin aufbereiten
+def gen_mail_allg(sender, rcpt, subject, description, vorspann, event, dtstart):
+    body = "DESCRIPTION " + description + " " + "\n\nDein freundlicher FFDO Autoreminder\n.\n"
+    msg = EmailMessage()
+    msg["From"]=sender
+    msg["To"]=rcpt
+    msg["Subject"]="Reminder " + subject
+    msg.set_content(body)
+    att = vorspann + event + '\r\nEND:VCALENDAR'
+    evdat = dtstart.date()
+    msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
+    return msg
+
+# die mmarkdown Markierungen teilweise umsetzen
+#  --- bis ... markdown vorspann löschen
+# führende Leerstellen erhalten
+# dies ist rudimentär, kein markdown interpreter!!
+
+def zeilen_aufbereiten(tops):
+
+# html kommentar entfernen
+
+# beispiel für spamschutz entfernen
+# email = "tony@tiremove_thisger.net"
+# m = re.search("remove_this", email)
+# email[:m.start()] + email[m.end():]
+# Ergebnis: 'tony@tiger.net'
+
+    koma = re.search("<!--", tops)
+    kome = re.search("-->", tops)
+    topn = tops
+    if koma != None and kome != None:
+        if koma.end() < kome.start():
+            topn = tops[:koma.start()] + tops[kome.end():]
+
+    topsp = topn.partition("...")
+    if topsp[2]:
+        lines = topsp[2].split("\n")
+    else:
+        lines = topsp[0].split("\n")
+
+    body = ""
+    for l in lines:
+#        print(l)
+        if len(l.strip()) > 0:
+            if l.strip()[0] == "*":
+                prefix = l.partition("*")[0]
+                body += prefix + " - " + l.strip(' *\n') + "\n"
+            elif l.strip()[0] == "#":
+                continue
+            elif l.strip()[0] == "-":
+                prefix = l.partition("-")[0]
+                body += prefix + "- " + l.strip(' -\n') + "\n"
+            else:
+                body += l.rstrip(" \n") + "\n"
+    return body
+
+# Erinnerung für TOPs einstellen zum Monatstreffen aufbereiten
+def gen_mail_top(sender, rcpt, subject, tops, vorspann, event, dtstart):
+
+    body = "Hallo zusammen!\n\nIn zwei Tagen wird die Erinnerung für das nächste Monatstreffen verschickt. Bisher sind folgende TOPs eingetragen:\n\n"
+    body += zeilen_aufbereiten(tops)
+    body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + "\n\nDein freundlicher FFDO Auto-Reminder\n.\n"
+    msg = EmailMessage()
+    msg["From"]=sender
+    msg["To"]=rcpt
+    msg.set_content(body)
+    att = vorspann + event + '\r\nEND:VCALENDAR'
+    evdat = dtstart.date()
+    msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
+    msg["Subject"]="Reminder TOPs für " + subject
+    return msg
+
+# Erinnerung für Monatstreffen aufbereiten
+def gen_mail_monat(sender, rcpt, subject, tops,  vorspann, event, dtstart):
+
+    wotag = WOTAGE[dtstart.weekday()]
+
+    stzeit = str(dtstart.time())[:5]
+    body = "Hallo zusammen! \n\nAm kommenden " + wotag + " um " + stzeit + " Uhr steht das nächste Monatstreffen auf dem Plan.  Bisher sind folgende TOPs eingetragen: \n\n"
+    body += zeilen_aufbereiten(tops)
+    body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + " \n\nDein freundlicher Auto-Reminder\n.\n"
+
+    msg = EmailMessage()
+    msg["From"]=sender
+    msg["To"]=rcpt
+    msg.set_content(body)
+    att = vorspann + event + '\r\nEND:VCALENDAR'
+    evdat=dtstart.date()
+    msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
+    msg["Subject"]="Reminder " + subject
+    return msg
+
+# key in string suchen ende mit crlf
+def s_in_event(string,such):
+    ergebnis = ''
+    inda=string.find(such)
+    if inda > -1:
+        inda += len(such)
+        inde=string[inda:].find('\r\n')
+        ergebnis=string[inda:inda + inde]
+    return ergebnis
+
+def main():
+    start = date.today()
+
+    # Intervall für prüfungen festlegen
+    end = start + timedelta(days=REM_INTERVALL)
+    if testmode:        # bei test auch 2 vorige Tage prüfen
+        start -= timedelta(2)
+
+    mylog.info("Start remind testmode: " + str(testmode))
+    remstandard = start + timedelta(days=REM_STANDARD)
+    remmontreffen = start + timedelta(days=REM_TREFFEN_MONAT)
+    remmontops = start + timedelta(days=REM_TREFFEN_TOPS)
+
+
+#  jetzt die Kalenderdaten einsammeln und zerlegen
+    kals = []
+    msgs = []
+    msguids = []
+    tops = None
+
+    icss = []
+    calurls = []
+#              termin URL für lfd Monat erzeugen
+    calurls.append(CALURL_TEMPLATE.format(start.year,start.month))
+    if start.month != end.month:
+#              termin URL für nächsten Monat erzeugen
+        calurls.append(CALURL_TEMPLATE.format(end.year,end.month))
+
+# Termindaten holen (1 oder 2 Monate) und in Kalenderformat bringen
+# und auch als ics daten behalten
+    for calurl in calurls:
+        try:
+            ics = requests.get(calurl)
+#   gibt es Termine für den lfd / nächsten Monat
+            if ics:
+                icss.append(ics.text)
+        except ConnectionError as ce:
+            mylog.error("Fehler bei Holen Kalender: " + str(ce))
+            continue
+
+#    Events isolieren und auswerten
+        veanf=0
+        voreins=True
+        icswk=ics.text
+        while veanf > -1:
+            veanf=icswk.find('BEGIN:VEVENT')
+            if veanf == -1: break       # keine events mehr Ende while
+            if voreins:                 # Kalendervorspann  retten
+                voreins=False
+                vorspann=icswk[:veanf]
+                if testmode > 1:
+                    print(len(vorspann))
+                    print(vorspann)
+
+            veend=icswk.find('END:VEVENT')
+            vevent=icswk[veanf:veend+len('END:VEVENT')]
+
+            #icswk setzen damit ein continue später klappt
+            icswk=icswk[veend+len('END:VEVENT'):]
+
+            vstdat=vevent.find('DTSTART')
+            vstdat1=vevent[vstdat:].find(':')
+            vdatum=vevent[vstdat+vstdat1+1:vstdat+vstdat1+16]
+            dtstart=datetime.strptime(vdatum, '%Y%m%dT%H%M%S')
+            if testmode > 1:
+                print(len(vevent))
+                print(vevent)
+                print(vdatum)
+                print(dtstart)
+            if dtstart.date() >= start and ( dtstart.date() <= end or testmode):
+                if testmode > 1:
+                    print('\n\n selektiert, im Intervall\n\n')
+
+                subject = s_in_event(vevent,'SUMMARY:')
+                subject += ' ' +str(dtstart)
+
+                description = s_in_event(vevent,'DESCRIPTION:')
+                for such in ('\\n', '\\,'):
+                    ix = description.find(such)
+                    while ix > 0:
+                        description = description[:ix] + description[ix+2]
+                        ix = description.find(such)
+
+                location = s_in_event(vevent,'LOCATTION:')
+
+                url = s_in_event(vevent,'URL:')
+
+                description += '\n' + location + '\n' + url
+
+                uid = s_in_event(vevent,'UID:')
+                if uid in msguids:
+                    if testmode:
+                        print("doppelt ", uid)
+                    continue
+
+# an FF-DO liste per default
+                liste = DEFAULT_LISTE
+
+#         subject/SUMMARY für Ermittlung der Vorwarnzeit prüfen
+                if "Freifunktreffen" in subject or\
+                   "Monatstreffen" in subject or\
+                   "Orga-Treff" in subject:
+                    if testmode or dtstart.date() == remmontops:
+#          Aufforderung  TOPs einzustellen
+                        try:
+                            tops = requests.get(TOP_LINK_PAGE)
+                        except ConnectionError as ce:
+                            mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
+                            continue
+
+                        m = gen_mail_top(MAIL_FROM, liste, subject, tops.text,
+                                vorspann, vevent, dtstart)
+                        msgs.append(m)
+                        msguids.append(uid)
+                    elif testmode or dtstart.date() == remmontreffen:
+#          Tagesordnungspunkte für nächstes Monatstreffen
+                        try:
+                            tops = requests.get(TOP_LINK_PAGE)
+                        except ConnectionError as ce:
+                            mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
+                            continue
+
+                        m = gen_mail_monat(MAIL_FROM, liste, subject, tops.text,
+                                vorspann, vevent, dtstart)
+                        msgs.append(m)
+                        msguids.append(uid)
+
+                elif "FF@home" in subject:
+                    if testmode or dtstart.date() == remstandard:
+                        liste = INFRA_LISTE
+                        m = gen_mail_allg(MAIL_FROM, liste, subject,
+                                description, vorspann, vevent, dtstart)
+                        msgs.append(m)
+                        msguids.append(uid)
+
+                elif "Öffentlichkeitsarbeit" in subject:
+                    if testmode or dtstart.date() == remstandard:
+                        liste = VEREIN_LISTE
+                        m = gen_mail_allg(MAIL_FROM, liste, subject,
+                                description, vorspann, vevent, dtstart)
+                        msgs.append(m)
+                        msguids.append(uid)
+                else:
+                    if testmode or dtstart.date() == remstandard:
+                        m = gen_mail_allg(MAIL_FROM, liste, subject,
+                                description, vorspann, vevent, dtstart)
+                        msgs.append(m)
+                        msguids.append(uid)
+
+### willi=willi ### für exception test
+#       Erinnerungen verschicken wenn kein testmode oder testmode 2
+    if len(msgs):
+        send(msgs, MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD)
+
+#    print("\n-------", len(msgs), "Erinnerung(en)\n")
+    mylog.info("Ende " + str(len(msgs)) + " Erinnerungen")
+
+
+if __name__ == '__main__':
+    try:
+        main()
+    except:
+        falls_daily_versuche_hourly(ABSFILE)
+        traceback.print_exc()
+####    print_exc_plus() #für bessere Fehlersuche aktivieren
+        mylog.error(traceback.format_exc())
+    else:
+        falls_hourly_loe(ABSFILE)
+    finally:
+        logging.shutdown()
+    exit(0)