Compare commits

...

8 Commits
master ... jfw

Author SHA1 Message Date
Simon Zeyer
d816c9d302 fix removed file on print hook 2024-10-16 18:23:22 +00:00
Simon Zeyer
a4006721d3 sachverhalt as message 2024-10-16 14:09:08 +00:00
Simon Zeyer
47c1c5ceb8 print pdf with chrome 2024-10-16 13:36:43 +00:00
Simon Zeyer
b82d1bed1f forgot requirements 2024-10-16 12:15:33 +00:00
Simon Zeyer
69da949aa8 add basic auth 2024-10-16 12:07:22 +00:00
Simon Zeyer
fa67553299 rename flask app and add upload function 2024-10-16 11:59:02 +00:00
Simon Zeyer
b5c9a2761f fix requirements flask import 2024-10-15 20:01:33 +00:00
Simon Zeyer
1748f754ca http for jfw bf tag 2024-10-15 19:08:21 +00:00
6 changed files with 246 additions and 26 deletions

View File

@ -12,3 +12,4 @@ alarminator_token=""
alarminator_zvies_use_PEALGRP="False" alarminator_zvies_use_PEALGRP="False"
printer=DEFAULT printer=DEFAULT
print_num=0 print_num=0
BASIC_AUTH_PASSWORD=""

View File

@ -13,6 +13,8 @@ ENV alarminator_zvies_use_PEALGRP="False"
ENV print_num=0 ENV print_num=0
ENV printer="DEFAULT" ENV printer="DEFAULT"
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
ENV MAPS_API_KEY=""
ENV BASIC_AUTH_PASSWORD=""
COPY *.deb / COPY *.deb /
@ -44,6 +46,7 @@ RUN apt-get --yes --force-yes install ca-certificates cups cups-filters libcups
WORKDIR /usr/src/app WORKDIR /usr/src/app
EXPOSE 631 EXPOSE 631
EXPOSE 5000
# Add user and disable sudo password checking # Add user and disable sudo password checking
RUN useradd \ RUN useradd \
--groups=sudo,lp,lpadmin \ --groups=sudo,lp,lpadmin \
@ -54,11 +57,29 @@ RUN useradd \
print \ print \
&& sed -i '/%sudo[[:space:]]/ s/ALL[[:space:]]*$/NOPASSWD:ALL/' /etc/sudoers && sed -i '/%sudo[[:space:]]/ s/ALL[[:space:]]*$/NOPASSWD:ALL/' /etc/sudoers
# Print PDF
RUN apt-get update && apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
--no-install-recommends \
&& curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update && apt-get install -y \
google-chrome-stable \
--no-install-recommends
# It won't run from the root user.
RUN groupadd chrome && useradd -g chrome -s /bin/bash -G audio,video chrome \
&& mkdir -p /home/chrome && chown -R chrome:chrome /home/chrome
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir waitress
COPY ./app . COPY ./app .
COPY *.crt /usr/local/share/ca-certificates/ COPY *.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates RUN update-ca-certificates
CMD [ "sh","-c","/etc/init.d/cups start && python3 /usr/src/app/exchange_connect.py" ] CMD [ "sh","-c","/etc/init.d/cups start && waitress-serve --port=5000 app:app && python3 exchange_connect.py" ]

136
app/app.py Normal file
View File

@ -0,0 +1,136 @@
from flask import Flask, flash, request, redirect, send_from_directory, get_flashed_messages
import os
import glob
import securecad_parser
import datetime
from hooks import webhook, alarminator_api, cups_print
from pathlib import Path
from flask_basicauth import BasicAuth
app = Flask(__name__)
app.secret_key = 'super secret key'
app.config['SESSION_TYPE'] = 'filesystem'
app.config['BASIC_AUTH_USERNAME'] = 'admin'
app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('BASIC_AUTH_PASSWORD','password')
basic_auth = BasicAuth(app)
def wrapHtml(innerHtml):
return '''<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JFW Alarm</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body><div class="container text-center">
''' + flashMessages() +'''
''' + innerHtml + '''
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>'''
def flashMessages():
messages = get_flashed_messages(with_categories=True)
html = ""
if messages:
for category, message in messages:
html = html + category + ": "+message+"<br>"
return html
def alarmsPath():
dir_path = os.path.dirname(os.path.realpath(__file__))
return dir_path + "/alarms" + os.sep
def getAlarmFiles():
a_list = glob.glob(alarmsPath()+"*.html")
a_list.sort()
return [os.path.basename(f) for f in a_list]
def parseAlarm(f):
f = alarmsPath() + os.sep + f
with open(f, "r") as o:
_r: str = o.read()
now = datetime.datetime.now()
_r = _r.replace('%DATUM%',now.strftime("%d.%m.%Y"))
_r = _r.replace('%UHRZEIT%',now.strftime("%H:%M"))
if _r != '':
return [securecad_parser.parse_securecad_message(_r),_r]
def allowed_file(filename:str):
print(filename.rsplit('.', 1)[1].lower())
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ["html"]
@app.route("/")
@basic_auth.required
def root():
html = ""
file = request.args.get('sendalarm')
if file is not None:
parsed_body, raw = parseAlarm(file)
if parsed_body != None:
if 'ALARMDEPESCHE' in parsed_body:
webhook(parsed_body)
alarminator_api(parsed_body)
cups_print(parsed_body,raw)
flash("Alarm gesendet!")
return redirect('/')
html = html + '<div class="row align-items-start p-3 ">'
i: str
files = getAlarmFiles()
for f in files:
html = html + "<div class=\"col-6\" style='padding: 5px;'><a style='display:block; padding:20px' class=\"btn btn-block btn-danger\" href=\"/?sendalarm="+f+"\">" + f + "</a></div>"
html = html + "</div>"
return wrapHtml(html)
@app.route('/files', methods=['POST'])
@basic_auth.required
def upload_file():
# check if the post request has the file part
print(request.files)
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
print(file.filename)
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
file.save(os.path.join(alarmsPath(), file.filename))
return redirect('/files')
@app.route('/files/<path:filename>', methods=['GET'])
@basic_auth.required
def download(filename):
uploads = alarmsPath()
return send_from_directory(uploads, filename, as_attachment=True)
@app.route("/files", methods=['GET'])
@basic_auth.required
def files():
Path(alarmsPath()).mkdir(parents=True, exist_ok=True)
file = request.args.get('del')
if file is not None:
try:
os.remove(alarmsPath()+os.sep+file)
except:
pass
flash("Datei gelöscht!")
return redirect('/files')
files = getAlarmFiles()
html = '<ul class="list-group">'
for f in files:
html = html + '<li class="list-group-item"><a href="/files/'+f+'">'+f+'</a> <a href="/files?del='+f+'">löschen</a></li>'
html = html + '</ul><form method="post" enctype="multipart/form-data"><input type="file" name="file"><input type=submit value=Upload></form>'
return html
# if __name__ == "__main__":
# app.run(port=5000, host='0.0.0.0')

View File

@ -2,9 +2,9 @@ import os
import logging import logging
import requests import requests
import cups import cups
from weasyprint import HTML
from requests.adapters import Retry from requests.adapters import Retry
import uuid import uuid
import subprocess
retries = Retry(total=5, retries = Retry(total=5,
backoff_factor=0.1, backoff_factor=0.1,
@ -26,18 +26,18 @@ def alarminator_api(parsed_body: dict):
alarminator_api = os.environ.get('alarminator_api') if os.environ.get('alarminator_api') else "" alarminator_api = os.environ.get('alarminator_api') if os.environ.get('alarminator_api') else ""
alarminator_token = os.environ.get('alarminator_token') if os.environ.get('alarminator_token') else "" alarminator_token = os.environ.get('alarminator_token') if os.environ.get('alarminator_token') else ""
alarminator_zvies_use_PEALGRP = True if os.environ.get('alarminator_zvies_use_PEALGRP') == 'True' else False alarminator_zvies_use_PEALGRP = True if os.environ.get('alarminator_zvies_use_PEALGRP') == 'True' else False
maps_api_key = os.environ.get('MAPS_API_KEY',"")
if alarminator_api != "" and alarminator_token != "": if alarminator_api != "" and alarminator_token != "":
if 'ALARMDEPESCHE' in parsed_body: # sendAlarm triggern if 'ALARMDEPESCHE' in parsed_body: # sendAlarm triggern
logging.info("GET zu {}/operations/sendAlarm".format(alarminator_api)) logging.info("GET zu {}/operations/sendAlarm".format(alarminator_api))
s = requests.Session() s = requests.Session()
s.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries)) s.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
#&object=Kirmesplatz &district=Oberlinxweilerstrasse &subject=THK (TH klein &street=Oberlinxweilerstrasse &ils=\"secur.CAD\" <leitstelle@zrf-saar.de>&connector=mailParser&token=ea2110e1-11b9-421f-a53d-96cc0fc82c31
req_string = "" req_string = ""
req_string +="?token={}".format(alarminator_token) req_string +="?token={}".format(alarminator_token)
if 'Einsatzbeginn(Soll)' in parsed_body: if 'Einsatzbeginn(Soll)' in parsed_body:
req_string +="&alarmdate={}".format(parsed_body['Einsatzbeginn(Soll)'].split(" ")[0]) req_string +="&alarmdate={}".format(parsed_body['Einsatzbeginn(Soll)'].split(" ")[0])
req_string +="&alarmtime={}".format(parsed_body['Einsatzbeginn(Soll)'].split(" ")[1]) req_string +="&alarmtime={}".format(parsed_body['Einsatzbeginn(Soll)'].split(" ")[1])
if 'Auftragsnummer' in parsed_body: if 'Auftragsnummer' in parsed_body and not (os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True"):
req_string +="&operationnumber={}".format(parsed_body['Auftragsnummer']) req_string +="&operationnumber={}".format(parsed_body['Auftragsnummer'])
if 'Sachverhalt' in parsed_body: if 'Sachverhalt' in parsed_body:
req_string +="&message={}".format(parsed_body['Sachverhalt']) req_string +="&message={}".format(parsed_body['Sachverhalt'])
@ -48,21 +48,29 @@ def alarminator_api(parsed_body: dict):
req_string +="&location={}".format(parsed_body['Einsatzziel']['PLZ / Ort']) req_string +="&location={}".format(parsed_body['Einsatzziel']['PLZ / Ort'])
if 'Objekt' in parsed_body['Einsatzziel']: if 'Objekt' in parsed_body['Einsatzziel']:
req_string +="&object={}".format(parsed_body['Einsatzziel']['Objekt']) req_string +="&object={}".format(parsed_body['Einsatzziel']['Objekt'])
street = []
if 'Strasse' in parsed_body['Einsatzziel']: if 'Strasse' in parsed_body['Einsatzziel']:
req_string +="&street={}".format(parsed_body['Einsatzziel']['Strasse']) + (("\n"+parsed_body['Einsatzziel']['Info ']) if 'Info ' in parsed_body['Einsatzziel'] else "" ) street.append(parsed_body['Einsatzziel']['Strasse'])
if 'Zusatz Strasse' in parsed_body['Einsatzziel']:
street.append(parsed_body['Einsatzziel']['Zusatz Strasse'])
if 'Strasse / Hs.-Nr.' in parsed_body['Einsatzziel']: if 'Strasse / Hs.-Nr.' in parsed_body['Einsatzziel']:
req_string +="&street={}".format(parsed_body['Einsatzziel']['Strasse / Hs.-Nr.'] + (("\n"+parsed_body['Einsatzziel']['Info ']) if 'Info ' in parsed_body['Einsatzziel'] else "" )) street.append(parsed_body['Einsatzziel']['Strasse / Hs.-Nr.'])
if 'Info' in parsed_body['Einsatzziel']:
street.append(parsed_body['Einsatzziel']['Info'])
if street.__len__() > 0:
req_string +="&street={}".format("\n".join(street))
if 'Einsatzmittelliste' in parsed_body: if 'Einsatzmittelliste' in parsed_body:
gear = [] gear = []
for r in parsed_body['Einsatzmittelliste']: for r in parsed_body['Einsatzmittelliste']:
if r['Typ'] != 'PEALGRP': #if r['Typ'] != 'PEALGRP':
if r['Ressourcen'] not in gear:
gear.append(r['Ressourcen']) gear.append(r['Ressourcen'])
req_string +="&gear={}".format(';'.join(gear)) req_string +="&gear={}".format(';'.join(gear))
#req_string +="&district={}".format('district') # req_string +="&district={}".format('district')
#req_string +="&floor={}".format('floor') # req_string +="&floor={}".format('floor')
#req_string +="&section={}".format('section') # req_string +="&section={}".format('section')
req_string +="&keywordRaw={}".format(parsed_body['Einsatzstichwort']) req_string +="&keywordRaw={}".format(parsed_body['Einsatzstichwort'])
#req_string +="&keywordId={}".format('keywordId') #req_string +="&keywordId={}".format('keywordId')
req_string +="&keywordCategory={}".format(parsed_body['Einsatzstichwort'].split("(")[0]) req_string +="&keywordCategory={}".format(parsed_body['Einsatzstichwort'].split("(")[0])
@ -76,35 +84,67 @@ def alarminator_api(parsed_body: dict):
req_string +="&zveis={}".format(';'.join(zveis)) req_string +="&zveis={}".format(';'.join(zveis))
else: else:
req_string +="&zveis={}".format(parsed_body['ALARMDEPESCHE']) req_string +="&zveis={}".format(parsed_body['ALARMDEPESCHE'])
if maps_api_key != "":
try:
maps_address_param = []
if 'Objekt' in parsed_body['Einsatzziel']:
maps_address_param.append("{}".format(parsed_body['Einsatzziel']['Objekt']))
if street.__len__() > 0:
maps_address_param.append('{}'.format(",".join(street)))
if 'Stadt' in parsed_body['Einsatzziel']:
maps_address_param.append("{}".format(parsed_body['Einsatzziel']['Stadt']))
if 'PLZ / Ort' in parsed_body['Einsatzziel']:
maps_address_param.append("{}".format(parsed_body['Einsatzziel']['PLZ / Ort']))
maps_request = requests.get('https://maps.google.com/maps/api/geocode/json?address={}&key={}'.format(','.join(maps_address_param),maps_api_key))
if maps_request.json()['results'].__len__() == 1:
req_string +="&lat={}".format(maps_request.json()['results'][0]['geometry']['location']['lat'])
req_string +="&lon={}".format(maps_request.json()['results'][0]['geometry']['location']['lng'])
except Exception as maps_e:
logging.error('error getting maps',maps_e)
# req_string +="&gkx={}".format() if False # req_string +="&gkx={}".format() if False
# req_string +="&gky={}".format() if False # req_string +="&gky={}".format() if False
# req_string +="&lat={}".format() if False # if False
# req_string +="&lon={}".format() if False # req_string +="&lon={}".format() if False
subject = "" subject = ""
if 'Notfallgeschehen' in parsed_body:
subject = parsed_body['Notfallgeschehen'] + "\n"
if 'Notfallgeschehen' in parsed_body: if 'Notfallgeschehen' in parsed_body:
subject = parsed_body['Notfallgeschehen'] + "\n" subject = parsed_body['Notfallgeschehen'] + "\n"
req_string +="&subject={}".format(subject) req_string +="&subject={}".format(subject)
req_string +="&ils={}".format("ILS Saar") req_string +="&ils={}".format("ILS Saar")
req_string +="&connector={}".format("MailParser") req_string +="&connector={}".format("MailParser")
if os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True":
req_string +="&isTest=1"
s.get(alarminator_api+"/operations/sendAlarm/"+req_string) s.get(alarminator_api+"/operations/sendAlarm/"+req_string)
except Exception as e: except Exception as e:
logging.error("alarminator_api", e) logging.error("alarminator_api", e)
def generate_pdf(html_body, filename):
f = "/tmp/{}.html".format(uuid.uuid4())
with open(f,"w") as _f:
_f.write(html_body)
subprocess.run(["/usr/bin/google-chrome-stable", "--headless", "--no-sandbox", "--disable-gpu", "--print-to-pdf="+filename, "--no-pdf-header-footer", "--print-to-pdf-no-header", "--no-margins", f])
if os.path.exists(f):
os.remove(f)
def cups_print(parsed_body: dict, body: str): def cups_print(parsed_body: dict, body: str):
if os.environ.get('IS_DEV') and os.environ.get('IS_DEV') == "True":
generate_pdf(body, "{}.pdf".format(uuid.uuid4()))
fname = "/tmp/{}.pdf".format(uuid.uuid4()) fname = "/tmp/{}.pdf".format(uuid.uuid4())
try: try:
conn = cups.Connection () conn = cups.Connection ()
printer = os.environ.get('printer',"DEFAULT") printer_arr = os.environ.get('printer',"DEFAULT").split(";")
print_num = int(os.environ.get('print_num',0)) print_num = int(os.environ.get('print_num',0))
if printer_arr.__len__() > 0:
generate_pdf(body, fname)
for printer in printer_arr:
if 'ALARMDEPESCHE' in parsed_body: if 'ALARMDEPESCHE' in parsed_body:
with open(fname,"wb") as f:
f.write(HTML(string=body, base_url="").write_pdf())
for i in range(0, print_num): for i in range(0, print_num):
conn.printFile (printer, fname, "Alarmfax", {}) conn.printFile (printer, fname, "Alarmfax", {})
os.remove(fname)
except Exception as e: except Exception as e:
logging.error("cups_print", e)
finally:
if os.path.exists(fname): if os.path.exists(fname):
os.remove(fname) os.remove(fname)
logging.error("cups_print", e)

View File

@ -2,9 +2,12 @@ version: "2"
services: services:
app: app:
build: ./ build: ./
image: gitea.simonzeyer.de/simon/wnd_ils_alarmfax_parser:latest #image: gitea.simonzeyer.de/simon/wnd_ils_alarmfax_parser:latest
restart: always restart: always
privileged: true privileged: true
# ports:
# - 631:631
# - 5000:5000
environment: environment:
- username=${username} - username=${username}
- password=${password} - password=${password}
@ -20,6 +23,8 @@ services:
- alarminator_zvies_use_PEALGRP=${alarminator_zvies_use_PEALGRP} - alarminator_zvies_use_PEALGRP=${alarminator_zvies_use_PEALGRP}
- printer=${printer} - printer=${printer}
- print_num=${print_num} - print_num=${print_num}
- MAPS_API_KEY=${MAPS_API_KEY}
- BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD}
volumes: volumes:
- ./cups:/etc/cups - ./cups:/etc/cups
- ./cupsd.conf.txt:/etc/cups/cupsd.conf - ./cupsd.conf.txt:/etc/cups/cupsd.conf

View File

@ -1,20 +1,35 @@
blinker==1.8.2
Brotli==1.1.0
cached-property==1.5.2 cached-property==1.5.2
certifi==2022.12.7 certifi==2022.12.7
cffi==1.15.1 cffi==1.15.1
charset-normalizer==3.0.1 charset-normalizer==3.0.1
click==8.1.7
cryptography==39.0.0 cryptography==39.0.0
cssselect2==0.7.0
defusedxml==0.7.1 defusedxml==0.7.1
dnspython==2.3.0 dnspython==2.3.0
exchangelib==4.9.0 exchangelib==4.9.0
Flask==3.0.3
Flask-BasicAuth==0.2.0
fonttools==4.54.1
html5lib==1.1
idna==3.4 idna==3.4
isodate==0.6.1 isodate==0.6.1
itsdangerous==2.2.0
Jinja2==3.1.4
lxml==4.9.2 lxml==4.9.2
MarkupSafe==3.0.1
ntlm-auth==1.5.0 ntlm-auth==1.5.0
numpy==1.24.1 numpy==1.24.1
oauthlib==3.2.2 oauthlib==3.2.2
pandas==1.5.2 pandas==1.5.2
pillow==10.4.0
pycparser==2.21 pycparser==2.21
pycups==2.0.1
pydyf==0.11.0
Pygments==2.14.0 Pygments==2.14.0
pyphen==0.16.0
python-dateutil==2.8.2 python-dateutil==2.8.2
pytz==2022.7.1 pytz==2022.7.1
pytz-deprecation-shim==0.1.0.post0 pytz-deprecation-shim==0.1.0.post0
@ -22,10 +37,12 @@ requests==2.28.2
requests-ntlm==1.1.0 requests-ntlm==1.1.0
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
six==1.16.0 six==1.16.0
tinycss2==1.3.0
tzdata==2022.7 tzdata==2022.7
tzlocal==4.2 tzlocal==4.2
urllib3==1.26.14 urllib3==1.26.14
webencodings==0.5.1
Werkzeug==3.0.4
xmltodict==0.12.0 xmltodict==0.12.0
xmltojson==2.0.1 xmltojson==2.0.1
pycups==2.0.1 zopfli==0.2.3
weasyprint