diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..20d3a04
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 3
+
+ - package-ecosystem: "pip"
+ directory: "/kaaf"
+ schedule:
+ interval: "weekly"
\ No newline at end of file
diff --git a/.github/workflows/deploy-refusjon.yml b/.github/workflows/deploy-refusjon.yml
index 47713fd..a3c420a 100644
--- a/.github/workflows/deploy-refusjon.yml
+++ b/.github/workflows/deploy-refusjon.yml
@@ -2,27 +2,27 @@ name: Deploy refusjon container to Azure
on:
push:
- branches: [ refusjon-master ]
+ branches: refusjon-master
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: azure/docker-login@v1
with:
- login-server: ntnuiskjema.azurecr.io
+ login-server: ntnuiservices.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
- docker build . -t ntnuiskjema.azurecr.io/skjema-refusjon:latest
- docker push ntnuiskjema.azurecr.io/skjema-refusjon:latest
+ docker build . -t ntnuiservices.azurecr.io/skjema-refusjon:latest
+ docker push ntnuiservices.azurecr.io/skjema-refusjon:latest
- uses: azure/webapps-deploy@v2
with:
- app-name: 'ntnuirefusjon'
+ app-name: 'ntnui-skjema-refusjon'
publish-profile: ${{ secrets.REFUSJON_AZURE_WEBAPP_PUBLISH_PROFILE }}
- images: 'ntnuiskjema.azurecr.io/skjema-refusjon:latest'
\ No newline at end of file
+ images: 'ntnuiservices.azurecr.io/skjema-refusjon:latest'
\ No newline at end of file
diff --git a/.github/workflows/deploy-reise.yml b/.github/workflows/deploy-reise.yml
index 8ef82ab..20a85c7 100644
--- a/.github/workflows/deploy-reise.yml
+++ b/.github/workflows/deploy-reise.yml
@@ -2,27 +2,27 @@ name: Deploy reise container to Azure
on:
push:
- branches: [ reise-master ]
+ branches: reise-master
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: azure/docker-login@v1
with:
- login-server: ntnuiskjema.azurecr.io
+ login-server: ntnuiservices.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
- docker build . -t ntnuiskjema.azurecr.io/skjema-reise:latest
- docker push ntnuiskjema.azurecr.io/skjema-reise:latest
+ docker build . -t ntnuiservices.azurecr.io/skjema-reise:latest
+ docker push ntnuiservices.azurecr.io/skjema-reise:latest
- uses: azure/webapps-deploy@v2
with:
- app-name: 'ntnuireise'
+ app-name: 'ntnui-skjema-reise'
publish-profile: ${{ secrets.REISE_AZURE_WEBAPP_PUBLISH_PROFILE }}
- images: 'ntnuiskjema.azurecr.io/skjema-reise:latest'
\ No newline at end of file
+ images: 'ntnuiservices.azurecr.io/skjema-reise:latest'
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b392aab..a89c9f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@ webapp/.next/
webapp/out/
venv/
kaaf/__pycache__/
-.vscode
\ No newline at end of file
+.vscode
+.env
+output.pdf
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 506e471..cfe3d64 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,5 @@
-FROM openfaas/of-watchdog:0.8.2 as watchdog
-FROM python:3.7-slim AS build-backend
-
-RUN apt-get update && apt-get install -y poppler-utils
+FROM ghcr.io/openfaas/of-watchdog:0.9.11 as watchdog
+FROM python:3.11 AS build-backend
WORKDIR /app
@@ -10,6 +8,8 @@ RUN chmod +x /usr/bin/fwatchdog
COPY ./kaaf/req.txt ./kaaf/req.txt
+RUN python -m pip install --upgrade pip
+
RUN pip install --no-cache-dir -r kaaf/req.txt
FROM node:16-alpine3.11 AS build-frontend
diff --git a/README.md b/README.md
index 7473aa9..4909757 100644
--- a/README.md
+++ b/README.md
@@ -17,29 +17,29 @@ To run just the frontend:
To run the backend/everything:
- Make a virtual env with `python -m venv venv`
-- Enter the env with `source venv/bin/activate`
+- Enter the env with (Unix) `source venv/bin/activate` or (Windows) `source venv/Scripts/activate`
+- Make sure you are using latest pip with `python -m pip install --upgrade pip`
- Install packages with `pip install -r kaaf/req.txt`
- Start the server with `python kaaf/server.py`
- If the frontend is exported (`yarn export`), the webapp will be available at `localhost:5000` when running `server.py`
-> One of the packages (pdf2image) will require poppler to work correctly with tmp files. Most linux distros come with this.
-> For MacOS `brew install poppler`
-
### Generating PDFs
It might be nice to be able to quickly generate PDFs when developing, without having to start up everything. To do this you can run:
```python
-python kaaf/generate-example.py signature.png output.pdf image0.png image1.png ...
+python kaaf/generate-example.py signature.png attachment1.png attachment2.pdf ...
```
-Where `signature.png` and `imageN.png` are paths to image files (the latter images are optional)
+Where `signature.png` and `attachmentN.XYZ` are paths to image files.
## Environment variables
+While developing locally, you can temporarily add environment variables to the Dockerfile, such as `ENV MAIL_ADDRESS="no-reply@ntnui.no"`, or by creating an **.env** file with `KEY=VALUE` pairs separated by a newline.
+
| Variable | Function |
| --------------- | -------------------------------------------- |
-| `MAIL_ADDRESS` | Set the mail address for generated receipts |
-| `MAIL_PASSWORD` | Password for the mail account |
+| `SERVICE_ACCOUNT_STR` | Google service account string |
+| `MAIL_ADDRESS` | Set the mail address for generated receipts |
| `ENVIRONMENT` | Set to "production" for sentry errors |
| `SENTRY_DSN` | Ingest errors to sentry |
diff --git a/images/16bit-depth.png b/images/16bit-depth.png
new file mode 100644
index 0000000..c7ef124
Binary files /dev/null and b/images/16bit-depth.png differ
diff --git a/images/dragvoll.HEIC b/images/dragvoll.HEIC
new file mode 100644
index 0000000..f219f2b
Binary files /dev/null and b/images/dragvoll.HEIC differ
diff --git a/images/example-old-output.pdf b/images/example-old-output.pdf
new file mode 100644
index 0000000..8787234
Binary files /dev/null and b/images/example-old-output.pdf differ
diff --git a/images/example-signature.png b/images/example-signature.png
new file mode 100644
index 0000000..54df77b
Binary files /dev/null and b/images/example-signature.png differ
diff --git a/kaaf/generate-example.py b/kaaf/generate-example.py
index 47e0ec4..1b4205d 100644
--- a/kaaf/generate-example.py
+++ b/kaaf/generate-example.py
@@ -1,49 +1,78 @@
import argparse
import base64
+import sys
+import os
+import magic
-from handler import create_pdf, modify_data
+from handler import create_pdf
-default_data = {
- "date": "2020-12-27",
- "amount": "69 kr",
- "name": "Mats",
- "accountNumber": "010101010101",
- "committee": "Hovedstyret",
- "occasion": "Teste litt",
- "comment": "pls work",
+test_data = {
+ "name": "John Doe",
+ "mailfrom": "johndoe@ntnui.dev",
+ "committee": "Sprint",
+ "accountNumber": "123456789",
+ "amount": "69.69",
+ "date": "2023-05-17",
+ "occasion": "Expense reimbursement",
+ "comment": "Some comment\n with multiple\n newlines",
}
+if len(sys.argv) < 3:
+ print("Error: Missing arguments")
+ print(f"Usage: python3 {sys.argv[0]} signature_file, attachment_files")
+ print(f"Output: output.pdf")
+ sys.exit(1)
-def main(data, out):
- data = modify_data(data)
+# Parse the command line arguments
+signature_file = sys.argv[1]
+attachment_files = sys.argv[2:]
- pdf = create_pdf(data)
+allowed_extensions = {".pdf", ".jpg", ".jpeg", ".png", ".gif", ".heic"}
- with open(out, "wb") as f:
- f.write(pdf.encode("latin-1"))
- print("Done!")
+def is_valid_file_extension(file_path, allowed_extensions):
+ _, file_extension = os.path.splitext(file_path)
+ return file_extension.lower() in allowed_extensions
-def encode_image(img):
- with open(img, "rb") as f:
- b64 = base64.b64encode(f.read()).decode("ascii")
- return f'data:image/{img.split(".")[-1]};base64,{b64}'
+# Return exception if signature or attachment files are not valid
+if not is_valid_file_extension(signature_file, allowed_extensions):
+ raise Exception(f"Invalid signature file extension: {signature_file}")
+for file_path in attachment_files:
+ if not is_valid_file_extension(file_path, allowed_extensions):
+ raise Exception(f"Invalid attachment file extension: {file_path}")
+# Convert signature file to base64:image/png
+with open(signature_file, "rb") as f:
+ signature = f.read()
+ signature = base64.b64encode(signature).decode("utf-8")
+ signature = f"data:image/png;base64,{signature}"
-if __name__ == "__main__":
- parser = argparse.ArgumentParser()
- parser.add_argument("signature", help="Path to signature")
- parser.add_argument("out", help="Path to the generated pdf")
- parser.add_argument(
- "images", nargs=argparse.REMAINDER, default=[], help="Paths to images"
- )
- args = parser.parse_args()
+# Check file type and convert attachment files to base64 with MIME type
+attachments = []
+for file_path in attachment_files:
+ with open(file_path, "rb") as f:
+ # Read the file as bytes
+ file_data = f.read()
- data = {
- **default_data,
- "signature": encode_image(args.signature),
- "images": [encode_image(img) for img in args.images],
- }
+ # Detect the filetype using python-magic
+ file_type = magic.from_buffer(file_data, mime=True)
- main(data, args.out)
+ # Convert the file data to base64
+ file_data = base64.b64encode(file_data).decode("utf-8")
+
+ # Add the filetype prefix to the base64 string
+ file_data = f"data:{file_type};base64,{file_data}"
+
+ # Check if the filetype is one of the allowed ones
+ allowed_types = ["application/pdf", "image/jpeg", "image/png", "image/heic"]
+ if file_type in allowed_types:
+ # Append the file data to the attachments list
+ attachments.append(file_data)
+
+print(f"Signature: {signature[:50]}")
+for attachment in attachments:
+ print(f"Attachment: {attachment[:50]}")
+
+# Call the create_pdf function to generate the PDF
+create_pdf(test_data, signature, attachments)
diff --git a/kaaf/handler.py b/kaaf/handler.py
index 4418dcb..dfaf7cb 100644
--- a/kaaf/handler.py
+++ b/kaaf/handler.py
@@ -1,20 +1,12 @@
import base64
import logging
-import io
+import os
import tempfile
import mail
-import functools
-import operator
-
-from fpdf import FPDF
-from PIL import Image
+import fitz
from sentry_sdk import configure_scope
-
-# Handle PDF files
-from pdf2image import convert_from_path
-
-# Handle HEIC photoes
-import pyheif
+import io
+from PIL import Image
class UnsupportedFileException(Exception):
@@ -32,6 +24,8 @@ class UnsupportedFileException(Exception):
"comment": "Kommentar:",
}
+temporary_files = []
+
def data_is_valid(data):
fields = [
@@ -49,131 +43,151 @@ def data_is_valid(data):
return [f for f in fields if f not in data or len(data[f]) == 0]
-class PDF(FPDF):
- def header(self):
- self.image("images/ntnui.png", 10, 10, 33)
- self.set_font("Arial", "B", 15)
- self.ln(20)
-
- def footer(self):
- self.set_y(-15)
- self.set_font("Arial", "I", 8)
- self.cell(0, 10, f"Side {str(self.page_no())}/{{nb}}", 0, 0, "C")
-
-
-def image_to_byte_array(image: Image, fmt=None):
- imgByteArr = io.BytesIO()
- image.save(imgByteArr, format=fmt if fmt is not None else image.format)
- imgByteArr = imgByteArr.getvalue()
- return imgByteArr
-
-
-def create_image_file(image):
- """
- Take an image in BASE64 format and return a NamedTemporaryFile containing the image.
- Will handle PNG, JPEG and GIF without any changes, as FPDF will handle those files
- without problem. For PDFs we use pdf2image to convert each page to an image. For HEIC
- pictures we use pyheif to convert it to a jpeg.
- """
-
- if not "image/" in image and not "application/pdf" in image:
- raise UnsupportedFileException(image[:30])
- parts = image.split(";base64,")
- decoded = base64.b64decode(parts[1])
- suffix = "pdf" if "application/pdf" in image else parts[0].split("image/")[1]
- suffix = suffix.lower()
- f = tempfile.NamedTemporaryFile(suffix=f".{suffix}")
- f.write(decoded)
- f.flush()
-
- """
- FPDF does not support pdf files as input, therefore convert file:pdf to array[image:jpg]
- """
- if suffix == "pdf":
- files = []
- pil_images = convert_from_path(f.name, fmt="jpeg")
- for img in pil_images:
- f = tempfile.NamedTemporaryFile(suffix=f".{suffix}")
- f.write(image_to_byte_array(img))
- files.append({"file": f, "type": "jpeg"})
- f.flush()
- return files
-
- """
- FPDF does not support heic files as input, therefore we covert a image:heic image:jpg
- """
- if suffix == "heic":
- fmt = "JPEG"
- heif_file = pyheif.read(f.name)
- img = Image.frombytes(
- heif_file.mode,
- heif_file.size,
- heif_file.data,
- "raw",
- heif_file.mode,
- heif_file.stride,
- )
- f = tempfile.NamedTemporaryFile(suffix=f".{fmt}")
- f.write(image_to_byte_array(img, fmt))
- f.flush()
- return [{"file": f, "type": fmt}]
+def data_to_str(data, field_title_map):
+ left_column = []
+ right_column = []
+
+ for key, title in field_title_map.items():
+ if key in data:
+ left_column.append(f"{title}")
+ right_column.append(f"{data[key]}")
+
+ left_text = "\n\n".join(left_column)
+ right_text = "\n\n".join(right_column)
+
+ return left_text, right_text
+
+
+# Decode the base64 string and save it to a temporary file
+def base64_to_file(base64_string):
+ # Decode the base64 string
+ decoded = base64.b64decode(base64_string)
+ # Create a temporary file
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temporary_files.append(temp_file.name)
+ # Write the decoded data to the temporary file
+ temp_file.write(decoded)
+ # Close the file
+ temp_file.close()
+ # Return the path to the temporary file
+ return temp_file.name
+
+def add_page_number(page, page_number, total_pages):
+ footer_text = f"Side {page_number} av {total_pages}"
+ fontsize = 9
+ fontname = "Helvetica"
+
+ # Measure the width of the rendered footer_text
+ font = fitz.Font(fontname)
+ text_width = font.text_length(footer_text, fontsize)
+
+ footer_position = fitz.Point(
+ (page.rect.width - text_width) / 2,
+ page.rect.height - 30
+ )
+ page.insert_text(
+ footer_position, footer_text, fontname=fontname, fontsize=fontsize
+ )
+
- return [{"file": f, "type": suffix.upper()}]
+def create_pdf(data, signature=None, images=None):
+ doc = fitz.open()
+ page = doc.new_page()
+ page.insert_text(
+ fitz.Point(50, 75), "Refusjonsskjema", fontname="Helvetica-Bold", fontsize=24
+ )
-def modify_data(data):
- signature = data.pop("signature")
- images = data.pop("images")
+ logo = fitz.Pixmap("images/ntnui.png")
+ page.insert_image(fitz.Rect(425, 40, 525, 90), pixmap=logo)
- data["signature"] = create_image_file(signature)[0]
- data["images"] = functools.reduce(
- operator.iconcat, [create_image_file(img) for img in images], []
+ # Add the input values in a two-column layout
+ left_text, right_text = data_to_str(data, field_title_map)
+ page.insert_text(
+ fitz.Point(50, 150), left_text, fontname="Helvetica-Bold", fontsize=11
+ )
+ page.insert_text(
+ fitz.Point(250, 150), right_text, fontname="Helvetica", fontsize=11
)
- return data
-
-
-def create_pdf(data):
- pdf = PDF()
- pdf.alias_nb_pages()
- pdf.add_page()
- pdf.set_font("Arial", "B", 16)
-
- signature = data.pop("signature")
- images = data.pop("images")
-
- pdf.cell(0, 14, "Refusjonsskjema", ln=1)
-
- pdf.set_font("Arial", "", 12)
- data["amount"] = data["amount"].replace(".", ",") # Format amount to Norwegian standard
- for key in field_title_map.keys():
- pdf.set_font("", "B")
- pdf.cell(90, 8, txt=field_title_map[key])
- pdf.set_font("", "")
- pdf.multi_cell(0, 8, txt=data[key])
-
- pdf.set_font("", "B")
- pdf.cell(0, 20, txt="Signatur:", ln=1)
- pdf.image(signature["file"].name, h=30, type=signature["type"])
- signature["file"].close()
- pdf.cell(0, 5, txt="", ln=1)
- pdf.cell(0, 20, txt="Vedlegg:", ln=1)
- max_img_width = 190
- max_img_height = 220
- for image in images:
- img = Image.open(image["file"].name)
- w, h = img.size
- img.close()
-
- size = (
- {"w": max_img_width}
- if w / h >= max_img_width / max_img_height
- else {"h": max_img_height}
+ # Add the signature image
+ if signature is None:
+ raise RuntimeError("No signature provided")
+ if signature.startswith("data:image"):
+ signature = base64_to_file(signature.split(",")[1])
+ page.insert_text(
+ fitz.Point(50, page.bound().height * 0.67),
+ "Signatur:",
+ fontname="Helvetica-Bold",
+ fontsize=12,
+ )
+ signature_pixmap = fitz.Pixmap(signature)
+ signature_rect = fitz.Rect(
+ 50, page.bound().height * 0.67, 550, page.bound().height * 0.97
+ )
+ page.insert_image(signature_rect, pixmap=signature_pixmap)
+
+ # Add the remaining pages with the receipt attachments
+ if images is None:
+ raise RuntimeError("No images provided")
+ if not isinstance(images, list):
+ images = [images]
+ for attachment in images:
+ # Get file type from base64 string
+ if not "image/" in attachment and not "application/pdf" in attachment:
+ raise UnsupportedFileException(
+ f"Unsupported file type in base64 string: {attachment[:30]}"
+ )
+ parts = attachment.split(";base64,")
+ file_type = (
+ "pdf" if "application/pdf" in attachment else parts[0].split("image/")[1]
)
-
- pdf.image(image["file"].name, **size, type=image["type"])
- image["file"].close()
- return pdf.output(dest="S")
+ attachment = base64_to_file(parts[1])
+ if file_type == "pdf":
+ pdf_doc = fitz.open(attachment)
+ for i in range(pdf_doc.page_count):
+ page = doc.new_page()
+ page.show_pdf_page(fitz.Rect(0, 0, 612, 792), pdf_doc, i)
+ pdf_doc.close()
+ elif file_type in ["jpg", "jpeg", "png", "gif"]:
+ page = doc.new_page()
+ pixmap = fitz.Pixmap(attachment)
+ page.insert_image(page.rect, pixmap=pixmap)
+ ## TODO: HEIC is received as application/octet-stream, not as image/heic
+ # elif file_type == 'heic':
+ # heif_image = pyheif.read(attachment)
+ # png_image = Image.frombytes(
+ # heif_image.mode,
+ # heif_image.size,
+ # heif_image.data,
+ # "raw",
+ # heif_image.mode,
+ # heif_image.stride,
+ # )
+ # png_bytes = io.BytesIO()
+ # png_image.save(png_bytes, format="PNG")
+ # width, height = png_image.size
+ # samples = png_image.tobytes()
+ # pixmap = fitz.Pixmap(fitz.csRGB, width, height, samples)
+ # page.insert_image(page.rect, pixmap=pixmap)
+ else:
+ raise UnsupportedFileException(
+ f"Unsupported file type: {file_type}. Use pdf, jpg, jpeg or png"
+ )
+
+ # Add page numbers to all pages
+ for i, page in enumerate(doc):
+ add_page_number(page, i + 1, doc.page_count)
+
+ # Save the PDF document
+ doc.save("output.pdf")
+ with open("output.pdf", "rb") as pdf_file:
+ pdf_bytes = pdf_file.read()
+ doc.close()
+ for f in temporary_files:
+ os.remove(f)
+ temporary_files.remove(f)
+ return pdf_bytes # Return the PDF document as bytes
def handle(data):
@@ -189,16 +203,7 @@ def handle(data):
return f'Requires fields {", ".join(req_fields)}', 400
try:
- data = modify_data(data)
- except UnsupportedFileException as e:
- logging.error(f"Unsupported file type: {e}")
- return (
- "En av filene som ble lastet opp er ikke i støttet format. Bruk PNG, JPEG, GIF, HEIC eller PDF",
- 400,
- )
-
- try:
- file = create_pdf(data)
+ file = create_pdf(data, data["signature"], data["images"])
mail.send_mail([data["mailto"], data["mailfrom"]], data, file)
except RuntimeError as e:
logging.warning(f"Failed to generate pdf with exception: {e}")
diff --git a/kaaf/mail.py b/kaaf/mail.py
index da1400e..5eda767 100644
--- a/kaaf/mail.py
+++ b/kaaf/mail.py
@@ -1,3 +1,4 @@
+from io import BytesIO
import logging
import os
import json
@@ -17,16 +18,16 @@ class MailConfigurationException(Exception):
def service_account_login(mail_from, service_account_str):
- SCOPES = ['https://www.googleapis.com/auth/gmail.send']
- credentials = service_account.Credentials.from_service_account_info(json.loads(base64.b64decode(service_account_str)), scopes=SCOPES)
+ SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
+ credentials = service_account.Credentials.from_service_account_info(
+ json.loads(base64.b64decode(service_account_str)), scopes=SCOPES
+ )
delegated_credentials = credentials.with_subject(mail_from)
- return build('gmail', 'v1', credentials=delegated_credentials)
+ return build("gmail", "v1", credentials=delegated_credentials)
def create_mail(msg, body):
- msg[
- "Subject"
- ] = f'Refusjonsskjema - {body.get("name", "")}'
+ msg["Subject"] = f'Refusjonsskjema - {body.get("name", "")}'
text = ""
text += f'Navn: {body.get("name", "")}\n'
@@ -37,7 +38,7 @@ def create_mail(msg, body):
text += f'Dato: {body.get("date", "")}\n'
text += f'Anledning/arrangement: {body.get("occasion", "")}\n'
text += f'Kommentar: {body.get("comment", "")}\n'
- text += f'\n'
+ text += f"\n"
text += f"Refusjonsskjema er generert og vedlagt. Ved spørsmål ta kontakt med kasserer@ntnui.no!"
msg.attach(MIMEText(text))
@@ -56,7 +57,9 @@ def send_mail(mail_to, body, file):
create_mail(msg, body)
- filename = body.get("date", "") + " Refusjonsskjema " + body.get("name", "") + ".pdf"
+ filename = (
+ body.get("date", "") + " Refusjonsskjema " + body.get("name", "") + ".pdf"
+ )
part = MIMEApplication(file, Name=filename)
part["Content-Disposition"] = f'attachment; filename="{filename}"'
msg.attach(part)
@@ -65,6 +68,6 @@ def send_mail(mail_to, body, file):
service = service_account_login(mail_from, service_account_str)
raw = base64.urlsafe_b64encode(msg.as_bytes())
- body = { 'raw': raw.decode() }
+ body = {"raw": raw.decode()}
messages = service.users().messages()
messages.send(userId="me", body=body).execute()
diff --git a/kaaf/req.txt b/kaaf/req.txt
index d054637..86a534c 100644
--- a/kaaf/req.txt
+++ b/kaaf/req.txt
@@ -1,10 +1,14 @@
-flask
-gevent
-fpdf
-black
-Pillow
+flask==2.3.2
+gevent==22.10.2
+PyMuPDF==1.22.2
+pymupdf-fonts==1.0.5
+fonttools==4.39.3
+black==23.3.0
+Pillow==9.5.0
sentry-sdk[flask]==0.16.2
-pdf2image
-pyheif
-google
-google-api-python-client
\ No newline at end of file
+pdf2image==1.16.3
+# pyheif # not available on Windows, and HEIC attachments is broken
+google==3.0.0
+google-api-python-client==2.86.0
+python-magic==0.4.27
+python-dotenv==1.0.0
diff --git a/kaaf/server.py b/kaaf/server.py
index 95873bf..54d2ae2 100644
--- a/kaaf/server.py
+++ b/kaaf/server.py
@@ -6,6 +6,8 @@
from sentry_sdk.integrations.flask import FlaskIntegration
from handler import handle
+from dotenv import load_dotenv
+
static_file_directory = os.environ.get("STATIC_DIRECTORY", "../webapp/out/")
@@ -15,6 +17,12 @@
environment=os.environ.get("ENVIRONMENT"),
integrations=[FlaskIntegration()],
)
+else:
+ if os.path.exists(".env") and os.path.getsize(".env") != 0:
+ print("✔ .env found")
+ load_dotenv(verbose=True)
+ else:
+ print("⚠ .env file not found, or is empty")
app = Flask(__name__, static_folder=static_file_directory, static_url_path="")
@@ -26,7 +34,7 @@ def fix_transfer_encoding():
"""
transfer_encoding = request.headers.get("Transfer-Encoding", None)
- if transfer_encoding == u"chunked":
+ if transfer_encoding == "chunked":
request.environ["wsgi.input_terminated"] = True
@@ -43,4 +51,5 @@ def main_route():
if __name__ == "__main__":
http_server = WSGIServer(("", 5000), app)
+ print("✔ Server started at http://localhost:5000/")
http_server.serve_forever()
diff --git a/webapp/components/Form.tsx b/webapp/components/Form.tsx
index 3744bc5..81b5565 100644
--- a/webapp/components/Form.tsx
+++ b/webapp/components/Form.tsx
@@ -68,6 +68,7 @@ const Form = (): JSX.Element => {
{
{
{
-///
///
// NOTE: This file should not be edited