fix: Cleanup blueprint names and add tests
All checks were successful
Build Docker / BuildImage (push) Successful in 2m17s

This commit is contained in:
2025-10-13 15:32:31 +11:00
parent 3d5c16f9cb
commit 7f591e2724
16 changed files with 320 additions and 201 deletions

View File

@@ -7,7 +7,7 @@ acme_bp = Blueprint('acme', __name__)
@acme_bp.route("/hnsdoh-acme", methods=["POST"])
def acme_post():
def post():
# Get the TXT record from the request
if not request.is_json or not request.json:
return json_response(request, "415 Unsupported Media Type", 415)

View File

@@ -1,13 +1,16 @@
from flask import Blueprint, request, jsonify, make_response
from flask import Blueprint, request, jsonify
import os
import datetime
import requests
from mail import sendEmail
from sol import create_transaction
from tools import getClientIP, getGitCommit, json_response
from blueprints.sol import sol_bp
api_bp = Blueprint('api', __name__)
# Register solana blueprint
api_bp.register_blueprint(sol_bp)
NC_CONFIG = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
@@ -19,7 +22,7 @@ if 'time-zone' not in NC_CONFIG:
@api_bp.route("/")
@api_bp.route("/help")
def help_get():
def help():
return jsonify({
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
"endpoints": {
@@ -39,12 +42,12 @@ def help_get():
@api_bp.route("/version")
def version_get():
def version():
return jsonify({"version": getGitCommit()})
@api_bp.route("/time")
def time_get():
def time():
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
timezone = datetime.timezone(offset=timezone_offset)
time = datetime.datetime.now(tz=timezone)
@@ -59,7 +62,7 @@ def time_get():
@api_bp.route("/timezone")
def timezone_get():
def timezone():
return jsonify({
"timezone": NC_CONFIG["time-zone"],
"ip": getClientIP(request),
@@ -67,7 +70,7 @@ def timezone_get():
})
@api_bp.route("/message")
def message_get():
def message():
return jsonify({
"message": NC_CONFIG["message"],
"ip": getClientIP(request),
@@ -76,7 +79,7 @@ def message_get():
@api_bp.route("/ip")
def ip_get():
def ip():
return jsonify({
"ip": getClientIP(request),
"status": 200
@@ -105,7 +108,7 @@ def email_post():
@api_bp.route("/project")
def project_get():
def project():
gitinfo = {
"website": None,
}
@@ -135,83 +138,3 @@ def project_get():
"ip": getClientIP(request),
"status": 200
})
# 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

View File

@@ -8,7 +8,7 @@ from tools import isCurl, getClientIP
blog_bp = Blueprint('blog', __name__)
def list_blog_page_files():
def list_page_files():
blog_pages = os.listdir("data/blog")
# Remove .md extension
blog_pages = [page.removesuffix(".md")
@@ -17,7 +17,7 @@ def list_blog_page_files():
return blog_pages
def render_blog_page(date, handshake_scripts=None):
def render_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
@@ -83,9 +83,9 @@ def fix_numbered_lists(html):
return str(soup)
def render_blog_home(handshake_scripts=None):
def render_home(handshake_scripts=None):
# Get a list of pages
blog_pages = list_blog_page_files()
blog_pages = list_page_files()
# Create a html list of pages
blog_pages = [
f"""<li class="list-group-item">
@@ -105,7 +105,7 @@ def render_blog_home(handshake_scripts=None):
@blog_bp.route("/")
def blog_index_get():
def index():
if not isCurl(request):
global handshake_scripts
@@ -117,10 +117,10 @@ def blog_index_get():
or request.host == "test.nathan.woodburn.au"
):
handshake_scripts = ""
return render_blog_home(handshake_scripts)
return render_home(handshake_scripts)
# Get a list of pages
blog_pages = list_blog_page_files()
blog_pages = list_page_files()
# Create a html list of pages
blog_pages = [
{"name":page.replace("_", " "),"url":f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages
@@ -138,7 +138,7 @@ def blog_index_get():
@blog_bp.route("/<path:path>")
def blog_path_get(path):
def path(path):
if not isCurl(request):
global handshake_scripts
# If localhost, don't load handshake
@@ -150,7 +150,7 @@ def blog_path_get(path):
):
handshake_scripts = ""
return render_blog_page(path, handshake_scripts)
return render_page(path, handshake_scripts)
# Convert md to html
if not os.path.exists(f"data/blog/{path}.md"):
@@ -170,7 +170,7 @@ def blog_path_get(path):
}), 200
@blog_bp.route("/<path:path>.md")
def blog_path_md_get(path):
def path_md(path):
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
@@ -178,4 +178,4 @@ def blog_path_md_get(path):
content = f.read()
# Return the raw markdown file
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}

View File

@@ -5,7 +5,7 @@ import os
# Create blueprint
now_bp = Blueprint('now', __name__)
def list_now_page_files():
def list_page_files():
now_pages = os.listdir("templates/now")
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
@@ -13,31 +13,31 @@ def list_now_page_files():
now_pages.sort(reverse=True)
return now_pages
def list_now_dates():
now_pages = list_now_page_files()
def list_dates():
now_pages = list_page_files()
now_dates = [page.split(".")[0] for page in now_pages]
return now_dates
def get_latest_now_date(formatted=False):
def get_latest_date(formatted=False):
if formatted:
date=list_now_dates()[0]
date=list_dates()[0]
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
return date
return list_now_dates()[0]
return list_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_latest(handshake_scripts=None):
now_page = list_dates()[0]
return render(now_page,handshake_scripts=handshake_scripts)
def render_now_page(date,handshake_scripts=None):
def render(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)
return render_latest(handshake_scripts=handshake_scripts)
# Remove .html
date = date.removesuffix(".html")
if date not in list_now_dates():
if date not in list_dates():
return render_template("404.html"), 404
@@ -46,7 +46,7 @@ def render_now_page(date,handshake_scripts=None):
return render_template(f"now/{date}.html",DATE=date_formatted,handshake_scripts=handshake_scripts)
@now_bp.route("/")
def now_index_get():
def index():
handshake_scripts = ''
# If localhost, don't load handshake
if (
@@ -59,11 +59,11 @@ def now_index_get():
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)
return render_latest(handshake_scripts)
@now_bp.route("/<path:path>")
def now_path_get(path):
def path(path):
handshake_scripts = ''
# If localhost, don't load handshake
if (
@@ -76,12 +76,12 @@ def now_path_get(path):
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)
return render(path, handshake_scripts)
@now_bp.route("/old")
@now_bp.route("/old/")
def now_old_get():
def old():
handshake_scripts = ''
# If localhost, don't load handshake
if (
@@ -94,9 +94,9 @@ def now_old_get():
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:]
now_dates = list_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>'
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
for date in now_dates:
link = date
@@ -113,12 +113,12 @@ def now_old_get():
@now_bp.route("/now.rss")
@now_bp.route("/now.xml")
@now_bp.route("/rss.xml")
def now_rss_get():
def rss():
host = "https://" + request.host
if ":" in request.host:
host = "http://" + request.host
# Generate RSS feed
now_pages = list_now_page_files()
now_pages = list_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")
@@ -130,8 +130,8 @@ def now_rss_get():
@now_bp.route("/now.json")
def now_json_get():
now_pages = list_now_page_files()
def json():
now_pages = list_page_files()
host = "https://" + request.host
if ":" in request.host:
host = "http://" + request.host

View File

@@ -5,7 +5,7 @@ import requests
podcast_bp = Blueprint('podcast', __name__)
@podcast_bp.route("/ID1")
def podcast_index_get():
def index():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1")
if req.status_code != 200:
@@ -17,7 +17,7 @@ def podcast_index_get():
@podcast_bp.route("/ID1/")
def podcast_contents_get():
def contents():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
if req.status_code != 200:
@@ -28,7 +28,7 @@ def podcast_contents_get():
@podcast_bp.route("/ID1/<path:path>")
def podcast_path_get(path):
def path(path):
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
if req.status_code != 200:
@@ -39,7 +39,7 @@ def podcast_path_get(path):
@podcast_bp.route("/ID1.xml")
def podcast_xml_get():
def xml():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
if req.status_code != 200:
@@ -50,10 +50,10 @@ def podcast_xml_get():
@podcast_bp.route("/podsync.opml")
def podcast_podsync_get():
def podsync():
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
)

125
blueprints/sol.py Normal file
View File

@@ -0,0 +1,125 @@
from flask import Blueprint, request, jsonify, make_response
from solders.pubkey import Pubkey
from solana.rpc.api import Client
from solders.system_program import TransferParams, transfer
from solders.message import MessageV0
from solders.transaction import VersionedTransaction
from solders.null_signer import NullSigner
import binascii
import base64
import os
sol_bp = Blueprint('sol', __name__)
SOLANA_HEADERS = {
"Content-Type": "application/json",
"X-Action-Version": "2.4.2",
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
}
SOLANA_ADDRESS = None
if os.path.isfile(".well-known/wallets/SOL"):
with open(".well-known/wallets/SOL") as file:
address = file.read()
SOLANA_ADDRESS = Pubkey.from_string(address.strip())
def create_transaction(sender_address: str, amount: float) -> 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.")
# Create transaction
sender = Pubkey.from_string(sender_address)
transfer_ix = transfer(
TransferParams(
from_pubkey=sender, to_pubkey=SOLANA_ADDRESS, lamports=int(
amount * 1000000000)
)
)
solana_client = Client("https://api.mainnet-beta.solana.com")
blockhashData = solana_client.get_latest_blockhash()
blockhash = blockhashData.value.blockhash
msg = MessageV0.try_compile(
payer=sender,
instructions=[transfer_ix],
address_lookup_table_accounts=[],
recent_blockhash=blockhash,
)
tx = VersionedTransaction(message=msg, keypairs=[NullSigner(sender)])
tx = bytes(tx).hex()
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)
@sol_bp.route("/donate", methods=["GET", "OPTIONS"])
def sol_donate():
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
@sol_bp.route("/donate/<amount>")
def sol_donate_amount(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
@sol_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

View File

@@ -5,12 +5,12 @@ wk_bp = Blueprint('well-known', __name__)
@wk_bp.route("/<path:path>")
def wk_index_get(path):
def index(path):
return send_from_directory(".well-known", path)
@wk_bp.route("/wallets/<path:path>")
def wk_wallet_get(path):
def wallets(path):
if path[0] == "." and 'proof' not in path:
return send_from_directory(
".well-known/wallets", path, mimetype="application/json"
@@ -29,7 +29,7 @@ def wk_wallet_get(path):
@wk_bp.route("/nostr.json")
def wk_nostr_get():
def nostr():
# Get name parameter
name = request.args.get("name")
if name:
@@ -51,7 +51,7 @@ def wk_nostr_get():
@wk_bp.route("/xrp-ledger.toml")
def wk_xrp_get():
def xrp():
# Create a response with the xrp-ledger.toml file
with open(".well-known/xrp-ledger.toml") as file:
toml = file.read()