feat: Add api routes for email and other basic info
All checks were successful
Build Docker / BuildImage (push) Successful in 39s

This commit is contained in:
Nathan Woodburn 2024-10-31 21:11:13 +11:00
parent 855f6b3c99
commit 42aff1f455
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
3 changed files with 263 additions and 78 deletions

88
mail.py Normal file
View File

@ -0,0 +1,88 @@
import smtplib
import re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
from flask import jsonify
import os
def validateSender(email):
domains = os.getenv("EMAIL_DOMAINS").split(",")
for domain in domains:
if re.match(r".+@" + domain, email):
return True
return False
def sendEmail(data):
fromEmail = "noreply@woodburn.au"
if "from" in data:
fromEmail = data["from"]
if not validateSender(fromEmail):
return jsonify({
"status": 400,
"message": "Bad request 'from' email invalid"
})
if "to" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'to' json data missing"
})
to = data["to"]
if "subject" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'subject' json data missing"
})
subject = data["subject"]
if "body" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'body' json data missing"
})
body = data["body"]
if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
raise ValueError("Invalid recipient email address.")
if not subject:
raise ValueError("Subject cannot be empty.")
if not body:
raise ValueError("Body cannot be empty.")
fromName = "Nathan Woodburn"
if 'sender' in data:
fromName = data['sender']
# Create the email message
msg = MIMEMultipart()
msg['From'] = formataddr((fromName, fromEmail))
msg['To'] = to
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
# Sending the email
try:
with smtplib.SMTP_SSL(os.getenv("EMAIL_SMTP"), 465) as server:
server.login(os.getenv("EMAIL_USER"), os.getenv("EMAIL_PASS"))
server.sendmail(fromEmail, to, msg.as_string())
print("Email sent successfully.")
return jsonify({
"status": 200,
"message": "Send email successfully"
})
except Exception as e:
return jsonify({
"status": 500,
"error": "Sending email failed",
"exception":e
})

46
now.py Normal file
View File

@ -0,0 +1,46 @@
import os
from flask import render_template
from datetime import datetime
def list_now_page_files():
now_pages = os.listdir("templates/now")
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
]
now_pages.sort(reverse=True)
return now_pages
def list_now_dates():
now_pages = list_now_page_files()
now_dates = [page.split(".")[0] for page in now_pages]
return now_dates
def get_latest_now_date(formatted=False):
if formatted:
date=list_now_dates()[0]
date = datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
return date
return list_now_dates()[0]
#region Rendering
def render_now_page(date,handshake_scripts=None):
# If the date is not available, render the latest page
if date is None:
return render_latest_now(handshake_scripts=handshake_scripts)
if not date in list_now_dates():
return render_template("404.html"), 404
date_formatted = datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
return render_template(f"now/{date}.html",DATE=date_formatted,handshake_scripts=handshake_scripts)
def render_latest_now(handshake_scripts=None):
now_page = list_now_dates()[0]
print(now_page)
return render_now_page(now_page,handshake_scripts=handshake_scripts)
#endregion

199
server.py
View File

@ -31,6 +31,8 @@ from solders.message import MessageV0
from solders.transaction import VersionedTransaction from solders.transaction import VersionedTransaction
from solders.null_signer import NullSigner from solders.null_signer import NullSigner
from PIL import Image from PIL import Image
from mail import sendEmail
import now
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@ -295,7 +297,7 @@ def donateAmount(amount):
"icon": "https://nathan.woodburn.au/assets/img/profile.png", "icon": "https://nathan.woodburn.au/assets/img/profile.png",
"label": f"Donate {amount} SOL to Nathan.Woodburn/", "label": f"Donate {amount} SOL to Nathan.Woodburn/",
"title": "Donate to Nathan.Woodburn/", "title": "Donate to Nathan.Woodburn/",
"description": "Donate {amount} SOL to Nathan.Woodburn/", "description": f"Donate {amount} SOL to Nathan.Woodburn/",
} }
return jsonify(data) return jsonify(data)
@ -342,6 +344,80 @@ def donateAmountPost(amount):
return jsonify({"message": "Success", "transaction": base64_string}) return jsonify({"message": "Success", "transaction": base64_string})
# endregion
#region Other API routes
@app.route("/api/time")
def time():
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"])
timezone = datetime.timezone(offset=timezone_offset)
time = datetime.datetime.now(tz=timezone)
return jsonify({
"timestring": time.strftime("%A, %B %d, %Y %I:%M %p"),
"timestamp": time.timestamp(),
"timezone": ncConfig["time-zone"],
"timeISO": time.isoformat()
})
@app.route("/api/timezone")
def timezone():
return jsonify({"timezone": ncConfig["time-zone"]})
@app.route("/api/timezone", methods=["POST"])
def timezonePost():
# Refresh config
global ncConfig
conf = requests.get("https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json")
if conf.status_code != 200:
return jsonify({"message": "Error: Could not get timezone"})
if not conf.json():
return jsonify({"message": "Error: Could not get timezone"})
conf = conf.json()
if "time-zone" not in conf:
return jsonify({"message": "Error: Could not get timezone"})
ncConfig = conf
return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]})
@app.route("/api/message")
def nc():
return jsonify({"message": ncConfig["message"]})
@app.route("/api/ip")
def ip():
return jsonify({"ip": request.remote_addr})
@app.route("/api/email", methods=["POST"])
def email():
# Verify json
if not request.is_json:
return jsonify({
"status": 400,
"error": "Bad request JSON Data missing"
})
# Check if api key sent
data = request.json
if "key" not in data:
return jsonify({
"status": 401,
"error": "Unauthorized 'key' missing"
})
if data["key"] != os.getenv("EMAIL_KEY"):
return jsonify({
"status": 401,
"error": "Unauthorized 'key' invalid"
})
return sendEmail(data)
#endregion #endregion
# endregion # endregion
@ -349,6 +425,10 @@ def donateAmountPost(amount):
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): def index():
global handshake_scripts
global projects
global projectsUpdated
# Check if host if podcast.woodburn.au # Check if host if podcast.woodburn.au
if "podcast.woodburn.au" in request.host: if "podcast.woodburn.au" in request.host:
return render_template("podcast.html") return render_template("podcast.html")
@ -361,6 +441,16 @@ def index():
# Check if crawler # Check if crawler
if request.headers: if request.headers:
# Check if curl
if "curl" in request.headers.get("User-Agent"):
return jsonify(
{
"message": "Welcome to Nathan.Woodburn/! This is a personal website. For more information, visit https://nathan.woodburn.au",
"ip": request.remote_addr,
"dev": handshake_scripts == "",
}
)
if "Googlebot" not in request.headers.get( if "Googlebot" not in request.headers.get(
"User-Agent" "User-Agent"
) and "Bingbot" not in request.headers.get("User-Agent"): ) and "Bingbot" not in request.headers.get("User-Agent"):
@ -377,10 +467,6 @@ def index():
resp.set_cookie("loaded", "true", max_age=604800) resp.set_cookie("loaded", "true", max_age=604800)
return resp return resp
global handshake_scripts
global projects
global projectsUpdated
try: try:
git = requests.get( git = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1", "https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
@ -477,26 +563,15 @@ def index():
<script> <script>
function startClock(timezoneOffset) { function startClock(timezoneOffset) {
function updateClock() { function updateClock() {
// Get current UTC time
const now = new Date(); const now = new Date();
// Calculate the local time based on the timezone offset
const localTime = new Date(now.getTime() + timezoneOffset * 3600 * 1000); const localTime = new Date(now.getTime() + timezoneOffset * 3600 * 1000);
// Generate timezone name dynamically
const tzName = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC`; const tzName = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC`;
// Format the local time as HH:MM:SS
const hours = String(localTime.getUTCHours()).padStart(2, '0'); const hours = String(localTime.getUTCHours()).padStart(2, '0');
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0'); const minutes = String(localTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0'); const seconds = String(localTime.getUTCSeconds()).padStart(2, '0');
// Display the formatted time with the timezone name
const timeString = `${hours}:${minutes}:${seconds} ${tzName}`; const timeString = `${hours}:${minutes}:${seconds} ${tzName}`;
document.getElementById('time').textContent = timeString; document.getElementById('time').textContent = timeString;
} }
// Update the clock immediately and then every second
updateClock(); updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
} }
@ -536,7 +611,7 @@ def index():
# region Now Pages # region Now Pages
@app.route("/now") @app.route("/now")
@app.route("/now/") @app.route("/now/")
def now(): def now_page():
global handshake_scripts global handshake_scripts
# If localhost, don't load handshake # If localhost, don't load handshake
@ -548,18 +623,7 @@ def now():
): ):
handshake_scripts = "" handshake_scripts = ""
# Get latest now page return now.render_latest_now(handshake_scripts)
files = os.listdir("templates/now")
# Remove template
files = [file for file in files if file != "template.html" and file != "old.html"]
files.sort(reverse=True)
date = files[0].strip(".html")
# Convert to date
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
return render_template(
"now/" + files[0], handshake_scripts=handshake_scripts, DATE=date
)
@app.route("/now/<path:path>") @app.route("/now/<path:path>")
@ -574,30 +638,8 @@ def now_path(path):
): ):
handshake_scripts = "" handshake_scripts = ""
date = path
date = date.strip(".html")
try: return now.render_now_page(path,handshake_scripts)
# Convert to date
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
except:
date = ""
if path.lower().replace(".html", "") == "template":
return render_template("404.html"), 404
# If file exists, load it
if os.path.isfile("templates/now/" + path):
return render_template(
"now/" + path, handshake_scripts=handshake_scripts, DATE=date
)
if os.path.isfile("templates/now/" + path + ".html"):
return render_template(
"now/" + path + ".html", handshake_scripts=handshake_scripts, DATE=date
)
return render_template("404.html"), 404
@app.route("/old") @app.route("/old")
@ -615,19 +657,15 @@ def now_old():
): ):
handshake_scripts = "" handshake_scripts = ""
now_pages = os.listdir("templates/now") now_dates = now.list_now_dates()[1:]
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
]
now_pages.sort(reverse=True)
html = '<ul class="list-group">' html = '<ul class="list-group">'
latest = " (Latest)" html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{now.get_latest_now_date(True)}</li></a>'
for page in now_pages:
link = page.strip(".html") for date in now_dates:
date = datetime.datetime.strptime(link, "%y_%m_%d") link = date
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y") date = date.strftime("%A, %B %d, %Y")
html += f'<a style="text-decoration:none;" href="/now/{link}"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{date}{latest}</li></a>' html += f'<a style="text-decoration:none;" href="/now/{link}"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{date}</li></a>'
latest = ""
html += "</ul>" html += "</ul>"
return render_template( return render_template(
@ -640,11 +678,7 @@ def now_rss():
if ":" in request.host: if ":" in request.host:
host = "http://" + request.host host = "http://" + request.host
# Generate RSS feed # Generate RSS feed
now_pages = os.listdir("templates/now") now_pages = now.list_now_page_files()
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
]
now_pages.sort(reverse=True)
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />' rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
for page in now_pages: for page in now_pages:
link = page.strip(".html") link = page.strip(".html")
@ -656,11 +690,7 @@ def now_rss():
@app.route("/now.json") @app.route("/now.json")
def now_json(): def now_json():
now_pages = os.listdir("templates/now") now_pages = now.list_now_page_files()
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
]
now_pages.sort(reverse=True)
host = "https://" + request.host host = "https://" + request.host
if ":" in request.host: if ":" in request.host:
host = "http://" + request.host host = "http://" + request.host
@ -878,7 +908,7 @@ def catch_all(path: str):
if path.lower().replace(".html", "") in restricted: if path.lower().replace(".html", "") in restricted:
return render_template("404.html"), 404 return render_template("404.html"), 404
print(path)
if path in redirects: if path in redirects:
return redirect(redirects[path], code=302) return redirect(redirects[path], code=302)
@ -904,6 +934,16 @@ def catch_all(path: str):
if filename: if filename:
return send_file(filename) return send_file(filename)
if request.headers:
# Check if curl
if "curl" in request.headers.get("User-Agent"):
return jsonify(
{
"status": 404,
"message": "Page not found",
"ip": request.remote_addr,
}
), 404
return render_template("404.html"), 404 return render_template("404.html"), 404
@ -996,6 +1036,17 @@ def podsync():
# 404 catch all # 404 catch all
@app.errorhandler(404) @app.errorhandler(404)
def not_found(e): def not_found(e):
if request.headers:
# Check if curl
if "curl" in request.headers.get("User-Agent"):
return jsonify(
{
"status": 404,
"message": "Page not found",
"ip": request.remote_addr,
}
), 404
return render_template("404.html"), 404 return render_template("404.html"), 404