feat: Add initial code
All checks were successful
Build Docker / BuildImage (push) Successful in 54s

This commit is contained in:
Nathan Woodburn 2024-10-04 20:43:58 +10:00
parent 8508e5fbd7
commit 039a24bc64
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
8 changed files with 529 additions and 3 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__/
.env .env
.vs/ .vs/
.venv/ .venv/
notifications.json

View File

@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY . /app COPY . /app
# Optionally mount /data to store the data # Optionally mount /data to store the data
# VOLUME /data VOLUME /data
ENTRYPOINT ["python3"] ENTRYPOINT ["python3"]
CMD ["main.py"] CMD ["main.py"]

View File

@ -1,4 +1,6 @@
flask flask
gunicorn gunicorn
requests requests
python-dotenv python-dotenv
cachetools
markdownify

352
server.py
View File

@ -15,11 +15,17 @@ import json
import requests import requests
from datetime import datetime from datetime import datetime
import dotenv import dotenv
from functools import cache
from markdownify import markdownify as md
dotenv.load_dotenv() dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
notification_file = "/data/notifications.json"
if not os.path.exists("/data"):
notification_file = "./notifications.json"
def find(name, path): def find(name, path):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
@ -27,6 +33,8 @@ def find(name, path):
return os.path.join(root, name) return os.path.join(root, name)
# Assets routes # Assets routes
@app.route("/assets/<path:path>") @app.route("/assets/<path:path>")
def send_assets(path): def send_assets(path):
if path.endswith(".json"): if path.endswith(".json"):
@ -74,7 +82,335 @@ def wellknown(path):
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): 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>") @app.route("/<path:path>")
@ -100,10 +436,24 @@ def catch_all(path: str):
# endregion # 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 # region Error Catching
# 404 catch all # 404 catch all
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
return render_template("404.html"), 404 return render_template("404.html"), 404

View File

@ -2,19 +2,103 @@ body {
background-color: #000000; background-color: #000000;
color: #ffffff; color: #ffffff;
} }
h1 { h1 {
font-size: 50px; font-size: 50px;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.centre { .centre {
margin-top: 10%; margin-top: 10%;
text-align: center; text-align: center;
} }
a { a {
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
text-decoration: underline; 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
View 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
View 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>

View 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>