feat: Split code into modules
All checks were successful
Build Docker / BuildImage (push) Successful in 4m7s
All checks were successful
Build Docker / BuildImage (push) Successful in 4m7s
This commit is contained in:
255
blueprints/api.py
Normal file
255
blueprints/api.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from flask import Blueprint, request, jsonify, make_response
|
||||
import os
|
||||
import datetime
|
||||
import requests
|
||||
from mail import sendEmail
|
||||
from sol import create_transaction, get_solana_address
|
||||
import json
|
||||
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
ncReq = requests.get(
|
||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
||||
)
|
||||
ncConfig = ncReq.json()
|
||||
|
||||
if 'time-zone' not in ncConfig:
|
||||
ncConfig['time-zone'] = 10
|
||||
|
||||
|
||||
def getClientIP(request):
|
||||
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(",")[0]
|
||||
else:
|
||||
ip = request.remote_addr
|
||||
return ip
|
||||
|
||||
def getGitCommit():
|
||||
# if .git exists, get the latest commit hash
|
||||
if os.path.isdir(".git"):
|
||||
git_dir = ".git"
|
||||
head_ref = ""
|
||||
with open(os.path.join(git_dir, "HEAD")) as file:
|
||||
head_ref = file.read().strip()
|
||||
if head_ref.startswith("ref: "):
|
||||
head_ref = head_ref[5:]
|
||||
with open(os.path.join(git_dir, head_ref)) as file:
|
||||
return file.read().strip()
|
||||
else:
|
||||
return head_ref
|
||||
|
||||
# Check if env SOURCE_COMMIT is set
|
||||
if "SOURCE_COMMIT" in os.environ:
|
||||
return os.environ["SOURCE_COMMIT"]
|
||||
|
||||
return "failed to get version"
|
||||
|
||||
@api_bp.route("/")
|
||||
@api_bp.route("/help")
|
||||
def api_help_get():
|
||||
return jsonify({
|
||||
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
||||
"endpoints": {
|
||||
"/time": "Get the current time",
|
||||
"/timezone": "Get the current timezone",
|
||||
"/message": "Get the message from the config",
|
||||
"/ip": "Get your IP address",
|
||||
"/project": "Get the current project from git",
|
||||
"/version": "Get the current version of the website",
|
||||
"/help": "Get this help message"
|
||||
},
|
||||
"version": getGitCommit()
|
||||
})
|
||||
|
||||
@api_bp.route("/version")
|
||||
def api_version_get():
|
||||
return jsonify({"version": getGitCommit()})
|
||||
|
||||
@api_bp.route("/time")
|
||||
def api_time_get():
|
||||
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()
|
||||
})
|
||||
|
||||
@api_bp.route("/timezone")
|
||||
def api_timezone_get():
|
||||
return jsonify({"timezone": ncConfig["time-zone"]})
|
||||
|
||||
@api_bp.route("/timezone", methods=["POST"])
|
||||
def api_timezone_post():
|
||||
# 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"]})
|
||||
|
||||
@api_bp.route("/message")
|
||||
def api_message_get():
|
||||
return jsonify({"message": ncConfig["message"]})
|
||||
|
||||
|
||||
@api_bp.route("/ip")
|
||||
def api_ip_get():
|
||||
return jsonify({"ip": getClientIP(request)})
|
||||
|
||||
|
||||
@api_bp.route("/email", methods=["POST"])
|
||||
def api_email_post():
|
||||
# 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 not data:
|
||||
return jsonify({
|
||||
"status": 400,
|
||||
"error": "Bad request JSON Data missing"
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@api_bp.route("/project")
|
||||
def api_project_get():
|
||||
try:
|
||||
git = requests.get(
|
||||
"https://git.woodburn.au/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||
headers={"Authorization": os.getenv("git_token")},
|
||||
)
|
||||
git = git.json()
|
||||
git = git[0]
|
||||
repo_name = git["repo"]["name"]
|
||||
repo_name = repo_name.lower()
|
||||
repo_description = git["repo"]["description"]
|
||||
except Exception as e:
|
||||
repo_name = "nathanwoodburn.github.io"
|
||||
repo_description = "Personal website"
|
||||
git = {
|
||||
"repo": {
|
||||
"html_url": "https://nathan.woodburn.au",
|
||||
"name": "nathanwoodburn.github.io",
|
||||
"description": "Personal website",
|
||||
}
|
||||
}
|
||||
print(f"Error getting git data: {e}")
|
||||
|
||||
return jsonify({
|
||||
"repo_name": repo_name,
|
||||
"repo_description": repo_description,
|
||||
"git": git,
|
||||
})
|
||||
|
||||
# region Solana Links
|
||||
SOLANA_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Action-Version": "2.4.2",
|
||||
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@api_bp.route("/donate", methods=["GET", "OPTIONS"])
|
||||
def sol_donate_get():
|
||||
data = {
|
||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||
"label": "Donate to Nathan.Woodburn/",
|
||||
"title": "Donate to Nathan.Woodburn/",
|
||||
"description": "Student, developer, and crypto enthusiast",
|
||||
"links": {
|
||||
"actions": [
|
||||
{"label": "0.01 SOL", "href": "/api/v1/donate/0.01"},
|
||||
{"label": "0.1 SOL", "href": "/api/v1/donate/0.1"},
|
||||
{"label": "1 SOL", "href": "/api/v1/donate/1"},
|
||||
{
|
||||
"href": "/api/v1/donate/{amount}",
|
||||
"label": "Donate",
|
||||
"parameters": [
|
||||
{"name": "amount", "label": "Enter a custom SOL amount"}
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
response = make_response(jsonify(data), 200, SOLANA_HEADERS)
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = (
|
||||
"Content-Type,Authorization,Content-Encoding,Accept-Encoding,X-Action-Version,X-Blockchain-Ids"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@api_bp.route("/donate/<amount>")
|
||||
def sol_donate_amount_get(amount):
|
||||
data = {
|
||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||
"title": "Donate to Nathan.Woodburn/",
|
||||
"description": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||
}
|
||||
return jsonify(data), 200, SOLANA_HEADERS
|
||||
|
||||
|
||||
@api_bp.route("/donate/<amount>", methods=["POST"])
|
||||
def sol_donate_post(amount):
|
||||
|
||||
if not request.json:
|
||||
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
|
||||
|
||||
|
||||
if "account" not in request.json:
|
||||
return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS
|
||||
|
||||
sender = request.json["account"]
|
||||
|
||||
# Make sure amount is a number
|
||||
try:
|
||||
amount = float(amount)
|
||||
except ValueError:
|
||||
amount = 1 # Default to 1 SOL if invalid
|
||||
|
||||
if amount < 0.0001:
|
||||
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
|
||||
|
||||
transaction = create_transaction(sender, amount)
|
||||
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import os
|
||||
from flask import render_template
|
||||
from flask import Blueprint, render_template, request
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
|
||||
blog_bp = Blueprint('blog', __name__)
|
||||
|
||||
|
||||
def list_blog_page_files():
|
||||
blog_pages = os.listdir("data/blog")
|
||||
# Remove .md extension
|
||||
blog_pages = [page.removesuffix(".md") for page in blog_pages if page.endswith(".md")]
|
||||
blog_pages = [page.removesuffix(".md")
|
||||
for page in blog_pages if page.endswith(".md")]
|
||||
|
||||
return blog_pages
|
||||
|
||||
|
||||
def render_blog_page(date,handshake_scripts=None):
|
||||
def render_blog_page(date, handshake_scripts=None):
|
||||
# Convert md to html
|
||||
if not os.path.exists(f"data/blog/{date}.md"):
|
||||
return render_template("404.html"), 404
|
||||
@@ -23,12 +26,12 @@ def render_blog_page(date,handshake_scripts=None):
|
||||
# Get the title from the file name
|
||||
title = date.removesuffix(".md").replace("_", " ")
|
||||
# Convert the md to html
|
||||
content = markdown.markdown(content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
|
||||
content = markdown.markdown(
|
||||
content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
|
||||
# Add target="_blank" to all links
|
||||
content = content.replace('<a href="', '<a target="_blank" href="')
|
||||
|
||||
content = content.replace("<h4", "<h4 style='margin-bottom:0px;'")
|
||||
|
||||
content = fix_numbered_lists(content)
|
||||
|
||||
return render_template(
|
||||
@@ -45,7 +48,7 @@ def fix_numbered_lists(html):
|
||||
# Find the <p> tag containing numbered steps
|
||||
paragraphs = soup.find_all('p')
|
||||
for p in paragraphs:
|
||||
content = p.decode_contents() # type: ignore
|
||||
content = p.decode_contents() # type: ignore
|
||||
|
||||
# Check for likely numbered step structure
|
||||
if re.search(r'1\.\s', content):
|
||||
@@ -62,7 +65,8 @@ def fix_numbered_lists(html):
|
||||
for i in range(0, len(steps), 2):
|
||||
if i+1 < len(steps):
|
||||
step_html = steps[i+1].strip()
|
||||
ol_items.append(f"<li style='list-style: auto;'>{step_html}</li>")
|
||||
ol_items.append(
|
||||
f"<li style='list-style: auto;'>{step_html}</li>")
|
||||
|
||||
# Build the final list HTML
|
||||
ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>"
|
||||
@@ -85,7 +89,7 @@ def render_blog_home(handshake_scripts=None):
|
||||
blog_pages = [
|
||||
f"""<li class="list-group-item">
|
||||
|
||||
<p style="margin-bottom: 0px;"><a href='/blog/{page}'>{page.replace("_"," ")}</a></p>
|
||||
<p style="margin-bottom: 0px;"><a href='/blog/{page}'>{page.replace("_", " ")}</a></p>
|
||||
</li>"""
|
||||
for page in blog_pages
|
||||
]
|
||||
@@ -97,3 +101,34 @@ def render_blog_home(handshake_scripts=None):
|
||||
blogs=blog_pages,
|
||||
handshake_scripts=handshake_scripts,
|
||||
)
|
||||
|
||||
|
||||
@blog_bp.route("/")
|
||||
def blog_index_get():
|
||||
global handshake_scripts
|
||||
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_blog_home(handshake_scripts)
|
||||
|
||||
|
||||
@blog_bp.route("/<path:path>")
|
||||
def blog_path_get(path):
|
||||
global handshake_scripts
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_blog_page(path, handshake_scripts)
|
||||
140
blueprints/now.py
Normal file
140
blueprints/now.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from flask import Blueprint, render_template, make_response, request, jsonify
|
||||
import datetime
|
||||
import os
|
||||
|
||||
# Create blueprint
|
||||
now_bp = Blueprint('now', __name__)
|
||||
|
||||
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.datetime.strptime(date, "%y_%m_%d")
|
||||
date = date.strftime("%A, %B %d, %Y")
|
||||
return date
|
||||
return list_now_dates()[0]
|
||||
|
||||
def render_latest_now(handshake_scripts=None):
|
||||
now_page = list_now_dates()[0]
|
||||
return render_now_page(now_page,handshake_scripts=handshake_scripts)
|
||||
|
||||
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)
|
||||
# Remove .html
|
||||
date = date.removesuffix(".html")
|
||||
|
||||
if date not in list_now_dates():
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
date_formatted = datetime.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)
|
||||
|
||||
@now_bp.route("/")
|
||||
def now_index_get():
|
||||
handshake_scripts = ''
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
else:
|
||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
||||
|
||||
return render_latest_now(handshake_scripts)
|
||||
|
||||
|
||||
@now_bp.route("/<path:path>")
|
||||
def now_path_get(path):
|
||||
handshake_scripts = ''
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
else:
|
||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
||||
|
||||
return render_now_page(path, handshake_scripts)
|
||||
|
||||
|
||||
@now_bp.route("/old")
|
||||
@now_bp.route("/old/")
|
||||
def now_old_get():
|
||||
handshake_scripts = ''
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
else:
|
||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
||||
|
||||
now_dates = list_now_dates()[1:]
|
||||
html = '<ul class="list-group">'
|
||||
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_now_date(True)}</li></a>'
|
||||
|
||||
for date in now_dates:
|
||||
link = date
|
||||
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||
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}</li></a>'
|
||||
|
||||
html += "</ul>"
|
||||
return render_template(
|
||||
"now/old.html", handshake_scripts=handshake_scripts, now_pages=html
|
||||
)
|
||||
|
||||
|
||||
@now_bp.route("/now.rss")
|
||||
@now_bp.route("/now.xml")
|
||||
@now_bp.route("/rss.xml")
|
||||
def now_rss_get():
|
||||
host = "https://" + request.host
|
||||
if ":" in request.host:
|
||||
host = "http://" + request.host
|
||||
# Generate RSS feed
|
||||
now_pages = list_now_page_files()
|
||||
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:
|
||||
link = page.strip(".html")
|
||||
date = datetime.datetime.strptime(link, "%y_%m_%d")
|
||||
date = date.strftime("%A, %B %d, %Y")
|
||||
rss += f'<item><title>What\'s Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>'
|
||||
rss += "</channel></rss>"
|
||||
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
||||
|
||||
|
||||
@now_bp.route("/now.json")
|
||||
def now_json_get():
|
||||
now_pages = list_now_page_files()
|
||||
host = "https://" + request.host
|
||||
if ":" in request.host:
|
||||
host = "http://" + request.host
|
||||
now_pages = [{"url": host+"/now/"+page.strip(".html"), "date": datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
|
||||
"%A, %B %d, %Y"), "title": "What's Happening "+datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime("%A, %B %d, %Y")} for page in now_pages]
|
||||
return jsonify(now_pages)
|
||||
62
blueprints/wellknown.py
Normal file
62
blueprints/wellknown.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from flask import Blueprint, render_template, make_response, request, jsonify, send_from_directory, redirect
|
||||
import os
|
||||
|
||||
wk_bp = Blueprint('well-known', __name__)
|
||||
|
||||
|
||||
@wk_bp.route("/<path:path>")
|
||||
def wk_index_get(path):
|
||||
return send_from_directory(".well-known", path)
|
||||
|
||||
|
||||
@wk_bp.route("/wallets/<path:path>")
|
||||
def wk_wallet_get(path):
|
||||
if path[0] == "." and 'proof' not in path:
|
||||
return send_from_directory(
|
||||
".well-known/wallets", path, mimetype="application/json"
|
||||
)
|
||||
elif os.path.isfile(".well-known/wallets/" + path):
|
||||
address = ""
|
||||
with open(".well-known/wallets/" + path) as file:
|
||||
address = file.read()
|
||||
address = address.strip()
|
||||
return make_response(address, 200, {"Content-Type": "text/plain"})
|
||||
|
||||
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
||||
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
||||
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
@wk_bp.route("/nostr.json")
|
||||
def wk_nostr_get():
|
||||
# Get name parameter
|
||||
name = request.args.get("name")
|
||||
if name:
|
||||
return jsonify(
|
||||
{
|
||||
"names": {
|
||||
name: "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82"
|
||||
}
|
||||
}
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
"names": {
|
||||
"nathan": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
|
||||
"_": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@wk_bp.route("/xrp-ledger.toml")
|
||||
def wk_xrp_get():
|
||||
# Create a response with the xrp-ledger.toml file
|
||||
with open(".well-known/xrp-ledger.toml") as file:
|
||||
toml = file.read()
|
||||
|
||||
response = make_response(toml, 200, {"Content-Type": "application/toml"})
|
||||
# Set cors headers
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
43
now.py
43
now.py
@@ -1,43 +0,0 @@
|
||||
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]
|
||||
|
||||
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)
|
||||
# Remove .html
|
||||
date = date.removesuffix(".html")
|
||||
|
||||
if date not 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]
|
||||
return render_now_page(now_page,handshake_scripts=handshake_scripts)
|
||||
483
server.py
483
server.py
@@ -20,20 +20,21 @@ from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
|
||||
from ansi2html import Ansi2HTMLConverter
|
||||
from functools import cache
|
||||
from PIL import Image
|
||||
from mail import sendEmail
|
||||
from now import (
|
||||
list_now_dates,
|
||||
get_latest_now_date,
|
||||
list_now_page_files,
|
||||
render_latest_now,
|
||||
render_now_page,
|
||||
)
|
||||
from blog import render_blog_home, render_blog_page
|
||||
from blueprints.now import now_bp
|
||||
from blueprints.blog import blog_bp
|
||||
from blueprints.wellknown import wk_bp
|
||||
from blueprints.api import api_bp, getGitCommit, getClientIP
|
||||
from sol import create_transaction
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Register the now blueprint with the URL prefix
|
||||
app.register_blueprint(now_bp, url_prefix='/now')
|
||||
app.register_blueprint(blog_bp, url_prefix='/blog')
|
||||
app.register_blueprint(wk_bp, url_prefix='/.well-known')
|
||||
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# Rate limiting for hosting enquiries
|
||||
@@ -93,35 +94,6 @@ def getFilePath(name, path):
|
||||
return os.path.join(root, name)
|
||||
|
||||
|
||||
def getClientIP(request):
|
||||
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(",")[0]
|
||||
else:
|
||||
ip = request.remote_addr
|
||||
return ip
|
||||
|
||||
|
||||
def getGitCommit():
|
||||
# if .git exists, get the latest commit hash
|
||||
if os.path.isdir(".git"):
|
||||
git_dir = ".git"
|
||||
head_ref = ""
|
||||
with open(os.path.join(git_dir, "HEAD")) as file:
|
||||
head_ref = file.read().strip()
|
||||
if head_ref.startswith("ref: "):
|
||||
head_ref = head_ref[5:]
|
||||
with open(os.path.join(git_dir, head_ref)) as file:
|
||||
return file.read().strip()
|
||||
else:
|
||||
return head_ref
|
||||
|
||||
# Check if env SOURCE_COMMIT is set
|
||||
if "SOURCE_COMMIT" in os.environ:
|
||||
return os.environ["SOURCE_COMMIT"]
|
||||
|
||||
return "failed to get version"
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
@@ -190,68 +162,6 @@ def javascript_get(name):
|
||||
return render_template("404.html"), 404
|
||||
return send_from_directory("templates/assets/js", request.path.split("/")[-1])
|
||||
|
||||
# endregion
|
||||
|
||||
# region Well-known routes
|
||||
|
||||
|
||||
@app.route("/.well-known/<path:path>")
|
||||
def wk_index_get(path):
|
||||
return send_from_directory(".well-known", path)
|
||||
|
||||
|
||||
@app.route("/.well-known/wallets/<path:path>")
|
||||
def wk_wallet_get(path):
|
||||
if path[0] == "." and 'proof' not in path:
|
||||
return send_from_directory(
|
||||
".well-known/wallets", path, mimetype="application/json"
|
||||
)
|
||||
elif os.path.isfile(".well-known/wallets/" + path):
|
||||
address = ""
|
||||
with open(".well-known/wallets/" + path) as file:
|
||||
address = file.read()
|
||||
address = address.strip()
|
||||
return make_response(address, 200, {"Content-Type": "text/plain"})
|
||||
|
||||
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
||||
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
||||
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
@app.route("/.well-known/nostr.json")
|
||||
def wk_nostr_get():
|
||||
# Get name parameter
|
||||
name = request.args.get("name")
|
||||
if name:
|
||||
return jsonify(
|
||||
{
|
||||
"names": {
|
||||
name: "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82"
|
||||
}
|
||||
}
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
"names": {
|
||||
"nathan": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
|
||||
"_": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/.well-known/xrp-ledger.toml")
|
||||
def wk_xrp_get():
|
||||
# Create a response with the xrp-ledger.toml file
|
||||
with open(".well-known/xrp-ledger.toml") as file:
|
||||
toml = file.read()
|
||||
|
||||
response = make_response(toml, 200, {"Content-Type": "application/toml"})
|
||||
# Set cors headers
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
|
||||
# endregion
|
||||
# region PWA routes
|
||||
|
||||
@@ -279,236 +189,6 @@ def serviceWorker_get():
|
||||
# endregion
|
||||
|
||||
|
||||
# region Solana Links
|
||||
SOLANA_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Action-Version": "2.4.2",
|
||||
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
||||
}
|
||||
|
||||
|
||||
@app.route("/actions.json")
|
||||
def sol_actions_get():
|
||||
return jsonify(
|
||||
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/donate**"}]}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/donate", methods=["GET", "OPTIONS"])
|
||||
def sol_donate_get():
|
||||
data = {
|
||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||
"label": "Donate to Nathan.Woodburn/",
|
||||
"title": "Donate to Nathan.Woodburn/",
|
||||
"description": "Student, developer, and crypto enthusiast",
|
||||
"links": {
|
||||
"actions": [
|
||||
{"label": "0.01 SOL", "href": "/api/donate/0.01"},
|
||||
{"label": "0.1 SOL", "href": "/api/donate/0.1"},
|
||||
{"label": "1 SOL", "href": "/api/donate/1"},
|
||||
{
|
||||
"href": "/api/donate/{amount}",
|
||||
"label": "Donate",
|
||||
"parameters": [
|
||||
{"name": "amount", "label": "Enter a custom SOL amount"}
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
response = make_response(jsonify(data), 200, SOLANA_HEADERS)
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = (
|
||||
"Content-Type,Authorization,Content-Encoding,Accept-Encoding,X-Action-Version,X-Blockchain-Ids"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/api/donate/<amount>")
|
||||
def sol_donate_amount_get(amount):
|
||||
data = {
|
||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
||||
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||
"title": "Donate to Nathan.Woodburn/",
|
||||
"description": f"Donate {amount} SOL to Nathan.Woodburn/",
|
||||
}
|
||||
return jsonify(data), 200, SOLANA_HEADERS
|
||||
|
||||
|
||||
@app.route("/api/donate/<amount>", methods=["POST"])
|
||||
def sol_donate_post(amount):
|
||||
if not request.json:
|
||||
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
|
||||
|
||||
if "account" not in request.json:
|
||||
return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS
|
||||
|
||||
sender = request.json["account"]
|
||||
|
||||
# Make sure amount is a number
|
||||
try:
|
||||
amount = float(amount)
|
||||
except ValueError:
|
||||
return jsonify({"message": "Error: Invalid amount"}), 400, SOLANA_HEADERS
|
||||
|
||||
if amount < 0.0001:
|
||||
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
|
||||
|
||||
transaction = create_transaction(sender, amount)
|
||||
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
|
||||
|
||||
# endregion
|
||||
|
||||
# region API routes
|
||||
|
||||
|
||||
@app.route("/api/version")
|
||||
@app.route("/api/v1/version")
|
||||
def api_version_get():
|
||||
return jsonify({"version": getGitCommit()})
|
||||
|
||||
@app.route("/api")
|
||||
@app.route("/api/")
|
||||
@app.route("/api/v1")
|
||||
@app.route("/api/v1/")
|
||||
@app.route("/api/help")
|
||||
def api_help_get():
|
||||
return jsonify({
|
||||
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
||||
"endpoints": {
|
||||
"/api/v1/time": "Get the current time",
|
||||
"/api/v1/timezone": "Get the current timezone",
|
||||
"/api/v1/message": "Get the message from the config",
|
||||
"/api/v1/ip": "Get your IP address",
|
||||
"/api/v1/project": "Get the current project from git",
|
||||
"/api/v1/version": "Get the current version of the website",
|
||||
"/api/v1/help": "Get this help message"
|
||||
},
|
||||
"version": getGitCommit()
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/time")
|
||||
@app.route("/api/v1/time")
|
||||
def api_time_get():
|
||||
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")
|
||||
@app.route("/api/v1/timezone")
|
||||
def api_timezone_get():
|
||||
return jsonify({"timezone": ncConfig["time-zone"]})
|
||||
|
||||
|
||||
@app.route("/api/timezone", methods=["POST"])
|
||||
@app.route("/api/v1/timezone", methods=["POST"])
|
||||
def api_timezone_post():
|
||||
# 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")
|
||||
@app.route("/api/v1/message")
|
||||
def api_message_get():
|
||||
return jsonify({"message": ncConfig["message"]})
|
||||
|
||||
|
||||
@app.route("/api/ip")
|
||||
@app.route("/api/v1/ip")
|
||||
def api_ip_get():
|
||||
return jsonify({"ip": getClientIP(request)})
|
||||
|
||||
|
||||
@app.route("/api/email", methods=["POST"])
|
||||
@app.route("/api/v1/email", methods=["POST"])
|
||||
def api_email_post():
|
||||
# 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 not data:
|
||||
return jsonify({
|
||||
"status": 400,
|
||||
"error": "Bad request JSON Data missing"
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@app.route("/api/v1/project")
|
||||
def api_project_get():
|
||||
try:
|
||||
git = requests.get(
|
||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||
headers={"Authorization": os.getenv("git_token")},
|
||||
)
|
||||
git = git.json()
|
||||
git = git[0]
|
||||
repo_name = git["repo"]["name"]
|
||||
repo_name = repo_name.lower()
|
||||
repo_description = git["repo"]["description"]
|
||||
except Exception as e:
|
||||
repo_name = "nathanwoodburn.github.io"
|
||||
repo_description = "Personal website"
|
||||
git = {
|
||||
"repo": {
|
||||
"html_url": "https://nathan.woodburn.au",
|
||||
"name": "nathanwoodburn.github.io",
|
||||
"description": "Personal website",
|
||||
}
|
||||
}
|
||||
print(f"Error getting git data: {e}")
|
||||
|
||||
return jsonify({
|
||||
"repo_name": repo_name,
|
||||
"repo_description": repo_description,
|
||||
"git": git,
|
||||
})
|
||||
|
||||
# endregion
|
||||
|
||||
# region Misc routes
|
||||
|
||||
|
||||
@@ -530,6 +210,17 @@ def links_get():
|
||||
def generator_get():
|
||||
return render_template(request.path.split("/")[-2] + ".html")
|
||||
|
||||
@app.route("/api/<path:function>")
|
||||
def api_old_get(function):
|
||||
return redirect(f"/api/v1/{function}", code=301)
|
||||
|
||||
@app.route("/actions.json")
|
||||
def sol_actions_get():
|
||||
return jsonify(
|
||||
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Main routes
|
||||
@@ -729,138 +420,6 @@ def index_get():
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
# region Now Pages
|
||||
@app.route("/now")
|
||||
@app.route("/now/")
|
||||
def now_index_get():
|
||||
global handshake_scripts
|
||||
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_latest_now(handshake_scripts)
|
||||
|
||||
|
||||
@app.route("/now/<path:path>")
|
||||
def now_path_get(path):
|
||||
global handshake_scripts
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_now_page(path, handshake_scripts)
|
||||
|
||||
|
||||
@app.route("/old")
|
||||
@app.route("/old/")
|
||||
@app.route("/now/old")
|
||||
@app.route("/now/old/")
|
||||
def now_old_get():
|
||||
global handshake_scripts
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
now_dates = list_now_dates()[1:]
|
||||
html = '<ul class="list-group">'
|
||||
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_now_date(True)}</li></a>'
|
||||
|
||||
for date in now_dates:
|
||||
link = date
|
||||
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||
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}</li></a>'
|
||||
|
||||
html += "</ul>"
|
||||
return render_template(
|
||||
"now/old.html", handshake_scripts=handshake_scripts, now_pages=html
|
||||
)
|
||||
|
||||
|
||||
@app.route("/now.rss")
|
||||
@app.route("/now.xml")
|
||||
@app.route("/rss.xml")
|
||||
def now_rss_get():
|
||||
host = "https://" + request.host
|
||||
if ":" in request.host:
|
||||
host = "http://" + request.host
|
||||
# Generate RSS feed
|
||||
now_pages = list_now_page_files()
|
||||
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:
|
||||
link = page.strip(".html")
|
||||
date = datetime.datetime.strptime(link, "%y_%m_%d")
|
||||
date = date.strftime("%A, %B %d, %Y")
|
||||
rss += f'<item><title>What\'s Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>'
|
||||
rss += "</channel></rss>"
|
||||
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
||||
|
||||
|
||||
@app.route("/now.json")
|
||||
def now_json_get():
|
||||
now_pages = list_now_page_files()
|
||||
host = "https://" + request.host
|
||||
if ":" in request.host:
|
||||
host = "http://" + request.host
|
||||
now_pages = [{"url": host+"/now/"+page.strip(".html"), "date": datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
|
||||
"%A, %B %d, %Y"), "title": "What's Happening "+datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime("%A, %B %d, %Y")} for page in now_pages]
|
||||
return jsonify(now_pages)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Blog Pages
|
||||
|
||||
|
||||
@app.route("/blog")
|
||||
@app.route("/blog/")
|
||||
def blog_index_get():
|
||||
global handshake_scripts
|
||||
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_blog_home(handshake_scripts)
|
||||
|
||||
|
||||
@app.route("/blog/<path:path>")
|
||||
def blog_path_get(path):
|
||||
global handshake_scripts
|
||||
# If localhost, don't load handshake
|
||||
if (
|
||||
request.host == "localhost:5000"
|
||||
or request.host == "127.0.0.1:5000"
|
||||
or os.getenv("dev") == "true"
|
||||
or request.host == "test.nathan.woodburn.au"
|
||||
):
|
||||
handshake_scripts = ""
|
||||
|
||||
return render_blog_page(path, handshake_scripts)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Donate
|
||||
|
||||
|
||||
|
||||
5
sol.py
5
sol.py
@@ -40,3 +40,8 @@ def create_transaction(sender_address: str, amount: float) -> str:
|
||||
raw_bytes = binascii.unhexlify(tx)
|
||||
base64_string = base64.b64encode(raw_bytes).decode("utf-8")
|
||||
return base64_string
|
||||
|
||||
def get_solana_address() -> str:
|
||||
if SOLANA_ADDRESS is None:
|
||||
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
|
||||
return str(SOLANA_ADDRESS)
|
||||
Reference in New Issue
Block a user