generated from nathanwoodburn/python-webserver-template
feat: Add initial code
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
This commit is contained in:
parent
8508e5fbd7
commit
039a24bc64
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ __pycache__/
|
||||
.env
|
||||
.vs/
|
||||
.venv/
|
||||
notifications.json
|
@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
COPY . /app
|
||||
|
||||
# Optionally mount /data to store the data
|
||||
# VOLUME /data
|
||||
VOLUME /data
|
||||
|
||||
ENTRYPOINT ["python3"]
|
||||
CMD ["main.py"]
|
||||
|
@ -1,4 +1,6 @@
|
||||
flask
|
||||
gunicorn
|
||||
requests
|
||||
python-dotenv
|
||||
python-dotenv
|
||||
cachetools
|
||||
markdownify
|
352
server.py
352
server.py
@ -15,11 +15,17 @@ import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
import dotenv
|
||||
from functools import cache
|
||||
from markdownify import markdownify as md
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
notification_file = "/data/notifications.json"
|
||||
if not os.path.exists("/data"):
|
||||
notification_file = "./notifications.json"
|
||||
|
||||
|
||||
def find(name, path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
@ -27,6 +33,8 @@ def find(name, path):
|
||||
return os.path.join(root, name)
|
||||
|
||||
# Assets routes
|
||||
|
||||
|
||||
@app.route("/assets/<path:path>")
|
||||
def send_assets(path):
|
||||
if path.endswith(".json"):
|
||||
@ -74,7 +82,335 @@ def wellknown(path):
|
||||
# region Main routes
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
# Check if user is logged in
|
||||
token = request.cookies.get("token")
|
||||
if token:
|
||||
return redirect("/inbox")
|
||||
|
||||
return render_template("login.html", login=f"https://login.hns.au/auth?return={request.url}login")
|
||||
|
||||
|
||||
@app.route("/login")
|
||||
def login():
|
||||
# Get username and token from args
|
||||
token = request.args.get("token")
|
||||
# Set token cookie
|
||||
response = make_response(redirect("/inbox"))
|
||||
response.set_cookie("token", token)
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
# Delete token cookie
|
||||
response = make_response(redirect("/"))
|
||||
response.set_cookie("token", "")
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/inbox")
|
||||
def inbox():
|
||||
# Check if user is logged in
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return redirect("/")
|
||||
|
||||
# Get user info
|
||||
|
||||
user = get_user(token)
|
||||
if not user:
|
||||
return redirect("/logout")
|
||||
|
||||
email = user["hemail"]
|
||||
return render_template("inbox.html", emails=get_email(email))
|
||||
|
||||
|
||||
@app.route("/delete/<path:path>")
|
||||
def delete(path):
|
||||
# Check if user is logged in
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return redirect("/")
|
||||
|
||||
# Get user info
|
||||
user = get_user(token)
|
||||
if not user:
|
||||
return redirect("/logout")
|
||||
|
||||
email = user["hemail"]
|
||||
delete_email(path, email)
|
||||
return redirect("/inbox")
|
||||
|
||||
|
||||
@app.route("/notifications")
|
||||
def notifications():
|
||||
# Check if user is logged in
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return redirect("/")
|
||||
|
||||
# Get user info
|
||||
user = get_user(token)
|
||||
if not user:
|
||||
return redirect("/logout")
|
||||
|
||||
email = user["hemail"]
|
||||
notification = get_notification_settings(email)
|
||||
if not notification:
|
||||
return render_template("notifications.html", email=email)
|
||||
return render_template("notifications.html", email=email, webhook=notification["webhook"], email_alert=notification["email"])
|
||||
|
||||
|
||||
@app.route("/notifications", methods=["POST"])
|
||||
def notification_set():
|
||||
# Check if user is logged in
|
||||
token = request.cookies.get("token")
|
||||
if not token:
|
||||
return redirect("/")
|
||||
|
||||
# Get user info
|
||||
user = get_user(token)
|
||||
if not user:
|
||||
return redirect("/logout")
|
||||
|
||||
email = user["hemail"]
|
||||
webhook = request.form.get("webhook")
|
||||
email_alert = request.form.get("email")
|
||||
|
||||
save_notification_settings(email, webhook, email_alert)
|
||||
|
||||
return redirect("/notifications")
|
||||
|
||||
|
||||
def save_notification_settings(email, webhook, email_alert):
|
||||
if not os.path.exists(notification_file):
|
||||
with open(notification_file, "w") as f:
|
||||
json.dump({
|
||||
email: {
|
||||
"webhook": webhook,
|
||||
"email": email_alert
|
||||
}
|
||||
}, f, indent=4)
|
||||
|
||||
with open(notification_file, "r") as f:
|
||||
data = json.load(f)
|
||||
data[email] = {
|
||||
"webhook": webhook,
|
||||
"email": email_alert
|
||||
}
|
||||
with open(notification_file, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
|
||||
def get_notification_settings(email):
|
||||
if not os.path.exists(notification_file):
|
||||
return False
|
||||
|
||||
with open(notification_file, "r") as f:
|
||||
data = json.load(f)
|
||||
if email not in data:
|
||||
return {"webhook": "", "email": ""}
|
||||
return data[email]
|
||||
|
||||
|
||||
def delete_email(conversation_id, email):
|
||||
# Get email by id
|
||||
conversation = get_conversation(conversation_id)
|
||||
if not conversation:
|
||||
return
|
||||
print(conversation)
|
||||
|
||||
if len(conversation) < 1:
|
||||
return
|
||||
|
||||
headers = {
|
||||
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||
}
|
||||
for thread in conversation:
|
||||
for message in thread["messages"]:
|
||||
if email in message["to"]:
|
||||
requests.delete(
|
||||
f"https://ticket.woodburn.au/api/conversations/{thread['id']}", headers=headers)
|
||||
return
|
||||
|
||||
|
||||
@app.route("/api/email", methods=["POST"])
|
||||
def newemail():
|
||||
# Get the json
|
||||
json = request.get_json()
|
||||
|
||||
# Get header x-freescout-event
|
||||
event = request.headers.get("x-freescout-event")
|
||||
|
||||
if event == "convo.created":
|
||||
send_notification(json)
|
||||
elif event == "convo.customer.reply.created":
|
||||
send_notification(json)
|
||||
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
@app.route("/api/email/<path:path>")
|
||||
def getemail(path):
|
||||
email = f'{path}@login.hns.au'
|
||||
return jsonify(get_email(email))
|
||||
|
||||
|
||||
def send_notification(data):
|
||||
emails = data["cc"]
|
||||
for email in emails:
|
||||
notifications = get_notification_settings(email)
|
||||
if not notifications:
|
||||
continue
|
||||
if notifications["email"] != "":
|
||||
send_email(notifications["email"], data)
|
||||
if notifications["webhook"] != "":
|
||||
send_webhook(notifications["webhook"], data)
|
||||
|
||||
|
||||
def send_email(email, data):
|
||||
print(f"Sending email to {email}")
|
||||
headers = {
|
||||
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||
}
|
||||
sender = f"{data['createdBy']['firstName']} {
|
||||
data['createdBy']['lastName']} ({data['createdBy']['email']})"
|
||||
message = data["_embedded"]["threads"][0]["body"]
|
||||
|
||||
body = {
|
||||
"type": "email",
|
||||
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||
"subject": f"New email from {sender}",
|
||||
"customer": {
|
||||
"email": email
|
||||
},
|
||||
"threads": [
|
||||
{
|
||||
"text": f"You have a new email from {sender}\n\n{message}",
|
||||
"type": "message",
|
||||
"user": 1
|
||||
}
|
||||
],
|
||||
"imported": False,
|
||||
"status": "closed",
|
||||
}
|
||||
req = requests.post(
|
||||
"https://ticket.woodburn.au/api/conversations", json=body, headers=headers)
|
||||
if req.status_code != 201:
|
||||
print(f"Failed to send email")
|
||||
print(req.text)
|
||||
|
||||
|
||||
def send_webhook(webhook, data):
|
||||
print(f"Sending webhook to {webhook}")
|
||||
sender = f"{data['createdBy']['firstName']} {
|
||||
data['createdBy']['lastName']} ({data['createdBy']['email']})"
|
||||
message = data["_embedded"]["threads"][0]["body"]
|
||||
|
||||
message = md(message)
|
||||
|
||||
trimmed = len(message) > 3999
|
||||
# Only keep 4000 chars
|
||||
message = message[:4000]
|
||||
|
||||
subject = data["subject"]
|
||||
body = {
|
||||
"content": "You have a new email",
|
||||
"embeds": [
|
||||
{
|
||||
"title": subject,
|
||||
"description": message,
|
||||
"url": "https://email.hns.au",
|
||||
"color": 5814783,
|
||||
"author": {
|
||||
"name": sender
|
||||
}
|
||||
}
|
||||
],
|
||||
"username": "HNS Login Email",
|
||||
"avatar_url": "https://hns.au/favicon.png",
|
||||
"attachments": []
|
||||
}
|
||||
if trimmed:
|
||||
body["embeds"][0]["footer"] = {
|
||||
"text": "Email has been trimmed"
|
||||
}
|
||||
|
||||
req = requests.post(
|
||||
webhook, json=body)
|
||||
|
||||
|
||||
@cache
|
||||
def get_user(token):
|
||||
req = requests.get(f"https://login.hns.au/auth/user?token={token}")
|
||||
if req.status_code != 200:
|
||||
return False
|
||||
return req.json()
|
||||
|
||||
|
||||
@cache
|
||||
def get_email(email):
|
||||
params = {
|
||||
"embed": "threads",
|
||||
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||
"status": "active",
|
||||
"type": "email",
|
||||
"sortField": "updatedAt",
|
||||
"sortOrder": "desc",
|
||||
"page": "1",
|
||||
"pageSize": "100"
|
||||
}
|
||||
headers = {
|
||||
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||
}
|
||||
|
||||
conversations = requests.get(
|
||||
f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers)
|
||||
|
||||
if conversations.status_code != 200:
|
||||
return jsonify({"email": email, "error": "Could not get conversations"}, status=400)
|
||||
|
||||
threads = []
|
||||
conversations = conversations.json()
|
||||
for conversation in conversations["_embedded"]["conversations"]:
|
||||
addresses = conversation["cc"]
|
||||
for address in addresses:
|
||||
if address == email:
|
||||
threads.append(parse_email(conversation))
|
||||
|
||||
return {"email": email, "conversations": threads}
|
||||
|
||||
|
||||
@cache
|
||||
def get_conversation(id):
|
||||
params = {
|
||||
"number": id,
|
||||
"embed": "threads",
|
||||
"mailboxId": os.getenv("FREESCOUT_MAILBOX"),
|
||||
"status": "active",
|
||||
"type": "email",
|
||||
"sortField": "updatedAt",
|
||||
"sortOrder": "desc",
|
||||
"page": "1",
|
||||
"pageSize": "100",
|
||||
|
||||
}
|
||||
headers = {
|
||||
"X-FreeScout-API-Key": os.getenv("FREESCOUT_API_KEY"),
|
||||
}
|
||||
|
||||
conversations = requests.get(
|
||||
f"https://ticket.woodburn.au/api/conversations", json=params, headers=headers)
|
||||
|
||||
if conversations.status_code != 200:
|
||||
return {"conversations": []}
|
||||
|
||||
threads = []
|
||||
conversations = conversations.json()
|
||||
for conversation in conversations["_embedded"]["conversations"]:
|
||||
threads.append(parse_email(conversation))
|
||||
|
||||
return threads
|
||||
|
||||
|
||||
@app.route("/<path:path>")
|
||||
@ -100,10 +436,24 @@ def catch_all(path: str):
|
||||
|
||||
|
||||
# endregion
|
||||
def parse_email(conversation):
|
||||
messages = []
|
||||
threads = conversation["_embedded"]["threads"]
|
||||
for thread in threads:
|
||||
messages.append({
|
||||
"from": thread["createdBy"]["email"],
|
||||
"name": f'{thread["createdBy"]["firstName"]} {thread["createdBy"]["lastName"]}',
|
||||
"body": thread["body"],
|
||||
"date": thread["createdAt"],
|
||||
"to": thread["to"]
|
||||
})
|
||||
|
||||
return {"messages": messages, "id": conversation["id"]}
|
||||
|
||||
# region Error Catching
|
||||
# 404 catch all
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template("404.html"), 404
|
||||
|
@ -2,19 +2,103 @@ body {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 50px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.centre {
|
||||
margin-top: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 5px;
|
||||
color: #000000;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #000000;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
border: 5px solid #ffffff;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 25px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.email {
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 5px;
|
||||
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.from {
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.date {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #000000;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.body a {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 5px;
|
||||
color: #000000;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: min(500px, 90%);
|
||||
}
|
37
templates/inbox.html
Normal file
37
templates/inbox.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inbox | HNS Login Email</title>
|
||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header style="display: inline;">
|
||||
<h1>HNS Login Email</h1> <span style="float:right">{{emails.email}} | <a href="/notifications">Notifications</a> | <a href="/logout">Logout</a></span>
|
||||
</header>
|
||||
|
||||
<div class="centre">
|
||||
{% for conversation in emails.conversations %}
|
||||
<div class="conversation">
|
||||
<span>Conversation: {{conversation.id}}</span>
|
||||
{% for message in conversation.messages %}
|
||||
<div class="email">
|
||||
<div class="from">{{message.name}} ({{message.from}})</div> <div class="date">{{message.date}}</div>
|
||||
<div class="body">{{message.body|safe}}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<a class="button" href="/delete/{{conversation.id}}">Delete Conversation</a>
|
||||
</div>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
19
templates/login.html
Normal file
19
templates/login.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HNS Login Email</title>
|
||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="centre">
|
||||
<a href="{{login}}"><h1>Login to HNS Login Email</h1></a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
33
templates/notifications.html
Normal file
33
templates/notifications.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notifications | HNS Login Email</title>
|
||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header style="display: inline;">
|
||||
<h1>HNS Login Email</h1> <span style="float:right">{{email}} | <a href="/inbox">Inbox</a> | <a
|
||||
href="/logout">Logout</a></span>
|
||||
</header>
|
||||
|
||||
<div class="centre">
|
||||
<form action="/notifications" method="post">
|
||||
<!-- Discord webhook url -->
|
||||
<label for="webhook">Discord Webhook URL</label>
|
||||
<input type="text" name="webhook" placeholder="https://discord.com/api/webhooks/..." value="{{webhook}}">
|
||||
<!-- Email -->
|
||||
<br>
|
||||
<label for="email">Email Notification</label>
|
||||
<input type="email" name="email" placeholder="example@example.com" value="{{email_alert}}">
|
||||
<br>
|
||||
<input class="button" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user