13 Commits

Author SHA1 Message Date
7b2b3659bb fix: Remove strict slashes from index routes
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-30 19:50:03 +11:00
872373dffd feat: Remove strict slashes for now pages 2025-10-30 19:44:01 +11:00
8d832372cd feat: Add curl support for now pages
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-30 19:36:43 +11:00
03dae87272 feat: Refactor blueprints to make them easier to import
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 18:22:21 +11:00
4c654fcb78 feat: Add HTTPie to CLI agents
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-10-30 17:26:06 +11:00
c9542e4af7 fix: Remove X- headers
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-10-30 17:18:50 +11:00
e184375897 feat: Add Posting to CLI tools
All checks were successful
Build Docker / BuildImage (push) Successful in 59s
2025-10-30 17:12:43 +11:00
844f1b52e2 feat: Update isCurl to isCLI to allow more CLI agents 2025-10-30 17:07:20 +11:00
19c51c3665 feat: Add header route for troubleshooting 2025-10-30 17:03:17 +11:00
85ebd460ed feat: Add progress bar to spotify widget
All checks were successful
Build Docker / BuildImage (push) Successful in 1m57s
2025-10-30 11:45:04 +11:00
50879b4f0e feat: Add Vesktop to desktop applications
All checks were successful
Build Docker / BuildImage (push) Successful in 2m0s
2025-10-29 14:59:08 +11:00
6c09923281 feat: Add curl error page and new tests
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-28 15:04:21 +11:00
332c408b89 fix: Animation for spotify toggle
All checks were successful
Build Docker / BuildImage (push) Successful in 2m8s
2025-10-28 14:46:09 +11:00
18 changed files with 336 additions and 131 deletions

View File

@@ -3,10 +3,10 @@ import os
from cloudflare import Cloudflare from cloudflare import Cloudflare
from tools import json_response from tools import json_response
acme_bp = Blueprint('acme', __name__) app = Blueprint('acme', __name__)
@acme_bp.route("/hnsdoh-acme", methods=["POST"]) @app.route("/hnsdoh-acme", methods=["POST"])
def post(): def post():
# Get the TXT record from the request # Get the TXT record from the request
if not request.is_json or not request.json: if not request.is_json or not request.json:

View File

@@ -5,7 +5,7 @@ import requests
import re import re
from mail import sendEmail from mail import sendEmail
from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data
from blueprints.sol import sol_bp from blueprints import sol
from dateutil import parser as date_parser from dateutil import parser as date_parser
from blueprints.spotify import get_spotify_track from blueprints.spotify import get_spotify_track
@@ -17,9 +17,9 @@ HTTP_NOT_FOUND = 404
HTTP_UNSUPPORTED_MEDIA = 415 HTTP_UNSUPPORTED_MEDIA = 415
HTTP_SERVER_ERROR = 500 HTTP_SERVER_ERROR = 500
api_bp = Blueprint('api', __name__) app = Blueprint('api', __name__, url_prefix='/api/v1')
# Register solana blueprint # Register solana blueprint
api_bp.register_blueprint(sol_bp) app.register_blueprint(sol.app)
# Load configuration # Load configuration
NC_CONFIG = requests.get( NC_CONFIG = requests.get(
@@ -30,8 +30,8 @@ if 'time-zone' not in NC_CONFIG:
NC_CONFIG['time-zone'] = 10 NC_CONFIG['time-zone'] = 10
@api_bp.route("/") @app.route("/", strict_slashes=False)
@api_bp.route("/help") @app.route("/help")
def help(): def help():
"""Provide API documentation and help.""" """Provide API documentation and help."""
return jsonify({ return jsonify({
@@ -40,7 +40,6 @@ def help():
"/time": "Get the current time", "/time": "Get the current time",
"/timezone": "Get the current timezone", "/timezone": "Get the current timezone",
"/message": "Get the message from the config", "/message": "Get the message from the config",
"/ip": "Get your IP address",
"/project": "Get the current project from git", "/project": "Get the current project from git",
"/version": "Get the current version of the website", "/version": "Get the current version of the website",
"/page_date?url=URL&verbose=BOOL": "Get the last modified date of a webpage (verbose is optional, default false)", "/page_date?url=URL&verbose=BOOL": "Get the last modified date of a webpage (verbose is optional, default false)",
@@ -48,6 +47,8 @@ def help():
"/playing": "Get the currently playing Spotify track", "/playing": "Get the currently playing Spotify track",
"/status": "Just check if the site is up", "/status": "Just check if the site is up",
"/ping": "Just check if the site is up", "/ping": "Just check if the site is up",
"/ip": "Get your IP address",
"/headers": "Get your request headers",
"/help": "Get this help message" "/help": "Get this help message"
}, },
"base_url": "/api/v1", "base_url": "/api/v1",
@@ -56,18 +57,18 @@ def help():
"status": HTTP_OK "status": HTTP_OK
}) })
@api_bp.route("/status") @app.route("/status")
@api_bp.route("/ping") @app.route("/ping")
def status(): def status():
return json_response(request, "200 OK", HTTP_OK) return json_response(request, "200 OK", HTTP_OK)
@api_bp.route("/version") @app.route("/version")
def version(): def version():
"""Get the current version of the website.""" """Get the current version of the website."""
return jsonify({"version": getGitCommit()}) return jsonify({"version": getGitCommit()})
@api_bp.route("/time") @app.route("/time")
def time(): def time():
"""Get the current time in the configured timezone.""" """Get the current time in the configured timezone."""
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"]) timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
@@ -83,7 +84,7 @@ def time():
}) })
@api_bp.route("/timezone") @app.route("/timezone")
def timezone(): def timezone():
"""Get the current timezone setting.""" """Get the current timezone setting."""
return jsonify({ return jsonify({
@@ -93,7 +94,7 @@ def timezone():
}) })
@api_bp.route("/message") @app.route("/message")
def message(): def message():
"""Get the message from the configuration.""" """Get the message from the configuration."""
return jsonify({ return jsonify({
@@ -103,7 +104,7 @@ def message():
}) })
@api_bp.route("/ip") @app.route("/ip")
def ip(): def ip():
"""Get the client's IP address.""" """Get the client's IP address."""
return jsonify({ return jsonify({
@@ -112,7 +113,7 @@ def ip():
}) })
@api_bp.route("/email", methods=["POST"]) @app.route("/email", methods=["POST"])
def email_post(): def email_post():
"""Send an email via the API (requires API key).""" """Send an email via the API (requires API key)."""
# Verify json # Verify json
@@ -134,7 +135,7 @@ def email_post():
return sendEmail(data) return sendEmail(data)
@api_bp.route("/project") @app.route("/project")
def project(): def project():
"""Get information about the current git project.""" """Get information about the current git project."""
gitinfo = { gitinfo = {
@@ -167,7 +168,7 @@ def project():
"status": HTTP_OK "status": HTTP_OK
}) })
@api_bp.route("/tools") @app.route("/tools")
def tools(): def tools():
"""Get a list of tools used by Nathan Woodburn.""" """Get a list of tools used by Nathan Woodburn."""
try: try:
@@ -183,7 +184,7 @@ def tools():
return json_response(request, {"tools": tools}, HTTP_OK) return json_response(request, {"tools": tools}, HTTP_OK)
@api_bp.route("/playing") @app.route("/playing")
def playing(): def playing():
"""Get the currently playing Spotify track.""" """Get the currently playing Spotify track."""
track_info = get_spotify_track() track_info = get_spotify_track()
@@ -191,7 +192,31 @@ def playing():
return json_response(request, track_info, HTTP_OK) return json_response(request, track_info, HTTP_OK)
return json_response(request, {"spotify": track_info}, HTTP_OK) return json_response(request, {"spotify": track_info}, HTTP_OK)
@api_bp.route("/page_date")
@app.route("/headers")
def headers():
"""Get the request headers."""
headers = dict(request.headers)
# For each header, convert list-like headers to lists
toremove = []
for key, _ in headers.items():
# If header is like X- something
if key.startswith("X-"):
# Remove from headers
toremove.append(key)
for key in toremove:
headers.pop(key)
return jsonify({
"headers": headers,
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/page_date")
def page_date(): def page_date():
url = request.args.get("url") url = request.args.get("url")
if not url: if not url:

View File

@@ -3,9 +3,9 @@ from flask import Blueprint, render_template, request, jsonify
import markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re import re
from tools import isCurl, getClientIP, getHandshakeScript from tools import isCLI, getClientIP, getHandshakeScript
blog_bp = Blueprint('blog', __name__) app = Blueprint('blog', __name__, url_prefix='/blog')
def list_page_files(): def list_page_files():
@@ -108,9 +108,9 @@ def render_home(handshake_scripts: str | None = None):
) )
@blog_bp.route("/") @app.route("/", strict_slashes=False)
def index(): def index():
if not isCurl(request): if not isCLI(request):
return render_home(handshake_scripts=getHandshakeScript(request.host)) return render_home(handshake_scripts=getHandshakeScript(request.host))
# Get a list of pages # Get a list of pages
@@ -129,9 +129,9 @@ def index():
}), 200 }), 200
@blog_bp.route("/<path:path>") @app.route("/<path:path>")
def path(path): def path(path):
if not isCurl(request): if not isCLI(request):
return render_page(path, handshake_scripts=getHandshakeScript(request.host)) return render_page(path, handshake_scripts=getHandshakeScript(request.host))
# Convert md to html # Convert md to html
@@ -152,7 +152,7 @@ def path(path):
}), 200 }), 200
@blog_bp.route("/<path:path>.md") @app.route("/<path:path>.md")
def path_md(path): def path_md(path):
if not os.path.exists(f"data/blog/{path}.md"): if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404 return render_template("404.html"), 404

View File

@@ -1,10 +1,13 @@
from flask import Blueprint, render_template, make_response, request, jsonify from flask import Blueprint, render_template, make_response, request, jsonify
import datetime import datetime
import os import os
from tools import getHandshakeScript from tools import getHandshakeScript, error_response, isCLI
from curl import get_header
from bs4 import BeautifulSoup
import re
# Create blueprint # Create blueprint
now_bp = Blueprint('now', __name__) app = Blueprint('now', __name__, url_prefix='/now')
def list_page_files(): def list_page_files():
@@ -44,27 +47,100 @@ def render(date, handshake_scripts=None):
date = date.removesuffix(".html") date = date.removesuffix(".html")
if date not in list_dates(): if date not in list_dates():
return render_template("404.html"), 404 return error_response(request)
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d") date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y") date_formatted = date_formatted.strftime("%A, %B %d, %Y")
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts) return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
def render_curl(date=None):
# If the date is not available, render the latest page
if date is None:
date = get_latest_date()
@now_bp.route("/") # Remove .html if present
date = date.removesuffix(".html")
if date not in list_dates():
return error_response(request)
# Format the date nicely
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
# Load HTML
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
raw_html = f.read().replace("{{ date }}", date_formatted)
soup = BeautifulSoup(raw_html, 'html.parser')
posts = []
# Find divs matching your pattern
divs = soup.find_all("div", style=re.compile(r"max-width:\s*700px", re.IGNORECASE))
if not divs:
return error_response(request, message="No content found for CLI rendering.")
for div in divs:
# header could be h1/h2/h3 inside the div
header_tag = div.find(["h1", "h2", "h3"]) # type: ignore
# content is usually one or more <p> tags inside the div
p_tags = div.find_all("p") # type: ignore
if header_tag and p_tags:
header_text = header_tag.get_text(strip=True) # type: ignore
content_lines = []
for p in p_tags:
# Extract text
text = p.get_text(strip=False)
# Extract any <a> links in the paragraph
links = [a.get("href") for a in p.find_all("a", href=True)] # type: ignore
if links:
text += "\nLinks: " + ", ".join(links) # type: ignore
content_lines.append(text)
content_text = "\n\n".join(content_lines)
posts.append({"header": header_text, "content": content_text})
# Build final response
response = ""
for post in posts:
response += f"{post['header']}\n\n{post['content']}\n\n"
return render_template("now.ascii", date=date_formatted, content=response, header=get_header())
@app.route("/", strict_slashes=False)
def index(): def index():
if isCLI(request):
return render_curl()
return render_latest(handshake_scripts=getHandshakeScript(request.host)) return render_latest(handshake_scripts=getHandshakeScript(request.host))
@now_bp.route("/<path:path>") @app.route("/<path:path>")
def path(path): def path(path):
if isCLI(request):
return render_curl(path)
return render(path, handshake_scripts=getHandshakeScript(request.host)) return render(path, handshake_scripts=getHandshakeScript(request.host))
@now_bp.route("/old") @app.route("/old", strict_slashes=False)
@now_bp.route("/old/")
def old(): def old():
now_dates = list_dates()[1:] now_dates = list_dates()[1:]
if isCLI(request):
response = ""
for date in now_dates:
link = date
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
date_fmt = date_fmt.strftime("%A, %B %d, %Y")
response += f"{date_fmt} - /now/{link}\n"
return render_template("now.ascii", date="Old Now Pages", content=response, header=get_header())
html = '<ul class="list-group">' 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_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>'
@@ -80,9 +156,9 @@ def old():
) )
@now_bp.route("/now.rss") @app.route("/now.rss")
@now_bp.route("/now.xml") @app.route("/now.xml")
@now_bp.route("/rss.xml") @app.route("/rss.xml")
def rss(): def rss():
host = "https://" + request.host host = "https://" + request.host
if ":" in request.host: if ":" in request.host:
@@ -99,7 +175,7 @@ def rss():
return make_response(rss, 200, {"Content-Type": "application/rss+xml"}) return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
@now_bp.route("/now.json") @app.route("/now.json")
def json(): def json():
now_pages = list_page_files() now_pages = list_page_files()
host = "https://" + request.host host = "https://" + request.host

View File

@@ -2,9 +2,9 @@ from flask import Blueprint, make_response, request
from tools import error_response from tools import error_response
import requests import requests
podcast_bp = Blueprint('podcast', __name__) app = Blueprint('podcast', __name__)
@podcast_bp.route("/ID1") @app.route("/ID1")
def index(): def index():
# Proxy to ID1 url # Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1") req = requests.get("https://podcasts.c.woodburn.au/ID1")
@@ -16,7 +16,7 @@ def index():
) )
@podcast_bp.route("/ID1/") @app.route("/ID1/")
def contents(): def contents():
# Proxy to ID1 url # Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/") req = requests.get("https://podcasts.c.woodburn.au/ID1/")
@@ -27,7 +27,7 @@ def contents():
) )
@podcast_bp.route("/ID1/<path:path>") @app.route("/ID1/<path:path>")
def path(path): def path(path):
# Proxy to ID1 url # Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path) req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
@@ -38,7 +38,7 @@ def path(path):
) )
@podcast_bp.route("/ID1.xml") @app.route("/ID1.xml")
def xml(): def xml():
# Proxy to ID1 url # Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml") req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
@@ -49,7 +49,7 @@ def xml():
) )
@podcast_bp.route("/podsync.opml") @app.route("/podsync.opml")
def podsync(): def podsync():
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml") req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
if req.status_code != 200: if req.status_code != 200:

View File

@@ -9,7 +9,7 @@ import binascii
import base64 import base64
import os import os
sol_bp = Blueprint('sol', __name__) app = Blueprint('sol', __name__)
SOLANA_HEADERS = { SOLANA_HEADERS = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -55,7 +55,7 @@ def get_solana_address() -> str:
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.") 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) return str(SOLANA_ADDRESS)
@sol_bp.route("/donate", methods=["GET", "OPTIONS"]) @app.route("/donate", methods=["GET", "OPTIONS"])
def sol_donate(): def sol_donate():
data = { data = {
"icon": "https://nathan.woodburn.au/assets/img/profile.png", "icon": "https://nathan.woodburn.au/assets/img/profile.png",
@@ -90,7 +90,7 @@ def sol_donate():
return response return response
@sol_bp.route("/donate/<amount>") @app.route("/donate/<amount>")
def sol_donate_amount(amount): def sol_donate_amount(amount):
data = { data = {
"icon": "https://nathan.woodburn.au/assets/img/profile.png", "icon": "https://nathan.woodburn.au/assets/img/profile.png",
@@ -101,7 +101,7 @@ def sol_donate_amount(amount):
return jsonify(data), 200, SOLANA_HEADERS return jsonify(data), 200, SOLANA_HEADERS
@sol_bp.route("/donate/<amount>", methods=["POST"]) @app.route("/donate/<amount>", methods=["POST"])
def sol_donate_post(amount): def sol_donate_post(amount):
if not request.json: if not request.json:
@@ -122,4 +122,10 @@ def sol_donate_post(amount):
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
transaction = create_transaction(sender, amount) transaction = create_transaction(sender, amount)
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
@app.route("/actions.json")
def sol_actions():
return jsonify(
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
)

View File

@@ -5,7 +5,7 @@ import requests
import time import time
import base64 import base64
spotify_bp = Blueprint('spotify', __name__) app = Blueprint('spotify', __name__, url_prefix='/spotify')
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
@@ -48,7 +48,7 @@ def refresh_access_token():
TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600) TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600)
return ACCESS_TOKEN return ACCESS_TOKEN
@spotify_bp.route("/login") @app.route("/login")
def login(): def login():
auth_query = ( auth_query = (
f"{SPOTIFY_AUTH_URL}?response_type=code&client_id={CLIENT_ID}" f"{SPOTIFY_AUTH_URL}?response_type=code&client_id={CLIENT_ID}"
@@ -56,7 +56,7 @@ def login():
) )
return redirect(auth_query) return redirect(auth_query)
@spotify_bp.route("/callback") @app.route("/callback")
def callback(): def callback():
code = request.args.get("code") code = request.args.get("code")
if not code: if not code:
@@ -89,34 +89,11 @@ def callback():
print("Refresh Token:", REFRESH_TOKEN) print("Refresh Token:", REFRESH_TOKEN)
return redirect(url_for("spotify.currently_playing")) return redirect(url_for("spotify.currently_playing"))
@spotify_bp.route("/") @app.route("/", strict_slashes=False)
@spotify_bp.route("/currently-playing") @app.route("/playing")
def currently_playing(): def currently_playing():
"""Public endpoint showing your current track.""" """Public endpoint showing your current track."""
token = refresh_access_token() track = get_spotify_track()
if not token:
return json_response(request, {"error": "Failed to refresh access token"}, 500)
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
if response.status_code == 204:
return json_response(request, {"message": "Nothing is currently playing."}, 200)
elif response.status_code != 200:
return json_response(request, {"error": "Spotify API error", "status": response.status_code}, response.status_code)
data = response.json()
if not data.get("item"):
return json_response(request, {"message": "Nothing is currently playing."}, 200)
track = {
"song_name": data["item"]["name"],
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
"album_name": data["item"]["album"]["name"],
"album_art": data["item"]["album"]["images"][0]["url"],
"is_playing": data["is_playing"]
}
return json_response(request, {"spotify":track}, 200) return json_response(request, {"spotify":track}, 200)
def get_spotify_track(): def get_spotify_track():
@@ -137,12 +114,13 @@ def get_spotify_track():
if not data.get("item"): if not data.get("item"):
return {"error": "Nothing is currently playing."} return {"error": "Nothing is currently playing."}
track = { track = {
"song_name": data["item"]["name"], "song_name": data["item"]["name"],
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]), "artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
"album_name": data["item"]["album"]["name"], "album_name": data["item"]["album"]["name"],
"album_art": data["item"]["album"]["images"][0]["url"], "album_art": data["item"]["album"]["images"][0]["url"],
"is_playing": data["is_playing"] "is_playing": data["is_playing"],
"progress_ms": data.get("progress_ms",0),
"duration_ms": data["item"].get("duration_ms",1)
} }
return track return track

View File

@@ -1,9 +1,9 @@
from flask import Blueprint, request from flask import Blueprint, request
from tools import json_response from tools import json_response
template_bp = Blueprint('template', __name__) app = Blueprint('template', __name__)
@template_bp.route("/") @app.route("/", strict_slashes=False)
def index(): def index():
return json_response(request, "Success", 200) return json_response(request, "Success", 200)

View File

@@ -1,15 +1,16 @@
from flask import Blueprint, render_template, make_response, request, jsonify, send_from_directory, redirect from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
from tools import error_response
import os import os
wk_bp = Blueprint('well-known', __name__) app = Blueprint('well-known', __name__, url_prefix='/.well-known')
@wk_bp.route("/<path:path>") @app.route("/<path:path>")
def index(path): def index(path):
return send_from_directory(".well-known", path) return send_from_directory(".well-known", path)
@wk_bp.route("/wallets/<path:path>") @app.route("/wallets/<path:path>")
def wallets(path): def wallets(path):
if path[0] == "." and 'proof' not in path: if path[0] == "." and 'proof' not in path:
return send_from_directory( return send_from_directory(
@@ -25,10 +26,10 @@ def wallets(path):
if os.path.isfile(".well-known/wallets/" + path.upper()): if os.path.isfile(".well-known/wallets/" + path.upper()):
return redirect("/.well-known/wallets/" + path.upper(), code=302) return redirect("/.well-known/wallets/" + path.upper(), code=302)
return render_template("404.html"), 404 return error_response(request)
@wk_bp.route("/nostr.json") @app.route("/nostr.json")
def nostr(): def nostr():
# Get name parameter # Get name parameter
name = request.args.get("name") name = request.args.get("name")
@@ -50,7 +51,7 @@ def nostr():
) )
@wk_bp.route("/xrp-ledger.toml") @app.route("/xrp-ledger.toml")
def xrp(): def xrp():
# Create a response with the xrp-ledger.toml file # Create a response with the xrp-ledger.toml file
with open(".well-known/xrp-ledger.toml") as file: with open(".well-known/xrp-ledger.toml") as file:

10
curl.py
View File

@@ -1,5 +1,5 @@
from flask import render_template from flask import render_template
from tools import error_response, getAddress, get_tools_data, getClientIP from tools import getAddress, get_tools_data, getClientIP
import os import os
from functools import lru_cache from functools import lru_cache
import requests import requests
@@ -115,7 +115,6 @@ def curl_response(request):
tools = get_tools_data() tools = get_tools_data()
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'} return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if os.path.exists(f"templates/{path}.ascii"): if os.path.exists(f"templates/{path}.ascii"):
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'} return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
@@ -123,4 +122,9 @@ def curl_response(request):
if os.path.exists(f"templates/{path}.html"): if os.path.exists(f"templates/{path}.html"):
return render_template(f"{path}.html") return render_template(f"{path}.html")
return error_response(request) # Return curl error page
error = {
"code": 404,
"message": "The requested resource was not found on this server."
}
return render_template("error.ascii",header=get_header(),error=error), 404, {'Content-Type': 'text/plain; charset=utf-8'}

View File

@@ -23,6 +23,12 @@
"url": "https://code.visualstudio.com/", "url": "https://code.visualstudio.com/",
"description": "Source-code editor developed by Microsoft" "description": "Source-code editor developed by Microsoft"
}, },
{
"name": "Vesktop",
"type": "Desktop Applications",
"url": "https://vesktop.dev/",
"description": "Vesktop is a customizable and privacy friendly Discord desktop app!"
},
{ {
"name": "Zellij", "name": "Zellij",
"type": "Terminal Tools", "type": "Terminal Tools",

View File

@@ -19,27 +19,17 @@ from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
from ansi2html import Ansi2HTMLConverter from ansi2html import Ansi2HTMLConverter
from PIL import Image from PIL import Image
# Import blueprints # Import blueprints
from blueprints.now import now_bp from blueprints import now, blog, wellknown, api, podcast, acme, spotify
from blueprints.blog import blog_bp from tools import isCLI, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getHandshakeScript, get_tools_data
from blueprints.wellknown import wk_bp
from blueprints.api import api_bp
from blueprints.podcast import podcast_bp
from blueprints.acme import acme_bp
from blueprints.spotify import spotify_bp
from tools import isCurl, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getHandshakeScript, get_tools_data
from curl import curl_response from curl import curl_response
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# Register blueprints # Register blueprints
app.register_blueprint(now_bp, url_prefix='/now') for module in [now, blog, wellknown, api, podcast, acme, spotify]:
app.register_blueprint(blog_bp, url_prefix='/blog') app.register_blueprint(module.app)
app.register_blueprint(wk_bp, url_prefix='/.well-known')
app.register_blueprint(api_bp, url_prefix='/api/v1')
app.register_blueprint(podcast_bp)
app.register_blueprint(acme_bp)
app.register_blueprint(spotify_bp, url_prefix='/spotify')
dotenv.load_dotenv() dotenv.load_dotenv()
@@ -54,7 +44,8 @@ RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
RESTRICTED_ROUTES = ["ascii"] RESTRICTED_ROUTES = ["ascii"]
REDIRECT_ROUTES = { REDIRECT_ROUTES = {
"contact": "/#contact" "contact": "/#contact",
"old": "/now/old"
} }
DOWNLOAD_ROUTES = { DOWNLOAD_ROUTES = {
"pgp": "data/nathanwoodburn.asc" "pgp": "data/nathanwoodburn.asc"
@@ -188,7 +179,6 @@ def serviceWorker():
# region Misc routes # region Misc routes
@app.route("/meet") @app.route("/meet")
@app.route("/meeting") @app.route("/meeting")
@app.route("/appointment") @app.route("/appointment")
@@ -212,13 +202,6 @@ def api_legacy(function):
return redirect(f"/api/v1/{function}", code=301) return redirect(f"/api/v1/{function}", code=301)
return error_response(request, message="404 Not Found", code=404) return error_response(request, message="404 Not Found", code=404)
@app.route("/actions.json")
def sol_actions():
return jsonify(
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
)
# endregion # endregion
# region Main routes # region Main routes
@@ -244,7 +227,7 @@ def index():
# Always load if load is in the query string # Always load if load is in the query string
if request.args.get("load"): if request.args.get("load"):
loaded = False loaded = False
if isCurl(request): if isCLI(request):
return curl_response(request) return curl_response(request)
if not loaded and not isCrawler(request): if not loaded and not isCrawler(request):
@@ -397,7 +380,7 @@ def index():
# region Donate # region Donate
@app.route("/donate") @app.route("/donate")
def donate(): def donate():
if isCurl(request): if isCLI(request):
return curl_response(request) return curl_response(request)
coinList = os.listdir(".well-known/wallets") coinList = os.listdir(".well-known/wallets")
@@ -687,7 +670,7 @@ def resume_pdf():
@app.route("/tools") @app.route("/tools")
def tools(): def tools():
if isCurl(request): if isCLI(request):
return curl_response(request) return curl_response(request)
return render_template("tools.html", tools=get_tools_data()) return render_template("tools.html", tools=get_tools_data())
@@ -704,7 +687,7 @@ def catch_all(path: str):
return error_response(request, message="Restricted route", code=403) return error_response(request, message="Restricted route", code=403)
# If curl request, return curl response # If curl request, return curl response
if isCurl(request): if isCLI(request):
return curl_response(request) return curl_response(request)
if path in REDIRECT_ROUTES: if path in REDIRECT_ROUTES:

9
templates/error.ascii Normal file
View File

@@ -0,0 +1,9 @@
{{header}}
───────────────────────────────────────────────
 ERROR: {{ error.code }} 
────────────
{{ error.message }}
If you believe this is an error, please contact me via my socials listed at /contact

View File

@@ -9,7 +9,7 @@ Contact [/contact]
Projects [/projects] Projects [/projects]
Tools [/tools] Tools [/tools]
Donate [/donate] Donate [/donate]
API [/api/v1/] API [/api/v1]
─────────────────────────────────────────────── ───────────────────────────────────────────────
 ABOUT ME   ABOUT ME 

View File

@@ -294,7 +294,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18ZM11 6C11 5.44772 10.5523 5 10 5C9.44771 5 9 5.44772 9 6V10C9 10.2652 9.10536 10.5196 9.29289 10.7071L12.1213 13.5355C12.5118 13.9261 13.145 13.9261 13.5355 13.5355C13.9261 13.145 13.9261 12.5118 13.5355 12.1213L11 9.58579V6Z" fill="currentColor"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18ZM11 6C11 5.44772 10.5523 5 10 5C9.44771 5 9 5.44772 9 6V10C9 10.2652 9.10536 10.5196 9.29289 10.7071L12.1213 13.5355C12.5118 13.9261 13.145 13.9261 13.5355 13.5355C13.9261 13.145 13.9261 12.5118 13.5355 12.1213L11 9.58579V6Z" fill="currentColor"></path>
</svg><span style="margin-left: 10px;font-family: 'Anonymous Pro', monospace;">{{time|safe}}</span></div><!-- Pop-out button for mobile --> </svg><span style="margin-left: 10px;font-family: 'Anonymous Pro', monospace;">{{time|safe}}</span></div><!-- Pop-out button for mobile -->
<button id="spotify-toggle" style=" <button id="spotify-toggle" style="
display: none; display: block;
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
@@ -305,6 +305,8 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
background: none; background: none;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
transition: transform 0.5s ease;
transform: translateX(200%); /* start hidden off-screen *
"> ">
<img src="/assets/img/external/spotify.png" alt="Spotify" style=" <img src="/assets/img/external/spotify.png" alt="Spotify" style="
width: 100%; width: 100%;
@@ -360,6 +362,21 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
"></div> "></div>
<!-- Progress Bar -->
<div style="
margin-top: 6px;
height: 4px;
background: #333;
border-radius: 2px;
overflow: hidden;
">
<div id="spotify-progress" style="
width: 0%;
height: 100%;
background: #1DB954;
transition: width 1s linear;
"></div>
</div>
</div> </div>
</div> </div>
@@ -374,16 +391,17 @@ function isMobile() {
function updateVisibility() { function updateVisibility() {
if(isMobile()){ if(isMobile()){
widget.style.transform = 'translateX(120%)'; // hidden off-screen widget.style.transform = 'translateX(120%)'; // hidden off-screen
toggleBtn.style.display = 'block'; toggleBtn.style.transform = 'translateX(0)'; // visible
} else { } else {
widget.style.transform = 'translateX(0)'; // visible widget.style.transform = 'translateX(0)'; // visible
toggleBtn.style.display = 'none'; toggleBtn.style.transform = 'translateX(200%)'; // hidden off-screen
} }
} }
// Toggle widget slide in/out on mobile // Toggle widget slide in/out on mobile
toggleBtn.addEventListener('click', (e) => { toggleBtn.addEventListener('click', (e) => {
widget.style.transform = 'translateX(0)'; // slide in widget.style.transform = 'translateX(0)'; // slide in
toggleBtn.style.transform = 'translateX(200%)'; // hide button
e.stopPropagation(); e.stopPropagation();
}); });
@@ -392,6 +410,7 @@ document.addEventListener('click', (e) => {
if(isMobile()){ if(isMobile()){
if(!widget.contains(e.target) && e.target !== toggleBtn){ if(!widget.contains(e.target) && e.target !== toggleBtn){
widget.style.transform = 'translateX(120%)'; // slide out widget.style.transform = 'translateX(120%)'; // slide out
toggleBtn.style.transform = 'translateX(0)'; // show button
} }
} }
}); });
@@ -401,10 +420,19 @@ widget.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
// Variable to track progress bar animation
let progressInterval = null;
let progressSpeed = 0;
let lastUpdateTime = Date.now();
let currentProgress = 0;
let targetProgress = 0;
let trackDuration = 0;
let currentTrackId = null;
// --- Spotify fetch --- // --- Spotify fetch ---
async function updateSpotifyWidget() { async function updateSpotifyWidget() {
try { try {
const res = await fetch('/spotify/'); const res = await fetch('/api/v1/playing');
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
@@ -419,6 +447,11 @@ async function updateSpotifyWidget() {
document.getElementById('spotify-song').textContent = 'Not Playing'; document.getElementById('spotify-song').textContent = 'Not Playing';
document.getElementById('spotify-artist').textContent = ''; document.getElementById('spotify-artist').textContent = '';
document.getElementById('spotify-album').textContent = ''; document.getElementById('spotify-album').textContent = '';
document.getElementById('spotify-progress').style.width = '0%';
clearInterval(progressInterval);
progressInterval = null;
currentProgress = 0;
currentTrackId = null;
return; return;
} }
@@ -428,14 +461,53 @@ async function updateSpotifyWidget() {
if (!document.getElementById('spotify-song').textContent) { if (!document.getElementById('spotify-song').textContent) {
firstLoad = true; firstLoad = true;
} }
// Check if track has changed (new song started)
const trackId = track.song_name + track.artist; // Simple track identifier
const isNewTrack = currentTrackId !== null && currentTrackId !== trackId;
if (isNewTrack) {
// Reset progress bar instantly for new track
currentProgress = 0;
document.getElementById('spotify-progress').style.transition = 'none';
document.getElementById('spotify-progress').style.width = '0%';
// Force reflow to apply the instant reset
document.getElementById('spotify-progress').offsetHeight;
// Re-enable transition
document.getElementById('spotify-progress').style.transition = 'width 0.1s linear';
}
currentTrackId = trackId;
document.getElementById('spotify-album-art').src = track.album_art; document.getElementById('spotify-album-art').src = track.album_art;
document.getElementById('spotify-song').textContent = track.song_name; document.getElementById('spotify-song').textContent = track.song_name;
document.getElementById('spotify-artist').textContent = track.artist; document.getElementById('spotify-artist').textContent = track.artist;
document.getElementById('spotify-album').textContent = track.album_name; document.getElementById('spotify-album').textContent = track.album_name;
// Update progress bar
if (track.is_playing) {
currentProgress = (track.progress_ms / track.duration_ms) * 100;
trackDuration = track.duration_ms;
lastUpdateTime = Date.now();
document.getElementById('spotify-progress').style.width = currentProgress + '%';
// Clear existing interval
if (progressInterval) {
clearInterval(progressInterval);
}
// Start interval to animate progress bar
progressInterval = setInterval(animateProgressBar, 100);
} else {
document.getElementById('spotify-progress').style.width = currentProgress + '%';
clearInterval(progressInterval);
progressInterval = null;
}
// If first load and desktop, slide in the widget
if (firstLoad) { if (firstLoad) {
widget.style.transform = 'translateX(0)'; // slide in on first load updateVisibility();
} }
} catch (err) { } catch (err) {
@@ -443,6 +515,30 @@ async function updateSpotifyWidget() {
} }
} }
// Animate progress bar
function animateProgressBar() {
if (trackDuration === 0) return;
const now = Date.now();
const elapsed = now - lastUpdateTime;
lastUpdateTime = now;
// Calculate progress increment based on elapsed time
const progressIncrement = (elapsed / trackDuration) * 100;
currentProgress += progressIncrement;
if (currentProgress >= 100) {
currentProgress = 100;
document.getElementById('spotify-progress').style.width = '100%';
clearInterval(progressInterval);
progressInterval = null;
// Refresh API when progress reaches 100%
setTimeout(updateSpotifyWidget, 500);
} else {
document.getElementById('spotify-progress').style.width = currentProgress + '%';
}
}
// Wait for Spotify API to have responded before initial display // Wait for Spotify API to have responded before initial display
updateSpotifyWidget(); updateSpotifyWidget();

6
templates/now.ascii Normal file
View File

@@ -0,0 +1,6 @@
{{header}}
───────────────────────────────────────────────
 Now {{ date }} 
────────────
{{content | safe}}

View File

@@ -6,10 +6,15 @@ GET http://127.0.0.1:5000/api/v1/ip
HTTP 200 HTTP 200
[Asserts] [Asserts]
jsonpath "$.ip" == "127.0.0.1" jsonpath "$.ip" == "127.0.0.1"
GET http://127.0.0.1:5000/api/v1/time GET http://127.0.0.1:5000/api/v1/time
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/api/v1/timezone GET http://127.0.0.1:5000/api/v1/timezone
HTTP 200 HTTP 200
[Asserts]
jsonpath "$.timezone" >= 10
jsonpath "$.timezone" <= 12
GET http://127.0.0.1:5000/api/v1/message GET http://127.0.0.1:5000/api/v1/message
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/api/v1/project GET http://127.0.0.1:5000/api/v1/project
@@ -17,4 +22,7 @@ HTTP 200
GET http://127.0.0.1:5000/api/v1/tools GET http://127.0.0.1:5000/api/v1/tools
HTTP 200 HTTP 200
[Asserts] [Asserts]
jsonpath "$.tools" count > 5 jsonpath "$.tools" count > 5
GET http://127.0.0.1:5000/api/v1/playing
HTTP 200

View File

@@ -27,6 +27,14 @@ CRAWLERS = [
"Twitterbot" "Twitterbot"
] ]
CLI_AGENTS = [
"curl",
"hurl",
"xh",
"Posting",
"HTTPie"
]
def getClientIP(request: Request) -> str: def getClientIP(request: Request) -> str:
""" """
@@ -75,7 +83,7 @@ def getGitCommit() -> str:
return "failed to get version" return "failed to get version"
def isCurl(request: Request) -> bool: def isCLI(request: Request) -> bool:
""" """
Check if the request is from curl or hurl. Check if the request is from curl or hurl.
@@ -87,7 +95,7 @@ def isCurl(request: Request) -> bool:
""" """
if request.headers and request.headers.get("User-Agent"): if request.headers and request.headers.get("User-Agent"):
user_agent = request.headers.get("User-Agent", "") user_agent = request.headers.get("User-Agent", "")
return "curl" in user_agent or "hurl" in user_agent return any(agent in user_agent for agent in CLI_AGENTS)
return False return False
@@ -202,7 +210,6 @@ def json_response(request: Request, message: Union[str, Dict] = "404 Not Found",
"ip": getClientIP(request), "ip": getClientIP(request),
}), code }), code
def error_response( def error_response(
request: Request, request: Request,
message: str = "404 Not Found", message: str = "404 Not Found",
@@ -221,7 +228,7 @@ def error_response(
Returns: Returns:
Union[Tuple[Dict, int], object]: The JSON or HTML response Union[Tuple[Dict, int], object]: The JSON or HTML response
""" """
if force_json or isCurl(request): if force_json or isCLI(request):
return json_response(request, message, code) return json_response(request, message, code)
# Check if <error code>.html exists in templates # Check if <error code>.html exists in templates