remind 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. efbeff 2024-04-16
  5. Dieses script läuft z.Zt. produktiv auf Duesseldorf blech02
  6. Freifunk Dortmund Terminerinnerungen aus dem Kalender erzeugen
  7. und über die Mailingliste(n) verschicken.
  8. Die Erinnerung für das Monatstreffen wird 5 Tage vorher verschickt,
  9. die restlichen (FF@HOME, ... 2 Tage vorher )
  10. Unterschieden wird es durch den ical tag SUMMARY.
  11. Aus der WEBseite lassen sich die Termine leider nur monatsweise lesen,
  12. deshalb werden 2 Monatskalender gelesen, falls das Prüfintervall über eine
  13. Monatsgrenze geht.
  14. Das alte script von FFdo als Basis erheblich überarbeitet
  15. Dies ist die zweite Überarbeitung mit erstellen von *.ics Anhängen
  16. Dies ist die dritte Überarbeitung mit Entfernen HTML Kommentar
  17. Es darf nur 1mal am Tag laufen, sonst gibt es mehrfache Erinnerungen
  18. Der Absender reminder@freifunk-dortmund.de ist keine echte mailadresse,
  19. sondern nur als Nichtmitglied aber sendeberechtigt zugelassen in der
  20. DEFAULT_LISTE, VEREIN_LISTE und INFRA_LISTE
  21. Dafür in der Listenadministration jeweils unter
  22. Abo-regeln und Adressfilter
  23. Absender-filter
  24. Anti-Spam-filter
  25. reminder@freifunk-dortmund.de
  26. eintragen.
  27. Mails an reminder@freifunk-dortmund.de werden abgewiesen.
  28. """
  29. # testmode 0 setzen für Produktion
  30. # testmode 1 nur aufbereiten und in log schreiben
  31. # testmode 2 über smarthost an einzelnen Empfänger schicken
  32. # setzt eventuell username PW und port voraus
  33. # ACHTUNG PW im Klartext
  34. testmode = 0
  35. # bei testmode 1 wird keine mail versandt, die Erinnerungen werden
  36. # nur aufbereitet und ins log geschrieben
  37. import logging
  38. import logging.handlers
  39. import sys
  40. import traceback
  41. import os
  42. import shutil
  43. import smtplib
  44. import requests
  45. from email.message import EmailMessage
  46. from datetime import datetime, timedelta, date
  47. ########from icalendar import Calendar, Event
  48. import re
  49. ABSFILE=os.path.abspath(__file__)
  50. CALURL_TEMPLATE='http://www.freifunk-dortmund.de/termine/{}-{:02d}/?ical=1'
  51. # empfängerlisten
  52. DEFAULT_LISTE="freifunk-do@list.free.de"
  53. INFRA_LISTE="freifunk-do-infra@list.free.de"
  54. VEREIN_LISTE="freifunk-do-verein@list.free.de"
  55. # Absender
  56. MAIL_FROM="FF-DO Termine <reminder@freifunk-dortmund.de>" if testmode < 2 else\
  57. "fbeythien@gmx.de"
  58. MAIL_SUBJECT="Monatstreffen" # wird angepasst
  59. if testmode < 2:
  60. MAIL_HOST="list.free.de"
  61. MAIL_STARTTLS=False
  62. MAIL_USER=""
  63. MAIL_PASSWORD=""
  64. MAIL_PORT="25"
  65. else:
  66. MAIL_HOST="mail.gmx.net"
  67. MAIL_STARTTLS=True
  68. MAIL_USER="xxxx"
  69. MAIL_PASSWORD="xxxx"
  70. MAIL_PORT="587"
  71. DEFAULT_LISTE="efbe@prima.de"
  72. INFRA_LISTE=DEFAULT_LISTE
  73. VEREIN_LISTE=DEFAULT_LISTE
  74. # Tagesordnungspunkte Monatstreffen
  75. TOP_LINK="https://wiki.ffdo.de/Community/TOPs"
  76. TOP_LINK_PAGE= TOP_LINK +".page"
  77. WOTAGE = ("Montag","Dienstag","Mittwoch","Donnerstag","Freitag",
  78. "Samstag","Sonntag")
  79. REM_TREFFEN_MONAT = 4 # Tage vorher erinnern Monatstreffen
  80. REM_TREFFEN_TOPS = REM_TREFFEN_MONAT + 2 # Tage vorher erinnern TOP ergänzen
  81. REM_STANDARD = 2 # Tage vorher erinnern FF@home, ...
  82. REM_INTERVALL = 10 # heute + x Tage Termine prüfen
  83. # logging init
  84. logging.basicConfig(level=logging.DEBUG)
  85. mylog = logging.getLogger("remind")
  86. sysl = logging.handlers.SysLogHandler(address='/dev/log')
  87. #formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  88. formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
  89. sysl.setFormatter(formatter)
  90. mylog.addHandler(sysl)
  91. def print_exc_plus():
  92. """
  93. Print the usual traceback information, followed by a listing of all the
  94. local variables in each frame.
  95. """
  96. tb = sys.exc_info()[2]
  97. while 1:
  98. if not tb.tb_next:
  99. break
  100. tb = tb.tb_next
  101. stack = []
  102. f = tb.tb_frame
  103. while f:
  104. stack.append(f)
  105. f = f.f_back
  106. stack.reverse()
  107. traceback.print_exc()
  108. print("Locals by frame, innermost last")
  109. for frame in stack:
  110. print()
  111. print("Frame %s in %s at line %s" % (frame.f_code.co_name,
  112. frame.f_code.co_filename,
  113. frame.f_lineno))
  114. for key, value in frame.f_locals.items():
  115. print("\t%20s =" % key,end=" ")
  116. #We have to be careful not to cause a new error in our error
  117. #printer! Calling str() on an unknown object could cause an
  118. #error we don't want.
  119. try:
  120. print(value)
  121. except:
  122. print( "<ERROR WHILE PRINTING VALUE>")
  123. #falls remind nicht erfolgreich beim cron.daily Lauf (z.B. temporäre Netzproblem)
  124. #dann versuche es in cron.hourly bis es klappt
  125. def falls_daily_versuche_hourly(progfile):
  126. (pfad, datei) = os.path.split(progfile)
  127. if pfad[-6:] == ".daily":
  128. hpfad = pfad[:-5] + "hourly/"
  129. shutil.copy2(progfile, hpfad, follow_symlinks=False)
  130. mylog.info("remind nach cron.hourly kopiert.")
  131. # evtl remind aus hourly wieder entfernen nach erfolgreichem Lauf
  132. def falls_hourly_loe(progfile):
  133. if "hourly" in progfile:
  134. os.remove(progfile)
  135. mylog.info("remind in cron.hourly gelöscht")
  136. # alle gefundenen Erinnerungen versenden wenn kein testmode gesetzt
  137. def send(msgs, sender, server, username=None, password=None):
  138. if testmode == 0:
  139. s = smtplib.SMTP(server, port=MAIL_PORT)
  140. # s.set_debuglevel(2)
  141. for m in msgs:
  142. s.send_message(m)
  143. mylog.info(str(m))
  144. # print('\n', str(m), '\n')
  145. s.quit()
  146. elif testmode == 1:
  147. for m in msgs:
  148. mylog.info(str(m))
  149. # print('\n', str(m), '\n')
  150. elif testmode == 2:
  151. with smtplib.SMTP(MAIL_HOST, port=MAIL_PORT) as s:
  152. s.set_debuglevel(2)
  153. s.starttls()
  154. s.login(MAIL_USER, MAIL_PASSWORD, initial_response_ok=True)
  155. for m in msgs:
  156. s.send_message(m)
  157. mylog.info(str(m))
  158. # print('\n', str(m), '\n')
  159. s.close()
  160. else:
  161. mylog.info(" testmode nicht 0 - 2 ungültig.")
  162. # Erinnerung für allgemeinen Termin aufbereiten
  163. def gen_mail_allg(sender, rcpt, subject, description, vorspann, event, dtstart):
  164. body = "DESCRIPTION " + description + " " + "\n\nDein freundlicher FFDO Autoreminder\n.\n"
  165. msg = EmailMessage()
  166. msg["From"]=sender
  167. msg["To"]=rcpt
  168. msg["Subject"]="Reminder " + subject
  169. msg.set_content(body)
  170. att = vorspann + event + '\r\nEND:VCALENDAR'
  171. evdat = dtstart.date()
  172. msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
  173. return msg
  174. # die mmarkdown Markierungen teilweise umsetzen
  175. # --- bis ... markdown vorspann löschen
  176. # führende Leerstellen erhalten
  177. # dies ist rudimentär, kein markdown interpreter!!
  178. def zeilen_aufbereiten(tops):
  179. # html kommentar entfernen
  180. # beispiel für spamschutz entfernen
  181. # email = "tony@tiremove_thisger.net"
  182. # m = re.search("remove_this", email)
  183. # email[:m.start()] + email[m.end():]
  184. # Ergebnis: 'tony@tiger.net'
  185. koma = re.search("<!--", tops)
  186. kome = re.search("-->", tops)
  187. topn = tops
  188. if koma != None and kome != None:
  189. if koma.end() < kome.start():
  190. topn = tops[:koma.start()] + tops[kome.end():]
  191. topsp = topn.partition("...")
  192. if topsp[2]:
  193. lines = topsp[2].split("\n")
  194. else:
  195. lines = topsp[0].split("\n")
  196. body = ""
  197. for l in lines:
  198. # print(l)
  199. if len(l.strip()) > 0:
  200. if l.strip()[0] == "*":
  201. prefix = l.partition("*")[0]
  202. body += prefix + " - " + l.strip(' *\n') + "\n"
  203. elif l.strip()[0] == "#":
  204. continue
  205. elif l.strip()[0] == "-":
  206. prefix = l.partition("-")[0]
  207. body += prefix + "- " + l.strip(' -\n') + "\n"
  208. else:
  209. body += l.rstrip(" \n") + "\n"
  210. return body
  211. # Erinnerung für TOPs einstellen zum Monatstreffen aufbereiten
  212. def gen_mail_top(sender, rcpt, subject, tops, vorspann, event, dtstart):
  213. body = "Hallo zusammen!\n\nIn zwei Tagen wird die Erinnerung für das nächste Monatstreffen verschickt. Bisher sind folgende TOPs eingetragen:\n\n"
  214. body += zeilen_aufbereiten(tops)
  215. 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"
  216. msg = EmailMessage()
  217. msg["From"]=sender
  218. msg["To"]=rcpt
  219. msg.set_content(body)
  220. att = vorspann + event + '\r\nEND:VCALENDAR'
  221. evdat = dtstart.date()
  222. msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
  223. msg["Subject"]="Reminder TOPs für " + subject
  224. return msg
  225. # Erinnerung für Monatstreffen aufbereiten
  226. def gen_mail_monat(sender, rcpt, subject, tops, vorspann, event, dtstart):
  227. wotag = WOTAGE[dtstart.weekday()]
  228. stzeit = str(dtstart.time())[:5]
  229. 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"
  230. body += zeilen_aufbereiten(tops)
  231. body += "\nFalls du noch ein weiteres Thema besprechen möchtest, trag es bitte hier ein: " + TOP_LINK + " \n\nDein freundlicher Auto-Reminder\n.\n"
  232. msg = EmailMessage()
  233. msg["From"]=sender
  234. msg["To"]=rcpt
  235. msg.set_content(body)
  236. att = vorspann + event + '\r\nEND:VCALENDAR'
  237. evdat=dtstart.date()
  238. msg.add_attachment(att, subtype="ics", filename=str(evdat)+".ics")
  239. msg["Subject"]="Reminder " + subject
  240. return msg
  241. # key in string suchen ende mit crlf
  242. def s_in_event(string,such):
  243. ergebnis = ''
  244. inda=string.find(such)
  245. if inda > -1:
  246. inda += len(such)
  247. inde=string[inda:].find('\r\n')
  248. ergebnis=string[inda:inda + inde]
  249. return ergebnis
  250. def main():
  251. start = date.today()
  252. # Intervall für prüfungen festlegen
  253. end = start + timedelta(days=REM_INTERVALL)
  254. if testmode: # bei test auch 2 vorige Tage prüfen
  255. start -= timedelta(2)
  256. mylog.info("Start remind testmode: " + str(testmode))
  257. remstandard = start + timedelta(days=REM_STANDARD)
  258. remmontreffen = start + timedelta(days=REM_TREFFEN_MONAT)
  259. remmontops = start + timedelta(days=REM_TREFFEN_TOPS)
  260. # jetzt die Kalenderdaten einsammeln und zerlegen
  261. kals = []
  262. msgs = []
  263. msguids = []
  264. tops = None
  265. icss = []
  266. calurls = []
  267. # termin URL für lfd Monat erzeugen
  268. calurls.append(CALURL_TEMPLATE.format(start.year,start.month))
  269. if start.month != end.month:
  270. # termin URL für nächsten Monat erzeugen
  271. calurls.append(CALURL_TEMPLATE.format(end.year,end.month))
  272. # Termindaten holen (1 oder 2 Monate) und in Kalenderformat bringen
  273. # und auch als ics daten behalten
  274. for calurl in calurls:
  275. try:
  276. ics = requests.get(calurl)
  277. # gibt es Termine für den lfd / nächsten Monat
  278. if ics:
  279. icss.append(ics.text)
  280. except ConnectionError as ce:
  281. mylog.error("Fehler bei Holen Kalender: " + str(ce))
  282. continue
  283. # Events isolieren und auswerten
  284. veanf=0
  285. voreins=True
  286. icswk=ics.text
  287. while veanf > -1:
  288. veanf=icswk.find('BEGIN:VEVENT')
  289. if veanf == -1: break # keine events mehr Ende while
  290. if voreins: # Kalendervorspann retten
  291. voreins=False
  292. vorspann=icswk[:veanf]
  293. if testmode > 1:
  294. print(len(vorspann))
  295. print(vorspann)
  296. veend=icswk.find('END:VEVENT')
  297. vevent=icswk[veanf:veend+len('END:VEVENT')]
  298. #icswk setzen damit ein continue später klappt
  299. icswk=icswk[veend+len('END:VEVENT'):]
  300. vstdat=vevent.find('DTSTART')
  301. vstdat1=vevent[vstdat:].find(':')
  302. vdatum=vevent[vstdat+vstdat1+1:vstdat+vstdat1+16]
  303. dtstart=datetime.strptime(vdatum, '%Y%m%dT%H%M%S')
  304. if testmode > 1:
  305. print(len(vevent))
  306. print(vevent)
  307. print(vdatum)
  308. print(dtstart)
  309. if dtstart.date() >= start and ( dtstart.date() <= end or testmode):
  310. if testmode > 1:
  311. print('\n\n selektiert, im Intervall\n\n')
  312. subject = s_in_event(vevent,'SUMMARY:')
  313. subject += ' ' +str(dtstart)
  314. description = s_in_event(vevent,'DESCRIPTION:')
  315. for such in ('\\n', '\\,'):
  316. ix = description.find(such)
  317. while ix > 0:
  318. description = description[:ix] + description[ix+2]
  319. ix = description.find(such)
  320. location = s_in_event(vevent,'LOCATTION:')
  321. url = s_in_event(vevent,'URL:')
  322. description += '\n' + location + '\n' + url
  323. uid = s_in_event(vevent,'UID:')
  324. if uid in msguids:
  325. if testmode:
  326. print("doppelt ", uid)
  327. continue
  328. # an FF-DO liste per default
  329. liste = DEFAULT_LISTE
  330. # subject/SUMMARY für Ermittlung der Vorwarnzeit prüfen
  331. if "Freifunktreffen" in subject or\
  332. "Monatstreffen" in subject or\
  333. "Orga-Treff" in subject:
  334. if testmode or dtstart.date() == remmontops:
  335. # Aufforderung TOPs einzustellen
  336. try:
  337. tops = requests.get(TOP_LINK_PAGE)
  338. except ConnectionError as ce:
  339. mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
  340. continue
  341. m = gen_mail_top(MAIL_FROM, liste, subject, tops.text,
  342. vorspann, vevent, dtstart)
  343. msgs.append(m)
  344. msguids.append(uid)
  345. elif testmode or dtstart.date() == remmontreffen:
  346. # Tagesordnungspunkte für nächstes Monatstreffen
  347. try:
  348. tops = requests.get(TOP_LINK_PAGE)
  349. except ConnectionError as ce:
  350. mylog.error("Fehler holen TOPs mit requests.get(): " + str(ce))
  351. continue
  352. m = gen_mail_monat(MAIL_FROM, liste, subject, tops.text,
  353. vorspann, vevent, dtstart)
  354. msgs.append(m)
  355. msguids.append(uid)
  356. elif "FF@home" in subject:
  357. if testmode or dtstart.date() == remstandard:
  358. liste = INFRA_LISTE
  359. m = gen_mail_allg(MAIL_FROM, liste, subject,
  360. description, vorspann, vevent, dtstart)
  361. msgs.append(m)
  362. msguids.append(uid)
  363. elif "Öffentlichkeitsarbeit" in subject:
  364. if testmode or dtstart.date() == remstandard:
  365. liste = VEREIN_LISTE
  366. m = gen_mail_allg(MAIL_FROM, liste, subject,
  367. description, vorspann, vevent, dtstart)
  368. msgs.append(m)
  369. msguids.append(uid)
  370. else:
  371. if testmode or dtstart.date() == remstandard:
  372. m = gen_mail_allg(MAIL_FROM, liste, subject,
  373. description, vorspann, vevent, dtstart)
  374. msgs.append(m)
  375. msguids.append(uid)
  376. ### willi=willi ### für exception test
  377. # Erinnerungen verschicken wenn kein testmode oder testmode 2
  378. if len(msgs):
  379. send(msgs, MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD)
  380. # print("\n-------", len(msgs), "Erinnerung(en)\n")
  381. mylog.info("Ende " + str(len(msgs)) + " Erinnerungen")
  382. if __name__ == '__main__':
  383. try:
  384. main()
  385. except:
  386. falls_daily_versuche_hourly(ABSFILE)
  387. traceback.print_exc()
  388. #### print_exc_plus() #für bessere Fehlersuche aktivieren
  389. mylog.error(traceback.format_exc())
  390. else:
  391. falls_hourly_loe(ABSFILE)
  392. finally:
  393. logging.shutdown()
  394. exit(0)